diff --git a/.vscode/launch.json b/.vscode/launch.json index 34f0ff72..7631bae6 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -44,6 +44,18 @@ ], "console": "integratedTerminal", }, + { + "type": "node", + "request": "launch", + "name": "Launch API Server (serve-dev)", + "skipFiles": ["/**"], + "program": "${workspaceFolder}/src/main.ts", + "preLaunchTask": "tsc: build - tsconfig.json", + "outFiles": ["${workspaceFolder}/build/**/*.js"], + "runtimeExecutable": "yarn", + "runtimeArgs": ["run", "serve-dev"], + "console": "integratedTerminal" + }, { "name": "Debug Jest Tests", "type": "node", diff --git a/README.md b/README.md index 91dd85b2..0780ee28 100644 --- a/README.md +++ b/README.md @@ -124,6 +124,18 @@ These links explain the structure and key abstractions of our codebase. It's a g - GraphQL online playground: https://graphiql-online.com/ +- Bypassing auth when developing locally + + Start up your local server with `yarn serve-dev` (instead of `yarn serve`) + ```bash + # Run this in open-tacos project + yarn serve-dev + ``` + + This allows the current user to run any `Query` or `Mutation` (irrespective of the current user’s UUID) + + (How it works: `auth/middleware.ts` and `auth/permissions.ts` are conditionally swapped out on server initialization (`server.ts`), based on whether the env var `LOCAL_DEV_BYPASS_AUTH` is set) + - Full stack development Connect your [frontend](https://github.com/OpenBeta/open-tacos) dev env to this local server diff --git a/db-migrations/0005-area-sorting.js b/db-migrations/0005-area-sorting.js new file mode 100644 index 00000000..8bbae411 --- /dev/null +++ b/db-migrations/0005-area-sorting.js @@ -0,0 +1,5 @@ +/** + * Issue: 375 + */ + +db.areas.dropIndexes('metadata.leftRightIndex_1') diff --git a/package.json b/package.json index a83b795f..4880fcee 100644 --- a/package.json +++ b/package.json @@ -71,6 +71,7 @@ "build-release": "tsc -p tsconfig.release.json", "clean": "tsc -b --clean && rm -rf build/*", "serve": "yarn build && node --experimental-json-modules build/main.js", + "serve-dev": "echo \"🚨 LOCAL_DEV_BYPASS_AUTH enabled 🚨\" && LOCAL_DEV_BYPASS_AUTH=true yarn serve", "refresh-db": "./refresh-db.sh", "seed-usa": "yarn build && node build/db/import/usa/USADay0Seed.js", "seed-db": "./seed-db.sh", diff --git a/src/auth/local-dev/middleware.ts b/src/auth/local-dev/middleware.ts new file mode 100644 index 00000000..779066ec --- /dev/null +++ b/src/auth/local-dev/middleware.ts @@ -0,0 +1,21 @@ +/* +* This file is a mod of src/auth/middleware.ts and is used when starting the server via `yarn serve-dev` +* It bypasses the authentication for local development +*/ +import muuid, { MUUID } from 'uuid-mongodb' +import { AuthUserType } from '../../types.js' +import { logger } from '../../logger.js' + +export const localDevBypassAuthMiddleware = (() => { + const testUUID: MUUID = muuid.v4() + + return async ({ req }): Promise => { + const user: AuthUserType = { + roles: ['user_admin', 'org_admin', 'editor'], + uuid: testUUID, + isBuilder: false + } + logger.info(`The user.roles for this session is: ${user.roles.toString()}`) + return { user } + } +})() diff --git a/src/auth/local-dev/permissions.ts b/src/auth/local-dev/permissions.ts new file mode 100644 index 00000000..ac2ffd49 --- /dev/null +++ b/src/auth/local-dev/permissions.ts @@ -0,0 +1,19 @@ +/* +* This file is a mod of src/auth/permissions.ts and is used when starting the server via `yarn serve-dev` +* It bypasses the authorization for local development and allows all queries and mutations +*/ +import { shield, allow } from 'graphql-shield' + +const localDevBypassAuthPermissions = shield({ + Query: { + '*': allow + }, + Mutation: { + '*': allow + } +}, { + allowExternalErrors: true, + fallbackRule: allow +}) + +export default localDevBypassAuthPermissions diff --git a/src/db/AreaSchema.ts b/src/db/AreaSchema.ts index e27ded05..1493c744 100644 --- a/src/db/AreaSchema.ts +++ b/src/db/AreaSchema.ts @@ -118,8 +118,9 @@ export const AreaSchema = new Schema({ }, { timestamps: true }) AreaSchema.index({ _deleting: 1 }, { expireAfterSeconds: 0 }) -AreaSchema.index({ 'metadata.leftRightIndex': 1 }, { - unique: true, +AreaSchema.index({ + 'metadata.leftRightIndex': 1 +}, { partialFilterExpression: { 'metadata.leftRightIndex': { $gt: -1 diff --git a/src/model/MutableAreaDataSource.ts b/src/model/MutableAreaDataSource.ts index bdd67b08..e8d9d8b3 100644 --- a/src/model/MutableAreaDataSource.ts +++ b/src/model/MutableAreaDataSource.ts @@ -427,21 +427,6 @@ export default class MutableAreaDataSource extends AreaDataSource { const opType = OperationType.orderAreas const change = await changelogDataSource.create(session, user, opType) const updates: any[] = [] - let expectedOpCount = input.length - - // Clear existing indices so we can re-order without running into duplicate key errors. - if (input.some(i => i.leftRightIndex >= 0)) { - updates.push({ - updateMany: { - filter: { 'metadata.area_id': { $in: input.map(i => muuid.from(i.areaId)) } }, - update: { - $set: { 'metadata.leftRightIndex': -1 } - // Don't record change since this is an intermediate step. - } - } - }) - expectedOpCount = expectedOpCount * 2 - } input.forEach(({ areaId, leftRightIndex }, index) => { updates.push({ @@ -465,7 +450,7 @@ export default class MutableAreaDataSource extends AreaDataSource { const rs = (await this.areaModel.bulkWrite(updates, { session })).toJSON() - if (rs.ok === 1 && rs.nMatched === rs.nModified && rs.nMatched === expectedOpCount) { + if (rs.ok === 1 && rs.nMatched === rs.nModified) { return input.map(item => item.areaId) } else { throw new Error(`Expect to update ${input.length} areas but found ${rs.nMatched}.`) diff --git a/src/model/__tests__/updateAreas.ts b/src/model/__tests__/updateAreas.ts index 693edc68..82b7cadb 100644 --- a/src/model/__tests__/updateAreas.ts +++ b/src/model/__tests__/updateAreas.ts @@ -286,28 +286,6 @@ describe('Areas', () => { leftRightIndex: change2.leftRightIndex }) })) - - // Able to overwrite existing leftRightIndices without duplicate key errors - const change3: UpdateSortingOrderType = { - areaId: a1.metadata.area_id.toUUID().toString(), - leftRightIndex: 9 - } - const change4: UpdateSortingOrderType = { - areaId: a2.metadata.area_id.toUUID().toString(), - leftRightIndex: 10 - } - - await expect(areas.updateSortingOrder(testUser, [change3, change4])).resolves.toStrictEqual( - [a1.metadata.area_id.toUUID().toString(), a2.metadata.area_id.toUUID().toString()]) - - // Make sure we can't have duplicate leftToRight indices >= 0 - await expect( - areas.updateSortingOrder(testUser, [{ ...change3, leftRightIndex: change4.leftRightIndex }])) - .rejects.toThrowError(/E11000/) - - // But we can have duplicate indices < 0 to indicate unsorted - await areas.updateSortingOrder(testUser, - [{ ...change3, leftRightIndex: -1 }, { ...change4, leftRightIndex: -1 }]) }) it('should update self and childrens pathTokens', async () => { diff --git a/src/server.ts b/src/server.ts index b7c98621..7159ec8d 100644 --- a/src/server.ts +++ b/src/server.ts @@ -8,7 +8,10 @@ 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, permissions } from './auth/index.js' +import { createContext } from './auth/middleware.js' +import permissions from './auth/permissions.js' +import { 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' import MutableOrgDS from './model/MutableOrganizationDataSource.js' @@ -19,7 +22,7 @@ import UserDataSource from './model/UserDataSource.js' export async function createServer (): Promise { const schema = applyMiddleware( graphqlSchema, - permissions.generate(graphqlSchema) + (process.env.LOCAL_DEV_BYPASS_AUTH === 'true' ? localDevBypassAuthPermissions : permissions).generate(graphqlSchema) ) const dataSources: () => DataSources = () => ({ climbs: MutableClimbDataSource.getInstance(), @@ -36,10 +39,11 @@ export async function createServer (): Promise { xmedia: new XMediaDataSource(mongoose.connection.db.collection('xmedia')), post: new PostDataSource(mongoose.connection.db.collection('post')) }) + const server = new ApolloServer({ introspection: true, schema, - context: createContext, + context: process.env.LOCAL_DEV_BYPASS_AUTH === 'true' ? localDevBypassAuthMiddleware : createContext, dataSources, cache: 'bounded' })