New Drive UI initial version
This commit is contained in:
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user