feat: add preview feature for social media sharing (#293)

This commit is contained in:
Copilot
2025-10-20 10:56:19 -03:00
committed by GitHub
parent 39dc94b7f8
commit cce9847242
24 changed files with 443 additions and 35 deletions

View File

@@ -536,4 +536,17 @@ export class ReverseShareController {
return reply.status(500).send({ error: "Internal server error" });
}
}
async getReverseShareMetadataByAlias(request: FastifyRequest, reply: FastifyReply) {
try {
const { alias } = request.params as { alias: string };
const metadata = await this.reverseShareService.getReverseShareMetadataByAlias(alias);
return reply.send(metadata);
} catch (error: any) {
if (error.message === "Reverse share not found") {
return reply.status(404).send({ error: error.message });
}
return reply.status(400).send({ error: error.message });
}
}
}

View File

@@ -592,4 +592,32 @@ export async function reverseShareRoutes(app: FastifyInstance) {
},
reverseShareController.copyFileToUserFiles.bind(reverseShareController)
);
app.get(
"/reverse-shares/alias/:alias/metadata",
{
schema: {
tags: ["Reverse Share"],
operationId: "getReverseShareMetadataByAlias",
summary: "Get reverse share metadata by alias for Open Graph",
description: "Get lightweight metadata for a reverse share by alias, used for social media previews",
params: z.object({
alias: z.string().describe("Alias of the reverse share"),
}),
response: {
200: z.object({
name: z.string().nullable(),
description: z.string().nullable(),
totalFiles: z.number(),
hasPassword: z.boolean(),
isExpired: z.boolean(),
isInactive: z.boolean(),
maxFiles: z.number().nullable(),
}),
404: z.object({ error: z.string() }),
},
},
},
reverseShareController.getReverseShareMetadataByAlias.bind(reverseShareController)
);
}

View File

@@ -773,4 +773,30 @@ export class ReverseShareService {
updatedAt: file.updatedAt.toISOString(),
};
}
async getReverseShareMetadataByAlias(alias: string) {
const reverseShare = await this.reverseShareRepository.findByAlias(alias);
if (!reverseShare) {
throw new Error("Reverse share not found");
}
// Check if reverse share is expired
const isExpired = reverseShare.expiration && new Date(reverseShare.expiration) < new Date();
// Check if inactive
const isInactive = !reverseShare.isActive;
const totalFiles = reverseShare.files?.length || 0;
const hasPassword = !!reverseShare.password;
return {
name: reverseShare.name,
description: reverseShare.description,
totalFiles,
hasPassword,
isExpired,
isInactive,
maxFiles: reverseShare.maxFiles,
};
}
}

View File

@@ -295,4 +295,17 @@ export class ShareController {
return reply.status(400).send({ error: error.message });
}
}
async getShareMetadataByAlias(request: FastifyRequest, reply: FastifyReply) {
try {
const { alias } = request.params as { alias: string };
const metadata = await this.shareService.getShareMetadataByAlias(alias);
return reply.send(metadata);
} catch (error: any) {
if (error.message === "Share not found") {
return reply.status(404).send({ error: error.message });
}
return reply.status(400).send({ error: error.message });
}
}
}

View File

@@ -17,6 +17,9 @@ export interface IShareRepository {
findShareBySecurityId(
securityId: string
): Promise<(Share & { security: ShareSecurity; files: any[]; folders: any[] }) | null>;
findShareByAlias(
alias: string
): Promise<(Share & { security: ShareSecurity; files: any[]; folders: any[]; recipients: any[] }) | null>;
updateShare(id: string, data: Partial<Share>): Promise<Share>;
updateShareSecurity(id: string, data: Partial<ShareSecurity>): Promise<ShareSecurity>;
deleteShare(id: string): Promise<Share>;
@@ -130,6 +133,41 @@ export class PrismaShareRepository implements IShareRepository {
});
}
async findShareByAlias(alias: string) {
const shareAlias = await prisma.shareAlias.findUnique({
where: { alias },
include: {
share: {
include: {
security: true,
files: true,
folders: {
select: {
id: true,
name: true,
description: true,
objectName: true,
parentId: true,
userId: true,
createdAt: true,
updatedAt: true,
_count: {
select: {
files: true,
children: true,
},
},
},
},
recipients: true,
},
},
},
});
return shareAlias?.share || null;
}
async updateShare(id: string, data: Partial<Share>): Promise<Share> {
return prisma.share.update({
where: { id },

View File

@@ -347,4 +347,32 @@ export async function shareRoutes(app: FastifyInstance) {
},
shareController.notifyRecipients.bind(shareController)
);
app.get(
"/shares/alias/:alias/metadata",
{
schema: {
tags: ["Share"],
operationId: "getShareMetadataByAlias",
summary: "Get share metadata by alias for Open Graph",
description: "Get lightweight metadata for a share by alias, used for social media previews",
params: z.object({
alias: z.string().describe("The share alias"),
}),
response: {
200: z.object({
name: z.string().nullable(),
description: z.string().nullable(),
totalFiles: z.number(),
totalFolders: z.number(),
hasPassword: z.boolean(),
isExpired: z.boolean(),
isMaxViewsReached: z.boolean(),
}),
404: z.object({ error: z.string() }),
},
},
},
shareController.getShareMetadataByAlias.bind(shareController)
);
}

View File

@@ -440,4 +440,31 @@ export class ShareService {
notifiedRecipients,
};
}
async getShareMetadataByAlias(alias: string) {
const share = await this.shareRepository.findShareByAlias(alias);
if (!share) {
throw new Error("Share not found");
}
// Check if share is expired
const isExpired = share.expiration && new Date(share.expiration) < new Date();
// Check if max views reached
const isMaxViewsReached = share.security.maxViews !== null && share.views >= share.security.maxViews;
const totalFiles = share.files?.length || 0;
const totalFolders = share.folders?.length || 0;
const hasPassword = !!share.security.password;
return {
name: share.name,
description: share.description,
totalFiles,
totalFolders,
hasPassword,
isExpired,
isMaxViewsReached,
};
}
}