Fix packages/server

This commit is contained in:
Jerko Steiner 2019-09-16 10:46:17 +07:00
parent 92912af839
commit 7631f085ef
101 changed files with 885 additions and 910 deletions

View File

@ -1,3 +1,4 @@
packages/*/lib packages/*/lib
packages/*/esm packages/*/esm
build/ build/
packages/*/src/migrations

View File

@ -1,8 +1,8 @@
extends: extends:
- eslint:recommended - eslint:recommended
- plugin:react/recommended - plugin:react/recommended
- plugin:@typescript-eslint/eslint-recommended - plugin:@typescript-eslint/eslint-recommended
- plugin:@typescript-eslint/recommended - plugin:@typescript-eslint/recommended
settings: settings:
react: react:
version: 'detect' version: 'detect'
@ -43,11 +43,13 @@ rules:
- ignoreRestArgs: true - ignoreRestArgs: true
overrides: overrides:
- files: - files:
- '*.test.ts' - '*.test.ts'
- '*.test.tsx' - '*.test.tsx'
rules: rules:
'@typescript-eslint/no-explicit-any': off '@typescript-eslint/no-explicit-any': off
- files: - files:
- '*.js' - '*.js'
rules:
'@typescript-eslint/no-var-requires': off
env: env:
node: true node: true

View File

@ -0,0 +1,13 @@
import { NewUser, UserProfile } from "../user";
import { ChangePasswordParams } from "./ChangePasswordParams";
import { Credentials } from "./Credentials";
export interface AuthService {
createUser(credentials: NewUser): Promise<UserProfile>
changePassword(params: ChangePasswordParams): Promise<void>
validateCredentials(
credentials: Credentials,
): Promise<UserProfile | undefined>
findOne(id: number): Promise<UserProfile | undefined>
findUserByEmail(email: string): Promise<UserProfile | undefined>
}

View File

@ -0,0 +1,5 @@
export interface ChangePasswordParams {
userId: number
oldPassword: string
newPassword: string
}

View File

@ -1 +1,3 @@
export * from './AuthService'
export * from './Credentials' export * from './Credentials'
export * from './ChangePasswordParams'

View File

@ -5,6 +5,7 @@ export * from './entities'
export * from './filterProps' export * from './filterProps'
export * from './guard' export * from './guard'
export * from './indexBy' export * from './indexBy'
export * from './permissions'
export * from './StringUtils' export * from './StringUtils'
export * from './team' export * from './team'
export * from './types' export * from './types'

View File

@ -0,0 +1,4 @@
export interface BelongsToTeamParams {
userId: number
teamId: number
}

View File

@ -0,0 +1,6 @@
import {BelongsToTeamParams} from './BelongsToTeamParams'
export interface UserPermissions {
// TODO check for role too
belongsToTeam(params: BelongsToTeamParams): Promise<void>
}

View File

@ -0,0 +1,2 @@
export * from './BelongsToTeamParams'
export * from './UserPermissions'

View File

@ -0,0 +1,3 @@
export * from './BelongsToTeamParams'
export * from './UserPermissions'

View File

@ -0,0 +1,4 @@
extends:
- ../../.eslintrc.yaml
rules:
'@typescript-eslint/no-explicit-any': off

View File

@ -1,17 +1,17 @@
module.exports = { module.exports = {
roots: [ roots: [
'<rootDir>/src' '<rootDir>/src',
], ],
transform: { transform: {
'^.+\\.tsx?$': 'ts-jest' '^.+\\.tsx?$': 'ts-jest',
}, },
testRegex: '(/__tests__/.*|\\.(test|spec))\\.tsx?$', testRegex: '(/__tests__/.*|\\.(test|spec))\\.tsx?$',
moduleFileExtensions: [ moduleFileExtensions: [
'ts', 'ts',
'tsx', 'tsx',
'js', 'js',
'jsx' 'jsx',
], ],
setupFiles: ['<rootDir>/jest.setup.js'], setupFiles: ['<rootDir>/jest.setup.js'],
verbose: false verbose: false,
} }

View File

@ -1,6 +1,6 @@
import { Server } from 'http' import { Server } from 'http'
export interface IAppServer { export interface AppServer {
listen(callback?: () => void): Server listen(callback?: () => void): Server
listen(callback?: () => void): Server listen(callback?: () => void): Server
listen(portOrPath: number | string, callback?: () => void): Server listen(portOrPath: number | string, callback?: () => void): Server

View File

@ -0,0 +1,7 @@
import { Database } from '../database/Database'
import { AppServer } from './AppServer'
export interface Application {
readonly server: AppServer
readonly database: Database
}

View File

@ -1,173 +1,13 @@
import assert from 'assert' import {AddressInfo} from 'net'
import { createNamespace, Namespace } from 'cls-hooked' import {Application} from './Application'
import { Server } from 'http' import {Database} from '../database/Database'
import { AddressInfo } from 'net' import {Config} from './Config'
import { Database } from '../database/Database'
import { IDatabase } from '../database/IDatabase'
import { loggerFactory, SqlLogger } from '../logger'
import { ServerConfigurator } from './configureServer'
import { createServer } from './createServer'
import { IApplication } from './IApplication'
import { IBootstrap } from './IBootstrap'
import { IConfig } from './IConfig'
import { IServerConfig } from './IServerConfig'
export interface IBootstrapParams { export interface Bootstrap {
readonly config: IConfig readonly application: Application
readonly configureServer: ServerConfigurator readonly database: Database
readonly namespace?: Namespace getConfig(): Config
readonly exit?: (code: number) => void listen(port?: number | string, hostname?: string): Promise<void>
readonly entities?: object getAddress(): AddressInfo | string
readonly migrations?: object close(): Promise<void>
}
// tslint:disable-next-line
function getFunctions(obj: object): Function[] {
return Object.keys(obj)
.map(k => (obj as any)[k])
.filter(f => typeof f === 'function')
}
export class Bootstrap implements IBootstrap {
protected config: IConfig
protected configureServer: ServerConfigurator
protected namespace: Namespace
protected exit: (code: number) => void
protected server?: Server
protected inUse: boolean = false
readonly application: IApplication
readonly database: IDatabase
constructor(params: IBootstrapParams) {
this.config = {
...params.config,
app: {
...params.config.app,
db: {
...params.config.app.db,
entities: params.entities
? getFunctions(params.entities)
: params.config.app.db.entities,
migrations: params.migrations
? getFunctions(params.migrations)
: params.config.app.db.migrations,
},
},
}
this.configureServer = params.configureServer
this.namespace = params.namespace || createNamespace('application')
this.exit = params.exit || process.exit
this.database = this.createDatabase()
this.application = this.createApplication(this.database)
}
getConfig(): IConfig {
return this.config
}
protected createDatabase(): IDatabase {
const {namespace} = this
const sqlLogger = new SqlLogger(loggerFactory.getLogger('sql'), namespace)
return new Database(namespace, sqlLogger, this.getConfig().app.db)
}
protected createApplication(database: IDatabase): IApplication {
const {configureServer} = this
return createServer(configureServer(this.getConfig(), database))
}
async exec(command: string = 'listen') {
switch (command) {
case 'listen':
await this.listen()
return
case 'migrate':
await this.migrate()
return
case 'migrate-undo':
await this.migrateUndo()
return
default:
throw new Error('Unknown command: ' + command)
}
}
async listen(
port: number | string | undefined = process.env.PORT || 3000,
hostname: string | undefined= process.env.BIND_HOST,
) {
const apiLogger = loggerFactory.getLogger('api')
try {
await this.start(port, hostname)
} catch (err) {
apiLogger.error('Error starting server: %s', err.stack)
this.exit(1)
throw err
}
}
async migrate() {
const connection = await this.database.connect()
await connection.runMigrations()
await connection.close()
}
async migrateUndo() {
const connection = await this.database.connect()
await connection.undoLastMigration()
await connection.close()
}
protected async start(
port: number | string | undefined = process.env.PORT,
hostname?: string,
): Promise<void> {
assert.ok(!this.inUse, 'Server already in use!')
this.inUse = true
await this.database.connect()
await new Promise((resolve, reject) => {
const _resolve = () => resolve()
if (!port) {
this.server = this.application.server.listen(_resolve)
return
} else if (typeof port === 'number' && hostname) {
this.server = this.application.server.listen(port, hostname, _resolve)
} else {
this.server = this.application.server.listen(port, _resolve)
}
})
const apiLogger = loggerFactory.getLogger('api')
if (hostname) {
apiLogger.info('Listening on %s %s', port, hostname)
} else {
apiLogger.info('Listening on %s', port)
}
}
getAddress(): AddressInfo | string {
const address = this.server!.address()
if (!address) {
throw new Error('Server addres is null')
}
return address
}
async close(): Promise<void> {
return new Promise((resolve, reject) => {
this.server!.close(err => {
if (!err) {
return resolve()
}
reject(err)
})
this.server = undefined
this.inUse = false
})
}
} }

View File

@ -0,0 +1,171 @@
import assert from 'assert'
import { createNamespace, Namespace } from 'cls-hooked'
import { Server } from 'http'
import { AddressInfo } from 'net'
import { Database, SQLDatabase } from '../database'
import { loggerFactory, SQLLogger } from '../logger'
import { Application } from './Application'
import { Bootstrap } from './Bootstrap'
import { Config } from './Config'
import { ServerConfigurator } from './configureServer'
import { createServer } from './createServer'
export interface CLIBootstrapParams {
readonly config: Config
readonly configureServer: ServerConfigurator
readonly namespace?: Namespace
readonly exit?: (code: number) => void
readonly entities?: object
readonly migrations?: object
}
// tslint:disable-next-line
function getFunctions(obj: object): Function[] {
return Object.keys(obj)
.map(k => (obj as any)[k])
.filter(f => typeof f === 'function')
}
export class CLIBootstrap implements Bootstrap {
protected config: Config
protected configureServer: ServerConfigurator
protected namespace: Namespace
protected exit: (code: number) => void
protected server?: Server
protected inUse = false
readonly application: Application
readonly database: Database
constructor(params: CLIBootstrapParams) {
this.config = {
...params.config,
app: {
...params.config.app,
db: {
...params.config.app.db,
entities: params.entities
? getFunctions(params.entities)
: params.config.app.db.entities,
migrations: params.migrations
? getFunctions(params.migrations)
: params.config.app.db.migrations,
},
},
}
this.configureServer = params.configureServer
this.namespace = params.namespace || createNamespace('application')
this.exit = params.exit || process.exit
this.database = this.createDatabase()
this.application = this.createApplication(this.database)
}
getConfig(): Config {
return this.config
}
protected createDatabase(): Database {
const {namespace} = this
const sqlLogger = new SQLLogger(loggerFactory.getLogger('sql'), namespace)
return new SQLDatabase(namespace, sqlLogger, this.getConfig().app.db)
}
protected createApplication(database: Database): Application {
const {configureServer} = this
return createServer(configureServer(this.getConfig(), database))
}
async exec(command = 'listen') {
switch (command) {
case 'listen':
await this.listen()
return
case 'migrate':
await this.migrate()
return
case 'migrate-undo':
await this.migrateUndo()
return
default:
throw new Error('Unknown command: ' + command)
}
}
async listen(
port: number | string | undefined = process.env.PORT || 3000,
hostname: string | undefined= process.env.BIND_HOST,
) {
const apiLogger = loggerFactory.getLogger('api')
try {
await this.start(port, hostname)
} catch (err) {
apiLogger.error('Error starting server: %s', err.stack)
this.exit(1)
throw err
}
}
async migrate() {
const connection = await this.database.connect()
await connection.runMigrations()
await connection.close()
}
async migrateUndo() {
const connection = await this.database.connect()
await connection.undoLastMigration()
await connection.close()
}
protected async start(
port: number | string | undefined = process.env.PORT,
hostname?: string,
): Promise<void> {
assert.ok(!this.inUse, 'Server already in use!')
this.inUse = true
await this.database.connect()
await new Promise((resolve, reject) => {
const _resolve = () => resolve()
if (!port) {
this.server = this.application.server.listen(_resolve)
return
} else if (typeof port === 'number' && hostname) {
this.server = this.application.server.listen(port, hostname, _resolve)
} else {
this.server = this.application.server.listen(port, _resolve)
}
})
const apiLogger = loggerFactory.getLogger('api')
if (hostname) {
apiLogger.info('Listening on %s %s', port, hostname)
} else {
apiLogger.info('Listening on %s', port)
}
}
getAddress(): AddressInfo | string {
const address = this.server!.address()
if (!address) {
throw new Error('Server addres is null')
}
return address
}
async close(): Promise<void> {
return new Promise((resolve, reject) => {
this.server!.close(err => {
if (!err) {
return resolve()
}
reject(err)
})
this.server = undefined
this.inUse = false
})
}
}

View File

@ -1,7 +1,7 @@
import {UrlWithStringQuery} from 'url' import {UrlWithStringQuery} from 'url'
import {ConnectionOptions} from 'typeorm' import {ConnectionOptions} from 'typeorm'
export interface IConfig { export interface Config {
readonly app: { readonly app: {
readonly name: string readonly name: string
readonly baseUrl: UrlWithStringQuery readonly baseUrl: UrlWithStringQuery

View File

@ -1,7 +0,0 @@
import { IDatabase } from '../database/IDatabase'
import { IAppServer } from './IAppServer'
export interface IApplication {
readonly server: IAppServer
readonly database: IDatabase
}

View File

@ -1,13 +0,0 @@
import {AddressInfo} from 'net'
import {IApplication} from './IApplication'
import {IDatabase} from '../database/IDatabase'
import {IConfig} from './IConfig'
export interface IBootstrap {
readonly application: IApplication
readonly database: IDatabase
getConfig(): IConfig
listen(port?: number | string, hostname?: string): Promise<void>
getAddress(): AddressInfo | string
close(): Promise<void>
}

View File

@ -1,20 +0,0 @@
import { IConfig } from './IConfig'
import { IDatabase } from '../database'
import { ILogger } from '@rondo.dev/logger'
import { IServices } from './IServices'
import { RequestHandlerParams, ErrorRequestHandler } from 'express-serve-static-core'
export interface IServerMiddleware {
path: string
handle: RequestHandlerParams[]
error?: ErrorRequestHandler
}
export interface IServerConfig {
readonly config: IConfig
readonly database: IDatabase
readonly logger: ILogger
readonly services: IServices
readonly globalErrorHandler: ErrorRequestHandler
readonly framework: Record<string, IServerMiddleware>
}

View File

@ -1,6 +0,0 @@
import { IAuthService, IUserPermissions } from '../services'
export interface IServices {
authService: IAuthService
userPermissions: IUserPermissions
}

View File

@ -0,0 +1,20 @@
import { Config } from './Config'
import { Database } from '../database'
import { Logger } from '@rondo.dev/logger'
import { Services } from './Services'
import { RequestHandlerParams, ErrorRequestHandler } from 'express-serve-static-core'
export interface ServerMiddleware {
path: string
handle: RequestHandlerParams[]
error?: ErrorRequestHandler
}
export interface ServerConfig {
readonly config: Config
readonly database: Database
readonly logger: Logger
readonly services: Services
readonly globalErrorHandler: ErrorRequestHandler
readonly framework: Record<string, ServerMiddleware>
}

View File

@ -0,0 +1,6 @@
import { AuthService, UserPermissions } from '@rondo.dev/common'
export interface Services {
authService: AuthService
userPermissions: UserPermissions
}

View File

@ -1,54 +1,52 @@
import { IContext } from '@rondo.dev/common'
import { IRoutes } from '@rondo.dev/http-types'
import { bulkjsonrpc, jsonrpc } from '@rondo.dev/jsonrpc' import { bulkjsonrpc, jsonrpc } from '@rondo.dev/jsonrpc'
import { json } from 'body-parser' import { json } from 'body-parser'
import cookieParser from 'cookie-parser' import cookieParser from 'cookie-parser'
import { IDatabase } from '../database' import { Database } from '../database'
import { loggerFactory } from '../logger' import { loggerFactory } from '../logger'
import * as Middleware from '../middleware' import * as Middleware from '../middleware'
import { TransactionalRouter } from '../router' import { TransactionalRouter } from '../router'
import * as routes from '../routes' import * as routes from '../routes'
import * as rpc from '../rpc' import { SQLTeamService, SQLUserService, Context } from '../rpc'
import * as Services from '../services' import { SQLAuthService, SQLUserPermissions } from '../services'
import { IConfig } from './IConfig' import { Config } from './Config'
import { IServerConfig } from './IServerConfig' import { ServerConfig } from './ServerConfig'
import { IServices } from './IServices' import { Services } from './Services'
import { Routes } from '@rondo.dev/http-types'
import { configureAuthRoutes } from '../routes/configureAuthRoutes'
import { TransactionMiddleware, CSRFMiddleware, RequestLogger } from '../middleware'
export type ServerConfigurator< export type ServerConfigurator<
T extends IServerConfig = IServerConfig T extends ServerConfig = ServerConfig
> = ( > = (
config: IConfig, config: Config,
database: IDatabase, database: Database,
) => T ) => T
export const configureServer: ServerConfigurator = (config, database) => { export const configureServer: ServerConfigurator = (config, database) => {
const logger = loggerFactory.getLogger('api') const logger = loggerFactory.getLogger('api')
const services: IServices = { const services: Services = {
authService: new Services.AuthService(database), authService: new SQLAuthService(database),
userPermissions: new Services.UserPermissions(database), userPermissions: new SQLUserPermissions(database),
} }
const rpcServices = { const rpcServices = {
userService: new rpc.UserService(database), userService: new SQLUserService(database),
teamService: new rpc.TeamService(database, services.userPermissions), teamService: new SQLTeamService(database, services.userPermissions),
} }
const getContext = (req: Express.Request): IContext => ({user: req.user}) const getContext = (req: Express.Request): Context => ({user: req.user})
const rpcMiddleware = jsonrpc( const rpcMiddleware = jsonrpc(
req => getContext(req), req => getContext(req),
logger, logger,
// (details, invoke) => database
// .transactionManager
// .doInNewTransaction(() => invoke()),
) )
const authenticator = new Middleware.Authenticator(services.authService) const authenticator = new Middleware.Authenticator(services.authService)
const transactionManager = database.transactionManager const transactionManager = database.transactionManager
const createTransactionalRouter = <T extends IRoutes>() => const createTransactionalRouter = <T extends Routes>() =>
new TransactionalRouter<T>(transactionManager) new TransactionalRouter<T>(transactionManager)
const globalErrorHandler = new Middleware.ErrorPageHandler(logger).handle const globalErrorHandler = new Middleware.ErrorPageHandler(logger).handle
@ -69,14 +67,14 @@ export const configureServer: ServerConfigurator = (config, database) => {
sessionName: config.app.session.name, sessionName: config.app.session.name,
sessionSecret: config.app.session.secret, sessionSecret: config.app.session.secret,
}).handle, }).handle,
new Middleware.RequestLogger(logger).handle, new RequestLogger(logger).handle,
json(), json(),
cookieParser(config.app.session.secret), cookieParser(config.app.session.secret),
new Middleware.CSRFMiddleware({ new CSRFMiddleware({
baseUrl: config.app.baseUrl, baseUrl: config.app.baseUrl,
cookieName: config.app.session.name + '_csrf', cookieName: config.app.session.name + '_csrf',
}).handle, }).handle,
new Middleware.Transaction(database.namespace).handle, new TransactionMiddleware(database.namespace).handle,
authenticator.handle, authenticator.handle,
], ],
}, },
@ -87,11 +85,11 @@ export const configureServer: ServerConfigurator = (config, database) => {
api: { api: {
path: '/api', path: '/api',
handle: [ handle: [
new routes.AuthRoutes( configureAuthRoutes(
services.authService, services.authService,
authenticator, authenticator,
createTransactionalRouter(), createTransactionalRouter(),
).handle, ),
], ],
error: new Middleware.ErrorApiHandler(logger).handle, error: new Middleware.ErrorApiHandler(logger).handle,
}, },

View File

@ -1,8 +1,8 @@
import { IServerConfig } from './IServerConfig'
import { IApplication } from './IApplication'
import express from 'express' import express from 'express'
import { Application } from './Application'
import { ServerConfig } from './ServerConfig'
export function createServer(appConfig: IServerConfig): IApplication { export function createServer(appConfig: ServerConfig): Application {
const {config, database, framework} = appConfig const {config, database, framework} = appConfig
const server = express() const server = express()

View File

@ -1,7 +1,8 @@
export * from './CLIBootstrap'
export * from './Bootstrap' export * from './Bootstrap'
export * from './IApplication' export * from './Application'
export * from './IConfig' export * from './Config'
export * from './IServerConfig' export * from './ServerConfig'
export * from './IServices' export * from './Services'
export * from './configureServer' export * from './configureServer'
export * from './createServer' export * from './createServer'

View File

@ -1,8 +1,8 @@
import {config} from './config' import { CLIBootstrap } from './application'
import {Bootstrap} from './application/Bootstrap' import { configureServer } from './application/configureServer'
import {configureServer} from './application/configureServer' import { config } from './config'
export default new Bootstrap({ export default new CLIBootstrap({
config, config,
configureServer, configureServer,
}) })

View File

@ -1,12 +1,12 @@
import ConfigReader from '@rondo.dev/config' import ConfigReader from '@rondo.dev/config'
import {IConfig} from './application' import {Config} from './application'
import URL from 'url' import URL from 'url'
const cfg = new ConfigReader(__dirname).read() const cfg = new ConfigReader(__dirname).read()
const baseUrl = URL.parse(cfg.get('app.baseUrl')) const baseUrl = URL.parse(cfg.get('app.baseUrl'))
export const config: IConfig = { export const config: Config = {
app: { app: {
name: cfg.get('app.name'), name: cfg.get('app.name'),
assets: cfg.get('app.assets'), assets: cfg.get('app.assets'),

View File

@ -1,58 +1,15 @@
import {IDatabase} from './IDatabase' import { Namespace } from 'cls-hooked'
import {Namespace} from 'cls-hooked' import { Connection, EntityManager, EntitySchema, ObjectType, Repository } from 'typeorm'
import {TransactionManager} from './TransactionManager' import { TransactionManager } from './TransactionManager'
import {
createConnection,
Connection,
ConnectionOptions,
Logger,
EntitySchema,
ObjectType,
Repository,
} from 'typeorm'
export class Database implements IDatabase { export interface Database {
protected connection?: Connection namespace: Namespace
transactionManager: TransactionManager transactionManager: TransactionManager
connect(): Promise<Connection>
constructor( getConnection(): Connection
readonly namespace: Namespace, getEntityManager(): EntityManager
protected readonly logger: Logger,
protected readonly options: ConnectionOptions,
) {
this.transactionManager = new TransactionManager(
namespace,
this.getConnection,
)
}
async connect(): Promise<Connection> {
this.connection = await createConnection({
...this.options,
logger: this.logger,
})
return this.connection
}
getConnection = (): Connection => {
if (!this.connection) {
throw new Error('Not connected! Did you call connect?')
}
return this.connection
}
async close() {
await this.getConnection().close()
}
getEntityManager() {
return this.transactionManager.getEntityManager()
}
getRepository<Entity>( getRepository<Entity>(
target: ObjectType<Entity> | EntitySchema<Entity> | string, target: ObjectType<Entity> | EntitySchema<Entity> | string,
): Repository<Entity> { ): Repository<Entity>
return this.transactionManager.getRepository(target) close(): Promise<void>
}
} }

View File

@ -1,17 +0,0 @@
import {
Connection, EntityManager, ObjectType, EntitySchema, Repository
} from 'typeorm'
import {ITransactionManager} from './ITransactionManager'
import {Namespace} from 'cls-hooked'
export interface IDatabase {
namespace: Namespace
transactionManager: ITransactionManager
connect(): Promise<Connection>
getConnection(): Connection
getEntityManager(): EntityManager
getRepository<Entity>(
target: ObjectType<Entity> | EntitySchema<Entity> | string,
): Repository<Entity>
close(): Promise<void>
}

View File

@ -1,28 +0,0 @@
import {
EntityManager,
EntitySchema,
ObjectType,
Repository,
} from 'typeorm'
export const ENTITY_MANAGER = 'ENTITY_MANAGER'
export const TRANSACTION_ID = 'TRANSACTION_ID'
export interface ITransactionManager {
getEntityManager: () => EntityManager
getRepository: <Entity>(
target: ObjectType<Entity> | EntitySchema<Entity> | string,
) => Repository<Entity>
isInTransaction: () => boolean
/**
* Start a new or reuse an existing transaction.
*/
doInTransaction: <T>(
fn: (entityManager: EntityManager) => Promise<T>) => Promise<T>
/**
* Always start a new transaction, regardless if there is one already in
* progress in the current context.
*/
doInNewTransaction: <T>(
fn: (entityManager: EntityManager) => Promise<T>) => Promise<T>
}

