Hey hey, logging out works! The server side of this was easy, but I made a few refactors to support it well on the client, like `useLoginActions` becoming just `useLogout` lol, and updating how the nav menu chooses between buttons vs menu because I wanted `<LogoutButton />` to contain some state. We also did good Apollo cache stuff to update the page after you log in or out! I think some fields that don't derive from `User`, like `Item.currentUserOwnsThis`, are gonna fail to update until you reload the page but like that's fine idk :p There's a known bug where logging out on the Your Outfits page turns into an infinite loop situation, because it's trying to do Auth0 stuff but the login keeps failing to have any effect because we're in db mode! I'll fix that next.
165 lines
4.8 KiB
JavaScript
165 lines
4.8 KiB
JavaScript
import { beelinePlugin } from "./lib/beeline-graphql";
|
|
import { gql, makeExecutableSchema } from "apollo-server";
|
|
import { getUserIdFromToken as getUserIdFromTokenViaAuth0 } from "./auth";
|
|
import connectToDb from "./db";
|
|
import buildLoaders from "./loaders";
|
|
import { plugin as cacheControlPluginFork } from "./lib/apollo-cache-control-fork";
|
|
import {
|
|
getAuthToken,
|
|
getUserIdFromToken as getUserIdFromTokenViaDb,
|
|
} from "./auth-by-db";
|
|
|
|
const rootTypeDefs = gql`
|
|
enum CacheScope {
|
|
PUBLIC
|
|
PRIVATE
|
|
}
|
|
directive @cacheControl(
|
|
maxAge: Int
|
|
staleWhileRevalidate: Int
|
|
scope: CacheScope
|
|
) on FIELD_DEFINITION | OBJECT
|
|
|
|
type Mutation
|
|
type Query
|
|
`;
|
|
|
|
function mergeTypeDefsAndResolvers(modules) {
|
|
const allTypeDefs = [];
|
|
const allResolvers = {};
|
|
|
|
for (const { typeDefs, resolvers } of modules) {
|
|
allTypeDefs.push(typeDefs);
|
|
for (const typeName of Object.keys(resolvers)) {
|
|
allResolvers[typeName] = {
|
|
...allResolvers[typeName],
|
|
...resolvers[typeName],
|
|
};
|
|
}
|
|
}
|
|
|
|
return { typeDefs: allTypeDefs, resolvers: allResolvers };
|
|
}
|
|
|
|
const schema = makeExecutableSchema(
|
|
mergeTypeDefsAndResolvers([
|
|
{ typeDefs: rootTypeDefs, resolvers: {} },
|
|
require("./types/AppearanceLayer"),
|
|
require("./types/ClosetList"),
|
|
require("./types/Item"),
|
|
require("./types/MutationsForSupport"),
|
|
require("./types/Outfit"),
|
|
require("./types/Pet"),
|
|
require("./types/PetAppearance"),
|
|
require("./types/User"),
|
|
require("./types/Zone"),
|
|
])
|
|
);
|
|
|
|
const plugins = [cacheControlPluginFork({ calculateHttpHeaders: true })];
|
|
|
|
if (process.env["NODE_ENV"] !== "test") {
|
|
plugins.push(beelinePlugin);
|
|
}
|
|
|
|
const config = {
|
|
schema,
|
|
context: async ({ req, res }) => {
|
|
const db = await connectToDb();
|
|
|
|
let authMode = req.headers["dti-auth-mode"] || "auth0";
|
|
let currentUserId;
|
|
if (authMode === "auth0") {
|
|
const auth = (req && req.headers && req.headers.authorization) || "";
|
|
const authMatch = auth.match(/^Bearer (.+)$/);
|
|
const token = authMatch && authMatch[1];
|
|
currentUserId = await getUserIdFromTokenViaAuth0(token);
|
|
} else if (authMode === "db") {
|
|
currentUserId = await getCurrentUserIdViaDb(req);
|
|
} else {
|
|
console.warn(
|
|
`Unexpected auth mode: ${JSON.stringify(authMode)}. Skipping auth.`
|
|
);
|
|
currentUserId = null;
|
|
}
|
|
|
|
return {
|
|
db,
|
|
currentUserId,
|
|
login: async (params) => {
|
|
const authToken = await getAuthToken(params, db);
|
|
if (authToken == null) {
|
|
return null;
|
|
}
|
|
const oneWeekFromNow = new Date();
|
|
oneWeekFromNow.setDate(oneWeekFromNow.getDate() + 7);
|
|
res.setHeader(
|
|
"Set-Cookie",
|
|
`DTIAuthToken=${encodeURIComponent(JSON.stringify(authToken))}; ` +
|
|
`Max-Age=${60 * 60 * 24 * 7}; Secure; HttpOnly; SameSite=Strict`
|
|
);
|
|
return authToken;
|
|
},
|
|
logout: async () => {
|
|
// NOTE: This function isn't actually async in practice, but we mark it
|
|
// as such for consistency with `login`!
|
|
// Set a header to delete the cookie. (That is, empty and expired.)
|
|
res.setHeader("Set-Cookie", `DTIAuthToken=; Max-Age=-1`);
|
|
},
|
|
...buildLoaders(db),
|
|
};
|
|
},
|
|
formatResponse: (res, context) => {
|
|
// The Authorization header can affect the response, so we signal that here
|
|
// for caching user data! That way, login/logout will refresh user data,
|
|
// even if it was briefly cached.
|
|
//
|
|
// NOTE: Our frontend JS only sends the Authorization header for user data
|
|
// queries. For public data, the header will be absent, and different
|
|
// users will still be able to share the same public cache data.
|
|
//
|
|
// NOTE: At time of writing, I'm not sure we use this in app? I think all
|
|
// current user data queries request fields with `maxAge: 0`. But I'm
|
|
// adding it just to remove a potential surprise gotcha later!
|
|
context.response.http.headers.set("Vary", "Authorization");
|
|
|
|
return res;
|
|
},
|
|
|
|
plugins,
|
|
|
|
// We use our own fork of the cacheControl plugin!
|
|
cacheControl: false,
|
|
|
|
// Enable Playground in production :)
|
|
introspection: true,
|
|
playground: {
|
|
endpoint: "/api/graphql",
|
|
},
|
|
};
|
|
|
|
async function getCurrentUserIdViaDb(req) {
|
|
const authTokenCookieString = req.cookies.DTIAuthToken;
|
|
if (!authTokenCookieString) {
|
|
return null;
|
|
}
|
|
|
|
let authTokenFromCookie = null;
|
|
try {
|
|
authTokenFromCookie = JSON.parse(authTokenCookieString);
|
|
} catch (error) {
|
|
console.warn(`DTIAuthToken cookie was not valid JSON, ignoring.`);
|
|
}
|
|
|
|
return await getUserIdFromTokenViaDb(authTokenFromCookie);
|
|
}
|
|
|
|
if (require.main === module) {
|
|
const { ApolloServer } = require("apollo-server");
|
|
const server = new ApolloServer(config);
|
|
server.listen().then(({ url }) => {
|
|
console.info(`🚀 Server ready at ${url}`);
|
|
});
|
|
}
|
|
|
|
module.exports = { config };
|