feat: Signup query params tracking (#2098)
* feat: Add tracking of signup query params * fix: Headers already sent to client * fix: OAuth error wipes previously written query params cookie
This commit is contained in:
@ -5,6 +5,7 @@ import { BackIcon, EmailIcon } from "outline-icons";
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { Redirect, Link, type Location } from "react-router-dom";
|
import { Redirect, Link, type Location } from "react-router-dom";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
|
import { setCookie } from "tiny-cookie";
|
||||||
import ButtonLarge from "components/ButtonLarge";
|
import ButtonLarge from "components/ButtonLarge";
|
||||||
import Fade from "components/Fade";
|
import Fade from "components/Fade";
|
||||||
import Flex from "components/Flex";
|
import Flex from "components/Flex";
|
||||||
@ -16,6 +17,7 @@ import TeamLogo from "components/TeamLogo";
|
|||||||
import Notices from "./Notices";
|
import Notices from "./Notices";
|
||||||
import Provider from "./Provider";
|
import Provider from "./Provider";
|
||||||
import env from "env";
|
import env from "env";
|
||||||
|
import useQuery from "hooks/useQuery";
|
||||||
import useStores from "hooks/useStores";
|
import useStores from "hooks/useStores";
|
||||||
|
|
||||||
type Props = {|
|
type Props = {|
|
||||||
@ -23,6 +25,7 @@ type Props = {|
|
|||||||
|};
|
|};
|
||||||
|
|
||||||
function Login({ location }: Props) {
|
function Login({ location }: Props) {
|
||||||
|
const query = useQuery();
|
||||||
const { auth } = useStores();
|
const { auth } = useStores();
|
||||||
const { config } = auth;
|
const { config } = auth;
|
||||||
const [emailLinkSentTo, setEmailLinkSentTo] = React.useState("");
|
const [emailLinkSentTo, setEmailLinkSentTo] = React.useState("");
|
||||||
@ -40,6 +43,17 @@ function Login({ location }: Props) {
|
|||||||
auth.fetchConfig();
|
auth.fetchConfig();
|
||||||
}, [auth]);
|
}, [auth]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const entries = Object.fromEntries(query.entries());
|
||||||
|
|
||||||
|
// We don't want to override this cookie if we're viewing an error notice
|
||||||
|
// sent back from the server via query string (notice=), or if there are no
|
||||||
|
// query params at all.
|
||||||
|
if (Object.keys(entries).length && !query.get("notice")) {
|
||||||
|
setCookie("signupQueryParams", JSON.stringify(entries));
|
||||||
|
}
|
||||||
|
}, [query]);
|
||||||
|
|
||||||
if (auth.authenticated) {
|
if (auth.authenticated) {
|
||||||
return <Redirect to="/home" />;
|
return <Redirect to="/home" />;
|
||||||
}
|
}
|
||||||
|
@ -101,7 +101,7 @@ router.get("email.callback", async (ctx) => {
|
|||||||
await user.update({ lastActiveAt: new Date() });
|
await user.update({ lastActiveAt: new Date() });
|
||||||
|
|
||||||
// set cookies on response and redirect to team subdomain
|
// set cookies on response and redirect to team subdomain
|
||||||
signIn(ctx, user, user.team, "email", false);
|
await signIn(ctx, user, user.team, "email", false, false);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
ctx.redirect(`/?notice=expired-token`);
|
ctx.redirect(`/?notice=expired-token`);
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,7 @@ export default function createMiddleware(providerName: string) {
|
|||||||
return passport.authorize(
|
return passport.authorize(
|
||||||
providerName,
|
providerName,
|
||||||
{ session: false },
|
{ session: false },
|
||||||
(err, _, result: AccountProvisionerResult) => {
|
async (err, _, result: AccountProvisionerResult) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
|
||||||
@ -39,7 +39,14 @@ export default function createMiddleware(providerName: string) {
|
|||||||
return ctx.redirect("/?notice=suspended");
|
return ctx.redirect("/?notice=suspended");
|
||||||
}
|
}
|
||||||
|
|
||||||
signIn(ctx, result.user, result.team, providerName, result.isNewUser);
|
await signIn(
|
||||||
|
ctx,
|
||||||
|
result.user,
|
||||||
|
result.team,
|
||||||
|
providerName,
|
||||||
|
result.isNewUser,
|
||||||
|
result.isNewTeam
|
||||||
|
);
|
||||||
}
|
}
|
||||||
)(ctx);
|
)(ctx);
|
||||||
};
|
};
|
||||||
|
14
server/migrations/20210430024222-marketing-tracking.js
Normal file
14
server/migrations/20210430024222-marketing-tracking.js
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
up: async (queryInterface, Sequelize) => {
|
||||||
|
await queryInterface.addColumn("teams", "signupQueryParams", {
|
||||||
|
type: Sequelize.JSONB,
|
||||||
|
allowNull: true,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
down: async (queryInterface, Sequelize) => {
|
||||||
|
await queryInterface.removeColumn("teams", "signupQueryParams");
|
||||||
|
}
|
||||||
|
};
|
@ -55,6 +55,10 @@ const Team = sequelize.define(
|
|||||||
googleId: { type: DataTypes.STRING, allowNull: true },
|
googleId: { type: DataTypes.STRING, allowNull: true },
|
||||||
avatarUrl: { type: DataTypes.STRING, allowNull: true },
|
avatarUrl: { type: DataTypes.STRING, allowNull: true },
|
||||||
sharing: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: true },
|
sharing: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: true },
|
||||||
|
signupQueryParams: {
|
||||||
|
type: DataTypes.JSONB,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
guestSignin: {
|
guestSignin: {
|
||||||
type: DataTypes.BOOLEAN,
|
type: DataTypes.BOOLEAN,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
// @flow
|
// @flow
|
||||||
|
import querystring from "querystring";
|
||||||
|
import * as Sentry from "@sentry/node";
|
||||||
import addMonths from "date-fns/add_months";
|
import addMonths from "date-fns/add_months";
|
||||||
import { type Context } from "koa";
|
import { type Context } from "koa";
|
||||||
|
import { pick } from "lodash";
|
||||||
import { User, Event, Team } from "../models";
|
import { User, Event, Team } from "../models";
|
||||||
import { getCookieDomain } from "../utils/domains";
|
import { getCookieDomain } from "../utils/domains";
|
||||||
|
|
||||||
@ -10,17 +13,36 @@ export function getAllowedDomains(): string[] {
|
|||||||
return env ? env.split(",") : [];
|
return env ? env.split(",") : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function signIn(
|
export async function signIn(
|
||||||
ctx: Context,
|
ctx: Context,
|
||||||
user: User,
|
user: User,
|
||||||
team: Team,
|
team: Team,
|
||||||
service: string,
|
service: string,
|
||||||
isFirstSignin: boolean = false
|
isNewUser: boolean = false,
|
||||||
|
isNewTeam: boolean = false
|
||||||
) {
|
) {
|
||||||
if (user.isSuspended) {
|
if (user.isSuspended) {
|
||||||
return ctx.redirect("/?notice=suspended");
|
return ctx.redirect("/?notice=suspended");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isNewTeam) {
|
||||||
|
// see: scenes/Login/index.js for where this cookie is written when
|
||||||
|
// viewing the /login or /create pages. It is a URI encoded JSON string.
|
||||||
|
const cookie = ctx.cookies.get("signupQueryParams");
|
||||||
|
|
||||||
|
if (cookie) {
|
||||||
|
try {
|
||||||
|
const signupQueryParams = pick(
|
||||||
|
JSON.parse(querystring.unescape(cookie)),
|
||||||
|
["ref", "utm_content", "utm_medium", "utm_source", "utm_campaign"]
|
||||||
|
);
|
||||||
|
await team.update({ signupQueryParams });
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.captureException(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// update the database when the user last signed in
|
// update the database when the user last signed in
|
||||||
user.updateSignedIn(ctx.request.ip);
|
user.updateSignedIn(ctx.request.ip);
|
||||||
|
|
||||||
@ -77,6 +99,6 @@ export function signIn(
|
|||||||
httpOnly: false,
|
httpOnly: false,
|
||||||
expires,
|
expires,
|
||||||
});
|
});
|
||||||
ctx.redirect(`${team.url}/home${isFirstSignin ? "?welcome" : ""}`);
|
ctx.redirect(`${team.url}/home${isNewUser ? "?welcome" : ""}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user