chore: Move to Typescript (#2783)
This PR moves the entire project to Typescript. Due to the ~1000 ignores this will lead to a messy codebase for a while, but the churn is worth it – all of those ignore comments are places that were never type-safe previously. closes #1282
This commit is contained in:
@@ -1,20 +1,21 @@
|
||||
// @flow
|
||||
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module '@out... Remove this comment to see the full error message
|
||||
import passport from "@outlinewiki/koa-passport";
|
||||
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module '@out... Remove this comment to see the full error message
|
||||
import { Strategy as AzureStrategy } from "@outlinewiki/passport-azure-ad-oauth2";
|
||||
import jwt from "jsonwebtoken";
|
||||
import Router from "koa-router";
|
||||
import accountProvisioner from "../../../commands/accountProvisioner";
|
||||
import env from "../../../env";
|
||||
import { MicrosoftGraphError } from "../../../errors";
|
||||
import passportMiddleware from "../../../middlewares/passport";
|
||||
import { StateStore, request } from "../../../utils/passport";
|
||||
import accountProvisioner from "@server/commands/accountProvisioner";
|
||||
import env from "@server/env";
|
||||
import { MicrosoftGraphError } from "@server/errors";
|
||||
import passportMiddleware from "@server/middlewares/passport";
|
||||
import { StateStore, request } from "@server/utils/passport";
|
||||
|
||||
const router = new Router();
|
||||
const providerName = "azure";
|
||||
const AZURE_CLIENT_ID = process.env.AZURE_CLIENT_ID;
|
||||
const AZURE_CLIENT_SECRET = process.env.AZURE_CLIENT_SECRET;
|
||||
const AZURE_RESOURCE_APP_ID = process.env.AZURE_RESOURCE_APP_ID;
|
||||
|
||||
// @ts-expect-error ts-migrate(7034) FIXME: Variable 'scopes' implicitly has type 'any[]' in s... Remove this comment to see the full error message
|
||||
const scopes = [];
|
||||
|
||||
export const config = {
|
||||
@@ -32,8 +33,10 @@ if (AZURE_CLIENT_ID) {
|
||||
passReqToCallback: true,
|
||||
resource: AZURE_RESOURCE_APP_ID,
|
||||
store: new StateStore(),
|
||||
// @ts-expect-error ts-migrate(7005) FIXME: Variable 'scopes' implicitly has an 'any[]' type.
|
||||
scope: scopes,
|
||||
},
|
||||
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'req' implicitly has an 'any' type.
|
||||
async function (req, accessToken, refreshToken, params, _, done) {
|
||||
try {
|
||||
// see docs for what the fields in profile represent here:
|
||||
@@ -46,8 +49,9 @@ if (AZURE_CLIENT_ID) {
|
||||
`https://graph.microsoft.com/v1.0/me`,
|
||||
accessToken
|
||||
);
|
||||
|
||||
if (!profileResponse) {
|
||||
throw new MicrosoftGraphError(
|
||||
throw MicrosoftGraphError(
|
||||
"Unable to load user profile from Microsoft Graph API"
|
||||
);
|
||||
}
|
||||
@@ -58,16 +62,19 @@ if (AZURE_CLIENT_ID) {
|
||||
`https://graph.microsoft.com/v1.0/organization`,
|
||||
accessToken
|
||||
);
|
||||
|
||||
if (!organizationResponse) {
|
||||
throw new MicrosoftGraphError(
|
||||
throw MicrosoftGraphError(
|
||||
"Unable to load organization info from Microsoft Graph API"
|
||||
);
|
||||
}
|
||||
|
||||
const organization = organizationResponse.value[0];
|
||||
// @ts-expect-error ts-migrate(2531) FIXME: Object is possibly 'null'.
|
||||
const email = profile.email || profileResponse.mail;
|
||||
|
||||
if (!email) {
|
||||
throw new MicrosoftGraphError(
|
||||
throw MicrosoftGraphError(
|
||||
"'email' property is required but could not be found in user profile."
|
||||
);
|
||||
}
|
||||
@@ -75,7 +82,6 @@ if (AZURE_CLIENT_ID) {
|
||||
const domain = email.split("@")[1];
|
||||
const subdomain = domain.split(".")[0];
|
||||
const teamName = organization.displayName;
|
||||
|
||||
const result = await accountProvisioner({
|
||||
ip: req.ip,
|
||||
team: {
|
||||
@@ -84,18 +90,23 @@ if (AZURE_CLIENT_ID) {
|
||||
subdomain,
|
||||
},
|
||||
user: {
|
||||
// @ts-expect-error ts-migrate(2531) FIXME: Object is possibly 'null'.
|
||||
name: profile.name,
|
||||
email,
|
||||
// @ts-expect-error ts-migrate(2531) FIXME: Object is possibly 'null'.
|
||||
avatarUrl: profile.picture,
|
||||
},
|
||||
authenticationProvider: {
|
||||
name: providerName,
|
||||
// @ts-expect-error ts-migrate(2531) FIXME: Object is possibly 'null'.
|
||||
providerId: profile.tid,
|
||||
},
|
||||
authentication: {
|
||||
// @ts-expect-error ts-migrate(2531) FIXME: Object is possibly 'null'.
|
||||
providerId: profile.oid,
|
||||
accessToken,
|
||||
refreshToken,
|
||||
// @ts-expect-error ts-migrate(7005) FIXME: Variable 'scopes' implicitly has an 'any[]' type.
|
||||
scopes,
|
||||
},
|
||||
});
|
||||
@@ -105,7 +116,6 @@ if (AZURE_CLIENT_ID) {
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
passport.use(strategy);
|
||||
|
||||
router.get("azure", passport.authenticate(providerName));
|
||||
@@ -1,30 +1,25 @@
|
||||
// @flow
|
||||
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module 'fetc... Remove this comment to see the full error message
|
||||
import TestServer from "fetch-test-server";
|
||||
import mailer from "../../../mailer";
|
||||
import webService from "../../../services/web";
|
||||
import { buildUser, buildGuestUser, buildTeam } from "../../../test/factories";
|
||||
import { flushdb } from "../../../test/support";
|
||||
import mailer from "@server/mailer";
|
||||
import webService from "@server/services/web";
|
||||
import { buildUser, buildGuestUser, buildTeam } from "@server/test/factories";
|
||||
import { flushdb } from "@server/test/support";
|
||||
|
||||
const app = webService();
|
||||
const server = new TestServer(app.callback());
|
||||
|
||||
jest.mock("../../../mailer");
|
||||
|
||||
beforeEach(async () => {
|
||||
await flushdb();
|
||||
|
||||
// $FlowFixMe – does not understand Jest mocks
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'mockReset' does not exist on type '(type... Remove this comment to see the full error message
|
||||
mailer.sendTemplate.mockReset();
|
||||
});
|
||||
afterAll(() => server.close());
|
||||
|
||||
describe("email", () => {
|
||||
it("should require email param", async () => {
|
||||
const res = await server.post("/auth/email", {
|
||||
body: {},
|
||||
});
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(400);
|
||||
expect(body.error).toEqual("validation_error");
|
||||
expect(body.ok).toEqual(false);
|
||||
@@ -32,12 +27,12 @@ describe("email", () => {
|
||||
|
||||
it("should respond with redirect location when user is SSO enabled", async () => {
|
||||
const user = await buildUser();
|
||||
|
||||
const res = await server.post("/auth/email", {
|
||||
body: { email: user.email },
|
||||
body: {
|
||||
email: user.email,
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.redirect).toMatch("slack");
|
||||
expect(mailer.sendTemplate).not.toHaveBeenCalled();
|
||||
@@ -46,19 +41,19 @@ describe("email", () => {
|
||||
it("should respond with redirect location when user is SSO enabled on another subdomain", async () => {
|
||||
process.env.URL = "http://localoutline.com";
|
||||
process.env.SUBDOMAINS_ENABLED = "true";
|
||||
|
||||
const user = await buildUser();
|
||||
|
||||
await buildTeam({
|
||||
subdomain: "example",
|
||||
});
|
||||
|
||||
const res = await server.post("/auth/email", {
|
||||
body: { email: user.email },
|
||||
headers: { host: "example.localoutline.com" },
|
||||
body: {
|
||||
email: user.email,
|
||||
},
|
||||
headers: {
|
||||
host: "example.localoutline.com",
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.redirect).toMatch("slack");
|
||||
expect(mailer.sendTemplate).not.toHaveBeenCalled();
|
||||
@@ -66,12 +61,12 @@ describe("email", () => {
|
||||
|
||||
it("should respond with success when user is not SSO enabled", async () => {
|
||||
const user = await buildGuestUser();
|
||||
|
||||
const res = await server.post("/auth/email", {
|
||||
body: { email: user.email },
|
||||
body: {
|
||||
email: user.email,
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.success).toEqual(true);
|
||||
expect(mailer.sendTemplate).toHaveBeenCalled();
|
||||
@@ -79,97 +74,116 @@ describe("email", () => {
|
||||
|
||||
it("should respond with success regardless of whether successful to prevent crawling email logins", async () => {
|
||||
const res = await server.post("/auth/email", {
|
||||
body: { email: "user@example.com" },
|
||||
body: {
|
||||
email: "user@example.com",
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.success).toEqual(true);
|
||||
expect(mailer.sendTemplate).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe("with multiple users matching email", () => {
|
||||
it("should default to current subdomain with SSO", async () => {
|
||||
process.env.URL = "http://localoutline.com";
|
||||
process.env.SUBDOMAINS_ENABLED = "true";
|
||||
|
||||
const email = "sso-user@example.org";
|
||||
const team = await buildTeam({
|
||||
subdomain: "example",
|
||||
});
|
||||
|
||||
await buildGuestUser({ email });
|
||||
await buildUser({ email, teamId: team.id });
|
||||
|
||||
await buildGuestUser({
|
||||
email,
|
||||
});
|
||||
await buildUser({
|
||||
email,
|
||||
teamId: team.id,
|
||||
});
|
||||
const res = await server.post("/auth/email", {
|
||||
body: { email },
|
||||
headers: { host: "example.localoutline.com" },
|
||||
body: {
|
||||
email,
|
||||
},
|
||||
headers: {
|
||||
host: "example.localoutline.com",
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.redirect).toMatch("slack");
|
||||
expect(mailer.sendTemplate).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should default to current subdomain with guest email", async () => {
|
||||
process.env.URL = "http://localoutline.com";
|
||||
process.env.SUBDOMAINS_ENABLED = "true";
|
||||
|
||||
const email = "guest-user@example.org";
|
||||
const team = await buildTeam({
|
||||
subdomain: "example",
|
||||
});
|
||||
|
||||
await buildUser({ email });
|
||||
await buildGuestUser({ email, teamId: team.id });
|
||||
|
||||
await buildUser({
|
||||
email,
|
||||
});
|
||||
await buildGuestUser({
|
||||
email,
|
||||
teamId: team.id,
|
||||
});
|
||||
const res = await server.post("/auth/email", {
|
||||
body: { email },
|
||||
headers: { host: "example.localoutline.com" },
|
||||
body: {
|
||||
email,
|
||||
},
|
||||
headers: {
|
||||
host: "example.localoutline.com",
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.success).toEqual(true);
|
||||
expect(mailer.sendTemplate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should default to custom domain with SSO", async () => {
|
||||
const email = "sso-user-2@example.org";
|
||||
const team = await buildTeam({
|
||||
domain: "docs.mycompany.com",
|
||||
});
|
||||
|
||||
await buildGuestUser({ email });
|
||||
await buildUser({ email, teamId: team.id });
|
||||
|
||||
await buildGuestUser({
|
||||
email,
|
||||
});
|
||||
await buildUser({
|
||||
email,
|
||||
teamId: team.id,
|
||||
});
|
||||
const res = await server.post("/auth/email", {
|
||||
body: { email },
|
||||
headers: { host: "docs.mycompany.com" },
|
||||
body: {
|
||||
email,
|
||||
},
|
||||
headers: {
|
||||
host: "docs.mycompany.com",
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.redirect).toMatch("slack");
|
||||
expect(mailer.sendTemplate).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should default to custom domain with guest email", async () => {
|
||||
const email = "guest-user-2@example.org";
|
||||
const team = await buildTeam({
|
||||
domain: "docs.mycompany.com",
|
||||
});
|
||||
|
||||
await buildUser({ email });
|
||||
await buildGuestUser({ email, teamId: team.id });
|
||||
|
||||
await buildUser({
|
||||
email,
|
||||
});
|
||||
await buildGuestUser({
|
||||
email,
|
||||
teamId: team.id,
|
||||
});
|
||||
const res = await server.post("/auth/email", {
|
||||
body: { email },
|
||||
headers: { host: "docs.mycompany.com" },
|
||||
body: {
|
||||
email,
|
||||
},
|
||||
headers: {
|
||||
host: "docs.mycompany.com",
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.success).toEqual(true);
|
||||
expect(mailer.sendTemplate).toHaveBeenCalled();
|
||||
@@ -1,20 +1,16 @@
|
||||
// @flow
|
||||
import { subMinutes } from "date-fns";
|
||||
import Router from "koa-router";
|
||||
import { find } from "lodash";
|
||||
import {
|
||||
parseDomain,
|
||||
isCustomSubdomain,
|
||||
} from "../../../../shared/utils/domains";
|
||||
import { AuthorizationError } from "../../../errors";
|
||||
import mailer from "../../../mailer";
|
||||
import errorHandling from "../../../middlewares/errorHandling";
|
||||
import methodOverride from "../../../middlewares/methodOverride";
|
||||
import validation from "../../../middlewares/validation";
|
||||
import { User, Team } from "../../../models";
|
||||
import { signIn } from "../../../utils/authentication";
|
||||
import { isCustomDomain } from "../../../utils/domains";
|
||||
import { getUserForEmailSigninToken } from "../../../utils/jwt";
|
||||
import { parseDomain, isCustomSubdomain } from "@shared/utils/domains";
|
||||
import { AuthorizationError } from "@server/errors";
|
||||
import mailer from "@server/mailer";
|
||||
import errorHandling from "@server/middlewares/errorHandling";
|
||||
import methodOverride from "@server/middlewares/methodOverride";
|
||||
import { User, Team } from "@server/models";
|
||||
import { signIn } from "@server/utils/authentication";
|
||||
import { isCustomDomain } from "@server/utils/domains";
|
||||
import { getUserForEmailSigninToken } from "@server/utils/jwt";
|
||||
import { assertEmail, assertPresent } from "@server/validation";
|
||||
|
||||
const router = new Router();
|
||||
|
||||
@@ -24,23 +20,25 @@ export const config = {
|
||||
};
|
||||
|
||||
router.use(methodOverride());
|
||||
router.use(validation());
|
||||
|
||||
router.post("email", errorHandling(), async (ctx) => {
|
||||
const { email } = ctx.body;
|
||||
|
||||
ctx.assertEmail(email, "email is required");
|
||||
|
||||
assertEmail(email, "email is required");
|
||||
const users = await User.scope("withAuthentications").findAll({
|
||||
where: { email: email.toLowerCase() },
|
||||
where: {
|
||||
email: email.toLowerCase(),
|
||||
},
|
||||
});
|
||||
|
||||
if (users.length) {
|
||||
// @ts-expect-error ts-migrate(7034) FIXME: Variable 'team' implicitly has type 'any' in some ... Remove this comment to see the full error message
|
||||
let team;
|
||||
|
||||
if (isCustomDomain(ctx.request.hostname)) {
|
||||
team = await Team.scope("withAuthenticationProviders").findOne({
|
||||
where: { domain: ctx.request.hostname },
|
||||
where: {
|
||||
domain: ctx.request.hostname,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -52,12 +50,15 @@ router.post("email", errorHandling(), async (ctx) => {
|
||||
const domain = parseDomain(ctx.request.hostname);
|
||||
const subdomain = domain ? domain.subdomain : undefined;
|
||||
team = await Team.scope("withAuthenticationProviders").findOne({
|
||||
where: { subdomain },
|
||||
where: {
|
||||
subdomain,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// If there are multiple users with this email address then give precedence
|
||||
// to the one that is active on this subdomain/domain (if any)
|
||||
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'user' implicitly has an 'any' type.
|
||||
let user = users.find((user) => team && user.teamId === team.id);
|
||||
|
||||
// A user was found for the email address, but they don't belong to the team
|
||||
@@ -94,7 +95,7 @@ router.post("email", errorHandling(), async (ctx) => {
|
||||
}
|
||||
|
||||
if (!team.guestSignin) {
|
||||
throw new AuthorizationError();
|
||||
throw AuthorizationError();
|
||||
}
|
||||
|
||||
// basic rate limit of endpoint to prevent send email abuse
|
||||
@@ -116,7 +117,6 @@ router.post("email", errorHandling(), async (ctx) => {
|
||||
token: user.getEmailSigninToken(),
|
||||
teamUrl: team.url,
|
||||
});
|
||||
|
||||
user.lastSigninEmailSentAt = new Date();
|
||||
await user.save();
|
||||
}
|
||||
@@ -129,17 +129,20 @@ router.post("email", errorHandling(), async (ctx) => {
|
||||
|
||||
router.get("email.callback", async (ctx) => {
|
||||
const { token } = ctx.request.query;
|
||||
|
||||
ctx.assertPresent(token, "token is required");
|
||||
assertPresent(token, "token is required");
|
||||
|
||||
try {
|
||||
// @ts-expect-error ts-migrate(2345) FIXME: Argument of type 'string | string[] | undefined' i... Remove this comment to see the full error message
|
||||
const user = await getUserForEmailSigninToken(token);
|
||||
|
||||
if (!user.team.guestSignin) {
|
||||
return ctx.redirect("/?notice=auth-error");
|
||||
}
|
||||
|
||||
if (user.isSuspended) {
|
||||
return ctx.redirect("/?notice=suspended");
|
||||
}
|
||||
|
||||
if (user.isInvited) {
|
||||
await mailer.sendTemplate("welcome", {
|
||||
to: user.email,
|
||||
@@ -147,8 +150,9 @@ 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
|
||||
await signIn(ctx, user, user.team, "email", false, false);
|
||||
} catch (err) {
|
||||
@@ -1,24 +1,24 @@
|
||||
// @flow
|
||||
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module '@out... Remove this comment to see the full error message
|
||||
import passport from "@outlinewiki/koa-passport";
|
||||
import Router from "koa-router";
|
||||
import { capitalize } from "lodash";
|
||||
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module 'pass... Remove this comment to see the full error message
|
||||
import { Strategy as GoogleStrategy } from "passport-google-oauth2";
|
||||
import accountProvisioner from "../../../commands/accountProvisioner";
|
||||
import env from "../../../env";
|
||||
import accountProvisioner from "@server/commands/accountProvisioner";
|
||||
import env from "@server/env";
|
||||
import {
|
||||
GoogleWorkspaceRequiredError,
|
||||
GoogleWorkspaceInvalidError,
|
||||
} from "../../../errors";
|
||||
import passportMiddleware from "../../../middlewares/passport";
|
||||
import { getAllowedDomains } from "../../../utils/authentication";
|
||||
import { StateStore } from "../../../utils/passport";
|
||||
} from "@server/errors";
|
||||
import passportMiddleware from "@server/middlewares/passport";
|
||||
import { getAllowedDomains } from "@server/utils/authentication";
|
||||
import { StateStore } from "@server/utils/passport";
|
||||
|
||||
const router = new Router();
|
||||
const providerName = "google";
|
||||
const GOOGLE_CLIENT_ID = process.env.GOOGLE_CLIENT_ID;
|
||||
const GOOGLE_CLIENT_SECRET = process.env.GOOGLE_CLIENT_SECRET;
|
||||
const allowedDomains = getAllowedDomains();
|
||||
|
||||
const scopes = [
|
||||
"https://www.googleapis.com/auth/userinfo.profile",
|
||||
"https://www.googleapis.com/auth/userinfo.email",
|
||||
@@ -40,21 +40,21 @@ if (GOOGLE_CLIENT_ID) {
|
||||
store: new StateStore(),
|
||||
scope: scopes,
|
||||
},
|
||||
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'req' implicitly has an 'any' type.
|
||||
async function (req, accessToken, refreshToken, profile, done) {
|
||||
try {
|
||||
const domain = profile._json.hd;
|
||||
|
||||
if (!domain) {
|
||||
throw new GoogleWorkspaceRequiredError();
|
||||
throw GoogleWorkspaceRequiredError();
|
||||
}
|
||||
|
||||
if (allowedDomains.length && !allowedDomains.includes(domain)) {
|
||||
throw new GoogleWorkspaceInvalidError();
|
||||
throw GoogleWorkspaceInvalidError();
|
||||
}
|
||||
|
||||
const subdomain = domain.split(".")[0];
|
||||
const teamName = capitalize(subdomain);
|
||||
|
||||
const result = await accountProvisioner({
|
||||
ip: req.ip,
|
||||
team: {
|
||||
@@ -1,10 +1,11 @@
|
||||
// @flow
|
||||
import { signin } from "../../../../shared/utils/routeHelpers";
|
||||
import { requireDirectory } from "../../../utils/fs";
|
||||
import { signin } from "@shared/utils/routeHelpers";
|
||||
import { requireDirectory } from "@server/utils/fs";
|
||||
|
||||
let providers = [];
|
||||
// @ts-expect-error ts-migrate(7034) FIXME: Variable 'providers' implicitly has type 'any[]' i... Remove this comment to see the full error message
|
||||
const providers = [];
|
||||
|
||||
requireDirectory(__dirname).forEach(([module, id]) => {
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'config' does not exist on type 'unknown'... Remove this comment to see the full error message
|
||||
const { config, default: router } = module;
|
||||
|
||||
if (id === "index") {
|
||||
@@ -34,4 +35,5 @@ requireDirectory(__dirname).forEach(([module, id]) => {
|
||||
}
|
||||
});
|
||||
|
||||
// @ts-expect-error ts-migrate(7005) FIXME: Variable 'providers' implicitly has an 'any[]' typ... Remove this comment to see the full error message
|
||||
export default providers;
|
||||
@@ -1,26 +1,26 @@
|
||||
// @flow
|
||||
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module '@out... Remove this comment to see the full error message
|
||||
import passport from "@outlinewiki/koa-passport";
|
||||
import Router from "koa-router";
|
||||
import get from "lodash/get";
|
||||
import { get } from "lodash";
|
||||
import { Strategy } from "passport-oauth2";
|
||||
import accountProvisioner from "../../../commands/accountProvisioner";
|
||||
import env from "../../../env";
|
||||
import accountProvisioner from "@server/commands/accountProvisioner";
|
||||
import env from "@server/env";
|
||||
import {
|
||||
OIDCMalformedUserInfoError,
|
||||
AuthenticationError,
|
||||
} from "../../../errors";
|
||||
import passportMiddleware from "../../../middlewares/passport";
|
||||
import { getAllowedDomains } from "../../../utils/authentication";
|
||||
import { StateStore, request } from "../../../utils/passport";
|
||||
} from "@server/errors";
|
||||
import passportMiddleware from "@server/middlewares/passport";
|
||||
import { getAllowedDomains } from "@server/utils/authentication";
|
||||
import { StateStore, request } from "@server/utils/passport";
|
||||
|
||||
const router = new Router();
|
||||
const providerName = "oidc";
|
||||
const OIDC_DISPLAY_NAME = process.env.OIDC_DISPLAY_NAME || "OpenID Connect";
|
||||
const OIDC_CLIENT_ID = process.env.OIDC_CLIENT_ID;
|
||||
const OIDC_CLIENT_SECRET = process.env.OIDC_CLIENT_SECRET;
|
||||
const OIDC_AUTH_URI = process.env.OIDC_AUTH_URI;
|
||||
const OIDC_TOKEN_URI = process.env.OIDC_TOKEN_URI;
|
||||
const OIDC_USERINFO_URI = process.env.OIDC_USERINFO_URI;
|
||||
const OIDC_CLIENT_ID = process.env.OIDC_CLIENT_ID || "";
|
||||
const OIDC_CLIENT_SECRET = process.env.OIDC_CLIENT_SECRET || "";
|
||||
const OIDC_AUTH_URI = process.env.OIDC_AUTH_URI || "";
|
||||
const OIDC_TOKEN_URI = process.env.OIDC_TOKEN_URI || "";
|
||||
const OIDC_USERINFO_URI = process.env.OIDC_USERINFO_URI || "";
|
||||
const OIDC_SCOPES = process.env.OIDC_SCOPES || "";
|
||||
const OIDC_USERNAME_CLAIM =
|
||||
process.env.OIDC_USERNAME_CLAIM || "preferred_username";
|
||||
@@ -30,7 +30,6 @@ export const config = {
|
||||
name: OIDC_DISPLAY_NAME,
|
||||
enabled: !!OIDC_CLIENT_ID,
|
||||
};
|
||||
|
||||
const scopes = OIDC_SCOPES.split(" ");
|
||||
|
||||
Strategy.prototype.userProfile = async function (accessToken, done) {
|
||||
@@ -54,40 +53,45 @@ if (OIDC_CLIENT_ID) {
|
||||
callbackURL: `${env.URL}/auth/${providerName}.callback`,
|
||||
passReqToCallback: true,
|
||||
scope: OIDC_SCOPES,
|
||||
// @ts-expect-error custom state store
|
||||
store: new StateStore(),
|
||||
state: true,
|
||||
pkce: false,
|
||||
},
|
||||
|
||||
// OpenID Connect standard profile claims can be found in the official
|
||||
// specification.
|
||||
// https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims
|
||||
|
||||
// Non-standard claims may be configured by individual identity providers.
|
||||
// Any claim supplied in response to the userinfo request will be
|
||||
// available on the `profile` parameter
|
||||
async function (req, accessToken, refreshToken, profile, done) {
|
||||
async function (
|
||||
req: any,
|
||||
accessToken: string,
|
||||
refreshToken: string,
|
||||
profile: Record<string, string>,
|
||||
done: any
|
||||
) {
|
||||
try {
|
||||
if (!profile.email) {
|
||||
throw new AuthenticationError(
|
||||
throw AuthenticationError(
|
||||
`An email field was not returned in the profile parameter, but is required.`
|
||||
);
|
||||
}
|
||||
|
||||
const parts = profile.email.split("@");
|
||||
const domain = parts.length && parts[1];
|
||||
|
||||
if (!domain) {
|
||||
throw new OIDCMalformedUserInfoError();
|
||||
throw OIDCMalformedUserInfoError();
|
||||
}
|
||||
|
||||
if (allowedDomains.length && !allowedDomains.includes(domain)) {
|
||||
throw new AuthenticationError(
|
||||
throw AuthenticationError(
|
||||
`Domain ${domain} is not on the whitelist`
|
||||
);
|
||||
}
|
||||
|
||||
const subdomain = domain.split(".")[0];
|
||||
|
||||
const result = await accountProvisioner({
|
||||
ip: req.ip,
|
||||
team: {
|
||||
@@ -115,7 +119,6 @@ if (OIDC_CLIENT_ID) {
|
||||
scopes,
|
||||
},
|
||||
});
|
||||
|
||||
return done(null, result.user, result);
|
||||
} catch (err) {
|
||||
return done(err, null);
|
||||
@@ -1,203 +0,0 @@
|
||||
// @flow
|
||||
import passport from "@outlinewiki/koa-passport";
|
||||
import Router from "koa-router";
|
||||
import { Strategy as SlackStrategy } from "passport-slack-oauth2";
|
||||
import accountProvisioner from "../../../commands/accountProvisioner";
|
||||
import env from "../../../env";
|
||||
import auth from "../../../middlewares/authentication";
|
||||
import passportMiddleware from "../../../middlewares/passport";
|
||||
import {
|
||||
IntegrationAuthentication,
|
||||
Collection,
|
||||
Integration,
|
||||
Team,
|
||||
} from "../../../models";
|
||||
import { StateStore } from "../../../utils/passport";
|
||||
import * as Slack from "../../../utils/slack";
|
||||
|
||||
const router = new Router();
|
||||
const providerName = "slack";
|
||||
const SLACK_CLIENT_ID = process.env.SLACK_KEY;
|
||||
const SLACK_CLIENT_SECRET = process.env.SLACK_SECRET;
|
||||
|
||||
const scopes = [
|
||||
"identity.email",
|
||||
"identity.basic",
|
||||
"identity.avatar",
|
||||
"identity.team",
|
||||
];
|
||||
|
||||
export const config = {
|
||||
name: "Slack",
|
||||
enabled: !!SLACK_CLIENT_ID,
|
||||
};
|
||||
|
||||
if (SLACK_CLIENT_ID) {
|
||||
const strategy = new SlackStrategy(
|
||||
{
|
||||
clientID: SLACK_CLIENT_ID,
|
||||
clientSecret: SLACK_CLIENT_SECRET,
|
||||
callbackURL: `${env.URL}/auth/slack.callback`,
|
||||
passReqToCallback: true,
|
||||
store: new StateStore(),
|
||||
scope: scopes,
|
||||
},
|
||||
async function (req, accessToken, refreshToken, profile, done) {
|
||||
try {
|
||||
const result = await accountProvisioner({
|
||||
ip: req.ip,
|
||||
team: {
|
||||
name: profile.team.name,
|
||||
subdomain: profile.team.domain,
|
||||
avatarUrl: profile.team.image_230,
|
||||
},
|
||||
user: {
|
||||
name: profile.user.name,
|
||||
email: profile.user.email,
|
||||
avatarUrl: profile.user.image_192,
|
||||
},
|
||||
authenticationProvider: {
|
||||
name: providerName,
|
||||
providerId: profile.team.id,
|
||||
},
|
||||
authentication: {
|
||||
providerId: profile.user.id,
|
||||
accessToken,
|
||||
refreshToken,
|
||||
scopes,
|
||||
},
|
||||
});
|
||||
return done(null, result.user, result);
|
||||
} catch (err) {
|
||||
return done(err, null);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// For some reason the author made the strategy name capatilised, I don't know
|
||||
// why but we need everything lowercase so we just monkey-patch it here.
|
||||
strategy.name = providerName;
|
||||
passport.use(strategy);
|
||||
|
||||
router.get("slack", passport.authenticate(providerName));
|
||||
|
||||
router.get("slack.callback", passportMiddleware(providerName));
|
||||
|
||||
router.get("slack.commands", auth({ required: false }), async (ctx) => {
|
||||
const { code, state, error } = ctx.request.query;
|
||||
const user = ctx.state.user;
|
||||
ctx.assertPresent(code || error, "code is required");
|
||||
|
||||
if (error) {
|
||||
ctx.redirect(`/settings/integrations/slack?error=${error}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// this code block accounts for the root domain being unable to
|
||||
// access authentication for subdomains. We must forward to the appropriate
|
||||
// subdomain to complete the oauth flow
|
||||
if (!user) {
|
||||
if (state) {
|
||||
try {
|
||||
const team = await Team.findByPk(state);
|
||||
return ctx.redirect(
|
||||
`${team.url}/auth${ctx.request.path}?${ctx.request.querystring}`
|
||||
);
|
||||
} catch (err) {
|
||||
return ctx.redirect(
|
||||
`/settings/integrations/slack?error=unauthenticated`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
return ctx.redirect(
|
||||
`/settings/integrations/slack?error=unauthenticated`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const endpoint = `${process.env.URL || ""}/auth/slack.commands`;
|
||||
const data = await Slack.oauthAccess(code, endpoint);
|
||||
|
||||
const authentication = await IntegrationAuthentication.create({
|
||||
service: "slack",
|
||||
userId: user.id,
|
||||
teamId: user.teamId,
|
||||
token: data.access_token,
|
||||
scopes: data.scope.split(","),
|
||||
});
|
||||
|
||||
await Integration.create({
|
||||
service: "slack",
|
||||
type: "command",
|
||||
userId: user.id,
|
||||
teamId: user.teamId,
|
||||
authenticationId: authentication.id,
|
||||
settings: {
|
||||
serviceTeamId: data.team_id,
|
||||
},
|
||||
});
|
||||
|
||||
ctx.redirect("/settings/integrations/slack");
|
||||
});
|
||||
|
||||
router.get("slack.post", auth({ required: false }), async (ctx) => {
|
||||
const { code, error, state } = ctx.request.query;
|
||||
const user = ctx.state.user;
|
||||
ctx.assertPresent(code || error, "code is required");
|
||||
|
||||
const collectionId = state;
|
||||
ctx.assertUuid(collectionId, "collectionId must be an uuid");
|
||||
|
||||
if (error) {
|
||||
ctx.redirect(`/settings/integrations/slack?error=${error}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// this code block accounts for the root domain being unable to
|
||||
// access authentication for subdomains. We must forward to the
|
||||
// appropriate subdomain to complete the oauth flow
|
||||
if (!user) {
|
||||
try {
|
||||
const collection = await Collection.findByPk(state);
|
||||
const team = await Team.findByPk(collection.teamId);
|
||||
return ctx.redirect(
|
||||
`${team.url}/auth${ctx.request.path}?${ctx.request.querystring}`
|
||||
);
|
||||
} catch (err) {
|
||||
return ctx.redirect(
|
||||
`/settings/integrations/slack?error=unauthenticated`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const endpoint = `${process.env.URL || ""}/auth/slack.post`;
|
||||
const data = await Slack.oauthAccess(code, endpoint);
|
||||
|
||||
const authentication = await IntegrationAuthentication.create({
|
||||
service: "slack",
|
||||
userId: user.id,
|
||||
teamId: user.teamId,
|
||||
token: data.access_token,
|
||||
scopes: data.scope.split(","),
|
||||
});
|
||||
|
||||
await Integration.create({
|
||||
service: "slack",
|
||||
type: "post",
|
||||
userId: user.id,
|
||||
teamId: user.teamId,
|
||||
authenticationId: authentication.id,
|
||||
collectionId,
|
||||
events: [],
|
||||
settings: {
|
||||
url: data.incoming_webhook.url,
|
||||
channel: data.incoming_webhook.channel,
|
||||
channelId: data.incoming_webhook.channel_id,
|
||||
},
|
||||
});
|
||||
|
||||
ctx.redirect("/settings/integrations/slack");
|
||||
});
|
||||
}
|
||||
|
||||
export default router;
|
||||
211
server/routes/auth/providers/slack.ts
Normal file
211
server/routes/auth/providers/slack.ts
Normal file
@@ -0,0 +1,211 @@
|
||||
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module '@out... Remove this comment to see the full error message
|
||||
import passport from "@outlinewiki/koa-passport";
|
||||
import Router from "koa-router";
|
||||
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module 'pass... Remove this comment to see the full error message
|
||||
import { Strategy as SlackStrategy } from "passport-slack-oauth2";
|
||||
import accountProvisioner from "@server/commands/accountProvisioner";
|
||||
import env from "@server/env";
|
||||
import auth from "@server/middlewares/authentication";
|
||||
import passportMiddleware from "@server/middlewares/passport";
|
||||
import {
|
||||
IntegrationAuthentication,
|
||||
Collection,
|
||||
Integration,
|
||||
Team,
|
||||
} from "@server/models";
|
||||
import { StateStore } from "@server/utils/passport";
|
||||
import * as Slack from "@server/utils/slack";
|
||||
import { assertPresent, assertUuid } from "@server/validation";
|
||||
|
||||
const router = new Router();
|
||||
const providerName = "slack";
|
||||
const SLACK_CLIENT_ID = process.env.SLACK_KEY;
|
||||
const SLACK_CLIENT_SECRET = process.env.SLACK_SECRET;
|
||||
const scopes = [
|
||||
"identity.email",
|
||||
"identity.basic",
|
||||
"identity.avatar",
|
||||
"identity.team",
|
||||
];
|
||||
|
||||
export const config = {
|
||||
name: "Slack",
|
||||
enabled: !!SLACK_CLIENT_ID,
|
||||
};
|
||||
|
||||
if (SLACK_CLIENT_ID) {
|
||||
const strategy = new SlackStrategy(
|
||||
{
|
||||
clientID: SLACK_CLIENT_ID,
|
||||
clientSecret: SLACK_CLIENT_SECRET,
|
||||
callbackURL: `${env.URL}/auth/slack.callback`,
|
||||
passReqToCallback: true,
|
||||
store: new StateStore(),
|
||||
scope: scopes,
|
||||
},
|
||||
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'req' implicitly has an 'any' type.
|
||||
async function (req, accessToken, refreshToken, profile, done) {
|
||||
try {
|
||||
const result = await accountProvisioner({
|
||||
ip: req.ip,
|
||||
team: {
|
||||
name: profile.team.name,
|
||||
subdomain: profile.team.domain,
|
||||
avatarUrl: profile.team.image_230,
|
||||
},
|
||||
user: {
|
||||
name: profile.user.name,
|
||||
email: profile.user.email,
|
||||
avatarUrl: profile.user.image_192,
|
||||
},
|
||||
authenticationProvider: {
|
||||
name: providerName,
|
||||
providerId: profile.team.id,
|
||||
},
|
||||
authentication: {
|
||||
providerId: profile.user.id,
|
||||
accessToken,
|
||||
refreshToken,
|
||||
scopes,
|
||||
},
|
||||
});
|
||||
return done(null, result.user, result);
|
||||
} catch (err) {
|
||||
return done(err, null);
|
||||
}
|
||||
}
|
||||
);
|
||||
// For some reason the author made the strategy name capatilised, I don't know
|
||||
// why but we need everything lowercase so we just monkey-patch it here.
|
||||
strategy.name = providerName;
|
||||
passport.use(strategy);
|
||||
|
||||
router.get("slack", passport.authenticate(providerName));
|
||||
|
||||
router.get("slack.callback", passportMiddleware(providerName));
|
||||
|
||||
router.get(
|
||||
"slack.commands",
|
||||
auth({
|
||||
required: false,
|
||||
}),
|
||||
async (ctx) => {
|
||||
const { code, state, error } = ctx.request.query;
|
||||
const user = ctx.state.user;
|
||||
assertPresent(code || error, "code is required");
|
||||
|
||||
if (error) {
|
||||
ctx.redirect(`/settings/integrations/slack?error=${error}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// this code block accounts for the root domain being unable to
|
||||
// access authentication for subdomains. We must forward to the appropriate
|
||||
// subdomain to complete the oauth flow
|
||||
if (!user) {
|
||||
if (state) {
|
||||
try {
|
||||
const team = await Team.findByPk(state);
|
||||
return ctx.redirect(
|
||||
`${team.url}/auth${ctx.request.path}?${ctx.request.querystring}`
|
||||
);
|
||||
} catch (err) {
|
||||
return ctx.redirect(
|
||||
`/settings/integrations/slack?error=unauthenticated`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
return ctx.redirect(
|
||||
`/settings/integrations/slack?error=unauthenticated`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const endpoint = `${process.env.URL || ""}/auth/slack.commands`;
|
||||
// @ts-expect-error ts-migrate(2345) FIXME: Argument of type 'string | string[] | undefined' i... Remove this comment to see the full error message
|
||||
const data = await Slack.oauthAccess(code, endpoint);
|
||||
const authentication = await IntegrationAuthentication.create({
|
||||
service: "slack",
|
||||
userId: user.id,
|
||||
teamId: user.teamId,
|
||||
token: data.access_token,
|
||||
scopes: data.scope.split(","),
|
||||
});
|
||||
await Integration.create({
|
||||
service: "slack",
|
||||
type: "command",
|
||||
userId: user.id,
|
||||
teamId: user.teamId,
|
||||
authenticationId: authentication.id,
|
||||
settings: {
|
||||
serviceTeamId: data.team_id,
|
||||
},
|
||||
});
|
||||
ctx.redirect("/settings/integrations/slack");
|
||||
}
|
||||
);
|
||||
|
||||
router.get(
|
||||
"slack.post",
|
||||
auth({
|
||||
required: false,
|
||||
}),
|
||||
async (ctx) => {
|
||||
const { code, error, state } = ctx.request.query;
|
||||
const user = ctx.state.user;
|
||||
assertPresent(code || error, "code is required");
|
||||
const collectionId = state;
|
||||
assertUuid(collectionId, "collectionId must be an uuid");
|
||||
|
||||
if (error) {
|
||||
ctx.redirect(`/settings/integrations/slack?error=${error}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// this code block accounts for the root domain being unable to
|
||||
// access authentication for subdomains. We must forward to the
|
||||
// appropriate subdomain to complete the oauth flow
|
||||
if (!user) {
|
||||
try {
|
||||
const collection = await Collection.findByPk(state);
|
||||
const team = await Team.findByPk(collection.teamId);
|
||||
return ctx.redirect(
|
||||
`${team.url}/auth${ctx.request.path}?${ctx.request.querystring}`
|
||||
);
|
||||
} catch (err) {
|
||||
return ctx.redirect(
|
||||
`/settings/integrations/slack?error=unauthenticated`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const endpoint = `${process.env.URL || ""}/auth/slack.post`;
|
||||
// @ts-expect-error ts-migrate(2345) FIXME: Argument of type 'string | string[] | undefined' i... Remove this comment to see the full error message
|
||||
const data = await Slack.oauthAccess(code, endpoint);
|
||||
const authentication = await IntegrationAuthentication.create({
|
||||
service: "slack",
|
||||
userId: user.id,
|
||||
teamId: user.teamId,
|
||||
token: data.access_token,
|
||||
scopes: data.scope.split(","),
|
||||
});
|
||||
await Integration.create({
|
||||
service: "slack",
|
||||
type: "post",
|
||||
userId: user.id,
|
||||
teamId: user.teamId,
|
||||
authenticationId: authentication.id,
|
||||
collectionId,
|
||||
events: [],
|
||||
settings: {
|
||||
url: data.incoming_webhook.url,
|
||||
channel: data.incoming_webhook.channel,
|
||||
channelId: data.incoming_webhook.channel_id,
|
||||
},
|
||||
});
|
||||
ctx.redirect("/settings/integrations/slack");
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default router;
|
||||
Reference in New Issue
Block a user