Skip to content

Commit

Permalink
feat(server): add /import/json endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
Silthus committed Feb 8, 2024
1 parent 904566f commit c2c2f49
Show file tree
Hide file tree
Showing 9 changed files with 121 additions and 25 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
13 changes: 12 additions & 1 deletion src/auth/local-dev/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<any> => {
Expand All @@ -19,3 +19,14 @@ export const localDevBypassAuthMiddleware = (() => {
return { user }
}
})()

export const localDevBypassAuthMiddleware = async (req, res, next): Promise<any> => {
req.user = {
roles: ['user_admin', 'org_admin', 'editor'],
uuid: muuid.v4(),
isBuilder: false
}
req.userId = req.user.uuid
req.token = 'local-dev-bypass'
next()
}
58 changes: 41 additions & 17 deletions src/auth/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,31 +7,55 @@ import { logger } from '../logger.js'
* Create a middleware context for Apollo server
*/
export const createContext = async ({ req }): Promise<any> => {
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<void> => {
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")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ describe('bulk import e2e', () => {
const assertBulkImport = async (...json: AreaJson[]): Promise<TestResult> => {
const result = await bulkImportJson({
user: testUser,
json,
json: {areas: json},
areas,
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export type ClimbJson = Partial<Omit<ClimbType, 'metadata' | 'pitches'>> & {

export interface BulkImportOptions {
user: MUUID
json: AreaJson[]
json: { areas?: AreaJson[] }
session?: mongoose.ClientSession
areas?: MutableAreaDataSource
climbs?: MutableClimbDataSource
Expand Down Expand Up @@ -59,15 +59,16 @@ 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) {
logger.error('bulk import failed', e)
result.errors.push(e)
} finally {
await session.endSession()
logger.debug('bulk import complete')
}
return result
}
Expand Down Expand Up @@ -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)))
}
Expand Down
File renamed without changes.
18 changes: 18 additions & 0 deletions src/db/import/json/request-handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { logger } from '../../../logger.js'
import { bulkImportJson } from './import-json.js'

export const importJsonRequestHandler = async (req, res): Promise<void> => {
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}`)
}
}
14 changes: 11 additions & 3 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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<ApolloServer> {
const schema = applyMiddleware(
Expand Down Expand Up @@ -48,10 +50,16 @@ export async function startServer (port = 4000): Promise<ApolloServer> {
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: '/' })

Expand Down
33 changes: 33 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down

0 comments on commit c2c2f49

Please sign in to comment.