View File

@ -0,0 +1,51 @@
import { Namespace } from 'cls-hooked'
import { Connection, ConnectionOptions, createConnection, EntitySchema, Logger, ObjectType, Repository } from 'typeorm'
import { Database } from './Database'
import { SQLTransactionManager } from './SQLTransactionManager'
import { TransactionManager } from './TransactionManager'
export class SQLDatabase implements Database {
protected connection?: Connection
transactionManager: TransactionManager
constructor(
readonly namespace: Namespace,
protected readonly logger: Logger,
protected readonly options: ConnectionOptions,
) {
this.transactionManager = new SQLTransactionManager(
namespace,
this.getConnection,
)
}
async connect(): Promise<Connection> {
this.connection = await createConnection({
...this.options,
logger: this.logger,
})
return this.connection
}
getConnection = (): Connection => {
if (!this.connection) {
throw new Error('Not connected! Did you call connect?')
}
return this.connection
}
async close() {
await this.getConnection().close()
}
getEntityManager() {
return this.transactionManager.getEntityManager()
}
getRepository<Entity>(
target: ObjectType<Entity> | EntitySchema<Entity> | string,
): Repository<Entity> {
return this.transactionManager.getRepository(target)
}
}

View File

@ -0,0 +1,72 @@
import loggerFactory from '@rondo.dev/logger'
import { Namespace } from 'cls-hooked'
import shortid from 'shortid'
import { Connection, EntityManager, EntitySchema, ObjectType, Repository } from 'typeorm'
import { ENTITY_MANAGER, TransactionManager, TRANSACTION_ID } from './TransactionManager'
const log = loggerFactory.getLogger('db')
export type GetConnection = () => Connection
export class SQLTransactionManager implements TransactionManager {
constructor(
readonly ns: Namespace,
readonly getConnection: GetConnection,
) {}
getEntityManager = (): EntityManager => {
const entityManager = this.ns.get(ENTITY_MANAGER) as EntityManager
if (entityManager) {
return entityManager
}
return this.getConnection().manager
}
getRepository = <Entity>(
target: ObjectType<Entity> | EntitySchema<Entity> | string,
): Repository<Entity> => {
return this.getEntityManager().getRepository(target)
}
isInTransaction = (): boolean => {
return !!this.ns.get(ENTITY_MANAGER)
}
async doInTransaction<T>(fn: (em: EntityManager) => Promise<T>) {
const alreadyInTransaction = this.isInTransaction()
if (alreadyInTransaction) {
log.info('doInTransaction: reusing existing transaction')
return await fn(this.getEntityManager())
}
log.info('doInTransaction: starting new transaction')
return this.doInNewTransaction(fn)
}
async doInNewTransaction<T>(fn: (em: EntityManager) => Promise<T>) {
return this.ns.runAndReturn(async () => {
this.setTransactionId(shortid())
try {
return await this.getConnection().manager
.transaction(async entityManager => {
this.setEntityManager(entityManager)
try {
return await fn(entityManager)
} finally {
this.setEntityManager(undefined)
}
})
} finally {
this.setTransactionId(undefined)
}
})
}
protected setTransactionId(transactionId: string | undefined) {
this.ns.set(TRANSACTION_ID, transactionId)
}
protected setEntityManager(entityManager: EntityManager | undefined) {
this.ns.set(ENTITY_MANAGER, entityManager)
}
}

