📁 Changed TDrive root folder (#16)

📁 Changed TDrive root folder
This commit is contained in:
Montassar Ghanmy
2023-04-11 10:04:29 +01:00
committed by GitHub
parent 3b3f67af07
commit e0615fa867
10548 changed files with 48 additions and 48 deletions
@@ -0,0 +1,21 @@
import { ConsoleRemoteClient } from "./clients/remote";
import { ConsoleServiceClient } from "./client-interface";
import { ConsoleType } from "./types";
import { ConsoleInternalClient } from "./clients/internal";
import { ConsoleServiceImpl } from "./service";
class StaticConsoleClientFactory {
create(consoleInstance: ConsoleServiceImpl): ConsoleServiceClient {
const type: ConsoleType = consoleInstance.consoleType;
switch (type) {
case "remote":
return new ConsoleRemoteClient(consoleInstance);
case "internal":
return new ConsoleInternalClient(consoleInstance);
default:
throw new Error(`${type} is not supported`);
}
}
}
export const ConsoleClientFactory = new StaticConsoleClientFactory();
@@ -0,0 +1,66 @@
import {
ConsoleCompany,
ConsoleHookCompany,
ConsoleHookUser,
CreateConsoleCompany,
CreateConsoleUser,
CreatedConsoleCompany,
CreatedConsoleUser,
CreateInternalUser,
UpdateConsoleUserRole,
UpdatedConsoleUserRole,
} from "./types";
import User from "../user/entities/user";
import Company, { CompanySearchKey } from "../user/entities/company";
export interface ConsoleServiceClient {
/**
* Create a company
*
* @param company
*/
createCompany(company: CreateConsoleCompany): Promise<CreatedConsoleCompany>;
/**
* Add user to company
*
* @param company
* @param user
* @param inviter
*/
addUserToCompany(company: ConsoleCompany, user: CreateConsoleUser): Promise<CreatedConsoleUser>;
/**
* Add user to tdrive in general (for non-console version)
*
* @param user
*/
addUserToTdrive(user: CreateInternalUser): Promise<User>;
/**
* Update user role
*
* @param company
* @param user
*/
updateUserRole(
company: ConsoleCompany,
user: UpdateConsoleUserRole,
): Promise<UpdatedConsoleUserRole>;
updateLocalCompanyFromConsole(companyDTO: ConsoleHookCompany): Promise<Company>;
updateLocalUserFromConsole(user: ConsoleHookUser): Promise<User>;
removeCompanyUser(consoleUserId: string, company: Company): Promise<void>;
removeUser(consoleUserId: string): Promise<void>;
removeCompany(companySearchKey: CompanySearchKey): Promise<void>;
fetchCompanyInfo(consoleCompanyCode: string): Promise<ConsoleHookCompany>;
getUserByAccessToken(idToken: string): Promise<ConsoleHookUser>;
resendVerificationEmail(email: string): Promise<void>;
}
@@ -0,0 +1,107 @@
import { AxiosInstance } from "axios";
import { merge } from "lodash";
import { ConsoleServiceClient } from "../client-interface";
import {
ConsoleCompany,
ConsoleHookCompany,
ConsoleHookUser,
CreateConsoleCompany,
CreateConsoleUser,
CreatedConsoleCompany,
CreatedConsoleUser,
UpdateConsoleUserRole,
UpdatedConsoleUserRole,
} from "../types";
import { v1 as uuidv1 } from "uuid";
import User, { getInstance as getUserInstance } from "../../user/entities/user";
import Company, { CompanySearchKey } from "../../user/entities/company";
import { logger } from "../../../core/platform/framework/logger";
import gr from "../../global-resolver";
import { ConsoleServiceImpl } from "../service";
export class ConsoleInternalClient implements ConsoleServiceClient {
version: "1";
client: AxiosInstance;
constructor(private consoleInstance: ConsoleServiceImpl) {}
async addUserToCompany(
company: ConsoleCompany,
user: CreateConsoleUser,
): Promise<CreatedConsoleUser> {
logger.info("Internal: addUserToCompany");
await gr.services.companies.setUserRole(company.id, user.id, user.role);
return merge(user, { _id: user.id });
}
async updateUserRole(
_company: ConsoleCompany,
_user: UpdateConsoleUserRole,
): Promise<UpdatedConsoleUserRole> {
logger.info("Internal: updateUserRole");
throw Error("ConsoleInternalClient.updateUserRole is not implemented");
}
async createCompany(_company: CreateConsoleCompany): Promise<CreatedConsoleCompany> {
logger.info("Internal: ");
throw Error("ConsoleInternalClient.createCompany is not implemented");
}
async addUserToTdrive(user: CreateConsoleUser): Promise<User> {
logger.info("Internal: addUserToTdrive");
const userToCreate = getUserInstance({
id: uuidv1(),
email_canonical: user.email,
first_name: user.firstName,
last_name: user.lastName,
});
const createdUser = await gr.services.users.save(userToCreate).then(result => result.entity);
if (user.password) {
await gr.services.users.setPassword({ id: createdUser.id }, user.password);
}
return createdUser;
}
updateLocalCompanyFromConsole(_companyDTO: ConsoleHookCompany): Promise<Company> {
logger.info("Internal: updateLocalCompanyFromConsole");
throw new Error("Method should not be implemented.");
}
updateLocalUserFromConsole(_user: ConsoleHookUser): Promise<User> {
logger.info("Internal: updateLocalUserFromConsole");
throw new Error("Method should not be implemented.");
}
removeCompany(_companySearchKey: CompanySearchKey): Promise<void> {
logger.info("Internal: removeCompany");
throw new Error("Method should not be implemented.");
}
removeCompanyUser(_consoleUserId: string, _company: Company): Promise<void> {
logger.info("Internal: removeCompanyUser");
throw new Error("Method should not be implemented.");
}
removeUser(_consoleUserId: string): Promise<void> {
logger.info("Internal: removeUser");
throw new Error("Method should not be implemented.");
}
fetchCompanyInfo(_consoleCompanyCode: string): Promise<ConsoleHookCompany> {
logger.info("Internal: fetchCompanyInfo");
throw new Error("Method should not be implemented.");
}
getUserByAccessToken(_idToken: string): Promise<ConsoleHookUser> {
logger.info("Internal: getUserByAccessToken");
throw new Error("Method should not be implemented.");
}
resendVerificationEmail(_accessToken: string): Promise<void> {
logger.info("Internal: resendVerificationEmail");
throw new Error("Method should not be implemented.");
}
}
@@ -0,0 +1,97 @@
import JwksClient from "jwks-rsa";
import nJwt from "njwt";
function verifyAudience(expected: string, aud: string | Array<string>) {
if (!expected) {
throw new Error("expected audience is required");
}
if (Array.isArray(aud) && !aud.includes(expected)) {
throw new Error(
`audience claim ${expected} does not match one of the expected audiences: ${aud.join(", ")}`,
);
}
if (!Array.isArray(aud) && aud !== expected) {
throw new Error(`audience claim ${aud} does not match expected audience: ${expected}`);
}
}
function verifyIssuer(expected: string, issuer: string) {
if (issuer !== expected) {
throw new Error(`issuer ${issuer} does not match expected issuer: ${expected}`);
}
}
export class OidcJwtVerifier {
public claimsToAssert: any;
private issuer: string;
private jwksUri: string;
private jwksClient: any;
private verifier: any;
constructor(options: { [key: string]: any } = {}) {
// https://github.com/auth0/node-jwks-rsa/blob/master/CHANGELOG.md#request-agent-options
if (options.requestAgentOptions) {
// jwks-rsa no longer accepts 'requestAgentOptions' and instead requires a http(s).Agent be passed directly
const msg = `\`requestAgentOptions\` has been deprecated, use \`requestAgent\` instead.
For more info see https://github.com/auth0/node-jwks-rsa/blob/master/CHANGELOG.md#request-agent-options`;
throw new Error(msg);
}
this.claimsToAssert = options.assertClaims || {};
this.issuer = options.issuer;
this.jwksUri = options.jwksUri;
this.jwksClient = JwksClient({
jwksUri: this.jwksUri,
cache: true,
cacheMaxAge: options.cacheMaxAge || 60 * 60 * 1000,
cacheMaxEntries: 3,
jwksRequestsPerMinute: options.jwksRequestsPerMinute || 10,
rateLimit: true,
requestAgent: options.requestAgent,
});
this.verifier = nJwt
.createVerifier()
.setSigningAlgorithm("RS256")
.withKeyResolver((kid: string, cb: any) => {
if (kid) {
this.jwksClient.getSigningKey(kid, (err: any, key: any) => {
cb(err, key && (key.publicKey || key.rsaPublicKey));
});
} else {
cb("No KID specified", null);
}
return null;
});
}
async verifyAsPromise(tokenString: string): Promise<any> {
return new Promise((resolve, reject) => {
// Convert to a promise
this.verifier.verify(tokenString, (err: any, jwt: any) => {
if (err) {
return reject(err);
}
const jwtBodyProxy = new Proxy(jwt.body, {});
Object.defineProperty(jwt, "claims", {
enumerable: true,
writable: false,
value: jwtBodyProxy,
});
delete jwt.body;
resolve(jwt);
});
});
}
async verifyIdToken(idTokenString: string, expectedClientId: string) {
const jwt = await this.verifyAsPromise(idTokenString);
verifyAudience(expectedClientId, jwt.claims.aud);
verifyIssuer(this.issuer, jwt.claims.iss);
return jwt;
}
}
@@ -0,0 +1,237 @@
import { AxiosInstance } from "axios";
import { ConsoleServiceClient } from "../client-interface";
import {
ConsoleCompany,
ConsoleHookCompany,
ConsoleHookUser,
ConsoleOptions,
CreateConsoleCompany,
CreateConsoleUser,
CreatedConsoleCompany,
CreatedConsoleUser,
UpdateConsoleUserRole,
UpdatedConsoleUserRole,
} from "../types";
import { OidcJwtVerifier } from "./remote-jwks-verifier";
import { CrudException } from "../../../core/platform/framework/api/crud-service";
import { logger } from "../../../core/platform/framework/logger";
import gr from "../../global-resolver";
import Company, { CompanySearchKey } from "../../user/entities/company";
import User, { getInstance } from "../../user/entities/user";
import { getInstance as getCompanyInstance } from "../../user/entities/company";
import { ConsoleServiceImpl } from "../service";
import coalesce from "../../../utils/coalesce";
export class ConsoleRemoteClient implements ConsoleServiceClient {
version: "1";
client: AxiosInstance;
private infos: ConsoleOptions;
private verifier: OidcJwtVerifier;
constructor(consoleInstance: ConsoleServiceImpl) {
this.infos = consoleInstance.consoleOptions;
this.verifier = new OidcJwtVerifier({
clientId: this.infos.client_id,
issuer: this.infos.issuer?.replace(/\/+$/, ""),
jwksUri: this.infos.jwks_uri,
// For local deployment create a https agent that ignore self signed certificate
// eslint-disable-next-line @typescript-eslint/no-var-requires
requestAgent: new (require("https").Agent)({
rejectUnauthorized: this.infos.issuer.includes("example.com") ? false : true,
}),
});
}
fetchCompanyInfo(consoleCompanyCode: string): Promise<ConsoleHookCompany> {
throw new Error(`Method not implemented, ${consoleCompanyCode}.`);
}
resendVerificationEmail(email: string): Promise<void> {
throw new Error(`Method not implemented, ${email}.`);
}
private auth() {
return {};
}
async addUserToCompany(
company: ConsoleCompany,
user: CreateConsoleUser,
): Promise<CreatedConsoleUser> {
logger.info(`Method not implemented, ${company.id}, ${user.id}.`);
return null;
}
async updateUserRole(
company: ConsoleCompany,
user: UpdateConsoleUserRole,
): Promise<UpdatedConsoleUserRole> {
logger.info("Remote: updateUserRole");
logger.info(`Method not implemented, ${company.id}, ${user.id}.`);
return null;
}
async createCompany(company: CreateConsoleCompany): Promise<CreatedConsoleCompany> {
logger.info("Remote: createCompany");
logger.info(`Method not implemented, ${company}.`);
return null;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
addUserToTdrive(user: CreateConsoleUser): Promise<User> {
logger.info("Remote: addUserToTdrive");
//should do noting for real console
return Promise.resolve(undefined);
}
async updateLocalCompanyFromConsole(partialCompanyDTO: ConsoleHookCompany): Promise<Company> {
logger.info(`Method not implemented, ${partialCompanyDTO}.`);
return null;
}
async updateLocalUserFromConsole(userDTO: ConsoleHookUser): Promise<User> {
logger.info("Remote: updateLocalUserFromConsole");
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 === "tdrive"),
);
//REMOVE LATER
logger.info(`Roles are: ${roles}.`);
let user = await gr.services.users.getByConsoleId(userDTO.email);
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)) {
throw CrudException.badRequest("Console user not created because email already exists");
}
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.email;
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);
}
user.picture = userDTO.avatar.value;
await gr.services.users.save(user);
//For now TDrive works with only one company as we don't get it from the SSO
let company = await gr.services.companies.getCompany({
id: "00000000-0000-4000-0000-000000000000",
});
if (!company) {
company = await gr.services.companies.createCompany(
getCompanyInstance({
id: "00000000-0000-4000-0000-000000000000",
name: "Tdrive",
plan: { name: "Local", limits: undefined, features: undefined },
}),
);
}
await gr.services.companies.setUserRole(company.id, user.id, "admin");
await gr.services.users.save(user, { user: { id: user.id, server_request: true } });
return user;
}
async removeCompanyUser(consoleUserId: string, company: Company): Promise<void> {
logger.info("Remote: removeCompanyUser");
const user = await gr.services.users.getByConsoleId(consoleUserId);
if (!user) {
throw CrudException.notFound(`User ${consoleUserId} doesn't exists`);
}
await gr.services.companies.removeUserFromCompany({ id: company.id }, { id: user.id });
}
async removeUser(consoleUserId: string): Promise<void> {
logger.info("Remote: removeUser");
const user = await gr.services.users.getByConsoleId(consoleUserId);
if (!user) {
throw new Error("User does not exists on Tdrive.");
}
await gr.services.users.anonymizeAndDelete(
{ id: user.id },
{
user: { id: user.id, server_request: true },
},
);
}
async removeCompany(companySearchKey: CompanySearchKey): Promise<void> {
logger.info("Remote: removeCompany");
await gr.services.companies.removeCompany(companySearchKey);
}
async getUserByAccessToken(idToken: string): Promise<ConsoleHookUser> {
const user = (await this.verifier.verifyIdToken(idToken, this.infos.client_id))?.claims as {
sub: string;
email: string;
family_name: string;
given_name: string;
name: string;
locale?: string;
picture?: string;
};
return {
_id: user.email || 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,
},
};
}
}
@@ -0,0 +1,23 @@
import { Prefix, TdriveService } from "../../core/platform/framework";
import WebServerAPI from "../../core/platform/services/webserver/provider";
import web from "./web";
@Prefix("/internal/services/console/v1")
export default class ConsoleService extends TdriveService<undefined> {
version = "1";
name = "console";
public async doInit(): Promise<this> {
const fastify = this.context.getProvider<WebServerAPI>("webserver").getServer();
fastify.register((instance, _opts, next) => {
web(instance, { prefix: this.prefix });
next();
});
return this;
}
// TODO: remove
api(): undefined {
return undefined;
}
}
@@ -0,0 +1,58 @@
import { DatabaseServiceAPI } from "../../core/platform/services/database/api";
import { ConsoleOptions, ConsoleType } from "./types";
import { ConsoleServiceClient } from "./client-interface";
import { ConsoleClientFactory } from "./client-factory";
import User from "../user/entities/user";
import gr from "../global-resolver";
import { Configuration, TdriveServiceProvider } from "../../core/platform/framework";
import assert from "assert";
import { ExecutionContext } from "../../core/platform/framework/api/crud-service";
export class ConsoleServiceImpl implements TdriveServiceProvider {
version: "1";
consoleType: ConsoleType;
consoleOptions: ConsoleOptions;
services: {
database: DatabaseServiceAPI;
};
private configuration: Configuration;
constructor(options?: ConsoleOptions) {
this.consoleOptions = options;
}
async init(): Promise<this> {
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");
const s = this.configuration.get(type) as ConsoleOptions;
this.consoleOptions = {
type: type,
authority: s.authority,
client_id: s.client_id,
client_secret: s.client_secret,
audience: s.audience,
issuer: s.issuer,
jwks_uri: s.jwks_uri,
redirect_uris: s.redirect_uris,
disable_account_creation: s.disable_account_creation,
};
this.consoleOptions.type = type;
this.consoleType = type;
return this;
}
getClient(): ConsoleServiceClient {
return ConsoleClientFactory.create(this);
}
async processPendingUser(user: User, context?: ExecutionContext): Promise<void> {
await gr.services.workspaces.processPendingUser(user, null, context);
}
}
@@ -0,0 +1,239 @@
import { Observable } from "rxjs";
import Company from "../user/entities/company";
import CompanyUser from "../user/entities/company_user";
import { CompanyUserRole } from "../user/web/types";
import { ExecutionContext } from "../../core/platform/framework/api/crud-service";
import { AccessToken } from "../../utils/types";
export interface CreateConsoleCompany {
code: string;
displayName: string;
avatar: {
type: "url";
value: string;
};
country?: string;
address?: string;
logo?: string;
status: string;
domain?: string[];
applications?: string[];
planId?: string;
limits?: {
members?: number; //Old console version
guests?: number; //Old console version
tdrive: {
members: number;
guests: number;
storage: number;
};
};
}
export type CreatedConsoleCompany = Partial<CreateConsoleCompany>;
export interface CreateConsoleUser {
id?: string;
email: string;
firstName: string;
lastName: string;
name: string;
avatar: {
type: "url";
value: string;
};
password: string;
role: CompanyUserRole;
skipInvite: boolean;
inviterEmail: string;
}
export interface CreateInternalUser {
email: string;
password: string;
}
export type CreatedConsoleUser = Partial<CreateConsoleUser> & { _id: string };
export type UpdateConsoleUserRole = Pick<CreateConsoleUser, "role"> & { id: string };
export type UpdatedConsoleUserRole = UpdateConsoleUserRole;
export type ConsoleUser = {
id: string;
companyCode: string;
};
export type ConsoleCompany = {
id?: string;
code: string;
};
export type CompanyCreatedStreamObject = {
source: Company;
destination: ConsoleCompany;
// Creation error
error?: Error;
};
export type UserCreatedStreamObject = {
source: {
user: CompanyUser;
company: Company;
};
destination: ConsoleUser;
// Creation error
error?: Error;
};
type ReportStatus = "success" | "failure";
export type CompanyReport = {
sourceId: string;
destinationCode: string;
status: ReportStatus;
company: CompanyCreatedStreamObject;
error?: Error;
};
export type UserReport = {
sourceId: string;
destinationId: string;
destinationCompanyCode: string;
status: ReportStatus;
user: UserCreatedStreamObject;
error?: Error;
};
export type MergeProgress = Observable<ProcessReport>;
export type ProcessReport = {
type:
| "user:updated"
| "user:updating"
| "user:created"
| "user:error"
| "company:created"
| "processing:owner"
| "log"
| "company:withoutadmin";
//error?: Error;
message?: string;
data?: UserReport | CompanyReport | Error | Company[];
};
export type ConsoleType = "remote" | "internal";
export type ConsoleOptions = {
type: ConsoleType;
authority: string;
client_id: string;
client_secret: string;
issuer: string;
jwks_uri: string;
audience: string;
redirect_uris: string[];
disable_account_creation: boolean;
};
export type ConsoleHookCompany = {
stats: string;
limits: {
members?: number; //Old console version
guests?: number; //Old console version
tdrive: {
members: number;
guests: number;
storage: number;
};
};
value: string;
details: {
code: string;
logo: string;
avatar: {
value: string;
type: string;
};
name: string;
country: string;
address: string;
};
};
export type ConsoleHookUser = {
_id: string;
roles: [
{
targetCode: string;
roleCode: CompanyUserRole;
status: "active" | "deactivated";
applications: {
code: "tdrive";
}[];
},
];
email: string;
name: string;
surname: string;
isVerified: boolean;
preference: {
locale: string;
timeZone: number;
allowTrackingPersonalInfo: boolean;
};
avatar: {
type: string;
value: string;
};
};
export type ConsoleHookBodyContent = {
company: ConsoleHookCompany;
user: ConsoleHookUser;
};
export type ConsoleHookCompanyDeletedContent = {
companyCode: string;
};
export type ConsoleHookPreferenceContent = {
preference: {
targetCode: string;
};
};
export type ConsoleHookBody = {
type: string;
content:
| ConsoleHookBodyContent
| ConsoleHookUser
| ConsoleHookCompany
| ConsoleHookCompanyDeletedContent;
signature: string;
secret_key?: string;
};
export class ConsoleHookQueryString {
secret_key: string;
}
export class ConsoleHookResponse {
success?: boolean;
error?: string;
}
export interface ConsoleExecutionContext extends ExecutionContext {
options: ConsoleOptions;
}
export interface AuthRequest {
email?: string;
password?: string;
oidc_id_token?: string;
}
export interface AuthResponse {
access_token?: AccessToken;
error?: string;
}
@@ -0,0 +1,298 @@
import { FastifyReply, FastifyRequest } from "fastify";
import {
AuthRequest,
AuthResponse,
ConsoleHookBody,
ConsoleHookBodyContent,
ConsoleHookCompany,
ConsoleHookCompanyDeletedContent,
ConsoleHookPreferenceContent,
ConsoleHookResponse,
ConsoleHookUser,
ConsoleType,
} from "../types";
import Company from "../../user/entities/company";
import { CrudException } from "../../../core/platform/framework/api/crud-service";
import PasswordEncoder from "../../../utils/password-encoder";
import { AccessToken } from "../../../utils/types";
import assert from "assert";
import { logger } from "../../../core/platform/framework";
import { getInstance } from "../../user/entities/user";
import { getInstance as getCompanyInstance } from "../../../services/user/entities/company";
import Workspace from "../../../services/workspaces/entities/workspace";
import gr from "../../global-resolver";
import { Configuration } from "../../../core/platform/framework";
export class ConsoleController {
private passwordEncoder: PasswordEncoder;
constructor() {
this.passwordEncoder = new PasswordEncoder();
}
async auth(request: FastifyRequest<{ Body: AuthRequest }>): Promise<AuthResponse> {
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 {
throw CrudException.badRequest("remote_access_token or email+password are required");
}
}
async signup(
request: FastifyRequest<{
Body: { email: string; password: string; first_name: string; last_name: string };
}>,
): Promise<AuthResponse> {
const configuration = new Configuration("console");
const type = configuration.get("type") as ConsoleType;
try {
//Allow only if no console is set up in this case everyone will be in the same company
//Console is set up
if (type !== "internal") {
throw new Error("Unable to signup in console mode");
}
//Allow only if signup isn't disabled
if (configuration.get("disable_account_creation")) {
throw new Error("Account creation is disabled");
}
const email = request.body.email.trim().toLocaleLowerCase();
if (await gr.services.users.getByEmail(email)) {
throw new Error("This email is already used");
}
try {
const newUser = getInstance({
first_name: request.body.first_name,
last_name: request.body.last_name,
email_canonical: email,
username_canonical: (email.replace("@", ".") || "").toLocaleLowerCase(),
});
const user = await gr.services.users.create(newUser);
await gr.services.users.setPassword({ id: user.entity.id }, request.body.password);
//Create a global company for all users in local mode
const companies = await gr.services.companies.getCompanies();
let company = companies.getEntities()?.[0];
if (!company) {
const newCompany = getCompanyInstance({
name: "Tdrive",
plan: { name: "Local", limits: undefined, features: undefined },
});
company = await gr.services.companies.createCompany(newCompany);
}
await gr.services.companies.setUserRole(company.id, user.entity.id, "admin");
//In case someone invited us to a workspace
await gr.services.workspaces.processPendingUser(user.entity);
//If user is in no workspace, then we create one for they
const workspaces = await gr.services.workspaces.getAllForUser(
{ userId: user.entity.id },
{ id: company.id },
);
if (workspaces.length === 0) {
gr.services.workspaces.create(
{
company_id: company.id,
name: `${
newUser.first_name || newUser.last_name || newUser.username_canonical
}'s space`,
} as Workspace,
{ user: { id: user.entity.id } },
);
}
return {
access_token: await this.authByPassword(request.body.email, request.body.password),
};
} catch (err) {
throw new Error("An unknown error occured");
}
} catch (err) {
return { error: err.toString() };
}
}
async tokenRenewal(request: FastifyRequest): Promise<AuthResponse> {
return {
access_token: gr.platformServices.auth.generateJWT(
request.currentUser.id,
request.currentUser.email,
{
track: request.currentUser?.allow_tracking || false,
provider_id: request.currentUser.identity_provider_id,
},
),
};
}
async resendVerificationEmail(
request: FastifyRequest,
): Promise<{ success: boolean; email: string }> {
const user = await gr.services.users.get({ id: request.currentUser.id });
await gr.services.console.getClient().resendVerificationEmail(user.email_canonical);
return {
success: true,
email: user.email_canonical,
};
}
private async getCompanyDataFromConsole(
company: ConsoleHookCompany | ConsoleHookCompany["details"] | { code: string },
): Promise<ConsoleHookCompany> {
return gr.services.console
.getClient()
.fetchCompanyInfo(
(company as ConsoleHookCompany["details"])?.code ||
(company as ConsoleHookCompany)?.details?.code,
);
}
private async updateCompany(company: ConsoleHookCompany): Promise<Company> {
const companyDTO = await this.getCompanyDataFromConsole(company);
return gr.services.console.getClient().updateLocalCompanyFromConsole(companyDTO);
}
async hook(
request: FastifyRequest<{ Body: ConsoleHookBody }>,
reply: FastifyReply,
): Promise<ConsoleHookResponse> {
try {
logger.info(`Received event ${request.body.type}`);
switch (request.body.type) {
case "user_created":
case "company_user_added":
case "company_user_activated":
case "company_user_updated":
await this.userAdded(request.body.content as ConsoleHookBodyContent);
break;
case "company_user_deactivated":
case "company_user_deleted":
await this.userDisabled(request.body.content as ConsoleHookBodyContent);
break;
case "user_updated":
await this.userUpdated((request.body.content as ConsoleHookBodyContent).user._id);
break;
case "user_preference_updated":
await this.userUpdated(
(request.body.content as unknown as ConsoleHookPreferenceContent).preference.targetCode,
);
break;
case "user_deleted":
await this.userRemoved(request.body.content as ConsoleHookUser);
break;
case "plan_updated":
await this.planUpdated(request.body.content as ConsoleHookBodyContent);
break;
case "company_deleted":
await this.companyRemoved(request.body.content as ConsoleHookCompanyDeletedContent);
break;
case "company_created":
await this.companyUpdated(request.body.content as ConsoleHookBodyContent);
if ((request.body.content as ConsoleHookBodyContent)?.user?._id) {
await this.userUpdated((request.body.content as ConsoleHookBodyContent).user._id);
}
break;
case "company_updated":
await this.companyUpdated(request.body.content as ConsoleHookBodyContent);
break;
default:
logger.info("Event not recognized");
throw CrudException.notImplemented("Unimplemented");
}
} catch (e) {
reply.status(400);
return {
error: e.message,
};
}
return {
success: true,
};
}
private async userAdded(content: ConsoleHookBodyContent): Promise<void> {
const userDTO = content.user;
const user = await gr.services.console.getClient().updateLocalUserFromConsole(userDTO);
await this.updateCompany(content.company);
await gr.services.console.processPendingUser(user);
}
private async userDisabled(content: ConsoleHookBodyContent): Promise<void> {
const company = await this.updateCompany(content.company);
await gr.services.console.getClient().removeCompanyUser(content.user._id, company);
}
private async userRemoved(content: ConsoleHookUser): Promise<void> {
await gr.services.console.getClient().removeUser(content._id);
}
private async userUpdated(code: string) {
//Not implemented yet
throw CrudException.notImplemented(`Method not implemented, ${code}.`);
}
private async companyRemoved(content: ConsoleHookCompanyDeletedContent) {
assert(content.companyCode, "content.companyCode is missing");
await gr.services.console.getClient().removeCompany({
identity_provider: "console",
identity_provider_id: content.companyCode,
});
}
private async companyUpdated(content: ConsoleHookBodyContent) {
await this.updateCompany(content.company);
}
private async planUpdated(content: ConsoleHookBodyContent) {
await this.updateCompany(content.company);
}
private async authByPassword(email: string, password: string): Promise<AccessToken> {
const user = await gr.services.users.getByEmail(email);
if (!user) {
throw CrudException.forbidden("User doesn't exists");
}
// allow to login in development mode with any password. This can be used to test without the console provider because the password is not stored locally...
if (process.env.NODE_ENV !== "development") {
const [storedPassword, salt] = await gr.services.users.getHashedPassword({
id: user.id,
});
if (!(await this.passwordEncoder.isPasswordValid(storedPassword, password, salt))) {
throw CrudException.forbidden("Password doesn't match");
}
} else if (process.env.NODE_ENV === "development") {
logger.warn("ERROR_NOTONPROD: YOU ARE RUNNING IN DEVELOPMENT MODE, AUTH IS DISABLED!!!");
}
return gr.platformServices.auth.generateJWT(user.id, user.email_canonical, {
track: user?.preferences?.allow_tracking || false,
provider_id: user.identity_provider_id,
});
}
private async authByToken(idToken: string): Promise<AccessToken> {
const client = gr.services.console.getClient();
const userDTO = await client.getUserByAccessToken(idToken);
const user = await client.updateLocalUserFromConsole(userDTO);
if (!user) {
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,
provider_id: user.identity_provider_id,
});
}
}
@@ -0,0 +1,12 @@
import { FastifyInstance, FastifyRegisterOptions } from "fastify";
import routes from "./routes";
export default (
fastify: FastifyInstance,
options: FastifyRegisterOptions<{
prefix: string;
}>,
): void => {
fastify.log.debug("Configuring /console routes");
fastify.register(routes, options);
};
@@ -0,0 +1,57 @@
import { FastifyInstance, FastifyPluginCallback, FastifyRequest } from "fastify";
import { authenticationSchema, consoleHookSchema, tokenRenewalSchema } from "./schemas";
// import { WorkspaceBaseRequest, WorkspaceUsersBaseRequest, WorkspaceUsersRequest } from "./types";
import { ConsoleController } from "./controller";
import { ConsoleHookBody, ConsoleHookQueryString } from "../types";
const hookUrl = "/hook";
const routes: FastifyPluginCallback = (fastify: FastifyInstance, options, next) => {
const controller = new ConsoleController();
const accessControl = async (
request: FastifyRequest<{ Body: ConsoleHookBody; Querystring: ConsoleHookQueryString }>,
) => {
throw fastify.httpErrors.notImplemented(`Hook service doesn't exist anymore, ${request}.`);
};
fastify.route({
method: "POST",
url: `${hookUrl}`,
preHandler: [accessControl],
schema: consoleHookSchema,
handler: controller.hook.bind(controller),
});
fastify.route({
method: "POST",
url: "/login",
schema: authenticationSchema,
handler: controller.auth.bind(controller),
});
fastify.route({
method: "POST",
url: "/signup",
handler: controller.signup.bind(controller),
});
fastify.route({
method: "POST",
url: "/token",
preValidation: [fastify.authenticate],
schema: tokenRenewalSchema,
handler: controller.tokenRenewal.bind(controller),
});
fastify.route({
method: "POST",
url: "/resend-verification-email",
preValidation: [fastify.authenticate],
handler: controller.resendVerificationEmail.bind(controller),
});
next();
};
export default routes;
@@ -0,0 +1,77 @@
export const consoleHookSchema = {
querystring: {
type: "object",
properties: {
secret_key: { type: "string" },
},
},
body: {
type: "object",
properties: {
type: { type: "string" },
content: { type: "object" },
signature: { type: "string" },
secret_key: { type: "string" },
},
required: ["type", "content"],
},
response: {
"2xx": {
type: "object",
properties: {
success: { type: "boolean" },
error: { type: "string" },
},
},
},
};
export const authenticationSchema = {
body: {
type: "object",
properties: {
email: { type: "string" },
password: { type: "string" },
oidc_id_token: { type: "string" },
},
},
response: {
"2xx": {
type: "object",
properties: {
access_token: {
type: "object",
properties: {
time: { type: "number" },
expiration: { type: "number" },
refresh_expiration: { type: "number" },
value: { type: "string" },
refresh: { type: "string" },
type: { type: "string" },
},
},
},
},
},
};
export const tokenRenewalSchema = {
response: {
"2xx": {
type: "object",
properties: {
access_token: {
type: "object",
properties: {
time: { type: "number" },
expiration: { type: "number" },
refresh_expiration: { type: "number" },
value: { type: "string" },
refresh: { type: "string" },
type: { type: "string" },
},
},
},
},
},
};