Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 28 additions & 1 deletion apps/web/actions/account/remove-profile-image.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@
import { db } from "@cap/database";
import { getCurrentUser } from "@cap/database/auth/session";
import { users } from "@cap/database/schema";
import { S3Buckets } from "@cap/web-backend";
Comment thread
ameer2468 marked this conversation as resolved.
import { eq } from "drizzle-orm";
import { Effect, Option } from "effect";
import { revalidatePath } from "next/cache";
import { runPromise } from "@/lib/server";

export async function removeProfileImage() {
const user = await getCurrentUser();
Expand All @@ -13,10 +16,34 @@
throw new Error("Unauthorized");
}

const image = user.image;

// Delete the profile image from S3 if it exists
if (image) {
try {
// Extract the S3 key - handle both old URL format and new key format
let s3Key = image;
if (image.includes("amazonaws.com")) {
Comment thread Fixed
const url = new URL(image);
s3Key = url.pathname.substring(1); // Remove leading slash
}

// Only delete if it looks like a user profile image key
if (s3Key.startsWith("users/")) {
await Effect.gen(function* () {
const [bucket] = yield* S3Buckets.getBucketAccess(Option.none());
yield* bucket.deleteObject(s3Key);
}).pipe(runPromise);
}
Comment thread
ameer2468 marked this conversation as resolved.
} catch (error) {
console.error("Error deleting profile image from S3:", error);
// Continue with database update even if S3 deletion fails
}
}

await db().update(users).set({ image: null }).where(eq(users.id, user.id));

revalidatePath("/dashboard/settings/account");
revalidatePath("/dashboard", "layout");

return { success: true } as const;
}
47 changes: 30 additions & 17 deletions apps/web/actions/account/upload-profile-image.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import { db } from "@cap/database";
import { getCurrentUser } from "@cap/database/auth/session";
import { users } from "@cap/database/schema";
import { serverEnv } from "@cap/env";
import { S3Buckets } from "@cap/web-backend";
import { eq } from "drizzle-orm";
import { Effect, Option } from "effect";
Expand Down Expand Up @@ -43,15 +42,38 @@
throw new Error("File size must be 3MB or less");
}

// Get the old profile image to delete it later
const oldImageUrlOrKey = user.image;

const fileKey = `users/${user.id}/profile-${Date.now()}-${randomUUID()}.${fileExtension}`;

