mirror of
https://github.com/outline/outline.git
synced 2025-12-30 07:19:52 -06:00
179 lines
4.8 KiB
TypeScript
179 lines
4.8 KiB
TypeScript
import querystring from "querystring";
|
|
import { addMonths } from "date-fns";
|
|
import type { Context } from "koa";
|
|
import pick from "lodash/pick";
|
|
import { Client } from "@shared/types";
|
|
import { getCookieDomain } from "@shared/utils/domains";
|
|
import env from "@server/env";
|
|
import Logger from "@server/logging/Logger";
|
|
import { Event, Collection, View } from "@server/models";
|
|
import type { APIContext, AuthenticationResult } from "@server/types";
|
|
import { AuthenticationType } from "@server/types";
|
|
|
|
/**
|
|
* Parse and return the details from the "sessions" cookie in the request, if
|
|
* any. The cookie is on the apex domain and includes session details for
|
|
* other subdomains.
|
|
*
|
|
* @param ctx The Koa context
|
|
* @returns The session details
|
|
*/
|
|
export function getSessionsInCookie(ctx: Context) {
|
|
try {
|
|
const sessionCookie = ctx.cookies.get("sessions") || "";
|
|
const decodedSessionCookie = decodeURIComponent(sessionCookie);
|
|
return decodedSessionCookie ? JSON.parse(decodedSessionCookie) : {};
|
|
} catch (_err) {
|
|
return {};
|
|
}
|
|
}
|
|
|
|
export async function signIn(
|
|
ctx: Context | APIContext,
|
|
service: string,
|
|
{ user, team, client, isNewTeam }: AuthenticationResult
|
|
) {
|
|
const { transaction } = ctx.state;
|
|
|
|
if (team.isSuspended) {
|
|
return ctx.redirect("/?notice=team-suspended");
|
|
}
|
|
if (user.isSuspended) {
|
|
return ctx.redirect("/?notice=user-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,
|
|
},
|
|
{
|
|
transaction,
|
|
}
|
|
);
|
|
} catch (error) {
|
|
Logger.error(`Error persisting signup query params`, error);
|
|
}
|
|
}
|
|
}
|
|
|
|
// update the database when the user last signed in
|
|
await user.updateSignedIn(ctx);
|
|
|
|
await Event.createFromContext(
|
|
ctx,
|
|
{
|
|
name: "users.signin",
|
|
userId: user.id,
|
|
authType: AuthenticationType.APP,
|
|
data: {
|
|
name: user.name,
|
|
service,
|
|
},
|
|
},
|
|
{
|
|
actorId: user.id,
|
|
teamId: team.id,
|
|
}
|
|
);
|
|
|
|
const domain = getCookieDomain(ctx.request.hostname, env.isCloudHosted);
|
|
const expires = addMonths(new Date(), 3);
|
|
|
|
// set a cookie for which service we last signed in with. This is
|
|
// only used to display a UI hint for the user for next time
|
|
ctx.cookies.set("lastSignedIn", service, {
|
|
httpOnly: false,
|
|
sameSite: true,
|
|
expires: new Date("2100"),
|
|
domain,
|
|
});
|
|
|
|
// set a transfer cookie for the access token itself and redirect
|
|
// to the teams subdomain if subdomains are enabled
|
|
if (env.isCloudHosted && team.subdomain) {
|
|
// get any existing sessions (teams signed in) and add this team
|
|
const existing = getSessionsInCookie(ctx);
|
|
const sessions = encodeURIComponent(
|
|
JSON.stringify({
|
|
...existing,
|
|
[team.id]: {
|
|
name: team.name,
|
|
logoUrl: team.avatarUrl,
|
|
url: team.url,
|
|
},
|
|
})
|
|
);
|
|
ctx.cookies.set("sessions", sessions, {
|
|
httpOnly: false,
|
|
expires,
|
|
domain,
|
|
});
|
|
|
|
// If the authentication request originally came from the desktop app then we send the user
|
|
// back to a screen in the web app that will immediately redirect to the desktop. The reason
|
|
// to do this from the client is that if you redirect from the server then the browser ends up
|
|
// stuck on the SSO screen.
|
|
if (client === Client.Desktop) {
|
|
ctx.redirect(
|
|
`${team.url}/desktop-redirect?token=${user.getTransferToken()}`
|
|
);
|
|
} else {
|
|
ctx.redirect(
|
|
`${team.url}/auth/redirect?token=${user.getTransferToken()}`
|
|
);
|
|
}
|
|
} else {
|
|
ctx.cookies.set("accessToken", user.getJwtToken(expires), {
|
|
sameSite: "lax",
|
|
expires,
|
|
});
|
|
|
|
const defaultCollectionId = team.defaultCollectionId;
|
|
|
|
if (defaultCollectionId) {
|
|
const collection = await Collection.findOne({
|
|
where: {
|
|
id: defaultCollectionId,
|
|
teamId: team.id,
|
|
},
|
|
transaction,
|
|
});
|
|
|
|
if (collection) {
|
|
ctx.redirect(`${team.url}${collection.url}`);
|
|
return;
|
|
}
|
|
}
|
|
|
|
const [collection, view] = await Promise.all([
|
|
Collection.findFirstCollectionForUser(user, {
|
|
transaction,
|
|
}),
|
|
View.findOne({
|
|
where: {
|
|
userId: user.id,
|
|
},
|
|
transaction,
|
|
}),
|
|
]);
|
|
const hasViewedDocuments = !!view;
|
|
|
|
ctx.redirect(
|
|
!hasViewedDocuments && collection
|
|
? `${team.url}${collection.url}`
|
|
: `${team.url}/home`
|
|
);
|
|
}
|
|
}
|