New Drive UI initial version

This commit is contained in:
Romaric Mourgues
2023-03-27 18:30:36 +02:00
parent fcb0f993fe
commit 42420df02d
261 changed files with 1496 additions and 16604 deletions
+19 -24
View File
@@ -8,37 +8,37 @@
"mobile_appstore": "https://apps.apple.com/fr/app/twake/id1588764852?l=en",
"mobile_googleplay": "https://play.google.com/store/apps/details?id=com.twake.twake&gl=FR"
},
"app_grid": [
{
"name": "Tmail",
"logo": "/public/img/grid/tmail.png",
"url": "https://tmail.linagora.com/"
},
{
"name": "Twake",
"logo": "/public/img/grid/twake.png",
"url": "https://twake.app/"
}
],
"accounts": {
"_type": "console",
"_type": "remote",
"type": "internal",
"internal": {
"disable_account_creation": false
},
"console": {
"account_management_url": "http://web.twake-console.local/profile?company-code={company_id}",
"remote": {
"authority": "http://auth.example.com/",
"client_id": "twakeweb",
"client_secret": "",
"issuer": "",
"audience": "",
"redirect_uris": [""],
"account_management_url": "http://web.twake-console.local/profile?company-code={company_id}",
"collaborators_management_url": "http://web.twake-console.local/compaies/{company_id}/users?company-code={company_id}",
"company_management_url": "http://web.twake-console.local/companies?company-code={company_id}"
}
}
},
"console": {
"_type": "remote",
"type": "internal",
"remote": {
"new_console": false,
"username": "",
"password": "12345678",
"url": "https://some-remote-console-url/",
"hook": {
"token": "12345678"
}
},
"internal": {
"disable_account_creation": false
}
},
"sentry": {
"dsn": ""
},
@@ -63,10 +63,6 @@
"root": "./public"
}
},
"phpnode": {
"secret": "api_supersecret",
"php_endpoint": "http://nginx:80"
},
"websocket": {
"path": "/socket/",
"adapters": {
@@ -154,7 +150,6 @@
"api_key": "secret",
"sender": "noreply@twake.app"
},
"applications": [],
"services": [
"auth",
"push",
+3
View File
@@ -120,6 +120,7 @@
"@fastify/formbody": "^6.0.0",
"@fastify/static": "^5.0.1",
"@ffprobe-installer/ffprobe": "^1.4.1",
"@okta/jwt-verifier": "^3.0.1",
"@sentry/node": "^6.19.7",
"@sentry/tracing": "^6.19.7",
"@socket.io/redis-adapter": "^7.1.0",
@@ -160,6 +161,7 @@
"get-website-favicon": "^0.0.7",
"html-metadata-parser": "^2.0.4",
"html-to-text": "^8.2.1",
"idtoken-verifier": "^2.2.3",
"jsonwebtoken": "^8.5.1",
"keyv": "^4.5.0",
"lodash": "^4.17.21",
@@ -172,6 +174,7 @@
"node-cron": "^3.0.0",
"node-fetch": "^2.6.7",
"node-uuid": "^1.4.8",
"openid-client": "^5.4.0",
"ora": "^5.4.0",
"pdf-parse": "^1.1.1",
"pdf2pic": "^2.1.4",
@@ -1,14 +0,0 @@
import { CommandModule } from "yargs";
const command: CommandModule = {
describe: "Manage Twake Console",
command: "console <command>",
builder: yargs =>
yargs.commandDir("console_cmds", {
visit: commandModule => commandModule.default,
}),
// eslint-disable-next-line @typescript-eslint/no-empty-function
handler: () => {},
};
export default command;
@@ -1,257 +0,0 @@
import yargs from "yargs";
import ora from "ora";
import twake from "../../../twake";
import { CompanyReport, UserReport } from "../../../services/console/types";
import Company from "../../../services/user/entities/company";
import gr from "../../../services/global-resolver";
import { ConsoleServiceImpl } from "../../../services/console/service";
/**
* Merge command parameters. Check the builder definition below for more details.
*/
type MergeParams = {
url: string;
concurrent: number;
dry: boolean;
console: string;
link: boolean;
csv: boolean;
client: string;
secret: string;
};
const services = [
"platform-services",
"user",
"channels",
"notifications",
"database",
"webserver",
"message-queue",
"console",
];
const command: yargs.CommandModule<MergeParams, MergeParams> = {
command: "merge",
describe: "Merge Twake Chat users in the Twake Console",
builder: {
url: {
default: "http://localhost:8080",
type: "string",
description: "URL of the Twake console",
},
concurrent: {
default: 1,
type: "number",
description: "Number of concurrent imports",
},
dry: {
default: false,
type: "boolean",
description: "Make a dry run without creating anything on the Twake console",
},
csv: {
default: false,
type: "boolean",
description: "Generate result as CSV",
},
console: {
default: "console",
type: "string",
description: "The console service identifier to user to link user and companies",
},
link: {
default: true,
type: "boolean",
description:
"Link the companies/users to external companies/user. Works with the --console parameter",
implies: "console",
},
client: {
default: "twake",
type: "string",
description: "Client identifier to be used to authenticate calls to the Console",
},
secret: {
default: "secret",
type: "string",
description: "Client secret to be used to authenticate calls to the Console",
},
},
handler: async argv => {
const spinner = ora({ text: `Importing Twake data on ${argv.url}` }).start();
const platform = await twake.run(services);
await gr.doInit(platform);
const consoleService = platform.getProvider<ConsoleServiceImpl>("console");
const merge = consoleService.merge(
argv.url,
argv.concurrent,
argv.dry,
argv.console,
argv.link,
argv.client,
argv.secret,
);
const start = Date.now();
let stop: number = start;
const userReports: UserReport[] = [];
const companyReports: CompanyReport[] = [];
const ownerReports: UserReport[] = [];
let companiesWithoutAdmin: Company[] = [];
const process = merge.subscribe({
next: report => {
if (report.type === "company:created") {
const companyReport = report.data as CompanyReport;
companyReports.push(companyReport);
if (companyReport.status === "success") {
spinner.succeed(
`Company created: ${companyReport.company.source.id} ${companyReport.company.source.displayName}`,
);
} else {
spinner.fail(
`Creation error for company ${companyReport.company.source.id} (${companyReport.company.source.displayName}): ${companyReport?.error?.message}`,
);
}
}
if (report.type === "user:created") {
const userReport = report.data as UserReport;
userReports.push(userReport);
if (userReport.status === "success") {
spinner.succeed(
`Company ${userReport.user.source.company.id}: User created ${userReport.user.destination.id}`,
);
} else {
spinner.fail(
`Company ${userReport.user.source.company.id}: User creation error ${userReport.user.destination.id}: ${userReport?.error?.message}`,
);
}
}
if (report.type === "user:updated") {
const userReport = report.data as UserReport;
ownerReports.push(userReport);
if (userReport.status === "success") {
spinner.succeed(
`Company ${userReport.destinationCompanyCode} owner updated to user ${userReport.destinationId}`,
);
} else {
spinner.fail(
`Company ${userReport.destinationCompanyCode}: Owner update error for user ${userReport.destinationId}: ${userReport?.error?.message}`,
);
}
}
if (report.type === "processing:owner") {
spinner.start("Computing company owners, please wait...");
}
if (report.type === "user:updating") {
//spinner.succeed(`Updating owner for company ${report?.company?.sourceId}`);
}
if (report.type === "log") {
report.message && (spinner.text = report.message);
}
if (report.type === "company:withoutadmin") {
report.message && (spinner.text = report.message);
const companies = (report.data || []) as Company[];
if (companies.length) {
companiesWithoutAdmin = companies;
}
}
},
error: () => {
spinner.fail("Fatal error");
},
complete: async () => {
stop = Date.now();
spinner.succeed("Merge is complete");
displayStats();
await tearDown();
},
});
async function tearDown() {
await platform.stop();
process.unsubscribe();
}
function reportAsCSV() {
const users = userReports.map(
user =>
`${user.sourceId},${user.destinationId},${user.destinationCompanyCode},${user.status}`,
);
const userCSV = [...["sourceId,destinationId,destinationCompanyCode,status"], ...users].join(
"\n",
);
const companies = companyReports.map(
company => `${company.sourceId},${company.destinationCode},${company.status}`,
);
const companyCSV = [...["sourceId,destinationCode,status"], ...companies].join("\n");
console.log(userCSV);
console.log(companyCSV);
}
function displayStats() {
const userFailures = userReports.filter(user => user.error);
const companyFailures = companyReports.filter(company => company.error);
const ownerFailures = ownerReports.filter(report => report.error);
console.log("# Import report");
console.log(`Data imported in ${(stop - start) / 1000} seconds`);
console.log("## Company");
console.log("- Companies success:", companyReports.filter(company => !company.error).length);
console.log("- Companies failure:", companyFailures.length);
if (companyFailures.length) {
console.log("### Failures");
companyFailures.forEach(failure =>
console.log(
`- Company ${failure.company.source.id} error: ${failure.company.error?.message}`,
),
);
}
console.log("## User");
console.log("- Users success:", userReports.filter(user => !user.error).length);
console.log("- Users failure:", userFailures.length);
if (userFailures.length) {
console.log("### Failures");
userFailures.forEach(failure =>
console.log(
`- User ${failure.user.source.user.user_id} error: ${failure.user.error?.message}`,
),
);
}
console.log("## Owner");
console.log("- Owners success:", ownerReports.filter(report => !report.error).length);
console.log("- Owners failure:", ownerFailures.length);
if (ownerFailures.length) {
console.log("### Failures");
ownerFailures.forEach(report =>
console.log(`- User ${report.destinationId} error: ${report?.error?.message}`),
);
}
console.log("## Warnings");
console.log("### Companies witout admins");
companiesWithoutAdmin.forEach(company => {
console.log(`- ${company.id} - ${company.displayName}`);
});
if (argv.csv) {
reportAsCSV();
}
}
},
};
export default command;
@@ -60,7 +60,7 @@ export interface ConsoleServiceClient {
fetchCompanyInfo(consoleCompanyCode: string): Promise<ConsoleHookCompany>;
getUserByAccessToken(accessToken: string): Promise<ConsoleHookUser>;
getUserByAccessToken(idToken: string): Promise<ConsoleHookUser>;
resendVerificationEmail(email: string): Promise<void>;
}
@@ -95,7 +95,7 @@ export class ConsoleInternalClient implements ConsoleServiceClient {
throw new Error("Method should not be implemented.");
}
getUserByAccessToken(_accessToken: string): Promise<ConsoleHookUser> {
getUserByAccessToken(_idToken: string): Promise<ConsoleHookUser> {
logger.info("Internal: getUserByAccessToken");
throw new Error("Method should not be implemented.");
}
@@ -1,5 +1,5 @@
import axios, { AxiosInstance } from "axios";
import { v1 as uuidv1 } from "uuid";
import { AxiosInstance } from "axios";
import { Issuer } from "openid-client";
import { ConsoleServiceClient } from "../client-interface";
import {
ConsoleCompany,
@@ -14,16 +14,12 @@ import {
UpdatedConsoleUserRole,
} from "../types";
import User, { getInstance } from "../../user/entities/user";
import Company, {
CompanySearchKey,
getInstance as getCompanyInstance,
} from "../../user/entities/company";
import OktaJwtVerifier from "@okta/jwt-verifier";
import { CrudException } from "../../../core/platform/framework/api/crud-service";
import coalesce from "../../../utils/coalesce";
import { logger } from "../../../core/platform/framework/logger";
import { CompanyFeaturesEnum, CompanyLimitsEnum } from "../../user/web/types";
import gr from "../../global-resolver";
import Company, { CompanySearchKey } from "../../user/entities/company";
import User from "../../user/entities/user";
import { ConsoleServiceImpl } from "../service";
export class ConsoleRemoteClient implements ConsoleServiceClient {
@@ -31,92 +27,30 @@ export class ConsoleRemoteClient implements ConsoleServiceClient {
client: AxiosInstance;
private infos: ConsoleOptions;
private verifier: OktaJwtVerifier;
constructor(consoleInstance: ConsoleServiceImpl, private dryRun: boolean) {
this.infos = consoleInstance.consoleOptions;
this.client = axios.create({ baseURL: this.infos.url });
this.verifier = new OktaJwtVerifier({
issuer: this.infos.issuer,
});
}
fetchCompanyInfo(consoleCompanyCode: string): Promise<ConsoleHookCompany> {
throw new Error("Method not implemented.");
}
resendVerificationEmail(email: string): Promise<void> {
throw new Error("Method not implemented.");
}
private auth() {
return { username: this.infos.username, password: this.infos.password };
return {};
}
async addUserToCompany(
company: ConsoleCompany,
user: CreateConsoleUser,
): Promise<CreatedConsoleUser> {
logger.info("Remote: addUserToCompany");
const isNewConsole = this.infos.new_console;
if (this.dryRun) {
return {
_id: uuidv1(),
};
}
if (user.skipInvite && user.name && user.email && user.password) {
return this.client
.post(`/api/companies/${company.code}/users`, user, {
auth: this.auth(),
headers: {
"Content-Type": "application/json",
},
params: {
skipInvite: user.skipInvite,
},
})
.then(({ data }) => data);
} else {
const invitationData = {
role: user.role,
emails: [
{
email: user.email,
role: user.role,
},
],
inviter: { email: user.inviterEmail },
...(isNewConsole
? {
applicationCodes: ["twake"],
}
: {}),
};
const result = await this.client
.post(
`/api/companies/${company.code}/${isNewConsole ? "invitations" : "users/invitation"}`,
invitationData,
{
auth: this.auth(),
headers: {
"Content-Type": "application/json",
},
},
)
.then(async ({ data, status }) => {
//Fixme: When console solve https://gitlab.com/COMPANY_LINAGORA/software/saas/twake-console-account/-/issues/36
// we can remove this fallback
if ([200, 201].indexOf(status) >= 0) {
return data;
} else {
return this.client
.post(`/api/companies/${company.code}/users`, user, {
auth: this.auth(),
headers: {
"Content-Type": "application/json",
},
params: {
skipInvite: user.skipInvite,
},
})
.then(({ data }) => data);
}
});
return result;
}
return null;
}
async updateUserRole(
@@ -124,47 +58,12 @@ export class ConsoleRemoteClient implements ConsoleServiceClient {
user: UpdateConsoleUserRole,
): Promise<UpdatedConsoleUserRole> {
logger.info("Remote: updateUserRole");
if (this.dryRun) {
return {
id: user.id,
role: user.role,
};
}
const result = await this.client
.put(
`/api/companies/${company.code}/users/${user.id}`,
{ role: user.role },
{
auth: this.auth(),
headers: {
"Content-Type": "application/json",
},
},
)
.then(({ data }) => data);
return result;
return null;
}
async createCompany(company: CreateConsoleCompany): Promise<CreatedConsoleCompany> {
logger.info("Remote: createCompany");
if (this.dryRun) {
return company;
}
const result = await this.client
.post("/api/companies", company, {
auth: this.auth(),
headers: {
"Content-Type": "application/json",
},
})
.then(({ data }) => data);
return result;
return null;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -175,216 +74,11 @@ export class ConsoleRemoteClient implements ConsoleServiceClient {
}
async updateLocalCompanyFromConsole(partialCompanyDTO: ConsoleHookCompany): Promise<Company> {
logger.info("Remote: updateLocalCompanyFromConsole");
const companyDTO = await this.fetchCompanyInfo(partialCompanyDTO.details.code);
let company = await gr.services.companies.getCompany({
identity_provider_id: companyDTO.details.code,
});
if (!company) {
const newCompany = getCompanyInstance({
id: uuidv1(),
identity_provider: "console",
identity_provider_id: companyDTO.details.code,
});
company = await gr.services.companies.createCompany(newCompany);
}
const details = companyDTO.details;
if (details) {
company.name = coalesce(details.name, company.name);
company.displayName = coalesce(details.name, company.displayName);
const avatar = details.avatar;
company.logo =
details.logo ||
(avatar && avatar.type && avatar.type !== "url"
? this.infos.url.replace(/\/$/, "") + "/api/avatars/" + avatar.value
: companyDTO.value || "");
}
if (!company.plan) {
company.plan = { name: "", limits: undefined, features: undefined };
}
const limits = companyDTO.limits.twake || companyDTO.limits;
//FIXME this is a hack right now!
let planFeatures: any = {
[CompanyFeaturesEnum.CHAT_GUESTS]: true,
[CompanyFeaturesEnum.CHAT_MESSAGE_HISTORY]: true,
[CompanyFeaturesEnum.CHAT_MULTIPLE_WORKSPACES]: true,
[CompanyFeaturesEnum.CHAT_EDIT_FILES]: true,
[CompanyFeaturesEnum.CHAT_UNLIMITED_STORAGE]: true,
[CompanyFeaturesEnum.COMPANY_INVITE_MEMBER]: true,
};
if (limits.members < 0 && this.infos.type === "remote") {
//Hack to say this is free version
planFeatures = {
[CompanyFeaturesEnum.CHAT_GUESTS]: false,
[CompanyFeaturesEnum.CHAT_MESSAGE_HISTORY]: false,
[CompanyFeaturesEnum.CHAT_MULTIPLE_WORKSPACES]: false,
[CompanyFeaturesEnum.CHAT_EDIT_FILES]: false,
[CompanyFeaturesEnum.CHAT_UNLIMITED_STORAGE]: false, // Currently inactive
};
company.plan.name = "free";
} else {
company.plan.name = "standard";
}
company.plan.features = { ...planFeatures };
company.plan.limits = {
[CompanyLimitsEnum.CHAT_MESSAGE_HISTORY_LIMIT]: 10000, // To remove duplicata since we define this in formatCompany function
[CompanyLimitsEnum.COMPANY_MEMBERS_LIMIT]: limits["members"],
[CompanyLimitsEnum.COMPANY_GUESTS_LIMIT]: limits["guests"],
};
company.stats = coalesce(companyDTO.stats, company.stats);
await gr.services.companies.updateCompany(company);
return company;
return null;
}
async updateLocalUserFromConsole(code: string): Promise<User> {
logger.info("Remote: updateLocalUserFromConsole");
const userDTO = await this.fetchUserInfo(code);
if (!userDTO) {
throw CrudException.badRequest("User not found on Console");
}
const roles = userDTO.roles.filter(
role => role.applications === undefined || role.applications.find(a => a.code === "twake"),
);
let user = await gr.services.users.getByConsoleId(userDTO._id);
if (!user) {
if (!userDTO.email) {
throw CrudException.badRequest("Email is required");
}
let username = userDTO.email
.split("@")[0]
.toLocaleLowerCase()
.replace(/[^a-zA-Z0-9]/g, "")
.replace(/ +/g, "_");
if (await gr.services.users.isEmailAlreadyInUse(userDTO.email)) {
const user = await gr.services.users.getByEmail(userDTO.email);
if (user.identity_provider === "console") {
let emailUserOnConsole = null;
try {
emailUserOnConsole = await this.fetchUserInfo(user.identity_provider_id);
} catch (err) {}
if (emailUserOnConsole?._id) {
throw CrudException.badRequest(
`Console user not created because email already exists on console with different id: ${emailUserOnConsole._id} while requested to provision user with id: ${userDTO._id}`,
);
}
//If not present on console, then anonymise this one and create a new one
await gr.services.users.anonymizeAndDelete(
{ id: user.id },
{
user: { id: user.id, server_request: true },
},
);
//Now we can create the new user
}
}
username = await gr.services.users.getAvailableUsername(username);
if (!username) {
throw CrudException.badRequest("Console user not created because username already exists");
}
user = getInstance({});
user.username_canonical = (username || "").toLocaleLowerCase();
user.email_canonical = userDTO.email;
user.deleted = false;
}
user.email_canonical = coalesce(userDTO.email, user.email_canonical);
user.phone = "";
user.first_name = coalesce(userDTO.name, user.first_name);
user.last_name = coalesce(userDTO.surname, user.last_name);
user.identity_provider = "console";
user.identity_provider_id = userDTO._id;
user.mail_verified = coalesce(userDTO.isVerified, user.mail_verified);
if (userDTO.preference) {
user.preferences = user.preferences || {};
user.preferences.allow_tracking = coalesce(
userDTO.preference.allowTrackingPersonalInfo,
user.preferences?.allow_tracking,
);
user.preferences.language = coalesce(userDTO.preference.locale, user.preferences?.language);
user.preferences.timezone = coalesce(userDTO.preference.timeZone, user.preferences?.timezone);
}
const avatar = userDTO.avatar;
user.picture =
avatar && avatar.type && avatar.type !== "url"
? this.infos.url.replace(/\/$/, "") + "/api/avatars/" + avatar.value
: "";
await gr.services.users.save(user);
const getCompanyByCode = async (companyCode: string) => {
let company = await gr.services.companies.getCompany({
identity_provider_id: companyCode,
});
if (!company) {
const companyDTO = await this.fetchCompanyInfo(companyCode);
await this.updateLocalCompanyFromConsole(companyDTO);
company = await gr.services.companies.getCompany({
identity_provider_id: companyCode,
});
}
return company;
};
const updatedListOfCompanies = [];
if (roles) {
for (const role of roles) {
const companyConsoleCode = role.targetCode;
const roleName = role.roleCode;
const company = await getCompanyByCode(companyConsoleCode);
if (!company) {
throw CrudException.notFound(`Company ${companyConsoleCode} not found`);
}
//Make sure user is active, if not we remove it
if (role.status !== "deactivated") {
updatedListOfCompanies.push(company);
const applications = (role.applications || []).map(a => a.code);
await gr.services.companies.setUserRole(company.id, user.id, roleName, applications);
}
}
}
// Remove user from companies not in the console
const currentCompanies = await gr.services.companies.getAllForUser(user.id);
for (const company of currentCompanies) {
if (!updatedListOfCompanies.map(c => c.id).includes(company.group_id)) {
await gr.services.companies.removeUserFromCompany(
{ id: company.group_id },
{ id: user.id },
);
}
}
await gr.services.users.save(user, { user: { id: user.id, server_request: true } });
return user;
return null;
}
async removeCompanyUser(consoleUserId: string, company: Company): Promise<void> {
@@ -419,81 +113,25 @@ export class ConsoleRemoteClient implements ConsoleServiceClient {
await gr.services.companies.removeCompany(companySearchKey);
}
fetchCompanyInfo(consoleCompanyCode: string): Promise<ConsoleHookCompany> {
logger.info(`Remote: fetchCompanyInfo ${consoleCompanyCode}`);
return this.client
.get(`/api/companies/${consoleCompanyCode}`, {
auth: this.auth(),
headers: {
"Content-Type": "application/json",
},
})
.then(({ data }) => data.company)
.catch(e => {
if (e.response.status === 401) {
throw CrudException.forbidden("Bad console credentials");
}
throw e;
});
}
fetchUserInfo(consoleUserId: string): Promise<ConsoleHookUser> {
logger.info(`Remote: fetchUserInfo ${consoleUserId}`);
return this.client
.get(`/api/users/${consoleUserId}`, {
auth: this.auth(),
headers: {
"Content-Type": "application/json",
},
})
.then(({ data }) => data)
.catch(e => {
if (e.response.status === 401) {
throw CrudException.forbidden("Bad console credentials");
}
throw e;
});
}
getUserByAccessToken(accessToken: string): Promise<ConsoleHookUser> {
logger.info("Remote: getUserByAccessToken");
return this.client
.get("/api/users/profile", {
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${accessToken}`,
},
})
.then(({ data }) => data)
.catch(e => {
if (e.response?.status === 401) {
throw CrudException.forbidden("Bad access token credentials");
}
throw e;
});
}
async resendVerificationEmail(email: string) {
logger.info("Remote: resendVerificationEmail");
return this.client
.post(
"/api/users/resend-verification-email",
{
email,
},
{
auth: this.auth(),
headers: {
"Content-Type": "application/json",
},
},
)
.then(({ data }) => data)
.catch(e => {
if (e.response.status === 401) {
throw CrudException.forbidden("Bad credentials");
}
throw e;
});
async getUserByAccessToken(idToken: string): Promise<ConsoleHookUser> {
const user = (await this.verifier.verifyIdToken(idToken, this.infos.audience)).claims as any;
console.log("user", user);
return {
_id: user.sub,
roles: [] as any,
email: user.email,
name: user.given_name,
surname: user.family_name,
isVerified: true,
preference: {
locale: user.locale,
timeZone: 0,
allowTrackingPersonalInfo: true,
},
avatar: {
type: "url",
value: user.picture,
},
};
}
}
@@ -1,426 +0,0 @@
import passwordGenerator from "generate-password";
import { concat, EMPTY, forkJoin, from, Observable, ReplaySubject } from "rxjs";
import {
count,
distinct,
filter,
groupBy,
map,
mergeMap,
min,
share,
tap,
toArray,
} from "rxjs/operators";
import { getLogger } from "../../../core/platform/framework";
import {
ExecutionContext,
Paginable,
Pagination,
} from "../../../core/platform/framework/api/crud-service";
import Company from "../../user/entities/company";
import User from "../../user/entities/user";
import {
CompanyCreatedStreamObject,
CompanyReport,
ConsoleOptions,
CreatedConsoleCompany,
CreatedConsoleUser,
MergeProgress,
ProcessReport,
UpdateConsoleUserRole,
UserCreatedStreamObject,
UserReport,
} from "../types";
import { getInstance as getExternalUserInstance } from "../../user/entities/external_user";
import { getInstance as getExternalGroupInstance } from "../../user/entities/external_company";
import CompanyUser from "../../user/entities/company_user";
import { ConsoleRemoteClient } from "../clients/remote";
import { ConsoleServiceClient } from "../client-interface";
import { DatabaseServiceAPI } from "../../../core/platform/services/database/api";
import { CompanyUserRole } from "../../user/web/types";
import gr from "../../global-resolver";
import { ConsoleServiceImpl } from "../service";
const logger = getLogger("console.process.merge");
export class MergeProcess {
private client: ConsoleServiceClient;
constructor(
private database: DatabaseServiceAPI,
private dryRun: boolean,
private consoleId: string = "console",
private linkExternal: boolean = true,
consoleClientOptions: ConsoleOptions,
) {
const consoleService = new ConsoleServiceImpl(consoleClientOptions);
this.client = new ConsoleRemoteClient(consoleService, dryRun);
}
merge(concurrent: number = 1): MergeProgress {
const progress$ = new ReplaySubject<ProcessReport>();
const { users$, companies$ } = this.getStreams(concurrent);
const owners$ = users$.pipe(
// get only the admins
filter(userInCompany => userInCompany.source.user.level.valueOf() === 3),
// group by company id
groupBy(userInCompany => userInCompany.source.company.id),
mergeMap(group =>
group.pipe(
// get the admin with the smaller creation date, let's say that this is the first one in the company so it is the owner
min((a, b) => {
return (a.source?.user?.dateAdded || 0) <= (b.source?.user?.dateAdded || 0) ? -1 : 1;
}),
),
),
);
const numberOfAdmins$ = users$.pipe(count(user => user.source.user.level.valueOf() === 3));
const companyWithoutAdmin$ = users$.pipe(
// group by company
groupBy(user => user.source.company.id),
mergeMap(group =>
group.pipe(
// get users as array
// TODO: we may be able to do this without doing a toArray but just by playing with the group
toArray(),
// filter groups where there is no user with level === 3
filter(users => !users.some(user => user.source.user.level.valueOf() === 3)),
// take first element to get the company
map(users => users[0].source.company),
),
),
toArray(),
);
const companiesSubscription = companies$.subscribe({
next(company) {
progress$.next({
type: "company:created",
data: {
sourceId: company.source.id,
destinationCode: company.destination.code,
status: company.error ? "failure" : "success",
company,
error: company.error,
} as CompanyReport,
});
},
});
const usersSubscription = users$.subscribe({
next(user) {
progress$.next({
type: "user:created",
data: {
sourceId: user.source.user.id,
destinationId: user.destination.id,
destinationCompanyCode: user.destination.companyCode,
status: user.error ? "failure" : "success",
user,
error: user.error,
} as UserReport,
});
},
error(err: Error) {
console.error(err);
},
});
const updateOwners$ = owners$.pipe(
mergeMap(owner => this.updateUserRole(owner, "owner"), concurrent),
tap(user =>
progress$.next({
type: "user:updated",
data: {
sourceId: user.source.source.user.id,
destinationId: user.source.destination.id,
destinationCompanyCode: user.source.destination.companyCode,
status: user.result.error ? "failure" : "success",
user: user.source,
error: user.result.error,
} as UserReport,
}),
),
);
const process$ = forkJoin([users$, companies$, numberOfAdmins$, companyWithoutAdmin$]).pipe(
tap(result => {
progress$.next({ type: "log", message: `There are ${result[2]} admins` });
progress$.next({
type: "company:withoutadmin",
message: `Companies without admins ${result[3].length}`,
data: result[3],
});
progress$.next({ type: "processing:owner" });
}),
mergeMap(() => from(updateOwners$), concurrent),
);
const process = process$.subscribe({
complete() {
usersSubscription.unsubscribe();
companiesSubscription.unsubscribe();
process.unsubscribe();
progress$.complete();
},
});
return progress$;
}
private getStreams(
concurrent: number = 1,
context?: ExecutionContext,
): {
// hot companies observable
companies$: Observable<CompanyCreatedStreamObject>;
// hot users observable
users$: Observable<UserCreatedStreamObject>;
} {
const companies$ = this.getCompanies().pipe(
mergeMap(company => this.createCompany(company), concurrent),
// make it hot
share(),
);
const users$ = companies$.pipe(
filter(company => !company.error),
mergeMap(company =>
this.getUserIds(company.source).pipe(
map(user => ({
user,
company,
})),
),
),
mergeMap(
userInCompany => this.createUser(userInCompany.company.source, userInCompany.user, context),
concurrent,
),
// make it hot
share(),
);
return {
companies$,
users$,
};
}
private getUserIds(company: Company, paginable?: Paginable): Observable<CompanyUser> {
const pagination = new Pagination(
paginable?.page_token,
paginable?.limitStr,
paginable?.reversed,
);
return from(gr.services.companies.getUsers({ group_id: company.id }, pagination)).pipe(
mergeMap(companyUsers => {
const items$ = from(companyUsers.getEntities());
const next$ = companyUsers?.nextPage?.page_token
? this.getUserIds(company, companyUsers.nextPage)
: EMPTY;
return concat(items$, next$);
}),
distinct(user => user.user_id),
);
}
private getCompanies(paginable?: Paginable): Observable<Company> {
return from(gr.services.companies.getCompanies(paginable)).pipe(
mergeMap(companiesResult => {
const items$ = from(companiesResult.getEntities());
const next$ = companiesResult?.nextPage?.page_token
? this.getCompanies(companiesResult.nextPage)
: EMPTY;
return concat(items$, next$);
}),
distinct((company: Company) => company.id),
);
}
private async createCompany(company: Company): Promise<CompanyCreatedStreamObject> {
logger.debug("Creating company in the Console %s", company.displayName);
let createdCompany: CreatedConsoleCompany;
let error;
try {
createdCompany = await this.client.createCompany({
code: company.id,
displayName: company.displayName,
avatar: {
type: "url",
value: company.logo,
},
status: "active",
});
if (this.linkExternal) {
await this.createCompanyLink(company, createdCompany, this.consoleId);
}
} catch (err) {
logger.warn("Error while creating company %s", company.displayName);
error = err;
}
return {
source: company,
destination: {
code: createdCompany?.code,
},
error,
};
}
private async createUser(
company: Company,
companyUser: CompanyUser,
context: ExecutionContext,
): Promise<UserCreatedStreamObject> {
logger.debug("Creating user in console %o", companyUser.user_id);
let error: Error;
let result: CreatedConsoleUser;
try {
const user = await gr.services.users.get({ id: companyUser.user_id });
if (!user) {
throw new Error(`User ${companyUser.user_id} not found`);
}
const firstName =
user.first_name && user.first_name.trim().length ? user.first_name : user.email_canonical;
const lastName =
user.last_name && user.last_name.trim().length ? user.last_name : user.email_canonical;
const name = (firstName + " " + lastName).trim();
let role: "admin" | "guest" | "member" =
companyUser.level.valueOf() === 3 ? "admin" : "member";
if (role != "admin") {
if (companyUser.isExterne) {
role = "guest";
}
const workspacesUsers = await gr.services.workspaces.getAllForUser(
{ userId: companyUser.user_id },
{ id: company.id },
);
workspacesUsers.forEach(e => {
if (e.isExternal) {
role = "guest";
}
});
}
result = await this.client.addUserToCompany(
{ code: company.id },
{
email: user.email_canonical,
// console requires that firstname/lastname are defined and at least 1 chat long
firstName,
lastName,
name,
avatar: {
type: "url",
value: user.picture,
},
password: passwordGenerator.generate({
length: 10,
numbers: true,
}),
skipInvite: true,
role,
inviterEmail: "",
},
);
const companyUserRepository = await this.database.getRepository<CompanyUser>(
"group_user",
CompanyUser,
);
companyUser.role = role;
await companyUserRepository.save(companyUser, context);
if (this.linkExternal) {
await this.createUserLink(user, result, this.consoleId);
}
} catch (err) {
logger.warn("Error while creating the user %o", companyUser.user_id);
error = err;
}
return {
source: { user: companyUser, company },
destination: {
id: result?._id,
companyCode: company.id,
},
error,
};
}
private async createUserLink(
localUser: User,
remoteUser: CreatedConsoleUser,
serviceId: string,
): Promise<void> {
logger.debug("Creating user link for user %s", localUser.id);
if (this.dryRun) {
return;
}
await gr.services.externalUser.createExternalUser(
getExternalUserInstance({
service_id: serviceId,
external_id: remoteUser._id,
user_id: localUser.id,
}),
);
}
private async createCompanyLink(
localCompany: Company,
remoteCompany: CreatedConsoleCompany,
serviceId: string,
): Promise<void> {
if (this.dryRun) {
return;
}
await gr.services.externalUser.createExternalGroup(
getExternalGroupInstance({
service_id: serviceId,
company_id: localCompany.id,
external_id: remoteCompany.code,
}),
);
}
private updateUserRole(
user: UserCreatedStreamObject,
role: CompanyUserRole,
): Promise<{
source: UserCreatedStreamObject;
result: UpdateConsoleUserRole & { error?: Error };
}> {
logger.debug(
`Updating user role for user ${user.source.user.id} in company ${user.source.company.id}`,
);
return this.client
.updateUserRole({ code: user.destination.companyCode }, { id: user.destination.id, role })
.then(result => ({ source: user, result }))
.catch((err: Error) => {
return {
source: user,
result: {
id: user.destination.id,
role,
error: err,
},
};
});
}
}
@@ -1,5 +1,4 @@
import { DatabaseServiceAPI } from "../../core/platform/services/database/api";
import { MergeProcess } from "./processing/merge";
import { ConsoleOptions, ConsoleType, MergeProgress } from "./types";
import { ConsoleServiceClient } from "./client-interface";
import { ConsoleClientFactory } from "./client-factory";
@@ -24,7 +23,7 @@ export class ConsoleServiceImpl implements TwakeServiceProvider {
}
async init(): Promise<this> {
this.configuration = new Configuration("console");
this.configuration = new Configuration("general.accounts");
assert(this.configuration, "console configuration is missing");
const type = this.configuration.get("type") as ConsoleType;
assert(type, "console configuration type is not defined");
@@ -33,14 +32,12 @@ export class ConsoleServiceImpl implements TwakeServiceProvider {
this.consoleOptions = {
type: type,
new_console: s.new_console,
username: s.username,
password: s.password,
url: s.url,
hook: {
token: s.hook?.token,
public_key: s.hook?.public_key,
},
authority: s.authority,
client_id: s.client_id,
client_secret: s.client_secret,
audience: s.audience,
issuer: s.issuer,
redirect_uris: s.redirect_uris,
disable_account_creation: s.disable_account_creation,
};
@@ -50,24 +47,6 @@ export class ConsoleServiceImpl implements TwakeServiceProvider {
return this;
}
merge(
baseUrl: string,
concurrent: number = 1,
dryRun: boolean = false,
console: string = "console",
link: boolean = true,
client: string,
secret: string,
_context?: ExecutionContext,
): MergeProgress {
return new MergeProcess(this.services.database, dryRun, console, link, {
type: "remote",
username: client,
password: secret,
url: baseUrl,
} as ConsoleOptions).merge(concurrent);
}
getClient(): ConsoleServiceClient {
return ConsoleClientFactory.create(this);
}
@@ -126,14 +126,12 @@ export type ConsoleType = "remote" | "internal";
export type ConsoleOptions = {
type: ConsoleType;
new_console: boolean;
username: string;
password: string;
url: string;
hook: {
token: string;
public_key?: string;
};
authority: string;
client_id: string;
client_secret: string;
issuer: string;
audience: string;
redirect_uris: string[];
disable_account_creation: boolean;
};
@@ -231,7 +229,7 @@ export interface ConsoleExecutionContext extends ExecutionContext {
export interface AuthRequest {
email?: string;
password?: string;
remote_access_token?: string;
oidc_id_token?: string;
}
export interface AuthResponse {
@@ -31,8 +31,8 @@ export class ConsoleController {
}
async auth(request: FastifyRequest<{ Body: AuthRequest }>): Promise<AuthResponse> {
if (request.body.remote_access_token) {
return { access_token: await this.authByToken(request.body.remote_access_token) };
if (request.body.oidc_id_token) {
return { access_token: await this.authByToken(request.body.oidc_id_token) };
} else if (request.body.email && request.body.password) {
return { access_token: await this.authByPassword(request.body.email, request.body.password) };
} else {
@@ -283,12 +283,12 @@ export class ConsoleController {
});
}
private async authByToken(accessToken: string): Promise<AccessToken> {
private async authByToken(idToken: string): Promise<AccessToken> {
const client = gr.services.console.getClient();
const userDTO = await client.getUserByAccessToken(accessToken);
const userDTO = await client.getUserByAccessToken(idToken);
const user = await client.updateLocalUserFromConsole(userDTO._id);
if (!user) {
throw CrudException.notFound(`User details not found for access token ${accessToken}`);
throw CrudException.notFound(`User details not found for access token ${idToken}`);
}
return gr.platformServices.auth.generateJWT(user.id, user.email_canonical, {
track: user?.preferences?.allow_tracking || false,
@@ -14,27 +14,7 @@ const routes: FastifyPluginCallback = (fastify: FastifyInstance, options, next)
const accessControl = async (
request: FastifyRequest<{ Body: ConsoleHookBody; Querystring: ConsoleHookQueryString }>,
) => {
if (gr.services.console.consoleType != "remote") {
throw fastify.httpErrors.notImplemented("Hook service is only for the remote console");
}
if (
(request.body.secret_key || request.query.secret_key) !==
gr.services.console.consoleOptions.hook.token
) {
throw fastify.httpErrors.forbidden("Wrong secret");
}
const publicKey = gr.services.console.consoleOptions.hook.public_key;
if (publicKey) {
const input = JSON.stringify({ content: request.body.content, type: request.body.type });
const signatureBase64 = request.body.signature;
const verifier = crypto.createVerify("RSA-SHA512").update(input);
if (!verifier.verify(publicKey, signatureBase64, "base64")) {
throw fastify.httpErrors.forbidden("Signature verification failed");
}
}
throw fastify.httpErrors.notImplemented("Hook service doesn't exist anymore");
};
fastify.route({
@@ -32,7 +32,7 @@ export const authenticationSchema = {
properties: {
email: { type: "string" },
password: { type: "string" },
remote_access_token: { type: "string" },
oidc_id_token: { type: "string" },
},
},
response: {
@@ -16,14 +16,15 @@ export type ServerConfiguration = {
help_url: string | null;
pricing_plan_url: string | null;
app_download_url: string | null;
app_grid: { logo: string; name: string; url: string }[];
mobile: {
mobile_redirect: string;
mobile_appstore: string;
mobile_googleplay: string;
};
accounts: {
type: "console" | "internal";
console: null | {
type: "remote" | "internal";
remote: null | {
authority: string;
client_id: string;
account_management_url: string;
@@ -24,11 +24,12 @@ const routes: FastifyPluginCallback<{ configuration: ServerConfiguration["config
"pricing_plan_url",
"mobile",
"app_download_url",
"app_grid",
),
accounts: {
type: accounts.type,
console: _.pick(
accounts.console,
remote: _.pick(
accounts.remote,
"authority",
"client_id",
"account_management_url",
+190 -4
View File
@@ -1456,6 +1456,14 @@
"@nodelib/fs.scandir" "2.1.5"
fastq "^1.6.0"
"@okta/jwt-verifier@^3.0.1":
version "3.0.1"
resolved "https://registry.yarnpkg.com/@okta/jwt-verifier/-/jwt-verifier-3.0.1.tgz#10facad0424052e0cfd6477304cdc51d84ac60be"
integrity sha512-8rNvCxAcgGlSJH9NilhYTrbZjyTPPF0ThJUDF/NI8XV/4NcqOFdO946meFsXpV1oFdZP6ASH/j2GjVhU4cHpPQ==
dependencies:
jwks-rsa "^3.0.0"
njwt "^2.0.0"
"@redis/bloom@1.0.2":
version "1.0.2"
resolved "https://registry.yarnpkg.com/@redis/bloom/-/bloom-1.0.2.tgz#42b82ec399a92db05e29fffcdfd9235a5fc15cdf"
@@ -1703,6 +1711,14 @@
resolved "https://registry.yarnpkg.com/@types/bluebird/-/bluebird-3.5.36.tgz#00d9301d4dc35c2f6465a8aec634bb533674c652"
integrity sha512-HBNx4lhkxN7bx6P0++W8E289foSu8kO8GCk2unhuVggO+cE7rh9DhZUyPhUxNRG9m+5B5BTKxZQ5ZP92x/mx9Q==
"@types/body-parser@*":
version "1.19.2"
resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.2.tgz#aea2059e28b7658639081347ac4fab3de166e6f0"
integrity sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==
dependencies:
"@types/connect" "*"
"@types/node" "*"
"@types/busboy@^0.2.3":
version "0.2.4"
resolved "https://registry.yarnpkg.com/@types/busboy/-/busboy-0.2.4.tgz#19922f8c7076ad6d47b2565da8c0a94c88776315"
@@ -1740,6 +1756,13 @@
resolved "https://registry.yarnpkg.com/@types/config/-/config-0.0.36.tgz#bf53ca640f3a1a6a2072a9f33e02a44def07a40b"
integrity sha512-EoAeT1MyFWh2BJvBDEFInY714bQBbHOAucqxqqhprhbBFqr+B7fuN5T9CJqUIGDzvwubnKKRqmSo6yPo0aSpNw==
"@types/connect@*":
version "3.4.35"
resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.35.tgz#5fcf6ae445e4021d1fc2219a4873cc73a3bb2ad1"
integrity sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==
dependencies:
"@types/node" "*"
"@types/cookie@^0.4.1":
version "0.4.1"
resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.4.1.tgz#bfd02c1f2224567676c1545199f87c3a861d878d"
@@ -1768,6 +1791,25 @@
resolved "https://registry.npmjs.org/@types/estree/-/estree-0.0.52.tgz"
integrity sha512-BZWrtCU0bMVAIliIV+HJO1f1PR41M7NKjfxrFJwwhKI1KwhwOxYw1SXg9ao+CIMt774nFuGiG6eU+udtbEI9oQ==
"@types/express-serve-static-core@^4.17.33":
version "4.17.33"
resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.33.tgz#de35d30a9d637dc1450ad18dd583d75d5733d543"
integrity sha512-TPBqmR/HRYI3eC2E5hmiivIzv+bidAfXofM+sbonAGvyDhySGw9/PQZFt2BLOrjUUR++4eJVpx6KnLQK1Fk9tA==
dependencies:
"@types/node" "*"
"@types/qs" "*"
"@types/range-parser" "*"
"@types/express@^4.17.14":
version "4.17.17"
resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.17.tgz#01d5437f6ef9cfa8668e616e13c2f2ac9a491ae4"
integrity sha512-Q4FmmuLGBG58btUnfS1c1r/NQdlp3DMfGDGig8WhfpA2YRUtEkxAjkZb0yvplJGYdF1fsQ81iMDcH24sSCNC/Q==
dependencies:
"@types/body-parser" "*"
"@types/express-serve-static-core" "^4.17.33"
"@types/qs" "*"
"@types/serve-static" "*"
"@types/fastify-multipart@^0.7.0":
version "0.7.0"
resolved "https://registry.yarnpkg.com/@types/fastify-multipart/-/fastify-multipart-0.7.0.tgz#fcbabfb35e69af0290a1c6c83199fc7e03855a51"
@@ -1853,6 +1895,13 @@
dependencies:
"@types/node" "*"
"@types/jsonwebtoken@^9.0.0":
version "9.0.1"
resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-9.0.1.tgz#29b1369c4774200d6d6f63135bf3d1ba3ef997a4"
integrity sha512-c5ltxazpWabia/4UzhIoaDcIza4KViOQhdbjRlfcIGVnsE3c3brkz9Z+F/EeJIECOQP7W7US2hNE930cWWkPiw==
dependencies:
"@types/node" "*"
"@types/keyv@^3.1.4":
version "3.1.4"
resolved "https://registry.yarnpkg.com/@types/keyv/-/keyv-3.1.4.tgz#3ccdb1c6751b0c7e52300bcdacd5bcbf8faa75b6"
@@ -1870,6 +1919,11 @@
resolved "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz"
integrity sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==
"@types/mime@*":
version "3.0.1"
resolved "https://registry.yarnpkg.com/@types/mime/-/mime-3.0.1.tgz#5f8f2bca0a5863cb69bc0b0acd88c96cb1d4ae10"
integrity sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==
"@types/minimatch@*":
version "5.1.2"
resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-5.1.2.tgz#07508b45797cb81ec3f273011b054cd0755eddca"
@@ -1926,6 +1980,11 @@
resolved "https://registry.npmjs.org/@types/node/-/node-14.18.21.tgz"
integrity sha512-x5W9s+8P4XteaxT/jKF0PSb7XEvo5VmqEWgsMlyeY4ZlLK8I6aH6g5TPPyDlLAep+GYf4kefb7HFyc7PAO3m+Q==
"@types/node@^15.0.1":
version "15.14.9"
resolved "https://registry.yarnpkg.com/@types/node/-/node-15.14.9.tgz#bc43c990c3c9be7281868bbc7b8fdd6e2b57adfa"
integrity sha512-qjd88DrCxupx/kJD5yQgZdcYKZKSIGBVDIBE1/LTGcNm3d2Np/jxojkdePDdfnBHJc5W7vSMpbJ1aB7p/Py69A==
"@types/normalize-package-data@^2.4.0":
version "2.4.1"
resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz#d3357479a0fdfdd5907fe67e17e0a85c906e1301"
@@ -1986,11 +2045,21 @@
dependencies:
"@types/node" "*"
"@types/qs@*":
version "6.9.7"
resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.7.tgz#63bb7d067db107cc1e457c303bc25d511febf6cb"
integrity sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==
"@types/random-useragent@^0.3.1":
version "0.3.1"
resolved "https://registry.yarnpkg.com/@types/random-useragent/-/random-useragent-0.3.1.tgz#1f0e1e08151f04d2c023ab77c8173dfa5b5049d2"
integrity sha512-yVz7897JjjwS6V+lmxlu8pktzNnNWgtF9MGxfaRfI8o+hQageO9K+0hfwr8PspdFJIIRE9KkLqp4o5tlQqv/0w==
"@types/range-parser@*":
version "1.2.4"
resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc"
integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==
"@types/redis@^4.0.11":
version "4.0.11"
resolved "https://registry.yarnpkg.com/@types/redis/-/redis-4.0.11.tgz#0bb4c11ac9900a21ad40d2a6768ec6aaf651c0e1"
@@ -2010,6 +2079,14 @@
resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.13.tgz#da4bfd73f49bd541d28920ab0e2bf0ee80f71c91"
integrity sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==
"@types/serve-static@*":
version "1.15.1"
resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.15.1.tgz#86b1753f0be4f9a1bee68d459fcda5be4ea52b5d"
integrity sha512-NUo5XNiAdULrJENtJXZZ3fHtfMolzZwczzBbnAeBbqBwG+LaG6YaJtuwzwGSQZ2wsCrxjEhNNjAkKigy3n8teQ==
dependencies:
"@types/mime" "*"
"@types/node" "*"
"@types/sharp@^0.29.5":
version "0.29.5"
resolved "https://registry.yarnpkg.com/@types/sharp/-/sharp-0.29.5.tgz#9c7032d30d138ad16dde6326beaff2af757b91b3"
@@ -2732,7 +2809,7 @@ base64-arraybuffer@0.1.4:
resolved "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.4.tgz"
integrity sha512-a1eIFi4R9ySrbiMuyTGx5e92uRH5tQY6kArNcFaKBUleIoLjdjBg7Zxm3Mqm3Kmkf27HLR/1fnxX9q8GQ7Iavg==
base64-js@^1.3.1:
base64-js@^1.3.1, base64-js@^1.5.1:
version "1.5.1"
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
@@ -3647,6 +3724,11 @@ crypto-browserify@^3.12.0:
randombytes "^2.0.0"
randomfill "^1.0.3"
crypto-js@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/crypto-js/-/crypto-js-4.1.1.tgz#9e485bcf03521041bd85844786b83fb7619736cf"
integrity sha512-o2JlM7ydqd3Qk9CA0L4NL6mTzU2sdx96a+oOfPu8Mkl/PK51vSyoi8/rQ8NknZtk44vq15lmhAj9CIAGwgeWKw==
crypto-random-string@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5"
@@ -4031,7 +4113,7 @@ dynamic-dedupe@^0.3.0:
dependencies:
xtend "^4.0.0"
ecdsa-sig-formatter@1.0.11:
ecdsa-sig-formatter@1.0.11, ecdsa-sig-formatter@^1.0.5:
version "1.0.11"
resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf"
integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==
@@ -4213,6 +4295,11 @@ es6-error@^4.1.1:
resolved "https://registry.yarnpkg.com/es6-error/-/es6-error-4.1.1.tgz#9e3af407459deed47e9a91f9b885a84eb05c561d"
integrity sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==
es6-promise@^4.2.8:
version "4.2.8"
resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a"
integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==
escalade@^3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
@@ -5572,6 +5659,18 @@ iconv-lite@0.4.24, iconv-lite@^0.4.4:
dependencies:
safer-buffer ">= 2.1.2 < 3"
idtoken-verifier@^2.2.3:
version "2.2.3"
resolved "https://registry.yarnpkg.com/idtoken-verifier/-/idtoken-verifier-2.2.3.tgz#1758e9b0596f7036134938d63e107a72045622b8"
integrity sha512-hhpzB+MRgEvbwqzRLFdVbG55lKdXQVfeYEjAA2qu0UC72MSLeR0nX7P7rY5Dycz1aISHPOwq80hIPFoJ/+SItA==
dependencies:
base64-js "^1.5.1"
crypto-js "^4.1.1"
es6-promise "^4.2.8"
jsbn "^1.1.0"
unfetch "^4.2.0"
url-join "^4.0.1"
ieee754@^1.1.13, ieee754@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
@@ -6475,6 +6574,11 @@ join-component@^1.1.0:
resolved "https://registry.npmjs.org/join-component/-/join-component-1.1.0.tgz"
integrity sha512-bF7vcQxbODoGK1imE2P9GS9aw4zD0Sd+Hni68IMZLj7zRnquH7dXUmMw9hDI5S/Jzt7q+IyTXN0rSg2GI0IKhQ==
jose@^4.10.0, jose@^4.10.4:
version "4.13.1"
resolved "https://registry.yarnpkg.com/jose/-/jose-4.13.1.tgz#449111bb5ab171db85c03f1bd2cb1647ca06db1c"
integrity sha512-MSJQC5vXco5Br38mzaQKiq9mwt7lwj2eXpgpRyQYNHYt2lq1PjkWa7DLXX0WVcQLE9HhMh3jPiufS7fhJf+CLQ==
joycon@^3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/joycon/-/joycon-3.1.1.tgz#bce8596d6ae808f8b68168f5fc69280996894f03"
@@ -6500,6 +6604,11 @@ js-yaml@^4.0.0:
dependencies:
argparse "^2.0.1"
jsbn@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-1.1.0.tgz#b01307cb29b618a1ed26ec79e911f803c4da0040"
integrity sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==
jsdom@^16.4.0:
version "16.7.0"
resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-16.7.0.tgz#918ae71965424b197c819f8183a754e18977b710"
@@ -6651,6 +6760,18 @@ jwa@^1.4.1:
ecdsa-sig-formatter "1.0.11"
safe-buffer "^5.0.1"
jwks-rsa@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/jwks-rsa/-/jwks-rsa-3.0.1.tgz#ba79ddca7ee7520f7bb26b942ef1aee91df8d7e4"
integrity sha512-UUOZ0CVReK1QVU3rbi9bC7N5/le8ziUj0A2ef1Q0M7OPD2KvjEYizptqIxGIo6fSLYDkqBrazILS18tYuRc8gw==
dependencies:
"@types/express" "^4.17.14"
"@types/jsonwebtoken" "^9.0.0"
debug "^4.3.4"
jose "^4.10.4"
limiter "^1.1.5"
lru-memoizer "^2.1.4"
jws@^3.2.2:
version "3.2.2"
resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304"
@@ -6747,6 +6868,11 @@ light-my-request@^4.2.0:
process-warning "^1.0.0"
set-cookie-parser "^2.4.1"
limiter@^1.1.5:
version "1.1.5"
resolved "https://registry.yarnpkg.com/limiter/-/limiter-1.1.5.tgz#8f92a25b3b16c6131293a0cc834b4a838a2aa7c2"
integrity sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==
lines-and-columns@^1.1.6:
version "1.2.4"
resolved "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz"
@@ -6776,6 +6902,11 @@ locate-path@^6.0.0:
dependencies:
p-locate "^5.0.0"
lodash.clonedeep@^4.5.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef"
integrity sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==
lodash.debounce@^4.0.8:
version "4.0.8"
resolved "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz"
@@ -6896,6 +7027,22 @@ lru-cache@^6.0.0:
dependencies:
yallist "^4.0.0"
lru-cache@~4.0.0:
version "4.0.2"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.0.2.tgz#1d17679c069cda5d040991a09dbc2c0db377e55e"
integrity sha512-uQw9OqphAGiZhkuPlpFGmdTU2tEuhxTourM/19qGJrxBPHAr/f8BT1a0i/lOclESnGatdJG/UCkP9kZB/Lh1iw==
dependencies:
pseudomap "^1.0.1"
yallist "^2.0.0"
lru-memoizer@^2.1.4:
version "2.2.0"
resolved "https://registry.yarnpkg.com/lru-memoizer/-/lru-memoizer-2.2.0.tgz#b9d90c91637b4b1a423ef76f3156566691293df8"
integrity sha512-QfOZ6jNkxCcM/BkIPnFsqDhtrazLRsghi9mBwFAzol5GCvj4EkFT899Za3+QwikCg5sRX8JstioBDwOxEyzaNw==
dependencies:
lodash.clonedeep "^4.5.0"
lru-cache "~4.0.0"
lru_map@^0.3.3:
version "0.3.3"
resolved "https://registry.npmjs.org/lru_map/-/lru_map-0.3.3.tgz"
@@ -7329,6 +7476,15 @@ nice-try@^1.0.4:
resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==
njwt@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/njwt/-/njwt-2.0.0.tgz#da8b7ad995980de67b8069dad63949d2bd5df27d"
integrity sha512-1RcqirhCqThBEe4KO83pFg0wPBa1c9NiXNCrocD2EbZqb6ksWWDVnp/w/p0gsyUcVa05PhhaaPjs9rc/GLmdxQ==
dependencies:
"@types/node" "^15.0.1"
ecdsa-sig-formatter "^1.0.5"
uuid "^8.3.2"
node-abi@^3.3.0:
version "3.22.0"
resolved "https://registry.npmjs.org/node-abi/-/node-abi-3.22.0.tgz"
@@ -7545,6 +7701,11 @@ object-copy@^0.1.0:
define-property "^0.2.5"
kind-of "^3.0.3"
object-hash@^2.0.1:
version "2.2.0"
resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-2.2.0.tgz#5ad518581eefc443bd763472b8ff2e9c2c0d54a5"
integrity sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==
object-inspect@^1.12.0, object-inspect@^1.9.0:
version "1.12.2"
resolved "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz"
@@ -7579,6 +7740,11 @@ object.pick@^1.3.0:
dependencies:
isobject "^3.0.1"
oidc-token-hash@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/oidc-token-hash/-/oidc-token-hash-5.0.1.tgz#ae6beec3ec20f0fd885e5400d175191d6e2f10c6"
integrity sha512-EvoOtz6FIEBzE+9q253HsLCVRiK/0doEJ2HCvvqMQb3dHZrP3WlJKYtJ55CRTw4jmYomzH4wkPuCj/I3ZvpKxQ==
on-exit-leak-free@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/on-exit-leak-free/-/on-exit-leak-free-2.1.0.tgz#5c703c968f7e7f851885f6459bf8a8a57edc9cc4"
@@ -7610,6 +7776,16 @@ openapi-types@^10.0.0:
resolved "https://registry.npmjs.org/openapi-types/-/openapi-types-10.0.0.tgz"
integrity sha512-Y8xOCT2eiKGYDzMW9R4x5cmfc3vGaaI4EL2pwhDmodWw1HlK18YcZ4uJxc7Rdp7/gGzAygzH9SXr6GKYIXbRcQ==
openid-client@^5.4.0:
version "5.4.0"
resolved "https://registry.yarnpkg.com/openid-client/-/openid-client-5.4.0.tgz#77f1cda14e2911446f16ea3f455fc7c405103eac"
integrity sha512-hgJa2aQKcM2hn3eyVtN12tEA45ECjTJPXCgUh5YzTzy9qwapCvmDTVPWOcWVL0d34zeQoQ/hbG9lJhl3AYxJlQ==
dependencies:
jose "^4.10.0"
lru-cache "^6.0.0"
object-hash "^2.0.1"
oidc-token-hash "^5.0.1"
optionator@^0.8.1:
version "0.8.3"
resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495"
@@ -8077,7 +8253,7 @@ ps-tree@^1.2.0:
dependencies:
event-stream "=3.3.4"
pseudomap@^1.0.2:
pseudomap@^1.0.1, pseudomap@^1.0.2:
version "1.0.2"
resolved "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz"
integrity sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==
@@ -9695,6 +9871,11 @@ undici@^3.3.6:
resolved "https://registry.yarnpkg.com/undici/-/undici-3.3.6.tgz#06d3b97b7eeff46bce6f8a71079c09f64dd59dc1"
integrity sha512-/j3YTZ5AobMB4ZrTY72mzM54uFUX32v0R/JRW9G2vOyF1uSKYAx+WT8dMsAcRS13TOFISv094TxIyWYk+WEPsA==
unfetch@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/unfetch/-/unfetch-4.2.0.tgz#7e21b0ef7d363d8d9af0fb929a5555f6ef97a3be"
integrity sha512-F9p7yYCn6cIW9El1zi0HI6vqpeIvBsr3dSuRO6Xuppb1u5rXpCPmMvLSyECLhybr9isec8Ohl0hPekMVrEinDA==
unicode-canonical-property-names-ecmascript@^2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz"
@@ -9801,6 +9982,11 @@ urix@^0.1.0:
resolved "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz"
integrity sha512-Am1ousAhSLBeB9cG/7k7r2R0zj50uDRlZHPGbazid5s9rlF1F/QKYObEKSIunSjIOkJZqwRRLpvewjEkM7pSqg==
url-join@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/url-join/-/url-join-4.0.1.tgz#b642e21a2646808ffa178c4c5fda39844e12cde7"
integrity sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==
url-parse-lax@^3.0.0:
version "3.0.0"
resolved "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz"
@@ -10148,7 +10334,7 @@ yallist@4.0.0, yallist@^4.0.0:
resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
yallist@^2.1.2:
yallist@^2.0.0, yallist@^2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52"
integrity sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==