try {
const sanitizedFile = await sanitizeFile(file);
let imageUrl: string | undefined;
let image: string | null = null;

await Effect.gen(function* () {
const [bucket] = yield* S3Buckets.getBucketAccess(Option.none());

// Delete old profile image if it exists
if (oldImageUrlOrKey) {
try {
// Extract the S3 key - handle both old URL format and new key format
let oldS3Key = oldImageUrlOrKey;
if (oldImageUrlOrKey.includes("amazonaws.com")) {
Comment thread Fixed
const url = new URL(oldImageUrlOrKey);
oldS3Key = url.pathname.substring(1); // Remove leading slash
}

Comment thread
ameer2468 marked this conversation as resolved.
// Only delete if it looks like a user profile image key
if (oldS3Key.startsWith("users/")) {
yield* bucket.deleteObject(oldS3Key);
}
} catch (error) {
console.error("Error deleting old profile image from S3:", error);
// Continue with upload even if deletion fails
}
}

const bodyBytes = yield* Effect.promise(async () => {
const buf = await sanitizedFile.arrayBuffer();
return new Uint8Array(buf);
Expand All @@ -61,32 +83,23 @@
contentType: file.type,
});

if (serverEnv().CAP_AWS_BUCKET_URL) {
imageUrl = `${serverEnv().CAP_AWS_BUCKET_URL}/${fileKey}`;
} else if (serverEnv().CAP_AWS_ENDPOINT) {
imageUrl = `${serverEnv().CAP_AWS_ENDPOINT}/${bucket.bucketName}/${fileKey}`;
} else {
imageUrl = `https://${bucket.bucketName}.s3.${
serverEnv().CAP_AWS_REGION || "us-east-1"
}.amazonaws.com/${fileKey}`;
}
image = fileKey;
}).pipe(runPromise);

if (typeof imageUrl !== "string" || imageUrl.length === 0) {
throw new Error("Failed to resolve uploaded profile image URL");
if (!image) {
throw new Error("Failed to resolve uploaded profile image key");
}

const finalImageUrl = imageUrl;
const finalImageUrlOrKey = image;

await db()
.update(users)
.set({ image: finalImageUrl })
.set({ image: finalImageUrlOrKey })
.where(eq(users.id, user.id));

revalidatePath("/dashboard/settings/account");
revalidatePath("/dashboard", "layout");

return { success: true, imageUrl: finalImageUrl } as const;
return { success: true, image: finalImageUrlOrKey } as const;
} catch (error) {
console.error("Error uploading profile image:", error);
throw new Error(error instanceof Error ? error.message : "Upload failed");
Expand Down
16 changes: 1 addition & 15 deletions apps/web/actions/organization/create-space.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { db } from "@cap/database";
import { getCurrentUser } from "@cap/database/auth/session";
import { nanoId, nanoIdLength } from "@cap/database/helpers";
import { spaceMembers, spaces, users } from "@cap/database/schema";
import { serverEnv } from "@cap/env";
import { S3Buckets } from "@cap/web-backend";
import { Space } from "@cap/web-domain";
import { and, eq, inArray } from "drizzle-orm";
Expand Down Expand Up @@ -100,20 +99,7 @@ export async function createSpace(
yield* Effect.promise(() => iconFile.bytes()),
{ contentType: iconFile.type },
);

// Construct the icon URL
if (serverEnv().CAP_AWS_BUCKET_URL) {
// If a custom bucket URL is defined, use it
iconUrl = `${serverEnv().CAP_AWS_BUCKET_URL}/${fileKey}`;
} else if (serverEnv().CAP_AWS_ENDPOINT) {
// For custom endpoints like MinIO
iconUrl = `${serverEnv().CAP_AWS_ENDPOINT}/${bucket.bucketName}/${fileKey}`;
} else {
// Default AWS S3 URL format
iconUrl = `https://${bucket.bucketName}.s3.${
serverEnv().CAP_AWS_REGION || "us-east-1"
}.amazonaws.com/${fileKey}`;
}
iconUrl = fileKey;
Comment thread
ameer2468 marked this conversation as resolved.
}).pipe(runPromise);
} catch (error) {
console.error("Error uploading space icon:", error);
Expand Down
28 changes: 28 additions & 0 deletions apps/web/actions/organization/remove-icon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@
import { db } from "@cap/database";
import { getCurrentUser } from "@cap/database/auth/session";
import { organizations } from "@cap/database/schema";
import { S3Buckets } from "@cap/web-backend";
import type { Organisation } from "@cap/web-domain";
import { eq } from "drizzle-orm";
import { Effect, Option } from "effect";
import { revalidatePath } from "next/cache";
import { runPromise } from "@/lib/server";

export async function removeOrganizationIcon(
organizationId: Organisation.OrganisationId,
Expand All @@ -29,6 +32,31 @@
throw new Error("Only the owner can remove the organization icon");
}

const iconUrl = organization[0]?.iconUrl;

// Delete the icon from S3 if it exists
if (iconUrl) {
try {
// Extract the S3 key - handle both old URL format and new key format
let s3Key = iconUrl;
if (iconUrl.includes("amazonaws.com")) {
Comment thread Fixed
const url = new URL(iconUrl);
s3Key = url.pathname.substring(1); // Remove leading slash
}
Comment thread
ameer2468 marked this conversation as resolved.

// Only delete if it looks like an organization icon key
if (s3Key.startsWith("organizations/")) {
await Effect.gen(function* () {
const [bucket] = yield* S3Buckets.getBucketAccess(Option.none());
yield* bucket.deleteObject(s3Key);
}).pipe(runPromise);
}
Comment thread
ameer2468 marked this conversation as resolved.
} catch (error) {
console.error("Error deleting organization icon from S3:", error);
// Continue with database update even if S3 deletion fails
}
}

// Update organization to remove icon URL
await db()
.update(organizations)
Expand Down
5 changes: 4 additions & 1 deletion apps/web/actions/organization/update-space.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,10 @@ export async function updateSpace(formData: FormData) {
const spaceArr = await db().select().from(spaces).where(eq(spaces.id, id));
const space = spaceArr[0];
if (space?.iconUrl) {
const key = space.iconUrl.match(/organizations\/.+/)?.[0];
// Extract the S3 key (it might already be a key or could be a legacy URL)
const key = space.iconUrl.startsWith("organizations/")
? space.iconUrl
: space.iconUrl.match(/organizations\/.+/)?.[0];

if (key) {
try {
Expand Down
41 changes: 25 additions & 16 deletions apps/web/actions/organization/upload-organization-icon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import { db } from "@cap/database";
import { getCurrentUser } from "@cap/database/auth/session";
import { organizations } from "@cap/database/schema";
import { serverEnv } from "@cap/env";
import { S3Buckets } from "@cap/web-backend";
import type { Organisation } from "@cap/web-domain";
import { eq } from "drizzle-orm";
Expand Down Expand Up @@ -52,39 +51,49 @@
throw new Error("File size must be less than 1MB");
}

// Get the old icon to delete it later
const oldIconUrlOrKey = organization[0]?.iconUrl;

// Create a unique file key
const fileExtension = file.name.split(".").pop();
const fileKey = `organizations/${organizationId}/icon-${Date.now()}.${fileExtension}`;

try {
const sanitizedFile = await sanitizeFile(file);
let iconUrl: string | undefined;

await Effect.gen(function* () {
const [bucket] = yield* S3Buckets.getBucketAccess(Option.none());

// Delete old icon if it exists
if (oldIconUrlOrKey) {
try {
// Extract the S3 key - handle both old URL format and new key format
let oldS3Key = oldIconUrlOrKey;
if (oldIconUrlOrKey.includes("amazonaws.com")) {
Comment thread Fixed
const url = new URL(oldIconUrlOrKey);
oldS3Key = url.pathname.substring(1); // Remove leading slash
}

// Only delete if it looks like an organization icon key
if (oldS3Key.startsWith("organizations/")) {
yield* bucket.deleteObject(oldS3Key);
}
} catch (error) {
console.error("Error deleting old organization icon from S3:", error);
// Continue with upload even if deletion fails
}
}
Comment thread
ameer2468 marked this conversation as resolved.

const bodyBytes = yield* Effect.promise(async () => {
const buf = await sanitizedFile.arrayBuffer();
return new Uint8Array(buf);
});

yield* bucket.putObject(fileKey, bodyBytes, { contentType: file.type });
// Construct the icon URL
if (serverEnv().CAP_AWS_BUCKET_URL) {
// If a custom bucket URL is defined, use it
iconUrl = `${serverEnv().CAP_AWS_BUCKET_URL}/${fileKey}`;
} else if (serverEnv().CAP_AWS_ENDPOINT) {
// For custom endpoints like MinIO
iconUrl = `${serverEnv().CAP_AWS_ENDPOINT}/${bucket.bucketName}/${fileKey}`;
} else {
// Default AWS S3 URL format
iconUrl = `https://${bucket.bucketName}.s3.${
serverEnv().CAP_AWS_REGION || "us-east-1"
}.amazonaws.com/${fileKey}`;
}
}).pipe(runPromise);

// Update organization with new icon URL
const iconUrl = fileKey;

await db()
.update(organizations)
.set({ iconUrl })
Expand Down
20 changes: 5 additions & 15 deletions apps/web/actions/organization/upload-space-icon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,10 @@ export async function uploadSpaceIcon(
try {
// Remove previous icon if exists
if (space.iconUrl) {
// Try to extract the previous S3 key from the URL
const key = space.iconUrl.match(/organizations\/.+/)?.[0];
// Extract the S3 key (it might already be a key or could be a legacy URL)
const key = space.iconUrl.startsWith("organizations/")
? space.iconUrl
: space.iconUrl.match(/organizations\/.+/)?.[0];
if (key) {
try {
await bucket.deleteObject(key).pipe(runPromise);
Expand All @@ -87,20 +89,8 @@ export async function uploadSpaceIcon(
)
.pipe(runPromise);

let iconUrl: string | undefined;

// Construct the icon URL
if (serverEnv().CAP_AWS_BUCKET_URL) {
iconUrl = `${serverEnv().CAP_AWS_BUCKET_URL}/${fileKey}`;
} else if (serverEnv().CAP_AWS_ENDPOINT) {
iconUrl = `${serverEnv().CAP_AWS_ENDPOINT}/${bucket.bucketName}/${fileKey}`;
} else {
iconUrl = `https://${bucket.bucketName}.s3.${
serverEnv().CAP_AWS_REGION || "us-east-1"
}.amazonaws.com/${fileKey}`;
}
const iconUrl = fileKey;

// Update space with new icon URL
await db().update(spaces).set({ iconUrl }).where(eq(spaces.id, spaceId));

revalidatePath("/dashboard");
Expand Down
2 changes: 1 addition & 1 deletion apps/web/actions/videos/new-comment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ export async function newComment(data: {
const commentWithAuthor = {
...newComment,
authorName: user.name,
authorImage: user.image ?? null,
authorImageUrlOrKey: user.image ?? null,
sending: false,
};

Expand Down
Loading