├── .angulardoc.json ├── .editorconfig ├── .gitignore ├── .vscode └── launch.json ├── Procfile ├── README.md ├── angular.json ├── e2e ├── app.e2e-spec.ts ├── app.po.ts ├── jest-e2e.json └── tsconfig.e2e.json ├── index.js ├── karma.conf.js ├── nodemon.json ├── package-lock.json ├── package.json ├── protractor.conf.js ├── proxy.conf.json ├── server ├── app.module.ts ├── auth │ ├── auth.controller.ts │ ├── auth.module.ts │ ├── auth.service.ts │ ├── constants.ts │ ├── guards │ │ ├── jwt-auth.guard.ts │ │ └── local-auth.guard.ts │ └── strategies │ │ ├── jwt.strategy.ts │ │ └── local.strategy.ts ├── common │ ├── decorators │ │ └── roles.decorator.ts │ ├── filters │ │ └── not-found-exception.filter.ts │ ├── guards │ │ └── roles.guard.ts │ └── pipes │ │ ├── parse-int.pipe.ts │ │ ├── parse-object-id.pipe.ts │ │ └── validation.pipe.ts ├── events.gateway.ts ├── jest.json ├── main.ts ├── models │ ├── common-result.ts │ ├── room.ts │ └── user.ts ├── rooms │ ├── rooms.controller.ts │ ├── rooms.module.ts │ └── rooms.service.ts ├── schemas │ ├── room.schema.ts │ └── user.schema.ts ├── tsconfig.json └── users │ ├── users.controller.ts │ ├── users.module.ts │ └── users.service.ts ├── src ├── app │ ├── app.component.css │ ├── app.component.html │ ├── app.component.ts │ ├── app.module.ts │ ├── common │ │ ├── ang-material.module.ts │ │ ├── animation.ts │ │ ├── error-handler.interceptor.ts │ │ └── snotify-helper.service.ts │ ├── components │ │ ├── chat │ │ │ ├── chat.component.css │ │ │ ├── chat.component.html │ │ │ ├── chat.component.ts │ │ │ ├── chat.module.ts │ │ │ ├── message │ │ │ │ ├── message.component.css │ │ │ │ ├── message.component.html │ │ │ │ ├── message.component.spec.ts │ │ │ │ └── message.component.ts │ │ │ └── userlist │ │ │ │ ├── userlist.component.css │ │ │ │ ├── userlist.component.html │ │ │ │ ├── userlist.component.spec.ts │ │ │ │ └── userlist.component.ts │ │ ├── container │ │ │ ├── container.component.css │ │ │ ├── container.component.html │ │ │ ├── container.component.spec.ts │ │ │ └── container.component.ts │ │ ├── login │ │ │ ├── login.component.css │ │ │ ├── login.component.html │ │ │ └── login.component.ts │ │ ├── menu │ │ │ ├── menu.component.css │ │ │ ├── menu.component.html │ │ │ └── menu.component.ts │ │ └── users-control │ │ │ ├── add-user │ │ │ ├── add-user.component.css │ │ │ ├── add-user.component.html │ │ │ └── add-user.component.ts │ │ │ ├── user-control.module.ts │ │ │ ├── users-control.component.css │ │ │ ├── users-control.component.html │ │ │ └── users-control.component.ts │ ├── directives │ │ └── sort │ │ │ └── sort.directive.ts │ ├── entities │ │ ├── common-result.ts │ │ ├── message.ts │ │ ├── room.ts │ │ └── user.ts │ ├── services │ │ ├── auth │ │ │ ├── auth-guard.service.ts │ │ │ ├── auth.interceptor.ts │ │ │ ├── authentication.service.ts │ │ │ └── local-storage.service.ts │ │ ├── chat-sockets │ │ │ ├── socket-one.service.ts │ │ │ └── socket.service.ts │ │ ├── paginator │ │ │ ├── paginator-helper.ts │ │ │ └── paginator.service.ts │ │ ├── rooms │ │ │ └── rooms.service.ts │ │ ├── search-sort-filter │ │ │ └── search-sort-filter.service.ts │ │ └── users │ │ │ └── users.service.ts │ └── shared │ │ └── shared.module.ts ├── assets │ ├── .gitkeep │ ├── fonts │ │ └── Cabin │ │ │ ├── Cabin-Bold.ttf │ │ │ ├── Cabin-BoldItalic.ttf │ │ │ ├── Cabin-Italic.ttf │ │ │ ├── Cabin-Medium.ttf │ │ │ ├── Cabin-MediumItalic.ttf │ │ │ ├── Cabin-Regular.ttf │ │ │ ├── Cabin-SemiBold.ttf │ │ │ └── Cabin-SemiBoldItalic.ttf │ └── reset.css ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── favicon.ico ├── index.html ├── main.ts ├── polyfills.ts ├── styles.scss ├── test.ts ├── tsconfig.app.json ├── tsconfig.spec.json └── typings.d.ts ├── tsconfig.build.json ├── tsconfig.json └── tslint.json /.angulardoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "repoId": "fb3d7975-dabc-48a6-a445-45bd499cb951", 3 | "lastSync": 0 4 | } -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://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 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /dist-server 6 | /tmp 7 | /out-tsc 8 | 9 | # dependencies 10 | /node_modules 11 | 12 | # tests 13 | /coverage 14 | /.nyc_output 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 | 32 | # misc 33 | /.sass-cache 34 | /connect.lock 35 | /coverage 36 | /libpeerconnection.log 37 | npm-debug.log 38 | testem.log 39 | /typings 40 | 41 | # e2e 42 | /e2e/*.js 43 | /e2e/*.map 44 | 45 | # System Files 46 | .DS_Store 47 | Thumbs.db 48 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Launch Program", 11 | "program":"${workspaceFolder}/server/main.ts", 12 | "preLaunchTask": "tsc: watch - tsconfig.build.json", 13 | "outFiles": ["${workspaceFolder}/dist/**/*.js"], 14 | "skipFiles": [ 15 | "${workspaceFolder}/node_modules/**/*.js", 16 | "/**/*.js" 17 | ] 18 | } 19 | 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: node --optimize_for_size --max_old_space_size=460 dist/server/main.js -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # # angular # nodejs # nestjs #socket #mongoose 2 | 0) node version lts 3 | 1) npm install 4 | 2) npm run start:dev && start-front-dev (you should have installed mongodb and run it before you start the backend) 5 | 3) open 'localhost:4200' in your browser 6 | 7 | 8 | ![alt text](https://i.imgur.com/0gzqmhV.png) 9 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "angular-nest-starter": { 7 | "root": "", 8 | "sourceRoot": "src", 9 | "projectType": "application", 10 | "architect": { 11 | "build": { 12 | "builder": "@angular-devkit/build-angular:browser", 13 | "options": { 14 | "aot": true, 15 | "outputPath": "dist", 16 | "index": "src/index.html", 17 | "main": "src/main.ts", 18 | "tsConfig": "src/tsconfig.app.json", 19 | "polyfills": "src/polyfills.ts", 20 | "assets": [ 21 | "src/assets", 22 | "src/favicon.ico" 23 | ], 24 | "styles": [ 25 | "src/styles.scss" 26 | ], 27 | "scripts": [] 28 | }, 29 | "configurations": { 30 | "production": { 31 | "budgets": [ 32 | { 33 | "type": "anyComponentStyle", 34 | "maximumWarning": "6kb" 35 | } 36 | ], 37 | "optimization": true, 38 | "outputHashing": "all", 39 | "sourceMap": false, 40 | "extractCss": true, 41 | "namedChunks": false, 42 | "aot": true, 43 | "extractLicenses": true, 44 | "vendorChunk": false, 45 | "buildOptimizer": true, 46 | "fileReplacements": [ 47 | { 48 | "replace": "src/environments/environment.ts", 49 | "with": "src/environments/environment.prod.ts" 50 | } 51 | ] 52 | } 53 | } 54 | }, 55 | "serve": { 56 | "builder": "@angular-devkit/build-angular:dev-server", 57 | "options": { 58 | "browserTarget": "angular-nest-starter:build" 59 | }, 60 | "configurations": { 61 | "production": { 62 | "browserTarget": "angular-nest-starter:build:production" 63 | } 64 | } 65 | }, 66 | "extract-i18n": { 67 | "builder": "@angular-devkit/build-angular:extract-i18n", 68 | "options": { 69 | "browserTarget": "angular-nest-starter:build" 70 | } 71 | }, 72 | "test": { 73 | "builder": "@angular-devkit/build-angular:karma", 74 | "options": { 75 | "main": "src/test.ts", 76 | "karmaConfig": "./karma.conf.js", 77 | "polyfills": "src/polyfills.ts", 78 | "tsConfig": "src/tsconfig.spec.json", 79 | "scripts": [], 80 | "styles": [ 81 | "src/styles.scss" 82 | ], 83 | "assets": [ 84 | "src/assets", 85 | "src/favicon.ico" 86 | ] 87 | } 88 | }, 89 | "lint": { 90 | "builder": "@angular-devkit/build-angular:tslint", 91 | "options": { 92 | "tsConfig": [ 93 | "src/tsconfig.app.json", 94 | "src/tsconfig.spec.json" 95 | ], 96 | "exclude": [ 97 | "**/node_modules/**" 98 | ] 99 | } 100 | } 101 | } 102 | }, 103 | "angular-nest-starter-e2e": { 104 | "root": "e2e", 105 | "sourceRoot": "e2e", 106 | "projectType": "application", 107 | "architect": { 108 | "e2e": { 109 | "builder": "@angular-devkit/build-angular:protractor", 110 | "options": { 111 | "protractorConfig": "./protractor.conf.js", 112 | "devServerTarget": "angular-nest-starter:serve" 113 | } 114 | }, 115 | "lint": { 116 | "builder": "@angular-devkit/build-angular:tslint", 117 | "options": { 118 | "tsConfig": [ 119 | "e2e/tsconfig.e2e.json" 120 | ], 121 | "exclude": [ 122 | "**/node_modules/**" 123 | ] 124 | } 125 | } 126 | } 127 | } 128 | }, 129 | "defaultProject": "angular-nest-starter", 130 | "schematics": { 131 | "@schematics/angular:component": { 132 | "prefix": "app", 133 | "style": "css" 134 | }, 135 | "@schematics/angular:directive": { 136 | "prefix": "app" 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /e2e/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from './app.po'; 2 | 3 | describe('angular-nest-startr App', () => { 4 | let page: AppPage; 5 | 6 | beforeEach(() => { 7 | page = new AppPage(); 8 | }); 9 | 10 | it('should display welcome message', () => { 11 | page.navigateTo(); 12 | expect(page.getParagraphText()).toEqual('Welcome to app!'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /e2e/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class AppPage { 4 | navigateTo() { 5 | return browser.get('/'); 6 | } 7 | 8 | getParagraphText() { 9 | return element(by.css('app-root h1')).getText(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /e2e/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": [ 3 | "ts", 4 | "tsx", 5 | "js", 6 | "json" 7 | ], 8 | "transform": { 9 | "^.+\\.tsx?$": "/../node_modules/ts-jest/preprocessor.js" 10 | }, 11 | "testRegex": "/e2e/.*\\.(e2e-test|e2e-spec).(ts|tsx|js)$", 12 | "collectCoverageFrom" : ["src/**/*.{js,jsx,tsx,ts}", "!**/node_modules/**", "!**/vendor/**"], 13 | "coverageReporters": ["json", "lcov"] 14 | } -------------------------------------------------------------------------------- /e2e/tsconfig.e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/e2e", 5 | "baseUrl": "./", 6 | "module": "commonjs", 7 | "target": "es5", 8 | "types": [ 9 | "jasmine", 10 | "jasminewd2", 11 | "node" 12 | ] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | require('ts-node').register({ project: 'server/tsconfig.json' }); 2 | require('./server/main'); -------------------------------------------------------------------------------- /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-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client:{ 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require('path').join(__dirname, 'coverage'), reports: [ 'html', 'lcovonly' ], 20 | fixWebpackSourcePaths: true 21 | }, 22 | 23 | reporters: ['progress', 'kjhtml'], 24 | port: 9876, 25 | colors: true, 26 | logLevel: config.LOG_INFO, 27 | autoWatch: true, 28 | browsers: ['Chrome'], 29 | singleRun: false 30 | }); 31 | }; 32 | -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": [ 3 | "server/*" 4 | ], 5 | "ext": "ts", 6 | "ignore": [ 7 | ".git", 8 | "node_modules/**/node_modules", 9 | "**/*.spec.ts" 10 | ], 11 | "exec": "ts-node --project ./server/tsconfig.json ./server/main.ts" 12 | } 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-nest-starter", 3 | "version": "0.1.0", 4 | "license": "MIT", 5 | "scripts": { 6 | "ng": "ng", 7 | "start-front-dev": "ng serve --progress=false --proxy-config proxy.conf.json", 8 | "prestart": "npm run build", 9 | "start": "node dist/server/main.js", 10 | "build": "ng build --prod && tsc -p ./server", 11 | "heroku-postbuild": "npm run build", 12 | "prebuild": "rimraf dist", 13 | "build-nest": "nest build", 14 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 15 | "start-nest": "nest start", 16 | "start:dev": "nest start --watch", 17 | "start:debug": "nest start --debug --watch", 18 | "start:prod": "node dist/main", 19 | "start-dev": "npm run start-nest && ng serve", 20 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", 21 | "test": "jest", 22 | "test:watch": "jest --watch", 23 | "test:cov": "jest --coverage", 24 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 25 | "test:e2e": "jest --config ./test/jest-e2e.json", 26 | "dev:ssr": "ng run universal-starter-v9:serve-ssr", 27 | "serve:ssr": "node dist/universal-starter-v9/server/main.js", 28 | "prebuild:ssr": "ngcc", 29 | "build:ssr": "ng run universal-starter-v9:server:production", 30 | "prerender": "ng run universal-starter-v9:prerender" 31 | }, 32 | "private": true, 33 | "dependencies": { 34 | "@angular-devkit/build-angular": "~0.900.5", 35 | "@angular-devkit/build-optimizer": "0.900.5", 36 | "@angular/animations": "9.0.5", 37 | "@angular/cdk": "^9.1.1", 38 | "@angular/cli": "^9.0.5", 39 | "@angular/common": "9.0.5", 40 | "@angular/compiler": "9.0.5", 41 | "@angular/compiler-cli": "^9.0.5", 42 | "@angular/core": "9.0.5", 43 | "@angular/forms": "9.0.5", 44 | "@angular/language-service": "9.0.5", 45 | "@angular/material": "^9.1.1", 46 | "@angular/platform-browser": "9.0.5", 47 | "@angular/platform-browser-dynamic": "9.0.5", 48 | "@angular/platform-server": "9.0.5", 49 | "@angular/router": "9.0.5", 50 | "@nestjs/cli": "^6.13.2", 51 | "@nestjs/common": "^6.10.14", 52 | "@nestjs/core": "^6.10.14", 53 | "@nestjs/jwt": "^7.0.0", 54 | "@nestjs/mongoose": "^6.3.1", 55 | "@nestjs/ng-universal": "^3.0.1", 56 | "@nestjs/passport": "^7.0.0", 57 | "@nestjs/platform-express": "^6.10.14", 58 | "@nestjs/platform-socket.io": "^6.11.11", 59 | "@nestjs/schematics": "^6.8.1", 60 | "@nestjs/serve-static": "^2.0.0", 61 | "@nestjs/testing": "^6.10.14", 62 | "@nestjs/websockets": "^6.11.11", 63 | "@nguniversal/express-engine": "^9.0.1", 64 | "@nguniversal/module-map-ngfactory-loader": "v8.2.6", 65 | "@types/express": "^4.17.2", 66 | "@types/jest": "25.1.4", 67 | "@types/node": "^12.11.1", 68 | "@types/socket.io": "^2.1.4", 69 | "@types/supertest": "^2.0.8", 70 | "class-transformer": "^0.3.1", 71 | "class-validator": "^0.11.0", 72 | "codelyzer": "^5.1.2", 73 | "concurrently": "^5.1.0", 74 | "core-js": "^3.6.4", 75 | "cors": "^2.8.5", 76 | "express": "^4.15.2", 77 | "hammerjs": "^2.0.8", 78 | "jasmine-core": "~3.5.0", 79 | "jasmine-spec-reporter": "~4.2.1", 80 | "jest": "^25.1.0", 81 | "karma": "^4.4.1", 82 | "karma-chrome-launcher": "~3.1.0", 83 | "karma-cli": "~2.0.0", 84 | "karma-coverage-istanbul-reporter": "^2.1.1", 85 | "karma-jasmine": "~3.1.1", 86 | "karma-jasmine-html-reporter": "^1.5.2", 87 | "mongoose": "^5.9.3", 88 | "ng-snotify": "^4.3.1", 89 | "ngx-socket-io": "^3.0.1", 90 | "nodemon": "^2.0.2", 91 | "passport": "^0.4.1", 92 | "passport-jwt": "^4.0.0", 93 | "passport-local": "^1.0.0", 94 | "protractor": "^5.4.3", 95 | "reflect-metadata": "^0.1.13", 96 | "rimraf": "^3.0.0", 97 | "rxjs": "^6.5.4", 98 | "socketio-jwt": "^4.5.0", 99 | "supertest": "^4.0.2", 100 | "ts-node": "~8.6.2", 101 | "tslib": "^1.10.0", 102 | "tslint": "~6.0.0", 103 | "typescript": "^3.7.5", 104 | "zone.js": "~0.10.2" 105 | }, 106 | "devDependencies": { 107 | "@nguniversal/builders": "^9.0.1", 108 | "@types/express": "^4.17.0", 109 | "@types/passport-jwt": "^3.0.3", 110 | "@types/passport-local": "^1.0.33", 111 | "class-transformer": "^0.3.1", 112 | "ts-loader": "^6.2.1", 113 | "webpack-cli": "^3.1.0" 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /protractor.conf.js: -------------------------------------------------------------------------------- 1 | // Protractor configuration file, see link for more information 2 | // https://github.com/angular/protractor/blob/master/lib/config.ts 3 | 4 | const { SpecReporter } = require('jasmine-spec-reporter'); 5 | 6 | exports.config = { 7 | allScriptsTimeout: 11000, 8 | specs: [ 9 | './e2e/**/*.e2e-spec.ts' 10 | ], 11 | capabilities: { 12 | 'browserName': 'chrome' 13 | }, 14 | directConnect: true, 15 | baseUrl: 'http://localhost:4200/', 16 | framework: 'jasmine', 17 | jasmineNodeOpts: { 18 | showColors: true, 19 | defaultTimeoutInterval: 30000, 20 | print: function() {} 21 | }, 22 | onPrepare() { 23 | require('ts-node').register({ 24 | project: 'e2e/tsconfig.e2e.json' 25 | }); 26 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /proxy.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "/api": { 3 | "target": "http://localhost:3000", 4 | "secure": false 5 | }, 6 | "/socket.io/": { 7 | "target": "http://localhost:3000", 8 | "secure": false 9 | } 10 | } -------------------------------------------------------------------------------- /server/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { MongooseModule } from '@nestjs/mongoose'; 3 | import { ServeStaticModule } from '@nestjs/serve-static'; 4 | import { join } from 'path'; 5 | import { AuthModule } from './auth/auth.module'; 6 | import { RoomsModule } from './rooms/rooms.module'; 7 | import { UsersModule } from './users/users.module'; 8 | 9 | 10 | const getMongoUrl = () => { 11 | if (process.env.MONGOUSER && process.env.MONGOPASSWORD) { 12 | return `mongodb://${process.env.MONGOUSER}:${process.env.MONGOPASSWORD}@ds249839.mlab.com:49839/angular-nest-chat-app`; 13 | } else { 14 | return 'mongodb://localhost:27017/nest'; 15 | } 16 | }; 17 | @Module({ 18 | imports: [ 19 | ServeStaticModule.forRoot({ 20 | rootPath: join(__dirname, '../../dist'), 21 | }), 22 | MongooseModule.forRoot(getMongoUrl()), 23 | UsersModule, 24 | RoomsModule, 25 | AuthModule 26 | ], 27 | }) 28 | export class AppModule {} 29 | -------------------------------------------------------------------------------- /server/auth/auth.controller.ts: -------------------------------------------------------------------------------- 1 | import { JwtAuthGuard } from './guards/jwt-auth.guard'; 2 | import { LocalAuthGuard } from './guards/local-auth.guard'; 3 | import { Controller, Post, HttpStatus, HttpCode, Get, Body, HttpException, UseGuards } from '@nestjs/common'; 4 | import { AuthService, AcessToken } from './auth.service'; 5 | import { ValidationPipe } from '../common/pipes/validation.pipe'; 6 | import { CommonResult } from '../models/common-result'; 7 | import { User } from '../models/user'; 8 | 9 | @Controller('api/auth/') 10 | export class AuthController { 11 | constructor(private readonly authService: AuthService) { 12 | } 13 | 14 | @HttpCode(HttpStatus.OK) 15 | @UseGuards(LocalAuthGuard) 16 | @Post('login') 17 | public async login(@Body(new ValidationPipe()) user: User): Promise { 18 | try { 19 | return await this.authService.login(user); 20 | } catch (err) { 21 | if (err instanceof HttpException) { 22 | throw err; 23 | } else { 24 | throw new HttpException(new CommonResult(false, 'Server error'), HttpStatus.FORBIDDEN); 25 | } 26 | } 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /server/auth/auth.module.ts: -------------------------------------------------------------------------------- 1 | import { JwtStrategy } from './strategies/jwt.strategy'; 2 | import { forwardRef, Module } from '@nestjs/common'; 3 | import { JwtModule } from '@nestjs/jwt'; 4 | import { PassportModule } from '@nestjs/passport'; 5 | import { RoomsModule } from './../rooms/rooms.module'; 6 | import { UsersModule } from './../users/users.module'; 7 | import { AuthController } from './auth.controller'; 8 | import { AuthService } from './auth.service'; 9 | import { jwtConstants } from './constants'; 10 | import { LocalStrategy } from './strategies/local.strategy'; 11 | 12 | @Module({ 13 | imports: [ 14 | RoomsModule, 15 | forwardRef(() => UsersModule), 16 | PassportModule.register({ defaultStrategy: 'local' }), 17 | JwtModule.register({ 18 | secret: jwtConstants.secret, 19 | signOptions: { expiresIn: jwtConstants.expireTokenTime}, 20 | }), 21 | ], 22 | providers: [AuthService, LocalStrategy, JwtStrategy ], 23 | controllers: [AuthController], 24 | exports: [AuthService ] 25 | }) 26 | export class AuthModule {} 27 | -------------------------------------------------------------------------------- /server/auth/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { jwtConstants } from './constants'; 2 | import { forwardRef, HttpException, HttpStatus, Inject, Injectable } from '@nestjs/common'; 3 | import { UsersService } from './../users/users.service'; 4 | import { User } from '../models/user'; 5 | import { JwtService } from '@nestjs/jwt'; 6 | @Injectable() 7 | export class AuthService { 8 | 9 | constructor( 10 | @Inject(forwardRef(() => UsersService)) 11 | private readonly usersService: UsersService, 12 | private jwtService: JwtService) { 13 | } 14 | 15 | public async validateUser(user: User): Promise { 16 | try { 17 | const existedUser: User = await this.usersService.findOneByUsernameAndPassword(user); 18 | return existedUser || null; 19 | } catch (err) { 20 | return null; 21 | } 22 | } 23 | 24 | public async login(user: User): Promise { 25 | const payload = { username: user.username, sub: user._id }; 26 | return { 27 | expireTime: jwtConstants.expireTokenTime, 28 | value: this.jwtService.sign(payload), 29 | }; 30 | } 31 | 32 | 33 | 34 | } 35 | 36 | export interface AcessToken { 37 | expireTime: string; 38 | value: string; 39 | } 40 | -------------------------------------------------------------------------------- /server/auth/constants.ts: -------------------------------------------------------------------------------- 1 | export const jwtConstants = { 2 | secret: 'secretKey', 3 | expireTokenTime: '60h' 4 | }; 5 | -------------------------------------------------------------------------------- /server/auth/guards/jwt-auth.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, UnauthorizedException } from '@nestjs/common'; 2 | import { AuthGuard } from '@nestjs/passport'; 3 | import { User } from '../../models/user'; 4 | 5 | @Injectable() 6 | export class JwtAuthGuard extends AuthGuard('jwt') { 7 | 8 | handleRequest(err, user, info) { 9 | if (err || !user) { 10 | throw err || new UnauthorizedException(); 11 | } 12 | return user; 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /server/auth/guards/local-auth.guard.ts: -------------------------------------------------------------------------------- 1 | import { AuthGuard } from '@nestjs/passport'; 2 | 3 | export class LocalAuthGuard extends AuthGuard('local') {} 4 | -------------------------------------------------------------------------------- /server/auth/strategies/jwt.strategy.ts: -------------------------------------------------------------------------------- 1 | import { ExtractJwt, Strategy } from 'passport-jwt'; 2 | import { PassportStrategy } from '@nestjs/passport'; 3 | import { Injectable } from '@nestjs/common'; 4 | import { jwtConstants } from '../constants'; 5 | 6 | @Injectable() 7 | export class JwtStrategy extends PassportStrategy(Strategy) { 8 | constructor() { 9 | super({ 10 | jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), 11 | ignoreExpiration: false, 12 | secretOrKey: jwtConstants.secret, 13 | }); 14 | } 15 | 16 | async validate(payload: JwtVerifyAnswer): Promise<{ username: string }> { 17 | return { username: payload.username }; 18 | } 19 | 20 | } 21 | 22 | export interface JwtVerifyAnswer { 23 | exp?: number; 24 | iat?: number; 25 | username: string; 26 | } 27 | -------------------------------------------------------------------------------- /server/auth/strategies/local.strategy.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, UnauthorizedException } from '@nestjs/common'; 2 | import { PassportStrategy } from '@nestjs/passport'; 3 | import { Strategy } from 'passport-local'; 4 | import { User } from '../../models/user'; 5 | import { AuthService } from '../auth.service'; 6 | import { ModuleRef, ContextIdFactory } from '@nestjs/core'; 7 | 8 | @Injectable() 9 | export class LocalStrategy extends PassportStrategy(Strategy) { 10 | constructor(private moduleRef: ModuleRef) { 11 | super({ 12 | passReqToCallback: true, 13 | }); 14 | } 15 | 16 | public async validate(request: Request, username: string, password: string ): Promise { 17 | const contextId = ContextIdFactory.getByRequest(request); 18 | 19 | const authService = await this.moduleRef.resolve(AuthService, contextId); 20 | 21 | const validateUser = await authService.validateUser(new User(username, password)); 22 | if (!validateUser) { 23 | throw new UnauthorizedException(); 24 | } 25 | return validateUser; 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /server/common/decorators/roles.decorator.ts: -------------------------------------------------------------------------------- 1 | import { ReflectMetadata } from '@nestjs/common'; 2 | 3 | export const Roles = (...roles: string[]) => ReflectMetadata('roles', roles); 4 | -------------------------------------------------------------------------------- /server/common/filters/not-found-exception.filter.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import { ExceptionFilter, Catch, NotFoundException, HttpException } from '@nestjs/common'; 3 | 4 | @Catch(NotFoundException) 5 | export class NotFoundExceptionFilter implements ExceptionFilter { 6 | catch(exception: NotFoundException, response) { 7 | // response.sendFile(path.join(__dirname, '../../../../dist/index.html')); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /server/common/guards/roles.guard.ts: -------------------------------------------------------------------------------- 1 | // import { CanActivate, ExecutionContext, Guard } from '@nestjs/common'; 2 | 3 | // import { Reflector } from '@nestjs/core'; 4 | 5 | // @Guard() 6 | // export class RolesGuard implements CanActivate { 7 | // constructor(private readonly reflector: Reflector) {} 8 | 9 | // canActivate(req, context: ExecutionContext): boolean { 10 | // const { handler } = context; 11 | // const roles = this.reflector.get('roles', handler); 12 | // if (!roles) { 13 | // return true; 14 | // } 15 | 16 | // const user = req.user; 17 | // const hasRole = () => 18 | // !!user.roles.find(role => !!roles.find(item => item === role)); 19 | // return user && user.roles && hasRole(); 20 | // } 21 | // } 22 | -------------------------------------------------------------------------------- /server/common/pipes/parse-int.pipe.ts: -------------------------------------------------------------------------------- 1 | import { HttpException, Injectable } from '@nestjs/common'; 2 | import { 3 | PipeTransform, 4 | ArgumentMetadata, 5 | HttpStatus, 6 | } from '@nestjs/common'; 7 | 8 | @Injectable() 9 | export class ParseIntPipe implements PipeTransform { 10 | async transform(value: string, metadata: ArgumentMetadata) { 11 | const val = parseInt(value, 10); 12 | if (isNaN(val)) { 13 | throw new HttpException('Validation failed', HttpStatus.BAD_REQUEST); 14 | } 15 | return val; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /server/common/pipes/parse-object-id.pipe.ts: -------------------------------------------------------------------------------- 1 | import { HttpException, Injectable } from '@nestjs/common'; 2 | import { 3 | PipeTransform, 4 | ArgumentMetadata, 5 | HttpStatus, 6 | } from '@nestjs/common'; 7 | 8 | @Injectable() 9 | export class ParseObjectIdPipe implements PipeTransform { 10 | async transform(value: string, metadata: ArgumentMetadata) { 11 | 12 | if (!value.match('^[a-fA-F0-9]{24}$')) { 13 | throw new HttpException('Validation failed', HttpStatus.BAD_REQUEST); 14 | } 15 | return value; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /server/common/pipes/validation.pipe.ts: -------------------------------------------------------------------------------- 1 | import { HttpException, Injectable } from '@nestjs/common'; 2 | import { 3 | PipeTransform, 4 | ArgumentMetadata, 5 | HttpStatus, 6 | } from '@nestjs/common'; 7 | import { validate } from 'class-validator'; 8 | import { plainToClass } from 'class-transformer'; 9 | 10 | @Injectable() 11 | export class ValidationPipe implements PipeTransform { 12 | async transform(value, metadata: ArgumentMetadata) { 13 | const { metatype } = metadata; 14 | if (!metatype || !this.toValidate(metatype)) { 15 | return value; 16 | } 17 | const object = plainToClass(metatype, value); 18 | const errors = await validate(object); 19 | if (errors.length > 0) { 20 | throw new HttpException('Validation failed', HttpStatus.BAD_REQUEST); 21 | } 22 | return value; 23 | } 24 | 25 | private toValidate(metatype): boolean { 26 | const types = [String, Boolean, Number, Array, Object]; 27 | return !types.find(type => metatype === type); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /server/events.gateway.ts: -------------------------------------------------------------------------------- 1 | import { OnModuleInit } from '@nestjs/common'; 2 | import { InjectModel } from '@nestjs/mongoose'; 3 | import { SubscribeMessage, WebSocketGateway, WebSocketServer } from '@nestjs/websockets'; 4 | import * as mongoose from 'mongoose'; 5 | import { Server, Socket } from 'socket.io'; 6 | import * as socketioJwt from 'socketio-jwt'; 7 | import { jwtConstants } from './auth/constants'; 8 | import { Room } from './models/room'; 9 | import { User } from './models/user'; 10 | 11 | const ObjectId = mongoose.Types.ObjectId; 12 | 13 | @WebSocketGateway() 14 | export class EventsGateway implements OnModuleInit { 15 | constructor(@InjectModel('Room') private roomModel) { 16 | } 17 | 18 | @WebSocketServer() 19 | server: Server; 20 | 21 | onModuleInit(): void { 22 | this.server.use(socketioJwt.authorize({ 23 | secret: jwtConstants.secret, 24 | handshake: true 25 | })); 26 | } 27 | 28 | @SubscribeMessage('message') 29 | async message(client: Socket, data): Promise> { 30 | const updatedRoom: Room = await this.roomModel.findOneAndUpdate({ _id: new ObjectId(data.room) }, 31 | { $push: { messages: { username: data.username, text: data.message } } }); 32 | const event = 'message'; 33 | const payload = { text: data.message, username: data.username, room: data.room }; 34 | client.to(data.room).emit(event, payload); 35 | return { event, data: payload }; 36 | } 37 | 38 | @SubscribeMessage('addroom') 39 | async addroom(client: Socket, data: string): Promise { 40 | const room: string = data; 41 | let newRoom: Room = await this.roomModel.findOne({ title: room }); 42 | if (newRoom) { 43 | client.emit('rooms', 'hello its voice from room'); 44 | } else { 45 | newRoom = await this.roomModel.create({ title: room }); 46 | client.emit('updatedRooms', newRoom); 47 | } 48 | } 49 | 50 | @SubscribeMessage('joinRoom') 51 | async enterRoom(client: Socket, data: string): Promise> { 52 | try { 53 | client.join(data); 54 | 55 | const clientIdList: string[] = await new Promise(resolve => { 56 | this.server 57 | .of('/') 58 | .in(data) 59 | .clients((err, clients: string[]) => resolve(clients)); 60 | }) 61 | 62 | const userNames: User[] = clientIdList 63 | .map((clientId: string) => { 64 | // socketio-jwt has incorrect type 65 | return (this.server.sockets.connected[clientId] as any).decoded_token.username; 66 | }); 67 | 68 | return { event: 'usersRoom', data: userNames }; 69 | } catch { 70 | return { event: 'usersRoom', data: [] }; 71 | } 72 | } 73 | } 74 | 75 | interface GatewayEventInterface { 76 | event: string; 77 | data: T; 78 | } 79 | -------------------------------------------------------------------------------- /server/jest.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": [ 3 | "ts", 4 | "tsx", 5 | "js", 6 | "json" 7 | ], 8 | "transform": { 9 | "^.+\\.tsx?$": "../node_modules/ts-jest/preprocessor.js" 10 | }, 11 | "testRegex": "/server/.*\\.(test|spec).(ts|tsx|js)$", 12 | "collectCoverageFrom" : ["server/**/*.{js,jsx,tsx,ts}", "!**/node_modules/**", "!**/vendor/**"], 13 | "coverageReporters": ["json", "lcov"] 14 | } -------------------------------------------------------------------------------- /server/main.ts: -------------------------------------------------------------------------------- 1 | import { NotFoundExceptionFilter } from './common/filters/not-found-exception.filter'; 2 | import { NestFactory } from '@nestjs/core'; 3 | import { AppModule } from './app.module'; 4 | import { ValidationPipe } from '@nestjs/common'; 5 | // tslint:disable-next-line: radix 6 | const port = parseInt(process.env.PORT) || 3000; 7 | async function bootstrap() { 8 | const app = await NestFactory.create(AppModule); 9 | app.enableCors(); 10 | app.useGlobalPipes(new ValidationPipe()); 11 | app.useGlobalFilters(new NotFoundExceptionFilter()); 12 | await app.listen(port); 13 | } 14 | bootstrap(); 15 | 16 | -------------------------------------------------------------------------------- /server/models/common-result.ts: -------------------------------------------------------------------------------- 1 | export class CommonResult { 2 | 3 | public title: string; 4 | 5 | public message: string; 6 | 7 | public success: boolean; 8 | 9 | public additionalInfo: T; 10 | 11 | 12 | constructor(success: boolean, message?: string, title?: string, additionalInfo?: T) { 13 | this.success = success; 14 | this.message = message; 15 | this.title = title; 16 | this.additionalInfo = additionalInfo; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /server/models/room.ts: -------------------------------------------------------------------------------- 1 | export class Room { 2 | _id: string; 3 | title: string; 4 | messages?: string[]; 5 | date: string; 6 | } 7 | -------------------------------------------------------------------------------- /server/models/user.ts: -------------------------------------------------------------------------------- 1 | export class User { 2 | username: string; 3 | password?: string; 4 | _id?: string; 5 | 6 | 7 | constructor(username: string, password: string) { 8 | this.username = username; 9 | this.password = password; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /server/rooms/rooms.controller.ts: -------------------------------------------------------------------------------- 1 | import { Room } from '../models/room'; 2 | import { Controller, Get, Req, HttpException, HttpStatus, UseGuards } from "@nestjs/common"; 3 | import { CommonResult } from '../models/common-result'; 4 | import { RoomsService } from './rooms.service'; 5 | import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; 6 | import { ExtractJwt } from 'passport-jwt'; 7 | 8 | @Controller("api/rooms") 9 | // @UseGuards(RolesGuard) 10 | export class RoomsController { 11 | constructor( 12 | private readonly roomsService: RoomsService, 13 | ) { 14 | } 15 | 16 | @UseGuards(JwtAuthGuard) 17 | @Get() 18 | async getAllRooms(@Req() request): Promise { 19 | try { 20 | const allRooms: Room[] = await this.roomsService.findAllRooms(); 21 | return allRooms.map(elem => { 22 | const newElem = { ...elem }; 23 | delete newElem.messages; 24 | return newElem; 25 | }).sort((a, b) => { 26 | return (b.date ? new Date(b.date).getTime() : 0) - (a.date ? new Date(a.date).getTime() : 0); 27 | }); 28 | } catch (err) { 29 | throw new HttpException(new CommonResult(false, 'Server error'), HttpStatus.BAD_REQUEST); 30 | } 31 | } 32 | 33 | @UseGuards(JwtAuthGuard) 34 | @Get('/room/:id') 35 | async findRoom(@Req() request): Promise { 36 | try { 37 | return await this.roomsService.findOneRooms(request.params.id); 38 | } catch (err) { 39 | throw new HttpException(new CommonResult(false, 'Server error'), HttpStatus.BAD_REQUEST); 40 | } 41 | } 42 | } 43 | 44 | -------------------------------------------------------------------------------- /server/rooms/rooms.module.ts: -------------------------------------------------------------------------------- 1 | import { UserSchema } from '../schemas/user.schema'; 2 | import { Module, NestModule, MiddlewareConsumer, RequestMethod } from '@nestjs/common'; 3 | import { RoomsController } from './rooms.controller'; 4 | import { RoomsService } from './rooms.service'; 5 | import { MongooseModule } from '@nestjs/mongoose'; 6 | // import { CorsMiddleware } from '../middlewares/cors.middleware'; 7 | import { RoomSchema } from './../schemas/room.schema'; 8 | import { EventsGateway } from '../events.gateway'; 9 | 10 | @Module({ 11 | imports: [MongooseModule.forFeature([{ name: 'Room', schema: RoomSchema }])], 12 | controllers: [RoomsController], 13 | providers: [RoomsService, EventsGateway], 14 | exports: [RoomsService] 15 | }) 16 | 17 | export class RoomsModule { 18 | } 19 | -------------------------------------------------------------------------------- /server/rooms/rooms.service.ts: -------------------------------------------------------------------------------- 1 | import { Room } from '../models/room'; 2 | import { InjectModel } from '@nestjs/mongoose'; 3 | import { Injectable, Inject } from '@nestjs/common'; 4 | 5 | @Injectable() 6 | export class RoomsService { 7 | private readonly rooms = []; 8 | 9 | constructor( @InjectModel('Room') private readonly roomModel) {} 10 | 11 | async createRoom(room: any): Promise { 12 | const createdRoom = new this.roomModel(room); 13 | const newRoom = await createdRoom.save(); 14 | return newRoom; 15 | } 16 | 17 | async findAllRooms(): Promise { 18 | return await this.roomModel.find().lean().exec(); 19 | } 20 | 21 | async findOneRooms(id: string): Promise { 22 | return await this.roomModel.findOne({ '_id': id }).lean().exec(); 23 | } 24 | 25 | async findOneByRoomname(username: string): Promise { 26 | return await this.roomModel.findOne({ 'email': username }).exec(); 27 | } 28 | 29 | async updateRoom(id: string, user: any): Promise { 30 | return await this.roomModel.findOneAndUpdate({ '_id': id }, user).exec(); 31 | } 32 | 33 | async deleteRoom(id: string) { 34 | return await this.roomModel.findOneAndDelete({ '_id': id }).exec(); 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /server/schemas/room.schema.ts: -------------------------------------------------------------------------------- 1 | import * as mongoose from 'mongoose'; 2 | export const RoomSchema = new mongoose.Schema({ 3 | title: String, 4 | connections: String, 5 | messages: [ { username: String, text: String } ], 6 | date: { type: Date, default: Date.now } 7 | }); 8 | -------------------------------------------------------------------------------- /server/schemas/user.schema.ts: -------------------------------------------------------------------------------- 1 | import * as mongoose from 'mongoose'; 2 | 3 | 4 | const SALT_WORK_FACTOR = 10; 5 | // const DEFAULT_USER_PICTURE = '/img/user.jpg'; 6 | 7 | export const UserSchema = new mongoose.Schema({ 8 | username: { type: String, required: true}, 9 | password: { type: String }, 10 | // socialId: { type: String, default: null }, 11 | // picture: { type: String, default: DEFAULT_USER_PICTURE} 12 | }); 13 | -------------------------------------------------------------------------------- /server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "outDir": "../dist/server", 6 | "mapRoot": "../dist/server", 7 | "sourceMap": true, 8 | "allowJs": true, 9 | "declaration": false, 10 | "skipLibCheck": true, 11 | "moduleResolution": "node", 12 | "emitDecoratorMetadata": true, 13 | "experimentalDecorators": true, 14 | "target": "es6" 15 | }, 16 | "include": [ 17 | "modules/**/*", 18 | "app.module.ts", 19 | "main.ts" 20 | ], 21 | "exclude": [ 22 | "node_modules", 23 | "**/*.spec.ts" 24 | ] 25 | } -------------------------------------------------------------------------------- /server/users/users.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Delete, forwardRef, Get, HttpException, HttpStatus, Inject, Param, Post, Put, Req, UseGuards, HttpCode } from "@nestjs/common"; 2 | import { CommonResult } from '../models/common-result'; 3 | import { User } from '../models/user'; 4 | import { AuthService } from './../auth/auth.service'; 5 | import { JwtAuthGuard } from './../auth/guards/jwt-auth.guard'; 6 | import { UsersService } from "./users.service"; 7 | import { JwtVerifyAnswer } from "../auth/strategies/jwt.strategy"; 8 | 9 | @Controller("api/users") 10 | // @UseGuards(RolesGuard) 11 | export class UsersController { 12 | constructor( 13 | private readonly usersService: UsersService, 14 | @Inject(forwardRef(() => AuthService)) 15 | private readonly authSesrvice: AuthService, 16 | ) { 17 | } 18 | 19 | 20 | @Post('/create') 21 | async createUser(@Req() request): Promise { 22 | try { 23 | const isNewUser: User = await this.usersService.findOne(request.body.username); 24 | if (isNewUser) { 25 | throw new HttpException(new CommonResult(false, 'User already exist'), HttpStatus.BAD_REQUEST); 26 | } 27 | const newUser: User = await this.usersService.createUser(request.body); 28 | if (newUser) { 29 | return new CommonResult(true, 'User succesfully created'); 30 | } else { 31 | throw new Error(); 32 | } 33 | } catch (err) { 34 | if (err instanceof HttpException) { 35 | throw err; 36 | } 37 | throw new HttpException(new CommonResult(false, 'Server error, try later'), HttpStatus.BAD_REQUEST); 38 | } 39 | } 40 | 41 | @HttpCode(HttpStatus.OK) 42 | @UseGuards(JwtAuthGuard) 43 | @Get('/get-profile') 44 | async getProfile(@Req() request): Promise { 45 | try { 46 | return {username: request.user.username}; 47 | } catch (err) { 48 | throw new HttpException(new CommonResult(false, 'Unexpected token'), HttpStatus.UNAUTHORIZED); 49 | } 50 | } 51 | 52 | @UseGuards(JwtAuthGuard) 53 | @Get('/get-users') 54 | async getAllUsers(@Req() request): Promise { 55 | try { 56 | return await this.usersService.findAllUsers(); 57 | } catch (err) { 58 | throw new HttpException(new CommonResult(false, 'Server error'), HttpStatus.BAD_REQUEST); 59 | } 60 | } 61 | 62 | @UseGuards(JwtAuthGuard) 63 | @Delete('/delete/:id') 64 | async deleteuser(@Param() param): Promise { 65 | try { 66 | await this.usersService.deleteUser(param.id); 67 | return new CommonResult(true, 'Successfully deleted'); 68 | } catch (err) { 69 | throw new HttpException(new CommonResult(false, 'Server error'), HttpStatus.BAD_REQUEST); 70 | } 71 | } 72 | 73 | @UseGuards(JwtAuthGuard) 74 | @Put('/edit/:id') 75 | async editUser(@Req() request, @Param() param): Promise { 76 | try { 77 | await this.usersService.updateUser(param.id, 78 | { username: request.body.username, password: request.body.password }); 79 | return new CommonResult(true, 'Successfully edited'); 80 | } catch (err) { 81 | throw new HttpException(new CommonResult(false, 'Server error'), HttpStatus.BAD_REQUEST); 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /server/users/users.module.ts: -------------------------------------------------------------------------------- 1 | import { UserSchema } from '../schemas/user.schema'; 2 | import { Module, MiddlewareConsumer, RequestMethod, forwardRef } from '@nestjs/common'; 3 | import { UsersController } from './users.controller'; 4 | import { UsersService } from './users.service'; 5 | import { MongooseModule } from '@nestjs/mongoose'; 6 | import { AuthModule } from './../auth/auth.module'; 7 | 8 | @Module({ 9 | imports: [ 10 | forwardRef(() => AuthModule), 11 | MongooseModule.forFeature([ { name: 'User', schema: UserSchema } ]) 12 | ], 13 | controllers: [ UsersController ], 14 | providers: [ UsersService,], 15 | exports: [ UsersService ] 16 | }) 17 | 18 | export class UsersModule { 19 | } 20 | -------------------------------------------------------------------------------- /server/users/users.service.ts: -------------------------------------------------------------------------------- 1 | import { User } from '../models/user'; 2 | import { Injectable } from '@nestjs/common'; 3 | import { InjectModel } from '@nestjs/mongoose'; 4 | 5 | @Injectable() 6 | export class UsersService { 7 | 8 | constructor( @InjectModel('User') private readonly userModel) { 9 | } 10 | 11 | async createUser(user: any): Promise { 12 | const createdUser = new this.userModel(user); 13 | const takeUser = await createdUser.save(); 14 | return takeUser; 15 | } 16 | 17 | async findAllUsers(): Promise { 18 | return await this.userModel.find().exec(); 19 | } 20 | 21 | async findOne(username: string): Promise { 22 | return await this.userModel.findOne({ 'username': username }).exec(); 23 | } 24 | 25 | async findOneByUsernameAndPassword(user: User): Promise { 26 | return await this.userModel.findOne({ 'username': user.username, 'password': user.password }).exec(); 27 | } 28 | 29 | async updateUser(id: string, user: any): Promise { 30 | return await this.userModel.findOneAndUpdate({ '_id': id }, user).exec(); 31 | } 32 | 33 | async deleteUser(id: string): Promise { 34 | return await this.userModel.findOneAndRemove({ '_id': id }).exec(); 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/app/app.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georgyim/angular-chat/620b5ddb0a1bc643b049fe2fa6fbdd55fadad6b3/src/app/app.component.css -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { SnotifyHelperService } from './common/snotify-helper.service'; 3 | 4 | @Component({ 5 | selector: 'app-root', 6 | templateUrl: './app.component.html', 7 | styleUrls: [ './app.component.css' ] 8 | }) 9 | export class AppComponent { 10 | 11 | constructor(public snotifyHelper: SnotifyHelperService) { 12 | } 13 | 14 | public getSnotifyStyle(): string { 15 | return this.snotifyHelper.getStyle(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { ContainerComponent } from './components/container/container.component'; 2 | // Core 3 | 4 | import { NgModule } from '@angular/core'; 5 | import { FormsModule, ReactiveFormsModule } from '@angular/forms'; 6 | import { BrowserModule } from '@angular/platform-browser'; 7 | import { Routes, RouterModule } from '@angular/router'; 8 | import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http'; 9 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 10 | 11 | 12 | // Modules 13 | import { SharedModule } from './shared/shared.module'; 14 | // import { UserControlModule } from './components/users-control/user-control.module'; 15 | import { SnotifyModule, SnotifyService, ToastDefaults } from 'ng-snotify'; 16 | 17 | // Other 18 | import { SocketIoModule } from 'ngx-socket-io'; 19 | import { AppComponent } from './app.component'; 20 | import { MenuComponent } from './components/menu/menu.component'; 21 | import { ChatComponent } from './components/chat/chat.component'; 22 | import { MessageComponent } from './components/chat/message/message.component'; 23 | import { AuthInterceptor } from './services/auth/auth.interceptor'; 24 | import { AuthGuard } from './services/auth/auth-guard.service'; 25 | import { UserlistComponent } from './components/chat/userlist/userlist.component'; 26 | import { ErrorHandlerInterceptor } from './common/error-handler.interceptor'; 27 | import { SnotifyHelperService } from './common/snotify-helper.service'; 28 | import { LoginComponent } from './components/login/login.component'; 29 | 30 | // App routes 31 | const routes: Routes = [ 32 | { 33 | path: 'auth', 34 | component: LoginComponent 35 | }, 36 | { 37 | path: '', 38 | component: ContainerComponent, 39 | canActivateChild: [AuthGuard], 40 | children: [ 41 | { 42 | path: 'chat', 43 | component: ChatComponent, 44 | }, 45 | { 46 | path: 'user-control', 47 | loadChildren: () => import('./components/users-control/user-control.module').then(m => m.UserControlModule) 48 | }, 49 | { 50 | path: '', 51 | redirectTo: 'chat', 52 | pathMatch: 'full' 53 | }, 54 | ] 55 | }, 56 | 57 | ]; 58 | 59 | @NgModule({ 60 | entryComponents: [], 61 | declarations: [ 62 | AppComponent, 63 | MenuComponent, 64 | ChatComponent, 65 | MessageComponent, 66 | UserlistComponent, 67 | ContainerComponent, 68 | LoginComponent 69 | ], 70 | imports: [ 71 | RouterModule.forRoot(routes), 72 | SharedModule.forRoot(), 73 | // UserControlModule, 74 | ReactiveFormsModule, 75 | BrowserModule.withServerTransition({ appId: 'serverApp' }), 76 | BrowserAnimationsModule, 77 | FormsModule, 78 | SocketIoModule, 79 | HttpClientModule, 80 | SnotifyModule, 81 | ], 82 | providers: [ 83 | AuthGuard, 84 | { provide: 'SnotifyToastConfig', useValue: ToastDefaults }, 85 | SnotifyService, 86 | SnotifyHelperService, 87 | { 88 | provide: HTTP_INTERCEPTORS, 89 | useClass: AuthInterceptor, 90 | multi: true 91 | }, 92 | { 93 | provide: HTTP_INTERCEPTORS, 94 | useClass: ErrorHandlerInterceptor, 95 | multi: true 96 | }, 97 | ], 98 | bootstrap: [AppComponent] 99 | }) 100 | 101 | export class AppModule { 102 | } 103 | -------------------------------------------------------------------------------- /src/app/common/ang-material.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { MatButtonModule } from '@angular/material/button'; 3 | import { MatCheckboxModule } from '@angular/material/checkbox'; 4 | import { MatDialogModule } from '@angular/material/dialog'; 5 | import { MatIconModule } from '@angular/material/icon'; 6 | import { CommonModule } from '@angular/common'; 7 | 8 | @NgModule({ 9 | imports: [ CommonModule, MatButtonModule, MatCheckboxModule, MatDialogModule, MatIconModule ], 10 | exports: [ MatButtonModule, MatCheckboxModule, MatDialogModule, MatIconModule ], 11 | }) 12 | export class MyOwnCustomMaterialModule { 13 | } 14 | -------------------------------------------------------------------------------- /src/app/common/animation.ts: -------------------------------------------------------------------------------- 1 | import { 2 | animate, AnimationTriggerMetadata, 3 | keyframes, 4 | style, 5 | transition, 6 | trigger 7 | } from '@angular/animations'; 8 | 9 | export const fadeOffAnimation: AnimationTriggerMetadata = 10 | trigger('fadeOffAnimation', [ 11 | transition('* => void', animate('500ms ease-in', style({ 12 | opacity: 0, 13 | transform: 'translateX(-155px)', 14 | }))), 15 | transition('void => *', animate('500ms ease-in', keyframes([ 16 | style({opacity: 0, transform: 'translateY(15px)', offset: 0}), 17 | style({opacity: .5, transform: 'translateY(7px)', offset: 0.5}), 18 | style({opacity: 1, transform: 'translateY(0)', offset: 1.0}), 19 | ]))), 20 | ]); 21 | 22 | -------------------------------------------------------------------------------- /src/app/common/error-handler.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { HttpErrorResponse, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http'; 2 | import { Injectable } from '@angular/core'; 3 | import { Observable, of, throwError, EMPTY } from 'rxjs'; 4 | import { catchError } from 'rxjs/operators'; 5 | import { SnotifyHelperService } from './snotify-helper.service'; 6 | import { AuthenticationService } from '../services/auth/authentication.service'; 7 | 8 | 9 | @Injectable() 10 | export class ErrorHandlerInterceptor implements HttpInterceptor { 11 | 12 | private readonly defaultTitle: string = 'Error'; 13 | 14 | constructor(private snotifyHelper: SnotifyHelperService, private authService: AuthenticationService) { 15 | } 16 | 17 | public intercept(request: HttpRequest, next: HttpHandler): Observable> { 18 | return next.handle(request) 19 | .pipe( 20 | catchError((err) => { 21 | if (err instanceof HttpErrorResponse && this.checkErrorHaveMessage(err.error)) { 22 | this.snotifyHelper.onError(err.error.message, err.error.title || this.defaultTitle); 23 | } 24 | return throwError(err); 25 | }) 26 | ); 27 | } 28 | 29 | private checkErrorHaveMessage(value: any): boolean { 30 | return Boolean(value && value.message); 31 | } 32 | } 33 | 34 | -------------------------------------------------------------------------------- /src/app/common/snotify-helper.service.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable } from '@angular/core'; 2 | import { Observable } from 'rxjs'; 3 | import { SnotifyPosition, SnotifyService, SnotifyToastConfig } from 'ng-snotify'; 4 | 5 | 6 | @Injectable() 7 | export class SnotifyHelperService { 8 | 9 | private readonly style = 'material'; 10 | 11 | private readonly position: SnotifyPosition = SnotifyPosition.centerCenter; 12 | 13 | public constructor(private snotifyService: SnotifyService, 14 | @Inject('SnotifyToastConfig') public snotifyDefaultCfg: SnotifyToastConfig) { 15 | } 16 | 17 | public getStyle(): string { 18 | return this.style; 19 | } 20 | 21 | /* 22 | Change global configuration 23 | */ 24 | public getConfig(): SnotifyToastConfig { 25 | const stotifyCfg: SnotifyToastConfig = { ...this.snotifyDefaultCfg }; 26 | stotifyCfg.position = this.position; 27 | return stotifyCfg; 28 | } 29 | 30 | public onSuccess(body: string, title: string) { 31 | this.snotifyService.success(body, title, this.getConfig()); 32 | } 33 | 34 | public onInfo(body: string, title: string) { 35 | this.snotifyService.info(body, title, this.getConfig()); 36 | } 37 | 38 | public onError(body: string, title: string) { 39 | this.snotifyService.error(body, title, this.getConfig()); 40 | } 41 | 42 | public onWarning(body: string, title: string) { 43 | this.snotifyService.warning(body, title, this.getConfig()); 44 | } 45 | 46 | public onSimple(body: string, title: string) { 47 | 48 | // const icon = `assets/custom-svg.svg`; 49 | const icon = `https://placehold.it/48x100`; 50 | 51 | this.snotifyService.simple(body, title, { 52 | ...this.getConfig(), 53 | icon: icon 54 | }); 55 | } 56 | 57 | public onAsyncLoading(body: string, title: string) { 58 | const errorAction = Observable.create(observer => { 59 | setTimeout(() => { 60 | observer.error({ 61 | title: 'Error', 62 | body: 'Example. Error 404. Service not found', 63 | }); 64 | }, 2000); 65 | }); 66 | 67 | const successAction = Observable.create(observer => { 68 | setTimeout(() => { 69 | observer.next({ 70 | body: 'Still loading.....', 71 | }); 72 | }, 2000); 73 | 74 | setTimeout(() => { 75 | observer.next({ 76 | title: 'Success', 77 | body: 'Example. Data loaded!', 78 | config: { 79 | closeOnClick: true, 80 | timeout: 5000, 81 | showProgressBar: true 82 | } 83 | }); 84 | observer.complete(); 85 | }, 5000); 86 | }); 87 | 88 | /* 89 | You should pass Promise or Observable of type Snotify to change some data or do some other actions 90 | More information how to work with observables: 91 | https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/operators/create.md 92 | */ 93 | const { timeout, ...config } = this.getConfig(); // Omit timeout 94 | this.snotifyService.async('This will resolve with error', 'Async', errorAction, config); 95 | this.snotifyService.async('This will resolve with success', successAction, config); 96 | this.snotifyService.async('Called with promise', 'Error async', 97 | new Promise((resolve, reject) => { 98 | setTimeout(() => reject({ 99 | title: 'Error!!!', 100 | body: 'We got an example error!', 101 | config: { 102 | closeOnClick: true 103 | } 104 | }), 1000); 105 | setTimeout(() => resolve(), 1500); 106 | }), config); 107 | } 108 | 109 | public onConfirmation(body: string, title: string) { 110 | /* 111 | Here we pass an buttons array, which contains of 2 element of type SnotifyButton 112 | */ 113 | const { timeout, closeOnClick, ...config } = this.getConfig(); // Omit props what i don't need 114 | this.snotifyService.confirm(body, title, { 115 | ...config, 116 | buttons: [ 117 | { text: 'Yes', action: () => console.log('Clicked: Yes'), bold: false }, 118 | { text: 'No', action: () => console.log('Clicked: No') }, 119 | { 120 | text: 'Later', action: (toast) => { 121 | console.log('Clicked: Later'); 122 | this.snotifyService.remove(toast.id); 123 | } 124 | }, 125 | { 126 | text: 'Close', action: (toast) => { 127 | console.log('Clicked: Close'); 128 | this.snotifyService.remove(toast.id); 129 | }, bold: true 130 | }, 131 | ] 132 | }); 133 | } 134 | 135 | public onPrompt(body: string, title: string) { 136 | /* 137 | Here we pass an buttons array, which contains of 2 element of type SnotifyButton 138 | At the action of the first button we can get what user entered into input field. 139 | At the second we can't get it. But we can remove this toast 140 | */ 141 | const { timeout, closeOnClick, ...config } = this.getConfig(); // Omit props what i don't need 142 | this.snotifyService.prompt(body, title, { 143 | ...config, 144 | buttons: [ 145 | { text: 'Yes', action: (toast) => console.log('Said Yes: ' + toast.value) }, 146 | { 147 | text: 'No', action: (toast) => { 148 | console.log('Said No: ' + toast.value); 149 | this.snotifyService.remove(toast.id); 150 | } 151 | }, 152 | ], 153 | placeholder: 'Enter "ng-snotify" to validate this input' // Max-length = 40 154 | }).on('input', (toast) => { 155 | console.log(toast.value) 156 | toast.valid = !!toast.value.match('ng-snotify'); 157 | }); 158 | } 159 | 160 | public onHtml(): void { 161 | const html = `
Html Bold Title
162 |
Html toast content
`; 163 | this.snotifyService.html(html, this.getConfig()); 164 | } 165 | 166 | 167 | public onClear(): void { 168 | this.snotifyService.clear(); 169 | } 170 | 171 | } 172 | -------------------------------------------------------------------------------- /src/app/components/chat/chat.component.css: -------------------------------------------------------------------------------- 1 | .chat-component { 2 | display: flex; 3 | justify-content: center; 4 | } 5 | 6 | .chat-container { 7 | display: flex; 8 | flex-flow: column; 9 | margin-top: 40px; 10 | width: 1240px; 11 | height: 800px; 12 | background: #f2f2fd; 13 | box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.2), 0 5px 5px 0 rgba(0, 0, 0, 0.24); 14 | } 15 | .chat__wrapper { 16 | height: calc(100% - 68px); 17 | display: flex; 18 | flex-flow: row; 19 | } 20 | 21 | .title { 22 | margin: 20px; 23 | font-size: 28px; 24 | color: #464b57; 25 | font-weight: bold; 26 | } 27 | 28 | .rooms { 29 | background: #f2f2fd; 30 | height: 100%; 31 | width: 300px; 32 | margin-right: 30px; 33 | padding-left: 20px; 34 | display: flex; 35 | flex-flow: column; 36 | overflow: auto; 37 | } 38 | 39 | .rooms__list { 40 | overflow: auto; 41 | padding-right: 5px; 42 | margin-bottom: 25px; 43 | } 44 | 45 | .rooms li { 46 | height: 50px; 47 | display: flex; 48 | justify-content: flex-start; 49 | align-items: center; 50 | margin-bottom: 20px; 51 | cursor: pointer; 52 | background: #ffffff; 53 | } 54 | 55 | .rooms li span { 56 | overflow: hidden; 57 | text-overflow: ellipsis; 58 | } 59 | 60 | .room-icon { 61 | margin: 10px; 62 | width: 22px; 63 | height: 30px; 64 | font-size: 16px; 65 | line-height: 30px; 66 | color: #eaa4bf; 67 | } 68 | 69 | .chat { 70 | padding-top: 20px; 71 | width: 450px; 72 | /* background: rgba(255,255,255,0.7); */ 73 | } 74 | 75 | .chat__header { 76 | margin-bottom: 20px; 77 | height: 40px; 78 | font-weight: bold; 79 | display: flex; 80 | align-items: center; 81 | font-size: 18px; 82 | color: #4c515d; 83 | border-bottom: 1px solid #d3d6df; 84 | overflow: hidden; 85 | } 86 | 87 | .chat__header i { 88 | margin: 10px; 89 | } 90 | 91 | .chat__header span { 92 | text-overflow: ellipsis; 93 | overflow: hidden; 94 | } 95 | 96 | .addroom__button { 97 | cursor: pointer; 98 | } 99 | 100 | .chat__content { 101 | display: flex; 102 | height: 550px; 103 | margin-bottom: 30px; 104 | overflow: auto; 105 | } 106 | 107 | .chat__footer { 108 | display: flex; 109 | } 110 | 111 | .addroom { 112 | margin-bottom: 20px; 113 | padding-top: 20px; 114 | } 115 | 116 | .addroom input { 117 | background-color: transparent; 118 | border-bottom: 1px solid #d3d6df; 119 | color: #84888e; 120 | height: 40px; 121 | width: 100%; 122 | margin-bottom: 10px; 123 | } 124 | 125 | .addroom input:focus { 126 | border-bottom: 1px solid #2b8cf1; 127 | } 128 | 129 | .addroom button { 130 | background: #2b8cf1; 131 | height: 40px; 132 | border: none; 133 | color: white; 134 | padding: 0 30px; 135 | } 136 | 137 | .messages { 138 | width: 100%; 139 | } 140 | 141 | .text-message { 142 | width: 100%; 143 | position: relative; 144 | } 145 | 146 | .text-message input { 147 | width: 100%; 148 | height: 50px; 149 | outline: none; 150 | padding-left: 20px; 151 | margin-bottom: 5px; 152 | box-shadow: 0 0 1px rgba(0, 0, 0, 0.1); 153 | } 154 | 155 | .send-message { 156 | position: absolute; 157 | right: 13px; 158 | top: 9px; 159 | background: #5ba3ee; 160 | color: white; 161 | display: flex; 162 | height: 32px; 163 | width: 32px; 164 | justify-content: center; 165 | align-items: center; 166 | border-radius: 50px; 167 | font-size: 22px; 168 | cursor: pointer; 169 | } 170 | 171 | .users { 172 | width: 250px; 173 | margin-left: 15px; 174 | } 175 | 176 | /* 177 | * SCROLL BAR 178 | */ 179 | 180 | .rooms__list::-webkit-scrollbar, 181 | .chat__content::-webkit-scrollbar { 182 | width: 8px; 183 | } 184 | 185 | .chat__content::-webkit-scrollbar-thumb, 186 | .rooms__list::-webkit-scrollbar-thumb { 187 | border-radius: 10px; 188 | background-color: #c1c5cd; 189 | } 190 | -------------------------------------------------------------------------------- /src/app/components/chat/chat.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

Chat

5 |
6 |
7 |
8 |
9 | 15 | 16 |
17 |
    18 |
  • 22 | star_rate 23 | {{ room.title }} 24 |
  • 25 |
26 |
27 |
28 |
29 | label_outline 30 | {{ title }} 31 |
32 |
33 |
34 | 39 |
40 | 41 |
42 | 55 |
56 |
57 | 58 |
59 |
60 |
61 |
62 | -------------------------------------------------------------------------------- /src/app/components/chat/chat.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnDestroy, OnInit } from '@angular/core'; 2 | import { Subject } from 'rxjs'; 3 | import { takeUntil } from 'rxjs/operators'; 4 | import { Message } from '../..//entities/message'; 5 | import { Room } from '../..//entities/room'; 6 | import { User } from '../../entities/user'; 7 | import { ChatService } from '../../services/chat-sockets/socket.service'; 8 | import { RoomService } from './../../services/rooms/rooms.service'; 9 | import { UsersService } from '../../services/users/users.service'; 10 | 11 | 12 | @Component({ 13 | selector: 'app-chat', 14 | templateUrl: './chat.component.html', 15 | styleUrls: ['./chat.component.css'] 16 | }) 17 | export class ChatComponent implements OnInit, OnDestroy { 18 | public text: string; 19 | public name: string; 20 | public newRoom: string; 21 | public messages: Message[] = []; 22 | public rooms: Room[] = []; 23 | public title: string; 24 | public currentRoomId: string; 25 | public users: User[] = []; 26 | 27 | private unsubscribeOnDestroy = new Subject(); 28 | 29 | public constructor( 30 | private chatService: ChatService, 31 | private userService: UsersService, 32 | private roomService: RoomService, 33 | ) { 34 | this.title = 'Eat a carrot )'; 35 | this.messages.push(new Message('Bugz Bunny', 'Choose the room =)')); 36 | } 37 | 38 | public ngOnInit() { 39 | this.getProfile(); 40 | this.chatService 41 | .getMessage() 42 | .pipe(takeUntil(this.unsubscribeOnDestroy)) 43 | .subscribe((data: Message) => { 44 | this.messages.push(data); 45 | }); 46 | this.getRooms(); 47 | this.getRoomsFromSocket(); 48 | this.getUsersList(); 49 | } 50 | 51 | public ngOnDestroy() { 52 | this.unsubscribeOnDestroy.next(); 53 | this.unsubscribeOnDestroy.complete(); 54 | } 55 | 56 | public send(): void { 57 | if (this.text && this.text.trim() !== '') { 58 | this.chatService.sendMessage(this.text, this.name, this.currentRoomId); 59 | this.text = ''; 60 | } 61 | } 62 | 63 | public addnewroom(): void { 64 | if (this.newRoom && this.newRoom.trim() !== '') { 65 | this.chatService.addRoom(this.newRoom); 66 | } 67 | } 68 | 69 | public getRoomsFromSocket(): void { 70 | this.chatService.getRooms() 71 | .pipe(takeUntil(this.unsubscribeOnDestroy)) 72 | .subscribe((res: Room) => { 73 | const updatedRooms = [res, ...this.rooms]; 74 | this.rooms = updatedRooms; 75 | }); 76 | } 77 | 78 | 79 | public getRooms(): void { 80 | this.roomService.getRooms() 81 | .pipe(takeUntil(this.unsubscribeOnDestroy)) 82 | .subscribe((res: Room[]) => { 83 | this.rooms = res; 84 | }); 85 | } 86 | 87 | public joinRoom(room): void { 88 | this.chatService.joinRoom(room); 89 | this.currentRoomId = room; 90 | } 91 | 92 | public getRoom(id): void { 93 | this.roomService.getRoom(id) 94 | .pipe(takeUntil(this.unsubscribeOnDestroy)) 95 | .subscribe((res: Room) => { 96 | this.messages = res.messages; 97 | this.title = res.title; 98 | }); 99 | } 100 | 101 | public getProfile(): void { 102 | this.userService.getProfile() 103 | .pipe(takeUntil(this.unsubscribeOnDestroy)) 104 | .subscribe((res: User) => { 105 | this.name = res.username; 106 | }); 107 | } 108 | 109 | public getUsersList(): void { 110 | this.chatService.getUsers() 111 | .pipe(takeUntil(this.unsubscribeOnDestroy)) 112 | .subscribe((users: User[]) => { 113 | this.users = users; 114 | }); 115 | } 116 | 117 | } 118 | -------------------------------------------------------------------------------- /src/app/components/chat/chat.module.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georgyim/angular-chat/620b5ddb0a1bc643b049fe2fa6fbdd55fadad6b3/src/app/components/chat/chat.module.ts -------------------------------------------------------------------------------- /src/app/components/chat/message/message.component.css: -------------------------------------------------------------------------------- 1 | .message-box { 2 | display: flex; 3 | margin-top: 15px; 4 | min-height: 40px; 5 | background: #e9e8ed; 6 | border-radius: 25px; 7 | } 8 | 9 | .message-box span.message-text { 10 | display: inline-flex; 11 | width: 100%; 12 | align-items: center; 13 | line-height: 1.5; 14 | padding: 10px 25px; 15 | color: #71737b; 16 | } 17 | 18 | .message-underline span.message-name { 19 | display: inline-flex; 20 | width: 100%; 21 | justify-content: flex-end; 22 | padding-right: 25px; 23 | color: #007bff; 24 | } 25 | -------------------------------------------------------------------------------- /src/app/components/chat/message/message.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | {{text}} 4 | 5 |
6 |
7 | 8 | {{name}} 9 | 10 |
11 | -------------------------------------------------------------------------------- /src/app/components/chat/message/message.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { MessageComponent } from './message.component'; 4 | 5 | describe('MessageComponent', () => { 6 | let component: MessageComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ MessageComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(MessageComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/components/chat/message/message.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Input, ChangeDetectionStrategy } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-message', 5 | templateUrl: './message.component.html', 6 | styleUrls: ['./message.component.css'], 7 | changeDetection: ChangeDetectionStrategy.OnPush 8 | }) 9 | export class MessageComponent implements OnInit { 10 | @Input() name: string; 11 | @Input() text: string; 12 | 13 | constructor() { } 14 | 15 | ngOnInit() { 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/app/components/chat/userlist/userlist.component.css: -------------------------------------------------------------------------------- 1 | :host { 2 | display: block; 3 | margin-top: 15px; 4 | width: 100%; 5 | height: 100%; 6 | overflow: hidden; 7 | } 8 | 9 | .title { 10 | margin-bottom: 15px; 11 | } 12 | 13 | .user-block { 14 | display: block; 15 | height: 30px; 16 | padding: 0 10px; 17 | border: 1px solid green; 18 | } -------------------------------------------------------------------------------- /src/app/components/chat/userlist/userlist.component.html: -------------------------------------------------------------------------------- 1 |
2 |

Userlist

3 |
4 | 5 |
    6 |
  • {{user}}
  • 7 |
8 | 9 |
10 | -------------------------------------------------------------------------------- /src/app/components/chat/userlist/userlist.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { UserlistComponent } from './userlist.component'; 4 | 5 | describe('UserlistComponent', () => { 6 | let component: UserlistComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ UserlistComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(UserlistComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/components/chat/userlist/userlist.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Input, ChangeDetectionStrategy } from '@angular/core'; 2 | import { User } from '../../../entities/user'; 3 | 4 | @Component({ 5 | selector: 'app-userlist', 6 | templateUrl: './userlist.component.html', 7 | styleUrls: ['./userlist.component.css'], 8 | changeDetection: ChangeDetectionStrategy.OnPush 9 | }) 10 | export class UserlistComponent implements OnInit { 11 | 12 | @Input() users: User[]; 13 | 14 | constructor() { } 15 | 16 | ngOnInit() { 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/app/components/container/container.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georgyim/angular-chat/620b5ddb0a1bc643b049fe2fa6fbdd55fadad6b3/src/app/components/container/container.component.css -------------------------------------------------------------------------------- /src/app/components/container/container.component.html: -------------------------------------------------------------------------------- 1 | 2 | > 3 | -------------------------------------------------------------------------------- /src/app/components/container/container.component.spec.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-unused-variable */ 2 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 3 | import { By } from '@angular/platform-browser'; 4 | import { DebugElement } from '@angular/core'; 5 | 6 | import { ContainerComponent } from './container.component'; 7 | 8 | describe('ContainerComponent', () => { 9 | let component: ContainerComponent; 10 | let fixture: ComponentFixture; 11 | 12 | beforeEach(async(() => { 13 | TestBed.configureTestingModule({ 14 | declarations: [ ContainerComponent ] 15 | }) 16 | .compileComponents(); 17 | })); 18 | 19 | beforeEach(() => { 20 | fixture = TestBed.createComponent(ContainerComponent); 21 | component = fixture.componentInstance; 22 | fixture.detectChanges(); 23 | }); 24 | 25 | it('should create', () => { 26 | expect(component).toBeTruthy(); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /src/app/components/container/container.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-container', 5 | templateUrl: './container.component.html', 6 | styleUrls: ['./container.component.css'] 7 | }) 8 | export class ContainerComponent implements OnInit { 9 | 10 | constructor() { 11 | } 12 | 13 | ngOnInit() { 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/app/components/login/login.component.css: -------------------------------------------------------------------------------- 1 | .login { 2 | display: flex; 3 | justify-content: center; 4 | height: 100%; 5 | padding-top: 15vh; 6 | } 7 | 8 | .login__container { 9 | display: flex; 10 | flex-flow: column; 11 | padding: 15px 30px; 12 | width: 400px; 13 | height: 400px; 14 | background-color: #F2F5F8; 15 | box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.2), 0 5px 5px 0 rgba(0, 0, 0, 0.24); 16 | } 17 | 18 | .login__title { 19 | text-align: center; 20 | font-size: 26px; 21 | margin-bottom: 50px; 22 | color: #474c58; 23 | } 24 | 25 | .login__subtitle { 26 | display: flex; 27 | text-transform: uppercase; 28 | margin-bottom: 5px; 29 | font-size: 14px; 30 | } 31 | 32 | .login__form { 33 | display: flex; 34 | flex-flow: column; 35 | width: 100%; 36 | } 37 | 38 | .login__username, .login__password { 39 | height: 100px; 40 | text-align: center; 41 | margin-bottom: 25px; 42 | } 43 | 44 | .login__input { 45 | height: 40px; 46 | width: 100%; 47 | padding-left: 15px; 48 | outline: none; 49 | border: 1px solid #e0dfdf; 50 | } 51 | 52 | .login__input:focus { 53 | border: 2px solid #2dacf8; 54 | } 55 | 56 | .login__buttons { 57 | display: flex; 58 | flex-flow: row; 59 | align-items: center; 60 | justify-content: space-between; 61 | } 62 | 63 | .login__button { 64 | background: #2b8cf1; 65 | height: 40px; 66 | border: none; 67 | color: white; 68 | padding: 0 30px; 69 | cursor: pointer; 70 | } 71 | 72 | .login__validate-message { 73 | display: flex; 74 | -webkit-box-pack: center; 75 | justify-content: center; 76 | -webkit-box-align: center; 77 | align-items: center; 78 | color: #fa464d; 79 | margin-top: 2px; 80 | } 81 | 82 | .disabled { 83 | cursor: not-allowed !important; 84 | } 85 | 86 | .login__input.ng-pristine.ng-invalid.ng-touched { 87 | border: 2px solid #fa454d; 88 | } 89 | 90 | /* .alert { 91 | margin-bottom: 20px; 92 | text-align: center; 93 | font-weight: bold; 94 | } 95 | 96 | .alert-danger { 97 | color: red; 98 | } 99 | 100 | .alert-success { 101 | color: green; 102 | } */ 103 | -------------------------------------------------------------------------------- /src/app/components/login/login.component.html: -------------------------------------------------------------------------------- 1 | 70 | -------------------------------------------------------------------------------- /src/app/components/login/login.component.ts: -------------------------------------------------------------------------------- 1 | import { UsersService } from '../../services/users/users.service'; 2 | import { Component } from '@angular/core'; 3 | import { FormControl, FormGroup, Validators } from '@angular/forms'; 4 | import { SnotifyHelperService } from '../../common/snotify-helper.service'; 5 | import { User } from '../../entities/user'; 6 | import { AuthenticationService } from './../../services/auth/authentication.service'; 7 | 8 | @Component({ 9 | selector: 'app-login', 10 | templateUrl: './login.component.html', 11 | styleUrls: ['./login.component.css'] 12 | }) 13 | export class LoginComponent { 14 | 15 | public loginForm: FormGroup = new FormGroup({ 16 | username: new FormControl('', [Validators.minLength(2), Validators.maxLength(10), Validators.required]), 17 | password: new FormControl('', [Validators.minLength(2), Validators.maxLength(10), Validators.required]), 18 | }); 19 | 20 | public user: User = new User(); 21 | 22 | constructor( 23 | private authenticationService: AuthenticationService, 24 | private snotify: SnotifyHelperService, 25 | private userService: UsersService 26 | ) { 27 | } 28 | 29 | login() { 30 | this.authenticationService.login(this.user) 31 | .subscribe(); 32 | } 33 | 34 | register() { 35 | this.userService.createUser(this.user) 36 | .subscribe((res: User) => { 37 | this.snotify.onSuccess('User succesfully register, now you can login.', null) 38 | }); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/app/components/menu/menu.component.css: -------------------------------------------------------------------------------- 1 | .header { 2 | min-height: 50px; 3 | background: #e0dffa; 4 | display: flex; 5 | flex-flow: row; 6 | justify-content: center; 7 | -webkit-box-shadow: 0px 0px 63px -11px rgba(15, 15, 15, 1); 8 | -moz-box-shadow: 0px 0px 63px -11px rgba(15, 15, 15, 1); 9 | box-shadow: 0px 0px 63px -11px rgba(15, 15, 15, 1); 10 | } 11 | 12 | .menu { 13 | display: flex; 14 | flex-flow: row; 15 | width: 1240px; 16 | justify-content: space-between; 17 | } 18 | 19 | .item { 20 | width: 250px; 21 | min-width: 250px; 22 | cursor: pointer; 23 | } 24 | 25 | .nav-link { 26 | color: #4f87ca; 27 | text-transform: uppercase; 28 | font-size: 16px; 29 | font-weight: bold; 30 | cursor: pointer; 31 | } 32 | -------------------------------------------------------------------------------- /src/app/components/menu/menu.component.html: -------------------------------------------------------------------------------- 1 | 2 | 28 | 29 | -------------------------------------------------------------------------------- /src/app/components/menu/menu.component.ts: -------------------------------------------------------------------------------- 1 | import { AuthenticationService } from './../../services/auth/authentication.service'; 2 | import { Component } from '@angular/core'; 3 | 4 | @Component({ 5 | selector: 'app-menu', 6 | templateUrl: './menu.component.html', 7 | styleUrls: [ './menu.component.css' ] 8 | }) 9 | export class MenuComponent { 10 | 11 | public constructor(public authService: AuthenticationService) { 12 | } 13 | 14 | public logout(): void { 15 | this.authService.logout(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/app/components/users-control/add-user/add-user.component.css: -------------------------------------------------------------------------------- 1 | .add-user { 2 | width: 300px; 3 | } 4 | 5 | .add-user__fields { 6 | display: flex; 7 | flex-direction: column; 8 | margin-bottom: 35px; 9 | } 10 | 11 | .add-user__field--username { 12 | margin-bottom: 15px; 13 | } 14 | 15 | .add-user__field { 16 | height: 40px; 17 | width: 100%; 18 | padding-left: 15px; 19 | outline: none; 20 | border: 1px solid #e0dfdf; 21 | } 22 | 23 | .add-user__field:focus { 24 | border: 2px solid #2dacf8; 25 | } 26 | 27 | .add-user__buttons { 28 | display: flex; 29 | flex-flow: row; 30 | align-items: center; 31 | justify-content: space-between; 32 | } 33 | 34 | .add-user__buttons button { 35 | background: #2b8cf1; 36 | height: 40px; 37 | border: none; 38 | color: white; 39 | padding: 0 30px; 40 | cursor: pointer; 41 | } 42 | 43 | .btn-close { 44 | background: #f6b1bb; 45 | } 46 | 47 | .error-message { 48 | color: #0b0a0c; 49 | display: flex; 50 | justify-content: center; 51 | color: #fa464d; 52 | margin-top: 2px; 53 | } 54 | -------------------------------------------------------------------------------- /src/app/components/users-control/add-user/add-user.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 9 | 15 | 16 | 17 |
18 | {{ error }} 19 |
20 |
21 |
22 | 23 |
24 | 31 | 32 |
33 |
34 | -------------------------------------------------------------------------------- /src/app/components/users-control/add-user/add-user.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Inject } from '@angular/core'; 2 | import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; 3 | import { UsersService } from './../../../services/users/users.service'; 4 | import { User } from '../../../entities/user'; 5 | import { CommonResult } from '../../../entities/common-result'; 6 | 7 | @Component({ 8 | selector: 'app-add-user', 9 | templateUrl: './add-user.component.html', 10 | styleUrls: ['./add-user.component.css'] 11 | }) 12 | export class AddUserComponent { 13 | 14 | /** 15 | * User 16 | */ 17 | public user: User = new User(null, null); 18 | 19 | /** 20 | * Error 21 | */ 22 | public error: any; 23 | 24 | /** 25 | * Edit 26 | */ 27 | public edit: boolean = false; 28 | 29 | public constructor( 30 | public dialogRef: MatDialogRef, 31 | @Inject(MAT_DIALOG_DATA) public data: User, 32 | private userService: UsersService) { 33 | if (data) { 34 | this.user = data; 35 | this.edit = true; 36 | } 37 | } 38 | 39 | /** 40 | * Add User 41 | */ 42 | public addUser() { 43 | if (!this.validate()) { 44 | return; 45 | } 46 | 47 | this.userService.createUser(this.user) 48 | .subscribe((res: User) => { 49 | this.dialogRef.close(res); 50 | }); 51 | } 52 | 53 | /** 54 | * Edit User 55 | */ 56 | public editUser() { 57 | if (!this.validate()) { 58 | return; 59 | } 60 | this.userService.editUser(this.user) 61 | .subscribe(res => { 62 | this.dialogRef.close(res); 63 | }); 64 | } 65 | 66 | /** 67 | * Validate 68 | * returns true if valid 69 | */ 70 | public validate(): boolean { 71 | if (!this.user || User.checkFieldsIsEmpty(this.user)) { 72 | this.error = 'Please fill all fields'; 73 | return false; 74 | } 75 | return true; 76 | } 77 | 78 | /** 79 | * Close dialog 80 | */ 81 | public closeDialog(): void { 82 | this.dialogRef.close(); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/app/components/users-control/user-control.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | import { CommonModule } from '@angular/common'; 4 | import { FormsModule, ReactiveFormsModule } from '@angular/forms'; 5 | 6 | import { UsersControlComponent } from './users-control.component'; 7 | import { AddUserComponent } from './add-user/add-user.component'; 8 | import { MyOwnCustomMaterialModule } from '../../common/ang-material.module'; 9 | import { SharedModule } from './../../shared/shared.module'; 10 | 11 | 12 | const routes: Routes = [ 13 | { 14 | path: '', 15 | component: UsersControlComponent 16 | } 17 | ]; 18 | 19 | 20 | @NgModule({ 21 | entryComponents: [ 22 | AddUserComponent 23 | ], 24 | imports: [ 25 | SharedModule, 26 | MyOwnCustomMaterialModule, 27 | CommonModule, 28 | FormsModule, 29 | ReactiveFormsModule, 30 | RouterModule.forChild(routes) 31 | ], 32 | providers: [], 33 | declarations: [UsersControlComponent , AddUserComponent], 34 | exports: [RouterModule], 35 | bootstrap: [] 36 | }) 37 | 38 | export class UserControlModule { } -------------------------------------------------------------------------------- /src/app/components/users-control/users-control.component.css: -------------------------------------------------------------------------------- 1 | .main-container { 2 | display: flex; 3 | justify-content: center; 4 | padding-top: 30px; 5 | } 6 | 7 | .user-container { 8 | width: 1240px; 9 | } 10 | 11 | .users-table-container { 12 | min-height: 551px; 13 | background: #f2f2fd; 14 | margin-bottom: 10px; 15 | } 16 | 17 | table { 18 | width: 100%; 19 | color: #66666d; 20 | border: 1px solid #c1c5cd; 21 | table-layout: fixed; 22 | } 23 | 24 | table, th, td { 25 | border: 1px inset #e2d5d5; 26 | border-collapse: collapse; 27 | border-spacing: 0; 28 | } 29 | 30 | th:hover { 31 | cursor: pointer; 32 | } 33 | 34 | th, td { 35 | text-align: center; 36 | height: 50px; 37 | vertical-align: middle; 38 | width: 170px; 39 | position: relative; 40 | } 41 | 42 | .th .mat-icon, .td .mat-icon { 43 | position: absolute; 44 | } 45 | 46 | .disabled { 47 | cursor: no-drop; 48 | } 49 | 50 | .fitler-input { 51 | margin: auto; 52 | padding-bottom: 15px; 53 | } 54 | 55 | .fitler-input input { 56 | padding-left: 15px; 57 | padding-left: 15px; 58 | height: 40px; 59 | width: 300px; 60 | } 61 | 62 | .fitler-input input:focus { 63 | border: 2px solid #2dacf8; 64 | } 65 | 66 | button { 67 | background: #2b8cf1; 68 | height: 40px; 69 | border: none; 70 | color: white; 71 | padding: 0 30px; 72 | } 73 | 74 | .pagination { 75 | margin: auto; 76 | margin-top: 72px; 77 | } 78 | 79 | .add-user { 80 | cursor: pointer; 81 | } 82 | -------------------------------------------------------------------------------- /src/app/components/users-control/users-control.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 10 |
11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 31 | 36 | 37 | 38 |
IDusernamepasswordeditdelete
{{ user._id }}{{ user.username }}{{ user.password }} 27 | mode_edit 29 | 30 | 32 | delete_sweep 34 | 35 |
39 |
40 | 41 | 42 | 43 | 72 |
73 |
74 | -------------------------------------------------------------------------------- /src/app/components/users-control/users-control.component.ts: -------------------------------------------------------------------------------- 1 | import { SearchFilterSortService } from './../../services/search-sort-filter/search-sort-filter.service'; 2 | import { Component, OnInit } from '@angular/core'; 3 | import { UsersService } from './../../services/users/users.service'; 4 | import { PaginatorService } from './../../services/paginator/paginator.service'; 5 | import { MatDialog, MatDialogConfig } from '@angular/material/dialog'; 6 | import { AddUserComponent } from './add-user/add-user.component'; 7 | import { fadeOffAnimation } from './../../common/animation'; 8 | import { User } from '../../entities/user'; 9 | import { PaginatorHelper } from '../../services/paginator/paginator-helper'; 10 | import { CommonResult } from '../../entities/common-result'; 11 | 12 | @Component({ 13 | selector: 'app-users-control', 14 | templateUrl: './users-control.component.html', 15 | styleUrls: ['./users-control.component.css'], 16 | animations: [fadeOffAnimation] 17 | }) 18 | export class UsersControlComponent implements OnInit { 19 | 20 | public users: User[]; 21 | 22 | public filteredUsers: User[]; 23 | 24 | public filterField: any; 25 | 26 | public disabledAnimation: boolean = false; 27 | 28 | public pager: PaginatorHelper; 29 | 30 | public pagedItems: User[] = []; 31 | 32 | public constructor( 33 | private userService: UsersService, 34 | private SFSService: SearchFilterSortService, 35 | private paginator: PaginatorService, 36 | public dialog: MatDialog, 37 | ) { 38 | } 39 | 40 | public ngOnInit() { 41 | this.getUsers(); 42 | } 43 | 44 | public getUsers(): void { 45 | this.userService.getUsers() 46 | .subscribe((res: User[]) => { 47 | this.users = res; 48 | this.filteredUsers = res; 49 | this.setPage(1); 50 | }); 51 | } 52 | 53 | 54 | public deleteUser(userForDelete: User): void { 55 | this.disabledAnimation = false; 56 | this.userService.deleteUser(userForDelete._id) 57 | .subscribe((res: CommonResult) => { 58 | this.filteredUsers = this.filteredUsers.filter((user: User) => user._id !== userForDelete._id); 59 | this.setPage(this.pager.currentPage) 60 | }); 61 | } 62 | 63 | public filter(): void { 64 | this.filteredUsers = this.SFSService.search(this.users, this.filterField, ["_id", "password"]); 65 | this.setPage(1); 66 | } 67 | 68 | setPage(page: number): void { 69 | if (this.pager && page !== this.pager.currentPage) { 70 | this.disabledAnimation = true; 71 | } 72 | this.pager = this.paginator.getPager(this.filteredUsers.length, page); 73 | this.pagedItems = this.filteredUsers.slice(this.pager.startIndex, this.pager.endIndex + 1); 74 | setTimeout(() => this.disabledAnimation = false, 0); 75 | } 76 | 77 | openAddUserModal(user?: User): void { 78 | const config = new MatDialogConfig(); 79 | if (user) { 80 | config.data = user; 81 | } 82 | let dialogRef = this.dialog.open(AddUserComponent, config); 83 | 84 | dialogRef.afterClosed().subscribe((result: CommonResult) => { 85 | if (result && result.success) { 86 | this.disabledAnimation = false; 87 | this.getUsers(); 88 | setTimeout(() => this.disabledAnimation = true, 0); 89 | } 90 | }); 91 | } 92 | 93 | } 94 | -------------------------------------------------------------------------------- /src/app/directives/sort/sort.directive.ts: -------------------------------------------------------------------------------- 1 | import { Directive, ElementRef, EventEmitter, HostListener, Input, OnInit, Output, Renderer2, ViewContainerRef } from '@angular/core'; 2 | import { SearchFilterSortService } from './../../services/search-sort-filter/search-sort-filter.service'; 3 | 4 | enum SortDirection { 5 | UP = 'up', 6 | DOWN = 'down' 7 | } 8 | 9 | 10 | @Directive({ 11 | selector: '[appSort]' 12 | }) 13 | export class SortDirective implements OnInit { 14 | 15 | @Input() array: T[]; 16 | 17 | @Input() value: string; 18 | 19 | @Output() setPage = new EventEmitter(); 20 | 21 | public sortedArray: T[]; 22 | 23 | public arrayDirection: string = SortDirection.UP; 24 | 25 | public matIcon: HTMLElement; 26 | 27 | @HostListener('click', [ '$event' ]) 28 | click() { 29 | if (this.arrayDirection === SortDirection.UP) { 30 | this.sort(true); 31 | this.addArrow(); 32 | } else { 33 | this.sort(); 34 | this.addArrow(SortDirection.UP); 35 | } 36 | this.setPage.emit(true); 37 | } 38 | 39 | 40 | public constructor(private el: ElementRef, 41 | private viewContainer: ViewContainerRef, 42 | private renderer: Renderer2, 43 | private SFSService: SearchFilterSortService) { 44 | } 45 | 46 | public ngOnInit(): void { 47 | this.addArrow(SortDirection.UP); 48 | } 49 | 50 | public addArrow(value?: string): void { 51 | let direction: string; 52 | if (Boolean(value)) { 53 | direction = 'arrow_upward'; 54 | this.arrayDirection = SortDirection.UP; 55 | } else { 56 | this.arrayDirection = SortDirection.DOWN; 57 | direction = 'arrow_downward'; 58 | } 59 | 60 | this.matIcon = this.renderer.createElement('mat-icon'); 61 | const iconName = this.renderer.createText(direction); 62 | 63 | this.renderer.addClass(this.matIcon, 'mat-icon'); 64 | this.renderer.addClass(this.matIcon, 'material-icons'); 65 | 66 | this.renderer.appendChild(this.matIcon, iconName); 67 | this.renderer.appendChild(this.el.nativeElement, this.matIcon); 68 | } 69 | 70 | public deleteArrow(): void { 71 | this.renderer.removeChild(this.el.nativeElement, this.matIcon); 72 | } 73 | 74 | public sort(reverse: boolean = false): void { 75 | this.deleteArrow(); 76 | this.sortedArray = this.SFSService.orderBy(this.array, this.value, reverse); 77 | } 78 | 79 | } 80 | 81 | -------------------------------------------------------------------------------- /src/app/entities/common-result.ts: -------------------------------------------------------------------------------- 1 | export class CommonResult { 2 | 3 | public title: string; 4 | 5 | public message: string; 6 | 7 | public success: boolean; 8 | 9 | public additionalInfo: T; 10 | 11 | constructor(success: boolean, message?: string, title?: string, additionalInfo?: T) { 12 | this.success = success; 13 | this.message = message; 14 | this.title = title; 15 | this.additionalInfo = additionalInfo; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/app/entities/message.ts: -------------------------------------------------------------------------------- 1 | export class Message { 2 | 3 | /** 4 | * Username 5 | */ 6 | public username: string; 7 | 8 | /** 9 | * Message text 10 | */ 11 | public text: string; 12 | 13 | 14 | public room: string; 15 | 16 | public constructor(username: string, text: string, room?: string) { 17 | this.username = username; 18 | this.text = text; 19 | this.room = room; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/app/entities/room.ts: -------------------------------------------------------------------------------- 1 | import { Message } from './message'; 2 | 3 | export class Room { 4 | 5 | /** 6 | * Title 7 | */ 8 | public title: string; 9 | 10 | /** 11 | * Id 12 | */ 13 | public _id: string; 14 | 15 | /** 16 | * Messages 17 | */ 18 | 19 | public messages: Message[]; 20 | 21 | /** 22 | * Date 23 | */ 24 | public date: string; 25 | 26 | public constructor(title: string, _id: string, messages?: Message[]) { 27 | this.title = title; 28 | this._id = _id; 29 | this.messages = messages || []; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/app/entities/user.ts: -------------------------------------------------------------------------------- 1 | export class User { 2 | 3 | /** 4 | * Password 5 | */ 6 | public password: string; 7 | 8 | /** 9 | * Username 10 | */ 11 | public username: string; 12 | 13 | /** 14 | * Id 15 | */ 16 | public _id: string; 17 | 18 | 19 | public constructor(username: string = null, password: string = null, _id?: string) { 20 | this.username = username; 21 | this.password = password; 22 | this._id = _id; 23 | } 24 | 25 | /** 26 | * returns true if fields not filled 27 | */ 28 | public static checkFieldsIsEmpty(user: User): boolean { 29 | if (!user.username || !user.password || !user.username.trim() || !user.password.trim()) { 30 | return true; 31 | } else { 32 | return false; 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/app/services/auth/auth-guard.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { CanActivate, CanActivateChild, Router } from '@angular/router'; 3 | import { SnotifyHelperService } from '../../common/snotify-helper.service'; 4 | import { AuthenticationService } from './authentication.service'; 5 | 6 | @Injectable() 7 | export class AuthGuard implements CanActivate, CanActivateChild { 8 | 9 | constructor(private authService: AuthenticationService, private router: Router, private snotify: SnotifyHelperService, 10 | ) { 11 | } 12 | 13 | canActivate(): boolean { 14 | return this.checkIfLoggedIn(); 15 | } 16 | 17 | canActivateChild(): boolean { 18 | return this.checkIfLoggedIn(); 19 | } 20 | 21 | private checkIfLoggedIn(): boolean { 22 | const isLoggedIn: boolean = this.authService.isLoggedIn(); 23 | if (isLoggedIn) { 24 | return true; 25 | } else { 26 | this.snotify.onWarning('Sorry, you are not logged in.', null) 27 | this.router.navigate(['/auth']); 28 | return false; 29 | } 30 | } 31 | } 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/app/services/auth/auth.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { catchError } from 'rxjs/operators'; 2 | import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest, HttpErrorResponse } from '@angular/common/http'; 3 | import { Injectable } from '@angular/core'; 4 | import { Router } from '@angular/router'; 5 | import { Observable, throwError } from 'rxjs'; 6 | import { AuthenticationService } from './authentication.service'; 7 | import { LocalStorageService } from './local-storage.service'; 8 | 9 | @Injectable() 10 | export class AuthInterceptor implements HttpInterceptor { 11 | 12 | public constructor(private storageService: LocalStorageService, private router: Router, private authService: AuthenticationService) { 13 | } 14 | 15 | public intercept(request: HttpRequest, next: HttpHandler): Observable> { 16 | if (request.url.includes('login')) { 17 | return next.handle(request); 18 | } else { 19 | const authToken: string = this.storageService.getToken(); 20 | if (authToken ) { 21 | request = this.addToken(request, authToken); 22 | } else { 23 | this.router.navigate(['/auth']); 24 | } 25 | return next.handle(request) 26 | .pipe( 27 | catchError((error: HttpErrorResponse) => { 28 | if (error.status === 401 ) { 29 | this.router.navigate(['/auth']); 30 | } 31 | return throwError(error); 32 | }) 33 | ) 34 | } 35 | } 36 | 37 | private addToken(request: HttpRequest, token: string): HttpRequest { 38 | return request.clone({ 39 | setHeaders: { 40 | 'Authorization': `Bearer ${token}` 41 | } 42 | }); 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/app/services/auth/authentication.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient, HttpParams } from '@angular/common/http'; 2 | import { Injectable } from '@angular/core'; 3 | import { Router } from '@angular/router'; 4 | import { EMPTY, NEVER, Observable } from 'rxjs'; 5 | import { catchError, tap } from 'rxjs/operators'; 6 | import { User } from '../../entities/user'; 7 | import { LocalStorageService } from './local-storage.service'; 8 | 9 | 10 | @Injectable({ 11 | providedIn: 'root', 12 | }) 13 | export class AuthenticationService { 14 | 15 | private readonly api = '/api/'; 16 | 17 | public constructor(private http: HttpClient, private storage: LocalStorageService, private router: Router) { 18 | } 19 | 20 | public login(user: User): Observable { 21 | let params: HttpParams = new HttpParams(); 22 | params = params.append('username', user.username); 23 | params = params.append('password', user.password); 24 | 25 | return this.http.post(this.api + 'auth/login', params) 26 | .pipe( 27 | catchError(() => { 28 | this.storage.removeToken(); 29 | return EMPTY; 30 | }), 31 | tap((response: AcessToken) => { 32 | this.storage.setToken(response.value); 33 | this.router.navigateByUrl('/chat'); 34 | }) 35 | ) 36 | } 37 | 38 | public isLoggedIn(): boolean { 39 | return !!this.storage.getToken(); 40 | } 41 | 42 | public logout(): void { 43 | this.storage.removeToken(); 44 | this.router.navigate(['/auth']); 45 | } 46 | 47 | } 48 | 49 | export interface AcessToken { 50 | expireTime: string; 51 | value: string; 52 | } 53 | 54 | export interface JwtVerifyAnswer { 55 | exp: number; 56 | iat: number; 57 | username: string; 58 | } 59 | -------------------------------------------------------------------------------- /src/app/services/auth/local-storage.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Inject, PLATFORM_ID } from '@angular/core'; 2 | import { isPlatformBrowser } from '@angular/common'; 3 | 4 | @Injectable({ 5 | providedIn: 'root', 6 | }) 7 | export class LocalStorageService { 8 | 9 | /** Token */ 10 | public token: string; 11 | 12 | /** Name of token as local storage key */ 13 | private readonly tokenName: string = 'authToken'; 14 | 15 | public constructor(@Inject(PLATFORM_ID) private platformId) { 16 | } 17 | 18 | public getItem(key: string): string { 19 | if (isPlatformBrowser(this.platformId)) { 20 | return localStorage.getItem(key) 21 | } 22 | } 23 | 24 | /** 25 | * Get token from lStorage 26 | */ 27 | public getToken(): string { 28 | if (isPlatformBrowser(this.platformId)) { 29 | return localStorage.getItem(this.tokenName); 30 | } 31 | } 32 | 33 | /** 34 | * Set token 35 | */ 36 | public setToken(value: any): void { 37 | if (isPlatformBrowser(this.platformId)) { 38 | localStorage.setItem(this.tokenName, value); 39 | } 40 | } 41 | 42 | /** 43 | * Remove token from lStorage 44 | */ 45 | public removeToken(): void { 46 | if (isPlatformBrowser(this.platformId)) { 47 | localStorage.removeItem(this.tokenName); 48 | } 49 | } 50 | 51 | /** 52 | * Returns true is token was already set 53 | */ 54 | public isTokenSet(): boolean { 55 | if (isPlatformBrowser(this.platformId)) { 56 | return Boolean(localStorage.getItem(this.tokenName)); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/app/services/chat-sockets/socket-one.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Socket } from 'ngx-socket-io'; 3 | import { LocalStorageService } from './../auth/local-storage.service'; 4 | 5 | @Injectable({ 6 | providedIn: 'root', 7 | }) 8 | export class SocketOne extends Socket { 9 | 10 | public constructor(private locatStorageService: LocalStorageService) { 11 | super({ 12 | url: '', options: { 13 | transportOptions: { 14 | polling: { 15 | extraHeaders: { 16 | Authorization: 'Bearer ' + locatStorageService.getItem('authToken') 17 | } 18 | } 19 | } 20 | } 21 | }); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/app/services/chat-sockets/socket.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { SocketOne } from './socket-one.service'; 3 | import { Message } from '../../entities/message'; 4 | import { Observable } from 'rxjs'; 5 | import { Room } from '../../entities/room'; 6 | import { User } from '../../entities/user'; 7 | 8 | @Injectable({ 9 | providedIn: 'root', 10 | }) 11 | export class ChatService { 12 | 13 | public constructor(private socket: SocketOne) { 14 | } 15 | 16 | public sendMessage(text: string, name: string, roomId: string): void { 17 | this.socket.emit('message', {message: text, username: name, room: roomId}); 18 | } 19 | 20 | public getMessage(): Observable { 21 | return this.socket.fromEvent('sendessage'); 22 | } 23 | 24 | public joinRoom(roomId: string): void { 25 | this.socket.emit('joinRoom', roomId); 26 | } 27 | 28 | public getRooms(): Observable { 29 | return this.socket.fromEvent('updatedRooms'); 30 | } 31 | 32 | public addRoom(roomName: string): void { 33 | this.socket.emit('addroom', roomName); 34 | } 35 | 36 | public getUsers(): Observable { 37 | return this.socket.fromEvent('usersRoom'); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/app/services/paginator/paginator-helper.ts: -------------------------------------------------------------------------------- 1 | export class PaginatorHelper { 2 | 3 | public totalItems: number; 4 | 5 | public totalPages: number; 6 | 7 | public pageSize: number; 8 | 9 | public currentPage: number; 10 | 11 | public startPage: number; 12 | 13 | public endPage: number; 14 | 15 | public startIndex: number; 16 | 17 | public endIndex: number; 18 | 19 | public pages: number[]; 20 | 21 | public constructor(totalItems: number, currentPage: number, pageSize: number, totalPages: number, startPage: number, endPage: number, 22 | startIndex: number, endIndex: number, pages: number[]) { 23 | this.totalItems = totalItems; 24 | this.currentPage = currentPage; 25 | this.pageSize = pageSize; 26 | this.totalPages = totalPages; 27 | this.startPage = startPage; 28 | this.endPage = endPage; 29 | this.startIndex = startIndex; 30 | this.endIndex = endIndex; 31 | this.pages = pages; 32 | } 33 | } -------------------------------------------------------------------------------- /src/app/services/paginator/paginator.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { PaginatorHelper } from './paginator-helper'; 3 | 4 | @Injectable({ 5 | providedIn: 'root', 6 | }) 7 | export class PaginatorService { 8 | 9 | public getPager(totalItems: number, currentPage: number = 1, pageSize: number = 10): PaginatorHelper { 10 | const totalPages: number = Math.ceil(totalItems / pageSize); 11 | let startPage: number, endPage: number; 12 | if (totalPages <= 10) { 13 | startPage = 1; 14 | endPage = totalPages; 15 | } else { 16 | if (currentPage <= 6) { 17 | startPage = 1; 18 | endPage = 10; 19 | } else if (currentPage + 4 >= totalPages) { 20 | startPage = totalPages - 9; 21 | endPage = totalPages; 22 | } else { 23 | startPage = currentPage - 5; 24 | endPage = currentPage + 4; 25 | } 26 | } 27 | const startIndex: number = (currentPage - 1) * pageSize; 28 | const endIndex: number = Math.min(startIndex + pageSize - 1, totalItems - 1); 29 | const pages: number[] = this.numberRange(startPage, endPage + 1); 30 | return new PaginatorHelper(totalItems, currentPage, pageSize, totalPages, startPage, endPage, startIndex, endIndex, pages); 31 | } 32 | 33 | private numberRange(start, end): number[] { 34 | return new Array(end - start).fill(start).map((d, i) => i + start); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/app/services/rooms/rooms.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient } from '@angular/common/http'; 2 | import { Injectable } from '@angular/core'; 3 | import { EMPTY, Observable } from 'rxjs'; 4 | import { catchError } from 'rxjs/operators'; 5 | import { SnotifyHelperService } from '../../common/snotify-helper.service'; 6 | import { Room } from '../../entities/room'; 7 | 8 | const api = '/api/'; 9 | 10 | @Injectable({ 11 | providedIn: 'root', 12 | }) 13 | export class RoomService { 14 | 15 | public constructor(private http: HttpClient, private snotify: SnotifyHelperService) { 16 | } 17 | 18 | /** 19 | * Get rooms 20 | */ 21 | public getRooms(): Observable { 22 | return this.http.get(`${api}rooms`) 23 | .pipe(catchError(() => { 24 | return EMPTY; 25 | })); 26 | } 27 | 28 | /** 29 | * Get room 30 | * @param id 31 | */ 32 | public getRoom(id: string): Observable { 33 | return this.http.get(`${api}rooms/room/${id}`) 34 | .pipe(catchError(() => { 35 | return EMPTY; 36 | })); 37 | ; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/app/services/search-sort-filter/search-sort-filter.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { DatePipe } from '@angular/common'; 3 | 4 | @Injectable({ 5 | providedIn: 'root', 6 | }) 7 | export class SearchFilterSortService { 8 | 9 | constructor(private datePipe: DatePipe) { 10 | } 11 | 12 | public search(array: any[], query: string, excludeProps?: string | string[], dateFormat?: string) { 13 | // Match query to strings and Date objects / ISO UTC strings 14 | // Optionally exclude properties from being searched 15 | // If matching dates, can optionally pass in date format string 16 | if (!query || !this._objArrayCheck(array)) { 17 | this._error('There was a problem with your search query, or the provided array could not be searched.'); 18 | return array; 19 | } 20 | 21 | const lQuery = query.toLowerCase(); 22 | const isoDateRegex = /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/; // ISO UTC 23 | const dateF = dateFormat ? dateFormat : 'medium'; 24 | const filteredArray = array.filter(item => { 25 | for (const key in item) { 26 | if (item.hasOwnProperty(key)) { 27 | if (!excludeProps || excludeProps.indexOf(key) === -1) { 28 | const thisVal = item[ key ]; 29 | if ( 30 | // Value is a string and NOT a UTC date 31 | typeof thisVal === 'string' && 32 | !thisVal.match(isoDateRegex) && 33 | thisVal.toLowerCase().indexOf(lQuery) !== -1 34 | ) { 35 | return true; 36 | } else if ( 37 | // Value is a Date object or UTC string 38 | (thisVal instanceof Date || thisVal.toString().match(isoDateRegex)) && 39 | // https://angular.io/api/common/DatePipe 40 | // Matching date format string passed in as param (or default to 'medium') 41 | this.datePipe.transform(thisVal, dateF).toLowerCase().indexOf(lQuery) !== -1 42 | ) { 43 | return true; 44 | } 45 | } 46 | } 47 | } 48 | }); 49 | return filteredArray; 50 | } 51 | 52 | public filter(array: any[], prop: string, value: any) { 53 | // Return only items with specific key/value pair 54 | if (!prop || value === undefined || !this._objArrayCheck(array)) { 55 | this._error('There was a problem with your filter request, or the provided array could not be filtered.'); 56 | return array; 57 | } 58 | const filteredArray = array.filter(item => { 59 | for (const key in item) { 60 | if (item.hasOwnProperty(key)) { 61 | if (key === prop && item[ key ] === value) { 62 | return true; 63 | } 64 | } 65 | } 66 | }); 67 | return filteredArray; 68 | } 69 | 70 | public orderBy(array: any[], prop: string, reverse?: boolean) { 71 | // Order an array of objects by a property 72 | // Supports string, number, and boolean values 73 | 74 | if (!prop || !this._objArrayCheck(array)) { 75 | this._error('Property does not exist, or the provided array could not be sorted.'); 76 | return array; 77 | } 78 | const propType = typeof array[ 0 ][ prop ]; 79 | let sortedArray; 80 | switch (propType) { 81 | case 'string': 82 | // Default: ascending (A->Z) 83 | sortedArray = array.sort((a, b) => { 84 | const itemA = a[ prop ].toLowerCase(); 85 | const itemB = b[ prop ].toLowerCase(); 86 | if (!reverse) { 87 | if (itemA < itemB) { 88 | return -1; 89 | } 90 | if (itemA > itemB) { 91 | return 1; 92 | } 93 | return 0; 94 | } else { 95 | if (itemA > itemB) { 96 | return -1; 97 | } 98 | if (itemA < itemB) { 99 | return 1; 100 | } 101 | return 0; 102 | } 103 | }); 104 | break; 105 | case 'number': 106 | // Default: ascending (0->9) 107 | sortedArray = array.sort((a, b) => { 108 | const itemA = a[ prop ]; 109 | const itemB = b[ prop ]; 110 | return !reverse ? itemA - itemB : itemB - itemA; 111 | }); 112 | break; 113 | case 'boolean': 114 | // Default: descending (true->false) 115 | sortedArray = array.sort((a, b) => { 116 | const itemA = a[ prop ]; 117 | const itemB = b[ prop ]; 118 | return !reverse ? itemB - itemA : itemA - itemB; 119 | }); 120 | break; 121 | default: 122 | sortedArray = array; 123 | } 124 | return sortedArray; 125 | } 126 | 127 | public orderByDate(array: any[], prop: string, reverse?: boolean) { 128 | // Order an array of objects by a date property 129 | // Default: ascending (1992->2017 | Jan->Dec) 130 | if (!prop || !this._objArrayCheck(array)) { 131 | this._error('Property does not exist, or the provided array could not be sorted.'); 132 | return array; 133 | } 134 | const sortedArray = array.sort((a, b) => { 135 | const dateA = new Date(a[ prop ]).getTime(); 136 | const dateB = new Date(b[ prop ]).getTime(); 137 | return !reverse ? dateA - dateB : dateB - dateA; 138 | }); 139 | return sortedArray; 140 | } 141 | 142 | private _objArrayCheck(array: any[]): boolean { 143 | // Checks if all items in array are objects 144 | // Necessary because some arrays passed in may have 145 | // models that don't match {[key: string]: any}[] 146 | // This check prevents uncaught reference errors 147 | const hasObjs = array.every((item) => !!(item !== null && Object.prototype.toString.call(item) === '[object Object]')); 148 | const check = hasObjs && array.length > 0; 149 | if (!check) { 150 | this._error(); 151 | } 152 | return check; 153 | } 154 | 155 | private _error(error?: string) { 156 | const msg = error ? error : 'Provided array did not contain objects; could not sort/filter.'; 157 | console.warn(`SearchFilterSortService Error: ${msg}`); 158 | } 159 | 160 | } 161 | -------------------------------------------------------------------------------- /src/app/services/users/users.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient, HttpParams } from '@angular/common/http'; 2 | import { Injectable } from '@angular/core'; 3 | import { EMPTY, Observable } from 'rxjs'; 4 | import { catchError } from 'rxjs/operators'; 5 | import { User } from '../../entities/user'; 6 | import { SnotifyHelperService } from '../../common/snotify-helper.service'; 7 | import { CommonResult } from '../../entities/common-result'; 8 | 9 | const api = '/api/users'; 10 | 11 | @Injectable({ 12 | providedIn: 'root', 13 | }) 14 | export class UsersService { 15 | 16 | public constructor(private http: HttpClient, private snotify: SnotifyHelperService) { 17 | } 18 | 19 | public getUsers(): Observable { 20 | return this.http.get(`${api}/get-users`); 21 | } 22 | 23 | public createUser(user: User): Observable { 24 | let body: HttpParams = new HttpParams(); 25 | body = body.append('username', user.username); 26 | body = body.append('password', user.password); 27 | return this.http.post(`${api}/create`, body) 28 | .pipe(catchError(() => { 29 | return EMPTY; 30 | })); 31 | } 32 | 33 | public deleteUser(id: string): Observable { 34 | return this.http.delete(`${api}/delete/${id}`) 35 | .pipe(catchError(() => { 36 | return EMPTY; 37 | })); 38 | } 39 | 40 | public editUser(user: User): Observable { 41 | let body = new HttpParams(); 42 | body = body.append('username', user.username); 43 | body = body.append('password', user.password); 44 | return this.http.put(`${api}/edit/${user._id}`, body) 45 | .pipe(catchError(() => { 46 | return EMPTY; 47 | })); 48 | 49 | } 50 | 51 | public getProfile(): Observable { 52 | return this.http.get(`${api}/get-profile`); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/app/shared/shared.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule, ModuleWithProviders } from '@angular/core'; 2 | 3 | // Services 4 | import { DatePipe } from '@angular/common'; 5 | import { SearchFilterSortService } from '../services/search-sort-filter/search-sort-filter.service'; 6 | import { ChatService } from '../services/chat-sockets/socket.service'; 7 | import { AuthenticationService } from '../services/auth/authentication.service'; 8 | import { LocalStorageService } from '../services/auth/local-storage.service'; 9 | import { RoomService } from '../services/rooms/rooms.service'; 10 | import { SocketOne } from '../services/chat-sockets/socket-one.service'; 11 | import { UsersService } from '../services/users/users.service'; 12 | import { PaginatorService } from '../services/paginator/paginator.service'; 13 | 14 | import { SortDirective } from './../directives/sort/sort.directive'; 15 | 16 | @NgModule({ 17 | imports: [], 18 | declarations: [ SortDirective ], 19 | exports: [ SortDirective ], 20 | bootstrap: [] 21 | }) 22 | 23 | export class SharedModule { 24 | static forRoot(): ModuleWithProviders { 25 | return { 26 | ngModule: SharedModule, 27 | providers: [ 28 | DatePipe, 29 | ] 30 | }; 31 | } 32 | } 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georgyim/angular-chat/620b5ddb0a1bc643b049fe2fa6fbdd55fadad6b3/src/assets/.gitkeep -------------------------------------------------------------------------------- /src/assets/fonts/Cabin/Cabin-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georgyim/angular-chat/620b5ddb0a1bc643b049fe2fa6fbdd55fadad6b3/src/assets/fonts/Cabin/Cabin-Bold.ttf -------------------------------------------------------------------------------- /src/assets/fonts/Cabin/Cabin-BoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georgyim/angular-chat/620b5ddb0a1bc643b049fe2fa6fbdd55fadad6b3/src/assets/fonts/Cabin/Cabin-BoldItalic.ttf -------------------------------------------------------------------------------- /src/assets/fonts/Cabin/Cabin-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georgyim/angular-chat/620b5ddb0a1bc643b049fe2fa6fbdd55fadad6b3/src/assets/fonts/Cabin/Cabin-Italic.ttf -------------------------------------------------------------------------------- /src/assets/fonts/Cabin/Cabin-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georgyim/angular-chat/620b5ddb0a1bc643b049fe2fa6fbdd55fadad6b3/src/assets/fonts/Cabin/Cabin-Medium.ttf -------------------------------------------------------------------------------- /src/assets/fonts/Cabin/Cabin-MediumItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georgyim/angular-chat/620b5ddb0a1bc643b049fe2fa6fbdd55fadad6b3/src/assets/fonts/Cabin/Cabin-MediumItalic.ttf -------------------------------------------------------------------------------- /src/assets/fonts/Cabin/Cabin-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georgyim/angular-chat/620b5ddb0a1bc643b049fe2fa6fbdd55fadad6b3/src/assets/fonts/Cabin/Cabin-Regular.ttf -------------------------------------------------------------------------------- /src/assets/fonts/Cabin/Cabin-SemiBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georgyim/angular-chat/620b5ddb0a1bc643b049fe2fa6fbdd55fadad6b3/src/assets/fonts/Cabin/Cabin-SemiBold.ttf -------------------------------------------------------------------------------- /src/assets/fonts/Cabin/Cabin-SemiBoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georgyim/angular-chat/620b5ddb0a1bc643b049fe2fa6fbdd55fadad6b3/src/assets/fonts/Cabin/Cabin-SemiBoldItalic.ttf -------------------------------------------------------------------------------- /src/assets/reset.css: -------------------------------------------------------------------------------- 1 | html, body, div, span, applet, object, iframe, 2 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 3 | a, abbr, acronym, address, big, cite, code, 4 | del, dfn, em, img, ins, kbd, q, s, samp, 5 | small, strike, strong, sub, sup, tt, var, 6 | b, u, i, center, 7 | dl, dt, dd, ol, ul, li, 8 | fieldset, form, label, legend, 9 | table, caption, tbody, tfoot, thead, tr, th, td, 10 | article, aside, canvas, details, embed, 11 | figure, figcaption, footer, header, hgroup, 12 | menu, nav, output, ruby, section, summary, 13 | time, mark, audio, video { 14 | margin: 0; 15 | padding: 0; 16 | border: 0; 17 | font-size: 100%; 18 | font: inherit; 19 | vertical-align: baseline; 20 | } 21 | /* HTML5 display-role reset for older browsers */ 22 | article, aside, details, figcaption, figure, 23 | footer, header, hgroup, menu, nav, section { 24 | display: block; 25 | } 26 | body { 27 | line-height: 1; 28 | } 29 | ol, ul { 30 | list-style: none; 31 | } 32 | blockquote, q { 33 | quotes: none; 34 | } 35 | blockquote:before, blockquote:after, 36 | q:before, q:after { 37 | content: ''; 38 | content: none; 39 | } 40 | table { 41 | border-collapse: collapse; 42 | border-spacing: 0; 43 | } 44 | input { 45 | border: none; 46 | outline: none; 47 | } 48 | input:active, input:hover, input:focus { 49 | border: none; 50 | outline: none; 51 | } 52 | button { 53 | border:none; 54 | outline: none; 55 | } 56 | button:active, button:hover, button:focus { 57 | border: none; 58 | outline: none; 59 | } -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // The file contents for the current environment will overwrite these during build. 2 | // The build system defaults to the dev environment which uses `environment.ts`, but if you do 3 | // `ng build --env=prod` then `environment.prod.ts` will be used instead. 4 | // The list of which env maps to which file can be found in `.angular-cli.json`. 5 | 6 | export const environment = { 7 | production: false 8 | }; 9 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georgyim/angular-chat/620b5ddb0a1bc643b049fe2fa6fbdd55fadad6b3/src/favicon.ico -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | AngularNestStarter 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /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 | import 'hammerjs'; 7 | 8 | if (environment.production) { 9 | enableProdMode(); 10 | } 11 | 12 | platformBrowserDynamic().bootstrapModule(AppModule) 13 | .catch(err => console.log(err)); 14 | -------------------------------------------------------------------------------- /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/docs/ts/latest/guide/browser-support.html 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE9, IE10 and IE11 requires all of the following polyfills. **/ 22 | // import 'core-js/es6/symbol'; 23 | // import 'core-js/es6/object'; 24 | // import 'core-js/es6/function'; 25 | // import 'core-js/es6/parse-int'; 26 | // import 'core-js/es6/parse-float'; 27 | // import 'core-js/es6/number'; 28 | // import 'core-js/es6/math'; 29 | // import 'core-js/es6/string'; 30 | // import 'core-js/es6/date'; 31 | // import 'core-js/es6/array'; 32 | // import 'core-js/es6/regexp'; 33 | // import 'core-js/es6/map'; 34 | // import 'core-js/es6/weak-map'; 35 | // import 'core-js/es6/set'; 36 | 37 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 38 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 39 | 40 | /** IE10 and IE11 requires the following for the Reflect API. */ 41 | // import 'core-js/es6/reflect'; 42 | 43 | 44 | /** Evergreen browsers require these. **/ 45 | // Used for reflect-metadata in JIT. If you use AOT (and only Angular decorators), you can remove. 46 | 47 | 48 | 49 | /** 50 | * Required to support Web Animations `@angular/platform-browser/animations`. 51 | * Needed for: All but Chrome, Firefox and Opera. http://caniuse.com/#feat=web-animation 52 | **/ 53 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 54 | 55 | 56 | 57 | /*************************************************************************************************** 58 | * Zone JS is required by default for Angular itself. 59 | */ 60 | import 'zone.js/dist/zone'; // Included with Angular CLI. 61 | 62 | 63 | 64 | /*************************************************************************************************** 65 | * APPLICATION IMPORTS 66 | */ 67 | (window as any).global = window; -------------------------------------------------------------------------------- /src/styles.scss: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | @import "assets/reset.css"; 3 | @import "~@angular/material/prebuilt-themes/indigo-pink.css"; 4 | /* For snotify */ 5 | @import url('https://fonts.googleapis.com/css?family=Audiowide'); 6 | .material { 7 | @import "~ng-snotify/styles/material"; 8 | } 9 | .simple { 10 | @import "~ng-snotify/styles/simple"; 11 | } 12 | .dark { 13 | @import "~ng-snotify/styles/dark"; 14 | } 15 | 16 | 17 | 18 | @font-face { 19 | font-family: "Cabin"; 20 | src: url("assets/fonts/Cabin/Cabin-Regular.ttf"); 21 | } 22 | @font-face { 23 | font-family: "Cabin"; 24 | src: url("assets/fonts/Cabin/Cabin-Bold.ttf"); 25 | font-weight: bold; 26 | } 27 | @font-face { 28 | font-family: "Cabin"; 29 | src: url("assets/fonts/Cabin/Cabin-Medium.ttf"); 30 | font-style: medium; 31 | } 32 | 33 | 34 | body { 35 | background: #896be7; 36 | height: 100vh; 37 | background: linear-gradient(135deg, #896be7 0%, #f2a9bd 100%); 38 | font-family: Cabin, Arial, Helvetica, sans-serif !important; 39 | } 40 | -------------------------------------------------------------------------------- /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/dist/zone-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: any; 11 | 12 | // First, initialize the Angular testing environment. 13 | getTestBed().initTestEnvironment( 14 | BrowserDynamicTestingModule, 15 | platformBrowserDynamicTesting() 16 | ); 17 | // Then we find all the tests. 18 | const context = require.context('./', true, /\.spec\.ts$/); 19 | // And load the modules. 20 | context.keys().map(context); 21 | -------------------------------------------------------------------------------- /src/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "baseUrl": "./", 6 | "types": [] 7 | }, 8 | "files": [ 9 | "main.ts", 10 | "polyfills.ts" 11 | ], 12 | "include": [ 13 | "src/**/*.d.ts" 14 | ], 15 | "angularCompilerOptions": { 16 | "enableIvy": false 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/spec", 5 | "baseUrl": "./", 6 | "types": [ 7 | "jasmine", 8 | "node" 9 | ] 10 | }, 11 | "files": [ 12 | "test.ts", 13 | "polyfills.ts" 14 | ], 15 | "include": [ 16 | "**/*.spec.ts", 17 | "**/*.d.ts" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /src/typings.d.ts: -------------------------------------------------------------------------------- 1 | /* SystemJS module definition */ 2 | declare var module: NodeModule; 3 | interface NodeModule { 4 | id: string; 5 | } 6 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./server/tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "importHelpers": true, 5 | "outDir": "./dist/out-tsc", 6 | "sourceMap": true, 7 | "declaration": false, 8 | "moduleResolution": "node", 9 | "emitDecoratorMetadata": true, 10 | "experimentalDecorators": true, 11 | "target": "es5", 12 | "typeRoots": [ 13 | "node_modules/@types" 14 | ], 15 | "lib": [ 16 | "es2017", 17 | "dom" 18 | ], 19 | "module": "esnext", 20 | "baseUrl": "./" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [ 3 | "node_modules/codelyzer" 4 | ], 5 | "rules": { 6 | "arrow-return-shorthand": true, 7 | "callable-types": true, 8 | "class-name": true, 9 | "comment-format": [ 10 | true, 11 | "check-space" 12 | ], 13 | "curly": true, 14 | "deprecation": { 15 | "severity": "warn" 16 | }, 17 | "eofline": true, 18 | "forin": true, 19 | "import-blacklist": [ 20 | true, 21 | "rxjs/Rx" 22 | ], 23 | "import-spacing": true, 24 | "indent": [ 25 | true, 26 | "spaces" 27 | ], 28 | "interface-over-type-literal": true, 29 | "label-position": true, 30 | "max-line-length": [ 31 | true, 32 | 140 33 | ], 34 | "member-access": false, 35 | "member-ordering": [ 36 | true, 37 | { 38 | "order": [ 39 | "static-field", 40 | "instance-field", 41 | "static-method", 42 | "instance-method" 43 | ] 44 | } 45 | ], 46 | "no-arg": true, 47 | "no-bitwise": true, 48 | "no-console": [ 49 | true, 50 | "debug", 51 | "info", 52 | "time", 53 | "timeEnd", 54 | "trace" 55 | ], 56 | "no-construct": true, 57 | "no-debugger": true, 58 | "no-duplicate-super": true, 59 | "no-empty": false, 60 | "no-empty-interface": true, 61 | "no-eval": true, 62 | "no-inferrable-types": [ 63 | true, 64 | "ignore-params" 65 | ], 66 | "no-misused-new": true, 67 | "no-non-null-assertion": true, 68 | "no-shadowed-variable": true, 69 | "no-string-literal": false, 70 | "no-string-throw": true, 71 | "no-switch-case-fall-through": true, 72 | "no-trailing-whitespace": true, 73 | "no-unnecessary-initializer": true, 74 | "no-unused-expression": true, 75 | "no-use-before-declare": true, 76 | "no-var-keyword": true, 77 | "object-literal-sort-keys": false, 78 | "one-line": [ 79 | true, 80 | "check-open-brace", 81 | "check-catch", 82 | "check-else", 83 | "check-whitespace" 84 | ], 85 | "prefer-const": true, 86 | "quotemark": [ 87 | true, 88 | "single" 89 | ], 90 | "radix": true, 91 | "semicolon": [ 92 | true, 93 | "always" 94 | ], 95 | "triple-equals": [ 96 | true, 97 | "allow-null-check" 98 | ], 99 | "typedef-whitespace": [ 100 | true, 101 | { 102 | "call-signature": "nospace", 103 | "index-signature": "nospace", 104 | "parameter": "nospace", 105 | "property-declaration": "nospace", 106 | "variable-declaration": "nospace" 107 | } 108 | ], 109 | "unified-signatures": true, 110 | "variable-name": false, 111 | "whitespace": [ 112 | true, 113 | "check-branch", 114 | "check-decl", 115 | "check-operator", 116 | "check-separator", 117 | "check-type" 118 | ], 119 | "directive-selector": [ 120 | true, 121 | "attribute", 122 | "app", 123 | "camelCase" 124 | ], 125 | "component-selector": [ 126 | true, 127 | "element", 128 | "app", 129 | "kebab-case" 130 | ], 131 | "no-output-on-prefix": true, 132 | "use-input-property-decorator": true, 133 | "use-output-property-decorator": true, 134 | "use-host-property-decorator": true, 135 | "no-input-rename": true, 136 | "no-output-rename": true, 137 | "use-life-cycle-interface": true, 138 | "use-pipe-transform-interface": true, 139 | "component-class-suffix": true, 140 | "directive-class-suffix": true 141 | } 142 | } 143 | --------------------------------------------------------------------------------