Add UserService (RPC version)
This commit is contained in:
parent
cd5ff9b5da
commit
7434c9fb42
@ -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}
|
||||||
|
|||||||
20
packages/common/src/user.ts
Normal file
20
packages/common/src/user.ts
Normal 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>
|
||||||
|
}
|
||||||
@ -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)
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
10
packages/server/src/rpc/RPC.ts
Normal file
10
packages/server/src/rpc/RPC.ts
Normal 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',
|
||||||
|
)
|
||||||
@ -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,
|
||||||
75
packages/server/src/rpc/UserService.ts
Normal file
75
packages/server/src/rpc/UserService.ts
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
3
packages/server/src/rpc/index.ts
Normal file
3
packages/server/src/rpc/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export * from './RPC'
|
||||||
|
export * from './TeamService'
|
||||||
|
export * from './UserService'
|
||||||
Loading…
x
Reference in New Issue
Block a user