diff --git a/package.json b/package.json index 997a7304..d2428680 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "apollo-server-express": "^3.13.0", "auth0": "^3.4.0", "axios": "^1.3.6", + "body-parser": "^1.20.2", "cors": "^2.8.5", "date-fns": "^2.30.0", "dot-object": "^2.1.4", diff --git a/src/auth/local-dev/middleware.ts b/src/auth/local-dev/middleware.ts index 779066ec..5df894b0 100644 --- a/src/auth/local-dev/middleware.ts +++ b/src/auth/local-dev/middleware.ts @@ -6,7 +6,7 @@ import muuid, { MUUID } from 'uuid-mongodb' import { AuthUserType } from '../../types.js' import { logger } from '../../logger.js' -export const localDevBypassAuthMiddleware = (() => { +export const localDevBypassAuthContext = (() => { const testUUID: MUUID = muuid.v4() return async ({ req }): Promise => { @@ -19,3 +19,14 @@ export const localDevBypassAuthMiddleware = (() => { return { user } } })() + +export const localDevBypassAuthMiddleware = async (req, res, next): Promise => { + req.user = { + roles: ['user_admin', 'org_admin', 'editor'], + uuid: muuid.v4(), + isBuilder: false + } + req.userId = req.user.uuid + req.token = 'local-dev-bypass' + next() +} diff --git a/src/auth/middleware.ts b/src/auth/middleware.ts index af81045c..6c554217 100644 --- a/src/auth/middleware.ts +++ b/src/auth/middleware.ts @@ -7,31 +7,55 @@ import { logger } from '../logger.js' * Create a middleware context for Apollo server */ export const createContext = async ({ req }): Promise => { - const { headers } = req - const user: AuthUserType = { roles: [], uuid: undefined, isBuilder: false } - const authHeader = String(headers?.authorization ?? '') - if (authHeader.startsWith('Bearer ')) { - const token = authHeader.substring(7, authHeader.length).trim() + try { + await validateTokenAndExtractUser(req) + } catch (e) { + logger.error(`Can't validate token and extract user ${e.toString() as string}`) + throw new Error('An unexpected error has occurred. Please notify us at support@openbeta.io.') + } - let payload - try { - payload = await verifyJWT(token) - } catch (e) { - logger.error(`Can't verify JWT token ${e.toString() as string}`) - throw new Error('An unexpected error has occurred. Please notify us at support@openbeta.io.') - } + return { user } +} - user.isBuilder = payload?.scope?.includes('builder:default') ?? false - user.roles = payload?.['https://tacos.openbeta.io/roles'] ?? [] - const uidStr: string | undefined = payload?.['https://tacos.openbeta.io/uuid'] - user.uuid = uidStr != null ? muid.from(uidStr) : undefined +export const authMiddleware = async (req, res, next): Promise => { + try { + const { user, token } = await validateTokenAndExtractUser(req) + req.user = user + req.userId = user.uuid + req.token = token + next() + } catch (e) { + logger.error(`Can't verify JWT token ${e.toString() as string}`) + res.status(401).send('Unauthorized') } +} - return { user } +async function validateTokenAndExtractUser (req: Request): Promise<{ user: AuthUserType, token: string }> { + const { headers } = req + const authHeader = String(headers?.authorization ?? '') + if (!authHeader.startsWith('Bearer ')) { + throw new Error('Unauthorized. Please provide a valid JWT token in the Authorization header.') + } + + const token = authHeader.substring(7, authHeader.length).trim() + try { + const payload = await verifyJWT(token) + return { + user: { + isBuilder: payload?.scope?.includes('builder:default') ?? false, + roles: payload?.['https://tacos.openbeta.io/roles'] ?? [], + uuid: payload?.['https://tacos.openbeta.io/uuid'] != null ? muid.from(payload['https://tacos.openbeta.io/uuid']) : undefined + }, + token + } + } catch (e) { + logger.error(`Can't verify JWT token ${e.toString() as string}`) + throw new Error("Unauthorized. Can't verify JWT token") + } } diff --git a/src/db/import/bulk/__tests__/import-json.test.ts b/src/db/import/json/__tests__/import-json.test.ts similarity index 99% rename from src/db/import/bulk/__tests__/import-json.test.ts rename to src/db/import/json/__tests__/import-json.test.ts index 70e2a883..561d8e58 100644 --- a/src/db/import/bulk/__tests__/import-json.test.ts +++ b/src/db/import/json/__tests__/import-json.test.ts @@ -28,7 +28,7 @@ describe('bulk import e2e', () => { const assertBulkImport = async (...json: AreaJson[]): Promise => { const result = await bulkImportJson({ user: testUser, - json, + json: {areas: json}, areas, }); diff --git a/src/db/import/bulk/import-json.ts b/src/db/import/json/import-json.ts similarity index 95% rename from src/db/import/bulk/import-json.ts rename to src/db/import/json/import-json.ts index 99d2707b..3fcc624d 100644 --- a/src/db/import/bulk/import-json.ts +++ b/src/db/import/json/import-json.ts @@ -26,7 +26,7 @@ export type ClimbJson = Partial> & { export interface BulkImportOptions { user: MUUID - json: AreaJson[] + json: { areas?: AreaJson[] } session?: mongoose.ClientSession areas?: MutableAreaDataSource climbs?: MutableClimbDataSource @@ -59,7 +59,9 @@ export async function bulkImportJson ({ const session = _session ?? (await mongoose.startSession()) try { await session.withTransaction(async () => { + logger.info('starting bulk import...', json) result = await _bulkImportJson({ user, json, areas, climbs, session }) + logger.info('bulk import successful', result) return result }) } catch (e) { @@ -67,7 +69,6 @@ export async function bulkImportJson ({ result.errors.push(e) } finally { await session.endSession() - logger.debug('bulk import complete') } return result } @@ -120,7 +121,7 @@ async function _bulkImportJson ({ return [...result] } - for (const node of json) { + for (const node of json?.areas ?? []) { // fails fast and throws errors up the chain addedAreas.push(...(await addArea(node))) } diff --git a/src/db/import/bulk/index.ts b/src/db/import/json/index.ts similarity index 100% rename from src/db/import/bulk/index.ts rename to src/db/import/json/index.ts diff --git a/src/db/import/json/request-handler.ts b/src/db/import/json/request-handler.ts new file mode 100644 index 00000000..ed505d13 --- /dev/null +++ b/src/db/import/json/request-handler.ts @@ -0,0 +1,18 @@ +import { logger } from '../../../logger.js' +import { bulkImportJson } from './import-json.js' + +export const importJsonRequestHandler = async (req, res): Promise => { + try { + const result = await bulkImportJson({ json: req.body, user: req.userId }) + if (result.errors.length > 0) { + logger.error(`Error importing JSON: ${result.errors.map(e => e.toString()).join(', ')}`) + res.status(500).send({ message: 'Error importing JSON', errors: result.errors.map((e) => e.toString()) }) + } else { + res.status(200).send(result) + logger.info(`Imported JSON: ${result.addedAreas.length} areas, ${result.climbIds.length} climbs`) + } + } catch (e) { + logger.error(`Error importing JSON: ${e.toString() as string}`) + res.status(500).send(`Error importing JSON: ${e.toString() as string}`) + } +} diff --git a/src/server.ts b/src/server.ts index 2336a041..05fe35f1 100644 --- a/src/server.ts +++ b/src/server.ts @@ -8,9 +8,9 @@ import ChangeLogDataSource from './model/ChangeLogDataSource.js' import MutableMediaDataSource from './model/MutableMediaDataSource.js' import MutableClimbDataSource from './model/MutableClimbDataSource.js' import TickDataSource from './model/TickDataSource.js' -import { createContext } from './auth/middleware.js' +import { authMiddleware, createContext } from './auth/middleware.js' import permissions from './auth/permissions.js' -import { localDevBypassAuthMiddleware } from './auth/local-dev/middleware.js' +import { localDevBypassAuthContext, localDevBypassAuthMiddleware } from './auth/local-dev/middleware.js' import localDevBypassAuthPermissions from './auth/local-dev/permissions.js' import XMediaDataSource from './model/XMediaDataSource.js' import PostDataSource from './model/PostDataSource.js' @@ -20,6 +20,8 @@ import type { DataSources } from 'apollo-server-core/dist/graphqlOptions' import UserDataSource from './model/UserDataSource.js' import express from 'express' import * as http from 'http' +import bodyParser from 'body-parser' +import { importJsonRequestHandler } from './db/import/json/request-handler.js' export async function startServer (port = 4000): Promise { const schema = applyMiddleware( @@ -48,10 +50,16 @@ export async function startServer (port = 4000): Promise { const server = new ApolloServer({ introspection: true, schema, - context: process.env.LOCAL_DEV_BYPASS_AUTH === 'true' ? localDevBypassAuthMiddleware : createContext, + context: process.env.LOCAL_DEV_BYPASS_AUTH === 'true' ? localDevBypassAuthContext : createContext, dataSources, cache: 'bounded' }) + app.post('/import/json', [ + process.env.LOCAL_DEV_BYPASS_AUTH === 'true' ? localDevBypassAuthMiddleware : authMiddleware, + bodyParser.json(), + importJsonRequestHandler + ]) + await server.start() server.applyMiddleware({ app, path: '/' }) diff --git a/yarn.lock b/yarn.lock index bbf6d3ad..19333e5c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2687,6 +2687,24 @@ body-parser@^1.19.0: type-is "~1.6.18" unpipe "1.0.0" +body-parser@^1.20.2: + version "1.20.2" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.2.tgz#6feb0e21c4724d06de7ff38da36dad4f57a747fd" + integrity sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA== + dependencies: + bytes "3.1.2" + content-type "~1.0.5" + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + http-errors "2.0.0" + iconv-lite "0.4.24" + on-finished "2.4.1" + qs "6.11.0" + raw-body "2.5.2" + type-is "~1.6.18" + unpipe "1.0.0" + bowser@^2.11.0: version "2.11.0" resolved "https://registry.yarnpkg.com/bowser/-/bowser-2.11.0.tgz#5ca3c35757a7aa5771500c70a73a9f91ef420a8f" @@ -3031,6 +3049,11 @@ content-type@~1.0.4: resolved "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz" integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== +content-type@~1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" + integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== + convert-source-map@^1.6.0, convert-source-map@^1.7.0: version "1.9.0" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.9.0.tgz#7faae62353fb4213366d0ca98358d22e8368b05f" @@ -6583,6 +6606,16 @@ raw-body@2.5.1: iconv-lite "0.4.24" unpipe "1.0.0" +raw-body@2.5.2: + version "2.5.2" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.2.tgz#99febd83b90e08975087e8f1f9419a149366b68a" + integrity sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA== + dependencies: + bytes "3.1.2" + http-errors "2.0.0" + iconv-lite "0.4.24" + unpipe "1.0.0" + rbush@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/rbush/-/rbush-3.0.1.tgz#5fafa8a79b3b9afdfe5008403a720cc1de882ecf"