Skip to content

Commit

Permalink
fix: use postgraphile's schema-only usage to create user
Browse files Browse the repository at this point in the history
Using an http request leads to a race condition as the auth callback's session might not be saved yet.
  • Loading branch information
matthieu-foucault committed Feb 15, 2022
1 parent dcfb490 commit f100f57
Show file tree
Hide file tree
Showing 6 changed files with 116 additions and 78 deletions.
4 changes: 2 additions & 2 deletions app/schema/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ input AddContactToRevisionInput {
payload verbatim. May be used to track mutations by the client.
"""
clientMutationId: String
contactIndex: Int
revisionId: Int
contactIndex: Int!
revisionId: Int!
}

"""The output of our `addContactToRevision` mutation."""
Expand Down
20 changes: 14 additions & 6 deletions app/schema/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,19 +29,27 @@
"name": "contactIndex",
"description": null,
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "revisionId",
"description": null,
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
}
},
"defaultValue": null
}
Expand Down
2 changes: 1 addition & 1 deletion app/server/db/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import pg from "pg";

const getDatabaseUrl = () => {
export const getDatabaseUrl = () => {
// If authentication is disabled use the user above to connect to the database
// Otherwise, use the PGUSER env variable
const PGUSER = process.env.PGUSER || "postgres";
Expand Down
2 changes: 1 addition & 1 deletion app/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ app.prepare().then(async () => {

server.use(await ssoMiddleware());

server.get("/auth-callback", createUserMiddleware("localhost", port));
server.get("/auth-callback", createUserMiddleware());

server.use(cookieParser());

Expand Down
25 changes: 5 additions & 20 deletions app/server/middleware/createUser.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Request, Response, NextFunction } from "express";

import { performQuery } from "./graphql";
// This middleware calls the createUserFromSession mutation.
// The request to that mutation is made with the current session
// cookies to ensure authentication.
Expand All @@ -12,28 +12,13 @@ mutation {
}
`;

const createUserMiddleware = (host: string, port: number) => {
const createUserMiddleware = () => {
return async (req: Request, _res: Response, next: NextFunction) => {
const fetchOptions = {
method: "POST",
body: JSON.stringify({
query: createUserMutation,
variables: null,
}),
headers: {
"Content-Type": "application/json",
cookie: req.headers.cookie,
},
};

const response = await fetch(
`http://${host}:${port}/graphql`,
fetchOptions
);
const response = await performQuery(createUserMutation, {}, req);

if (!response?.ok) {
if (response.errors) {
throw new Error(
`Failed to create user from session: ${response.statusText}`
`Failed to create user from session:\n${response.errors.join("\n")}`
);
}

Expand Down
141 changes: 93 additions & 48 deletions app/server/middleware/graphql/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import type { Request } from "express";
import { postgraphile } from "postgraphile";
import { pgPool } from "../../db";
import {
postgraphile,
createPostGraphileSchema,
withPostGraphileContext,
} from "postgraphile";
import { pgPool, getDatabaseUrl } from "../../db";
import { makePluginHook, PostGraphileOptions } from "postgraphile";
import PostgraphileRc from "../../../.postgraphilerc";
import PgManyToManyPlugin from "@graphile-contrib/pg-many-to-many";
Expand All @@ -13,10 +17,48 @@ import PgOrderByRelatedPlugin from "@graphile-contrib/pg-order-by-related";
import authenticationPgSettings from "./authenticationPgSettings";
import { generateDatabaseMockOptions } from "../../helpers/databaseMockPgOptions";
import FormChangeValidationPlugin from "./formChangeValidationPlugin";
import { graphql, GraphQLSchema } from "graphql";

async function saveRemoteFile({ stream }) {
const response = await fetch(
`${process.env.STORAGE_API_HOST}/api/v1/attachments/upload`,
{
method: "POST",
headers: {
"api-key": process.env.STORAGE_API_KEY,
"Content-Type": "multipart/form-data",
},
body: stream,
}
);
try {
return await response.json();
} catch (e) {
console.error(e);
}
}

async function resolveUpload(upload) {
const { createReadStream } = upload;
const stream = createReadStream();

// Save tile to remote storage system
const { uuid } = await saveRemoteFile({ stream });

return uuid;
}

// Use consola for logging instead of default logger
const pluginHook = makePluginHook([PostgraphileLogConsola]);

export const pgSettings = (req: Request) => {
const opts = {
...authenticationPgSettings(req),
...generateDatabaseMockOptions(req.cookies, ["mocks.mocked_timestamp"]),
};
return opts;
};

let postgraphileOptions: PostGraphileOptions = {
pluginHook,
appendPlugins: [
Expand All @@ -33,6 +75,17 @@ let postgraphileOptions: PostGraphileOptions = {
dynamicJson: true,
extendedErrors: ["hint", "detail", "errcode"],
showErrorStack: "json",
graphileBuildOptions: {
...PostgraphileRc.options.graphileBuildOptions,
uploadFieldDefinitions: [
{
match: ({ table, column }) =>
table === "attachment" && column === "file",
resolve: resolveUpload,
},
],
},
pgSettings,
};

if (process.env.NODE_ENV === "production") {
Expand All @@ -49,56 +102,48 @@ if (process.env.NODE_ENV === "production") {
};
}

async function saveRemoteFile({ stream }) {
const response = await fetch(
`${process.env.STORAGE_API_HOST}/api/v1/attachments/upload`,
{
method: "POST",
headers: {
"api-key": process.env.STORAGE_API_KEY,
"Content-Type": "multipart/form-data",
},
body: stream,
}
const postgraphileMiddleware = () => {
return postgraphile(
pgPool,
process.env.DATABASE_SCHEMA || "cif",
postgraphileOptions
);
try {
return await response.json();
} catch (e) {
console.error(e);
}
}
};

async function resolveUpload(upload) {
const { createReadStream } = upload;
const stream = createReadStream();
export default postgraphileMiddleware;

// Save tile to remote storage system
const { uuid } = await saveRemoteFile({ stream });
let postgraphileSchemaSingleton: GraphQLSchema;

return uuid;
}
const postgraphileSchema = async () => {
if (!postgraphileSchemaSingleton) {
postgraphileSchemaSingleton = await createPostGraphileSchema(
getDatabaseUrl(),
process.env.DATABASE_SCHEMA || "cif",
postgraphileOptions
);
}

const postgraphileMiddleware = () => {
return postgraphile(pgPool, process.env.DATABASE_SCHEMA || "cif", {
...postgraphileOptions,
graphileBuildOptions: {
...PostgraphileRc.options.graphileBuildOptions,
uploadFieldDefinitions: [
{
match: ({ table, column }) =>
table === "attachment" && column === "file",
resolve: resolveUpload,
},
],
},
pgSettings: (req: Request) => {
const opts = {
...authenticationPgSettings(req),
...generateDatabaseMockOptions(req.cookies, ["mocks.mocked_timestamp"]),
};
return opts;
},
});
return postgraphileSchemaSingleton;
};

export default postgraphileMiddleware;
export async function performQuery(query, variables, request: Request) {
const settings = pgSettings(request);
return withPostGraphileContext(
{
pgPool,
pgSettings: settings,
},
async (context) => {
// Execute your GraphQL query in this function with the provided
// `context` object, which should NOT be used outside of this
// function.
return graphql(
await postgraphileSchema(),
query,
null,
{ ...context }, // You can add more to context if you like
variables
);
}
);
}

0 comments on commit f100f57

Please sign in to comment.