Skip to content

Commit d8feb5e

Browse files
committed
cleanup image actions
1 parent 33840d6 commit d8feb5e

File tree

9 files changed

+174
-396
lines changed

9 files changed

+174
-396
lines changed

apps/web/actions/account/remove-profile-image.ts

Lines changed: 0 additions & 66 deletions
This file was deleted.

apps/web/actions/account/upload-profile-image.ts

Lines changed: 0 additions & 119 deletions
This file was deleted.
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
"use server";
2+
3+
import { db } from "@cap/database";
4+
import { organizations, users } from "@cap/database/schema";
5+
import { S3Buckets } from "@cap/web-backend";
6+
import { OrganisationId, UserId } from "@cap/web-domain";
7+
import { eq } from "drizzle-orm";
8+
import { Effect, Option } from "effect";
9+
import { revalidatePath } from "next/cache";
10+
import * as path from "path";
11+
import { runPromise } from "@/lib/server";
12+
13+
export async function removeImage(
14+
imageUrlOrKey: string,
15+
type: "user" | "organization",
16+
entityId: string,
17+
) {
18+
try {
19+
await Effect.gen(function* () {
20+
const [bucket] = yield* S3Buckets.getBucketAccess(Option.none());
21+
22+
// Extract the S3 key - handle both old URL format and new key format
23+
let s3Key = imageUrlOrKey;
24+
if (
25+
imageUrlOrKey.startsWith("http://") ||
26+
imageUrlOrKey.startsWith("https://")
27+
) {
28+
const url = new URL(imageUrlOrKey);
29+
const raw = url.pathname.startsWith("/")
30+
? url.pathname.slice(1)
31+
: url.pathname;
32+
const decoded = decodeURIComponent(raw);
33+
const normalized = path.posix.normalize(decoded);
34+
if (normalized.includes("..")) {
35+
throw new Error("Invalid S3 key path");
36+
}
37+
s3Key = normalized;
38+
}
39+
40+
// Only delete if it looks like the correct type of image key
41+
const expectedPrefix = type === "user" ? "users/" : "organizations/";
42+
if (s3Key.startsWith(expectedPrefix)) {
43+
yield* bucket.deleteObject(s3Key);
44+
}
45+
}).pipe(runPromise);
46+
47+
// Update database
48+
if (type === "user") {
49+
await db()
50+
.update(users)
51+
.set({ image: null })
52+
.where(eq(users.id, UserId.make(entityId)));
53+
} else {
54+
await db()
55+
.update(organizations)
56+
.set({ iconUrl: null })
57+
.where(eq(organizations.id, OrganisationId.make(entityId)));
58+
}
59+
60+
revalidatePath("/dashboard/settings/account");
61+
if (type === "organization") {
62+
revalidatePath("/dashboard/settings/organization");
63+
}
64+
65+
return { success: true } as const;
66+
} catch (error) {
67+
console.error(`Error removing ${type} image:`, error);
68+
throw new Error(error instanceof Error ? error.message : "Remove failed");
69+
}
70+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
"use server";
2+
3+
import { db } from "@cap/database";
4+
import { organizations, users } from "@cap/database/schema";
5+
import { S3Buckets } from "@cap/web-backend";
6+
import { OrganisationId, UserId } from "@cap/web-domain";
7+
import { eq } from "drizzle-orm";
8+
import { Effect, Option } from "effect";
9+
import { revalidatePath } from "next/cache";
10+
import { runPromise } from "@/lib/server";
11+
12+
export async function uploadImage(
13+
file: File,
14+
type: "user" | "organization",
15+
entityId: string,
16+
oldImageUrlOrKey?: string | null,
17+
) {
18+
try {
19+
const s3Key = await Effect.gen(function* () {
20+
const [bucket] = yield* S3Buckets.getBucketAccess(Option.none());
21+
22+
// Delete old image if it exists
23+
if (oldImageUrlOrKey) {
24+
try {
25+
// Extract the S3 key - handle both old URL format and new key format
26+
let oldS3Key = oldImageUrlOrKey;
27+
if (
28+
oldImageUrlOrKey.startsWith("http://") ||
29+
oldImageUrlOrKey.startsWith("https://")
30+
) {
31+
const url = new URL(oldImageUrlOrKey);
32+
oldS3Key = url.pathname.substring(1);
33+
}
34+
35+
// Only delete if it looks like the correct type of image key
36+
const expectedPrefix = type === "user" ? "users/" : "organizations/";
37+
if (oldS3Key.startsWith(expectedPrefix)) {
38+
yield* bucket.deleteObject(oldS3Key);
39+
}
40+
} catch (error) {
41+
console.error(`Error deleting old ${type} image from S3:`, error);
42+
}
43+
}
44+
45+
// Generate new S3 key
46+
const timestamp = Date.now();
47+
const fileExtension = file.name.split(".").pop() || "jpg";
48+
const s3Key = `${type}s/${entityId}/${timestamp}.${fileExtension}`;
49+
50+
// Upload new image
51+
const arrayBuffer = yield* Effect.promise(() => file.arrayBuffer());
52+
const buffer = Buffer.from(arrayBuffer);
53+
yield* bucket.putObject(s3Key, buffer, {
54+
contentType: file.type,
55+
});
56+
57+
return s3Key;
58+
}).pipe(runPromise);
59+
60+
// Update database
61+
if (type === "user") {
62+
await db()
63+
.update(users)
64+
.set({ image: s3Key })
65+
.where(eq(users.id, UserId.make(entityId)));
66+
} else {
67+
await db()
68+
.update(organizations)
69+
.set({ iconUrl: s3Key })
70+
.where(eq(organizations.id, OrganisationId.make(entityId)));
71+
}
72+
73+
revalidatePath("/dashboard/settings/account");
74+
if (type === "organization") {
75+
revalidatePath("/dashboard/settings/organization");
76+
}
77+
78+
return { success: true, image: s3Key } as const;
79+
} catch (error) {
80+
console.error(`Error uploading ${type} image:`, error);
81+
throw new Error(error instanceof Error ? error.message : "Upload failed");
82+
}
83+
}

0 commit comments

Comments
 (0)