🔐 Initial change on the public links using anonymous users (#69)
* Initial change on the public links using anonymous users Co-authored-by: Anton Shepilov <anton.shepilov@gmail.com>
This commit is contained in:
@@ -28,6 +28,7 @@ export default interface AuthServiceAPI extends TdriveServiceProvider {
|
||||
track: boolean;
|
||||
provider_id: string;
|
||||
application_id?: string;
|
||||
public_token_document_id?: string;
|
||||
} & any,
|
||||
): AccessToken;
|
||||
}
|
||||
|
||||
@@ -31,7 +31,12 @@ export class AuthService implements AuthServiceAPI {
|
||||
generateJWT(
|
||||
userId: uuid,
|
||||
email: string,
|
||||
options: { track: boolean; provider_id: string; application_id?: string },
|
||||
options: {
|
||||
track: boolean;
|
||||
provider_id: string;
|
||||
application_id?: string;
|
||||
public_token_document_id?: string;
|
||||
},
|
||||
): AccessToken {
|
||||
const now = Math.round(new Date().getTime() / 1000); // Current time in UTC
|
||||
assert(this.configuration.expiration, "jwt.expiration is missing");
|
||||
@@ -53,6 +58,7 @@ export class AuthService implements AuthServiceAPI {
|
||||
track: !!options.track,
|
||||
provider_id: options.provider_id || "",
|
||||
application_id: options.application_id,
|
||||
public_token_document_id: options.public_token_document_id,
|
||||
} as JwtType),
|
||||
refresh: this.sign({
|
||||
exp: jwtRefreshExpiration,
|
||||
@@ -64,6 +70,7 @@ export class AuthService implements AuthServiceAPI {
|
||||
track: !!options.track,
|
||||
provider_id: options.provider_id || "",
|
||||
application_id: options.application_id,
|
||||
public_token_document_id: options.public_token_document_id,
|
||||
} as JwtType),
|
||||
type: "Bearer",
|
||||
};
|
||||
|
||||
@@ -22,6 +22,7 @@ const jwtPlugin: FastifyPluginCallback = (fastify, _opts, next) => {
|
||||
...{ application_id: jwt.application_id || null },
|
||||
...{ server_request: jwt.server_request || false },
|
||||
...{ allow_tracking: jwt.track || false },
|
||||
...{ public_token_document_id: jwt.public_token_document_id || null },
|
||||
};
|
||||
request.log.debug(`Authenticated as user ${request.currentUser.id}`);
|
||||
};
|
||||
|
||||
@@ -125,7 +125,7 @@ export default class EmailPusherClass
|
||||
this.logger.info("email sent");
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error("Failed to send email", error);
|
||||
this.logger.error({ error: `${error}` }, "Failed to send email");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@ import { TransportRequestOptions } from "@elastic/elasticsearch/lib/Transport";
|
||||
import { logger } from "../../../../../../core/platform/framework/logger";
|
||||
import { EntityTarget, FindFilter, FindOptions, getEntityDefinition } from "../../api";
|
||||
import { asciiFold } from "../utils";
|
||||
import { comparisonType } from "src/core/platform/services/database/services/orm/repository/repository";
|
||||
import { comparisonType } from "../../../../../../core/platform/services/database/services/orm/repository/repository";
|
||||
|
||||
export function buildSearchQuery<Entity>(
|
||||
entityType: EntityTarget<Entity>,
|
||||
|
||||
@@ -9,4 +9,5 @@ export type JwtType = {
|
||||
refresh_nbf?: number;
|
||||
iat: number;
|
||||
track: boolean;
|
||||
public_token_document_id?: string;
|
||||
};
|
||||
|
||||
@@ -126,6 +126,7 @@ export class ConsoleController {
|
||||
{
|
||||
track: request.currentUser?.allow_tracking || false,
|
||||
provider_id: request.currentUser.identity_provider_id,
|
||||
public_token_document_id: request.currentUser.public_token_document_id,
|
||||
},
|
||||
),
|
||||
};
|
||||
|
||||
@@ -43,6 +43,7 @@ export const hasAccessLevel = (
|
||||
|
||||
/**
|
||||
* checks the current user is a guest
|
||||
* Company guests can be users accessing with a public link
|
||||
*
|
||||
* @param {CompanyExecutionContext} context
|
||||
* @returns {Promise<boolean>}
|
||||
@@ -149,7 +150,7 @@ export const getAccessLevel = async (
|
||||
context: CompanyExecutionContext & { public_token?: string; tdrive_tab_token?: string },
|
||||
): Promise<DriveFileAccessLevel | "none"> => {
|
||||
if (!id || id === "root")
|
||||
return !context?.user?.id ? "none" : (await isCompanyGuest(context)) ? "read" : "manage";
|
||||
return !context?.user?.id || (await isCompanyGuest(context)) ? "none" : "manage";
|
||||
if (id === "trash")
|
||||
return (await isCompanyGuest(context)) || !context?.user?.id
|
||||
? "none"
|
||||
@@ -164,6 +165,7 @@ export const getAccessLevel = async (
|
||||
}
|
||||
|
||||
let publicToken = context.public_token;
|
||||
const prevalidatedPublicTokenDocumentId = context?.user?.public_token_document_id;
|
||||
|
||||
try {
|
||||
item =
|
||||
@@ -187,10 +189,13 @@ export const getAccessLevel = async (
|
||||
*/
|
||||
|
||||
//Public access
|
||||
if (publicToken) {
|
||||
if (publicToken || prevalidatedPublicTokenDocumentId === item.id) {
|
||||
if (!item.access_info.public.token) return "none";
|
||||
const { token: itemToken, level: itemLevel, password, expiration } = item.access_info.public;
|
||||
if (expiration && expiration < Date.now()) return "none";
|
||||
if (prevalidatedPublicTokenDocumentId) {
|
||||
return itemLevel;
|
||||
}
|
||||
if (password) {
|
||||
const data = publicToken.split("+");
|
||||
if (data.length !== 2) return "none";
|
||||
@@ -227,7 +232,7 @@ export const getAccessLevel = async (
|
||||
const matchingCompany = accessEntities.find(
|
||||
a => a.type === "company" && a.id === context.company.id,
|
||||
);
|
||||
if (matchingCompany) otherLevels.push(matchingCompany.level);
|
||||
if (matchingCompany && !isCompanyGuest(context)) otherLevels.push(matchingCompany.level);
|
||||
}
|
||||
|
||||
//Parent folder
|
||||
|
||||
@@ -70,7 +70,7 @@ export class DocumentsProcessor
|
||||
content_keywords = extractKeywords(content_strings);
|
||||
} catch (error) {
|
||||
console.debug(error);
|
||||
logger.error("Failed to generate content keywords", error);
|
||||
logger.error({ error: `${error}` }, "Failed to generate content keywords");
|
||||
}
|
||||
|
||||
return { content_keywords, item: message.item };
|
||||
|
||||
@@ -81,7 +81,7 @@ export class DocumentsService {
|
||||
DriveTdriveTabEntity,
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error("Error while initializing Documents Service", error);
|
||||
logger.error({ error: `${error}` }, "Error while initializing Documents Service");
|
||||
}
|
||||
|
||||
return this;
|
||||
@@ -94,7 +94,10 @@ export class DocumentsService {
|
||||
* @param {DriveExecutionContext} context
|
||||
* @returns {Promise<DriveItemDetails>}
|
||||
*/
|
||||
get = async (id: string, context: DriveExecutionContext): Promise<DriveItemDetails> => {
|
||||
get = async (
|
||||
id: string,
|
||||
context: DriveExecutionContext & { public_token?: string },
|
||||
): Promise<DriveItemDetails> => {
|
||||
if (!context) {
|
||||
this.logger.error("invalid context");
|
||||
return null;
|
||||
@@ -123,11 +126,12 @@ export class DocumentsService {
|
||||
try {
|
||||
const hasAccess = await checkAccess(id, entity, "read", this.repository, context);
|
||||
if (!hasAccess) {
|
||||
this.logger.error("user does not have access drive item ", id);
|
||||
this.logger.error("user does not have access drive item " + id);
|
||||
throw Error("user does not have access to this item");
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error("Failed to grant access to the drive item", error);
|
||||
console.log(error);
|
||||
this.logger.error({ error: `${error}` }, "Failed to grant access to the drive item");
|
||||
throw new CrudException("User does not have access to this item or its children", 401);
|
||||
}
|
||||
|
||||
@@ -309,7 +313,7 @@ export class DocumentsService {
|
||||
|
||||
return driveItem;
|
||||
} catch (error) {
|
||||
this.logger.error("Failed to create drive item", error);
|
||||
this.logger.error({ error: `${error}` }, "Failed to create drive item");
|
||||
CrudException.throwMe(error, new CrudException("Failed to create item", 500));
|
||||
}
|
||||
};
|
||||
@@ -423,7 +427,7 @@ export class DocumentsService {
|
||||
return item;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
this.logger.error("Failed to update drive item", error);
|
||||
this.logger.error({ error: `${error}` }, "Failed to update drive item");
|
||||
throw new CrudException("Failed to update item", 500);
|
||||
}
|
||||
};
|
||||
@@ -468,7 +472,7 @@ export class DocumentsService {
|
||||
}),
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.error("Failed to empty trash", error);
|
||||
this.logger.error({ error: `${error}` }, "Failed to empty trash");
|
||||
throw new CrudException("Failed to empty trash", 500);
|
||||
}
|
||||
|
||||
@@ -492,7 +496,7 @@ export class DocumentsService {
|
||||
throw Error("user does not have access to this item");
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error("Failed to grant access to the drive item", error);
|
||||
this.logger.error({ error: `${error}` }, "Failed to grant access to the drive item");
|
||||
throw new CrudException("User does not have access to this item or its children", 401);
|
||||
}
|
||||
|
||||
@@ -626,7 +630,7 @@ export class DocumentsService {
|
||||
|
||||
return driveItemVersion;
|
||||
} catch (error) {
|
||||
this.logger.error("Failed to create Drive item version", error);
|
||||
this.logger.error({ error: `${error}` }, "Failed to create Drive item version");
|
||||
throw new CrudException("Failed to create Drive item version", 500);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
import { ExecutionContext } from "../../core/platform/framework/api/crud-service";
|
||||
import { DriveFile } from "./entities/drive-file";
|
||||
import { FileVersion } from "./entities/file-version";
|
||||
import { SortType } from "src/core/platform/services/search/api";
|
||||
import { SortType } from "../../core/platform/services/search/api";
|
||||
|
||||
export interface CompanyExecutionContext extends ExecutionContext {
|
||||
company: { id: string };
|
||||
}
|
||||
|
||||
export type DriveExecutionContext = CompanyExecutionContext & {
|
||||
public_token?: string;
|
||||
};
|
||||
export type DriveExecutionContext = CompanyExecutionContext;
|
||||
|
||||
export type RequestParams = {
|
||||
company_id: string;
|
||||
|
||||
@@ -228,7 +228,7 @@ export const getPath = async (
|
||||
): Promise<DriveFile[]> => {
|
||||
id = id || "root";
|
||||
if (isVirtualFolder(id))
|
||||
return !context.public_token || ignoreAccess
|
||||
return !context?.user?.public_token_document_id || ignoreAccess
|
||||
? [
|
||||
{
|
||||
id,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { FastifyReply, FastifyRequest } from "fastify";
|
||||
import { getInstance } from "../../../../services/user/entities/user";
|
||||
import { logger } from "../../../../core/platform/framework";
|
||||
import { CrudException, ListResult } from "../../../../core/platform/framework/api/crud-service";
|
||||
import { File } from "../../../../services/files/entities/file";
|
||||
@@ -68,7 +69,7 @@ export class DocumentsController {
|
||||
context,
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error("Failed to create Drive item", error);
|
||||
logger.error({ error: `${error}` }, "Failed to create Drive item");
|
||||
CrudException.throwMe(error, new CrudException("Failed to create Drive item", 500));
|
||||
}
|
||||
};
|
||||
@@ -91,7 +92,7 @@ export class DocumentsController {
|
||||
|
||||
reply.status(200).send();
|
||||
} catch (error) {
|
||||
logger.error("Failed to delete drive item", error);
|
||||
logger.error({ error: `${error}` }, "Failed to delete drive item");
|
||||
throw new CrudException("Failed to delete drive item", 500);
|
||||
}
|
||||
};
|
||||
@@ -281,7 +282,7 @@ export class DocumentsController {
|
||||
response.send(data.file);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("failed to download file", error);
|
||||
logger.error({ error: `${error}` }, "failed to download file");
|
||||
throw new CrudException("Failed to download file", 500);
|
||||
}
|
||||
};
|
||||
@@ -328,7 +329,7 @@ export class DocumentsController {
|
||||
|
||||
archive.pipe(reply.raw);
|
||||
} catch (error) {
|
||||
logger.error("failed to send zip file", error);
|
||||
logger.error({ error: `${error}` }, "failed to send zip file");
|
||||
throw new CrudException("Failed to create zip file", 500);
|
||||
}
|
||||
};
|
||||
@@ -361,7 +362,7 @@ export class DocumentsController {
|
||||
|
||||
return this.driveFileDTOBuilder.build(fileList, context, options.fields, options.view);
|
||||
} catch (error) {
|
||||
logger.error("error while searching for document", error);
|
||||
logger.error({ error: `${error}` }, "error while searching for document");
|
||||
this.throw500Search();
|
||||
}
|
||||
};
|
||||
@@ -401,6 +402,65 @@ export class DocumentsController {
|
||||
context,
|
||||
);
|
||||
};
|
||||
|
||||
async getAnonymousToken(
|
||||
req: FastifyRequest<{
|
||||
Body: {
|
||||
company_id: string;
|
||||
document_id: string;
|
||||
token: string;
|
||||
token_password?: string;
|
||||
};
|
||||
}>,
|
||||
): Promise<{
|
||||
access_token: {
|
||||
time: number;
|
||||
expiration: number;
|
||||
refresh_expiration: number;
|
||||
value: string;
|
||||
refresh: string;
|
||||
type: string;
|
||||
};
|
||||
}> {
|
||||
const document = await globalResolver.services.documents.documents.get(req.body.document_id, {
|
||||
public_token: req.body.token + (req.body.token_password ? "+" + req.body.token_password : ""),
|
||||
user: null,
|
||||
company: { id: req.body.company_id },
|
||||
});
|
||||
|
||||
if (!document || !document.access || document.access === "none")
|
||||
throw new CrudException("You don't have access to this document", 401);
|
||||
|
||||
const email = document.item.company_id + "-anonymous@tdrive.com";
|
||||
let user = await globalResolver.services.users.getByEmail(email);
|
||||
if (!user) {
|
||||
user = (
|
||||
await globalResolver.services.users.create(
|
||||
getInstance({
|
||||
first_name: "Anonymous",
|
||||
last_name: "",
|
||||
email_canonical: email,
|
||||
username_canonical: (email.replace("@", ".") || "").toLocaleLowerCase(),
|
||||
phone: "",
|
||||
identity_provider: "anonymous",
|
||||
identity_provider_id: email,
|
||||
mail_verified: true,
|
||||
}),
|
||||
)
|
||||
).entity;
|
||||
}
|
||||
await globalResolver.services.companies.setUserRole(document.item.company_id, user.id, "guest");
|
||||
|
||||
const token = globalResolver.platformServices.auth.generateJWT(user.id, user.email_canonical, {
|
||||
track: false,
|
||||
provider_id: "tdrive",
|
||||
public_token_document_id: req.body.document_id,
|
||||
});
|
||||
|
||||
return {
|
||||
access_token: token,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -410,9 +470,8 @@ export class DocumentsController {
|
||||
* @returns {CompanyExecutionContext}
|
||||
*/
|
||||
const getDriveExecutionContext = (
|
||||
req: FastifyRequest<{ Params: { company_id: string }; Querystring?: { public_token?: string } }>,
|
||||
req: FastifyRequest<{ Params: { company_id: string } }>,
|
||||
): DriveExecutionContext => ({
|
||||
public_token: req.query?.public_token,
|
||||
user: req.currentUser,
|
||||
company: { id: req.params.company_id },
|
||||
url: req.url,
|
||||
|
||||
@@ -101,6 +101,12 @@ const routes: FastifyPluginCallback = (fastify: FastifyInstance, _options, next)
|
||||
handler: documentsController.setTab.bind(documentsController),
|
||||
});
|
||||
|
||||
fastify.route({
|
||||
method: "POST",
|
||||
url: `${baseUrl}/anonymous/token`,
|
||||
handler: documentsController.getAnonymousToken.bind(documentsController),
|
||||
});
|
||||
|
||||
return next();
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { TdriveServiceProvider, Initializable } from "src/core/platform/framework/api";
|
||||
import Repository from "src/core/platform/services/database/services/orm/repository/repository";
|
||||
import { TdriveServiceProvider, Initializable } from "../../../core/platform/framework/api";
|
||||
import Repository from "../../../core/platform/services/database/services/orm/repository/repository";
|
||||
import { Tag, TagsType, TagsPrimaryKey } from "../entities";
|
||||
import gr from "../../global-resolver";
|
||||
import {
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
} from "../../../../utils/types";
|
||||
import { Tag } from "../../entities";
|
||||
import gr from "../../../global-resolver";
|
||||
import { ExecutionContext } from "src/core/platform/framework/api/crud-service";
|
||||
import { ExecutionContext } from "../../../../core/platform/framework/api/crud-service";
|
||||
import { handleError } from "../../../../utils/handleError";
|
||||
|
||||
export class TagsController
|
||||
|
||||
@@ -3,7 +3,7 @@ import { checkUserBelongsToCompany } from "../../../utils/company";
|
||||
import { Tag } from "../types";
|
||||
import { TagsController } from "./controllers";
|
||||
import gr from "../../global-resolver";
|
||||
import CompanyUser from "src/services/user/entities/company_user";
|
||||
import CompanyUser from "../../../services/user/entities/company_user";
|
||||
|
||||
const tagsUrl = "/companies/:company_id/tags";
|
||||
|
||||
|
||||
@@ -28,6 +28,8 @@ export interface User {
|
||||
application_id?: string;
|
||||
// allow_tracking
|
||||
allow_tracking?: boolean;
|
||||
// Prevalidated public token access
|
||||
public_token_document_id?: string;
|
||||
}
|
||||
|
||||
export const webSocketSchema = {
|
||||
|
||||
@@ -22,6 +22,17 @@ describe("the public links feature", () => {
|
||||
access_info: AccessInformation;
|
||||
}
|
||||
|
||||
class AccessTokenMockClass {
|
||||
access_token: {
|
||||
time: 0;
|
||||
expiration: number;
|
||||
refresh_expiration: number;
|
||||
value: string;
|
||||
refresh: string;
|
||||
type: "Bearer";
|
||||
};
|
||||
}
|
||||
|
||||
class FullDriveInfoMockClass {
|
||||
path: DriveFile[];
|
||||
item?: DriveFile;
|
||||
@@ -112,10 +123,28 @@ describe("the public links feature", () => {
|
||||
const file = deserialize<DriveFileMockClass>(DriveFileMockClass, res.body);
|
||||
expect(file.access_info.public?.level).toBe("read");
|
||||
|
||||
const accessRes = await platform.app.inject({
|
||||
method: "POST",
|
||||
url: `${url}/companies/${publicFile.company_id}/anonymous/token`,
|
||||
headers: {},
|
||||
payload: {
|
||||
company_id: publicFile.company_id,
|
||||
document_id: publicFile.id,
|
||||
token: publicFile.access_info.public?.token,
|
||||
},
|
||||
});
|
||||
const { access_token } = deserialize<AccessTokenMockClass>(
|
||||
AccessTokenMockClass,
|
||||
accessRes.body,
|
||||
);
|
||||
expect(access_token).toBeDefined();
|
||||
|
||||
const resPublicRaw = await platform.app.inject({
|
||||
method: "GET",
|
||||
url: `${url}/companies/${publicFile.company_id}/item/${publicFile.id}?public_token=${publicFile.access_info.public?.token}`,
|
||||
headers: {},
|
||||
url: `${url}/companies/${publicFile.company_id}/item/${publicFile.id}`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${access_token.value}`,
|
||||
},
|
||||
});
|
||||
const resPublic = deserialize<DriveItemDetails>(FullDriveInfoMockClass, resPublicRaw.body);
|
||||
expect(resPublicRaw.statusCode).toBe(200);
|
||||
@@ -135,10 +164,27 @@ describe("the public links feature", () => {
|
||||
},
|
||||
});
|
||||
|
||||
const accessRes = await platform.app.inject({
|
||||
method: "POST",
|
||||
url: `${url}/companies/${publicFile.company_id}/anonymous/token`,
|
||||
headers: {},
|
||||
payload: {
|
||||
company_id: publicFile.company_id,
|
||||
document_id: publicFile.id,
|
||||
token: publicFile.access_info.public?.token,
|
||||
},
|
||||
});
|
||||
const { access_token } = deserialize<AccessTokenMockClass>(
|
||||
AccessTokenMockClass,
|
||||
accessRes.body,
|
||||
);
|
||||
|
||||
let resPublicRaw = await platform.app.inject({
|
||||
method: "GET",
|
||||
url: `${url}/companies/${publicFile.company_id}/item/${publicFile.id}?public_token=${publicFile.access_info.public?.token}`,
|
||||
headers: {},
|
||||
url: `${url}/companies/${publicFile.company_id}/item/${publicFile.id}`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${access_token.value}`,
|
||||
},
|
||||
});
|
||||
const resPublic = deserialize<DriveItemDetails>(FullDriveInfoMockClass, resPublicRaw.body);
|
||||
expect(resPublicRaw.statusCode).toBe(200);
|
||||
@@ -158,8 +204,10 @@ describe("the public links feature", () => {
|
||||
|
||||
resPublicRaw = await platform.app.inject({
|
||||
method: "GET",
|
||||
url: `${url}/companies/${publicFile.company_id}/item/${publicFile.id}?public_token=${publicFile.access_info.public?.token}`,
|
||||
headers: {},
|
||||
url: `${url}/companies/${publicFile.company_id}/item/${publicFile.id}`,
|
||||
headers: {
|
||||
authorization: `Bearer ${access_token.value}`,
|
||||
},
|
||||
});
|
||||
expect(resPublicRaw.statusCode).toBe(401);
|
||||
|
||||
@@ -189,22 +237,45 @@ describe("the public links feature", () => {
|
||||
},
|
||||
});
|
||||
|
||||
const badAccessRes = await platform.app.inject({
|
||||
method: "POST",
|
||||
url: `${url}/companies/${publicFile.company_id}/anonymous/token`,
|
||||
headers: {},
|
||||
payload: {
|
||||
company_id: publicFile.company_id,
|
||||
document_id: publicFile.id,
|
||||
token: publicFile.access_info.public?.token,
|
||||
},
|
||||
});
|
||||
expect(badAccessRes.statusCode).toBe(401);
|
||||
|
||||
const accessRes = await platform.app.inject({
|
||||
method: "POST",
|
||||
url: `${url}/companies/${publicFile.company_id}/anonymous/token`,
|
||||
headers: {},
|
||||
payload: {
|
||||
company_id: publicFile.company_id,
|
||||
document_id: publicFile.id,
|
||||
token: publicFile.access_info.public?.token,
|
||||
token_password: "abcdef",
|
||||
},
|
||||
});
|
||||
const { access_token } = deserialize<AccessTokenMockClass>(
|
||||
AccessTokenMockClass,
|
||||
accessRes.body,
|
||||
);
|
||||
|
||||
let resPublicRaw = await platform.app.inject({
|
||||
method: "GET",
|
||||
url: `${url}/companies/${publicFile.company_id}/item/${publicFile.id}?public_token=${publicFile.access_info.public?.token}%2Babcdef`,
|
||||
headers: {},
|
||||
url: `${url}/companies/${publicFile.company_id}/item/${publicFile.id}`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${access_token.value}`,
|
||||
},
|
||||
});
|
||||
let resPublic = deserialize<DriveItemDetails>(FullDriveInfoMockClass, resPublicRaw.body);
|
||||
expect(resPublicRaw.statusCode).toBe(200);
|
||||
expect(resPublic.item?.id).toBe(publicFile.id);
|
||||
|
||||
resPublicRaw = await platform.app.inject({
|
||||
method: "GET",
|
||||
url: `${url}/companies/${publicFile.company_id}/item/${publicFile.id}?public_token=${publicFile.access_info.public?.token}`,
|
||||
headers: {},
|
||||
});
|
||||
expect(resPublicRaw.statusCode).toBe(401);
|
||||
|
||||
await e2e_updateDocument(platform, publicFile.id, {
|
||||
...publicFile,
|
||||
access_info: {
|
||||
|
||||
@@ -32,7 +32,7 @@ export default (props: { driveItem: DriveItem & { user?: UserType } }) => {
|
||||
const input = useRecoilValue(SearchInputState);
|
||||
const currentWorkspaceId = useRouterWorkspace();
|
||||
const companyApplications = useCompanyApplications();
|
||||
const [_, setParentId] = useRecoilState(DriveCurrentFolderAtom('root'));
|
||||
const [_, setParentId] = useRecoilState(DriveCurrentFolderAtom({ initialFolderId: 'root' }));
|
||||
const tdriveDriveApplicationId =
|
||||
companyApplications.applications.find(application => {
|
||||
return application.identity.code === 'tdrive_drive';
|
||||
|
||||
@@ -2,6 +2,7 @@ import Api from '../../global/framework/api-service';
|
||||
import { DriveItem, DriveItemDetails, DriveItemVersion } from '../types';
|
||||
import Workspace from '@deprecated/workspaces/workspaces';
|
||||
import Logger from 'features/global/framework/logger-service';
|
||||
import { JWTDataType } from 'app/features/auth/jwt-storage-service';
|
||||
export interface BaseSearchOptions {
|
||||
company_id?: string;
|
||||
workspace_id?: string;
|
||||
@@ -32,10 +33,7 @@ export const setTdriveTabToken = (token: string | null) => {
|
||||
tdriveTabToken = token;
|
||||
};
|
||||
|
||||
const appendPublicAndTdriveToken = (useAnd?: boolean) => {
|
||||
if (publicLinkToken) {
|
||||
return `${useAnd ? '&' : '?'}public_token=${publicLinkToken}`;
|
||||
}
|
||||
const appendTdriveToken = (useAnd?: boolean) => {
|
||||
if (tdriveTabToken) {
|
||||
return `${useAnd ? '&' : '?'}tdrive_tab_token=${tdriveTabToken}`;
|
||||
}
|
||||
@@ -44,21 +42,39 @@ const appendPublicAndTdriveToken = (useAnd?: boolean) => {
|
||||
|
||||
export class DriveApiClient {
|
||||
private static logger = Logger.getLogger('MessageAPIClientService');
|
||||
|
||||
static async getAnonymousToken(
|
||||
companyId: string,
|
||||
id: string,
|
||||
publicToken: string,
|
||||
password?: string,
|
||||
) {
|
||||
return await Api.post<
|
||||
{ company_id: string; document_id: string; token: string; token_password?: string },
|
||||
{ access_token: JWTDataType }
|
||||
>(`/internal/services/documents/v1/companies/${companyId}/anonymous/token`, {
|
||||
company_id: companyId,
|
||||
document_id: id,
|
||||
token: publicToken,
|
||||
token_password: password,
|
||||
});
|
||||
}
|
||||
|
||||
static async get(companyId: string, id: string | 'trash' | '') {
|
||||
return await Api.get<DriveItemDetails>(
|
||||
`/internal/services/documents/v1/companies/${companyId}/item/${id}${appendPublicAndTdriveToken()}`,
|
||||
`/internal/services/documents/v1/companies/${companyId}/item/${id}${appendTdriveToken()}`,
|
||||
);
|
||||
}
|
||||
|
||||
static async remove(companyId: string, id: string | 'trash' | '') {
|
||||
return await Api.delete<void>(
|
||||
`/internal/services/documents/v1/companies/${companyId}/item/${id}${appendPublicAndTdriveToken()}`,
|
||||
`/internal/services/documents/v1/companies/${companyId}/item/${id}${appendTdriveToken()}`,
|
||||
);
|
||||
}
|
||||
|
||||
static async update(companyId: string, id: string, update: Partial<DriveItem>) {
|
||||
return await Api.post<Partial<DriveItem>, DriveItem>(
|
||||
`/internal/services/documents/v1/companies/${companyId}/item/${id}${appendPublicAndTdriveToken()}`,
|
||||
`/internal/services/documents/v1/companies/${companyId}/item/${id}${appendTdriveToken()}`,
|
||||
update,
|
||||
);
|
||||
}
|
||||
@@ -72,14 +88,14 @@ export class DriveApiClient {
|
||||
{ item: Partial<DriveItem>; version: Partial<DriveItemVersion> },
|
||||
DriveItem
|
||||
>(
|
||||
`/internal/services/documents/v1/companies/${companyId}/item${appendPublicAndTdriveToken()}`,
|
||||
`/internal/services/documents/v1/companies/${companyId}/item${appendTdriveToken()}`,
|
||||
data as { item: Partial<DriveItem>; version: Partial<DriveItemVersion> },
|
||||
);
|
||||
}
|
||||
|
||||
static async createVersion(companyId: string, id: string, version: Partial<DriveItemVersion>) {
|
||||
return await Api.post<Partial<DriveItemVersion>, DriveItemVersion>(
|
||||
`/internal/services/documents/v1/companies/${companyId}/item/${id}/version${appendPublicAndTdriveToken()}`,
|
||||
`/internal/services/documents/v1/companies/${companyId}/item/${id}/version${appendTdriveToken()}`,
|
||||
version,
|
||||
);
|
||||
}
|
||||
@@ -88,7 +104,7 @@ export class DriveApiClient {
|
||||
return Api.get<{ token: string }>(
|
||||
`/internal/services/documents/v1/companies/${companyId}/item/download/token` +
|
||||
`?items=${ids.join(',')}&version_id=${versionId}` +
|
||||
appendPublicAndTdriveToken(true),
|
||||
appendTdriveToken(true),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -96,12 +112,12 @@ export class DriveApiClient {
|
||||
const { token } = await DriveApiClient.getDownloadToken(companyId, [id], versionId);
|
||||
if (versionId)
|
||||
return Api.route(
|
||||
`/internal/services/documents/v1/companies/${companyId}/item/${id}/download?version_id=${versionId}&token=${token}${appendPublicAndTdriveToken(
|
||||
`/internal/services/documents/v1/companies/${companyId}/item/${id}/download?version_id=${versionId}&token=${token}${appendTdriveToken(
|
||||
true,
|
||||
)}`,
|
||||
);
|
||||
return Api.route(
|
||||
`/internal/services/documents/v1/companies/${companyId}/item/${id}/download?token=${token}${appendPublicAndTdriveToken(
|
||||
`/internal/services/documents/v1/companies/${companyId}/item/${id}/download?token=${token}${appendTdriveToken(
|
||||
true,
|
||||
)}`,
|
||||
);
|
||||
@@ -112,7 +128,7 @@ export class DriveApiClient {
|
||||
return Api.route(
|
||||
`/internal/services/documents/v1/companies/${companyId}/item/download/zip` +
|
||||
`?items=${ids.join(',')}&token=${token}` +
|
||||
appendPublicAndTdriveToken(true),
|
||||
appendTdriveToken(true),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -26,17 +26,22 @@ import { PropertiesModal } from './modals/properties';
|
||||
import { AccessModal } from './modals/update-access';
|
||||
import { VersionsModal } from './modals/versions';
|
||||
|
||||
export const DriveCurrentFolderAtom = atomFamily<string, string>({
|
||||
export const DriveCurrentFolderAtom = atomFamily<
|
||||
string,
|
||||
{ context?: string; initialFolderId: string }
|
||||
>({
|
||||
key: 'DriveCurrentFolderAtom',
|
||||
default: startingParentId => startingParentId || 'root',
|
||||
default: options => options.initialFolderId || 'root',
|
||||
});
|
||||
|
||||
export default memo(
|
||||
({
|
||||
context,
|
||||
initialParentId,
|
||||
tdriveTabContextToken,
|
||||
inPublicSharing,
|
||||
}: {
|
||||
context?: string;
|
||||
initialParentId?: string;
|
||||
tdriveTabContextToken?: string;
|
||||
inPublicSharing?: boolean;
|
||||
@@ -45,7 +50,7 @@ export default memo(
|
||||
setTdriveTabToken(tdriveTabContextToken || null);
|
||||
|
||||
const [parentId, _setParentId] = useRecoilState(
|
||||
DriveCurrentFolderAtom(initialParentId || 'root'),
|
||||
DriveCurrentFolderAtom({ context: context, initialFolderId: initialParentId || 'root' }),
|
||||
);
|
||||
|
||||
const [loadingParentChange, setLoadingParentChange] = useState(false);
|
||||
@@ -118,7 +123,7 @@ export default memo(
|
||||
return (
|
||||
<UploadZone
|
||||
overClassName={''}
|
||||
className="h-full overflow-hidden"
|
||||
className="h-full overflow-hidden w-full relative"
|
||||
disableClick
|
||||
parent={''}
|
||||
multiple={true}
|
||||
|
||||
@@ -24,7 +24,9 @@ export const useOnBuildContextMenu = (children: DriveItem[], initialParentId?: s
|
||||
const [checkedIds, setChecked] = useRecoilState(DriveItemSelectedList);
|
||||
const checked = children.filter(c => checkedIds[c.id]);
|
||||
|
||||
const [_, setParentId] = useRecoilState(DriveCurrentFolderAtom(initialParentId || 'root'));
|
||||
const [_, setParentId] = useRecoilState(
|
||||
DriveCurrentFolderAtom({ initialFolderId: initialParentId || 'root' }),
|
||||
);
|
||||
|
||||
const { download, downloadZip, update } = useDriveActions();
|
||||
const setCreationModalState = useSetRecoilState(CreateModalAtom);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import MenusBodyLayer from '@components/menus/menus-body-layer';
|
||||
import JWTStorage from '@features/auth/jwt-storage-service';
|
||||
import Api from '@features/global/framework/api-service';
|
||||
import Languages from '@features/global/services/languages-service';
|
||||
import { addApiUrlIfNeeded } from '@features/global/utils/URLUtils';
|
||||
@@ -11,15 +12,18 @@ import { Input } from 'app/atoms/input/input-text';
|
||||
import { Base, Subtitle, Title } from 'app/atoms/text';
|
||||
import UploadsViewer from 'app/components/file-uploads/uploads-viewer';
|
||||
import { useDriveItem } from 'app/features/drive/hooks/use-drive-item';
|
||||
import { useDriveUpload } from 'app/features/drive/hooks/use-drive-upload';
|
||||
import { ToasterService } from 'app/features/global/services/toaster-service';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
import shortUUID from 'short-uuid';
|
||||
import Avatar from '../../../../atoms/avatar';
|
||||
import { setPublicLinkToken } from '../../../../features/drive/api-client/api-client';
|
||||
import {
|
||||
DriveApiClient,
|
||||
setPublicLinkToken,
|
||||
} from '../../../../features/drive/api-client/api-client';
|
||||
import useRouterCompany from '../../../../features/router/hooks/use-router-company';
|
||||
import { CreateModalAtom } from './modals/create';
|
||||
import { CreateModalWithUploadZones } from '../../side-bar/actions';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { DriveCurrentFolderAtom } from './browser';
|
||||
|
||||
export default () => {
|
||||
const companyId = useRouterCompany();
|
||||
@@ -44,7 +48,6 @@ export default () => {
|
||||
|
||||
const { token, documentId: _documentId } = useParams() as { token?: string; documentId?: string };
|
||||
const documentId = _documentId ? shortUUID().toUUID(_documentId || '') : '';
|
||||
setPublicLinkToken(token || null);
|
||||
|
||||
if (!companyId) {
|
||||
return <></>;
|
||||
@@ -77,7 +80,7 @@ export default () => {
|
||||
</div>
|
||||
<MenusBodyLayer />
|
||||
<UploadsViewer />
|
||||
<CreateModalWithUploadZones />
|
||||
<CreateModalWithUploadZones initialParentId={documentId} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -92,10 +95,36 @@ const AccessChecker = ({
|
||||
folderId: string;
|
||||
}) => {
|
||||
const { details, loading, refresh } = useDriveItem(folderId);
|
||||
const companyId = useRouterCompany();
|
||||
const [password, setPassword] = useState((token || '').split('+')[1] || '');
|
||||
|
||||
const setPublicToken = async (token: string, password?: string) => {
|
||||
try {
|
||||
setPublicLinkToken(token || null);
|
||||
|
||||
const { access_token } = await DriveApiClient.getAnonymousToken(
|
||||
companyId,
|
||||
folderId,
|
||||
token,
|
||||
password,
|
||||
);
|
||||
|
||||
if (!access_token) {
|
||||
throw new Error('Invalid password or token, or expired link.');
|
||||
}
|
||||
|
||||
JWTStorage.updateJWT(access_token);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
ToasterService.error('Unable to access documents: ' + e);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
refresh(folderId);
|
||||
(async () => {
|
||||
await setPublicToken(token || '');
|
||||
refresh(folderId);
|
||||
})();
|
||||
}, []);
|
||||
|
||||
if (!details?.item?.id && loading) {
|
||||
@@ -125,8 +154,8 @@ const AccessChecker = ({
|
||||
<Button
|
||||
className="rounded-l-none"
|
||||
theme="primary"
|
||||
onClick={() => {
|
||||
setPublicLinkToken(token ? encodeURIComponent(token + '+' + password) : null);
|
||||
onClick={async () => {
|
||||
await setPublicToken(token || '', password);
|
||||
refresh(folderId);
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -12,13 +12,17 @@ import { ConfirmDeleteModalAtom } from '../body/drive/modals/confirm-delete';
|
||||
import { CreateModal, CreateModalAtom } from '../body/drive/modals/create';
|
||||
import { Button } from '@atoms/button/button';
|
||||
|
||||
export const CreateModalWithUploadZones = () => {
|
||||
export const CreateModalWithUploadZones = ({ initialParentId }: { initialParentId?: string }) => {
|
||||
const companyId = useRouterCompany();
|
||||
const uploadZoneRef = useRef<UploadZone | null>(null);
|
||||
const uploadFolderZoneRef = useRef<UploadZone | null>(null);
|
||||
const setCreationModalState = useSetRecoilState(CreateModalAtom);
|
||||
const { uploadTree, uploadFromUrl } = useDriveUpload();
|
||||
const [parentId, _] = useRecoilState(DriveCurrentFolderAtom('root'));
|
||||
const [parentId, _] = useRecoilState(
|
||||
DriveCurrentFolderAtom({ initialFolderId: initialParentId || 'root' }),
|
||||
);
|
||||
|
||||
console.log(parentId);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -73,7 +77,7 @@ export const CreateModalWithUploadZones = () => {
|
||||
};
|
||||
|
||||
export default () => {
|
||||
const [parentId, _] = useRecoilState(DriveCurrentFolderAtom('root'));
|
||||
const [parentId, _] = useRecoilState(DriveCurrentFolderAtom({ initialFolderId: 'root' }));
|
||||
const { access, item, inTrash } = useDriveItem(parentId);
|
||||
const { children: trashChildren } = useDriveItem('trash');
|
||||
const uploadZoneRef = useRef<UploadZone | null>(null);
|
||||
@@ -89,7 +93,7 @@ export default () => {
|
||||
<div className="-m-4 overflow-hidden">
|
||||
<AnimatedHeight>
|
||||
<div className="p-4">
|
||||
<CreateModalWithUploadZones />
|
||||
<CreateModalWithUploadZones initialParentId={parentId} />
|
||||
|
||||
{inTrash && access === 'manage' && (
|
||||
<>
|
||||
|
||||
@@ -19,7 +19,9 @@ import DiskUsage from '../common/disk-usage';
|
||||
import Actions from './actions';
|
||||
|
||||
export default () => {
|
||||
const [parentId, setParentId] = useRecoilState(DriveCurrentFolderAtom('root'));
|
||||
const [parentId, setParentId] = useRecoilState(
|
||||
DriveCurrentFolderAtom({ initialFolderId: 'root' }),
|
||||
);
|
||||
const { user } = useCurrentUser();
|
||||
const active = false;
|
||||
const { access: rootAccess } = useDriveItem('root');
|
||||
|
||||
Reference in New Issue
Block a user