示例#1
0
export function registerWebhookRoutes(app: Express, route: RouterFunction<User>) {
    route({
        path: Paths.api.webhooks.base + "app-uninstalled",
        method: "all",
        requireAuth: false,
        validateShopifyWebhook: true,
        queryValidation: gwv.object<Requests.AppUninstalled>({
            shop_id: gwv.string(),
            shop: gwv.string(),
        }),
        handler: async function (req, res, next) {
            const query: Requests.AppUninstalled = req.query;
            const userSearch = await UserDb.find({
                selector: {
                    shopify_shop_id: {
                        $eq: parseInt(query.shop_id)
                    }
                }
            });

            if (userSearch.length === 0) {
                inspect(`Could not find owner of shop id ${query.shop_id} during app/uninstalled webhook. Returning true to prevent webhook retries.`);

                // No user found with that shopId. This webhook may be a duplicate. Return OK to prevent Shopify resending the webhook.
                res.status(200);

                return next();
            }

            const user = userSearch[0];

            // Shopify access token has already been invalidated at this point. Remove the user's Shopify data.
            user.shopify_access_token = undefined;
            user.shopify_domain = undefined;
            user.shopify_shop_id = undefined;
            user.shopify_shop_name = undefined;
            user.charge_id = undefined;
            user.plan_id = undefined;

            const update = await UserDb.put(user._id, user, user._rev);

            // Add the user's id to the auth-invalidation cache, forcing their next request to prompt them to login again.
            try {
                await Cache.setValue(CACHE_SEGMENT_AUTH, user._id, true, 21 * 1000 * 60 * 60 * 24 /* 21 days in milliseconds */);
            }
            catch (e) {
                inspect("Failed to delete user data from auth cache after handling app/uninstalled webhook.", e);
            }

            res.json({});

            return next();
        }
    })
}
示例#2
0
export function registerSessionRoutes(app: Express, route: RouterFunction<User>) {
    route({
        method: "post",
        path: Paths.api.sessions.base,
        requireAuth: false,
        bodyValidation: gwv.object<Requests.CreateSession>({
            username: gwv.string().required(),
            password: gwv.string().required(),
        }),
        handler: async function (req, res, next) {
            const model: Requests.CreateSession = req.validatedBody;
            let user: User;

            try {
                user = await UserDb.get(model.username.toLowerCase());
            } catch (e) {
                const err: boom.BoomError = e;

                if (err.isBoom && err.output.statusCode !== 404) {
                    return next(e);
                }
            }

            if (!user || !compareSync(model.password, user.hashed_password)) {
                return next(boom.unauthorized("No user found with that username or password combination."));
            }

            // The user has logged in again, so we'll remove their user id from the auth-invalidation cache.
            try {
                await Cache.deleteValue(CACHE_SEGMENT_AUTH, user._id);
            } catch (e) {
                inspect(`Failed to remove user ${user._id} from auth-invalidation cache.`, e);
            }

            res = await res.withSessionToken(user);

            return next();
        }
    });
}
示例#3
0
export default function registerRoutes(app: Express, route: RouterFunction<User>) {
    route({
        method: "post",
        path: Paths.api.users.base,
        requireAuth: false,
        bodyValidation: gwv.object<Requests.CreateOrUpdateAccount>({
            username: gwv.string().email().required(),
            password: gwv.string().min(6).max(100).required(),
        }),
        handler: async function (req, res, next) {
            const model: Requests.CreateOrUpdateAccount = req.validatedBody;

            if (await UserDb.exists(model.username.toLowerCase())) {
                return next(boom.badData(`A user with that username already exists.`));
            }

            const user: User = {
                _id: model.username.toLowerCase(),
                hashed_password: hashSync(model.password),
                date_created: new Date().toISOString(),
            }
            const createResult = await UserDb.post(user);

            await res.withSessionToken({ ...user, _id: createResult.id, _rev: createResult.rev });

            return next();
        }
    });

    route({
        method: "put",
        path: Paths.api.users.base + "username",
        requireAuth: true,
        bodyValidation: gwv.object<Requests.CreateOrUpdateAccount>({
            username: gwv.string().email().required(),
            password: gwv.string().required(),
        }),
        handler: async function (req, res, next) {
            const model: Requests.CreateOrUpdateAccount = req.validatedBody;

            if (await UserDb.exists(model.username.toLowerCase())) {
                return next(boom.badData(`A user with that username already exists.`));
            }

            let user = await UserDb.get(req.user._id);
            const { _id, _rev } = user;

            // Ensure the user's password is correct before changing their username
            if (!compareSync(model.password, user.hashed_password)) {
                return next(boom.forbidden(`Your password is incorrect.`));
            }

            try {
                // CouchDB does not allow modifying a doc's id, so we copy the user to a new document instead.
                const copyResult = await UserDb.copy(_id, model.username.toLowerCase());
                const user = await UserDb.get(copyResult.id);
            } catch (e) {
                inspect("Failed to copy user model to new id.", e);

                return next(boom.badData("Failed to save new user id."));
            }

            try {
                // Delete the old user document
                await UserDb.delete(_id, _rev);
            } catch (e) {
                inspect(`Failed to delete user doc ${_id} after changing username to ${model.username}`, e);
            }

            await res.withSessionToken(user);

            return next();
        }
    })

    route({
        method: "post",
        path: Paths.api.users.base + "password/forgot",
        requireAuth: false,
        bodyValidation: gwv.object<Requests.ForgotPassword>({
            username: gwv.string().email().required()
        }),
        handler: async function (req, res, next) {
            const model: Requests.ForgotPassword = req.validatedBody;
            let user: User;

            if (!UserDb.exists(model.username.toLowerCase())) {
                // Do not let the client know that the username does not exist.

                return next();
            }

            const token = await seal({
                exp: Date.now() + ((1000 * 60) * 90), // 90 minutes in milliseconds
                username: model.username,
            } as ResetToken, Constants.IRON_PASSWORD);

            const url = `${req.domainWithProtocol}/auth/reset-password?token=${encodeURIComponent(token)}`;
            const message = {
                content: {
                    from: {
                        name: "Support",
                        email: `support@${Constants.EMAIL_DOMAIN}`,
                    },
                    subject: `[${Constants.APP_NAME}] Reset your password.`,
                    html: `<p>Hello,</p><p>You recently requested to reset your password for ${Constants.APP_NAME}. Please click the link below to reset your password.</p><p><a href='${url}'>Click here to reset your password.</a></p><p>If you did not request a password reset, please ignore this email or reply to let us know. This password reset is only valid for the next 90 minutes.</p><p>Thanks, <br/> The ${Constants.APP_NAME} Team</p>`
                },
                recipients: [{
                    address: {
                        email: model.username,
                    }
                }]
            }

            //Send the password reset email
            const transporter = createTransport({ transport: 'sparkpost', sparkPostApiKey: Constants.SPARKPOST_API_KEY } as any);

            transporter.sendMail(message as any, (error, info) => {
                if (error) {
                    return next(boom.wrap(error));
                };

                res.json({});

                return next();
            });
        }
    })

    route({
        method: "post",
        path: Paths.api.users.base + "password/reset",
        requireAuth: false,
        bodyValidation: gwv.object<Requests.ResetPassword>({
            new_password: gwv.string().min(6).max(100).required(),
            reset_token: gwv.string().required(),
        }),
        handler: async function (req, res, next) {
            const payload: Requests.ResetPassword = req.validatedBody;
            const token = await unseal<ResetToken>(payload.reset_token, Constants.IRON_PASSWORD);

            if (token.exp < Date.now()) {
                // Token has expired
                return next(boom.unauthorized("Token has expired."));
            }

            const user = await UserDb.get(token.username.toLowerCase());
            user.hashed_password = hashSync(payload.new_password);
            const updateResult = await UserDb.put(user._id, user, user._rev);

            res.json({});

            return next();
        }
    })

    route({
        method: "put",
        path: Paths.api.users.base + "password",
        requireAuth: true,
        bodyValidation: gwv.object<Requests.UpdatePassword>({
            old_password: gwv.string().required(),
            new_password: gwv.string().min(6).max(100).required()
        }),
        handler: async function (req, res, next) {
            const payload: Requests.UpdatePassword = req.validatedBody;
            let user = await UserDb.get(req.user._id);

            // Ensure the user's current password is correct
            if (!compareSync(payload.old_password, user.hashed_password)) {
                return next(boom.forbidden(`Your current password is incorrect.`));
            }

            // Change the user's password
            user.hashed_password = hashSync(payload.new_password);

            try {
                const updateResult = await UserDb.put(user._id, user, user._rev);
                user._rev = updateResult.rev
            } catch (e) {
                inspect("Failed to update user's password.", e);

                return next(e);
            }

            await res.withSessionToken(user);

            return next();
        }
    })
}
示例#4
0
export function registerShopifyRoutes(app: Express, route: RouterFunction<User>) {
    route({
        method: "get",
        path: Paths.api.shopify.base + "url",
        requireAuth: true,
        queryValidation: gwv.object<Requests.GetOauthUrl>({
            shop_domain: gwv.string().required(),
            redirect_url: gwv.string().required(),
        }).unknown(true),
        handler: async function (req, res, next) {
            const query: Requests.GetOauthUrl = req.validatedQuery;
            const isValidUrl = await Auth.isValidShopifyDomain(req.validatedQuery.shop_domain);

            if (!isValidUrl) {
                return next(boom.notAcceptable(`${query.shop_domain} is not a valid Shopify shop domain.`));
            }

            const authUrl = await Auth.buildAuthorizationUrl(Constants.DEFAULT_SCOPES, req.validatedQuery.shop_domain, Constants.SHOPIFY_API_KEY, query.redirect_url);

            res.json({ url: authUrl });

            return next();
        }
    });

    route({
        method: "post",
        path: Paths.api.shopify.base + "authorize",
        requireAuth: true,
        validateShopifyRequest: true,
        bodyValidation: gwv.object<Requests.Authorize>({
            code: gwv.string().required(),
            shop: gwv.string().required(),
            hmac: gwv.string().required(),
            state: gwv.string()
        }).unknown(true),
        handler: async function (req, res, next) {
            const model: Requests.Authorize = req.validatedBody;
            let user: User;

            try {
                user = await UserDb.get(req.user._id);
            } catch (e) {
                inspect(`Error getting user ${req.user._id} from database.`, e);

                return next(e);
            }

            const accessToken = await Auth.authorize(model.code, model.shop, Constants.SHOPIFY_API_KEY, Constants.SHOPIFY_SECRET_KEY);

            // Store the user's shop data
            user.shopify_domain = model.shop;
            user.shopify_access_token = accessToken;
            user.permissions = Constants.DEFAULT_SCOPES;

            try {
                const shop = await new Shops(model.shop, accessToken).get({ fields: "name,id" });

                user.shopify_shop_name = shop.name;
                user.shopify_shop_id = shop.id;
            } catch (e) {
                inspect(`Failed to get shop data from ${model.shop}`, e);
            }

            try {
                const updateResult = await UserDb.put(user._id, user, user._rev);
                user._rev = updateResult.rev;
            } catch (e) {
                inspect(`Failed to update user ${user._id}'s Shopify access token`, e);

                return next(e);
            }

            await res.withSessionToken(user);

            // Don't create any webhooks unless this app is running on a real domain. Webhooks cannot point to localhost.
            if (Constants.ISLIVE) {
                // Create the AppUninstalled webhook immediately after the user accepts installation
                const webhooks = new Webhooks(model.shop, accessToken);
                const existingHooks = await webhooks.list({ topic: "app/uninstalled", fields: "id", limit: 1 });

                // Ensure the webhook doesn't already exist
                if (existingHooks.length === 0) {
                    const hook = await webhooks.create({
                        address: req.domainWithProtocol + Paths.api.webhooks.base + `app-uninstalled?shop_id=${user.shopify_shop_id}`,
                        topic: "app/uninstalled"
                    })
                }
            }

            return next();
        }
    })

    route({
        method: "get",
        path: Paths.api.shopify.base + "orders",
        requireAuth: true,
        queryValidation: gwv.object<Requests.ListOrders>({
            limit: gwv.number().default(50),
            page: gwv.gt(0).default(1),
            status: gwv.onlyStrings("any"),
        }).unknown(true),
        handler: async function (req, res, next) {
            const service = new Orders(req.user.shopify_domain, req.user.shopify_access_token);
            const orders = await service.list(req.validatedQuery);

            res.json(orders);

            return next();
        }
    })

    route({
        method: "post",
        path: Paths.api.shopify.base + "orders",
        requireAuth: true,
        bodyValidation: gwv.object<Requests.CreateOrder>({
            city: gwv.string().required(),
            email: gwv.string().required(),
            line_item: gwv.string().required(),
            name: gwv.string().required(),
            quantity: gwv.number().required(),
            state: gwv.string().required(),
            street: gwv.string().required(),
            zip: gwv.string().required(),
        }),
        handler: async function (req, res, next) {
            const model: Requests.CreateOrder = req.validatedBody;
            const service = new Orders(req.user.shopify_domain, req.user.shopify_access_token);
            const order = await service.create({
                billing_address: {
                    address1: model.street,
                    city: model.city,
                    province: model.state,
                    zip: model.zip,
                    name: model.name,
                    country_code: "US",
                    default: true,
                },
                line_items: [
                    {
                        name: model.line_item,
                        title: model.line_item,
                        quantity: model.quantity,
                        price: 5,
                    },
                ],
                financial_status: "authorized",
                email: model.email,
            });

            res.json(order);

            return next();
        }
    })

    route({
        method: "post",
        path: Paths.api.shopify.base + "orders/:id/open",
        requireAuth: true,
        paramValidation: gwv.object<Requests.OpenCloseDelete>({
            id: gwv.number().required()
        }),
        handler: async function (req, res, next) {
            const params: Requests.OpenCloseDelete = req.validatedParams;
            const service = new Orders(req.user.shopify_domain, req.user.shopify_access_token);
            const order = await service.open(params.id);

            res.json(order);

            return next();
        }
    })

    route({
        method: "post",
        path: Paths.api.shopify.base + "orders/:id/close",
        requireAuth: true,
        paramValidation: gwv.object<Requests.OpenCloseDelete>({
            id: gwv.number().required()
        }),
        handler: async function (req, res, next) {
            const params: Requests.OpenCloseDelete = req.validatedParams;
            const service = new Orders(req.user.shopify_domain, req.user.shopify_access_token);
            const order = await service.close(params.id);

            res.json(order);

            return next();
        }
    })

    route({
        method: "delete",
        path: Paths.api.shopify.base + "orders/:id",
        requireAuth: true,
        paramValidation: gwv.object<Requests.OpenCloseDelete>({
            id: gwv.number().required()
        }),
        handler: async function (req, res, next) {
            const params: Requests.OpenCloseDelete = req.validatedParams;
            const service = new Orders(req.user.shopify_domain, req.user.shopify_access_token);

            await service.delete(params.id);

            res.json({});

            return next();
        }
    })
}