diff --git a/packages/common/src/user.ts b/packages/common/src/user.ts index b0f5a72..76e30e6 100644 --- a/packages/common/src/user.ts +++ b/packages/common/src/user.ts @@ -1,6 +1,7 @@ import {ICredentials} from './ICredentials' import {IUser} from './IUser' import * as e from './entities' +import {keys} from 'ts-transformer-keys' export interface IChangePasswordParams { oldPassword: string @@ -13,8 +14,8 @@ export interface ICreateUserParams extends ICredentials { } export interface IUserService { - changePassword(params: IChangePasswordParams): Promise - // validateCredentials(credentials: ICredentials): Promise - findOne(id: number): Promise + getProfile(): Promise findUserByEmail(email: string): Promise } + +export const UserServiceMethods = keys() diff --git a/packages/jsonrpc/src/remote.ts b/packages/jsonrpc/src/remote.ts index e812163..21cc904 100644 --- a/packages/jsonrpc/src/remote.ts +++ b/packages/jsonrpc/src/remote.ts @@ -10,6 +10,7 @@ export const constantId = (val: string) => () => val export function createRemoteClient( url: string, methods: Array>, + headers: Record = {}, getNextRequestId: TRequestIdGenerator = constantId('c'), idempotentMethodRegex = IDEMPOTENT_METHOD_REGEX, ) { @@ -26,6 +27,7 @@ export function createRemoteClient( const response = await axios({ method: reqMethod, url, + headers, [payloadKey]: { id, jsonrpc: '2.0', diff --git a/packages/server/src/application/configureServer.ts b/packages/server/src/application/configureServer.ts index 60d6bf7..4efc46e 100644 --- a/packages/server/src/application/configureServer.ts +++ b/packages/server/src/application/configureServer.ts @@ -13,6 +13,8 @@ import * as routes from '../routes' import { TransactionalRouter } from '../router' import { IRoutes, IContext } from '@rondo.dev/common' import { Express } from 'express-serve-static-core' +import { jsonrpc, bulkjsonrpc } from '@rondo.dev/jsonrpc' +import * as rpc from '../rpc' export type ServerConfigurator< T extends IServerConfig = IServerConfig @@ -31,6 +33,21 @@ export const configureServer: ServerConfigurator = (config, database) => { userPermissions: new User.UserPermissions(database), } + const rpcServices = { + userService: new rpc.UserService(database), + teamService: new rpc.TeamService(database, services.userPermissions), + } + + const getContext = (req: Express.Request): IContext => ({user: req.user}) + + const rpcMiddleware = jsonrpc( + req => getContext(req), + logger, + // (details, invoke) => database + // .transactionManager + // .doInNewTransaction(() => invoke()), + ) + const authenticator = new Middleware.Authenticator(services.authService) const transactionManager = database.transactionManager @@ -90,6 +107,10 @@ export const configureServer: ServerConfigurator = (config, database) => { ], error: new Middleware.ErrorApiHandler(logger).handle, }, + rpc: { + path: '/rpc', + handle: [bulkjsonrpc(rpcMiddleware, rpcServices)], + }, }, } } diff --git a/packages/server/src/rpc/UserService.test.ts b/packages/server/src/rpc/UserService.test.ts new file mode 100644 index 0000000..496aed6 --- /dev/null +++ b/packages/server/src/rpc/UserService.test.ts @@ -0,0 +1,48 @@ +import {test} from '../test' +import {user} from '@rondo.dev/common' + +describe('user', () => { + + test.withDatabase() + + let cookie!: string + let token!: string + let headers: Record = {} + beforeEach(async () => { + await test.registerAccount() + const session = await test.login() + cookie = session.cookie + token = session.token + headers = {cookie, 'x-csrf-token': token} + }) + + const createService = () => { + return test.rpc( + '/rpc/userService', + user.UserServiceMethods, + headers, + ) + } + + describe('findOne', () => { + it('fetches current user\'s profile', async () => { + const profile = await createService().getProfile() + expect(profile.id).toEqual(jasmine.any(Number)) + }) + }) + + describe('findUserByEmail', () => { + it('returns undefined when user no found', async () => { + const profile = await createService().findUserByEmail( + 'totallynonexisting@email.com') + expect(profile).toBe(undefined) + }) + + it('returns user profile when found', async () => { + const profile = await createService().findUserByEmail( + test.username) + expect(profile).not.toBe(undefined) + }) + }) + +}) diff --git a/packages/server/src/rpc/UserService.ts b/packages/server/src/rpc/UserService.ts index 72e25e7..0092e0d 100644 --- a/packages/server/src/rpc/UserService.ts +++ b/packages/server/src/rpc/UserService.ts @@ -13,33 +13,13 @@ const MIN_PASSWORD_LENGTH = 10 export class UserService implements RPC { constructor(protected readonly db: IDatabase) {} - async changePassword(context: IContext, params: u.IChangePasswordParams) { + async getProfile(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(context: IContext, id: number) { - const user = await this.db.getRepository(User).findOne(id, { + // current user should always exist in the database + const user = (await this.db.getRepository(User).findOne(userId, { relations: ['emails'], - }) - - if (!user) { - return undefined - } + }))! return { id: user.id, diff --git a/packages/server/src/test-utils/TestUtils.ts b/packages/server/src/test-utils/TestUtils.ts index 2e960c9..8979694 100644 --- a/packages/server/src/test-utils/TestUtils.ts +++ b/packages/server/src/test-utils/TestUtils.ts @@ -10,6 +10,9 @@ import {RequestTester} from './RequestTester' import {Role} from '../entities/Role' import {CORRELATION_ID} from '../middleware' import shortid from 'shortid' +import { AddressInfo } from 'net' +import { createRemoteClient, FunctionPropertyNames, TAsyncified } from '@rondo.dev/jsonrpc' +import {Server} from 'http' export class TestUtils { readonly username = `test${process.env.JEST_WORKER_ID}@user.com` @@ -160,6 +163,39 @@ export class TestUtils { `${this.bootstrap.getConfig().app.baseUrl.path!}${baseUrl}`) } + /** + * Starts the server, invokes a rpc method, and closes the server after + * invocation. + */ + rpc = ( + serviceUrl: string, + methods: Array>, + headers: Record, + ) => { + const {app} = this + const url = `${this.bootstrap.getConfig().app.baseUrl.path!}${serviceUrl}` + + const service = methods.reduce((obj, method) => { + obj[method] = async function makeRequest(...args: any[]) { + let server!: Server + await new Promise(resolve => { + server = app.listen(0, '127.0.0.1', resolve) + }) + const addr = server.address() as AddressInfo + const fullUrl = `http://${addr.address}:${addr.port}${url}` + const remoteService = createRemoteClient(fullUrl, methods, headers) + try { + return await remoteService[method](...args as any) + } finally { + await new Promise(resolve => server.close(resolve)) + } + } + return obj + }, {} as any) + + return service as TAsyncified + } + private getCookies(setCookiesString: string[]): string { return setCookiesString.map(c => c.split('; ')[0]).join('; ') }