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:
Tom Moor
2021-05-01 13:46:08 -07:00
committed by GitHub
parent 4d68a34897
commit 77d6adb73b
6 changed files with 67 additions and 6 deletions

View File

@ -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" />;
} }

View File

@ -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`);
} }

View File

@ -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);
}; };

View 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");
}
};

View File

@ -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,

View File

@ -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" : ""}`);
} }
} }