feat: generate presigned URLs for avatars
This commit is contained in:
@@ -9,7 +9,7 @@ import { generateUID } from "@kan/shared/utils";
|
|||||||
|
|
||||||
import { env } from "~/env";
|
import { env } from "~/env";
|
||||||
import { withRateLimit } from "@kan/api/utils/rateLimit";
|
import { withRateLimit } from "@kan/api/utils/rateLimit";
|
||||||
import { createS3Client } from "@kan/api/utils/s3";
|
import { createS3Client } from "@kan/shared/utils";
|
||||||
import { assertPermission } from "@kan/api/utils/permissions";
|
import { assertPermission } from "@kan/api/utils/permissions";
|
||||||
|
|
||||||
const MAX_SIZE_BYTES = 50 * 1024 * 1024; // 50MB
|
const MAX_SIZE_BYTES = 50 * 1024 * 1024; // 50MB
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import * as userRepo from "@kan/db/repository/user.repo";
|
|||||||
|
|
||||||
import { env } from "~/env";
|
import { env } from "~/env";
|
||||||
import { withRateLimit } from "@kan/api/utils/rateLimit";
|
import { withRateLimit } from "@kan/api/utils/rateLimit";
|
||||||
import { createS3Client } from "@kan/api/utils/s3";
|
import { createS3Client } from "@kan/shared/utils";
|
||||||
|
|
||||||
const MAX_SIZE_BYTES = 2 * 1024 * 1024; // 2MB
|
const MAX_SIZE_BYTES = 2 * 1024 * 1024; // 2MB
|
||||||
const allowedContentTypes = ["image/jpeg", "image/png", "image/webp"];
|
const allowedContentTypes = ["image/jpeg", "image/png", "image/webp"];
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import { env } from "next-runtime-env";
|
|
||||||
|
|
||||||
export const formatToArray = (
|
export const formatToArray = (
|
||||||
value: string | string[] | undefined,
|
value: string | string[] | undefined,
|
||||||
): string[] => {
|
): string[] => {
|
||||||
@@ -52,14 +50,5 @@ export const getAvatarUrl = (imageOrKey: string | null) => {
|
|||||||
return imageOrKey;
|
return imageOrKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
const bucket = env("NEXT_PUBLIC_AVATAR_BUCKET_NAME");
|
return "";
|
||||||
const useVirtualHostedUrls = env("NEXT_PUBLIC_USE_VIRTUAL_HOSTED_URLS");
|
|
||||||
const storageDomain = env("NEXT_PUBLIC_STORAGE_DOMAIN");
|
|
||||||
|
|
||||||
if (useVirtualHostedUrls === "true" && storageDomain) {
|
|
||||||
return `https://${bucket}.${storageDomain}/${imageOrKey}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const storageUrl = env("NEXT_PUBLIC_STORAGE_URL");
|
|
||||||
return `${storageUrl}/${bucket}/${imageOrKey}`;
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -28,10 +28,6 @@
|
|||||||
"types": "./dist/utils/rateLimit.d.ts",
|
"types": "./dist/utils/rateLimit.d.ts",
|
||||||
"default": "./src/utils/rateLimit.ts"
|
"default": "./src/utils/rateLimit.ts"
|
||||||
},
|
},
|
||||||
"./utils/s3": {
|
|
||||||
"types": "./dist/utils/s3.d.ts",
|
|
||||||
"default": "./src/utils/s3.ts"
|
|
||||||
},
|
|
||||||
"./utils/permissions": {
|
"./utils/permissions": {
|
||||||
"types": "./dist/utils/permissions.d.ts",
|
"types": "./dist/utils/permissions.d.ts",
|
||||||
"default": "./src/utils/permissions.ts"
|
"default": "./src/utils/permissions.ts"
|
||||||
@@ -47,8 +43,6 @@
|
|||||||
"typecheck": "tsc --noEmit --emitDeclarationOnly false"
|
"typecheck": "tsc --noEmit --emitDeclarationOnly false"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-s3": "^3.802.0",
|
|
||||||
"@aws-sdk/s3-request-presigner": "^3.812.0",
|
|
||||||
"@kan/auth": "workspace:*",
|
"@kan/auth": "workspace:*",
|
||||||
"@kan/db": "workspace:*",
|
"@kan/db": "workspace:*",
|
||||||
"@kan/email": "workspace:^",
|
"@kan/email": "workspace:^",
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { generateUID } from "@kan/shared/utils";
|
|||||||
|
|
||||||
import { createTRPCRouter, protectedProcedure } from "../trpc";
|
import { createTRPCRouter, protectedProcedure } from "../trpc";
|
||||||
import { assertPermission } from "../utils/permissions";
|
import { assertPermission } from "../utils/permissions";
|
||||||
import { deleteObject, generateUploadUrl } from "../utils/s3";
|
import { deleteObject, generateUploadUrl } from "@kan/shared/utils";
|
||||||
|
|
||||||
export const attachmentRouter = createTRPCRouter({
|
export const attachmentRouter = createTRPCRouter({
|
||||||
generateUploadUrl: protectedProcedure
|
generateUploadUrl: protectedProcedure
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import * as workspaceRepo from "@kan/db/repository/workspace.repo";
|
|||||||
import { colours } from "@kan/shared/constants";
|
import { colours } from "@kan/shared/constants";
|
||||||
import {
|
import {
|
||||||
convertDueDateFiltersToRanges,
|
convertDueDateFiltersToRanges,
|
||||||
|
generateAvatarUrl,
|
||||||
generateSlug,
|
generateSlug,
|
||||||
generateUID,
|
generateUID,
|
||||||
} from "@kan/shared/utils";
|
} from "@kan/shared/utils";
|
||||||
@@ -142,7 +143,33 @@ export const boardRouter = createTRPCRouter({
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
return result;
|
// Generate presigned URLs for workspace member avatars
|
||||||
|
const workspaceWithAvatarUrls = result.workspace
|
||||||
|
? {
|
||||||
|
...result.workspace,
|
||||||
|
members: await Promise.all(
|
||||||
|
result.workspace.members.map(async (member) => {
|
||||||
|
if (!member.user?.image) {
|
||||||
|
return member;
|
||||||
|
}
|
||||||
|
|
||||||
|
const avatarUrl = await generateAvatarUrl(member.user.image);
|
||||||
|
return {
|
||||||
|
...member,
|
||||||
|
user: {
|
||||||
|
...member.user,
|
||||||
|
image: avatarUrl,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
: result.workspace;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...result,
|
||||||
|
workspace: workspaceWithAvatarUrls,
|
||||||
|
};
|
||||||
}),
|
}),
|
||||||
bySlug: publicProcedure
|
bySlug: publicProcedure
|
||||||
.meta({
|
.meta({
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import * as workspaceRepo from "@kan/db/repository/workspace.repo";
|
|||||||
import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc";
|
import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc";
|
||||||
import { mergeActivities } from "../utils/activities";
|
import { mergeActivities } from "../utils/activities";
|
||||||
import { assertCanDelete, assertCanEdit, assertPermission } from "../utils/permissions";
|
import { assertCanDelete, assertCanEdit, assertPermission } from "../utils/permissions";
|
||||||
import { generateDownloadUrl } from "../utils/s3";
|
import { generateAttachmentUrl, generateAvatarUrl } from "@kan/shared/utils";
|
||||||
|
|
||||||
export const cardRouter = createTRPCRouter({
|
export const cardRouter = createTRPCRouter({
|
||||||
create: protectedProcedure
|
create: protectedProcedure
|
||||||
@@ -631,45 +631,54 @@ export const cardRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Generate URLs for all attachments
|
// Generate URLs for all attachments
|
||||||
const bucket = process.env.NEXT_PUBLIC_ATTACHMENTS_BUCKET_NAME;
|
const attachmentsWithUrls = await Promise.all(
|
||||||
if (result.attachments && Array.isArray(result.attachments)) {
|
result.attachments.map(async (attachment) => {
|
||||||
const attachments = result.attachments as {
|
const url = await generateAttachmentUrl(attachment.s3Key);
|
||||||
publicId: string;
|
return {
|
||||||
contentType: string;
|
publicId: attachment.publicId,
|
||||||
s3Key: string;
|
contentType: attachment.contentType,
|
||||||
originalFilename: string | null;
|
s3Key: attachment.s3Key,
|
||||||
size?: number | null;
|
originalFilename: attachment.originalFilename,
|
||||||
}[];
|
size: attachment.size,
|
||||||
|
url,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
const attachmentsWithUrls = await Promise.all(
|
// Generate presigned URLs for workspace member avatars
|
||||||
attachments.map(async (attachment) => {
|
const workspaceWithAvatarUrls = result.list.board.workspace
|
||||||
const base = {
|
? {
|
||||||
publicId: attachment.publicId,
|
...result.list.board.workspace,
|
||||||
contentType: attachment.contentType,
|
members: await Promise.all(
|
||||||
s3Key: attachment.s3Key,
|
result.list.board.workspace.members.map(async (member) => {
|
||||||
originalFilename: attachment.originalFilename,
|
if (!member.user?.image) {
|
||||||
size: attachment.size,
|
return member;
|
||||||
};
|
}
|
||||||
if (!bucket || !attachment.s3Key) {
|
|
||||||
return { ...base, url: null };
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const url = await generateDownloadUrl(
|
|
||||||
bucket,
|
|
||||||
attachment.s3Key,
|
|
||||||
86400, // 24 hours expiration
|
|
||||||
);
|
|
||||||
return { ...base, url };
|
|
||||||
} catch {
|
|
||||||
// If URL generation fails, return attachment with url: null
|
|
||||||
return { ...base, url: null };
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
return { ...result, attachments: attachmentsWithUrls };
|
|
||||||
}
|
|
||||||
|
|
||||||
return { ...result, attachments: [] };
|
const avatarUrl = await generateAvatarUrl(member.user.image);
|
||||||
|
return {
|
||||||
|
...member,
|
||||||
|
user: {
|
||||||
|
...member.user,
|
||||||
|
image: avatarUrl,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
: result.list.board.workspace;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...result,
|
||||||
|
attachments: attachmentsWithUrls,
|
||||||
|
list: {
|
||||||
|
...result.list,
|
||||||
|
board: {
|
||||||
|
...result.list.board,
|
||||||
|
workspace: workspaceWithAvatarUrls,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
}),
|
}),
|
||||||
getActivities: publicProcedure
|
getActivities: publicProcedure
|
||||||
.meta({
|
.meta({
|
||||||
@@ -738,7 +747,39 @@ export const cardRouter = createTRPCRouter({
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const mergedActivities = mergeActivities(result.activities);
|
// Generate presigned URLs for user avatars in activities
|
||||||
|
const activitiesWithAvatarUrls = await Promise.all(
|
||||||
|
result.activities.map(async (activity) => {
|
||||||
|
const updatedActivity = { ...activity };
|
||||||
|
|
||||||
|
// Generate presigned URL for activity user avatar
|
||||||
|
if (activity.user?.image) {
|
||||||
|
const userAvatarUrl = await generateAvatarUrl(activity.user.image);
|
||||||
|
updatedActivity.user = {
|
||||||
|
...activity.user,
|
||||||
|
image: userAvatarUrl,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate presigned URL for member user avatar (if exists)
|
||||||
|
if (activity.member?.user?.image) {
|
||||||
|
const memberAvatarUrl = await generateAvatarUrl(
|
||||||
|
activity.member.user.image,
|
||||||
|
);
|
||||||
|
updatedActivity.member = {
|
||||||
|
...activity.member,
|
||||||
|
user: {
|
||||||
|
...activity.member.user,
|
||||||
|
image: memberAvatarUrl,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return updatedActivity;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const mergedActivities = mergeActivities(activitiesWithAvatarUrls);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
activities: mergedActivities,
|
activities: mergedActivities,
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import {
|
|||||||
createTRPCRouter,
|
createTRPCRouter,
|
||||||
publicProcedure,
|
publicProcedure,
|
||||||
} from "../trpc";
|
} from "../trpc";
|
||||||
import { createS3Client } from "../utils/s3";
|
import { createS3Client } from "@kan/shared/utils";
|
||||||
|
|
||||||
const checkDatabaseConnection = async (db: dbClient) => {
|
const checkDatabaseConnection = async (db: dbClient) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { z } from "zod";
|
|||||||
import * as userRepo from "@kan/db/repository/user.repo";
|
import * as userRepo from "@kan/db/repository/user.repo";
|
||||||
|
|
||||||
import { createTRPCRouter, protectedProcedure } from "../trpc";
|
import { createTRPCRouter, protectedProcedure } from "../trpc";
|
||||||
|
import { generateAvatarUrl } from "@kan/shared/utils";
|
||||||
|
|
||||||
export const userRouter = createTRPCRouter({
|
export const userRouter = createTRPCRouter({
|
||||||
getUser: protectedProcedure
|
getUser: protectedProcedure
|
||||||
@@ -55,8 +56,12 @@ export const userRouter = createTRPCRouter({
|
|||||||
|
|
||||||
const apiKey = result.apiKeys[0];
|
const apiKey = result.apiKeys[0];
|
||||||
|
|
||||||
|
// Generate presigned URL for avatar
|
||||||
|
const imageUrl = await generateAvatarUrl(result.image);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...result,
|
...result,
|
||||||
|
image: imageUrl,
|
||||||
apiKey: apiKey ?? null,
|
apiKey: apiKey ?? null,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
@@ -102,6 +107,12 @@ export const userRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
// Generate presigned URL for avatar
|
||||||
|
const imageUrl = await generateAvatarUrl(result.image);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...result,
|
||||||
|
image: imageUrl,
|
||||||
|
};
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { generateUID } from "@kan/shared/utils";
|
|||||||
|
|
||||||
import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc";
|
import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc";
|
||||||
import { assertPermission } from "../utils/permissions";
|
import { assertPermission } from "../utils/permissions";
|
||||||
|
import { generateAvatarUrl } from "@kan/shared/utils";
|
||||||
|
|
||||||
export const workspaceRouter = createTRPCRouter({
|
export const workspaceRouter = createTRPCRouter({
|
||||||
all: protectedProcedure
|
all: protectedProcedure
|
||||||
@@ -86,9 +87,27 @@ export const workspaceRouter = createTRPCRouter({
|
|||||||
const shouldShowEmails =
|
const shouldShowEmails =
|
||||||
isAdmin || result.showEmailsToMembers === true;
|
isAdmin || result.showEmailsToMembers === true;
|
||||||
|
|
||||||
|
// Generate presigned URLs for member avatars
|
||||||
|
const membersWithAvatarUrls = await Promise.all(
|
||||||
|
result.members.map(async (member) => {
|
||||||
|
if (!member.user?.image) {
|
||||||
|
return member;
|
||||||
|
}
|
||||||
|
|
||||||
|
const avatarUrl = await generateAvatarUrl(member.user.image);
|
||||||
|
return {
|
||||||
|
...member,
|
||||||
|
user: {
|
||||||
|
...member.user,
|
||||||
|
image: avatarUrl,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
// If emails should be hidden, filter them out
|
// If emails should be hidden, filter them out
|
||||||
if (!shouldShowEmails) {
|
if (!shouldShowEmails) {
|
||||||
const sanitizedMembers = result.members.map((member) => {
|
const sanitizedMembers = membersWithAvatarUrls.map((member) => {
|
||||||
// If user doesn't have a display name, use anonymous identifier
|
// If user doesn't have a display name, use anonymous identifier
|
||||||
const displayName =
|
const displayName =
|
||||||
member.user?.name?.trim() ?? `anonymous_${member.publicId}`;
|
member.user?.name?.trim() ?? `anonymous_${member.publicId}`;
|
||||||
@@ -120,7 +139,10 @@ export const workspaceRouter = createTRPCRouter({
|
|||||||
} as Awaited<ReturnType<typeof workspaceRepo.getByPublicIdWithMembers>>;
|
} as Awaited<ReturnType<typeof workspaceRepo.getByPublicIdWithMembers>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return {
|
||||||
|
...result,
|
||||||
|
members: membersWithAvatarUrls,
|
||||||
|
};
|
||||||
}),
|
}),
|
||||||
bySlug: publicProcedure
|
bySlug: publicProcedure
|
||||||
.meta({
|
.meta({
|
||||||
|
|||||||
@@ -7,8 +7,7 @@ import type { dbClient } from "@kan/db/client";
|
|||||||
import * as memberRepo from "@kan/db/repository/member.repo";
|
import * as memberRepo from "@kan/db/repository/member.repo";
|
||||||
import * as userRepo from "@kan/db/repository/user.repo";
|
import * as userRepo from "@kan/db/repository/user.repo";
|
||||||
import { notificationClient } from "@kan/email";
|
import { notificationClient } from "@kan/email";
|
||||||
import { createEmailUnsubscribeLink } from "@kan/shared";
|
import { createEmailUnsubscribeLink, createS3Client } from "@kan/shared";
|
||||||
import { createS3Client } from "@kan/api/utils/s3";
|
|
||||||
|
|
||||||
import { downloadImage } from "./utils";
|
import { downloadImage } from "./utils";
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,8 @@
|
|||||||
},
|
},
|
||||||
"prettier": "@kan/prettier-config",
|
"prettier": "@kan/prettier-config",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@aws-sdk/client-s3": "^3.802.0",
|
||||||
|
"@aws-sdk/s3-request-presigner": "^3.812.0",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"jose": "^6.1.2",
|
"jose": "^6.1.2",
|
||||||
"nanoid": "^5.0.9",
|
"nanoid": "^5.0.9",
|
||||||
|
|||||||
@@ -3,3 +3,4 @@ export * from "./generateSlug";
|
|||||||
export * from "./subscriptions";
|
export * from "./subscriptions";
|
||||||
export * from "./email";
|
export * from "./email";
|
||||||
export * from "./dueDateFilters";
|
export * from "./dueDateFilters";
|
||||||
|
export * from "./s3";
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
S3Client,
|
S3Client,
|
||||||
} from "@aws-sdk/client-s3";
|
} from "@aws-sdk/client-s3";
|
||||||
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
|
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
|
||||||
|
import { env } from "next-runtime-env";
|
||||||
|
|
||||||
export function createS3Client() {
|
export function createS3Client() {
|
||||||
const credentials =
|
const credentials =
|
||||||
@@ -67,3 +68,54 @@ export async function deleteObject(bucket: string, key: string) {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate presigned URL for an avatar image
|
||||||
|
* Returns null if image key is missing, bucket is not configured, or URL generation fails
|
||||||
|
*/
|
||||||
|
export async function generateAvatarUrl(
|
||||||
|
imageKey: string | null | undefined,
|
||||||
|
expiresIn = 86400, // 24 hours
|
||||||
|
): Promise<string | null> {
|
||||||
|
if (!imageKey) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bucket = env("NEXT_PUBLIC_AVATAR_BUCKET_NAME");
|
||||||
|
if (!bucket) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await generateDownloadUrl(bucket, imageKey, expiresIn);
|
||||||
|
} catch {
|
||||||
|
// If URL generation fails, return null
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate presigned URL for an attachment
|
||||||
|
* Returns null if attachment key is missing, bucket is not configured, or URL generation fails
|
||||||
|
*/
|
||||||
|
export async function generateAttachmentUrl(
|
||||||
|
attachmentKey: string | null | undefined,
|
||||||
|
expiresIn = 86400, // 24 hours
|
||||||
|
): Promise<string | null> {
|
||||||
|
if (!attachmentKey) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bucket = env("NEXT_PUBLIC_ATTACHMENTS_BUCKET_NAME");
|
||||||
|
if (!bucket) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await generateDownloadUrl(bucket, attachmentKey, expiresIn);
|
||||||
|
} catch {
|
||||||
|
// If URL generation fails, return null
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
12
pnpm-lock.yaml
generated
12
pnpm-lock.yaml
generated
@@ -293,12 +293,6 @@ importers:
|
|||||||
|
|
||||||
packages/api:
|
packages/api:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@aws-sdk/client-s3':
|
|
||||||
specifier: ^3.802.0
|
|
||||||
version: 3.879.0
|
|
||||||
'@aws-sdk/s3-request-presigner':
|
|
||||||
specifier: ^3.812.0
|
|
||||||
version: 3.879.0
|
|
||||||
'@kan/auth':
|
'@kan/auth':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../auth
|
version: link:../auth
|
||||||
@@ -483,6 +477,12 @@ importers:
|
|||||||
|
|
||||||
packages/shared:
|
packages/shared:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
'@aws-sdk/client-s3':
|
||||||
|
specifier: ^3.802.0
|
||||||
|
version: 3.879.0
|
||||||
|
'@aws-sdk/s3-request-presigner':
|
||||||
|
specifier: ^3.812.0
|
||||||
|
version: 3.879.0
|
||||||
date-fns:
|
date-fns:
|
||||||
specifier: ^4.1.0
|
specifier: ^4.1.0
|
||||||
version: 4.1.0
|
version: 4.1.0
|
||||||
|
|||||||
Reference in New Issue
Block a user