feat: signup enhancements; more extensions to schema

This commit is contained in:
Wyatt Johnson
2018-07-10 15:54:52 -06:00
parent d46145f04c
commit 6efe36ceaa
16 changed files with 278 additions and 63 deletions
+79 -3
View File
@@ -1,8 +1,84 @@
import { RequestHandler } from "express";
import Joi from "joi";
import { Db } from "mongodb";
import { handle } from "talk-server/app/middleware/passport";
import { validate } from "talk-server/app/request/body";
import { GQLUSER_ROLE } from "talk-server/graph/tenant/schema/__generated__/types";
import { LocalProfile } from "talk-server/models/user";
import { create } from "talk-server/services/users";
import { Request } from "talk-server/types/express";
export const signup: RequestHandler = async (req: Request, res, next) => {
// TODO: implement
res.send("ok");
export interface SignupBody {
username: string;
password: string;
email: string;
displayName?: string;
}
const SignupBodySchema = Joi.object().keys({
username: Joi.string().trim(),
password: Joi.string().trim(),
email: Joi.string().trim(),
});
// Extends the default signup body schema with the displayName to allow it to be
// sent.
const SignupDisplayNameBodySchema = SignupBodySchema.keys({
displayName: Joi.string().trim(),
});
export interface SignupOptions {
db: Db;
}
export const signup = ({ db }: SignupOptions): RequestHandler => async (
req: Request,
res,
next
) => {
try {
// TODO: rate limit based on the IP address and user agent.
// Tenant is guaranteed at this point.
const tenant = req.tenant!;
if (!tenant.auth.integrations.local.enabled) {
// TODO: replace with better error.
return next(new Error("integration is disabled"));
}
// Get the fields from the body. We condition on the display name being
// enabled to allow the display name to be stripped in the event that the
// display name is not enabled, yielding a displayName being `undefined`,
// which will not be set in the resultant document. Validate will throw an
// error if the body does not conform to the specification.
const { username, password, email, displayName }: SignupBody = validate(
tenant.auth.displayNameEnable
? SignupDisplayNameBodySchema
: SignupBodySchema,
req.body
);
// Configure with profile.
const profile: LocalProfile = {
id: email,
type: "local",
};
// Create the new user.
const user = await create(db, tenant.id, {
email,
username,
displayName,
password,
profiles: [profile],
role: GQLUSER_ROLE.COMMENTER,
});
// Send off to the passport handler.
return handle(null, user)(req, res, next);
} catch (err) {
return handle(err)(req, res, next);
}
};
@@ -34,13 +34,18 @@ export function createPassport({
export const handle = (
err: Error | null,
user: User | null
user?: User | null
): RequestHandler => (req: Request, res, next) => {
if (err) {
// TODO: wrap error?
return next(err);
}
if (!user) {
// TODO: replace with better error.
return next(new Error("no user on request"));
}
// Set the cache control headers.
res.header("Cache-Control", "private, no-cache, no-store, must-revalidate");
res.header("Expires", "-1");
@@ -15,11 +15,11 @@ const verifyFactory = (db: Db) => async (
done: VerifyCallback
) => {
try {
// TODO: rate limit based on the IP address and user agent.
// The tenant is guaranteed at this point.
const tenant = req.tenant!;
// TODO: rate limit the ip address
// Get the user from the database.
const user = await retrieveUserWithProfile(db, tenant.id, {
id: email,
@@ -7,7 +7,8 @@ import { Strategy } from "passport-strategy";
import { reconstructURL } from "talk-server/app/url";
import { GQLUSER_ROLE } from "talk-server/graph/tenant/schema/__generated__/types";
import { OIDCAuthIntegration, Tenant } from "talk-server/models/tenant";
import { createUser, retrieveUserWithProfile } from "talk-server/models/user";
import { OIDCProfile, retrieveUserWithProfile } from "talk-server/models/user";
import { create } from "talk-server/services/users";
import { Request } from "talk-server/types/express";
import { VerifyCallback } from "./index";
@@ -50,7 +51,7 @@ export async function findOrCreateOIDCUser(
{ iss, sub, email, email_verified }: OIDCIDToken
) {
// Construct the profile that will be used to query for the user.
const profile = {
const profile: OIDCProfile = {
type: "oidc",
provider: iss,
id: sub,
@@ -62,7 +63,7 @@ export async function findOrCreateOIDCUser(
// FIXME: implement rules.
// Create the new user, as one didn't exist before!
user = await createUser(db, tenant.id, {
user = await create(db, tenant.id, {
username: null,
role: GQLUSER_ROLE.COMMENTER,
email,
@@ -157,7 +158,11 @@ export default class OIDCStrategy extends Strategy {
const { tenant } = req;
// Grab the JWKSClient.
const client = this.lookupJWKSClient(req, tenant!.id, tenant!.auth.oidc!);
const client = this.lookupJWKSClient(
req,
tenant!.id,
tenant!.auth.integrations.oidc!
);
// Verify that the id_token is valid or not.
jwt.verify(
@@ -180,7 +185,7 @@ export default class OIDCStrategy extends Strategy {
});
},
{
issuer: tenant!.auth.oidc!.issuer,
issuer: tenant!.auth.integrations.oidc!.issuer,
},
(err, decoded) => {
if (err) {
@@ -226,7 +231,7 @@ export default class OIDCStrategy extends Strategy {
// Get the integration from the tenant. If needed, it will be used to create
// a new strategy.
const integration = tenant.auth.oidc;
const integration = tenant.auth.integrations.oidc;
if (!integration) {
// TODO: return a better error.
throw new Error("integration not found");
+23
View File
@@ -0,0 +1,23 @@
import Joi from "joi";
/**
* validate will strip unknown fields and perform validation against it. It will
* throw any error encountered.
*
* @param schema the Joi schema to validate against
* @param body the body to parse and strip of unknown fields
*/
export const validate = (schema: Joi.SchemaLike, body: any) => {
// Extract the schema from the request.
const { value, error: err } = Joi.validate(body, schema, {
stripUnknown: true,
presence: "required",
});
if (err) {
// TODO: return better error.
throw new Error("Validation Error");
}
return value;
};
+1 -1
View File
@@ -39,7 +39,7 @@ async function createTenantRouter(app: AppOptions, options: RouterOptions) {
express.json(),
authenticate(options.passport, "local")
);
router.use("/auth/local/signup", express.json(), signup);
router.use("/auth/local/signup", express.json(), signup({ db: app.mongo }));
router.use("/auth/oidc", authenticate(options.passport, "oidc"));
router.use("/auth/oidc/callback", authenticate(options.passport, "oidc"));
// router.use("/auth/google", options.passport.authenticate("google"));