From 7434c9fb42b66a8205efe899c52f09b0f3529cd2 Mon Sep 17 00:00:00 2001 From: Jerko Steiner Date: Wed, 28 Aug 2019 18:26:26 +0700 Subject: [PATCH] Add UserService (RPC version) --- packages/common/src/index.ts | 3 + packages/common/src/user.ts | 20 +++++ packages/jsonrpc/src/express.ts | 11 ++- .../server/src/application/Application.ts | 18 +++++ packages/server/src/rpc/RPC.ts | 10 +++ .../TeamService2.ts => rpc/TeamService.ts} | 7 +- packages/server/src/rpc/UserService.ts | 75 +++++++++++++++++++ packages/server/src/rpc/index.ts | 3 + 8 files changed, 142 insertions(+), 5 deletions(-) create mode 100644 packages/common/src/user.ts create mode 100644 packages/server/src/rpc/RPC.ts rename packages/server/src/{services/TeamService2.ts => rpc/TeamService.ts} (95%) create mode 100644 packages/server/src/rpc/UserService.ts create mode 100644 packages/server/src/rpc/index.ts diff --git a/packages/common/src/index.ts b/packages/common/src/index.ts index 588c2c0..c76413f 100644 --- a/packages/common/src/index.ts +++ b/packages/common/src/index.ts @@ -22,5 +22,8 @@ export * from './without' import * as team from './team' export {team} +import * as user from './user' +export {user} + import * as entities from './entities' export {entities} diff --git a/packages/common/src/user.ts b/packages/common/src/user.ts new file mode 100644 index 0000000..b0f5a72 --- /dev/null +++ b/packages/common/src/user.ts @@ -0,0 +1,20 @@ +import {ICredentials} from './ICredentials' +import {IUser} from './IUser' +import * as e from './entities' + +export interface IChangePasswordParams { + oldPassword: string + newPassword: string +} + +export interface ICreateUserParams extends ICredentials { + firstName: string + lastName: string +} + +export interface IUserService { + changePassword(params: IChangePasswordParams): Promise + // validateCredentials(credentials: ICredentials): Promise + findOne(id: number): Promise + findUserByEmail(email: string): Promise +} diff --git a/packages/jsonrpc/src/express.ts b/packages/jsonrpc/src/express.ts index 0204d89..85d5c14 100644 --- a/packages/jsonrpc/src/express.ts +++ b/packages/jsonrpc/src/express.ts @@ -12,11 +12,20 @@ import { export type TGetContext = (req: Request) => Context +export interface IJSONRPCReturnType { + addService>( + path: string, + service: T, + methods: F[], + ): IJSONRPCReturnType, + router(): Router +} + export function jsonrpc( getContext: TGetContext, logger: ILogger, idempotentMethodRegex = IDEMPOTENT_METHOD_REGEX, -) { +): IJSONRPCReturnType { const handleError: ErrorRequestHandler = (err, req, res, next) => { logger.error('JSON-RPC Error: %s', err.stack) diff --git a/packages/server/src/application/Application.ts b/packages/server/src/application/Application.ts index 28b1411..a57baf5 100644 --- a/packages/server/src/application/Application.ts +++ b/packages/server/src/application/Application.ts @@ -1,10 +1,12 @@ import * as middleware from '../middleware' import * as routes from '../routes' +import * as rpc from '../rpc' import * as services from '../services' import * as team from '../team' import * as user from '../user' import cookieParser from 'cookie-parser' import express from 'express' +import {keys} from 'ts-transformer-keys' import {AsyncRouter, TransactionalRouter} from '../router' import {IApplication} from './IApplication' import {IConfig} from './IConfig' @@ -16,6 +18,7 @@ import {ITransactionManager} from '../database/ITransactionManager' import {loggerFactory} from '../logger' import {ILoggerFactory} from '@rondo.dev/logger' import {json} from 'body-parser' +import {jsonrpc} from '@rondo.dev/jsonrpc' export class Application implements IApplication { readonly transactionManager: ITransactionManager @@ -106,6 +109,21 @@ export class Application implements IApplication { this.services.userPermissions, this.createTransactionalRouter(), ).handle) + + router.use( + '/rpc', + jsonrpc( + req => ({user: req.user}), + this.getApiLogger(), + ) + .addService('/teamService', + new rpc.TeamService(this.database, this.services.userPermissions), + keys()) + .addService('/userService', + new rpc.UserService(this.database), + keys()) + .router(), + ) } protected configureApiErrorHandling(router: express.Router) { diff --git a/packages/server/src/rpc/RPC.ts b/packages/server/src/rpc/RPC.ts new file mode 100644 index 0000000..dc17c41 --- /dev/null +++ b/packages/server/src/rpc/RPC.ts @@ -0,0 +1,10 @@ +import { IContext } from '@rondo.dev/common' +import { Contextual, ensure } from '@rondo.dev/jsonrpc' + +export { IContext } +export type RPC = Contextual + +export const ensureLoggedIn = ensure( + c => !!c.user && !!c.user.id, + 'You must be logged in to perform this action', +) diff --git a/packages/server/src/services/TeamService2.ts b/packages/server/src/rpc/TeamService.ts similarity index 95% rename from packages/server/src/services/TeamService2.ts rename to packages/server/src/rpc/TeamService.ts index e658cf3..ca31728 100644 --- a/packages/server/src/services/TeamService2.ts +++ b/packages/server/src/rpc/TeamService.ts @@ -5,15 +5,14 @@ import {UserTeam} from '../entities/UserTeam' import {IUserPermissions} from '../user/IUserPermissions' import { trim, - IContext, entities as e, team as t, IUserInTeam, } from '@rondo.dev/common' -import {Contextual} from '@rondo.dev/jsonrpc' +import { ensureLoggedIn, IContext, RPC } from './RPC' -// TODO ensureLoggedIn -export class TeamService2 implements Contextual { +@ensureLoggedIn +export class TeamService implements RPC { constructor( protected readonly db: IDatabase, protected readonly permissions: IUserPermissions, diff --git a/packages/server/src/rpc/UserService.ts b/packages/server/src/rpc/UserService.ts new file mode 100644 index 0000000..5e127a0 --- /dev/null +++ b/packages/server/src/rpc/UserService.ts @@ -0,0 +1,75 @@ +import { user as u } from '@rondo.dev/common' +import { compare, hash } from 'bcrypt' +import createError from 'http-errors' +import { IDatabase } from '../database/IDatabase' +import { User } from '../entities/User' +import { UserEmail } from '../entities/UserEmail' +import { ensureLoggedIn, IContext, RPC } from './RPC' + +const SALT_ROUNDS = 10 +const MIN_PASSWORD_LENGTH = 10 + +@ensureLoggedIn +export class UserService implements RPC { + constructor(protected readonly db: IDatabase) {} + + async changePassword(params: u.IChangePasswordParams, context: IContext) { + const userId = context.user!.id + const {oldPassword, newPassword} = params + const userRepository = this.db.getRepository(User) + const user = await userRepository + .createQueryBuilder('user') + .select('user') + .addSelect('user.password') + .whereInIds([ userId ]) + .getOne() + const isValid = await compare(oldPassword, user ? user.password! : '') + if (!(user && isValid)) { + throw createError(400, 'Passwords do not match') + } + const password = await this.hash(newPassword) + await userRepository + .update(userId, { password }) + } + + async findOne(id: number) { + const user = await this.db.getRepository(User).findOne(id, { + relations: ['emails'], + }) + + if (!user) { + return undefined + } + + return { + id: user.id, + username: user.emails[0] ? user.emails[0].email : '', + firstName: user.firstName, + lastName: user.lastName, + } + } + + async findUserByEmail(email: string) { + const userEmail = await this.db.getRepository(UserEmail) + .findOne({ email }, { + relations: ['user'], + }) + + if (!userEmail) { + return + } + + const user = userEmail.user! + + return { + id: userEmail.userId!, + username: userEmail.email, + firstName: user.firstName, + lastName: user.lastName, + } + } + + protected async hash(password: string): Promise { + return hash(password, SALT_ROUNDS) + } +} diff --git a/packages/server/src/rpc/index.ts b/packages/server/src/rpc/index.ts new file mode 100644 index 0000000..fff2e94 --- /dev/null +++ b/packages/server/src/rpc/index.ts @@ -0,0 +1,3 @@ +export * from './RPC' +export * from './TeamService' +export * from './UserService'