├── .github └── FUNDING.yml ├── README.md ├── _user_stories ├── 00_overview.md ├── 01.md ├── 02.md ├── 03.md ├── 04.md ├── 05.md ├── 06.md ├── 07.md ├── 08.md ├── 09.md ├── 10.md ├── 11.md ├── 12.md ├── 13.md ├── 14.md ├── 15.md ├── 16.md └── 17.md ├── api ├── .eslintrc.js ├── .gitignore ├── .prettierrc ├── .vscode │ └── launch.json ├── Dockerfile ├── README.md ├── nest-cli.json ├── package-lock.json ├── package.json ├── src │ ├── app.controller.spec.ts │ ├── app.controller.ts │ ├── app.module.ts │ ├── app.service.ts │ ├── auth │ │ ├── auth.module.ts │ │ ├── guards │ │ │ └── jwt.guard.ts │ │ ├── service │ │ │ ├── auth.service.spec.ts │ │ │ └── auth.service.ts │ │ └── strategies │ │ │ └── jwt.strategy.ts │ ├── chat │ │ ├── chat.module.ts │ │ ├── gateway │ │ │ ├── chat.gateway.spec.ts │ │ │ └── chat.gateway.ts │ │ ├── model │ │ │ ├── connected-user │ │ │ │ ├── connected-user.entity.ts │ │ │ │ └── connected-user.interface.ts │ │ │ ├── joined-room │ │ │ │ ├── joined-room.entity.ts │ │ │ │ └── joined-room.interface.ts │ │ │ ├── message │ │ │ │ ├── message.entity.ts │ │ │ │ └── message.interface.ts │ │ │ ├── page.interface.ts │ │ │ └── room │ │ │ │ ├── room.entity.ts │ │ │ │ └── room.interface.ts │ │ └── service │ │ │ ├── connected-user │ │ │ ├── connected-user.service.spec.ts │ │ │ └── connected-user.service.ts │ │ │ ├── joined-room │ │ │ ├── joined-room.service.spec.ts │ │ │ └── joined-room.service.ts │ │ │ ├── message │ │ │ ├── message.service.spec.ts │ │ │ └── message.service.ts │ │ │ └── room-service │ │ │ ├── room.service.spec.ts │ │ │ └── room.service.ts │ ├── main.ts │ ├── middleware │ │ └── auth.middleware.ts │ └── user │ │ ├── controller │ │ ├── user.controller.spec.ts │ │ └── user.controller.ts │ │ ├── model │ │ ├── dto │ │ │ ├── create-user.dto.ts │ │ │ └── login-user.dto.ts │ │ ├── login-response.interface.ts │ │ ├── user.entity.ts │ │ └── user.interface.ts │ │ ├── service │ │ ├── user-helper │ │ │ ├── user-helper.service.spec.ts │ │ │ └── user-helper.service.ts │ │ └── user-service │ │ │ ├── user.service.spec.ts │ │ │ └── user.service.ts │ │ └── user.module.ts ├── test │ ├── app.e2e-spec.ts │ └── jest-e2e.json ├── tsconfig.build.json └── tsconfig.json ├── docker-compose.yml ├── frontend ├── .browserslistrc ├── .editorconfig ├── .gitignore ├── Dockerfile ├── README.md ├── angular.json ├── karma.conf.js ├── package.json ├── src │ ├── app │ │ ├── app-routing.module.ts │ │ ├── app.component.html │ │ ├── app.component.scss │ │ ├── app.component.spec.ts │ │ ├── app.component.ts │ │ ├── app.module.ts │ │ ├── guards │ │ │ ├── auth.guard.spec.ts │ │ │ └── auth.guard.ts │ │ ├── model │ │ │ ├── login-response.interface.ts │ │ │ ├── message.interface.ts │ │ │ ├── meta.interface.ts │ │ │ ├── room.interface.ts │ │ │ └── user.interface.ts │ │ ├── private │ │ │ ├── components │ │ │ │ ├── chat-message │ │ │ │ │ ├── chat-message.component.html │ │ │ │ │ ├── chat-message.component.scss │ │ │ │ │ ├── chat-message.component.spec.ts │ │ │ │ │ └── chat-message.component.ts │ │ │ │ ├── chat-room │ │ │ │ │ ├── chat-room.component.html │ │ │ │ │ ├── chat-room.component.scss │ │ │ │ │ ├── chat-room.component.spec.ts │ │ │ │ │ └── chat-room.component.ts │ │ │ │ ├── create-room │ │ │ │ │ ├── create-room.component.html │ │ │ │ │ ├── create-room.component.scss │ │ │ │ │ ├── create-room.component.spec.ts │ │ │ │ │ └── create-room.component.ts │ │ │ │ ├── dashboard │ │ │ │ │ ├── dashboard.component.html │ │ │ │ │ ├── dashboard.component.scss │ │ │ │ │ ├── dashboard.component.spec.ts │ │ │ │ │ └── dashboard.component.ts │ │ │ │ └── select-users │ │ │ │ │ ├── select-users.component.html │ │ │ │ │ ├── select-users.component.scss │ │ │ │ │ ├── select-users.component.spec.ts │ │ │ │ │ └── select-users.component.ts │ │ │ ├── private-routing.module.ts │ │ │ ├── private.module.ts │ │ │ ├── services │ │ │ │ └── chat-service │ │ │ │ │ ├── chat.service.spec.ts │ │ │ │ │ └── chat.service.ts │ │ │ └── sockets │ │ │ │ └── custom-socket.ts │ │ ├── public │ │ │ ├── _helpers │ │ │ │ └── custom-validators.ts │ │ │ ├── components │ │ │ │ ├── login │ │ │ │ │ ├── login.component.html │ │ │ │ │ ├── login.component.scss │ │ │ │ │ ├── login.component.spec.ts │ │ │ │ │ └── login.component.ts │ │ │ │ └── register │ │ │ │ │ ├── register.component.html │ │ │ │ │ ├── register.component.scss │ │ │ │ │ ├── register.component.spec.ts │ │ │ │ │ └── register.component.ts │ │ │ ├── public-routing.module.ts │ │ │ ├── public.module.ts │ │ │ └── services │ │ │ │ ├── auth-service │ │ │ │ ├── auth.service.spec.ts │ │ │ │ └── auth.service.ts │ │ │ │ └── user-service │ │ │ │ ├── user.service.spec.ts │ │ │ │ └── user.service.ts │ │ └── services │ │ │ └── test-service │ │ │ ├── test.service.spec.ts │ │ │ └── test.service.ts │ ├── assets │ │ └── .gitkeep │ ├── environments │ │ ├── environment.prod.ts │ │ └── environment.ts │ ├── favicon.ico │ ├── index.html │ ├── main.ts │ ├── polyfills.ts │ ├── proxy.conf.json │ ├── styles.scss │ └── test.ts ├── tsconfig.app.json ├── tsconfig.json └── tsconfig.spec.json └── todos.md /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: ThomasOliver545 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # real-time-chat-nestjs-angular 2 | 3 | Link to Github: https://github.com/ThomasOliver545/real-time-chat-nestjs-angular 4 | Link to all Commits on Github: https://github.com/ThomasOliver545/real-time-chat-nestjs-angular/commits 5 | Link to Twitter: https://twitter.com/Thomas_OliverK 6 | 7 | Youtube Playlists: 8 | Playlist "Realtime/Live Chat App": https://youtube.com/playlist?list=PLVfq1luIZbSkICzoA8EuvTskPEROS68i9 9 | Playlist "2022": https://www.youtube.com/playlist?list=PLVfq1luIZbSmYCedSiFudCImiyCK1c2Qr 10 | Playlist "Realtime/Live Chat App - Released Versions": https://youtube.com/playlist?list=PLVfq1luIZbSmz1-qgUdpV0xjjtzhnBHFl 11 | 12 | Since i am doing this in my freetime Support via "Github Sponsor", "Github Follow", "Youtube Follow" etc. would be great. 13 | 14 | ## Good to know 15 | - Api build with NestJS 8, because v8 has Support for the latest Socket.io Release v4 16 | - Angular used in v12 & also Angular Material 17 | - Start Command `docker-compose up` 18 | - Sometimes the package-lock.json can cause issues, then just delete it and let docker do everything 19 | - The first Video is an Overview for the first Part of the series [Videos 1-17], there will also be more features & bugfixes added in the future 20 | (then with like another overview video) 21 | - All Commits that are matched with a video are prefixed with the Video Number, where they were made, e.g. `Video-17: ...` 22 | - In the folder _user_stories you can have a look at all the implemented user stories so far, in 00_overview you have a list of all videos 23 | - Sometimes when you follow the videos you might need to dump the images/containers and rebuild, look below to the tipps & tricks 24 | - in the todo.md file are some ideas that could be implemented in the future 25 | 26 | ## Overview of the Series 27 | This Series is about implementing a Realtime Chat with Websockets (here we used Socket.io v4). 28 | The Main features are: 29 | - Register a new User 30 | - Login with a user and get a valid jwt Token for Auth (API & Websocket) 31 | - Create a Chatroom and add other users by their username 32 | - Join one of your chatrooms and see the latest messages 33 | - Add a message to the chatroom, this will be emmitted immediately to all other joined Users for this Room that are currently online 34 | 35 | The NestJS API is build with NestJS 8, because v8 has support for the latest socket.io Release v4. 36 | 37 | ## How to run the Project 38 | Command: 39 | `docker-compose up` 40 | 41 | ### Tipps & Tricks 42 | If you follow the videos you something have to delete the images & containers and then start everything again. 43 | 44 | Command to remove all images: 45 | `docker rmi -f $(docker images -a -q)` 46 | 47 | Command to remove all containers: 48 | `docker rm -vf $(docker ps -a -q)` 49 | 50 | ### Docker commands 51 | 52 | Command to start: 53 | `docker-compose up` 54 | 55 | Command to build: 56 | `docker-compose build` 57 | 58 | 59 | Command to remove all images: 60 | `docker rmi -f $(docker images -a -q)` 61 | 62 | Command to remove all containers: 63 | `docker rm -vf $(docker ps -a -q)` -------------------------------------------------------------------------------- /_user_stories/00_overview.md: -------------------------------------------------------------------------------- 1 | 2 | 1. Video (NestJS & Angular Set Up) 3 | Link: https://youtu.be/IievSCity8c 4 | 5 | 2. Video (NestJS Login&Register Endpoints) 6 | Link: https://youtu.be/Hi6V6RbTmNM 7 | 8 | 3. Video (NestJS, Auth Module Jwt & Refactoring) 9 | Link: https://youtu.be/ydibF89YGV8 10 | 11 | 4. Video (Angular: User Registration with Reactive Forms) 12 | Link: https://youtu.be/t6BpRxV4b0M 13 | 14 | 5. Video (Angular: User Login with Reactive Forms and JWT saving) 15 | Link: https://youtu.be/XTGbg-9yN2k 16 | 17 | 6. Video (NestJS, Jwt.verify Middleware & Debugging NestJS) 18 | Link: https://youtu.be/BTLAcLSu1Rw 19 | 20 | 7. Video (NestJS & Angular, Add Socket.io, establish connection and validate JWT) 21 | Link: https://youtu.be/eMc9EsD4uqI 22 | 23 | 8. Video (NestJS & Angular, Create basic Room Entity, display Rooms for User on Socket.io Connect) 24 | Link: https://youtu.be/qEwMcZHrtnQ 25 | 26 | 9. Video (NestJS & Angular, Add Pagination for Rooms) 27 | Link: https://youtu.be/wqVRQMjxv9c 28 | 29 | 10. Video (NestJS, Refactor Observables to async/await and implement username search) 30 | Link: https://youtu.be/TnU8dMTnIKI 31 | 32 | 11. Video (Angular, Add ChatRoom Creation with realtime User Search) 33 | Link: https://youtu.be/WpLb2YH97D8 34 | 35 | 12. Video (NestJS, Emit created Room, to all his users, after creation) 36 | Link: https://youtu.be/0FVHwAHe0co 37 | 38 | 13. Video (NestJS, Refactor ConnectedUsers to ManyToOne and clean Connections table on startup) 39 | Link: https://youtu.be/TCqQo0k5Wrs 40 | 41 | 14. Video (NestJS, Add Message & JoinedRoom Entities & add Logic to Gateway to join & leave Room & addMessage) 42 | Link: https://youtu.be/tSqVeCH4URw 43 | 44 | 15. Video (Angular (&NestJS), addMessage and getMessages for Chatroom) 45 | Link: https://youtu.be/Tz75HKy8WGw 46 | 47 | 16. Video (Angular: Chatroom: sort Messages and display own Messages on right side ) 48 | Link: https://youtu.be/aOd4dJg2K9Y 49 | 50 | 17. Video (NestJS & Angular: Chatroom: emit New Message to all joined Users) 51 | Link: https://youtu.be/6DElULzFQj4 -------------------------------------------------------------------------------- /_user_stories/01.md: -------------------------------------------------------------------------------- 1 | 2 | ## Realtime Chat App with NestJS and Angular 3 | ### 1. Video (NestJS & Angular Set Up) 4 | Link: https://youtu.be/IievSCity8c 5 | 6 | You need: 7 | - Angular 8 | - Nestjs 9 | - npm 10 | - docker 11 | 12 | Story: 13 | As a developer I want the NestJS Backend and the Angular Frontend to be set up. 14 | Both should be run with a dockerfile. 15 | They should be run/started together via a docker-compose file, so we can start everything with one command. 16 | The NestJS Backend should connect to a Postgres database, that is also started with docker-compose file. 17 | At the end of this video the Frontend should be able to retrieve a value from the Backend, all inside docker. 18 | 19 | Acceptance Criteria: 20 | - Set Up Angular (/) 21 | - Set Up NestJS (/) 22 | - Start Angular with Docker-Compose/Dockerfile & access in the browser via http://localhost:4200 (/) 23 | - Start NestJS with Docker-Compose/Dockerfile & get a basic value via http://localhost:3000/api (/) 24 | - NestJS should connect to a postgres database with docker-compose (/) 25 | - Angular should display a value that it gets from the NestJS Backend (/) 26 | -------------------------------------------------------------------------------- /_user_stories/02.md: -------------------------------------------------------------------------------- 1 | ## Realtime Chat App with NestJS and Angular 2 | ### 2. Video (NestJS Login&Register Endpoints) 3 | Link: https://youtu.be/Hi6V6RbTmNM 4 | 5 | You need: 6 | - Angular 7 | - Nestjs 8 | - npm 9 | - docker 10 | 11 | Story: 12 | As a developer I want to be able to create one User, Login with User Credentials and findAll Users, via API calls. 13 | If you try to create a user, it should first be checked, if the email is already in use. 14 | The Password should be stored as a hash in the database and if a users tries to login, the provided password should be 15 | compared with the hash in the database. 16 | 17 | Acceptance Criteria: 18 | - new user module for nestjs (/) 19 | - New Endpoints: 20 | GET /api/users -> return all Users paginated (/) 21 | POST /api/users -> create new User (/) 22 | POST /api/users/login -> login a user, based on email and password (/) 23 | - Check if email is already in use, if new user is created (/) 24 | - Password is stored as a hash in the database (/) 25 | - Password should be compared to the hash on login (/) -------------------------------------------------------------------------------- /_user_stories/03.md: -------------------------------------------------------------------------------- 1 | ## Realtime Chat App with NestJS and Angular 2 | ### 3. Video (NestJS, Auth Module Jwt & Refactoring) 3 | Link: https://youtu.be/ydibF89YGV8 4 | 5 | You need: 6 | - Angular 7 | - Nestjs 8 | - npm 9 | - docker 10 | 11 | Story: 12 | As a developer i want to have the security (atm: bcrypt and Jwt Stuff) in a separate Authentication Module. 13 | All security stuff should be in a separate module, so we can import it later to every module where we need it. 14 | We also want to generate a jwt on login and be able to secure routes with a jwtguard. 15 | 16 | Acceptance Criteria: 17 | - new auth module (/) 18 | - bcrypt stuff moves to auth module (/) 19 | - generate a jwt with user payload (/) 20 | - have a guard, that checks for a valid jwt (/) 21 | - make the GET /api/users protected by the JwtAuthGuard (/) -------------------------------------------------------------------------------- /_user_stories/04.md: -------------------------------------------------------------------------------- 1 | ## Realtime Chat App with NestJS and Angular 2 | ### 4. Video (Angular: User Registration with Reactive Forms) 3 | Link: https://youtu.be/t6BpRxV4b0M 4 | 5 | You need: 6 | - Angular 7 | - Nestjs 8 | - npm 9 | - docker 10 | 11 | Story: 12 | As a User i want to be able to register myself an account through the Angular Web App. 13 | My Input data should be validated and on click on register i shall be redirected to the login page, if my account was created. 14 | 15 | 16 | Acceptance Criteria: 17 | - public module (/) 18 | - private module (/) 19 | - implement lazy loading for both modules (/) 20 | - set up components dashboard, register, login (/) 21 | - set up basic auth guard for private module (/) 22 | - set up routing for public module (/) 23 | - create registration form with angular reactive forms (/) 24 | - also display hints or errors in form (/) 25 | - user-service to create a user (/) 26 | - display a snackbar on success/failure (/) 27 | - add unique to username (nestjs) (/) -------------------------------------------------------------------------------- /_user_stories/05.md: -------------------------------------------------------------------------------- 1 | ## Realtime Chat App with NestJS and Angular 2 | ### 5. Video (Angular: User Login with Reactive Forms and JWT saving) 3 | Link: https://youtu.be/XTGbg-9yN2k 4 | 5 | You need: 6 | - Angular 7 | - Nestjs 8 | - npm 9 | - docker 10 | 11 | Story: 12 | As a User i want to be able to login to the app with my credentials. 13 | The input data should be validated. When my provided data was correct then the returned jwt token from the backend should be saved 14 | into the localstorage of the browser. 15 | The Auth Guard for the /private/ route should be updated, so that it checks, if there is a jwt in the localstorage that is not expired. 16 | 17 | Acceptance Criteria: 18 | - login form with reactive forms (/) 19 | - save jwt in localstorage (/) 20 | - update auth guard to check for expiration of jwt/if jwt exists (/) 21 | - send jwt token on every request (/) -------------------------------------------------------------------------------- /_user_stories/06.md: -------------------------------------------------------------------------------- 1 | ## Realtime Chat App with NestJS and Angular 2 | ### 6. Video (NestJS, Jwt.verify Middleware & Debugging NestJS) 3 | Link: https://youtu.be/BTLAcLSu1Rw 4 | 5 | You need: 6 | - Angular 7 | - Nestjs 8 | - npm 9 | - docker 10 | 11 | Story: 12 | As a developer i want to add a NestMiddleware, that checks every requests, except the register & login for a valid jwt 13 | and also gets the according user from the db and attach it to the request. So that we have access to it later and can be sure that 14 | the user is not deleted or modified. 15 | 16 | We also should add the debugging (launch.json) file for nestjs debugging. 17 | 18 | Acceptance Criteria: 19 | - add launch.json for nestjs debugging (/) 20 | - add nestmiddleware and apply to all routes except POST /api/users & POST /api/users/login (/) 21 | -------------------------------------------------------------------------------- /_user_stories/07.md: -------------------------------------------------------------------------------- 1 | ## Realtime Chat App with NestJS and Angular 2 | ### 7. Video (NestJS & Angular, Add Socket.io, establish connection and validate JWT) 3 | Link: https://youtu.be/eMc9EsD4uqI 4 | 5 | You need: 6 | - Angular 7 | - Nestjs 8 | - npm 9 | - docker 10 | 11 | Story: 12 | As a developer i want to implement Websockets with Socket.io so that we can later have realtime chatrooms for the users. 13 | For this we need to update NestJS to v8 (at the moment ALPHA status), because nestJS Websockets are on Socket.io v2 (which is not compatible with the new Socket.io v4). Also make sure to update rxjs to recent version. 14 | Only nestJS v8 will support Socket.io v4. 15 | When we establish a connection, we should also validate the user by checking his JWT Token. 16 | To make sure, that the connection works, we want to display simple Array Value in Angular. 17 | 18 | Acceptance Criteria: 19 | - NestJS dependencies are updated to v8 alpha, where available, also rxjs (/) 20 | - establish a connection beetween frontend and backend with socket.io (/) 21 | - validate the user with adding the jwt and checking it onHandle in Nest Gateway (/) 22 | - Display simple Array Value in Angular, returned by socket (/) -------------------------------------------------------------------------------- /_user_stories/08.md: -------------------------------------------------------------------------------- 1 | ## Realtime Chat App with NestJS and Angular 2 | ### 8. Video (NestJS & Angular, Create basic Room Entity, display Rooms for User on Socket.io Connect) 3 | Link: https://youtu.be/qEwMcZHrtnQ 4 | 5 | You need: 6 | - Angular 7 | - Nestjs 8 | - npm 9 | - docker 10 | 11 | Story: 12 | As a user i want to have displayed all rooms that i am currently listed in. 13 | For this to achieve we need to have a basic Room Entity in our Backend, that has a many to many relationship with the User Entity. (Messages & Room creation, pagination, styling etc will be coming in next videos.) 14 | 15 | Acceptance Criteria: 16 | - Create new Room.entity with ManyToMany Relationship to User.entity (/) 17 | - return all rooms for the user when he connects to socket.io (/) 18 | - display them basically in the frontend dashboard component (private module) (/) -------------------------------------------------------------------------------- /_user_stories/09.md: -------------------------------------------------------------------------------- 1 | ## Realtime Chat App with NestJS and Angular 2 | ### 9. Video (NestJS & Angular, Add Pagination for Rooms) 3 | Link: https://youtu.be/wqVRQMjxv9c 4 | 5 | You need: 6 | - Angular 7 | - Nestjs 8 | - npm 9 | - docker 10 | 11 | Story: 12 | As a user i want to be able to paginate through all the Chatrooms that i am listed in as a user currently. 13 | For this we need to add the Angular Material Paginator to our Dashboard and also have to emit and listen to Socket.io events. 14 | 15 | Acceptance Criteria: 16 | - The user is able to paginate through all his rooms (/) 17 | - The rooms are ordered By "updated_at" DESC (/) 18 | - With the room we get also the list of users returned (/) -------------------------------------------------------------------------------- /_user_stories/10.md: -------------------------------------------------------------------------------- 1 | ## Realtime Chat App with NestJS and Angular 2 | ### 10. Video (NestJS, Refactor Observables to async/await and implement username search) 3 | Link: https://youtu.be/TnU8dMTnIKI 4 | 5 | You need: 6 | - Angular 7 | - Nestjs 8 | - npm 9 | - docker 10 | 11 | Story: 12 | As a developer i want to refactor the observables to async/await so that the api code is consistent. 13 | We also want to add an endpoint to our user controller to search for a user by his username. 14 | Also refactor the code, so that we only save users with email and username toLowerCase. 15 | 16 | Acceptance Criteria: 17 | - refactor observables to async/await (/) 18 | - new GET endpoint to search by username, GET /api/users/find-by-username (/) 19 | - Refactor user creation, so that users email & username get inserted as lowercase (/) -------------------------------------------------------------------------------- /_user_stories/11.md: -------------------------------------------------------------------------------- 1 | ## Realtime Chat App with NestJS and Angular 2 | ### 11. Video (Angular, Add ChatRoom Creation with realtime User Search) 3 | Link: https://youtu.be/WpLb2YH97D8 4 | 5 | You need: 6 | - Angular 7 | - Nestjs 8 | - npm 9 | - docker 10 | 11 | Story: 12 | As a user we want to be able to create new chatrooms. 13 | The chatrooms should have at least a name and one user. I should be able to select the users by a search for the username. 14 | 15 | Acceptance Criteria: 16 | - Chatrooms can be created (/) 17 | - we can search in realtime for users by their username (/) 18 | - while in room creation, we also can remove users before the final creation of the room (/) 19 | - if there are no rooms, we want to a hint displayed on the dashboard (/) 20 | -------------------------------------------------------------------------------- /_user_stories/12.md: -------------------------------------------------------------------------------- 1 | ## Realtime Chat App with NestJS and Angular 2 | ### 12. Video (NestJS, Emit created Room, to all his users, after creation) 3 | Link: https://youtu.be/0FVHwAHe0co 4 | 5 | You need: 6 | - Angular 7 | - Nestjs 8 | - npm 9 | - docker 10 | 11 | Story: 12 | As a user i want to automatically have a new room, where i am listed as a user, displayed on my dashboard (in realtime), after creation. 13 | If User A creates a new Chatroom with User B, then User B should see this Chatroom immediately on his dashboard after creation. 14 | 15 | 16 | Acceptance Criteria: 17 | - create new Entity, ConnectedUsersEntity with a OneToOne Relation to users (/) 18 | - save the socketid in the new Entity (/) 19 | - Save the connectedUser onHandleConnection (/) 20 | - delete the connectedUser onDisconnect (/) 21 | - Notify all users with the room, that are users of the room, when a new room is created (/) -------------------------------------------------------------------------------- /_user_stories/13.md: -------------------------------------------------------------------------------- 1 | ## Realtime Chat App with NestJS and Angular 2 | ### 13. Video (NestJS, Refactor ConnectedUsers to ManyToOne and clean Connections table on startup) 3 | Link: https://youtu.be/TCqQo0k5Wrs 4 | 5 | You need: 6 | - Angular 7 | - Nestjs 8 | - npm 9 | - docker 10 | 11 | Story: 12 | As a developer we want to enable the users to have more than one connection. 13 | So we need to refactor the OneToOne Relationship to a ManyToOne, because a user could login from e.g. two devices. 14 | Also we want to remove all entries in our ConnectedUser table, when we start our App, so we don't have there old connections. 15 | 16 | 17 | Acceptance Criteria: 18 | - Refactor ConnectedUser Relationship to User from OneToOne to ManyToOne (/) 19 | - Clean ConnectedUserTable on Startup (Lyfecycle events) (/) -------------------------------------------------------------------------------- /_user_stories/14.md: -------------------------------------------------------------------------------- 1 | ## Realtime Chat App with NestJS and Angular 2 | ### 14. Video (NestJS, Add Message & JoinedRoom Entities & add Logic to Gateway to join & leave Room & addMessage) 3 | Link: https://youtu.be/tSqVeCH4URw 4 | 5 | You need: 6 | - Angular 7 | - Nestjs 8 | - npm 9 | - docker 10 | 11 | Story: 12 | As a developer i want that the user is able to join & leave one of his chatrooms. 13 | He should also be able to add a Message to a chatroom. 14 | For this we need to add a Message Entity & a joinedRoom Entity to our API and also some logic to our gateway. 15 | 16 | Acceptance Criteria: 17 | - new Message Entity with ManyToOne to User & Room (/) 18 | - new JoinedRoom Entity with ManyToOne to User & Room (/) 19 | - Gateway add: 'addMessage', 'joinRoom', 'leaveRoom' & update existing functions (/) 20 | -------------------------------------------------------------------------------- /_user_stories/15.md: -------------------------------------------------------------------------------- 1 | ## Realtime Chat App with NestJS and Angular 2 | ### 15. Video (Angular (&NestJS), addMessage and getMessages for Chatroom) 3 | Link: https://youtu.be/Tz75HKy8WGw 4 | 5 | You need: 6 | - Angular 7 | - Nestjs 8 | - npm 9 | - docker 10 | 11 | Story: 12 | As a user i want to be able to open my chatroom and to add a message to it. 13 | I also want to get displayed the latest messages. 14 | (Final Design, like displaying the own messages different and emit a new message directly will be done in next videos.) 15 | 16 | Acceptance Criteria: 17 | - new Component ChatRoom (/) 18 | - new Component Message (/) 19 | - Scrollable Rooms and Messages (/) 20 | - Design improvements (/) 21 | - modify the API message service, so that it filters with the roomid & adds the user to the message (/) -------------------------------------------------------------------------------- /_user_stories/16.md: -------------------------------------------------------------------------------- 1 | ## Realtime Chat App with NestJS and Angular 2 | ### 16. Video (Angular: Chatroom: sort Messages and display own Messages on right side ) 3 | Link: https://youtu.be/aOd4dJg2K9Y 4 | 5 | You need: 6 | - Angular 7 | - Nestjs 8 | - npm 9 | - docker 10 | 11 | Story: 12 | As a User i want to see the messages for a chatroom sorted by the date. 13 | I also want my own messages to be displayed on the right side and others on the left. 14 | 15 | Acceptance Criteria: 16 | - Sort Messages by Creation Date in Chatroom (/) 17 | - Display Own Messages on the right side (/) -------------------------------------------------------------------------------- /_user_stories/17.md: -------------------------------------------------------------------------------- 1 | ## Realtime Chat App with NestJS and Angular 2 | ### 17. Video (NestJS & Angular: Chatroom: emit New Message to all joined Users) 3 | Link: https://youtu.be/6DElULzFQj4 4 | 5 | You need: 6 | - Angular 7 | - Nestjs 8 | - npm 9 | - docker 10 | 11 | Story: 12 | As a user i want that a new message that i send to a chat room, immediately gets displayed to all the joinedUsers (the users that are currently online in this chatroom). 13 | The Chat should also scroll automatically to the bottom. 14 | 15 | Acceptance Criteria: 16 | - emit new message to all joined users that are currently online (/) 17 | - chat should scroll automatically to the bottom (/) 18 | - update readme file (/) -------------------------------------------------------------------------------- /api/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | sourceType: 'module', 6 | }, 7 | plugins: ['@typescript-eslint/eslint-plugin'], 8 | extends: [ 9 | 'plugin:@typescript-eslint/recommended', 10 | 'plugin:prettier/recommended', 11 | ], 12 | root: true, 13 | env: { 14 | node: true, 15 | jest: true, 16 | }, 17 | ignorePatterns: ['.eslintrc.js'], 18 | rules: { 19 | '@typescript-eslint/interface-name-prefix': 'off', 20 | '@typescript-eslint/explicit-function-return-type': 'off', 21 | '@typescript-eslint/explicit-module-boundary-types': 'off', 22 | '@typescript-eslint/no-explicit-any': 'off', 23 | "prettier/prettier": [ 24 | "error", 25 | { 26 | "endOfLine": "auto" 27 | }, 28 | ], 29 | }, 30 | }; 31 | -------------------------------------------------------------------------------- /api/.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | 13 | # OS 14 | .DS_Store 15 | 16 | # Tests 17 | /coverage 18 | /.nyc_output 19 | 20 | # IDEs and editors 21 | /.idea 22 | .project 23 | .classpath 24 | .c9/ 25 | *.launch 26 | .settings/ 27 | *.sublime-workspace 28 | 29 | # IDE - VSCode 30 | .vscode/* 31 | !.vscode/settings.json 32 | !.vscode/tasks.json 33 | !.vscode/launch.json 34 | !.vscode/extensions.json -------------------------------------------------------------------------------- /api/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "endOfLine":"auto" 5 | } -------------------------------------------------------------------------------- /api/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "node", 6 | "request": "attach", 7 | "name": "Debug: nest js real time chat", 8 | "remoteRoot": "/thomas/src/app", 9 | "localRoot": "${workspaceFolder}", 10 | "protocol": "inspector", 11 | "port": 9229, 12 | "restart": true, 13 | "address": "0.0.0.0", 14 | "skipFiles": [ 15 | "/**" 16 | ] 17 | } 18 | ] 19 | } -------------------------------------------------------------------------------- /api/Dockerfile: -------------------------------------------------------------------------------- 1 | # Specify Node Version and Image 2 | # Name Image development (can be anything) 3 | FROM node:14 AS development 4 | 5 | # Specify Working directory inside container 6 | WORKDIR /thomas/src/app 7 | 8 | # Copy package-lock.json & package.json from host to inside container working directory 9 | COPY package*.json ./ 10 | 11 | # Install deps inside container 12 | RUN npm install 13 | 14 | RUN npm run build 15 | 16 | EXPOSE 3000 17 | 18 | ################ 19 | ## PRODUCTION ## 20 | ################ 21 | # Build another image named production 22 | FROM node:14 AS production 23 | 24 | ARG NODE_ENV=production 25 | ENV NODE_ENV=${NODE_ENV} 26 | 27 | # Set work dir 28 | WORKDIR /thomas/src/app 29 | 30 | COPY --from=development /thomas/src/app/ . 31 | 32 | EXPOSE 3000 33 | 34 | # run app 35 | CMD [ "node", "dist/main"] -------------------------------------------------------------------------------- /api/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 | $ npm install 33 | ``` 34 | 35 | ## Running the app 36 | 37 | ```bash 38 | # development 39 | $ npm run start 40 | 41 | # watch mode 42 | $ npm run start:dev 43 | 44 | # production mode 45 | $ npm run start:prod 46 | ``` 47 | 48 | ## Test 49 | 50 | ```bash 51 | # unit tests 52 | $ npm run test 53 | 54 | # e2e tests 55 | $ npm run test:e2e 56 | 57 | # test coverage 58 | $ npm 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 | -------------------------------------------------------------------------------- /api/nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "collection": "@nestjs/schematics", 3 | "sourceRoot": "src" 4 | } 5 | -------------------------------------------------------------------------------- /api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "api", 3 | "version": "0.0.1", 4 | "description": "", 5 | "author": "", 6 | "private": true, 7 | "license": "UNLICENSED", 8 | "scripts": { 9 | "prebuild": "rimraf dist", 10 | "build": "nest build", 11 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 12 | "start": "nest start", 13 | "start:dev": "nest start --watch", 14 | "start:debug": "nest start --debug 0.0.0.0:9229 --watch", 15 | "start:prod": "node dist/main", 16 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", 17 | "test": "jest", 18 | "test:watch": "jest --watch", 19 | "test:cov": "jest --coverage", 20 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 21 | "test:e2e": "jest --config ./test/jest-e2e.json" 22 | }, 23 | "dependencies": { 24 | "@nestjs/common": "^8.2.4", 25 | "@nestjs/config": "^1.1.5", 26 | "@nestjs/core": "^8.2.4", 27 | "@nestjs/jwt": "^8.0.0", 28 | "@nestjs/passport": "^8.0.1", 29 | "@nestjs/platform-express": "^8.2.4", 30 | "@nestjs/platform-socket.io": "^8.2.4", 31 | "@nestjs/typeorm": "^8.0.2", 32 | "@nestjs/websockets": "^8.2.4", 33 | "bcrypt": "^5.0.1", 34 | "class-transformer": "^0.4.0", 35 | "class-validator": "^0.13.1", 36 | "nestjs-typeorm-paginate": "^3.1.3", 37 | "passport": "^0.4.1", 38 | "passport-jwt": "^4.0.0", 39 | "passport-local": "^1.0.0", 40 | "pg": "^8.6.0", 41 | "reflect-metadata": "^0.1.13", 42 | "rimraf": "^3.0.2", 43 | "rxjs": "^7.2.0", 44 | "socket.io": "^4.4.0", 45 | "typeorm": "^0.2.41" 46 | }, 47 | "devDependencies": { 48 | "@nestjs/cli": "^8.1.6", 49 | "@nestjs/schematics": "^8.0.5", 50 | "@nestjs/testing": "^8.2.4", 51 | "@types/express": "^4.17.13", 52 | "@types/jest": "27.0.2", 53 | "@types/node": "^16.0.0", 54 | "@types/supertest": "^2.0.11", 55 | "@typescript-eslint/eslint-plugin": "^5.0.0", 56 | "@typescript-eslint/parser": "^5.0.0", 57 | "eslint": "^8.0.1", 58 | "eslint-config-prettier": "^8.3.0", 59 | "eslint-plugin-prettier": "^4.0.0", 60 | "jest": "^27.2.5", 61 | "prettier": "^2.3.2", 62 | "source-map-support": "^0.5.20", 63 | "supertest": "^6.1.3", 64 | "ts-jest": "^27.0.3", 65 | "ts-loader": "^9.2.3", 66 | "ts-node": "^10.0.0", 67 | "tsconfig-paths": "^3.10.1", 68 | "typescript": "^4.3.5" 69 | }, 70 | "jest": { 71 | "moduleFileExtensions": [ 72 | "js", 73 | "json", 74 | "ts" 75 | ], 76 | "rootDir": "src", 77 | "testRegex": ".*\\.spec\\.ts$", 78 | "transform": { 79 | "^.+\\.(t|j)s$": "ts-jest" 80 | }, 81 | "collectCoverageFrom": [ 82 | "**/*.(t|j)s" 83 | ], 84 | "coverageDirectory": "../coverage", 85 | "testEnvironment": "node" 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /api/src/app.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { AppController } from './app.controller'; 3 | import { AppService } from './app.service'; 4 | 5 | describe('AppController', () => { 6 | let appController: AppController; 7 | 8 | beforeEach(async () => { 9 | const app: TestingModule = await Test.createTestingModule({ 10 | controllers: [AppController], 11 | providers: [AppService], 12 | }).compile(); 13 | 14 | appController = app.get(AppController); 15 | }); 16 | 17 | describe('root', () => { 18 | it('should return "Hello World!"', () => { 19 | expect(appController.getHello()).toBe('Hello World!'); 20 | }); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /api/src/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get } from '@nestjs/common'; 2 | import { AppService } from './app.service'; 3 | 4 | @Controller() 5 | export class AppController { 6 | constructor(private readonly appService: AppService) {} 7 | 8 | @Get() 9 | getHello(): Object { 10 | return this.appService.getHello(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /api/src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { MiddlewareConsumer, Module, NestModule, RequestMethod } from '@nestjs/common'; 2 | import { AppController } from './app.controller'; 3 | import { AppService } from './app.service'; 4 | import { ConfigModule } from '@nestjs/config'; 5 | import { TypeOrmModule } from '@nestjs/typeorm'; 6 | import { UserModule } from './user/user.module'; 7 | import { AuthModule } from './auth/auth.module'; 8 | import { AuthMiddleware } from './middleware/auth.middleware'; 9 | import { ChatModule } from './chat/chat.module'; 10 | 11 | @Module({ 12 | imports: [ 13 | ConfigModule.forRoot({isGlobal: true}), 14 | TypeOrmModule.forRoot({ 15 | type: 'postgres', 16 | url: process.env.DATABASE_URL, 17 | autoLoadEntities: true, 18 | synchronize: true 19 | }), 20 | UserModule, 21 | AuthModule, 22 | ChatModule 23 | ], 24 | controllers: [AppController], 25 | providers: [AppService], 26 | }) 27 | export class AppModule implements NestModule { 28 | configure(consumer: MiddlewareConsumer) { 29 | consumer 30 | .apply(AuthMiddleware) 31 | .exclude( 32 | { path: '/api/users', method: RequestMethod.POST}, 33 | {path: '/api/users/login', method: RequestMethod.POST} 34 | ) 35 | .forRoutes('') 36 | } 37 | } -------------------------------------------------------------------------------- /api/src/app.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class AppService { 5 | getHello(): Object { 6 | return {title: 'Hello Youtube!'}; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /api/src/auth/auth.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { ConfigModule, ConfigService } from '@nestjs/config'; 3 | import { JwtModule } from '@nestjs/jwt'; 4 | import { JwtAuthGuard } from './guards/jwt.guard'; 5 | import { AuthService } from './service/auth.service'; 6 | import { JwtStrategy } from './strategies/jwt.strategy'; 7 | 8 | @Module({ 9 | imports: [ 10 | JwtModule.registerAsync({ 11 | imports: [ConfigModule], 12 | inject: [ConfigService], 13 | useFactory: async (configService: ConfigService) => ({ 14 | secret: configService.get('JWT_SECRET'), 15 | signOptions: { expiresIn: '10000s'} 16 | }) 17 | }) 18 | ], 19 | providers: [AuthService, JwtStrategy, JwtAuthGuard], 20 | exports: [AuthService] 21 | }) 22 | export class AuthModule {} 23 | -------------------------------------------------------------------------------- /api/src/auth/guards/jwt.guard.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Injectable } from '@nestjs/common'; 3 | import { AuthGuard } from '@nestjs/passport'; 4 | 5 | @Injectable() 6 | export class JwtAuthGuard extends AuthGuard('jwt') {} 7 | -------------------------------------------------------------------------------- /api/src/auth/service/auth.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { AuthService } from './auth.service'; 3 | 4 | describe('AuthService', () => { 5 | let service: AuthService; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [AuthService], 10 | }).compile(); 11 | 12 | service = module.get(AuthService); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(service).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /api/src/auth/service/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { JwtService } from '@nestjs/jwt'; 3 | import { UserI } from 'src/user/model/user.interface'; 4 | 5 | const bcrypt = require('bcrypt'); 6 | 7 | @Injectable() 8 | export class AuthService { 9 | 10 | constructor(private readonly jwtService: JwtService) {} 11 | 12 | async generateJwt(user: UserI): Promise { 13 | return this.jwtService.signAsync({user}); 14 | } 15 | 16 | async hashPassword(password: string): Promise { 17 | return bcrypt.hash(password, 12); 18 | } 19 | 20 | async comparePasswords(password: string, storedPasswordHash: string): Promise { 21 | return bcrypt.compare(password, storedPasswordHash); 22 | } 23 | 24 | verifyJwt(jwt: string): Promise { 25 | return this.jwtService.verifyAsync(jwt); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /api/src/auth/strategies/jwt.strategy.ts: -------------------------------------------------------------------------------- 1 | 2 | import { ExtractJwt, Strategy } from 'passport-jwt'; 3 | import { PassportStrategy } from '@nestjs/passport'; 4 | import { Injectable } from '@nestjs/common'; 5 | import { ConfigService } from '@nestjs/config'; 6 | 7 | @Injectable() 8 | export class JwtStrategy extends PassportStrategy(Strategy) { 9 | 10 | constructor(private configService: ConfigService) { 11 | super({ 12 | jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), 13 | ignoreExpiration: false, 14 | secretOrKey: configService.get('JWT_SECRET') 15 | }); 16 | } 17 | 18 | async validate(payload: any) { 19 | return { ...payload.user }; 20 | } 21 | } -------------------------------------------------------------------------------- /api/src/chat/chat.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { TypeOrmModule } from '@nestjs/typeorm'; 3 | import { AuthModule } from 'src/auth/auth.module'; 4 | import { UserModule } from 'src/user/user.module'; 5 | import { ChatGateway } from './gateway/chat.gateway'; 6 | import { RoomEntity } from './model/room/room.entity'; 7 | import { RoomService } from './service/room-service/room.service'; 8 | import { ConnectedUserService } from './service/connected-user/connected-user.service'; 9 | import { ConnectedUserEntity } from './model/connected-user/connected-user.entity'; 10 | import { MessageEntity } from './model/message/message.entity'; 11 | import { JoinedRoomEntity } from './model/joined-room/joined-room.entity'; 12 | import { JoinedRoomService } from './service/joined-room/joined-room.service'; 13 | import { MessageService } from './service/message/message.service'; 14 | 15 | @Module({ 16 | imports: [AuthModule, UserModule, 17 | TypeOrmModule.forFeature([ 18 | RoomEntity, 19 | ConnectedUserEntity, 20 | MessageEntity, 21 | JoinedRoomEntity 22 | ]) 23 | ], 24 | providers: [ChatGateway, RoomService, ConnectedUserService, JoinedRoomService, MessageService] 25 | }) 26 | export class ChatModule { } 27 | -------------------------------------------------------------------------------- /api/src/chat/gateway/chat.gateway.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { ChatGateway } from './chat.gateway'; 3 | 4 | describe('ChatGateway', () => { 5 | let gateway: ChatGateway; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [ChatGateway], 10 | }).compile(); 11 | 12 | gateway = module.get(ChatGateway); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(gateway).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /api/src/chat/gateway/chat.gateway.ts: -------------------------------------------------------------------------------- 1 | import { OnGatewayConnection, OnGatewayDisconnect, SubscribeMessage, WebSocketGateway, WebSocketServer } from '@nestjs/websockets'; 2 | import { AuthService } from 'src/auth/service/auth.service'; 3 | import { Socket, Server } from 'socket.io'; 4 | import { UserI } from 'src/user/model/user.interface'; 5 | import { UserService } from 'src/user/service/user-service/user.service'; 6 | import { OnModuleInit, UnauthorizedException } from '@nestjs/common'; 7 | import { RoomService } from '../service/room-service/room.service'; 8 | import { PageI } from '../model/page.interface'; 9 | import { ConnectedUserService } from '../service/connected-user/connected-user.service'; 10 | import { RoomI } from '../model/room/room.interface'; 11 | import { ConnectedUserI } from '../model/connected-user/connected-user.interface'; 12 | import { JoinedRoomService } from '../service/joined-room/joined-room.service'; 13 | import { MessageService } from '../service/message/message.service'; 14 | import { MessageI } from '../model/message/message.interface'; 15 | import { JoinedRoomI } from '../model/joined-room/joined-room.interface'; 16 | 17 | @WebSocketGateway({ cors: { origin: ['https://hoppscotch.io', 'http://localhost:3000', 'http://localhost:4200'] } }) 18 | export class ChatGateway implements OnGatewayConnection, OnGatewayDisconnect, OnModuleInit { 19 | 20 | @WebSocketServer() 21 | server: Server; 22 | 23 | constructor( 24 | private authService: AuthService, 25 | private userService: UserService, 26 | private roomService: RoomService, 27 | private connectedUserService: ConnectedUserService, 28 | private joinedRoomService: JoinedRoomService, 29 | private messageService: MessageService) { } 30 | 31 | async onModuleInit() { 32 | await this.connectedUserService.deleteAll(); 33 | await this.joinedRoomService.deleteAll(); 34 | } 35 | 36 | async handleConnection(socket: Socket) { 37 | try { 38 | const decodedToken = await this.authService.verifyJwt(socket.handshake.headers.authorization); 39 | const user: UserI = await this.userService.getOne(decodedToken.user.id); 40 | if (!user) { 41 | return this.disconnect(socket); 42 | } else { 43 | socket.data.user = user; 44 | const rooms = await this.roomService.getRoomsForUser(user.id, { page: 1, limit: 10 }); 45 | // substract page -1 to match the angular material paginator 46 | rooms.meta.currentPage = rooms.meta.currentPage - 1; 47 | // Save connection to DB 48 | await this.connectedUserService.create({ socketId: socket.id, user }); 49 | // Only emit rooms to the specific connected client 50 | return this.server.to(socket.id).emit('rooms', rooms); 51 | } 52 | } catch { 53 | return this.disconnect(socket); 54 | } 55 | } 56 | 57 | async handleDisconnect(socket: Socket) { 58 | // remove connection from DB 59 | await this.connectedUserService.deleteBySocketId(socket.id); 60 | socket.disconnect(); 61 | } 62 | 63 | private disconnect(socket: Socket) { 64 | socket.emit('Error', new UnauthorizedException()); 65 | socket.disconnect(); 66 | } 67 | 68 | @SubscribeMessage('createRoom') 69 | async onCreateRoom(socket: Socket, room: RoomI) { 70 | const createdRoom: RoomI = await this.roomService.createRoom(room, socket.data.user); 71 | 72 | for (const user of createdRoom.users) { 73 | const connections: ConnectedUserI[] = await this.connectedUserService.findByUser(user); 74 | const rooms = await this.roomService.getRoomsForUser(user.id, { page: 1, limit: 10 }); 75 | // substract page -1 to match the angular material paginator 76 | rooms.meta.currentPage = rooms.meta.currentPage - 1; 77 | for (const connection of connections) { 78 | await this.server.to(connection.socketId).emit('rooms', rooms); 79 | } 80 | } 81 | } 82 | 83 | @SubscribeMessage('paginateRooms') 84 | async onPaginateRoom(socket: Socket, page: PageI) { 85 | const rooms = await this.roomService.getRoomsForUser(socket.data.user.id, this.handleIncomingPageRequest(page)); 86 | // substract page -1 to match the angular material paginator 87 | rooms.meta.currentPage = rooms.meta.currentPage - 1; 88 | return this.server.to(socket.id).emit('rooms', rooms); 89 | } 90 | 91 | @SubscribeMessage('joinRoom') 92 | async onJoinRoom(socket: Socket, room: RoomI) { 93 | const messages = await this.messageService.findMessagesForRoom(room, { limit: 10, page: 1 }); 94 | messages.meta.currentPage = messages.meta.currentPage - 1; 95 | // Save Connection to Room 96 | await this.joinedRoomService.create({ socketId: socket.id, user: socket.data.user, room }); 97 | // Send last messages from Room to User 98 | await this.server.to(socket.id).emit('messages', messages); 99 | } 100 | 101 | @SubscribeMessage('leaveRoom') 102 | async onLeaveRoom(socket: Socket) { 103 | // remove connection from JoinedRooms 104 | await this.joinedRoomService.deleteBySocketId(socket.id); 105 | } 106 | 107 | @SubscribeMessage('addMessage') 108 | async onAddMessage(socket: Socket, message: MessageI) { 109 | const createdMessage: MessageI = await this.messageService.create({...message, user: socket.data.user}); 110 | const room: RoomI = await this.roomService.getRoom(createdMessage.room.id); 111 | const joinedUsers: JoinedRoomI[] = await this.joinedRoomService.findByRoom(room); 112 | // TODO: Send new Message to all joined Users of the room (currently online) 113 | for(const user of joinedUsers) { 114 | await this.server.to(user.socketId).emit('messageAdded', createdMessage); 115 | } 116 | } 117 | 118 | private handleIncomingPageRequest(page: PageI) { 119 | page.limit = page.limit > 100 ? 100 : page.limit; 120 | // add page +1 to match angular material paginator 121 | page.page = page.page + 1; 122 | return page; 123 | } 124 | 125 | } 126 | -------------------------------------------------------------------------------- /api/src/chat/model/connected-user/connected-user.entity.ts: -------------------------------------------------------------------------------- 1 | import { UserEntity } from "src/user/model/user.entity"; 2 | import { Column, Entity, JoinColumn, ManyToOne, OneToOne, PrimaryGeneratedColumn } from "typeorm"; 3 | 4 | 5 | @Entity() 6 | export class ConnectedUserEntity { 7 | 8 | @PrimaryGeneratedColumn() 9 | id: number; 10 | 11 | @Column() 12 | socketId: string; 13 | 14 | @ManyToOne(() => UserEntity, user => user.connections) 15 | @JoinColumn() 16 | user: UserEntity; 17 | 18 | } -------------------------------------------------------------------------------- /api/src/chat/model/connected-user/connected-user.interface.ts: -------------------------------------------------------------------------------- 1 | import { UserI } from "src/user/model/user.interface"; 2 | 3 | 4 | export interface ConnectedUserI { 5 | id?: number; 6 | socketId: string; 7 | user: UserI; 8 | } -------------------------------------------------------------------------------- /api/src/chat/model/joined-room/joined-room.entity.ts: -------------------------------------------------------------------------------- 1 | import { UserEntity } from "src/user/model/user.entity"; 2 | import { Column, Entity, JoinColumn, ManyToOne, PrimaryGeneratedColumn } from "typeorm"; 3 | import { RoomEntity } from "../room/room.entity"; 4 | 5 | @Entity() 6 | export class JoinedRoomEntity { 7 | 8 | @PrimaryGeneratedColumn() 9 | id: number; 10 | 11 | @Column() 12 | socketId: string; 13 | 14 | @ManyToOne(() => UserEntity, user => user.joinedRooms) 15 | @JoinColumn() 16 | user: UserEntity; 17 | 18 | @ManyToOne(() => RoomEntity, room => room.joinedUsers) 19 | @JoinColumn() 20 | room: RoomEntity; 21 | 22 | } -------------------------------------------------------------------------------- /api/src/chat/model/joined-room/joined-room.interface.ts: -------------------------------------------------------------------------------- 1 | import { UserI } from "src/user/model/user.interface"; 2 | import { RoomI } from "../room/room.interface"; 3 | 4 | 5 | export interface JoinedRoomI { 6 | id?: number; 7 | socketId: string; 8 | user: UserI; 9 | room: RoomI; 10 | } -------------------------------------------------------------------------------- /api/src/chat/model/message/message.entity.ts: -------------------------------------------------------------------------------- 1 | import { UserEntity } from "src/user/model/user.entity"; 2 | import { Column, CreateDateColumn, Entity, JoinColumn, JoinTable, ManyToOne, PrimaryGeneratedColumn, UpdateDateColumn } from "typeorm"; 3 | import { RoomEntity } from "../room/room.entity"; 4 | 5 | @Entity() 6 | export class MessageEntity { 7 | 8 | @PrimaryGeneratedColumn() 9 | id: number; 10 | 11 | @Column() 12 | text: string; 13 | 14 | @ManyToOne(() => UserEntity, user => user.messages) 15 | @JoinColumn() 16 | user: UserEntity; 17 | 18 | @ManyToOne(() => RoomEntity, room => room.messages) 19 | @JoinTable() 20 | room: RoomEntity; 21 | 22 | @CreateDateColumn() 23 | created_at: Date; 24 | 25 | @UpdateDateColumn() 26 | updated_at: Date; 27 | 28 | } -------------------------------------------------------------------------------- /api/src/chat/model/message/message.interface.ts: -------------------------------------------------------------------------------- 1 | import { UserI } from "src/user/model/user.interface"; 2 | import { RoomI } from "../room/room.interface"; 3 | 4 | 5 | export interface MessageI { 6 | id?: number; 7 | text: string; 8 | user: UserI; 9 | room: RoomI; 10 | created_at: Date; 11 | updated_at: Date; 12 | } -------------------------------------------------------------------------------- /api/src/chat/model/page.interface.ts: -------------------------------------------------------------------------------- 1 | export interface PageI { 2 | page: number; 3 | limit: number; 4 | } -------------------------------------------------------------------------------- /api/src/chat/model/room/room.entity.ts: -------------------------------------------------------------------------------- 1 | import { UserEntity } from "src/user/model/user.entity"; 2 | import { Column, CreateDateColumn, Entity, JoinTable, ManyToMany, OneToMany, PrimaryGeneratedColumn, UpdateDateColumn } from "typeorm"; 3 | import { JoinedRoomEntity } from "../joined-room/joined-room.entity"; 4 | import { MessageEntity } from "../message/message.entity"; 5 | 6 | @Entity() 7 | export class RoomEntity { 8 | 9 | @PrimaryGeneratedColumn() 10 | id: number; 11 | 12 | @Column() 13 | name: string; 14 | 15 | @Column({nullable: true}) 16 | description: string; 17 | 18 | @ManyToMany(() => UserEntity) 19 | @JoinTable() 20 | users: UserEntity[]; 21 | 22 | @OneToMany(() => JoinedRoomEntity, joinedRoom => joinedRoom.room) 23 | joinedUsers: JoinedRoomEntity[]; 24 | 25 | @OneToMany(() => MessageEntity, message => message.room) 26 | messages: MessageEntity[]; 27 | 28 | @CreateDateColumn() 29 | created_at: Date; 30 | 31 | @UpdateDateColumn() 32 | updated_at: Date; 33 | 34 | } -------------------------------------------------------------------------------- /api/src/chat/model/room/room.interface.ts: -------------------------------------------------------------------------------- 1 | import { UserI } from "src/user/model/user.interface"; 2 | 3 | export interface RoomI { 4 | id?: number; 5 | name?: string; 6 | description?: string; 7 | users?: UserI[]; 8 | created_at?: Date; 9 | updated_at?: Date; 10 | } 11 | -------------------------------------------------------------------------------- /api/src/chat/service/connected-user/connected-user.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { ConnectedUserService } from './connected-user.service'; 3 | 4 | describe('ConnectedUserService', () => { 5 | let service: ConnectedUserService; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [ConnectedUserService], 10 | }).compile(); 11 | 12 | service = module.get(ConnectedUserService); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(service).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /api/src/chat/service/connected-user/connected-user.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { InjectRepository } from '@nestjs/typeorm'; 3 | import { ConnectedUserEntity } from 'src/chat/model/connected-user/connected-user.entity'; 4 | import { ConnectedUserI } from 'src/chat/model/connected-user/connected-user.interface'; 5 | import { UserI } from 'src/user/model/user.interface'; 6 | import { Repository } from 'typeorm'; 7 | 8 | @Injectable() 9 | export class ConnectedUserService { 10 | 11 | constructor( 12 | @InjectRepository(ConnectedUserEntity) 13 | private readonly connectedUserRepository: Repository 14 | ) { } 15 | 16 | async create(connectedUser: ConnectedUserI): Promise { 17 | return this.connectedUserRepository.save(connectedUser); 18 | } 19 | 20 | async findByUser(user: UserI): Promise { 21 | return this.connectedUserRepository.find({ user }); 22 | } 23 | 24 | async deleteBySocketId(socketId: string) { 25 | return this.connectedUserRepository.delete({ socketId }); 26 | } 27 | 28 | async deleteAll() { 29 | await this.connectedUserRepository 30 | .createQueryBuilder() 31 | .delete() 32 | .execute(); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /api/src/chat/service/joined-room/joined-room.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { JoinedRoomService } from './joined-room.service'; 3 | 4 | describe('JoinedRoomService', () => { 5 | let service: JoinedRoomService; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [JoinedRoomService], 10 | }).compile(); 11 | 12 | service = module.get(JoinedRoomService); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(service).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /api/src/chat/service/joined-room/joined-room.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { InjectRepository } from '@nestjs/typeorm'; 3 | import { JoinedRoomEntity } from 'src/chat/model/joined-room/joined-room.entity'; 4 | import { JoinedRoomI } from 'src/chat/model/joined-room/joined-room.interface'; 5 | import { RoomI } from 'src/chat/model/room/room.interface'; 6 | import { UserI } from 'src/user/model/user.interface'; 7 | import { Repository } from 'typeorm'; 8 | 9 | @Injectable() 10 | export class JoinedRoomService { 11 | 12 | constructor( 13 | @InjectRepository(JoinedRoomEntity) 14 | private readonly joinedRoomRepository: Repository 15 | ) { } 16 | 17 | async create(joinedRoom: JoinedRoomI): Promise { 18 | return this.joinedRoomRepository.save(joinedRoom); 19 | } 20 | 21 | async findByUser(user: UserI): Promise { 22 | return this.joinedRoomRepository.find({ user }); 23 | } 24 | 25 | async findByRoom(room: RoomI): Promise { 26 | return this.joinedRoomRepository.find({ room }); 27 | } 28 | 29 | async deleteBySocketId(socketId: string) { 30 | return this.joinedRoomRepository.delete({ socketId }); 31 | } 32 | 33 | async deleteAll() { 34 | await this.joinedRoomRepository 35 | .createQueryBuilder() 36 | .delete() 37 | .execute(); 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /api/src/chat/service/message/message.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { MessageService } from './message.service'; 3 | 4 | describe('MessageService', () => { 5 | let service: MessageService; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [MessageService], 10 | }).compile(); 11 | 12 | service = module.get(MessageService); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(service).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /api/src/chat/service/message/message.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { InjectRepository } from '@nestjs/typeorm'; 3 | import { IPaginationOptions, paginate, Pagination } from 'nestjs-typeorm-paginate'; 4 | import { MessageEntity } from 'src/chat/model/message/message.entity'; 5 | import { MessageI } from 'src/chat/model/message/message.interface'; 6 | import { RoomI } from 'src/chat/model/room/room.interface'; 7 | import { Repository } from 'typeorm'; 8 | 9 | @Injectable() 10 | export class MessageService { 11 | 12 | 13 | constructor( 14 | @InjectRepository(MessageEntity) 15 | private readonly messageRepository: Repository 16 | ) { } 17 | 18 | async create(message: MessageI): Promise { 19 | return this.messageRepository.save(this.messageRepository.create(message)); 20 | } 21 | 22 | async findMessagesForRoom(room: RoomI, options: IPaginationOptions): Promise> { 23 | const query = this.messageRepository 24 | .createQueryBuilder('message') 25 | .leftJoin('message.room', 'room') 26 | .where('room.id = :roomId', { roomId: room.id }) 27 | .leftJoinAndSelect('message.user', 'user') 28 | .orderBy('message.created_at', 'DESC'); 29 | 30 | return paginate(query, options); 31 | 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /api/src/chat/service/room-service/room.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { RoomService } from './room.service'; 3 | 4 | describe('RoomService', () => { 5 | let service: RoomService; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [RoomService], 10 | }).compile(); 11 | 12 | service = module.get(RoomService); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(service).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /api/src/chat/service/room-service/room.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { InjectRepository } from '@nestjs/typeorm'; 3 | import { IPaginationOptions, paginate, Pagination } from 'nestjs-typeorm-paginate'; 4 | import { RoomEntity } from 'src/chat/model/room/room.entity'; 5 | import { RoomI } from 'src/chat/model/room/room.interface'; 6 | import { UserI } from 'src/user/model/user.interface'; 7 | import { Repository } from 'typeorm'; 8 | 9 | @Injectable() 10 | export class RoomService { 11 | 12 | 13 | constructor( 14 | @InjectRepository(RoomEntity) 15 | private readonly roomRepository: Repository 16 | ) { } 17 | 18 | async createRoom(room: RoomI, creator: UserI): Promise { 19 | const newRoom = await this.addCreatorToRoom(room, creator); 20 | return this.roomRepository.save(newRoom); 21 | } 22 | 23 | async getRoom(roomId: number): Promise { 24 | return this.roomRepository.findOne(roomId, { 25 | relations: ['users'] 26 | }); 27 | } 28 | 29 | async getRoomsForUser(userId: number, options: IPaginationOptions): Promise> { 30 | const query = this.roomRepository 31 | .createQueryBuilder('room') 32 | .leftJoin('room.users', 'users') 33 | .where('users.id = :userId', { userId }) 34 | .leftJoinAndSelect('room.users', 'all_users') 35 | .orderBy('room.updated_at', 'DESC'); 36 | 37 | return paginate(query, options); 38 | } 39 | 40 | async addCreatorToRoom(room: RoomI, creator: UserI): Promise { 41 | room.users.push(creator); 42 | return room; 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /api/src/main.ts: -------------------------------------------------------------------------------- 1 | import { ValidationPipe } from '@nestjs/common'; 2 | import { NestFactory } from '@nestjs/core'; 3 | import { AppModule } from './app.module'; 4 | 5 | async function bootstrap() { 6 | const app = await NestFactory.create(AppModule); 7 | app.setGlobalPrefix('api'); 8 | app.useGlobalPipes(new ValidationPipe()); 9 | await app.listen(3000); 10 | } 11 | bootstrap(); 12 | -------------------------------------------------------------------------------- /api/src/middleware/auth.middleware.ts: -------------------------------------------------------------------------------- 1 | import { HttpException, HttpStatus, Injectable, NestMiddleware } from '@nestjs/common'; 2 | import { Request, Response, NextFunction } from 'express'; 3 | import { AuthService } from 'src/auth/service/auth.service'; 4 | import { UserI } from 'src/user/model/user.interface'; 5 | import { UserService } from 'src/user/service/user-service/user.service'; 6 | 7 | export interface RequestModel extends Request { 8 | user: UserI 9 | } 10 | 11 | 12 | @Injectable() 13 | export class AuthMiddleware implements NestMiddleware { 14 | 15 | constructor(private authService: AuthService, private userService: UserService) { } 16 | 17 | async use(req: RequestModel, res: Response, next: NextFunction) { 18 | try { 19 | const tokenArray: string[] = req.headers['authorization'].split(' '); 20 | const decodedToken = await this.authService.verifyJwt(tokenArray[1]); 21 | 22 | // make sure that the user is not deleted, or that props or rights changed compared to the time when the jwt was issued 23 | const user: UserI = await this.userService.getOne(decodedToken.user.id); 24 | if (user) { 25 | // add the user to our req object, so that we can access it later when we need it 26 | // if it would be here, we would like overwrite 27 | req.user = user; 28 | next(); 29 | } else { 30 | throw new HttpException('Unauthorized', HttpStatus.UNAUTHORIZED); 31 | } 32 | } catch { 33 | throw new HttpException('Unauthorized', HttpStatus.UNAUTHORIZED); 34 | } 35 | } 36 | 37 | } -------------------------------------------------------------------------------- /api/src/user/controller/user.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { UserController } from './user.controller'; 3 | 4 | describe('UserController', () => { 5 | let controller: UserController; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | controllers: [UserController], 10 | }).compile(); 11 | 12 | controller = module.get(UserController); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(controller).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /api/src/user/controller/user.controller.ts: -------------------------------------------------------------------------------- 1 | import { Body, Controller, Get, Post, Query, Req, UseGuards } from '@nestjs/common'; 2 | import { Pagination } from 'nestjs-typeorm-paginate'; 3 | import { CreateUserDto } from '../model/dto/create-user.dto'; 4 | import { LoginUserDto } from '../model/dto/login-user.dto'; 5 | import { LoginResponseI } from '../model/login-response.interface'; 6 | import { UserI } from '../model/user.interface'; 7 | import { UserHelperService } from '../service/user-helper/user-helper.service'; 8 | import { UserService } from '../service/user-service/user.service'; 9 | 10 | @Controller('users') 11 | export class UserController { 12 | 13 | constructor( 14 | private userService: UserService, 15 | private userHelperService: UserHelperService 16 | ) { } 17 | 18 | @Post() 19 | async create(@Body() createUserDto: CreateUserDto): Promise { 20 | const userEntity: UserI = this.userHelperService.createUserDtoToEntity(createUserDto); 21 | return this.userService.create(userEntity); 22 | } 23 | 24 | @Get() 25 | async findAll(@Query('page') page: number = 1, @Query('limit') limit: number = 10): Promise> { 26 | limit = limit > 100 ? 100 : limit; 27 | return this.userService.findAll({ page, limit, route: 'http://localhost:3000/api/users' }); 28 | } 29 | 30 | @Get('/find-by-username') 31 | async findAllByUsername(@Query('username') username: string) { 32 | return this.userService.findAllByUsername(username); 33 | } 34 | 35 | 36 | 37 | @Post('login') 38 | async login(@Body() loginUserDto: LoginUserDto): Promise { 39 | const userEntity: UserI = this.userHelperService.loginUserDtoToEntity(loginUserDto); 40 | const jwt: string = await this.userService.login(userEntity); 41 | return { 42 | access_token: jwt, 43 | token_type: 'JWT', 44 | expires_in: 10000 45 | }; 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /api/src/user/model/dto/create-user.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsNotEmpty, IsString } from "class-validator"; 2 | import { LoginUserDto } from "./login-user.dto"; 3 | 4 | 5 | export class CreateUserDto extends LoginUserDto { 6 | 7 | @IsString() 8 | @IsNotEmpty() 9 | username: string; 10 | 11 | } -------------------------------------------------------------------------------- /api/src/user/model/dto/login-user.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsEmail, IsNotEmpty } from "class-validator"; 2 | 3 | export class LoginUserDto { 4 | 5 | @IsEmail() 6 | email: string; 7 | 8 | @IsNotEmpty() 9 | password: string; 10 | 11 | } -------------------------------------------------------------------------------- /api/src/user/model/login-response.interface.ts: -------------------------------------------------------------------------------- 1 | export interface LoginResponseI { 2 | access_token: string; 3 | token_type: string; 4 | expires_in: number; 5 | } -------------------------------------------------------------------------------- /api/src/user/model/user.entity.ts: -------------------------------------------------------------------------------- 1 | import { ConnectedUserEntity } from "src/chat/model/connected-user/connected-user.entity"; 2 | import { JoinedRoomEntity } from "src/chat/model/joined-room/joined-room.entity"; 3 | import { MessageEntity } from "src/chat/model/message/message.entity"; 4 | import { RoomEntity } from "src/chat/model/room/room.entity"; 5 | import { BeforeInsert, BeforeUpdate, Column, Entity, ManyToMany, OneToMany, PrimaryGeneratedColumn } from "typeorm"; 6 | 7 | @Entity() 8 | export class UserEntity { 9 | 10 | @PrimaryGeneratedColumn() 11 | id: number; 12 | 13 | @Column({unique: true}) 14 | username: string; 15 | 16 | @Column({unique: true}) 17 | email: string; 18 | 19 | @Column({select: false}) 20 | password: string; 21 | 22 | @ManyToMany(() => RoomEntity, room => room.users) 23 | rooms: RoomEntity[] 24 | 25 | @OneToMany(() => ConnectedUserEntity, connection => connection.user) 26 | connections: ConnectedUserEntity[]; 27 | 28 | @OneToMany(() => JoinedRoomEntity, joinedRoom => joinedRoom.room) 29 | joinedRooms: JoinedRoomEntity[]; 30 | 31 | @OneToMany(() => MessageEntity, message => message.user) 32 | messages: MessageEntity[]; 33 | 34 | @BeforeInsert() 35 | @BeforeUpdate() 36 | emailToLowerCase() { 37 | this.email = this.email.toLowerCase(); 38 | this.username = this.username.toLowerCase(); 39 | } 40 | 41 | } -------------------------------------------------------------------------------- /api/src/user/model/user.interface.ts: -------------------------------------------------------------------------------- 1 | export interface UserI { 2 | id?: number; 3 | username?: string; 4 | email: string; 5 | password?: string; 6 | } -------------------------------------------------------------------------------- /api/src/user/service/user-helper/user-helper.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { UserHelperService } from './user-helper.service'; 3 | 4 | describe('UserHelperService', () => { 5 | let service: UserHelperService; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [UserHelperService], 10 | }).compile(); 11 | 12 | service = module.get(UserHelperService); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(service).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /api/src/user/service/user-helper/user-helper.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { CreateUserDto } from 'src/user/model/dto/create-user.dto'; 3 | import { LoginUserDto } from 'src/user/model/dto/login-user.dto'; 4 | import { UserI } from 'src/user/model/user.interface'; 5 | 6 | @Injectable() 7 | export class UserHelperService { 8 | 9 | createUserDtoToEntity(createUserDto: CreateUserDto): UserI { 10 | return { 11 | email: createUserDto.email, 12 | username: createUserDto.username, 13 | password: createUserDto.password 14 | }; 15 | } 16 | 17 | loginUserDtoToEntity(loginUserDto: LoginUserDto): UserI { 18 | return { 19 | email: loginUserDto.email, 20 | password: loginUserDto.password 21 | }; 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /api/src/user/service/user-service/user.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { UserService } from './user.service'; 3 | 4 | describe('UserService', () => { 5 | let service: UserService; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [UserService], 10 | }).compile(); 11 | 12 | service = module.get(UserService); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(service).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /api/src/user/service/user-service/user.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; 2 | import { InjectRepository } from '@nestjs/typeorm'; 3 | import { UserEntity } from 'src/user/model/user.entity'; 4 | import { UserI } from 'src/user/model/user.interface'; 5 | import { Like, Repository } from 'typeorm'; 6 | import { IPaginationOptions, paginate, Pagination } from 'nestjs-typeorm-paginate'; 7 | import { AuthService } from 'src/auth/service/auth.service'; 8 | 9 | @Injectable() 10 | export class UserService { 11 | 12 | constructor( 13 | @InjectRepository(UserEntity) 14 | private readonly userRepository: Repository, 15 | private authService: AuthService 16 | ) { } 17 | 18 | async create(newUser: UserI): Promise { 19 | try { 20 | const exists: boolean = await this.mailExists(newUser.email); 21 | if (!exists) { 22 | const passwordHash: string = await this.hashPassword(newUser.password); 23 | newUser.password = passwordHash; 24 | const user = await this.userRepository.save(this.userRepository.create(newUser)); 25 | return this.findOne(user.id); 26 | } else { 27 | throw new HttpException('Email is already in use', HttpStatus.CONFLICT); 28 | } 29 | } catch { 30 | throw new HttpException('Email is already in use', HttpStatus.CONFLICT); 31 | } 32 | } 33 | 34 | async login(user: UserI): Promise { 35 | try { 36 | const foundUser: UserI = await this.findByEmail(user.email.toLowerCase()); 37 | if (foundUser) { 38 | const matches: boolean = await this.validatePassword(user.password, foundUser.password); 39 | if (matches) { 40 | const payload: UserI = await this.findOne(foundUser.id); 41 | return this.authService.generateJwt(payload); 42 | } else { 43 | throw new HttpException('Login was not successfull, wrong credentials', HttpStatus.UNAUTHORIZED); 44 | } 45 | } else { 46 | throw new HttpException('Login was not successfull, wrong credentials', HttpStatus.UNAUTHORIZED); 47 | } 48 | } catch { 49 | throw new HttpException('User not found', HttpStatus.NOT_FOUND); 50 | } 51 | } 52 | 53 | async findAll(options: IPaginationOptions): Promise> { 54 | return paginate(this.userRepository, options); 55 | } 56 | 57 | async findAllByUsername(username: string): Promise { 58 | return this.userRepository.find({ 59 | where: { 60 | username: Like(`%${username.toLowerCase()}%`) 61 | } 62 | }) 63 | } 64 | 65 | // also returns the password 66 | private async findByEmail(email: string): Promise { 67 | return this.userRepository.findOne({ email }, { select: ['id', 'email', 'username', 'password'] }); 68 | } 69 | 70 | private async hashPassword(password: string): Promise { 71 | return this.authService.hashPassword(password); 72 | } 73 | 74 | private async validatePassword(password: string, storedPasswordHash: string): Promise { 75 | return this.authService.comparePasswords(password, storedPasswordHash); 76 | } 77 | 78 | private async findOne(id: number): Promise { 79 | return this.userRepository.findOne({ id }); 80 | } 81 | 82 | public getOne(id: number): Promise { 83 | return this.userRepository.findOneOrFail({ id }); 84 | } 85 | 86 | private async mailExists(email: string): Promise { 87 | const user = await this.userRepository.findOne({ email }); 88 | if (user) { 89 | return true; 90 | } else { 91 | return false; 92 | } 93 | } 94 | 95 | } 96 | -------------------------------------------------------------------------------- /api/src/user/user.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { TypeOrmModule } from '@nestjs/typeorm'; 3 | import { AuthModule } from 'src/auth/auth.module'; 4 | import { UserController } from './controller/user.controller'; 5 | import { UserEntity } from './model/user.entity'; 6 | import { UserHelperService } from './service/user-helper/user-helper.service'; 7 | import { UserService } from './service/user-service/user.service'; 8 | 9 | @Module({ 10 | imports: [ 11 | TypeOrmModule.forFeature([UserEntity]), 12 | AuthModule 13 | ], 14 | controllers: [UserController], 15 | providers: [UserService, UserHelperService], 16 | exports: [UserService] 17 | }) 18 | export class UserModule {} 19 | -------------------------------------------------------------------------------- /api/test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { INestApplication } from '@nestjs/common'; 3 | import * as request from 'supertest'; 4 | import { AppModule } from './../src/app.module'; 5 | 6 | describe('AppController (e2e)', () => { 7 | let app: INestApplication; 8 | 9 | beforeEach(async () => { 10 | const moduleFixture: TestingModule = await Test.createTestingModule({ 11 | imports: [AppModule], 12 | }).compile(); 13 | 14 | app = moduleFixture.createNestApplication(); 15 | await app.init(); 16 | }); 17 | 18 | it('/ (GET)', () => { 19 | return request(app.getHttpServer()) 20 | .get('/') 21 | .expect(200) 22 | .expect('Hello World!'); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /api/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 | -------------------------------------------------------------------------------- /api/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /api/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": "es2017", 10 | "sourceMap": true, 11 | "outDir": "./dist", 12 | "baseUrl": "./", 13 | "incremental": true 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.8" 2 | 3 | services: 4 | # Our NestJS Api 5 | api: 6 | build: 7 | dockerfile: Dockerfile 8 | context: ./api 9 | # Only build development stage from Dockerfile 10 | target: development 11 | # Mount our host dir to the docker container 12 | # Mount api directory (./api) to (:) docker container (/thomas/src/app) 13 | # Reflect File changes from host to container 14 | volumes: 15 | - ./api:/thomas/src/app 16 | - /thomas/src/app/node_modules/ 17 | # RUN in debug mode: npm run start:debug --> Also start your vscode debugger 18 | # Run in dev mode: npm run start:dev 19 | command: npm run start:debug 20 | depends_on: 21 | - postgres 22 | environment: 23 | DATABASE_URL: postgres://user:password@postgres:5432/db 24 | NODE_ENV: development 25 | JWT_SECRET: hard_to_guess_secret_123 26 | PORT: 3000 27 | ports: 28 | - 3000:3000 29 | - 9229:9229 30 | 31 | # Our Angular Frontend 32 | frontend: 33 | build: 34 | dockerfile: Dockerfile 35 | context: ./frontend 36 | target: development 37 | command: npm run start 38 | volumes: 39 | - ./frontend:/thomas/frontend/src/app 40 | - /thomas/frontend/src/app/node_modules 41 | ports: 42 | - 4200:4200 43 | links: 44 | - api 45 | 46 | # Our Postgres Database for NestJS to connect to 47 | postgres: 48 | image: postgres:10.4 49 | environment: 50 | POSTGRES_USER: user 51 | POSTGRES_PASSWORD: password 52 | POSTGRES_DB: db 53 | ports: 54 | - 35000:5432 55 | 56 | # The Postgres Admin tool if we want to run some custom queries and so on against our Database 57 | postgres_admin: 58 | image: dpage/pgadmin4:4.28 59 | depends_on: 60 | - postgres 61 | environment: 62 | PGADMIN_DEFAULT_EMAIL: admin@admin.com 63 | PGADMIN_DEFAULT_PASSWORD: password 64 | ports: 65 | - 5050:80 66 | 67 | 68 | -------------------------------------------------------------------------------- /frontend/.browserslistrc: -------------------------------------------------------------------------------- 1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | 5 | # For the full list of supported browsers by the Angular framework, please see: 6 | # https://angular.io/guide/browser-support 7 | 8 | # You can see what browsers were selected by your queries by running: 9 | # npx browserslist 10 | 11 | last 1 Chrome version 12 | last 1 Firefox version 13 | last 2 Edge major versions 14 | last 2 Safari major versions 15 | last 2 iOS major versions 16 | Firefox ESR 17 | not IE 11 # Angular supports IE 11 only as an opt-in. To opt-in, remove the 'not' prefix on this line. 18 | -------------------------------------------------------------------------------- /frontend/.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.ts] 12 | quote_type = single 13 | 14 | [*.md] 15 | max_line_length = off 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | # Only exists if Bazel was run 8 | /bazel-out 9 | 10 | # dependencies 11 | /node_modules 12 | 13 | # profiling files 14 | chrome-profiler-events*.json 15 | 16 | # IDEs and editors 17 | /.idea 18 | .project 19 | .classpath 20 | .c9/ 21 | *.launch 22 | .settings/ 23 | *.sublime-workspace 24 | 25 | # IDE - VSCode 26 | .vscode/* 27 | !.vscode/settings.json 28 | !.vscode/tasks.json 29 | !.vscode/launch.json 30 | !.vscode/extensions.json 31 | .history/* 32 | 33 | # misc 34 | /.sass-cache 35 | /connect.lock 36 | /coverage 37 | /libpeerconnection.log 38 | npm-debug.log 39 | yarn-error.log 40 | testem.log 41 | /typings 42 | 43 | # System Files 44 | .DS_Store 45 | Thumbs.db 46 | -------------------------------------------------------------------------------- /frontend/Dockerfile: -------------------------------------------------------------------------------- 1 | 2 | # Stage 1: build 3 | FROM node:14 AS development 4 | 5 | WORKDIR /thomas/frontend/src/app 6 | 7 | COPY package*.json ./ 8 | 9 | RUN npm install 10 | RUN npm install -g @angular/cli@12.0.0 11 | 12 | COPY . . 13 | 14 | RUN npm run build 15 | 16 | EXPOSE 4200 -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | # Frontend 2 | 3 | This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 12.0.0. 4 | 5 | ## Development server 6 | 7 | Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files. 8 | 9 | ## Code scaffolding 10 | 11 | Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. 12 | 13 | ## Build 14 | 15 | Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build. 16 | 17 | ## Running unit tests 18 | 19 | Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). 20 | 21 | ## Running end-to-end tests 22 | 23 | Run `ng e2e` to execute the end-to-end tests via a platform of your choice. 24 | 25 | ## Further help 26 | 27 | To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page. 28 | -------------------------------------------------------------------------------- /frontend/angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "frontend": { 7 | "projectType": "application", 8 | "schematics": { 9 | "@schematics/angular:component": { 10 | "style": "scss" 11 | }, 12 | "@schematics/angular:application": { 13 | "strict": true 14 | } 15 | }, 16 | "root": "", 17 | "sourceRoot": "src", 18 | "prefix": "app", 19 | "architect": { 20 | "build": { 21 | "builder": "@angular-devkit/build-angular:browser", 22 | "options": { 23 | "outputPath": "dist/frontend", 24 | "index": "src/index.html", 25 | "main": "src/main.ts", 26 | "polyfills": "src/polyfills.ts", 27 | "tsConfig": "tsconfig.app.json", 28 | "inlineStyleLanguage": "scss", 29 | "assets": [ 30 | "src/favicon.ico", 31 | "src/assets" 32 | ], 33 | "styles": [ 34 | "./node_modules/@angular/material/prebuilt-themes/indigo-pink.css", 35 | "src/styles.scss" 36 | ], 37 | "scripts": [] 38 | }, 39 | "configurations": { 40 | "production": { 41 | "budgets": [ 42 | { 43 | "type": "initial", 44 | "maximumWarning": "500kb", 45 | "maximumError": "1mb" 46 | }, 47 | { 48 | "type": "anyComponentStyle", 49 | "maximumWarning": "2kb", 50 | "maximumError": "4kb" 51 | } 52 | ], 53 | "fileReplacements": [ 54 | { 55 | "replace": "src/environments/environment.ts", 56 | "with": "src/environments/environment.prod.ts" 57 | } 58 | ], 59 | "outputHashing": "all" 60 | }, 61 | "development": { 62 | "buildOptimizer": false, 63 | "optimization": false, 64 | "vendorChunk": true, 65 | "extractLicenses": false, 66 | "sourceMap": true, 67 | "namedChunks": true 68 | } 69 | }, 70 | "defaultConfiguration": "production" 71 | }, 72 | "serve": { 73 | "builder": "@angular-devkit/build-angular:dev-server", 74 | "configurations": { 75 | "production": { 76 | "browserTarget": "frontend:build:production" 77 | }, 78 | "development": { 79 | "browserTarget": "frontend:build:development", 80 | "proxyConfig": "src/proxy.conf.json" 81 | } 82 | }, 83 | "defaultConfiguration": "development" 84 | }, 85 | "extract-i18n": { 86 | "builder": "@angular-devkit/build-angular:extract-i18n", 87 | "options": { 88 | "browserTarget": "frontend:build" 89 | } 90 | }, 91 | "test": { 92 | "builder": "@angular-devkit/build-angular:karma", 93 | "options": { 94 | "main": "src/test.ts", 95 | "polyfills": "src/polyfills.ts", 96 | "tsConfig": "tsconfig.spec.json", 97 | "karmaConfig": "karma.conf.js", 98 | "inlineStyleLanguage": "scss", 99 | "assets": [ 100 | "src/favicon.ico", 101 | "src/assets" 102 | ], 103 | "styles": [ 104 | "./node_modules/@angular/material/prebuilt-themes/indigo-pink.css", 105 | "src/styles.scss" 106 | ], 107 | "scripts": [] 108 | } 109 | } 110 | } 111 | } 112 | }, 113 | "defaultProject": "frontend" 114 | } 115 | -------------------------------------------------------------------------------- /frontend/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | jasmine: { 17 | // you can add configuration options for Jasmine here 18 | // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html 19 | // for example, you can disable the random execution with `random: false` 20 | // or set a specific seed with `seed: 4321` 21 | }, 22 | clearContext: false // leave Jasmine Spec Runner output visible in browser 23 | }, 24 | jasmineHtmlReporter: { 25 | suppressAll: true // removes the duplicated traces 26 | }, 27 | coverageReporter: { 28 | dir: require('path').join(__dirname, './coverage/frontend'), 29 | subdir: '.', 30 | reporters: [ 31 | { type: 'html' }, 32 | { type: 'text-summary' } 33 | ] 34 | }, 35 | reporters: ['progress', 'kjhtml'], 36 | port: 9876, 37 | colors: true, 38 | logLevel: config.LOG_INFO, 39 | autoWatch: true, 40 | browsers: ['Chrome'], 41 | singleRun: false, 42 | restartOnFileChange: true 43 | }); 44 | }; 45 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve --host=0.0.0.0 --port 4200", 7 | "build": "ng build", 8 | "watch": "ng build --watch --configuration development", 9 | "test": "ng test" 10 | }, 11 | "private": true, 12 | "dependencies": { 13 | "@angular/animations": "~12.0.0", 14 | "@angular/cdk": "~12.0.1", 15 | "@angular/common": "~12.0.0", 16 | "@angular/compiler": "~12.0.0", 17 | "@angular/core": "~12.0.0", 18 | "@angular/forms": "~12.0.0", 19 | "@angular/material": "~12.0.1", 20 | "@angular/platform-browser": "~12.0.0", 21 | "@angular/platform-browser-dynamic": "~12.0.0", 22 | "@angular/router": "~12.0.0", 23 | "@auth0/angular-jwt": "^5.0.2", 24 | "ngx-socket-io": "^4.1.0", 25 | "rxjs": "~6.6.0", 26 | "tslib": "^2.1.0", 27 | "zone.js": "~0.11.4" 28 | }, 29 | "devDependencies": { 30 | "@angular-devkit/build-angular": "~12.0.0", 31 | "@angular/cli": "~12.0.0", 32 | "@angular/compiler-cli": "~12.0.0", 33 | "@types/jasmine": "~3.6.0", 34 | "@types/node": "^12.11.1", 35 | "jasmine-core": "~3.7.0", 36 | "karma": "~6.3.0", 37 | "karma-chrome-launcher": "~3.1.0", 38 | "karma-coverage": "~2.0.3", 39 | "karma-jasmine": "~4.0.0", 40 | "karma-jasmine-html-reporter": "^1.5.0", 41 | "typescript": "~4.2.3" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /frontend/src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | import { AuthGuard } from './guards/auth.guard'; 4 | 5 | const routes: Routes = [ 6 | { 7 | path: 'private', 8 | canActivate: [AuthGuard], 9 | loadChildren: () => import('./private/private.module').then(m => m.PrivateModule) 10 | }, 11 | { 12 | path: 'public', 13 | loadChildren: () => import('./public/public.module').then(m => m.PublicModule) 14 | }, 15 | { 16 | path: '**', 17 | redirectTo: 'public', 18 | pathMatch: 'full' 19 | } 20 | ]; 21 | 22 | @NgModule({ 23 | imports: [RouterModule.forRoot(routes)], 24 | exports: [RouterModule] 25 | }) 26 | export class AppRoutingModule { } 27 | -------------------------------------------------------------------------------- /frontend/src/app/app.component.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/app/app.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TommiCodes/real-time-chat-nestjs-angular/726aa89bced98a1adcc9843b42d57945a51b63e0/frontend/src/app/app.component.scss -------------------------------------------------------------------------------- /frontend/src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | import { RouterTestingModule } from '@angular/router/testing'; 3 | import { AppComponent } from './app.component'; 4 | 5 | describe('AppComponent', () => { 6 | beforeEach(async () => { 7 | await TestBed.configureTestingModule({ 8 | imports: [ 9 | RouterTestingModule 10 | ], 11 | declarations: [ 12 | AppComponent 13 | ], 14 | }).compileComponents(); 15 | }); 16 | 17 | it('should create the app', () => { 18 | const fixture = TestBed.createComponent(AppComponent); 19 | const app = fixture.componentInstance; 20 | expect(app).toBeTruthy(); 21 | }); 22 | 23 | it(`should have as title 'frontend'`, () => { 24 | const fixture = TestBed.createComponent(AppComponent); 25 | const app = fixture.componentInstance; 26 | expect(app.title).toEqual('frontend'); 27 | }); 28 | 29 | it('should render title', () => { 30 | const fixture = TestBed.createComponent(AppComponent); 31 | fixture.detectChanges(); 32 | const compiled = fixture.nativeElement; 33 | expect(compiled.querySelector('.content span').textContent).toContain('frontend app is running!'); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /frontend/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { Observable } from 'rxjs'; 3 | import { Test, TestService } from './services/test-service/test.service'; 4 | 5 | @Component({ 6 | selector: 'app-root', 7 | templateUrl: './app.component.html', 8 | styleUrls: ['./app.component.scss'] 9 | }) 10 | export class AppComponent { 11 | title = 'frontend'; 12 | 13 | testValue: Observable = this.service.getTest(); 14 | 15 | constructor(private service: TestService) {} 16 | } 17 | -------------------------------------------------------------------------------- /frontend/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { HttpClientModule } from '@angular/common/http'; 2 | import { NgModule } from '@angular/core'; 3 | import { BrowserModule } from '@angular/platform-browser'; 4 | 5 | import { AppRoutingModule } from './app-routing.module'; 6 | import { AppComponent } from './app.component'; 7 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 8 | 9 | import { MatSnackBarModule } from '@angular/material/snack-bar'; 10 | import { JwtModule } from '@auth0/angular-jwt'; 11 | 12 | export function tokenGetter() { 13 | return localStorage.getItem("nestjs_chat_app"); 14 | } 15 | 16 | @NgModule({ 17 | declarations: [ 18 | AppComponent 19 | ], 20 | imports: [ 21 | BrowserModule, 22 | AppRoutingModule, 23 | HttpClientModule, 24 | BrowserAnimationsModule, 25 | MatSnackBarModule, 26 | JwtModule.forRoot({ 27 | config: { 28 | tokenGetter: tokenGetter, 29 | allowedDomains: ['localhost:3000'] 30 | } 31 | }) 32 | ], 33 | providers: [], 34 | bootstrap: [AppComponent] 35 | }) 36 | export class AppModule { } 37 | -------------------------------------------------------------------------------- /frontend/src/app/guards/auth.guard.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { AuthGuard } from './auth.guard'; 4 | 5 | describe('AuthGuard', () => { 6 | let guard: AuthGuard; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | guard = TestBed.inject(AuthGuard); 11 | }); 12 | 13 | it('should be created', () => { 14 | expect(guard).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /frontend/src/app/guards/auth.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot, UrlTree } from '@angular/router'; 3 | import { JwtHelperService } from '@auth0/angular-jwt'; 4 | import { Observable } from 'rxjs'; 5 | 6 | @Injectable({ 7 | providedIn: 'root' 8 | }) 9 | export class AuthGuard implements CanActivate { 10 | 11 | constructor(private router: Router, private jwtService: JwtHelperService) { } 12 | 13 | canActivate( 14 | route: ActivatedRouteSnapshot, 15 | state: RouterStateSnapshot): Observable | Promise | boolean | UrlTree { 16 | 17 | if (this.jwtService.isTokenExpired()) { 18 | this.router.navigate(['']); 19 | return false; 20 | } else { 21 | return true; 22 | } 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /frontend/src/app/model/login-response.interface.ts: -------------------------------------------------------------------------------- 1 | export interface LoginResponseI { 2 | access_token: string; 3 | token_type: string; 4 | expires_in: number; 5 | } -------------------------------------------------------------------------------- /frontend/src/app/model/message.interface.ts: -------------------------------------------------------------------------------- 1 | import { Meta } from "@angular/platform-browser"; 2 | import { RoomI } from "./room.interface"; 3 | import { UserI } from "./user.interface"; 4 | 5 | export interface MessageI { 6 | id?: number; 7 | text: string; 8 | user?: UserI; 9 | room: RoomI; 10 | created_at?: Date; 11 | updated_at?: Date; 12 | } 13 | 14 | export interface MessagePaginateI { 15 | items: MessageI[]; 16 | meta: Meta; 17 | } -------------------------------------------------------------------------------- /frontend/src/app/model/meta.interface.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface Meta { 3 | totalItems: number; 4 | itemCount: number; 5 | itemsPerPage: number; 6 | totalPages: number; 7 | currentPage: number; 8 | } -------------------------------------------------------------------------------- /frontend/src/app/model/room.interface.ts: -------------------------------------------------------------------------------- 1 | import { Meta } from "./meta.interface"; 2 | import { UserI } from "./user.interface"; 3 | 4 | 5 | export interface RoomI { 6 | id?: number; 7 | name?: string; 8 | description?: string; 9 | users?: UserI[]; 10 | created_at?: Date; 11 | updated_at?: Date; 12 | } 13 | 14 | export interface RoomPaginateI { 15 | items: RoomI[]; 16 | meta: Meta; 17 | } -------------------------------------------------------------------------------- /frontend/src/app/model/user.interface.ts: -------------------------------------------------------------------------------- 1 | export interface UserI { 2 | id?: number; 3 | email?: string; 4 | username?: string; 5 | password?: string; 6 | } -------------------------------------------------------------------------------- /frontend/src/app/private/components/chat-message/chat-message.component.html: -------------------------------------------------------------------------------- 1 | 2 |

