Skip to content

Commit

Permalink
init auth related code
Browse files Browse the repository at this point in the history
  • Loading branch information
eric-burel committed Jan 8, 2021
1 parent 6e7e639 commit 04c7964
Show file tree
Hide file tree
Showing 11 changed files with 804 additions and 6 deletions.
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,15 @@
},
"dependencies": {
"@apollo/client": "^3.2.0",
"@hapi/iron": "6.0.0",
"@material-ui/core": "^4.10.2",
"@mdx-js/loader": "^1.6.6",
"@mdx-js/react": "^1.6.13",
"@next/mdx": "^10.0.2",
"@vulcanjs/demo": "^0.0.7",
"@vulcanjs/mdx": "^0.0.7",
"@vulcanjs/mongo": "^0.1.8",
"@vulcanjs/react-hooks": "^0.1.8",
"apollo-server-express": "2.14.2",
"babel-jest": "26.0.1",
"babel-plugin-istanbul": "6.0.0",
Expand All @@ -71,6 +74,7 @@
"next": "^10.0.2",
"next-i18next": "^5.1.0",
"next-mdx-enhanced": "^4.0.0",
"passport-local": "1.0.0",
"polished": "^3.6.5",
"postcss-nested": "^4.2.1",
"react": "^17.0.1",
Expand Down
82 changes: 82 additions & 0 deletions src/api/context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/**
* Context creation, for graphql but also REST endpoints
*/
import { Connector, VulcanGraphqlModel } from "@vulcanjs/graphql";

import { createMongooseConnector } from "@vulcanjs/mongo";
import { User, UserConnector, UserType } from "~/models/user";
import { NextApiRequest } from "next";
import { getSession } from "./passport/iron";
import { Request } from "express";
import debug from "debug";
import models from "~/models";
const debugGraphqlContext = debug("vn:graphql:context");

/**
const models = [Tweek, Twaik];
* Expected shape of the context
* {
* "Foo": {
* model: Foo,
* connector: FooConnector
* }
* }
*/
interface ModelContext {
[typeName: string]: { model: VulcanGraphqlModel; connector: Connector };
}
/**
* Build a default graphql context for a list of models
* @param models
*/
const createContextForModels = (
models: Array<VulcanGraphqlModel>
): ModelContext => {
return models.reduce(
(context, model: VulcanGraphqlModel) => ({
...context,
[model.name]: {
model,
connector: createMongooseConnector(model),
},
}),
{}
);
};

// TODO: isolate context creation code like we do in Vulcan + initialize the currentUser too
export const contextBase = {
...createContextForModels(models),
// add some custom context here
[User.graphql.typeName]: {
model: User,
connector: UserConnector, // we use the premade connector
},
};

interface UserContext {
userId?: string;
currentUser?: UserType;
}
const userContextFromReq = async (
req: Request | NextApiRequest
): Promise<UserContext> => {
const session = await getSession(req);
if (!session) return {};
// Refetch the user from db in order to get the freshest data
const user = await UserConnector.findOneById(session._id);
if (user) {
return { userId: user._id, currentUser: user };
}
return {};
};
export const contextFromReq = async (req: Request) => {
// TODO
const userContext = await userContextFromReq(req);
const context = {
...contextBase,
...userContext,
};
debugGraphqlContext("Graphql context for current request:", context);
return context;
};
40 changes: 40 additions & 0 deletions src/api/passport/auth-cookies.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { serialize, parse } from "cookie";

export const TOKEN_NAME = "token";
const MAX_AGE = 60 * 60 * 8; // 8 hours

export function setTokenCookie(res, token) {
const cookie = serialize(TOKEN_NAME, token, {
maxAge: MAX_AGE,
expires: new Date(Date.now() + MAX_AGE * 1000),
httpOnly: true,
secure: process.env.NODE_ENV === "production",
path: "/",
sameSite: "lax",
});

res.setHeader("Set-Cookie", cookie);
}

export function removeTokenCookie(res) {
const cookie = serialize(TOKEN_NAME, "", {
maxAge: -1,
path: "/",
});

res.setHeader("Set-Cookie", cookie);
}

export function parseCookies(req) {
// For API Routes we don't need to parse the cookies.
if (req.cookies) return req.cookies;

// For pages we do need to parse the cookies.
const cookie = req.headers?.cookie;
return parse(cookie || "");
}

export function getTokenCookie(req) {
const cookies = parseCookies(req);
return cookies[TOKEN_NAME];
}
25 changes: 25 additions & 0 deletions src/api/passport/iron.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// For the token
// https://hapi.dev/module/iron/
import Iron from "@hapi/iron";
import { Request } from "express";
import { NextApiRequest } from "next";
import { UserType } from "~/models/user";
import { getTokenCookie } from "./auth-cookies";

// Use an environment variable here instead of a hardcoded value for production
const TOKEN_SECRET = "this-is-a-secret-value-with-at-least-32-characters";

export function encryptSession(session: UserType) {
return Iron.seal(session, TOKEN_SECRET, Iron.defaults);
}

/**
* Returns the user data from the token
* @param req
*/
export async function getSession(
req: NextApiRequest | Request
): Promise<UserType> {
const token = getTokenCookie(req);
return token && Iron.unseal(token, TOKEN_SECRET, Iron.defaults);
}
21 changes: 21 additions & 0 deletions src/api/passport/password-local.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
//@see http://www.passportjs.org/packages/passport-local/
import Local from "passport-local";
import { findUserByCredentials } from "~/models/user";

export const localStrategy = new Local.Strategy(function (
email,
password,
done
) {
findUserByCredentials({ email, password })
.then((user) => {
if (!user) {
done(new Error("Email/password not matching"));
} else {
done(null, user);
}
})
.catch((error) => {
done(error);
});
});
37 changes: 37 additions & 0 deletions src/api/seed.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { createMutator, getModelConnector } from "@vulcanjs/graphql";
import { User } from "~/models/user";

const seed = (context) => {
// Add your seed functions here based on the example of users
const UserConnector = getModelConnector(context, User);

const seedAdminUser = async () => {
const count = await UserConnector.count({ isAdmin: true });
if (count === 0) {
console.log("No admin user found, seeding admin");
const admin = {
email: process.env.ADMIN_EMAIL,
password: process.env.ADMIN_INITIAL_PASSWORD,
isAdmin: true,
};
try {
await createMutator({
model: User,
data: admin,
context,
asAdmin: true,
validate: false,
});
} catch (error) {
console.error("Could not seed admin user", error);
}
} else {
console.log(`Found ${count} Admin(s) in the database, no need to seed.`);
}
};

// Run the seed functions
seedAdminUser();
};

export default seed;
4 changes: 4 additions & 0 deletions src/models/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// Export all your models here
// Please do not remove the User model, which is necessary for auth
import { User } from "./user";
export default [User];
Loading

0 comments on commit 04c7964

Please sign in to comment.