View File

@ -1,72 +1,28 @@
import loggerFactory from '@rondo.dev/logger' import {
import { Namespace } from 'cls-hooked' EntityManager,
import shortid from 'shortid' EntitySchema,
import { Connection, EntityManager, EntitySchema, ObjectType, Repository } from 'typeorm' ObjectType,
import { ENTITY_MANAGER, ITransactionManager, TRANSACTION_ID } from './ITransactionManager' Repository,
} from 'typeorm'
const log = loggerFactory.getLogger('db') export const ENTITY_MANAGER = 'ENTITY_MANAGER'
export const TRANSACTION_ID = 'TRANSACTION_ID'
export type TConnectionGetter = () => Connection export interface TransactionManager {
getEntityManager: () => EntityManager
export class TransactionManager implements ITransactionManager { getRepository: <Entity>(
constructor(
readonly ns: Namespace,
readonly getConnection: TConnectionGetter,
) {}
getEntityManager = (): EntityManager => {
const entityManager = this.ns.get(ENTITY_MANAGER) as EntityManager
if (entityManager) {
return entityManager
}
return this.getConnection().manager
}
getRepository = <Entity>(
target: ObjectType<Entity> | EntitySchema<Entity> | string, target: ObjectType<Entity> | EntitySchema<Entity> | string,
): Repository<Entity> => { ) => Repository<Entity>
return this.getEntityManager().getRepository(target) isInTransaction: () => boolean
} /**
* Start a new or reuse an existing transaction.
isInTransaction = (): boolean => { */
return !!this.ns.get(ENTITY_MANAGER) doInTransaction: <T>(
} fn: (entityManager: EntityManager) => Promise<T>) => Promise<T>
/**
async doInTransaction<T>(fn: (em: EntityManager) => Promise<T>) { * Always start a new transaction, regardless if there is one already in
const alreadyInTransaction = this.isInTransaction() * progress in the current context.
if (alreadyInTransaction) { */
log.info('doInTransaction: reusing existing transaction') doInNewTransaction: <T>(
return await fn(this.getEntityManager()) fn: (entityManager: EntityManager) => Promise<T>) => Promise<T>
}
log.info('doInTransaction: starting new transaction')
return this.doInNewTransaction(fn)
}
async doInNewTransaction<T>(fn: (em: EntityManager) => Promise<T>) {
return this.ns.runAndReturn(async () => {
this.setTransactionId(shortid())
try {
return await this.getConnection().manager
.transaction(async entityManager => {
this.setEntityManager(entityManager)
try {
return await fn(entityManager)
} finally {
this.setEntityManager(undefined)
}
})
} finally {
this.setTransactionId(undefined)
}
})
}
protected setTransactionId(transactionId: string | undefined) {
this.ns.set(TRANSACTION_ID, transactionId)
}
protected setEntityManager(entityManager: EntityManager | undefined) {
this.ns.set(ENTITY_MANAGER, entityManager)
}
} }

View File