3 | {{message.user.username}}
{{message.text}} 4 |

5 | 6 |
-------------------------------------------------------------------------------- /frontend/src/app/private/components/chat-message/chat-message.component.scss: -------------------------------------------------------------------------------- 1 | p { 2 | font-size: medium; 3 | background-color: lightgray; 4 | padding: 5px; 5 | width: fit-content; 6 | border-radius: 5px; 7 | margin: 7px; 8 | } 9 | 10 | .ownContainer { 11 | justify-content: flex-end; 12 | } 13 | 14 | .ownMessage { 15 | background-color: lightblue; 16 | } 17 | -------------------------------------------------------------------------------- /frontend/src/app/private/components/chat-message/chat-message.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ChatMessageComponent } from './chat-message.component'; 4 | 5 | describe('ChatMessageComponent', () => { 6 | let component: ChatMessageComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ ChatMessageComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ChatMessageComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /frontend/src/app/private/components/chat-message/chat-message.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, OnInit } from '@angular/core'; 2 | import { MessageI } from 'src/app/model/message.interface'; 3 | import { UserI } from 'src/app/model/user.interface'; 4 | import { AuthService } from 'src/app/public/services/auth-service/auth.service'; 5 | 6 | @Component({ 7 | selector: 'app-chat-message', 8 | templateUrl: './chat-message.component.html', 9 | styleUrls: ['./chat-message.component.scss'] 10 | }) 11 | export class ChatMessageComponent { 12 | 13 | @Input() message: MessageI; 14 | user: UserI = this.authService.getLoggedInUser(); 15 | 16 | constructor(private authService: AuthService) { } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /frontend/src/app/private/components/chat-room/chat-room.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |
5 | 6 |

{{chatRoom.name}}

7 | 8 | event 9 |

{{chatRoom.updated_at | date}}

10 |
11 |
12 | 13 |

{{chatRoom.description}}

14 | {{user.username}}, 15 |
16 |
17 | 18 | 19 |
20 | 21 | 22 | 23 |
24 | 25 | 26 |
27 | 28 | 29 | 30 | 36 | 37 |
38 |
39 | 40 | 41 | 42 | 43 | 44 | No Chatroom Selected 45 | -------------------------------------------------------------------------------- /frontend/src/app/private/components/chat-room/chat-room.component.scss: -------------------------------------------------------------------------------- 1 | h3 { 2 | padding: 0px; 3 | padding-top: -10px; 4 | margin: 0px; 5 | } 6 | 7 | .message { 8 | background-color: rgb(236, 236, 236); 9 | height: 400px; 10 | overflow-y: scroll; 11 | scrollbar-color: lightgrey darkgrey; 12 | scrollbar-width: thin; 13 | } 14 | 15 | ::-webkit-scrollbar-track { 16 | -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3); 17 | box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3); 18 | border-radius: 10px; 19 | background-color: lightgray; 20 | } 21 | 22 | ::-webkit-scrollbar { 23 | width: 12px; 24 | background-color: lightgray; 25 | } 26 | 27 | ::-webkit-scrollbar-thumb { 28 | border-radius: 10px; 29 | -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3); 30 | box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3); 31 | background-color: lightgray; 32 | } -------------------------------------------------------------------------------- /frontend/src/app/private/components/chat-room/chat-room.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ChatRoomComponent } from './chat-room.component'; 4 | 5 | describe('ChatRoomComponent', () => { 6 | let component: ChatRoomComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ ChatRoomComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ChatRoomComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /frontend/src/app/private/components/chat-room/chat-room.component.ts: -------------------------------------------------------------------------------- 1 | import { AfterViewInit, Component, ElementRef, Input, OnChanges, OnDestroy, OnInit, SimpleChanges, ViewChild } from '@angular/core'; 2 | import { FormControl, Validators } from '@angular/forms'; 3 | import { combineLatest, Observable } from 'rxjs'; 4 | import { map, startWith, tap } from 'rxjs/operators'; 5 | import { MessagePaginateI } from 'src/app/model/message.interface'; 6 | import { RoomI } from 'src/app/model/room.interface'; 7 | import { ChatService } from '../../services/chat-service/chat.service'; 8 | 9 | @Component({ 10 | selector: 'app-chat-room', 11 | templateUrl: './chat-room.component.html', 12 | styleUrls: ['./chat-room.component.scss'] 13 | }) 14 | export class ChatRoomComponent implements OnChanges, OnDestroy, AfterViewInit { 15 | 16 | @Input() chatRoom: RoomI; 17 | @ViewChild('messages') private messagesScroller: ElementRef; 18 | 19 | messagesPaginate$: Observable = combineLatest([this.chatService.getMessages(), this.chatService.getAddedMessage().pipe(startWith(null))]).pipe( 20 | map(([messagePaginate, message]) => { 21 | if (message && message.room.id === this.chatRoom.id && !messagePaginate.items.some(m => m.id === message.id)) { 22 | messagePaginate.items.push(message); 23 | } 24 | const items = messagePaginate.items.sort((a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime()); 25 | messagePaginate.items = items; 26 | return messagePaginate; 27 | }), 28 | tap(() => this.scrollToBottom()) 29 | ) 30 | 31 | chatMessage: FormControl = new FormControl(null, [Validators.required]); 32 | 33 | constructor(private chatService: ChatService) { } 34 | 35 | ngOnChanges(changes: SimpleChanges) { 36 | this.chatService.leaveRoom(changes['chatRoom'].previousValue); 37 | if (this.chatRoom) { 38 | this.chatService.joinRoom(this.chatRoom); 39 | } 40 | } 41 | 42 | ngAfterViewInit() { 43 | if (this.messagesScroller) { 44 | this.scrollToBottom(); 45 | } 46 | } 47 | 48 | ngOnDestroy() { 49 | this.chatService.leaveRoom(this.chatRoom); 50 | } 51 | 52 | sendMessage() { 53 | this.chatService.sendMessage({ text: this.chatMessage.value, room: this.chatRoom }); 54 | this.chatMessage.reset(); 55 | } 56 | 57 | scrollToBottom(): void { 58 | try { 59 | setTimeout(() => { this.messagesScroller.nativeElement.scrollTop = this.messagesScroller.nativeElement.scrollHeight }, 1); 60 | } catch { } 61 | 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /frontend/src/app/private/components/create-room/create-room.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |
5 | Go to Dashboard 6 |
7 | 8 |
9 | 10 | Create Chatroom 11 | 12 |
13 | 14 | 15 | 16 | Name is required 17 | 18 | 19 | 20 | 21 | 22 | 23 | 25 | 26 | Please fill out all needed information 27 | 28 |
29 | 30 |
31 | 32 |
33 |
34 |
35 |
36 |
37 |
-------------------------------------------------------------------------------- /frontend/src/app/private/components/create-room/create-room.component.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | display: flex; 3 | justify-content: center; 4 | margin: 20px; 5 | min-width: 350px; 6 | 7 | .dashboard { 8 | margin-bottom: 20px; 9 | } 10 | 11 | mat-card { 12 | mat-card-content { 13 | form { 14 | display: flex; 15 | flex-direction: column; 16 | justify-content: center; 17 | 18 | mat-form-field { 19 | width: 100%; 20 | min-width: 300px; 21 | } 22 | 23 | .button { 24 | display: flex; 25 | justify-content: flex-end; 26 | } 27 | } 28 | } 29 | } 30 | 31 | } -------------------------------------------------------------------------------- /frontend/src/app/private/components/create-room/create-room.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { CreateRoomComponent } from './create-room.component'; 4 | 5 | describe('CreateRoomComponent', () => { 6 | let component: CreateRoomComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ CreateRoomComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(CreateRoomComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /frontend/src/app/private/components/create-room/create-room.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { FormArray, FormControl, FormGroup, Validators } from '@angular/forms'; 3 | import { ActivatedRoute, Router } from '@angular/router'; 4 | import { UserI } from 'src/app/model/user.interface'; 5 | import { ChatService } from '../../services/chat-service/chat.service'; 6 | 7 | @Component({ 8 | selector: 'app-create-room', 9 | templateUrl: './create-room.component.html', 10 | styleUrls: ['./create-room.component.scss'] 11 | }) 12 | export class CreateRoomComponent { 13 | 14 | form: FormGroup = new FormGroup({ 15 | name: new FormControl(null, [Validators.required]), 16 | description: new FormControl(null), 17 | users: new FormArray([], [Validators.required]) 18 | }); 19 | 20 | constructor(private chatService: ChatService, private router: Router, private activatedRoute: ActivatedRoute) { } 21 | 22 | create() { 23 | if (this.form.valid) { 24 | this.chatService.createRoom(this.form.getRawValue()); 25 | this.router.navigate(['../dashboard'], { relativeTo: this.activatedRoute }); 26 | } 27 | } 28 | 29 | initUser(user: UserI) { 30 | return new FormControl({ 31 | id: user.id, 32 | username: user.username, 33 | email: user.email 34 | }); 35 | } 36 | 37 | addUser(userFormControl: FormControl) { 38 | this.users.push(userFormControl); 39 | } 40 | 41 | removeUser(userId: number) { 42 | this.users.removeAt(this.users.value.findIndex((user: UserI) => user.id === userId)); 43 | } 44 | 45 | get name(): FormControl { 46 | return this.form.get('name') as FormControl; 47 | } 48 | 49 | get description(): FormControl { 50 | return this.form.get('description') as FormControl; 51 | } 52 | 53 | get users(): FormArray { 54 | return this.form.get('users') as FormArray; 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /frontend/src/app/private/components/dashboard/dashboard.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 |
6 |
7 | 8 |

My Chatrooms

9 | Logged in as {{user.username}} 10 |
11 | 12 |
13 | 14 |
15 | 16 | 17 | Name: {{room.name}} Id: {{room.id}} 18 | 19 | 20 | 21 | 24 |
25 |
26 | 27 |
28 | 29 | 30 |
31 | 32 |
33 |
34 |
35 | 36 | 37 |
38 | No Chatrooms for you so far, please create one or get invited 39 |
40 |
-------------------------------------------------------------------------------- /frontend/src/app/private/components/dashboard/dashboard.component.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | display: flex; 3 | justify-content: center; 4 | 5 | mat-card { 6 | position: fixed; 7 | margin: 20px; 8 | 9 | .rooms { 10 | height: 500px; 11 | width: 250; 12 | overflow-y: scroll; 13 | scrollbar-color: lightgrey darkgrey; 14 | scrollbar-width: thin; 15 | } 16 | .chatroom { 17 | margin-left: 40px; 18 | width: 600px; 19 | } 20 | } 21 | } 22 | 23 | ::-webkit-scrollbar-track { 24 | -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3); 25 | box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3); 26 | border-radius: 10px; 27 | background-color: lightgray; 28 | } 29 | 30 | ::-webkit-scrollbar { 31 | width: 12px; 32 | background-color: lightgray; 33 | } 34 | 35 | ::-webkit-scrollbar-thumb { 36 | border-radius: 10px; 37 | -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3); 38 | box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3); 39 | background-color: lightgray; 40 | } 41 | -------------------------------------------------------------------------------- /frontend/src/app/private/components/dashboard/dashboard.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { DashboardComponent } from './dashboard.component'; 4 | 5 | describe('DashboardComponent', () => { 6 | let component: DashboardComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ DashboardComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(DashboardComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /frontend/src/app/private/components/dashboard/dashboard.component.ts: -------------------------------------------------------------------------------- 1 | import { AfterViewInit, Component, OnInit } from '@angular/core'; 2 | import { MatSelectionListChange } from '@angular/material/list'; 3 | import { PageEvent } from '@angular/material/paginator'; 4 | import { Observable } from 'rxjs'; 5 | import { RoomPaginateI } from 'src/app/model/room.interface'; 6 | import { UserI } from 'src/app/model/user.interface'; 7 | import { AuthService } from 'src/app/public/services/auth-service/auth.service'; 8 | import { ChatService } from '../../services/chat-service/chat.service'; 9 | 10 | @Component({ 11 | selector: 'app-dashboard', 12 | templateUrl: './dashboard.component.html', 13 | styleUrls: ['./dashboard.component.scss'] 14 | }) 15 | export class DashboardComponent implements OnInit, AfterViewInit{ 16 | 17 | rooms$: Observable = this.chatService.getMyRooms(); 18 | selectedRoom = null; 19 | user: UserI = this.authService.getLoggedInUser(); 20 | 21 | constructor(private chatService: ChatService, private authService: AuthService) { } 22 | 23 | ngOnInit() { 24 | this.chatService.emitPaginateRooms(10, 0); 25 | } 26 | 27 | ngAfterViewInit() { 28 | this.chatService.emitPaginateRooms(10, 0); 29 | } 30 | 31 | onSelectRoom(event: MatSelectionListChange) { 32 | this.selectedRoom = event.source.selectedOptions.selected[0].value; 33 | } 34 | 35 | onPaginateRooms(pageEvent: PageEvent) { 36 | this.chatService.emitPaginateRooms(pageEvent.pageSize, pageEvent.pageIndex); 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /frontend/src/app/private/components/select-users/select-users.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | {{user.username}} 8 | | ID: {{user.id}} 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | {{user.username}} 18 | cancel 19 | 20 | 21 | 22 | 23 | No Users added so far 24 | -------------------------------------------------------------------------------- /frontend/src/app/private/components/select-users/select-users.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TommiCodes/real-time-chat-nestjs-angular/726aa89bced98a1adcc9843b42d57945a51b63e0/frontend/src/app/private/components/select-users/select-users.component.scss -------------------------------------------------------------------------------- /frontend/src/app/private/components/select-users/select-users.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { SelectUsersComponent } from './select-users.component'; 4 | 5 | describe('SelectUsersComponent', () => { 6 | let component: SelectUsersComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ SelectUsersComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(SelectUsersComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /frontend/src/app/private/components/select-users/select-users.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, OnInit, Output, EventEmitter } from '@angular/core'; 2 | import { FormControl } from '@angular/forms'; 3 | import { debounceTime, distinctUntilChanged, switchMap, tap } from 'rxjs/operators'; 4 | import { UserI } from 'src/app/model/user.interface'; 5 | import { UserService } from 'src/app/public/services/user-service/user.service'; 6 | 7 | 8 | @Component({ 9 | selector: 'app-select-users', 10 | templateUrl: './select-users.component.html', 11 | styleUrls: ['./select-users.component.scss'] 12 | }) 13 | export class SelectUsersComponent implements OnInit { 14 | 15 | @Input() users: UserI[] = []; 16 | @Output() addUser: EventEmitter = new EventEmitter(); 17 | @Output() removeuser: EventEmitter= new EventEmitter(); 18 | 19 | searchUsername = new FormControl(); 20 | filteredUsers: UserI[] = []; 21 | selectedUser: UserI = null; 22 | 23 | constructor(private userService: UserService) { } 24 | 25 | ngOnInit(): void { 26 | this.searchUsername.valueChanges.pipe( 27 | debounceTime(500), 28 | distinctUntilChanged(), 29 | switchMap((username: string) => this.userService.findByUsername(username).pipe( 30 | tap((users: UserI[]) => this.filteredUsers = users) 31 | )) 32 | ).subscribe(); 33 | } 34 | 35 | addUserToForm() { 36 | this.addUser.emit(this.selectedUser); 37 | this.filteredUsers = []; 38 | this.selectedUser = null; 39 | this.searchUsername.setValue(null); 40 | } 41 | 42 | removeUserFromForm(user: UserI) { 43 | this.removeuser.emit(user); 44 | } 45 | 46 | setSelectedUser(user: UserI) { 47 | this.selectedUser = user; 48 | } 49 | 50 | displayFn(user: UserI) { 51 | if(user) { 52 | return user.username; 53 | } else { 54 | return ''; 55 | } 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /frontend/src/app/private/private-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | import { CreateRoomComponent } from './components/create-room/create-room.component'; 4 | import { DashboardComponent } from './components/dashboard/dashboard.component'; 5 | 6 | const routes: Routes = [ 7 | { 8 | path: 'dashboard', 9 | component: DashboardComponent 10 | }, 11 | { 12 | path: 'create-room', 13 | component: CreateRoomComponent 14 | }, 15 | { 16 | path: '**', 17 | redirectTo: 'dashboard', 18 | pathMatch: 'full' 19 | } 20 | ]; 21 | 22 | @NgModule({ 23 | imports: [RouterModule.forChild(routes)], 24 | exports: [RouterModule] 25 | }) 26 | export class PrivateRoutingModule { } 27 | -------------------------------------------------------------------------------- /frontend/src/app/private/private.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | 4 | import { PrivateRoutingModule } from './private-routing.module'; 5 | import { DashboardComponent } from './components/dashboard/dashboard.component'; 6 | 7 | import {MatListModule} from '@angular/material/list'; 8 | import {MatPaginatorModule} from '@angular/material/paginator'; 9 | import { MatCardModule } from '@angular/material/card'; 10 | import { MatButtonModule } from '@angular/material/button'; 11 | import { CreateRoomComponent } from './components/create-room/create-room.component'; 12 | import { ReactiveFormsModule } from '@angular/forms'; 13 | import { MatFormFieldModule } from '@angular/material/form-field'; 14 | import { MatInputModule } from '@angular/material/input'; 15 | import {MatChipsModule} from '@angular/material/chips'; 16 | import {MatAutocompleteModule} from '@angular/material/autocomplete'; 17 | import {MatIconModule} from '@angular/material/icon'; 18 | import { SelectUsersComponent } from './components/select-users/select-users.component'; 19 | import { ChatRoomComponent } from './components/chat-room/chat-room.component'; 20 | import { ChatMessageComponent } from './components/chat-message/chat-message.component'; 21 | 22 | @NgModule({ 23 | declarations: [ 24 | DashboardComponent, 25 | CreateRoomComponent, 26 | SelectUsersComponent, 27 | ChatRoomComponent, 28 | ChatMessageComponent 29 | ], 30 | imports: [ 31 | CommonModule, 32 | PrivateRoutingModule, 33 | MatListModule, 34 | MatPaginatorModule, 35 | MatCardModule, 36 | MatButtonModule, 37 | ReactiveFormsModule, 38 | MatFormFieldModule, 39 | MatInputModule, 40 | MatChipsModule, 41 | MatAutocompleteModule, 42 | MatIconModule 43 | ] 44 | }) 45 | export class PrivateModule { } 46 | -------------------------------------------------------------------------------- /frontend/src/app/private/services/chat-service/chat.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { ChatService } from './chat.service'; 4 | 5 | describe('ChatService', () => { 6 | let service: ChatService; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | service = TestBed.inject(ChatService); 11 | }); 12 | 13 | it('should be created', () => { 14 | expect(service).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /frontend/src/app/private/services/chat-service/chat.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { MatSnackBar } from '@angular/material/snack-bar'; 3 | import { Observable } from 'rxjs'; 4 | import { MessageI, MessagePaginateI } from 'src/app/model/message.interface'; 5 | import { RoomI, RoomPaginateI } from 'src/app/model/room.interface'; 6 | import { CustomSocket } from '../../sockets/custom-socket'; 7 | 8 | @Injectable({ 9 | providedIn: 'root' 10 | }) 11 | export class ChatService { 12 | 13 | constructor(private socket: CustomSocket, private snackbar: MatSnackBar) { } 14 | 15 | getAddedMessage(): Observable { 16 | return this.socket.fromEvent('messageAdded'); 17 | } 18 | 19 | sendMessage(message: MessageI) { 20 | this.socket.emit('addMessage', message); 21 | } 22 | 23 | joinRoom(room: RoomI) { 24 | this.socket.emit('joinRoom', room); 25 | } 26 | 27 | leaveRoom(room: RoomI) { 28 | this.socket.emit('leaveRoom', room); 29 | } 30 | 31 | getMessages(): Observable { 32 | return this.socket.fromEvent('messages'); 33 | } 34 | 35 | getMyRooms(): Observable { 36 | return this.socket.fromEvent('rooms'); 37 | } 38 | 39 | emitPaginateRooms(limit: number, page: number) { 40 | this.socket.emit('paginateRooms', { limit, page }); 41 | } 42 | 43 | createRoom(room: RoomI) { 44 | this.socket.emit('createRoom', room); 45 | this.snackbar.open(`Room ${room.name} created successfully`, 'Close', { 46 | duration: 2000, horizontalPosition: 'right', verticalPosition: 'top' 47 | }); 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /frontend/src/app/private/sockets/custom-socket.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Socket, SocketIoConfig } from 'ngx-socket-io'; 3 | import { tokenGetter } from 'src/app/app.module'; 4 | 5 | // Workaround till ngx-socket-io fixes the missing "extraHeaders", add extraHeaders to Options 6 | export interface ExtendedSocketIoConfig extends SocketIoConfig { 7 | options?: { 8 | /** 9 | * Name of the path that is captured on the server side. Default: /socket.io 10 | */ 11 | path?: string; 12 | /** 13 | * Whether to reconnect automatically. Default: true 14 | */ 15 | reconnection?: boolean; 16 | /** 17 | * Number of reconnection attempts before giving up. Default: infinity 18 | */ 19 | reconnectionAttempts?: number; 20 | /** 21 | * How long to initially wait before attempting a new reconnection. Default: 1000 +- randomizationFactor 22 | */ 23 | reconnectionDelay?: number; 24 | /** 25 | * Maximum amount of time to wait between reconnections. Default: 5000 26 | */ 27 | reconnectionDelayMax?: number; 28 | /** 29 | * Randomization factor for the reconnection delay. Default: 0.5 30 | */ 31 | randomizationFactor?: number; 32 | /** 33 | * Connection timeout before a connect_error and connect_timeout events are emitted. Default: 20000 34 | */ 35 | timeout?: number; 36 | /** 37 | * By setting this false, you have to call manager.open whenever you decide it's appropriate. Default: true 38 | */ 39 | autoConnect?: boolean; 40 | /** 41 | * Additional query parameters that are sent when connecting a namespace (then found in socket.handshake.query object on the server-side) 42 | */ 43 | query?: { 44 | [key: string]: string | null; 45 | }; 46 | /** 47 | * The parser to use. Defaults to an instance of the Parser that ships with Socket.IO 48 | * Reference: https://github.com/socketio/socket.io-parser 49 | */ 50 | parser?: any; 51 | /** 52 | * Whether the client should try to upgrade the transport from long-polling to something better. Default: true 53 | */ 54 | upgrade?: boolean; 55 | /** 56 | * Forces JSONP for polling transport. Default: false 57 | */ 58 | forceJSONP?: boolean; 59 | /** 60 | * Determines whether to use JSONP when necessary for polling. If disabled (by settings to false) an error will be emitted (saying “No transports available”) if no other transports are available. If another transport is available for opening a connection (e.g. WebSocket) that transport will be used instead. Default: false 61 | */ 62 | jsonp?: boolean; 63 | /** 64 | * Forces base 64 encoding for polling transport even when XHR2 responseType is available and WebSocket even if the used standard supports binary. Default: false 65 | */ 66 | forceBase64?: boolean; 67 | /** 68 | * Enables XDomainRequest for IE8 to avoid loading bar flashing with click sound. default to false because XDomainRequest has a flaw of not sending cookie. Default: false 69 | */ 70 | enablesXDR?: boolean; 71 | /** 72 | * Whether to add the timestamp with each transport request. Note: polling requests are always stamped unless this option is explicitly set to false. 73 | */ 74 | timestampRequests?: boolean; 75 | /** 76 | * The timestamp parameter 77 | */ 78 | timestampParam?: string; 79 | /** 80 | * Port the policy server listens on. Default: 843 81 | */ 82 | policyPort?: number; 83 | /** 84 | * A list of transports to try (in order). Engine always attempts to connect directly with the first one, provided the feature detection test for it passes. Default: ["polling", "websocket"] 85 | */ 86 | transports?: string[]; 87 | /** 88 | * Hash of options, indexed by transport name, overriding the common options for the given transport. Default: {} 89 | */ 90 | transportOptions?: any; 91 | /** 92 | * If true and if the previous websocket connection to the server succeeded, the connection attempt will bypass the normal upgrade process and will initially try websocket. A connection attempt following a transport error will use the normal upgrade process. It is recommended you turn this on only when using SSL/TLS connections, or if you know that your network does not block websockets. Default: false. 93 | */ 94 | rememberUpgrade?: boolean; 95 | /** 96 | * Whether transport upgrades should be restricted to transports supporting binary data. Default: false 97 | */ 98 | onlyBinaryUpgrades?: boolean; 99 | /** 100 | * Timeout for xhr-polling requests in milliseconds (0) (only for polling transport). Default: 0 101 | */ 102 | requestTimeout?: number; 103 | /** 104 | * A list of subprotocols. See https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_servers#Subprotocols 105 | */ 106 | protocols?: any; 107 | 108 | // add extraHeaders to Options 109 | extraHeaders?: {} 110 | } 111 | } 112 | 113 | const config: ExtendedSocketIoConfig = { 114 | url: 'http://localhost:3000', options: { 115 | extraHeaders: { 116 | Authorization: tokenGetter() 117 | } 118 | } 119 | }; 120 | 121 | @Injectable({providedIn: 'root'}) 122 | export class CustomSocket extends Socket { 123 | constructor() { 124 | super(config) 125 | } 126 | } -------------------------------------------------------------------------------- /frontend/src/app/public/_helpers/custom-validators.ts: -------------------------------------------------------------------------------- 1 | import { AbstractControl, ValidationErrors } from "@angular/forms"; 2 | 3 | export class CustomValidators { 4 | 5 | static passwordsMatching(control: AbstractControl): ValidationErrors | null { 6 | const password = control.get('password')?.value; 7 | const passwordConfirm = control.get('passwordConfirm')?.value; 8 | 9 | if ((password === passwordConfirm) && (password !== null && passwordConfirm !== null)) { 10 | return null; 11 | } else { 12 | return { passwordsNotMatching: true }; 13 | } 14 | } 15 | 16 | } -------------------------------------------------------------------------------- /frontend/src/app/public/components/login/login.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |
5 | Go to Register 6 |

Hint: If you are here for the first time you first need to register a user

7 |
8 | 9 |
10 | 11 | Login 12 | 13 |
14 | 15 | 16 | 17 | Email is required 18 | Email must be valid 19 | 20 | 21 | 22 | 23 | Password is required 24 | 25 | 26 | Please fill out all needed information 27 | 28 |
29 | 30 |
31 | 32 |
33 |
34 |
35 |
36 |
37 |
-------------------------------------------------------------------------------- /frontend/src/app/public/components/login/login.component.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | display: flex; 3 | justify-content: center; 4 | margin: 100px; 5 | min-width: 350px; 6 | 7 | .register { 8 | margin-bottom: 20px; 9 | } 10 | 11 | mat-card { 12 | mat-card-content { 13 | form { 14 | display: flex; 15 | flex-direction: column; 16 | justify-content: center; 17 | 18 | mat-form-field { 19 | width: 100%; 20 | min-width: 300px; 21 | } 22 | 23 | .button { 24 | display: flex; 25 | justify-content: flex-end; 26 | } 27 | } 28 | } 29 | } 30 | 31 | } -------------------------------------------------------------------------------- /frontend/src/app/public/components/login/login.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { LoginComponent } from './login.component'; 4 | 5 | describe('LoginComponent', () => { 6 | let component: LoginComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ LoginComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(LoginComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /frontend/src/app/public/components/login/login.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { FormControl, FormGroup, Validators } from '@angular/forms'; 3 | import { Router } from '@angular/router'; 4 | import { tap } from 'rxjs/operators'; 5 | import { AuthService } from '../../services/auth-service/auth.service'; 6 | 7 | @Component({ 8 | selector: 'app-login', 9 | templateUrl: './login.component.html', 10 | styleUrls: ['./login.component.scss'] 11 | }) 12 | export class LoginComponent { 13 | 14 | form: FormGroup = new FormGroup({ 15 | email: new FormControl(null, [Validators.required, Validators.email]), 16 | password: new FormControl(null, [Validators.required]) 17 | }); 18 | 19 | constructor(private authService: AuthService, private router: Router) { } 20 | 21 | login() { 22 | if (this.form.valid) { 23 | this.authService.login({ 24 | email: this.email.value, 25 | password: this.password.value 26 | }).pipe( 27 | tap(() => this.router.navigate(['../../private/dashboard'])) 28 | ).subscribe() 29 | } 30 | } 31 | 32 | get email(): FormControl { 33 | return this.form.get('email') as FormControl; 34 | } 35 | 36 | get password(): FormControl { 37 | return this.form.get('password') as FormControl; 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /frontend/src/app/public/components/register/register.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 7 | 8 |
9 | 10 | 11 | Register 12 | 13 | 14 |
15 | 16 | 17 | 18 | Email is required 19 | Email must be valid 20 | 21 | 22 | 23 | 24 | Username is required 25 | 26 | 27 | 28 | 29 | Password is required 30 | 31 | 32 | 33 | 34 | 35 | 36 | Please fill out all needed information 37 | Passwords are not 38 | matching 39 | 40 |
41 | 42 |
43 | 44 |
45 |
46 | 47 |
48 | 49 |
50 | 51 | 52 |
53 | 54 |
-------------------------------------------------------------------------------- /frontend/src/app/public/components/register/register.component.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | display: flex; 3 | justify-content: center; 4 | margin: 100px; 5 | min-width: 350px; 6 | 7 | .login { 8 | margin-bottom: 20px; 9 | } 10 | 11 | mat-card { 12 | mat-card-content { 13 | form { 14 | display: flex; 15 | flex-direction: column; 16 | justify-content: center; 17 | 18 | mat-form-field { 19 | width: 100%; 20 | min-width: 300px; 21 | } 22 | 23 | .button { 24 | display: flex; 25 | justify-content: flex-end; 26 | } 27 | } 28 | } 29 | } 30 | 31 | } -------------------------------------------------------------------------------- /frontend/src/app/public/components/register/register.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { RegisterComponent } from './register.component'; 4 | 5 | describe('RegisterComponent', () => { 6 | let component: RegisterComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ RegisterComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(RegisterComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /frontend/src/app/public/components/register/register.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { FormControl, FormGroup, Validators } from '@angular/forms'; 3 | import { Router } from '@angular/router'; 4 | import { tap } from 'rxjs/operators'; 5 | import { UserService } from '../../services/user-service/user.service'; 6 | import { CustomValidators } from '../../_helpers/custom-validators'; 7 | 8 | @Component({ 9 | selector: 'app-register', 10 | templateUrl: './register.component.html', 11 | styleUrls: ['./register.component.scss'] 12 | }) 13 | export class RegisterComponent { 14 | 15 | form: FormGroup = new FormGroup({ 16 | email: new FormControl(null, [Validators.required, Validators.email]), 17 | username: new FormControl(null, [Validators.required]), 18 | password: new FormControl(null, [Validators.required]), 19 | passwordConfirm: new FormControl(null, [Validators.required]) 20 | }, 21 | { validators: CustomValidators.passwordsMatching } 22 | ); 23 | 24 | constructor(private userService: UserService, private router: Router) { } 25 | 26 | register() { 27 | if (this.form.valid) { 28 | this.userService.create({ 29 | email: this.email.value, 30 | password: this.password.value, 31 | username: this.username.value 32 | }).pipe( 33 | tap(() => this.router.navigate(['../login'])) 34 | ).subscribe(); 35 | } 36 | } 37 | 38 | get email(): FormControl { 39 | return this.form.get('email') as FormControl; 40 | } 41 | 42 | get username(): FormControl { 43 | return this.form.get('username') as FormControl; 44 | } 45 | 46 | get password(): FormControl { 47 | return this.form.get('password') as FormControl; 48 | } 49 | 50 | get passwordConfirm(): FormControl { 51 | return this.form.get('passwordConfirm') as FormControl; 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /frontend/src/app/public/public-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | import { LoginComponent } from './components/login/login.component'; 4 | import { RegisterComponent } from './components/register/register.component'; 5 | 6 | const routes: Routes = [ 7 | { 8 | path: 'login', 9 | component: LoginComponent 10 | }, 11 | { 12 | path: 'register', 13 | component: RegisterComponent 14 | }, 15 | { 16 | path: '**', 17 | redirectTo: 'login', 18 | pathMatch: 'full' 19 | } 20 | ]; 21 | 22 | @NgModule({ 23 | imports: [RouterModule.forChild(routes)], 24 | exports: [RouterModule] 25 | }) 26 | export class PublicRoutingModule { } 27 | -------------------------------------------------------------------------------- /frontend/src/app/public/public.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | 4 | import { PublicRoutingModule } from './public-routing.module'; 5 | import { LoginComponent } from './components/login/login.component'; 6 | import { RegisterComponent } from './components/register/register.component'; 7 | 8 | import {MatCardModule} from '@angular/material/card'; 9 | import { ReactiveFormsModule } from '@angular/forms'; 10 | import {MatFormFieldModule} from '@angular/material/form-field'; 11 | import { MatButtonModule } from '@angular/material/button'; 12 | import {MatInputModule} from '@angular/material/input'; 13 | 14 | 15 | @NgModule({ 16 | declarations: [ 17 | LoginComponent, 18 | RegisterComponent 19 | ], 20 | imports: [ 21 | CommonModule, 22 | PublicRoutingModule, 23 | MatCardModule, 24 | ReactiveFormsModule, 25 | MatFormFieldModule, 26 | MatInputModule, 27 | MatButtonModule 28 | ] 29 | }) 30 | export class PublicModule { } 31 | -------------------------------------------------------------------------------- /frontend/src/app/public/services/auth-service/auth.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { AuthService } from './auth.service'; 4 | 5 | describe('AuthService', () => { 6 | let service: AuthService; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | service = TestBed.inject(AuthService); 11 | }); 12 | 13 | it('should be created', () => { 14 | expect(service).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /frontend/src/app/public/services/auth-service/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient } from '@angular/common/http'; 2 | import { Injectable } from '@angular/core'; 3 | import { MatSnackBar } from '@angular/material/snack-bar'; 4 | import { JwtHelperService } from '@auth0/angular-jwt'; 5 | import { Observable } from 'rxjs'; 6 | import { tap } from 'rxjs/operators'; 7 | import { LoginResponseI } from 'src/app/model/login-response.interface'; 8 | import { UserI } from 'src/app/model/user.interface'; 9 | 10 | @Injectable({ 11 | providedIn: 'root' 12 | }) 13 | export class AuthService { 14 | 15 | constructor(private http: HttpClient, private snackbar: MatSnackBar, private jwtService: JwtHelperService) { } 16 | 17 | login(user: UserI): Observable { 18 | return this.http.post('api/users/login', user).pipe( 19 | tap((res: LoginResponseI) => localStorage.setItem('nestjs_chat_app', res.access_token)), 20 | tap(() => this.snackbar.open('Login Successfull', 'Close', { 21 | duration: 2000, horizontalPosition: 'right', verticalPosition: 'top' 22 | })) 23 | ); 24 | } 25 | 26 | getLoggedInUser() { 27 | const decodedToken = this.jwtService.decodeToken(); 28 | return decodedToken.user; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /frontend/src/app/public/services/user-service/user.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { UserService } from './user.service'; 4 | 5 | describe('UserService', () => { 6 | let service: UserService; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | service = TestBed.inject(UserService); 11 | }); 12 | 13 | it('should be created', () => { 14 | expect(service).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /frontend/src/app/public/services/user-service/user.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient } from '@angular/common/http'; 2 | import { Injectable } from '@angular/core'; 3 | import { MatSnackBar } from '@angular/material/snack-bar'; 4 | import { Observable, throwError } from 'rxjs'; 5 | import { UserI } from 'src/app/model/user.interface'; 6 | import { catchError, tap } from 'rxjs/operators'; 7 | 8 | @Injectable({ 9 | providedIn: 'root' 10 | }) 11 | export class UserService { 12 | 13 | constructor(private http: HttpClient, private snackbar: MatSnackBar) { } 14 | 15 | findByUsername(username: string): Observable { 16 | return this.http.get(`api/users/find-by-username?username=${username}`); 17 | } 18 | 19 | create(user: UserI): Observable { 20 | return this.http.post('api/users', user).pipe( 21 | tap((createdUser: UserI) => this.snackbar.open(`User ${createdUser.username} created successfully`, 'Close', { 22 | duration: 2000, horizontalPosition: 'right', verticalPosition: 'top' 23 | })), 24 | catchError(e => { 25 | this.snackbar.open(`User could not be created, due to: ${e.error.message}`, 'Close', { 26 | duration: 5000, horizontalPosition: 'right', verticalPosition: 'top' 27 | }) 28 | return throwError(e); 29 | }) 30 | ) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /frontend/src/app/services/test-service/test.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { TestService } from './test.service'; 4 | 5 | describe('TestService', () => { 6 | let service: TestService; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | service = TestBed.inject(TestService); 11 | }); 12 | 13 | it('should be created', () => { 14 | expect(service).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /frontend/src/app/services/test-service/test.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpClient } from '@angular/common/http'; 3 | import { Observable } from 'rxjs'; 4 | 5 | export interface Test { 6 | title: string; 7 | } 8 | 9 | 10 | @Injectable({ 11 | providedIn: 'root' 12 | }) 13 | export class TestService { 14 | 15 | constructor(private http: HttpClient) { } 16 | 17 | getTest(): Observable { 18 | return this.http.get('api'); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /frontend/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TommiCodes/real-time-chat-nestjs-angular/726aa89bced98a1adcc9843b42d57945a51b63e0/frontend/src/assets/.gitkeep -------------------------------------------------------------------------------- /frontend/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /frontend/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false 7 | }; 8 | 9 | /* 10 | * For easier debugging in development mode, you can import the following file 11 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 12 | * 13 | * This import should be commented out in production mode because it will have a negative impact 14 | * on performance if an error is thrown. 15 | */ 16 | // import 'zone.js/plugins/zone-error'; // Included with Angular CLI. 17 | -------------------------------------------------------------------------------- /frontend/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TommiCodes/real-time-chat-nestjs-angular/726aa89bced98a1adcc9843b42d57945a51b63e0/frontend/src/favicon.ico -------------------------------------------------------------------------------- /frontend/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Frontend 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /frontend/src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule) 12 | .catch(err => console.error(err)); 13 | -------------------------------------------------------------------------------- /frontend/src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** 22 | * IE11 requires the following for NgClass support on SVG elements 23 | */ 24 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 25 | 26 | /** 27 | * Web Animations `@angular/platform-browser/animations` 28 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. 29 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). 30 | */ 31 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 32 | 33 | /** 34 | * By default, zone.js will patch all possible macroTask and DomEvents 35 | * user can disable parts of macroTask/DomEvents patch by setting following flags 36 | * because those flags need to be set before `zone.js` being loaded, and webpack 37 | * will put import in the top of bundle, so user need to create a separate file 38 | * in this directory (for example: zone-flags.ts), and put the following flags 39 | * into that file, and then add the following code before importing zone.js. 40 | * import './zone-flags'; 41 | * 42 | * The flags allowed in zone-flags.ts are listed here. 43 | * 44 | * The following flags will work for all browsers. 45 | * 46 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 47 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 48 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 49 | * 50 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 51 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 52 | * 53 | * (window as any).__Zone_enable_cross_context_check = true; 54 | * 55 | */ 56 | 57 | /*************************************************************************************************** 58 | * Zone JS is required by default for Angular itself. 59 | */ 60 | import 'zone.js'; // Included with Angular CLI. 61 | 62 | 63 | /*************************************************************************************************** 64 | * APPLICATION IMPORTS 65 | */ 66 | -------------------------------------------------------------------------------- /frontend/src/proxy.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "/api": { 3 | "target": "http://api:3000", 4 | "secure": false, 5 | "changeOrigin": true 6 | }, 7 | "/socket.io/": { 8 | "target": "http://api:3000", 9 | "secure": false, 10 | "changeOrigin": true 11 | } 12 | } -------------------------------------------------------------------------------- /frontend/src/styles.scss: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | 3 | html, body { height: 100%; } 4 | body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; } 5 | -------------------------------------------------------------------------------- /frontend/src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | declare const require: { 11 | context(path: string, deep?: boolean, filter?: RegExp): { 12 | keys(): string[]; 13 | (id: string): T; 14 | }; 15 | }; 16 | 17 | // First, initialize the Angular testing environment. 18 | getTestBed().initTestEnvironment( 19 | BrowserDynamicTestingModule, 20 | platformBrowserDynamicTesting() 21 | ); 22 | // Then we find all the tests. 23 | const context = require.context('./', true, /\.spec\.ts$/); 24 | // And load the modules. 25 | context.keys().map(context); 26 | -------------------------------------------------------------------------------- /frontend/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/app", 6 | "types": [] 7 | }, 8 | "files": [ 9 | "src/main.ts", 10 | "src/polyfills.ts" 11 | ], 12 | "include": [ 13 | "src/**/*.d.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "compileOnSave": false, 4 | "compilerOptions": { 5 | "baseUrl": "./", 6 | "outDir": "./dist/out-tsc", 7 | "forceConsistentCasingInFileNames": true, 8 | "strict": false, 9 | "noImplicitReturns": true, 10 | "noFallthroughCasesInSwitch": true, 11 | "sourceMap": true, 12 | "declaration": false, 13 | "downlevelIteration": true, 14 | "experimentalDecorators": true, 15 | "moduleResolution": "node", 16 | "importHelpers": true, 17 | "target": "es2017", 18 | "module": "es2020", 19 | "lib": [ 20 | "es2018", 21 | "dom" 22 | ] 23 | }, 24 | "angularCompilerOptions": { 25 | "enableI18nLegacyMessageIdFormat": false, 26 | "strictInjectionParameters": true, 27 | "strictInputAccessModifiers": true, 28 | "strictTemplates": true 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /frontend/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/spec", 6 | "types": [ 7 | "jasmine" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts", 12 | "src/polyfills.ts" 13 | ], 14 | "include": [ 15 | "src/**/*.spec.ts", 16 | "src/**/*.d.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /todos.md: -------------------------------------------------------------------------------- 1 | ## Open Todos that could be implemented in coming versions 2 | 3 | - Create Chatroom, don't select own user 4 | - add logout 5 | - Infinite Scrolling for Chatroom: Messages 6 | - Infinite Scrolling for Rooms 7 | - Frontend Store Management 8 | - Display newest Message in the Rooms List also 9 | - Refactor Styling 10 | - Responsiveness? 11 | - Search for Rooms 12 | - Online List of users 13 | - ... --------------------------------------------------------------------------------