Add UserService (RPC version)

This commit is contained in:
Jerko Steiner 2019-08-28 18:26:26 +07:00
parent cd5ff9b5da
commit 7434c9fb42
8 changed files with 142 additions and 5 deletions

View File

@ -22,5 +22,8 @@ export * from './without'
import * as team from './team' import * as team from './team'
export {team} export {team}
import * as user from './user'
export {user}
import * as entities from './entities' import * as entities from './entities'
export {entities} export {entities}

View File

@ -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<void>
// validateCredentials(credentials: ICredentials): Promise<e.User | undefined>
findOne(id: number): Promise<IUser | undefined>
findUserByEmail(email: string): Promise<IUser | undefined>
}

View File

@ -12,11 +12,20 @@ import {
export type TGetContext<Context> = (req: Request) => Context export type TGetContext<Context> = (req: Request) => Context
export interface IJSONRPCReturnType {
addService<T, F extends FunctionPropertyNames<T>>(
path: string,
service: T,
methods: F[],
): IJSONRPCReturnType,
router(): Router
}
export function jsonrpc<Context>( export function jsonrpc<Context>(
getContext: TGetContext<Context>, getContext: TGetContext<Context>,
logger: ILogger, logger: ILogger,
idempotentMethodRegex = IDEMPOTENT_METHOD_REGEX, idempotentMethodRegex = IDEMPOTENT_METHOD_REGEX,
) { ): IJSONRPCReturnType {
const handleError: ErrorRequestHandler = (err, req, res, next) => { const handleError: ErrorRequestHandler = (err, req, res, next) => {
logger.error('JSON-RPC Error: %s', err.stack) logger.error('JSON-RPC Error: %s', err.stack)

View File

@ -1,10 +1,12 @@
import * as middleware from '../middleware' import * as middleware from '../middleware'
import * as routes from '../routes' import * as routes from '../routes'
import * as rpc from '../rpc'
import * as services from '../services' import * as services from '../services'
import * as team from '../team' import * as team from '../team'
import * as user from '../user' import * as user from '../user'
import cookieParser from 'cookie-parser' import cookieParser from 'cookie-parser'
import express from 'express' import express from 'express'
import {keys} from 'ts-transformer-keys'
import {AsyncRouter, TransactionalRouter} from '../router' import {AsyncRouter, TransactionalRouter} from '../router'
import {IApplication} from './IApplication' import {IApplication} from './IApplication'
import {IConfig} from './IConfig' import {IConfig} from './IConfig'
@ -16,6 +18,7 @@ import {ITransactionManager} from '../database/ITransactionManager'
import {loggerFactory} from '../logger' import {loggerFactory} from '../logger'
import {ILoggerFactory} from '@rondo.dev/logger' import {ILoggerFactory} from '@rondo.dev/logger'
import {json} from 'body-parser' import {json} from 'body-parser'
import {jsonrpc} from '@rondo.dev/jsonrpc'
export class Application implements IApplication { export class Application implements IApplication {
readonly transactionManager: ITransactionManager readonly transactionManager: ITransactionManager
@ -106,6 +109,21 @@ export class Application implements IApplication {
this.services.userPermissions, this.services.userPermissions,
this.createTransactionalRouter(), this.createTransactionalRouter(),
).handle) ).handle)
router.use(
'/rpc',
jsonrpc(
req => ({user: req.user}),
this.getApiLogger(),
)
.addService('/teamService',
new rpc.TeamService(this.database, this.services.userPermissions),
keys<rpc.TeamService>())
.addService('/userService',
new rpc.UserService(this.database),
keys<rpc.UserService>())
.router(),
)
} }
protected configureApiErrorHandling(router: express.Router) { protected configureApiErrorHandling(router: express.Router) {

View File

@ -0,0 +1,10 @@
import { IContext } from '@rondo.dev/common'
import { Contextual, ensure } from '@rondo.dev/jsonrpc'
export { IContext }
export type RPC<Service> = Contextual<Service, IContext>
export const ensureLoggedIn = ensure<IContext>(
c => !!c.user && !!c.user.id,
'You must be logged in to perform this action',
)

View File

@ -5,15 +5,14 @@ import {UserTeam} from '../entities/UserTeam'
import {IUserPermissions} from '../user/IUserPermissions' import {IUserPermissions} from '../user/IUserPermissions'
import { import {
trim, trim,
IContext,
entities as e, entities as e,
team as t, team as t,
IUserInTeam, IUserInTeam,
} from '@rondo.dev/common' } from '@rondo.dev/common'
import {Contextual} from '@rondo.dev/jsonrpc' import { ensureLoggedIn, IContext, RPC } from './RPC'
// TODO ensureLoggedIn @ensureLoggedIn
export class TeamService2 implements Contextual<t.ITeamService, IContext> { export class TeamService implements RPC<t.ITeamService> {
constructor( constructor(
protected readonly db: IDatabase, protected readonly db: IDatabase,
protected readonly permissions: IUserPermissions, protected readonly permissions: IUserPermissions,

View File

@ -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<u.IUserService> {
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<string> {
return hash(password, SALT_ROUNDS)
}
}

View File

@ -0,0 +1,3 @@
export * from './RPC'
export * from './TeamService'
export * from './UserService'