@ -1,15 +1,16 @@
import {Database, ENTITY_MANAGER} from './' import { createNamespace } from 'cls-hooked'
import express, { NextFunction, Request, Response } from 'express'
import request from 'supertest' import request from 'supertest'
import express, {Request, Response, NextFunction} from 'express' import { config } from '../config'
import {createNamespace} from 'cls-hooked' import { loggerFactory, SQLLogger } from '../logger'
import {CORRELATION_ID, Transaction} from '../middleware' import { CORRELATION_ID, TransactionMiddleware } from '../middleware'
import {config} from '../config' import { ENTITY_MANAGER } from './'
import {SqlLogger, loggerFactory} from '../logger' import { SQLDatabase } from './SQLDatabase'
const ns = createNamespace('clsify-test') const ns = createNamespace('clsify-test')
const database = new Database( const database = new SQLDatabase(
ns, ns,
new SqlLogger(loggerFactory.getLogger('sql'), ns), new SQLLogger(loggerFactory.getLogger('sql'), ns),
config.app.db, config.app.db,
) )
@ -25,7 +26,7 @@ describe('middleware/Transaction', () => {
setImmediate(next) setImmediate(next)
} }
app.use(new Transaction(ns).handle) app.use(new TransactionMiddleware(ns).handle)
app.use('/:id', handler) app.use('/:id', handler)
app.use('/:id', handler) app.use('/:id', handler)
app.use('/:id', handler) app.use('/:id', handler)
@ -61,7 +62,7 @@ describe('doInTransaction', () => {
let entityManager: any let entityManager: any
const app = express() const app = express()
app.use(new Transaction(ns).handle) app.use(new TransactionMiddleware(ns).handle)
app.use('/', (req, res, next) => { app.use('/', (req, res, next) => {
if (entityManager) { if (entityManager) {
ns.set(ENTITY_MANAGER, entityManager) ns.set(ENTITY_MANAGER, entityManager)

View File

@ -1,4 +1,4 @@
export * from './ITransactionManager'
export * from './TransactionManager' export * from './TransactionManager'
export * from './IDatabase' export * from './SQLTransactionManager'
export * from './Database' export * from './Database'
export * from './SQLDatabase'

View File

@ -1,8 +1,4 @@
import { import { CreateDateColumn, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm'
PrimaryGeneratedColumn,
CreateDateColumn,
UpdateDateColumn,
} from 'typeorm'
const transformer = { const transformer = {
from: (value: Date) => !isNaN(value.getTime()) ? value.toISOString() : value, from: (value: Date) => !isNaN(value.getTime()) ? value.toISOString() : value,

View File

@ -1,5 +1,5 @@
import {BaseEntity} from './BaseEntity' import { Column, Entity } from 'typeorm'
import {Column, Entity} from 'typeorm' import { BaseEntity } from './BaseEntity'
@Entity() @Entity()
export class Role extends BaseEntity { export class Role extends BaseEntity {

View File

@ -1,9 +1,9 @@
import {ISession} from '../session/ISession' import { Column, Entity, Index, ManyToOne, PrimaryColumn } from 'typeorm'
import {Column, Entity, PrimaryColumn, Index, ManyToOne} from 'typeorm' import { DefaultSession } from '../session/DefaultSession'
import {User} from './User' import { User } from './User'
@Entity() @Entity()
export class Session implements ISession { export class Session implements DefaultSession {
@PrimaryColumn() @PrimaryColumn()
id!: string id!: string

View File

@ -1,7 +1,7 @@
import {BaseEntity} from './BaseEntity' import { Column, Entity, Index, ManyToOne, OneToMany } from 'typeorm'
import {Column, Entity, OneToMany, ManyToOne, Index} from 'typeorm' import { BaseEntity } from './BaseEntity'
import {UserTeam} from './UserTeam' import { User } from './User'
import {User} from './User' import { UserTeam } from './UserTeam'
@Entity() @Entity()
export class Team extends BaseEntity { export class Team extends BaseEntity {

View File

@ -1,8 +1,8 @@
import {BaseEntity} from './BaseEntity' import { Column, Entity, OneToMany } from 'typeorm'
import {Column, Entity, OneToMany} from 'typeorm' import { BaseEntity } from './BaseEntity'
import {Session} from './Session' import { Session } from './Session'
import {UserTeam} from './UserTeam' import { UserEmail } from './UserEmail'
import {UserEmail} from './UserEmail' import { UserTeam } from './UserTeam'
@Entity() @Entity()
export class User extends BaseEntity { export class User extends BaseEntity {

View File

@ -1,6 +1,6 @@
import {BaseEntity} from './BaseEntity' import { Column, Entity, ManyToOne } from 'typeorm'
import {Column, Entity, PrimaryGeneratedColumn, ManyToOne} from 'typeorm' import { BaseEntity } from './BaseEntity'
import {User} from './User' import { User } from './User'
@Entity() @Entity()
export class UserEmail extends BaseEntity { export class UserEmail extends BaseEntity {

View File

@ -1,8 +1,8 @@
import {BaseEntity} from './BaseEntity' import { Column, Entity, ManyToOne } from 'typeorm'
import {Column, Entity, ManyToOne} from 'typeorm' import { BaseEntity } from './BaseEntity'
import {Role} from './Role' import { Role } from './Role'
import {Team} from './Team' import { Team } from './Team'
import {User} from './User' import { User } from './User'
@Entity() @Entity()
export class UserTeam extends BaseEntity { export class UserTeam extends BaseEntity {

View File

@ -1,4 +1,4 @@
import {TransformedError} from './TransformedError' import { TransformedError } from './TransformedError'
export class ErrorTransformer { export class ErrorTransformer {
constructor( constructor(

View File

@ -1,4 +1,4 @@
import {valueOrError} from './' import { valueOrError } from './'
describe('valueOrError', () => { describe('valueOrError', () => {

View File

@ -1,12 +1,12 @@
import {ILogger} from '@rondo.dev/logger' import { Logger } from '@rondo.dev/logger'
import {Logger, QueryRunner} from 'typeorm' import { Namespace } from 'cls-hooked'
import {Namespace} from 'cls-hooked' import { Logger as TypeORMLogger, QueryRunner } from 'typeorm'
import {CORRELATION_ID} from '../middleware/Transaction'
import { TRANSACTION_ID } from '../database' import { TRANSACTION_ID } from '../database'
import { CORRELATION_ID } from '../middleware/TransactionMiddleware'
export class SqlLogger implements Logger { export class SQLLogger implements TypeORMLogger {
constructor( constructor(
protected readonly logger: ILogger, protected readonly logger: Logger,
protected readonly ns: Namespace, protected readonly ns: Namespace,
) {} ) {}

View File

@ -1,6 +1,6 @@
export * from './SQLLogger' export * from './SQLLogger'
import loggerFactory from '@rondo.dev/logger' import loggerFactory from '@rondo.dev/logger'
export {loggerFactory} export { loggerFactory }
export const getLogger = loggerFactory.getLogger export const getLogger = loggerFactory.getLogger
export const apiLogger = getLogger('api') export const apiLogger = getLogger('api')

View File

@ -1,10 +1,9 @@
import express, {Application} from 'express' import { AuthService, Credentials } from '@rondo.dev/common'
import { urlencoded } from 'body-parser'
import express, { Application } from 'express'
import request from 'supertest' import request from 'supertest'
import {Authenticator} from './Authenticator' import { Authenticator } from './Authenticator'
import {ICredentials} from '@rondo.dev/common' import { handlePromise } from './handlePromise'
import {IAuthService} from '../services'
import {handlePromise} from './handlePromise'
import {urlencoded} from 'body-parser'
describe('Authenticator', () => { describe('Authenticator', () => {
@ -18,7 +17,7 @@ describe('Authenticator', () => {
firstName: 'test', firstName: 'test',
lastName: 'test', lastName: 'test',
} }
const authService = new (class implements IAuthService { const authService = new (class implements AuthService {
async createUser() { async createUser() {
return {id: 1, ...userInfo} return {id: 1, ...userInfo}
} }
@ -26,7 +25,7 @@ describe('Authenticator', () => {
async findOne(id: number) { async findOne(id: number) {
return {id, ...userInfo} return {id, ...userInfo}
} }
async validateCredentials({username, password}: ICredentials) { async validateCredentials({username, password}: Credentials) {
if (username === 'test' && password === 'pass') { if (username === 'test' && password === 'pass') {
return {id: 1, ...userInfo} return {id: 1, ...userInfo}
return return

View File

@ -1,15 +1,15 @@
import {Authenticator as A, Passport} from 'passport' import { Authenticator as A, Passport } from 'passport'
import {IAuthService} from '../services' import { Strategy as LocalStrategy } from 'passport-local'
import {Strategy as LocalStrategy} from 'passport-local' import { Middleware } from './Middleware'
import {THandler} from './THandler' import { Handler } from './Handler'
import {IMiddleware} from './IMiddleware' import { AuthService } from '@rondo.dev/common'
export class Authenticator implements IMiddleware { export class Authenticator implements Middleware {
protected readonly passport: A protected readonly passport: A
readonly handle: THandler[] readonly handle: Handler[]
constructor(protected readonly authService: IAuthService) { constructor(protected readonly authService: AuthService) {
this.passport = new Passport() as any this.passport = new Passport() as any
this.configurePassport() this.configurePassport()
@ -22,7 +22,7 @@ export class Authenticator implements IMiddleware {
] ]
} }
withLogInPromise: THandler = (req, res, next) => { withLogInPromise: Handler = (req, res, next) => {
req.logInPromise = (user) => { req.logInPromise = (user) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
req.logIn(user, err => { req.logIn(user, err => {
@ -72,7 +72,7 @@ export class Authenticator implements IMiddleware {
.catch(done) .catch(done)
} }
authenticate(strategy: string | string[]): THandler { authenticate(strategy: string | string[]): Handler {
return (req, res, next) => { return (req, res, next) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.passport.authenticate(strategy, (err: Error, user, info) => { this.passport.authenticate(strategy, (err: Error, user, info) => {

View File

@ -1,17 +1,17 @@
import Csurf from 'csurf' import Csurf from 'csurf'
import {THandler} from './THandler' import { UrlWithStringQuery } from 'url'
import {IMiddleware} from './IMiddleware' import { Middleware } from './Middleware'
import {UrlWithStringQuery} from 'url' import { Handler } from './Handler'
export interface ICSRFParams { export interface CSRFMiddlewareParams {
baseUrl: UrlWithStringQuery baseUrl: UrlWithStringQuery
cookieName: string cookieName: string
} }
export class CSRFMiddleware implements IMiddleware { export class CSRFMiddleware implements Middleware {
readonly handle: THandler readonly handle: Handler
constructor(readonly params: ICSRFParams) { constructor(readonly params: CSRFMiddlewareParams) {
this.handle = Csurf({ this.handle = Csurf({
cookie: { cookie: {
signed: true, signed: true,

View File

@ -1,12 +1,12 @@
import {TErrorHandler} from './TErrorHandler' import { Logger } from '@rondo.dev/logger'
import {ILogger} from '@rondo.dev/logger' import { ValidationError } from '@rondo.dev/validator'
import {IMiddleware} from './IMiddleware' import { ErrorHandler } from './ErrorHandler'
import {ValidationError} from '@rondo.dev/validator' import { Middleware } from './Middleware'
export class ErrorApiHandler implements IMiddleware { export class ErrorApiHandler implements Middleware {
constructor(readonly logger: ILogger) {} constructor(readonly logger: Logger) {}
handle: TErrorHandler = (err, req, res, next) => { handle: ErrorHandler = (err, req, res, next) => {
this.logger.error('%s An API error occurred: %s', this.logger.error('%s An API error occurred: %s',
req.correlationId, err.stack) req.correlationId, err.stack)
const statusCode = this.getStatus(err) const statusCode = this.getStatus(err)

View File

@ -1,4 +1,4 @@
import {Request, Response, NextFunction} from 'express' import {Request, Response, NextFunction} from 'express'
export type TErrorHandler = export type ErrorHandler =
(err: Error, req: Request, res: Response, next: NextFunction) => any (err: Error, req: Request, res: Response, next: NextFunction) => any

View File

@ -1,11 +1,11 @@
import {ILogger} from '@rondo.dev/logger' import { Logger } from '@rondo.dev/logger'
import {IMiddleware} from './IMiddleware' import { ErrorHandler } from './ErrorHandler'
import {TErrorHandler} from './TErrorHandler' import { Middleware } from './Middleware'
export class ErrorPageHandler implements IMiddleware { export class ErrorPageHandler implements Middleware {
constructor(readonly logger: ILogger) {} constructor(readonly logger: Logger) {}
handle: TErrorHandler = (err, req, res, next) => { handle: ErrorHandler = (err, req, res, next) => {
this.logger.error( this.logger.error(
'%s An error occurred: %s', '%s An error occurred: %s',
req.correlationId, err.stack) req.correlationId, err.stack)

View File

@ -0,0 +1,3 @@
import { NextFunction, Request, Response } from 'express'
export type Handler = (req: Request, res: Response, next: NextFunction) => any

View File

@ -1,6 +0,0 @@
import {THandler} from './THandler'
import {TErrorHandler} from './TErrorHandler'
export interface IMiddleware {
handle: THandler | THandler[] | TErrorHandler
}

View File

@ -0,0 +1,6 @@
import {Handler} from './Handler'
import {ErrorHandler} from './ErrorHandler'
export interface Middleware {
handle: Handler | Handler[] | ErrorHandler
}

View File

@ -0,0 +1,4 @@
import { NextFunction, Request, Response } from 'express'
export type PromiseHandler<T> =
(req: Request, res: Response, next: NextFunction) => Promise<T>

View File

@ -1,12 +1,12 @@
import {THandler} from './THandler' import { Logger } from '@rondo.dev/logger'
import {ILogger} from '@rondo.dev/logger'
import {IMiddleware} from './IMiddleware'
import shortid from 'shortid' import shortid from 'shortid'
import { Handler } from './Handler'
import { Middleware } from './Middleware'
export class RequestLogger implements IMiddleware { export class RequestLogger implements Middleware {
constructor(protected readonly logger: ILogger) {} constructor(protected readonly logger: Logger) {}
handle: THandler = (req, res, next) => { handle: Handler = (req, res, next) => {
const start = Date.now() const start = Date.now()
req.correlationId = shortid.generate() req.correlationId = shortid.generate()
res.on('finish', () => { res.on('finish', () => {

View File

@ -1,24 +1,24 @@
import ExpressSession from 'express-session' import ExpressSession from 'express-session'
import {THandler} from './THandler' import { UrlWithStringQuery } from 'url'
import {IMiddleware} from './IMiddleware' import { TransactionManager } from '../database'
import {ISession} from '../session/ISession' import { Session as SessionEntity } from '../entities/Session'
import {ITransactionManager} from '../database/ITransactionManager' import { apiLogger } from '../logger'
import {Session as SessionEntity} from '../entities/Session' import { SessionStore } from '../session/SessionStore'
import {SessionStore} from '../session/SessionStore' import { Handler } from './Handler'
import {UrlWithStringQuery} from 'url' import { Middleware } from './Middleware'
import {apiLogger} from '../logger' import { DefaultSession } from '../session'
export interface ISessionOptions { export interface SessionMiddlewareParams {
transactionManager: ITransactionManager, transactionManager: TransactionManager
baseUrl: UrlWithStringQuery, baseUrl: UrlWithStringQuery
sessionName: string, sessionName: string
sessionSecret: string | string[], sessionSecret: string | string[]
} }
export class SessionMiddleware implements IMiddleware { export class SessionMiddleware implements Middleware {
readonly handle: THandler readonly handle: Handler
constructor(readonly params: ISessionOptions) { constructor(readonly params: SessionMiddlewareParams) {
this.handle = ExpressSession({ this.handle = ExpressSession({
saveUninitialized: false, saveUninitialized: false,
secret: params.sessionSecret, secret: params.sessionSecret,
@ -42,8 +42,10 @@ export class SessionMiddleware implements IMiddleware {
}) })
} }
protected buildSession = (sessionData: Express.SessionData, sess: ISession) protected buildSession = (
: SessionEntity => { sessionData: Express.SessionData,
sess: DefaultSession,
): SessionEntity => {
return {...sess, userId: sessionData.userId } return {...sess, userId: sessionData.userId }
} }

View File

@ -1,3 +0,0 @@
import {Request, Response, NextFunction} from 'express'
export type THandler = (req: Request, res: Response, next: NextFunction) => any

View File

@ -1,4 +0,0 @@
import {Request, Response, NextFunction} from 'express'
export type TPromiseHandler<T> =
(req: Request, res: Response, next: NextFunction) => Promise<T>

View File

@ -1,14 +1,14 @@
import shortid from 'shortid' import shortid from 'shortid'
import {IMiddleware} from './IMiddleware' import {Middleware} from './Middleware'
import {THandler} from './THandler' import {Handler} from './Handler'
import {Namespace} from 'cls-hooked' import {Namespace} from 'cls-hooked'
export const CORRELATION_ID = 'CORRELATION_ID' export const CORRELATION_ID = 'CORRELATION_ID'
export class Transaction implements IMiddleware { export class TransactionMiddleware implements Middleware {
constructor(readonly ns: Namespace) {} constructor(readonly ns: Namespace) {}
handle: THandler = (req, res, next) => { handle: Handler = (req, res, next) => {
const {ns} = this const {ns} = this
ns.bindEmitter(req) ns.bindEmitter(req)
ns.bindEmitter(res) ns.bindEmitter(res)

View File

@ -1,8 +1,6 @@
import {ensureLoggedInApi, ensureLoggedInSite} from './ensureLoggedIn' import express, { Application, NextFunction as Next, Request, Response } from 'express'
import express, {
Application, Request, Response, NextFunction as Next,
} from 'express'
import request from 'supertest' import request from 'supertest'
import { ensureLoggedInApi, ensureLoggedInSite } from './ensureLoggedIn'
function createMockUserMiddleware() { function createMockUserMiddleware() {
const context: {user?: any} = {} const context: {user?: any} = {}

View File

@ -1,10 +1,10 @@
import { Request } from 'express'
import createError from 'http-errors' import createError from 'http-errors'
import {Request} from 'express' import { Handler } from './Handler'
import {THandler} from './THandler'
const isLoggedIn = (req: Request) => !!(req as any).user const isLoggedIn = (req: Request) => !!(req as any).user
export const ensureLoggedInApi: THandler = (req, res, next) => { export const ensureLoggedInApi: Handler = (req, res, next) => {
if (!isLoggedIn(req)) { if (!isLoggedIn(req)) {
next(createError(401)) next(createError(401))
return return
@ -12,7 +12,7 @@ export const ensureLoggedInApi: THandler = (req, res, next) => {
next() next()
} }
export const ensureLoggedInSite = (redirectTo: string): THandler => { export const ensureLoggedInSite = (redirectTo: string): Handler => {
return function _ensureLoggedInSite(req, res, next) { return function _ensureLoggedInSite(req, res, next) {
if (!isLoggedIn(req)) { if (!isLoggedIn(req)) {
res.redirect(redirectTo) res.redirect(redirectTo)

View File

@ -1,7 +1,7 @@
import {THandler} from './THandler' import { Handler } from './Handler'
import {TPromiseHandler} from './TPromiseHandler' import { PromiseHandler } from './PromiseHandler'
export function handlePromise<T>(endpoint: TPromiseHandler<T>): THandler { export function handlePromise<T>(endpoint: PromiseHandler<T>): Handler {
return (req, res, next) => { return (req, res, next) => {
const promise = endpoint(req, res, next) const promise = endpoint(req, res, next)
promise promise

View File

@ -2,12 +2,12 @@ export * from './Authenticator'
export * from './CSRFMiddleware' export * from './CSRFMiddleware'
export * from './ErrorApiHandler' export * from './ErrorApiHandler'
export * from './ErrorPageHandler' export * from './ErrorPageHandler'
export * from './IMiddleware' export * from './Middleware'
export * from './RequestLogger' export * from './RequestLogger'
export * from './SessionMiddleware' export * from './SessionMiddleware'
export * from './TErrorHandler' export * from './ErrorHandler'
export * from './THandler' export * from './Handler'
export * from './TPromiseHandler' export * from './PromiseHandler'
export * from './Transaction' export * from './TransactionMiddleware'
export * from './ensureLoggedIn' export * from './ensureLoggedIn'
export * from './handlePromise' export * from './handlePromise'

View File

@ -4,32 +4,32 @@ import {AsyncRouter} from './AsyncRouter'
describe('AsyncRouter', () => { describe('AsyncRouter', () => {
interface IResponse { interface Response {
value: string value: string
} }
interface IParam { interface Param {
param: string param: string
} }
interface IHandler { interface Handler {
params: IParam, params: Param
response: IResponse, response: Response
} }
interface IMyApi { interface MyApi {
'/test/:param': { '/test/:param': {
get: IHandler get: Handler
post: IHandler post: Handler
put: IHandler put: Handler
delete: IHandler delete: Handler
options: IHandler options: Handler
patch: IHandler patch: Handler
head: {}, head: {}
} }
'/middleware': { '/middleware': {
get: { get: {
response: IResponse response: Response
} }
} }
} }
@ -37,7 +37,7 @@ describe('AsyncRouter', () => {
const app = express() const app = express()
const router = express.Router() const router = express.Router()
app.use(router) app.use(router)
const asyncRouter = new AsyncRouter<IMyApi>(router) const asyncRouter = new AsyncRouter<MyApi>(router)
asyncRouter.get('/test/:param', async req => { asyncRouter.get('/test/:param', async req => {
return {value: req.params.param} return {value: req.params.param}
@ -72,7 +72,7 @@ describe('AsyncRouter', () => {
}) })
it('creates its own router when not provided', () => { it('creates its own router when not provided', () => {
const r = new AsyncRouter<IMyApi>() const r = new AsyncRouter<MyApi>()
expect(r.router).toBeTruthy() expect(r.router).toBeTruthy()
}) })

View File

@ -1,8 +1,8 @@
import { Method, Routes } from '@rondo.dev/http-types'
import express from 'express' import express from 'express'
import {IRoutes, TMethod} from '@rondo.dev/http-types' import { TypedHandler, TypedMiddleware } from './TypedHandler'
import {TTypedHandler, TTypedMiddleware} from './TTypedHandler'
export class AsyncRouter<R extends IRoutes> { export class AsyncRouter<R extends Routes> {
readonly router: express.Router readonly router: express.Router
readonly use: express.IRouterHandler<void> & express.IRouterMatcher<void> readonly use: express.IRouterHandler<void> & express.IRouterMatcher<void>
@ -11,12 +11,12 @@ export class AsyncRouter<R extends IRoutes> {
this.use = this.router.use.bind(this.router) as any this.use = this.router.use.bind(this.router) as any
} }
protected addRoute<M extends TMethod, P extends keyof R & string>( protected addRoute<M extends Method, P extends keyof R & string>(
method: M, method: M,
path: P, path: P,
...handlers: [TTypedHandler<R, P, M>] | [ ...handlers: [TypedHandler<R, P, M>] | [
Array<TTypedMiddleware<R, P, M>>, Array<TypedMiddleware<R, P, M>>,
TTypedHandler<R, P, M>, TypedHandler<R, P, M>,
] ]
) { ) {
const addRoute = this.router[method].bind(this.router as any) const addRoute = this.router[method].bind(this.router as any)
@ -31,8 +31,8 @@ export class AsyncRouter<R extends IRoutes> {
} }
protected wrapHandler<M extends TMethod, P extends keyof R & string>( protected wrapHandler<M extends Method, P extends keyof R & string>(
handler: TTypedHandler<R, P, M>, handler: TypedHandler<R, P, M>,
): express.RequestHandler { ): express.RequestHandler {
return (req, res, next) => { return (req, res, next) => {
handler(req, res, next) handler(req, res, next)
@ -45,9 +45,9 @@ export class AsyncRouter<R extends IRoutes> {
get<P extends keyof R & string>( get<P extends keyof R & string>(
path: P, path: P,
...handlers: [TTypedHandler<R, P, 'get'>] | [ ...handlers: [TypedHandler<R, P, 'get'>] | [
Array<TTypedMiddleware<R, P, 'get'>>, Array<TypedMiddleware<R, P, 'get'>>,
TTypedHandler<R, P, 'get'>, TypedHandler<R, P, 'get'>,
] ]
): void { ): void {
this.addRoute('get', path, ...handlers) this.addRoute('get', path, ...handlers)
@ -55,9 +55,9 @@ export class AsyncRouter<R extends IRoutes> {
post<P extends keyof R & string>( post<P extends keyof R & string>(
path: P, path: P,
...handlers: [TTypedHandler<R, P, 'post'>] | [ ...handlers: [TypedHandler<R, P, 'post'>] | [
Array<TTypedMiddleware<R, P, 'post'>>, Array<TypedMiddleware<R, P, 'post'>>,
TTypedHandler<R, P, 'post'>, TypedHandler<R, P, 'post'>,
] ]
) { ) {
this.addRoute('post', path, ...handlers) this.addRoute('post', path, ...handlers)
@ -65,9 +65,9 @@ export class AsyncRouter<R extends IRoutes> {
put<P extends keyof R & string>( put<P extends keyof R & string>(
path: P, path: P,
...handlers: [TTypedHandler<R, P, 'put'>] | [ ...handlers: [TypedHandler<R, P, 'put'>] | [
Array<TTypedMiddleware<R, P, 'put'>>, Array<TypedMiddleware<R, P, 'put'>>,
TTypedHandler<R, P, 'put'>, TypedHandler<R, P, 'put'>,
] ]
) { ) {
this.addRoute('put', path, ...handlers) this.addRoute('put', path, ...handlers)
@ -75,9 +75,9 @@ export class AsyncRouter<R extends IRoutes> {
delete<P extends keyof R & string>( delete<P extends keyof R & string>(
path: P, path: P,
...handlers: [TTypedHandler<R, P, 'delete'>] | [ ...handlers: [TypedHandler<R, P, 'delete'>] | [
Array<TTypedMiddleware<R, P, 'delete'>>, Array<TypedMiddleware<R, P, 'delete'>>,
TTypedHandler<R, P, 'delete'>, TypedHandler<R, P, 'delete'>,
] ]
) { ) {
this.addRoute('delete', path, ...handlers) this.addRoute('delete', path, ...handlers)
@ -85,9 +85,9 @@ export class AsyncRouter<R extends IRoutes> {
head<P extends keyof R & string>( head<P extends keyof R & string>(
path: P, path: P,
...handlers: [TTypedHandler<R, P, 'head'>] | [ ...handlers: [TypedHandler<R, P, 'head'>] | [
Array<TTypedMiddleware<R, P, 'head'>>, Array<TypedMiddleware<R, P, 'head'>>,
TTypedHandler<R, P, 'head'>, TypedHandler<R, P, 'head'>,
] ]
) { ) {
this.addRoute('head', path, ...handlers) this.addRoute('head', path, ...handlers)
@ -95,9 +95,9 @@ export class AsyncRouter<R extends IRoutes> {
options<P extends keyof R & string>( options<P extends keyof R & string>(
path: P, path: P,
...handlers: [TTypedHandler<R, P, 'options'>] | [ ...handlers: [TypedHandler<R, P, 'options'>] | [
Array<TTypedMiddleware<R, P, 'options'>>, Array<TypedMiddleware<R, P, 'options'>>,
TTypedHandler<R, P, 'options'>, TypedHandler<R, P, 'options'>,
] ]
) { ) {
this.addRoute('options', path, ...handlers) this.addRoute('options', path, ...handlers)
@ -105,9 +105,9 @@ export class AsyncRouter<R extends IRoutes> {
patch<P extends keyof R & string>( patch<P extends keyof R & string>(
path: P, path: P,
...handlers: [TTypedHandler<R, P, 'patch'>] | [ ...handlers: [TypedHandler<R, P, 'patch'>] | [
Array<TTypedMiddleware<R, P, 'patch'>>, Array<TypedMiddleware<R, P, 'patch'>>,
TTypedHandler<R, P, 'patch'>, TypedHandler<R, P, 'patch'>,
] ]
) { ) {
this.addRoute('patch', path, ...handlers) this.addRoute('patch', path, ...handlers)

View File

@ -1,8 +0,0 @@
import express from 'express'
import {IRoute} from '@rondo.dev/http-types'
export interface ITypedRequest<T extends IRoute> extends express.Request {
body: T['body']
params: T['params']
query: T['query']
}

View File

@ -1,23 +0,0 @@
import express from 'express'
import {IRoutes, TMethod} from '@rondo.dev/http-types'
import {ITypedRequest} from './ITypedRequest'
export type TTypedMiddleware<
R extends IRoutes,
P extends keyof R,
M extends TMethod
> = (
req: ITypedRequest<R[P][M]>,
res: express.Response,
next: express.NextFunction,
) => void
export type TTypedHandler<
R extends IRoutes,
P extends keyof R,
M extends TMethod
> = (
req: ITypedRequest<R[P][M]>,
res: express.Response,
next: express.NextFunction,
) => Promise<R[P][M]['response']>

View File

@ -1,16 +1,16 @@
import { Method, Routes } from '@rondo.dev/http-types'
import express from 'express' import express from 'express'
import {AsyncRouter} from './AsyncRouter' import { TransactionManager } from '../database/TransactionManager'
import {IRoutes, TMethod} from '@rondo.dev/http-types' import { AsyncRouter } from './AsyncRouter'
import {ITransactionManager} from '../database/ITransactionManager' import { TypedHandler } from './TypedHandler'
import {TTypedHandler} from './TTypedHandler'
export class TransactionalRouter<R extends IRoutes> extends AsyncRouter<R> { export class TransactionalRouter<R extends Routes> extends AsyncRouter<R> {
constructor(readonly transactionManager: ITransactionManager) { constructor(readonly transactionManager: TransactionManager) {
super() super()
} }
protected wrapHandler<M extends TMethod, P extends keyof R & string>( protected wrapHandler<M extends Method, P extends keyof R & string>(
handler: TTypedHandler<R, P, M>, handler: TypedHandler<R, P, M>,
): express.RequestHandler { ): express.RequestHandler {
return async (req, res, next) => { return async (req, res, next) => {
await this.transactionManager await this.transactionManager

View File

@ -0,0 +1,23 @@
import { Method, Routes } from '@rondo.dev/http-types'
import express from 'express'
import { TypedRequest } from './TypedRequest'
export type TypedMiddleware<
R extends Routes,
P extends keyof R,
M extends Method
> = (
req: TypedRequest<R[P][M]>,
res: express.Response,
next: express.NextFunction,
) => void
export type TypedHandler<
R extends Routes,
P extends keyof R,
M extends Method
> = (
req: TypedRequest<R[P][M]>,
res: express.Response,
next: express.NextFunction,
) => Promise<R[P][M]['response']>

View File

@ -0,0 +1,8 @@
import { Route } from '@rondo.dev/http-types'
import express from 'express'
export interface TypedRequest<T extends Route> extends express.Request {
body: T['body']
params: T['params']
query: T['query']
}

View File

@ -1,4 +1,4 @@
export * from './AsyncRouter' export * from './AsyncRouter'
export * from './TTypedHandler'
export * from './ITypedRequest'
export * from './TransactionalRouter' export * from './TransactionalRouter'
export * from './TypedHandler'
export * from './TypedRequest'

View File

@ -1,53 +0,0 @@
import {AsyncRouter} from '../router'
import {BaseRoute} from './BaseRoute'
import {IAPIDef} from '@rondo.dev/common'
import {IAuthService} from '../services'
import {Authenticator} from '../middleware'
import {ensureLoggedInApi} from '../middleware'
export class AuthRoutes extends BaseRoute<IAPIDef> {
constructor(
protected readonly authService: IAuthService,
protected readonly authenticator: Authenticator,
protected readonly t: AsyncRouter<IAPIDef>,
) {
super(t)
}
setup(t: AsyncRouter<IAPIDef>) {
t.post('/auth/register', async (req, res) => {
const user = await this.authService.createUser({
username: req.body.username,
password: req.body.password,
firstName: req.body.firstName,
lastName: req.body.lastName,
})
await req.logInPromise(user)
return user
})
t.post('/auth/login', async (req, res, next) => {
const user = await this.authenticator
.authenticate('local')(req, res, next)
if (!user) {
res.status(401)
return
}
await req.logInPromise(user)
return user
})
t.post('/auth/password', [ensureLoggedInApi], async req => {
await this.authService.changePassword({
userId: req.user!.id,
oldPassword: req.body.oldPassword,
newPassword: req.body.newPassword,
})
})
t.get('/auth/logout', async (req, res) => {
req.logout()
})
}
}

View File

@ -1,14 +0,0 @@
import {THandler} from '../middleware/THandler'
import {AsyncRouter} from '../router'
import {IRoutes} from '@rondo.dev/http-types'
export abstract class BaseRoute<T extends IRoutes> {
readonly handle: THandler
constructor(protected readonly t: AsyncRouter<T>) {
this.handle = t.router
this.setup(t)
}
protected abstract setup(t: AsyncRouter<T>): void
}

View File

@ -0,0 +1,46 @@
import { APIDef, AuthService } from '@rondo.dev/common'
import { Authenticator, ensureLoggedInApi } from '../middleware'
import { AsyncRouter } from '../router'
export function configureAuthRoutes(
authService: AuthService,
authenticator: Authenticator,
t: AsyncRouter<APIDef>,
) {
t.post('/auth/register', async (req, res) => {
const user = await authService.createUser({
username: req.body.username,
password: req.body.password,
firstName: req.body.firstName,
lastName: req.body.lastName,
})
await req.logInPromise(user)
return user
})
t.post('/auth/login', async (req, res, next) => {
const user = await authenticator
.authenticate('local')(req, res, next)
if (!user) {
res.status(401)
return
}
await req.logInPromise(user)
return user
})
t.post('/auth/password', [ensureLoggedInApi], async req => {
await authService.changePassword({
userId: req.user!.id,
oldPassword: req.body.oldPassword,
newPassword: req.body.newPassword,
})
})
t.get('/auth/logout', async (req, res) => {
req.logout()
})
return t.router
}

View File

@ -1,3 +1,2 @@
export * from './application' export * from './application'
export * from './AuthRoutes' export * from './configureAuthRoutes'
export * from './BaseRoute'

View File

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

View File

@ -1,4 +1,4 @@
import { TeamServiceMethods, ITeamService } from '@rondo.dev/common' import { TeamServiceMethods, TeamService } from '@rondo.dev/common'
import { test } from '../test' import { test } from '../test'
describe('team', () => { describe('team', () => {
@ -15,7 +15,7 @@ describe('team', () => {
}) })
const getClient = () => const getClient = () =>
test.rpc<ITeamService>( test.rpc<TeamService>(
'/rpc/teamService', '/rpc/teamService',
TeamServiceMethods, TeamServiceMethods,
headers, headers,

View File

@ -1,20 +1,19 @@
import { ITeamAddUserParams, ITeamCreateParams, ITeamRemoveParams, ITeamService, ITeamUpdateParams, trim } from '@rondo.dev/common' import { TeamAddUserParams, TeamCreateParams, TeamRemoveParams, TeamService, TeamUpdateParams, trim, UserPermissions } from '@rondo.dev/common'
import { IUserInTeam } from '@rondo.dev/common/lib/team/IUserInTeam' import { UserInTeam } from '@rondo.dev/common/lib/team/UserInTeam'
import Validator from '@rondo.dev/validator' import Validator from '@rondo.dev/validator'
import { IDatabase } from '../database/IDatabase' import { Database } from '../database/Database'
import { Team } from '../entities/Team' import { Team } from '../entities/Team'
import { UserTeam } from '../entities/UserTeam' import { UserTeam } from '../entities/UserTeam'
import { IUserPermissions } from '../services/IUserPermissions' import { ensureLoggedIn, Context, RPC } from './RPC'
import { ensureLoggedIn, IContext, RPC } from './RPC'
@ensureLoggedIn @ensureLoggedIn
export class TeamService implements RPC<ITeamService> { export class SQLTeamService implements RPC<TeamService> {
constructor( constructor(
protected readonly db: IDatabase, protected readonly db: Database,
protected readonly permissions: IUserPermissions, protected readonly permissions: UserPermissions,
) {} ) {}
async create(context: IContext, params: ITeamCreateParams) { async create(context: Context, params: TeamCreateParams) {
const userId = context.user!.id const userId = context.user!.id
const name = trim(params.name) const name = trim(params.name)
@ -38,7 +37,7 @@ export class TeamService implements RPC<ITeamService> {
return (await this.findOne(context, team.id))! return (await this.findOne(context, team.id))!
} }
async remove(context: IContext, {id}: ITeamRemoveParams) { async remove(context: Context, {id}: TeamRemoveParams) {
const userId = context.user!.id const userId = context.user!.id
await this.permissions.belongsToTeam({ await this.permissions.belongsToTeam({
@ -55,7 +54,7 @@ export class TeamService implements RPC<ITeamService> {
return {id} return {id}
} }
async update(context: IContext, {id, name}: ITeamUpdateParams) { async update(context: Context, {id, name}: TeamUpdateParams) {
const userId = context.user!.id const userId = context.user!.id
await this.permissions.belongsToTeam({ await this.permissions.belongsToTeam({
@ -73,7 +72,7 @@ export class TeamService implements RPC<ITeamService> {
return (await this.findOne(context, id))! return (await this.findOne(context, id))!
} }
async addUser(context: IContext, params: ITeamAddUserParams) { async addUser(context: Context, params: TeamAddUserParams) {
const {userId, teamId, roleId} = params const {userId, teamId, roleId} = params
await this.db.getRepository(UserTeam) await this.db.getRepository(UserTeam)
.save({userId, teamId, roleId}) .save({userId, teamId, roleId})
@ -89,7 +88,7 @@ export class TeamService implements RPC<ITeamService> {
return this._mapUserInTeam(userTeam!) return this._mapUserInTeam(userTeam!)
} }
async removeUser(context: IContext, params: ITeamAddUserParams) { async removeUser(context: Context, params: TeamAddUserParams) {
const {teamId, userId, roleId} = params const {teamId, userId, roleId} = params
await this.permissions.belongsToTeam({ await this.permissions.belongsToTeam({
@ -104,11 +103,11 @@ export class TeamService implements RPC<ITeamService> {
return {teamId, userId, roleId} return {teamId, userId, roleId}
} }
async findOne(context: IContext, id: number) { async findOne(context: Context, id: number) {
return this.db.getRepository(Team).findOne(id) return this.db.getRepository(Team).findOne(id)
} }
async find(context: IContext) { async find(context: Context) {
const userId = context.user!.id const userId = context.user!.id
return this.db.getRepository(Team) return this.db.getRepository(Team)
@ -119,7 +118,7 @@ export class TeamService implements RPC<ITeamService> {
.getMany() .getMany()
} }
async findUsers(context: IContext, teamId: number) { async findUsers(context: Context, teamId: number) {
const userId = context.user!.id const userId = context.user!.id
await this.permissions.belongsToTeam({ await this.permissions.belongsToTeam({
@ -139,7 +138,7 @@ export class TeamService implements RPC<ITeamService> {
} }
} }
protected _mapUserInTeam(ut: UserTeam): IUserInTeam { protected _mapUserInTeam(ut: UserTeam): UserInTeam {
return { return {
teamId: ut.teamId, teamId: ut.teamId,
userId: ut.userId, userId: ut.userId,

View File

@ -1,7 +1,7 @@
import {test} from '../test' import {test} from '../test'
import { IUserService, UserServiceMethods } from '@rondo.dev/common' import { UserService, UserServiceMethods } from '@rondo.dev/common'
describe('user', () => { describe('SQLUserService', () => {
test.withDatabase() test.withDatabase()
@ -12,7 +12,7 @@ describe('user', () => {
}) })
const createService = () => { const createService = () => {
return test.rpc<IUserService>( return test.rpc<UserService>(
'/rpc/userService', '/rpc/userService',
UserServiceMethods, UserServiceMethods,
headers, headers,

View File

@ -1,19 +1,18 @@
import { IUserService } from '@rondo.dev/common' import { UserService } from '@rondo.dev/common'
import { compare, hash } from 'bcrypt' import { hash } from 'bcrypt'
import createError from 'http-errors' import { Database } from '../database/Database'
import { IDatabase } from '../database/IDatabase'
import { User } from '../entities/User' import { User } from '../entities/User'
import { UserEmail } from '../entities/UserEmail' import { UserEmail } from '../entities/UserEmail'
import { ensureLoggedIn, IContext, RPC } from './RPC' import { Context, ensureLoggedIn, RPC } from './RPC'
const SALT_ROUNDS = 10 const SALT_ROUNDS = 10
const MIN_PASSWORD_LENGTH = 10 // const MIN_PASSWORD_LENGTH = 10
@ensureLoggedIn @ensureLoggedIn
export class UserService implements RPC<IUserService> { export class SQLUserService implements RPC<UserService> {
constructor(protected readonly db: IDatabase) {} constructor(protected readonly db: Database) {}
async getProfile(context: IContext) { async getProfile(context: Context) {
const userId = context.user!.id const userId = context.user!.id
// current user should always exist in the database // current user should always exist in the database
@ -29,7 +28,7 @@ export class UserService implements RPC<IUserService> {
} }
} }
async findUserByEmail(context: IContext, email: string) { async findUserByEmail(context: Context, email: string) {
const userEmail = await this.db.getRepository(UserEmail) const userEmail = await this.db.getRepository(UserEmail)
.findOne({ email }, { .findOne({ email }, {
relations: ['user'], relations: ['user'],

View File

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

View File

@ -1,13 +0,0 @@
import {ICredentials, INewUser, IUser} from '@rondo.dev/common'
export interface IAuthService {
createUser(credentials: INewUser): Promise<IUser>
changePassword(params: {
userId: number,
oldPassword: string,
newPassword: string,
}): Promise<any>
validateCredentials(credentials: ICredentials): Promise<IUser | undefined>
findOne(id: number): Promise<IUser | undefined>
findUserByEmail(email: string): Promise<IUser | undefined>
}

View File

@ -1,4 +0,0 @@
export interface IUserPermissions {
// TODO check for role too
belongsToTeam(params: {userId: number, teamId: number}): Promise<void>
}

View File

@ -1,14 +1,14 @@
import {test} from '../test' import { test } from '../test'
import {AuthService} from './AuthService' import { SQLAuthService } from './SQLAuthService'
describe('AuthService', () => { describe('SQLAuthService', () => {
test.withDatabase() test.withDatabase()
const username = test.username const username = test.username
const password = '1234567890' const password = '1234567890'
const authService = new AuthService(test.bootstrap.database) const authService = new SQLAuthService(test.bootstrap.database)
async function createUser(u = username, p = password) { async function createUser(u = username, p = password) {
return authService.createUser({ return authService.createUser({

View File

@ -1,20 +1,19 @@
import { ICredentials, INewUser, IUser, trim } from '@rondo.dev/common' import { AuthService, Credentials, NewUser, UserProfile, trim } from '@rondo.dev/common'
import Validator from '@rondo.dev/validator' import Validator from '@rondo.dev/validator'
import { compare, hash } from 'bcrypt' import { compare, hash } from 'bcrypt'
import { validate as validateEmail } from 'email-validator' import { validate as validateEmail } from 'email-validator'
import createError from 'http-errors' import createError from 'http-errors'
import { IDatabase } from '../database/IDatabase' import { Database } from '../database/Database'
import { User } from '../entities/User' import { User } from '../entities/User'
import { UserEmail } from '../entities/UserEmail' import { UserEmail } from '../entities/UserEmail'
import { IAuthService } from './IAuthService'
const SALT_ROUNDS = 10 const SALT_ROUNDS = 10
const MIN_PASSWORD_LENGTH = 10 const MIN_PASSWORD_LENGTH = 10
export class AuthService implements IAuthService { export class SQLAuthService implements AuthService {
constructor(protected readonly db: IDatabase) {} constructor(protected readonly db: Database) {}
async createUser(payload: INewUser): Promise<IUser> { async createUser(payload: NewUser): Promise<UserProfile> {
const newUser = { const newUser = {
username: trim(payload.username), username: trim(payload.username),
firstName: trim(payload.firstName), firstName: trim(payload.firstName),
@ -88,9 +87,9 @@ export class AuthService implements IAuthService {
} }
async changePassword(params: { async changePassword(params: {
userId: number, userId: number
oldPassword: string, oldPassword: string
newPassword: string, newPassword: string
}) { }) {
const {userId, oldPassword, newPassword} = params const {userId, oldPassword, newPassword} = params
const userRepository = this.db.getRepository(User) const userRepository = this.db.getRepository(User)
@ -109,7 +108,7 @@ export class AuthService implements IAuthService {
.update(userId, { password }) .update(userId, { password })
} }
async validateCredentials(credentials: ICredentials) { async validateCredentials(credentials: Credentials) {
const {username, password} = credentials const {username, password} = credentials
const user = await this.db.getRepository(User) const user = await this.db.getRepository(User)
.createQueryBuilder('user') .createQueryBuilder('user')

View File

@ -1,10 +1,10 @@
import { UserPermissions } from '@rondo.dev/common'
import createError from 'http-errors' import createError from 'http-errors'
import {IDatabase} from '../database/IDatabase' import { Database } from '../database/Database'
import {UserTeam} from '../entities/UserTeam' import { UserTeam } from '../entities/UserTeam'
import {IUserPermissions} from './IUserPermissions'
export class UserPermissions implements IUserPermissions { export class SQLUserPermissions implements UserPermissions {
constructor(protected readonly db: IDatabase) {} constructor(protected readonly db: Database) {}
async belongsToTeam(params: {userId: number, teamId: number}) { async belongsToTeam(params: {userId: number, teamId: number}) {
const {userId, teamId} = params const {userId, teamId} = params

View File

@ -1,4 +1,2 @@
export * from './IAuthService' export * from './SQLAuthService'
export * from './AuthService' export * from './SQLUserPermissions'
export * from './IUserPermissions'
export * from './UserPermissions'

View File

@ -1,4 +1,4 @@
export interface ISession { export interface DefaultSession {
// TODO use timestamp field // TODO use timestamp field
expiredAt: number expiredAt: number
id: string id: string

View File

@ -1,16 +1,13 @@
import express, {Application} from 'express'
import request from 'supertest'
import {SessionStore} from './SessionStore'
import {ISession} from './ISession'
import ExpressSession from 'express-session'
import loggerFactory from '@rondo.dev/logger' import loggerFactory from '@rondo.dev/logger'
import { import express, { Application } from 'express'
createConnection, Column, Connection, Entity, Index, PrimaryColumn, import ExpressSession from 'express-session'
Repository, import request from 'supertest'
} from 'typeorm' import { Column, Connection, createConnection, Entity, Index, PrimaryColumn, Repository } from 'typeorm'
import { DefaultSession } from './DefaultSession'
import { SessionStore } from './SessionStore'
@Entity() @Entity()
class Session implements ISession { class SessionEntity implements DefaultSession {
@PrimaryColumn() @PrimaryColumn()
id!: string id!: string
@ -28,15 +25,15 @@ class Session implements ISession {
describe('SessionStore', () => { describe('SessionStore', () => {
let connection!: Connection let connection!: Connection
let repository!: Repository<Session> let repository!: Repository<SessionEntity>
beforeEach(async () => { beforeEach(async () => {
connection = await createConnection({ connection = await createConnection({
type: 'sqlite', type: 'sqlite',
database: ':memory:', database: ':memory:',
entities: [Session], entities: [SessionEntity],
synchronize: true, synchronize: true,
}) })
repository = connection.getRepository(Session) repository = connection.getRepository(SessionEntity)
}) })
afterEach(() => connection!.close()) afterEach(() => connection!.close())
@ -88,7 +85,7 @@ describe('SessionStore', () => {
.expect(200) .expect(200)
} }
function getSession(app: Application, cookie: string = '') { function getSession(app: Application, cookie = '') {
return request(app) return request(app)
.get('/session') .get('/session')
.set('cookie', cookie) .set('cookie', cookie)

View File

@ -1,31 +1,31 @@
import {Store} from 'express-session' import { Logger } from '@rondo.dev/logger'
import {ISession} from './ISession' import { debounce } from '@rondo.dev/tasq'
import {Repository, LessThan} from 'typeorm' import { Store } from 'express-session'
import {debounce} from '@rondo.dev/tasq' import { LessThan, Repository } from 'typeorm'
import { ILogger } from '@rondo.dev/logger' import { DefaultSession } from './DefaultSession'
type SessionData = Express.SessionData type SessionData = Express.SessionData
type Callback = (err?: any, session?: SessionData) => void type Callback = (err?: any, session?: SessionData) => void
type CallbackErr = (err?: any) => void type CallbackErr = (err?: any) => void
export interface ISessionStoreOptions<S extends ISession> { export interface SessionStoreOptions<S extends DefaultSession> {
readonly ttl: number readonly ttl: number
readonly cleanupDelay: number readonly cleanupDelay: number
readonly getRepository: TRepositoryFactory<S> readonly getRepository: RepositoryFactory<S>
readonly logger: ILogger, readonly logger: Logger
buildSession(sessionData: SessionData, session: ISession): S buildSession(sessionData: SessionData, session: DefaultSession): S
} }
export type TRepositoryFactory<T> = () => Repository<T> export type RepositoryFactory<T> = () => Repository<T>
// TODO casting as any because TypeScript complains. Looks like this is a // TODO casting as any because TypeScript complains. Looks like this is a
// bug in TypeScript 3.2.2 // bug in TypeScript 3.2.2
// //
// https://github.com/typeorm/typeorm/issues/1544 // https://github.com/typeorm/typeorm/issues/1544
// https://github.com/Microsoft/TypeScript/issues/21592 // https://github.com/Microsoft/TypeScript/issues/21592
export class SessionStore<S extends ISession> extends Store { export class SessionStore<S extends DefaultSession> extends Store {
protected readonly getRepository: TRepositoryFactory<S> protected readonly getRepository: RepositoryFactory<S>
readonly cleanup = debounce(async () => { readonly cleanup = debounce(async () => {
try { try {
@ -43,7 +43,7 @@ export class SessionStore<S extends ISession> extends Store {
}, 1000) }, 1000)
constructor( constructor(
protected readonly options: ISessionStoreOptions<S>, protected readonly options: SessionStoreOptions<S>,
) { ) {
super() super()
this.getRepository = options.getRepository this.getRepository = options.getRepository

View File

@ -1,2 +1,2 @@
export * from './ISession' export * from './DefaultSession'
export * from './SessionStore' export * from './SessionStore'

View File

@ -1,4 +1,4 @@
import {Namespace} from 'cls-hooked' import { Namespace } from 'cls-hooked'
export class NamespaceMock implements Namespace { export class NamespaceMock implements Namespace {
readonly context: {[key: string]: any} = {} readonly context: {[key: string]: any} = {}

View File

@ -3,11 +3,11 @@ import {RequestTester} from './RequestTester'
describe('RequestTest', () => { describe('RequestTest', () => {
interface IAPI { interface API {
'/test': { '/test': {
'get': { 'get': {
response: {id: number}, response: {id: number}
}, }
} }
} }
@ -18,14 +18,14 @@ describe('RequestTest', () => {
describe('constructor', () => { describe('constructor', () => {
it('creates a blank baseUrl', () => { it('creates a blank baseUrl', () => {
const t = new RequestTester<IAPI>(app) const t = new RequestTester<API>(app)
expect(t.baseUrl).toEqual('') expect(t.baseUrl).toEqual('')
}) })
}) })
describe('RequestTester.request', () => { describe('RequestTester.request', () => {
it('creates a response', async () => { it('creates a response', async () => {
const t = new RequestTester<IAPI>(app, '/api') const t = new RequestTester<API>(app, '/api')
const result = await t.request('get', '/test') const result = await t.request('get', '/test')
expect(result.body.id).toBe(1) expect(result.body.id).toBe(1)
}) })

View File

@ -1,29 +1,32 @@
import { URLFormatter } from '@rondo.dev/http-client' /* eslint @typescript-eslint/no-explicit-any: 0 */
import { IRoutes, TMethod } from '@rondo.dev/http-types' import { Headers, URLFormatter } from '@rondo.dev/http-client'
import { Routes, Method } from '@rondo.dev/http-types'
import supertest from 'supertest' import supertest from 'supertest'
// https://stackoverflow.com/questions/48215950/exclude-property-from-type // https://stackoverflow.com/questions/48215950/exclude-property-from-type
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>> type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>
interface ITest extends Omit<supertest.Test, 'then' | 'catch' | 'finally'> {} interface Test extends Omit<supertest.Test, 'then' | 'catch' | 'finally'> {}
interface IResponse< interface Response<
R extends IRoutes, R extends Routes,
P extends keyof R, P extends keyof R,
M extends TMethod, M extends Method,
> extends supertest.Response { > extends supertest.Response {
body: R[P][M]['response'] body: R[P][M]['response']
header: {[key: string]: string} header: {[key: string]: string}
} }
interface IRequest< interface Request<
R extends IRoutes, R extends Routes,
P extends keyof R, P extends keyof R,
M extends TMethod, M extends Method,
> extends ITest, Promise<IResponse<R, P, M>> { > extends Test, Promise<Response<R, P, M>> {
send(value: R[P][M]['body'] | string): this send(value: R[P][M]['body'] | string): this
expect(status: number, body?: any): this expect(status: number, body?: any): this
expect(body: string | RegExp | object | ((res: Response) => any)): this expect(
body: string | RegExp | object | ((res: Response<R, P, M>,
) => any)): this
expect(field: string, val: string | RegExp): this expect(field: string, val: string | RegExp): this
// any other method that's called will return "this" from supertest's // any other method that's called will return "this" from supertest's
// or superagent's type definition and afterwards the promise will no longer // or superagent's type definition and afterwards the promise will no longer
@ -31,22 +34,18 @@ interface IRequest<
// type definition // type definition
} }
interface IRequestOptions< interface RequestOptions<
R extends IRoutes, R extends Routes,
P extends keyof R, P extends keyof R,
M extends TMethod, M extends Method,
> { > {
params?: R[P][M]['params'], params?: R[P][M]['params']
query?: R[P][M]['query'], query?: R[P][M]['query']
} }
export interface IHeaders { export class RequestTester<R extends Routes> {
[key: string]: string
}
export class RequestTester<R extends IRoutes> { protected headers: Headers = {}
protected headers: IHeaders = {}
protected formatter: URLFormatter = new URLFormatter() protected formatter: URLFormatter = new URLFormatter()
constructor( constructor(
@ -54,15 +53,14 @@ export class RequestTester<R extends IRoutes> {
readonly baseUrl = '', readonly baseUrl = '',
) {} ) {}
setHeaders(headers: IHeaders): this { setHeaders(headers: Headers): this {
this.headers = headers this.headers = headers
return this return this
} }
request<M extends TMethod, P extends keyof R & string>( request<M extends Method, P extends keyof R & string>(
method: M, path: P, options: IRequestOptions<R, P, 'post'> = {}, method: M, path: P, options: RequestOptions<R, P, 'post'> = {},
) ): Request<R, P, M> {
: IRequest<R, P, M> {
const url = this.formatter.format(path, options.params, options.query) const url = this.formatter.format(path, options.params, options.query)
const test = supertest(this.app)[method](`${this.baseUrl}${url}`) const test = supertest(this.app)[method](`${this.baseUrl}${url}`)
Object.keys(this.headers).forEach(key => { Object.keys(this.headers).forEach(key => {
@ -73,28 +71,28 @@ export class RequestTester<R extends IRoutes> {
get<P extends keyof R & string>( get<P extends keyof R & string>(
path: P, path: P,
options?: IRequestOptions<R, P, 'get'>, options?: RequestOptions<R, P, 'get'>,
) { ) {
return this.request('get', path, options) return this.request('get', path, options)
} }
post<P extends keyof R & string>( post<P extends keyof R & string>(
path: P, path: P,
options?: IRequestOptions<R, P, 'post'>, options?: RequestOptions<R, P, 'post'>,
) { ) {
return this.request('post', path, options) return this.request('post', path, options)
} }
put<P extends keyof R & string>( put<P extends keyof R & string>(
path: P, path: P,
options?: IRequestOptions<R, P, 'put'>, options?: RequestOptions<R, P, 'put'>,
) { ) {
return this.request('put', path, options) return this.request('put', path, options)
} }
delete<P extends keyof R & string>( delete<P extends keyof R & string>(
path: P, path: P,
options?: IRequestOptions<R, P, 'delete'>, options?: RequestOptions<R, P, 'delete'>,
) { ) {
return this.request('delete', path, options) return this.request('delete', path, options)
} }

View File

@ -1,29 +1,26 @@
import express from 'express' /* eslint @typescript-eslint/no-explicit-any: 0 */
import supertest from 'supertest' import { Routes } from '@rondo.dev/http-types'
import {Connection, QueryRunner} from 'typeorm'
import {
ENTITY_MANAGER, ITransactionManager, TRANSACTION_ID,
} from '../database/ITransactionManager'
import {IRoutes} from '@rondo.dev/http-types'
import {IBootstrap} from '../application/IBootstrap'
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, RPCClient } from '@rondo.dev/jsonrpc' import { createRemoteClient, FunctionPropertyNames, RPCClient } from '@rondo.dev/jsonrpc'
import {Server} from 'http' import { Server } from 'http'
import { IAppServer } from '../application/IAppServer' import { AddressInfo } from 'net'
import shortid from 'shortid'
import supertest from 'supertest'
import { Connection, QueryRunner } from 'typeorm'
import { AppServer } from '../application/AppServer'
import { Bootstrap } from '../application/Bootstrap'
import { ENTITY_MANAGER, TransactionManager, TRANSACTION_ID } from '../database/TransactionManager'
import { Role } from '../entities/Role'
import { RequestTester } from './RequestTester'
export class TestUtils<T extends IRoutes> { export class TestUtils<T extends Routes> {
readonly username = this.createTestUsername() readonly username = this.createTestUsername()
readonly password = 'Password10' readonly password = 'Password10'
readonly app: IAppServer readonly app: AppServer
readonly context: string readonly context: string
readonly transactionManager: ITransactionManager readonly transactionManager: TransactionManager
constructor(readonly bootstrap: IBootstrap) { constructor(readonly bootstrap: Bootstrap) {
this.app = bootstrap.application.server this.app = bootstrap.application.server
this.context = this.bootstrap.getConfig().app.context this.context = this.bootstrap.getConfig().app.context
this.transactionManager = this.bootstrap.database.transactionManager this.transactionManager = this.bootstrap.database.transactionManager
@ -82,7 +79,7 @@ export class TestUtils<T extends IRoutes> {
.save({name}) .save({name})
} }
async getError(promise: Promise<any>): Promise<Error> { async getError(promise: Promise<unknown>): Promise<Error> {
let error!: Error let error!: Error
try { try {
await promise await promise

View File

@ -1,3 +1,3 @@
export * from './TestUtils'
export * from './RequestTester'
export * from './NamespaceMock' export * from './NamespaceMock'
export * from './RequestTester'
export * from './TestUtils'

Some files were not shown because too many files have changed in this diff Show More