├── config ├── test.json ├── production.json └── default.json ├── public ├── favicon.ico ├── index.html └── client.js ├── .nycrc ├── src ├── middleware │ └── index.ts ├── services │ ├── messages │ │ ├── messages.class.ts │ │ ├── messages.hooks.ts │ │ └── messages.service.ts │ ├── index.ts │ └── users │ │ ├── users.service.ts │ │ ├── users.hooks.ts │ │ └── users.class.ts ├── models │ ├── messages.model.ts │ └── users.model.ts ├── index.ts ├── declarations.d.ts ├── logger.ts ├── app.hooks.ts ├── hooks │ ├── process-message.ts │ └── populate-user.ts ├── authentication.ts ├── app.ts └── channels.ts ├── tsconfig.json ├── .editorconfig ├── .github ├── workflows │ ├── nodejs.yml │ └── update-dependencies.yml ├── issue_template.md ├── pull_request_template.md └── contributing.md ├── test ├── authentication.test.ts ├── services │ ├── messages.test.ts │ └── users.test.ts └── app.test.ts ├── LICENSE ├── package.json ├── .gitignore └── README.md /config/test.json: -------------------------------------------------------------------------------- 1 | { 2 | "nedb": "../test/data" 3 | } 4 | -------------------------------------------------------------------------------- /config/production.json: -------------------------------------------------------------------------------- 1 | { 2 | "host": "feathers-chat-app.feathersjs.com", 3 | "port": "PORT" 4 | } 5 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/feathersjs-ecosystem/feathers-chat-ts/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /.nycrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@istanbuljs/nyc-config-typescript", 3 | "include": [ 4 | "src/**/*.ts", 5 | "src/**/*.tsx" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /src/middleware/index.ts: -------------------------------------------------------------------------------- 1 | import { Application } from '../declarations'; 2 | // Don't remove this comment. It's needed to format import lines nicely. 3 | 4 | export default function (app: Application) { 5 | } 6 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2018", 4 | "module": "commonjs", 5 | "outDir": "./lib", 6 | "rootDir": "./src", 7 | "strict": true, 8 | "esModuleInterop": true 9 | }, 10 | "exclude": [ 11 | "test" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /src/services/messages/messages.class.ts: -------------------------------------------------------------------------------- 1 | import { Service, NedbServiceOptions } from 'feathers-nedb'; 2 | import { Application } from '../../declarations'; 3 | 4 | export class Messages extends Service { 5 | constructor(options: Partial, app: Application) { 6 | super(options); 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /src/services/index.ts: -------------------------------------------------------------------------------- 1 | import { Application } from '../declarations'; 2 | import users from './users/users.service'; 3 | import messages from './messages/messages.service'; 4 | // Don't remove this comment. It's needed to format import lines nicely. 5 | 6 | export default function (app: Application) { 7 | app.configure(users); 8 | app.configure(messages); 9 | } 10 | -------------------------------------------------------------------------------- /src/models/messages.model.ts: -------------------------------------------------------------------------------- 1 | import NeDB from '@seald-io/nedb'; 2 | import path from 'path'; 3 | import { Application } from '../declarations'; 4 | 5 | export default function (app: Application) { 6 | const dbPath = app.get('nedb'); 7 | const Model = new NeDB({ 8 | filename: path.join(dbPath, 'messages.db'), 9 | autoload: true 10 | }); 11 | 12 | return Model; 13 | } 14 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import logger from './logger'; 2 | import app from './app'; 3 | 4 | const port = app.get('port'); 5 | const server = app.listen(port); 6 | 7 | process.on('unhandledRejection', (reason, p) => 8 | logger.error('Unhandled Rejection at: Promise ', p, reason) 9 | ); 10 | 11 | server.on('listening', () => 12 | logger.info('Feathers application started on http://%s:%d', app.get('host'), port) 13 | ); 14 | -------------------------------------------------------------------------------- /src/declarations.d.ts: -------------------------------------------------------------------------------- 1 | import { Application as ExpressFeathers } from '@feathersjs/express'; 2 | import { Service } from '@feathersjs/feathers'; 3 | import '@feathersjs/transport-commons'; 4 | 5 | // A mapping of service names to types. Will be extended in service files. 6 | export interface ServiceTypes {} 7 | // The application instance type that will be used everywhere else 8 | export type Application = ExpressFeathers; 9 | -------------------------------------------------------------------------------- /src/models/users.model.ts: -------------------------------------------------------------------------------- 1 | import NeDB from '@seald-io/nedb'; 2 | import path from 'path'; 3 | import { Application } from '../declarations'; 4 | 5 | export default function (app: Application) { 6 | const dbPath = app.get('nedb'); 7 | const Model = new NeDB({ 8 | filename: path.join(dbPath, 'users.db'), 9 | autoload: true 10 | }); 11 | 12 | Model.ensureIndex({ fieldName: 'email', unique: true }); 13 | 14 | return Model; 15 | } 16 | -------------------------------------------------------------------------------- /src/logger.ts: -------------------------------------------------------------------------------- 1 | import { createLogger, format, transports } from 'winston'; 2 | 3 | // Configure the Winston logger. For the complete documentation see https://github.com/winstonjs/winston 4 | const logger = createLogger({ 5 | // To see more detailed errors, change this to 'debug' 6 | level: 'info', 7 | format: format.combine( 8 | format.splat(), 9 | format.simple() 10 | ), 11 | transports: [ 12 | new transports.Console() 13 | ], 14 | }); 15 | 16 | export default logger; 17 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | strategy: 11 | matrix: 12 | node-version: [16.x] 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Use Node.js ${{ matrix.node-version }} 17 | uses: actions/setup-node@v1 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | - run: npm install 21 | - run: npm test 22 | env: 23 | CI: true 24 | -------------------------------------------------------------------------------- /src/app.hooks.ts: -------------------------------------------------------------------------------- 1 | // Application hooks that run for every service 2 | // Don't remove this comment. It's needed to format import lines nicely. 3 | 4 | export default { 5 | before: { 6 | all: [], 7 | find: [], 8 | get: [], 9 | create: [], 10 | update: [], 11 | patch: [], 12 | remove: [] 13 | }, 14 | 15 | after: { 16 | all: [], 17 | find: [], 18 | get: [], 19 | create: [], 20 | update: [], 21 | patch: [], 22 | remove: [] 23 | }, 24 | 25 | error: { 26 | all: [], 27 | find: [], 28 | get: [], 29 | create: [], 30 | update: [], 31 | patch: [], 32 | remove: [] 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /.github/issue_template.md: -------------------------------------------------------------------------------- 1 | ### Steps to reproduce 2 | 3 | (First please check that this issue is not already solved as [described 4 | here](https://github.com/feathersjs/feathers/blob/master/.github/contributing.md#report-a-bug)) 5 | 6 | - [ ] Tell us what broke. The more detailed the better. 7 | - [ ] If you can, please create a simple example that reproduces the issue and link to a gist, jsbin, repo, etc. 8 | 9 | ### Expected behavior 10 | Tell us what should happen 11 | 12 | ### Actual behavior 13 | Tell us what happens instead 14 | 15 | ### System configuration 16 | 17 | Tell us about the applicable parts of your setup. 18 | 19 | **Module versions** (especially the part that's not working): 20 | 21 | **NodeJS version**: 22 | 23 | **Operating System**: 24 | 25 | **Browser Version**: 26 | 27 | **React Native Version**: 28 | 29 | **Module Loader**: -------------------------------------------------------------------------------- /src/services/messages/messages.hooks.ts: -------------------------------------------------------------------------------- 1 | import * as authentication from '@feathersjs/authentication'; 2 | import processMessage from '../../hooks/process-message'; 3 | import populateUser from '../../hooks/populate-user'; 4 | // Don't remove this comment. It's needed to format import lines nicely. 5 | 6 | const { authenticate } = authentication.hooks; 7 | 8 | export default { 9 | before: { 10 | all: [ authenticate('jwt') ], 11 | find: [], 12 | get: [], 13 | create: [processMessage()], 14 | update: [], 15 | patch: [], 16 | remove: [] 17 | }, 18 | 19 | after: { 20 | all: [populateUser()], 21 | find: [], 22 | get: [], 23 | create: [], 24 | update: [], 25 | patch: [], 26 | remove: [] 27 | }, 28 | 29 | error: { 30 | all: [], 31 | find: [], 32 | get: [], 33 | create: [], 34 | update: [], 35 | patch: [], 36 | remove: [] 37 | } 38 | }; 39 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | FeathersJS chat 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /config/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "host": "localhost", 3 | "port": 3030, 4 | "public": "../public/", 5 | "paginate": { 6 | "default": 10, 7 | "max": 50 8 | }, 9 | "ts": true, 10 | "authentication": { 11 | "oauth": { 12 | "redirect": "/", 13 | "github": { 14 | "key": "", 15 | "secret": "" 16 | } 17 | }, 18 | "entity": "user", 19 | "service": "users", 20 | "secret": "Un5PhBN7NgRdM6EvAm7h/Pb4JkA=", 21 | "authStrategies": [ 22 | "jwt", 23 | "local" 24 | ], 25 | "jwtOptions": { 26 | "header": { 27 | "typ": "access" 28 | }, 29 | "audience": "https://yourdomain.com", 30 | "issuer": "feathers", 31 | "algorithm": "HS256", 32 | "expiresIn": "1d" 33 | }, 34 | "local": { 35 | "usernameField": "email", 36 | "passwordField": "password" 37 | } 38 | }, 39 | "nedb": "../data" 40 | } 41 | -------------------------------------------------------------------------------- /.github/workflows/update-dependencies.yml: -------------------------------------------------------------------------------- 1 | name: Update dependencies 2 | 3 | on: 4 | schedule: 5 | - cron: '0 0 1 * *' 6 | workflow_dispatch: 7 | jobs: 8 | update-dependencies: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | - name: Use Node.js 13 | uses: actions/setup-node@v1 14 | with: 15 | node-version: '15.x' 16 | - run: npm ci 17 | - run: | 18 | git config user.name "GitHub Actions Bot" 19 | git config user.email "hello@feathersjs.com" 20 | git checkout -b update-dependencies-$GITHUB_RUN_ID 21 | - run: | 22 | npm run update-dependencies 23 | npm install 24 | - run: | 25 | git commit -am "chore(dependencies): Update dependencies" 26 | git push origin update-dependencies-$GITHUB_RUN_ID 27 | - run: | 28 | gh pr create --title "chore(dependencies): Update all dependencies" --body "" 29 | env: 30 | GITHUB_TOKEN: ${{secrets.CI_ACCESS_TOKEN}} -------------------------------------------------------------------------------- /src/services/users/users.service.ts: -------------------------------------------------------------------------------- 1 | // Initializes the `users` service on path `/users` 2 | import { ServiceAddons } from '@feathersjs/feathers'; 3 | import { Application } from '../../declarations'; 4 | import { Users } from './users.class'; 5 | import createModel from '../../models/users.model'; 6 | import hooks from './users.hooks'; 7 | 8 | // Add this service to the service type index 9 | declare module '../../declarations' { 10 | interface ServiceTypes { 11 | 'users': Users & ServiceAddons; 12 | } 13 | } 14 | 15 | export default function (app: Application) { 16 | const Model = createModel(app); 17 | const paginate = app.get('paginate'); 18 | 19 | const options = { 20 | Model, 21 | paginate 22 | }; 23 | 24 | // Initialize our service with any options it requires 25 | app.use('/users', new Users(options, app)); 26 | 27 | // Get our initialized service so that we can register hooks 28 | const service = app.service('users'); 29 | 30 | service.hooks(hooks); 31 | } 32 | -------------------------------------------------------------------------------- /src/services/messages/messages.service.ts: -------------------------------------------------------------------------------- 1 | // Initializes the `messages` service on path `/messages` 2 | import { ServiceAddons } from '@feathersjs/feathers'; 3 | import { Application } from '../../declarations'; 4 | import { Messages } from './messages.class'; 5 | import createModel from '../../models/messages.model'; 6 | import hooks from './messages.hooks'; 7 | 8 | // Add this service to the service type index 9 | declare module '../../declarations' { 10 | interface ServiceTypes { 11 | 'messages': Messages & ServiceAddons; 12 | } 13 | } 14 | 15 | export default function (app: Application) { 16 | const Model = createModel(app); 17 | const paginate = app.get('paginate'); 18 | 19 | const options = { 20 | Model, 21 | paginate 22 | }; 23 | 24 | // Initialize our service with any options it requires 25 | app.use('/messages', new Messages(options, app)); 26 | 27 | // Get our initialized service so that we can register hooks 28 | const service = app.service('messages'); 29 | 30 | service.hooks(hooks); 31 | } 32 | -------------------------------------------------------------------------------- /test/authentication.test.ts: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import app from '../src/app'; 3 | 4 | describe('authentication', () => { 5 | it('registered the authentication service', () => { 6 | assert.ok(app.service('authentication')); 7 | }); 8 | 9 | describe('local strategy', () => { 10 | const userInfo = { 11 | email: 'someone@example.com', 12 | password: 'supersecret' 13 | }; 14 | 15 | before(async () => { 16 | try { 17 | await app.service('users').create(userInfo); 18 | } catch (error) { 19 | // Do nothing, it just means the user already exists and can be tested 20 | } 21 | }); 22 | 23 | it('authenticates user and creates accessToken', async () => { 24 | const { user, accessToken } = await app.service('authentication').create({ 25 | strategy: 'local', 26 | ...userInfo 27 | }, {}); 28 | 29 | assert.ok(accessToken, 'Created access token for user'); 30 | assert.ok(user, 'Includes user in authentication data'); 31 | }); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /src/hooks/process-message.ts: -------------------------------------------------------------------------------- 1 | // Use this hook to manipulate incoming or outgoing data. 2 | // For more information on hooks see: http://docs.feathersjs.com/api/hooks.html 3 | import { Hook, HookContext } from '@feathersjs/feathers'; 4 | 5 | export default () : Hook => { 6 | return async (context: HookContext) => { 7 | const { data } = context; 8 | 9 | // Throw an error if we didn't get a text 10 | if(!data.text) { 11 | throw new Error('A message must have a text'); 12 | } 13 | 14 | // The authenticated user 15 | const user = context.params.user; 16 | // The actual message text 17 | const text = context.data.text 18 | // Messages can't be longer than 400 characters 19 | .substring(0, 400); 20 | 21 | // Override the original data (so that people can't submit additional stuff) 22 | context.data = { 23 | text, 24 | // Set the user id 25 | userId: user!._id, 26 | // Add the current date 27 | createdAt: new Date().getTime() 28 | }; 29 | 30 | // Best practice: hooks should always return the context 31 | return context; 32 | }; 33 | } 34 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ### Summary 2 | 3 | (If you have not already please refer to the contributing guideline as [described 4 | here](https://github.com/feathersjs/feathers/blob/master/.github/contributing.md#pull-requests)) 5 | 6 | - [ ] Tell us about the problem your pull request is solving. 7 | - [ ] Are there any open issues that are related to this? 8 | - [ ] Is this PR dependent on PRs in other repos? 9 | 10 | If so, please mention them to keep the conversations linked together. 11 | 12 | ### Other Information 13 | 14 | If there's anything else that's important and relevant to your pull 15 | request, mention that information here. This could include 16 | benchmarks, or other information. 17 | 18 | Your PR will be reviewed by a core team member and they will work with you to get your changes merged in a timely manner. If merged your PR will automatically be added to the changelog in the next release. 19 | 20 | If your changes involve documentation updates please mention that and link the appropriate PR in [feathers-docs](https://github.com/feathersjs/feathers-docs). 21 | 22 | Thanks for contributing to Feathers! :heart: -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Feathers Contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /test/services/messages.test.ts: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import app from '../../src/app'; 3 | 4 | describe('\'messages\' service', () => { 5 | it('registered the service', () => { 6 | const service = app.service('messages'); 7 | 8 | assert.ok(service, 'Registered the service'); 9 | }); 10 | 11 | it('creates and processes message, adds user information', async () => { 12 | // Create a new user we can use for testing 13 | const user: any = await app.service('users').create({ 14 | email: 'messagetest@example.com', 15 | password: 'supersecret' 16 | }); 17 | 18 | // The messages service call params (with the user we just created) 19 | const params = { user }; 20 | const message = await app.service('messages').create({ 21 | text: 'a test', 22 | additional: 'should be removed' 23 | }, params); 24 | 25 | assert.equal(message.text, 'a test'); 26 | // `userId` should be set to passed users it 27 | assert.equal(message.userId, user._id); 28 | // Additional property has been removed 29 | assert.ok(!message.additional); 30 | // `user` has been populated 31 | assert.deepEqual(message.user, user); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /src/services/users/users.hooks.ts: -------------------------------------------------------------------------------- 1 | import * as feathersAuthentication from '@feathersjs/authentication'; 2 | import * as local from '@feathersjs/authentication-local'; 3 | // Don't remove this comment. It's needed to format import lines nicely. 4 | 5 | const { authenticate } = feathersAuthentication.hooks; 6 | const { hashPassword, protect } = local.hooks; 7 | 8 | export default { 9 | before: { 10 | all: [], 11 | find: [ authenticate('jwt') ], 12 | get: [ authenticate('jwt') ], 13 | create: [ hashPassword('password') ], 14 | update: [ hashPassword('password'), authenticate('jwt') ], 15 | patch: [ hashPassword('password'), authenticate('jwt') ], 16 | remove: [ authenticate('jwt') ] 17 | }, 18 | 19 | after: { 20 | all: [ 21 | // Make sure the password field is never sent to the client 22 | // Always must be the last hook 23 | protect('password') 24 | ], 25 | find: [], 26 | get: [], 27 | create: [], 28 | update: [], 29 | patch: [], 30 | remove: [] 31 | }, 32 | 33 | error: { 34 | all: [], 35 | find: [], 36 | get: [], 37 | create: [], 38 | update: [], 39 | patch: [], 40 | remove: [] 41 | } 42 | }; 43 | -------------------------------------------------------------------------------- /test/services/users.test.ts: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import app from '../../src/app'; 3 | 4 | describe('\'users\' service', () => { 5 | it('registered the service', () => { 6 | const service = app.service('users'); 7 | 8 | assert.ok(service, 'Registered the service'); 9 | }); 10 | 11 | it('creates a user, encrypts password and adds gravatar', async () => { 12 | const user: any = await app.service('users').create({ 13 | email: 'test@example.com', 14 | password: 'secret' 15 | }); 16 | 17 | // Verify Gravatar has been set as we'd expect 18 | assert.equal(user.avatar, 'https://s.gravatar.com/avatar/55502f40dc8b7c769880b10874abc9d0?s=60'); 19 | // Makes sure the password got encrypted 20 | assert.ok(user.password !== 'secret'); 21 | }); 22 | 23 | it('removes password for external requests', async () => { 24 | // Setting `provider` indicates an external request 25 | const params = { provider: 'rest' }; 26 | 27 | const user: any = await app.service('users').create({ 28 | email: 'test2@example.com', 29 | password: 'secret' 30 | }, params); 31 | 32 | // Make sure password has been removed 33 | assert.ok(!user.password); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /src/hooks/populate-user.ts: -------------------------------------------------------------------------------- 1 | // Use this hook to manipulate incoming or outgoing data. 2 | // For more information on hooks see: http://docs.feathersjs.com/api/hooks.html 3 | import { Hook, HookContext } from '@feathersjs/feathers'; 4 | 5 | export default (): Hook => { 6 | return async (context: HookContext) => { 7 | // Get `app`, `method`, `params` and `result` from the hook context 8 | const { app, method, result, params } = context; 9 | // Function that adds the user to a single message object 10 | const addUser = async (message: any) => { 11 | // Get the user based on their id, pass the `params` along so 12 | // that we get a safe version of the user data 13 | const user = await app.service('users').get(message.userId, params); 14 | 15 | // Merge the message content to include the `user` object 16 | return { 17 | ...message, 18 | user 19 | }; 20 | }; 21 | 22 | // In a find method we need to process the entire page 23 | if (method === 'find') { 24 | // Map all data to include the `user` information 25 | context.result.data = await Promise.all(result.data.map(addUser)); 26 | } else { 27 | // Otherwise just update the single result 28 | context.result = await addUser(result); 29 | } 30 | 31 | return context; 32 | }; 33 | } 34 | -------------------------------------------------------------------------------- /src/authentication.ts: -------------------------------------------------------------------------------- 1 | import { ServiceAddons, Params } from '@feathersjs/feathers'; 2 | import { AuthenticationService, JWTStrategy } from '@feathersjs/authentication'; 3 | import { LocalStrategy } from '@feathersjs/authentication-local'; 4 | import { expressOauth, OAuthStrategy, OAuthProfile } from '@feathersjs/authentication-oauth'; 5 | 6 | import { Application } from './declarations'; 7 | 8 | declare module './declarations' { 9 | interface ServiceTypes { 10 | 'authentication': AuthenticationService & ServiceAddons; 11 | } 12 | } 13 | 14 | class GitHubStrategy extends OAuthStrategy { 15 | async getEntityData(profile: OAuthProfile, existing: any, params: Params) { 16 | const baseData = await super.getEntityData(profile, existing, params); 17 | 18 | return { 19 | ...baseData, 20 | // You can also set the display name to profile.name 21 | name: profile.login, 22 | // The GitHub profile image 23 | avatar: profile.avatar_url, 24 | // The user email address (if available) 25 | email: profile.email 26 | }; 27 | } 28 | } 29 | 30 | export default function(app: Application) { 31 | const authentication = new AuthenticationService(app); 32 | 33 | authentication.register('jwt', new JWTStrategy()); 34 | authentication.register('local', new LocalStrategy()); 35 | authentication.register('github', new GitHubStrategy()); 36 | 37 | app.use('/authentication', authentication); 38 | app.configure(expressOauth()); 39 | } 40 | -------------------------------------------------------------------------------- /src/services/users/users.class.ts: -------------------------------------------------------------------------------- 1 | import crypto from 'crypto'; 2 | import { Params } from '@feathersjs/feathers'; 3 | import { Service, NedbServiceOptions } from 'feathers-nedb'; 4 | import { Application } from '../../declarations'; 5 | 6 | // The Gravatar image service 7 | const gravatarUrl = 'https://s.gravatar.com/avatar'; 8 | // The size query. Our chat needs 60px images 9 | const query = 's=60'; 10 | // Returns the Gravatar image for an email 11 | const getGravatar = (email: string) => { 12 | // Gravatar uses MD5 hashes from an email address (all lowercase) to get the image 13 | const hash = crypto.createHash('md5').update(email.toLowerCase()).digest('hex'); 14 | // Return the full avatar URL 15 | return `${gravatarUrl}/${hash}?${query}`; 16 | } 17 | 18 | // A type interface for our user (it does not validate any data) 19 | export interface UserData { 20 | _id?: string; 21 | email: string; 22 | password: string; 23 | name?: string; 24 | avatar?: string; 25 | githubId?: string; 26 | } 27 | 28 | export class Users extends Service { 29 | constructor(options: Partial, app: Application) { 30 | super(options); 31 | } 32 | 33 | create (data: UserData, params?: Params) { 34 | // This is the information we want from the user signup data 35 | const { email, password, githubId, name } = data; 36 | // Use the existing avatar image or return the Gravatar for the email 37 | const avatar = data.avatar || getGravatar(email); 38 | // The complete user 39 | const userData = { 40 | email, 41 | name, 42 | password, 43 | githubId, 44 | avatar 45 | }; 46 | 47 | // Call the original `create` method with existing `params` and new data 48 | return super.create(userData, params); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/app.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import favicon from 'serve-favicon'; 3 | import compress from 'compression'; 4 | import helmet from 'helmet'; 5 | 6 | import feathers from '@feathersjs/feathers'; 7 | import configuration from '@feathersjs/configuration'; 8 | import express from '@feathersjs/express'; 9 | import socketio from '@feathersjs/socketio'; 10 | 11 | 12 | import { Application } from './declarations'; 13 | import logger from './logger'; 14 | import middleware from './middleware'; 15 | import services from './services'; 16 | import appHooks from './app.hooks'; 17 | import channels from './channels'; 18 | import authentication from './authentication'; 19 | // Don't remove this comment. It's needed to format import lines nicely. 20 | 21 | const app: Application = express(feathers()); 22 | 23 | // Load app configuration 24 | app.configure(configuration()); 25 | // Enable security, CORS, compression, favicon and body parsing 26 | app.use(helmet({ 27 | contentSecurityPolicy: false 28 | })); 29 | app.use(compress()); 30 | app.use(express.json()); 31 | app.use(express.urlencoded({ extended: true })); 32 | app.use(favicon(path.join(app.get('public'), 'favicon.ico'))); 33 | // Host the public folder 34 | app.use('/', express.static(app.get('public'))); 35 | 36 | // Set up Plugins and providers 37 | app.configure(express.rest()); 38 | app.configure(socketio()); 39 | 40 | // Configure other middleware (see `middleware/index.js`) 41 | app.configure(middleware); 42 | app.configure(authentication); 43 | // Set up our services (see `services/index.js`) 44 | app.configure(services); 45 | // Set up event channels (see channels.js) 46 | app.configure(channels); 47 | 48 | // Configure a middleware for 404s and the error handler 49 | app.use(express.notFound()); 50 | app.use(express.errorHandler({ logger })); 51 | 52 | app.hooks(appHooks); 53 | 54 | export default app; 55 | -------------------------------------------------------------------------------- /test/app.test.ts: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import { Server } from 'http'; 3 | import url from 'url'; 4 | import axios from 'axios'; 5 | 6 | import app from '../src/app'; 7 | 8 | const port = app.get('port') || 8998; 9 | const getUrl = (pathname?: string) => url.format({ 10 | hostname: app.get('host') || 'localhost', 11 | protocol: 'http', 12 | port, 13 | pathname 14 | }); 15 | 16 | describe('Feathers application tests', () => { 17 | let server: Server; 18 | 19 | before(function(done) { 20 | server = app.listen(port); 21 | server.once('listening', () => done()); 22 | }); 23 | 24 | after(function(done) { 25 | server.close(done); 26 | }); 27 | 28 | it('starts and shows the index page', async () => { 29 | const { data } = await axios.get(getUrl()); 30 | 31 | assert.ok(data.indexOf('') !== -1); 32 | }); 33 | 34 | describe('404', function() { 35 | it('shows a 404 HTML page', async () => { 36 | try { 37 | await axios.get(getUrl('path/to/nowhere'), { 38 | headers: { 39 | 'Accept': 'text/html' 40 | } 41 | }); 42 | assert.fail('should never get here'); 43 | } catch (error: any) { 44 | const { response } = error; 45 | 46 | assert.equal(response.status, 404); 47 | assert.ok(response.data.indexOf('') !== -1); 48 | } 49 | }); 50 | 51 | it('shows a 404 JSON error without stack trace', async () => { 52 | try { 53 | await axios.get(getUrl('path/to/nowhere')); 54 | assert.fail('should never get here'); 55 | } catch (error: any) { 56 | const { response } = error; 57 | 58 | assert.equal(response.status, 404); 59 | assert.equal(response.data.code, 404); 60 | assert.equal(response.data.message, 'Page not found'); 61 | assert.equal(response.data.name, 'NotFound'); 62 | } 63 | }); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "feathers-chat", 3 | "description": "A FeathersJS real-time chat application written in TypeScript", 4 | "version": "0.0.0", 5 | "homepage": "", 6 | "main": "src", 7 | "keywords": [ 8 | "feathers" 9 | ], 10 | "author": { 11 | "name": "David Luecke", 12 | "email": "daff@neyeon.com" 13 | }, 14 | "contributors": [], 15 | "bugs": {}, 16 | "directories": { 17 | "lib": "src", 18 | "test": "test/", 19 | "config": "config/" 20 | }, 21 | "engines": { 22 | "node": "^11.0.0", 23 | "npm": ">= 3.0.0" 24 | }, 25 | "scripts": { 26 | "test": "npm run compile && npm run coverage", 27 | "dev": "ts-node-dev --no-notify src/", 28 | "start": "npm run compile && node lib/", 29 | "clean": "shx rm -rf test/data/", 30 | "coverage": "nyc npm run mocha", 31 | "mocha": "npm run clean && NODE_ENV=test mocha --require ts-node/register --require source-map-support/register \"test/**/*.ts\" --recursive --exit", 32 | "compile": "shx rm -rf lib/ && tsc", 33 | "update-dependencies": "ncu -u" 34 | }, 35 | "dependencies": { 36 | "@feathersjs/authentication": "^4.5.15", 37 | "@feathersjs/authentication-local": "^4.5.15", 38 | "@feathersjs/authentication-oauth": "^4.5.15", 39 | "@feathersjs/configuration": "^4.5.15", 40 | "@feathersjs/errors": "^4.5.15", 41 | "@feathersjs/express": "^4.5.15", 42 | "@feathersjs/feathers": "^4.5.15", 43 | "@feathersjs/socketio": "^4.5.15", 44 | "@seald-io/nedb": "^3.0.0", 45 | "compression": "^1.7.4", 46 | "cors": "^2.8.5", 47 | "feathers-nedb": "^6.0.0", 48 | "helmet": "^5.1.1", 49 | "serve-favicon": "^2.5.0", 50 | "winston": "^3.8.1" 51 | }, 52 | "devDependencies": { 53 | "@istanbuljs/nyc-config-typescript": "^1.0.2", 54 | "@types/compression": "^1.7.2", 55 | "@types/cors": "^2.8.12", 56 | "@types/jsonwebtoken": "^8.5.8", 57 | "@types/mocha": "^9.1.1", 58 | "@types/serve-favicon": "^2.5.3", 59 | "axios": "^0.27.2", 60 | "mocha": "^10.0.0", 61 | "nodemon": "^2.0.19", 62 | "npm-check-updates": "^16.0.4", 63 | "nyc": "^15.1.0", 64 | "shx": "^0.3.4", 65 | "ts-node": "^10.9.1", 66 | "ts-node-dev": "^2.0.0", 67 | "tslint": "^6.1.3", 68 | "typescript": "^4.7.4" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Commenting this out is preferred by some people, see 24 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 25 | node_modules 26 | 27 | # Users Environment Variables 28 | .lock-wscript 29 | 30 | # IDEs and editors (shamelessly copied from @angular/cli's .gitignore) 31 | /.idea 32 | .project 33 | .classpath 34 | .c9/ 35 | *.launch 36 | .settings/ 37 | *.sublime-workspace 38 | 39 | # IDE - VSCode 40 | .vscode/* 41 | !.vscode/tasks.json 42 | !.vscode/launch.json 43 | !.vscode/extensions.json 44 | 45 | ### Linux ### 46 | *~ 47 | 48 | # temporary files which can be created if a process still has a handle open of a deleted file 49 | .fuse_hidden* 50 | 51 | # KDE directory preferences 52 | .directory 53 | 54 | # Linux trash folder which might appear on any partition or disk 55 | .Trash-* 56 | 57 | # .nfs files are created when an open file is removed but is still being accessed 58 | .nfs* 59 | 60 | ### OSX ### 61 | *.DS_Store 62 | .AppleDouble 63 | .LSOverride 64 | 65 | # Icon must end with two \r 66 | Icon 67 | 68 | 69 | # Thumbnails 70 | ._* 71 | 72 | # Files that might appear in the root of a volume 73 | .DocumentRevisions-V100 74 | .fseventsd 75 | .Spotlight-V100 76 | .TemporaryItems 77 | .Trashes 78 | .VolumeIcon.icns 79 | .com.apple.timemachine.donotpresent 80 | 81 | # Directories potentially created on remote AFP share 82 | .AppleDB 83 | .AppleDesktop 84 | Network Trash Folder 85 | Temporary Items 86 | .apdisk 87 | 88 | ### Windows ### 89 | # Windows thumbnail cache files 90 | Thumbs.db 91 | ehthumbs.db 92 | ehthumbs_vista.db 93 | 94 | # Folder config file 95 | Desktop.ini 96 | 97 | # Recycle Bin used on file shares 98 | $RECYCLE.BIN/ 99 | 100 | # Windows Installer files 101 | *.cab 102 | *.msi 103 | *.msm 104 | *.msp 105 | 106 | # Windows shortcuts 107 | *.lnk 108 | 109 | # Others 110 | lib/ 111 | data/ 112 | test/data 113 | .nyc_output 114 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # feathers-chat-ts 2 | 3 | > __Important:__ This repository has been consolidated into https://github.com/feathersjs/feathers-chat 4 | 5 | [![CI](https://github.com/feathersjs/feathers-chat-ts/workflows/CI/badge.svg)](https://github.com/feathersjs/feathers-chat-ts/actions?query=workflow%3ACI) 6 | 7 | > A FeathersJS chat application written in TypeScript 8 | 9 | ## About 10 | 11 | This project uses [Feathers](http://feathersjs.com), a framework for real-time applications and REST APIs. It contains the chat application created in [the Feathers guide](https://docs.feathersjs.com/guides/) using TypeScript and a frontend in plain JavaScript. 12 | 13 | ![The Feathers chat application](https://docs.feathersjs.com/assets/img/feathers-chat.91960785.png) 14 | 15 | Other chat frontends can be found at: 16 | 17 | - JavaScript: [feathersjs/feathers-chat](https://github.com/feathersjs/feathers-chat) 18 | - React: [feathersjs-ecosystem/feathers-chat-react](https://github.com/feathersjs-ecosystem/feathers-chat-react) 19 | - React Native: [feathersjs-ecosystem/feathers-react-native-chat](https://github.com/feathersjs-ecosystem/feathers-react-native-chat) 20 | - Angular: [feathersjs-ecosystem/feathers-chat-angular](https://github.com/feathersjs-ecosystem/feathers-chat-angular) 21 | - VueJS with Vuex: [feathers-plus/feathers-chat-vuex](https://github.com/feathers-plus/feathers-chat-vuex) 22 | 23 | > __Important:__ This project requires NodeJS 10 or later. 24 | 25 | ## Getting Started 26 | 27 | Getting up and running is as easy as 1, 2, 3. 28 | 29 | 1. Make sure you have [NodeJS](https://nodejs.org/) and [npm](https://www.npmjs.com/) installed. 30 | 2. Install your dependencies 31 | 32 | ``` 33 | cd path/to/feathers-chat 34 | npm install 35 | ``` 36 | 37 | 3. Start your app 38 | 39 | ``` 40 | npm start 41 | ``` 42 | 43 | ## Testing 44 | 45 | Simply run `npm test` and all your tests in the `test/` directory will be run. 46 | 47 | ## Scaffolding 48 | 49 | Feathers has a powerful command line interface. Here are a few things it can do: 50 | 51 | ``` 52 | $ npm install -g @feathersjs/cli # Install Feathers CLI 53 | 54 | $ feathers generate service # Generate a new Service 55 | $ feathers generate hook # Generate a new Hook 56 | $ feathers help # Show all commands 57 | ``` 58 | 59 | ## Help 60 | 61 | For more information on all the things you can do with Feathers visit [docs.feathersjs.com](http://docs.feathersjs.com). 62 | -------------------------------------------------------------------------------- /src/channels.ts: -------------------------------------------------------------------------------- 1 | import { HookContext } from '@feathersjs/feathers'; 2 | import { Application } from './declarations'; 3 | 4 | export default function(app: Application) { 5 | if(typeof app.channel !== 'function') { 6 | // If no real-time functionality has been configured just return 7 | return; 8 | } 9 | 10 | app.on('connection', (connection: any) => { 11 | // On a new real-time connection, add it to the anonymous channel 12 | app.channel('anonymous').join(connection); 13 | }); 14 | 15 | app.on('login', (authResult: any, { connection }: any) => { 16 | // connection can be undefined if there is no 17 | // real-time connection, e.g. when logging in via REST 18 | if(connection) { 19 | // Obtain the logged in user from the connection 20 | // const user = connection.user; 21 | 22 | // The connection is no longer anonymous, remove it 23 | app.channel('anonymous').leave(connection); 24 | 25 | // Add it to the authenticated user channel 26 | app.channel('authenticated').join(connection); 27 | 28 | // Channels can be named anything and joined on any condition 29 | 30 | // E.g. to send real-time events only to admins use 31 | // if(user.isAdmin) { app.channel('admins').join(connection); } 32 | 33 | // If the user has joined e.g. chat rooms 34 | // if(Array.isArray(user.rooms)) user.rooms.forEach(room => app.channel(`rooms/${room.id}`).join(connection)); 35 | 36 | // Easily organize users by email and userid for things like messaging 37 | // app.channel(`emails/${user.email}`).join(connection); 38 | // app.channel(`userIds/${user.id}`).join(connection); 39 | } 40 | }); 41 | 42 | // eslint-disable-next-line no-unused-vars 43 | app.publish((data: any, hook: HookContext) => { 44 | // Here you can add event publishers to channels set up in `channels.js` 45 | // To publish only for a specific event use `app.publish(eventname, () => {})` 46 | 47 | console.log('Publishing all events to all authenticated users. See `channels.js` and https://docs.feathersjs.com/api/channels.html for more information.'); // eslint-disable-line 48 | 49 | // e.g. to publish all service events to all authenticated users use 50 | return app.channel('authenticated'); 51 | }); 52 | 53 | // Here you can also add service specific event publishers 54 | // e.g. the publish the `users` service `created` event to the `admins` channel 55 | // app.service('users').publish('created', () => app.channel('admins')); 56 | 57 | // With the userid and email organization from above you can easily select involved users 58 | // app.service('messages').publish(() => { 59 | // return [ 60 | // app.channel(`userIds/${data.createdBy}`), 61 | // app.channel(`emails/${data.recipientEmail}`) 62 | // ]; 63 | // }); 64 | }; 65 | -------------------------------------------------------------------------------- /.github/contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing to Feathers 2 | 3 | Thank you for contributing to Feathers! :heart: :tada: 4 | 5 | This repo is the main core and where most issues are reported. Feathers embraces modularity and is broken up across many repos. To make this easier to manage we currently use [Zenhub](https://www.zenhub.com/) for issue triage and visibility. They have a free browser plugin you can install so that you can see what is in flight at any time, but of course you also always see current issues in Github. 6 | 7 | ## Report a bug 8 | 9 | Before creating an issue please make sure you have checked out the docs, specifically the [FAQ](https://docs.feathersjs.com/help/faq.html) section. You might want to also try searching Github. It's pretty likely someone has already asked a similar question. 10 | 11 | If you haven't found your answer please feel free to join our [slack channel](http://slack.feathersjs.com), create an issue on Github, or post on [Stackoverflow](http://stackoverflow.com) using the `feathers` or `feathersjs` tag. We try our best to monitor Stackoverflow but you're likely to get more immediate responses in Slack and Github. 12 | 13 | Issues can be reported in the [issue tracker](https://github.com/feathersjs/feathers/issues). Since feathers combines many modules it can be hard for us to assess the root cause without knowing which modules are being used and what your configuration looks like, so **it helps us immensely if you can link to a simple example that reproduces your issue**. 14 | 15 | ## Report a Security Concern 16 | 17 | We take security very seriously at Feathers. We welcome any peer review of our 100% open source code to ensure nobody's Feathers app is ever compromised or hacked. As a web application developer you are responsible for any security breaches. We do our very best to make sure Feathers is as secure as possible by default. 18 | 19 | In order to give the community time to respond and upgrade we strongly urge you report all security issues to us. Send one of the core team members a PM in [Slack](http://slack.feathersjs.com) or email us at hello@feathersjs.com with details and we will respond ASAP. 20 | 21 | For full details refer to our [Security docs](https://docs.feathersjs.com/SECURITY.html). 22 | 23 | ## Pull Requests 24 | 25 | We :heart: pull requests and we're continually working to make it as easy as possible for people to contribute, including a [Plugin Generator](https://github.com/feathersjs/generator-feathers-plugin) and a [common test suite](https://github.com/feathersjs/feathers-service-tests) for database adapters. 26 | 27 | We prefer small pull requests with minimal code changes. The smaller they are the easier they are to review and merge. A core team member will pick up your PR and review it as soon as they can. They may ask for changes or reject your pull request. This is not a reflection of you as an engineer or a person. Please accept feedback graciously as we will also try to be sensitive when providing it. 28 | 29 | Although we generally accept many PRs they can be rejected for many reasons. We will be as transparent as possible but it may simply be that you do not have the same context or information regarding the roadmap that the core team members have. We value the time you take to put together any contributions so we pledge to always be respectful of that time and will try to be as open as possible so that you don't waste it. :smile: 30 | 31 | **All PRs (except documentation) should be accompanied with tests and pass the linting rules.** 32 | 33 | ### Code style 34 | 35 | Before running the tests from the `test/` folder `npm test` will run ESlint. You can check your code changes individually by running `npm run lint`. 36 | 37 | ### ES6 compilation 38 | 39 | Feathers uses [Babel](https://babeljs.io/) to leverage the latest developments of the JavaScript language. All code and samples are currently written in ES2015. To transpile the code in this repository run 40 | 41 | > npm run compile 42 | 43 | __Note:__ `npm test` will run the compilation automatically before the tests. 44 | 45 | ### Tests 46 | 47 | [Mocha](http://mochajs.org/) tests are located in the `test/` folder and can be run using the `npm run mocha` or `npm test` (with ESLint and code coverage) command. 48 | 49 | ### Documentation 50 | 51 | Feathers documentation is contained in Markdown files in the [feathers-docs](https://github.com/feathersjs/feathers-docs) repository. To change the documentation submit a pull request to that repo, referencing any other PR if applicable, and the docs will be updated with the next release. 52 | 53 | ## External Modules 54 | 55 | If you're written something awesome for Feathers, the Feathers ecosystem, or using Feathers please add it to the [showcase](https://docs.feathersjs.com/why/showcase.html). You also might want to check out the [Plugin Generator](https://github.com/feathersjs/generator-feathers-plugin) that can be used to scaffold plugins to be Feathers compliant from the start. 56 | 57 | If you think it would be a good core module then please contact one of the Feathers core team members in [Slack](http://slack.feathersjs.com) and we can discuss whether it belongs in core and how to get it there. :beers: 58 | 59 | ## Contributor Code of Conduct 60 | 61 | As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities. 62 | 63 | We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, or religion. 64 | 65 | Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct. 66 | 67 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team. 68 | 69 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers. 70 | 71 | This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.0.0, available at [http://contributor-covenant.org/version/1/0/0/](http://contributor-covenant.org/version/1/0/0/) 72 | -------------------------------------------------------------------------------- /public/client.js: -------------------------------------------------------------------------------- 1 | // Establish a Socket.io connection 2 | const socket = io(); 3 | // Initialize our Feathers client application through Socket.io 4 | // with hooks and authentication. 5 | const client = feathers(); 6 | 7 | client.configure(feathers.socketio(socket)); 8 | // Use localStorage to store our login token 9 | client.configure(feathers.authentication()); 10 | 11 | // Login screen 12 | const loginHTML = `
13 |
14 |
15 |

Log in or signup

16 |
17 |
18 |
19 |
20 |
21 |
22 | 23 |
24 | 25 |
26 | 27 |
28 | 29 | 32 | 33 | 36 | 37 | 38 | Login with GitHub 39 | 40 |
41 |
42 |
43 |
`; 44 | 45 | // Chat base HTML (without user list and messages) 46 | const chatHTML = `
47 |
48 |
49 | 51 | Chat 52 |
53 |
54 | 55 |
56 | 70 | 71 |
72 |
73 | 74 |
75 | 76 | 77 |
78 |
79 |
80 |
`; 81 | 82 | // Helper to safely escape HTML 83 | const escape = str => str.replace(/&/g, '&') 84 | .replace(//g, '>') 85 | 86 | // Add a new user to the list 87 | const addUser = user => { 88 | const userList = document.querySelector('.user-list'); 89 | 90 | if(userList) { 91 | // Add the user to the list 92 | userList.innerHTML += `
  • 93 | 94 | 95 | ${escape(user.name || user.email)} 96 | 97 |
  • `; 98 | 99 | // Update the number of users 100 | const userCount = document.querySelectorAll('.user-list li').length; 101 | 102 | document.querySelector('.online-count').innerHTML = userCount; 103 | } 104 | }; 105 | 106 | // Renders a message to the page 107 | const addMessage = message => { 108 | // The user that sent this message (added by the populate-user hook) 109 | const { user = {} } = message; 110 | const chat = document.querySelector('.chat'); 111 | // Escape HTML to prevent XSS attacks 112 | const text = escape(message.text); 113 | 114 | if(chat) { 115 | chat.innerHTML += `
    116 | ${user.name || user.email} 117 |
    118 |

    119 | ${escape(user.name || user.email)} 120 | ${moment(message.createdAt).format('MMM Do, hh:mm:ss')} 121 |

    122 |

    ${text}

    123 |
    124 |
    `; 125 | 126 | // Always scroll to the bottom of our message list 127 | chat.scrollTop = chat.scrollHeight - chat.clientHeight; 128 | } 129 | }; 130 | 131 | // Show the login page 132 | const showLogin = (error) => { 133 | if(document.querySelectorAll('.login').length && error) { 134 | document.querySelector('.heading').insertAdjacentHTML('beforeend', `

    There was an error: ${error.message}

    `); 135 | } else { 136 | document.getElementById('app').innerHTML = loginHTML; 137 | } 138 | }; 139 | 140 | // Shows the chat page 141 | const showChat = async () => { 142 | document.getElementById('app').innerHTML = chatHTML; 143 | 144 | // Find the latest 25 messages. They will come with the newest first 145 | const messages = await client.service('messages').find({ 146 | query: { 147 | $sort: { createdAt: -1 }, 148 | $limit: 25 149 | } 150 | }); 151 | 152 | // We want to show the newest message last 153 | messages.data.reverse().forEach(addMessage); 154 | 155 | // Find all users 156 | const users = await client.service('users').find(); 157 | 158 | // Add each user to the list 159 | users.data.forEach(addUser); 160 | }; 161 | 162 | // Retrieve email/password object from the login/signup page 163 | const getCredentials = () => { 164 | const user = { 165 | email: document.querySelector('[name="email"]').value, 166 | password: document.querySelector('[name="password"]').value 167 | }; 168 | 169 | return user; 170 | }; 171 | 172 | // Log in either using the given email/password or the token from storage 173 | const login = async credentials => { 174 | try { 175 | if(!credentials) { 176 | // Try to authenticate using an existing token 177 | await client.reAuthenticate(); 178 | } else { 179 | // Otherwise log in with the `local` strategy using the credentials we got 180 | await client.authenticate({ 181 | strategy: 'local', 182 | ...credentials 183 | }); 184 | } 185 | 186 | // If successful, show the chat page 187 | showChat(); 188 | } catch(error) { 189 | // If we got an error, show the login page 190 | showLogin(error); 191 | } 192 | }; 193 | 194 | const addEventListener = (selector, event, handler) => { 195 | document.addEventListener(event, async ev => { 196 | if (ev.target.closest(selector)) { 197 | handler(ev); 198 | } 199 | }); 200 | }; 201 | 202 | // "Signup and login" button click handler 203 | addEventListener('#signup', 'click', async () => { 204 | // For signup, create a new user and then log them in 205 | const credentials = getCredentials(); 206 | 207 | // First create the user 208 | await client.service('users').create(credentials); 209 | // If successful log them in 210 | await login(credentials); 211 | }); 212 | 213 | // "Login" button click handler 214 | addEventListener('#login', 'click', async () => { 215 | const user = getCredentials(); 216 | 217 | await login(user); 218 | }); 219 | 220 | // "Logout" button click handler 221 | addEventListener('#logout', 'click', async () => { 222 | await client.logout(); 223 | 224 | document.getElementById('app').innerHTML = loginHTML; 225 | }); 226 | 227 | // "Send" message form submission handler 228 | addEventListener('#send-message', 'submit', async ev => { 229 | // This is the message text input field 230 | const input = document.querySelector('[name="text"]'); 231 | 232 | ev.preventDefault(); 233 | 234 | // Create a new message and then clear the input field 235 | await client.service('messages').create({ 236 | text: input.value 237 | }); 238 | 239 | input.value = ''; 240 | }); 241 | 242 | // Listen to created events and add the new message in real-time 243 | client.service('messages').on('created', addMessage); 244 | 245 | // We will also see when new users get created in real-time 246 | client.service('users').on('created', addUser); 247 | 248 | // Call login right away so we can show the chat window 249 | // If the user can already be authenticated 250 | login(); 251 | --------------------------------------------------------------------------------