Something went wrong. Try again.
Monorepo for Aesthetic.Computer aesthetic.computer
Something went wrong. Try again.
JavaScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304// 🖋️ Presigned Upload URL Generator// This function generates a presigned URL for uploading to S3, and checks// the current bucket to make sure we are not uploading a duplicate file, by// autogenerating a uuid with nanoid.
// 🔏 Security// Please ensure the bucket used to upload media has a CORS policy that// matches the hosting domain.
// It should be... *.aesthetic.computer with// allowed methods GET, PUT and HEAD.
// - [] Show the media file expiration date to the user.// - [] Visiting aesthetic.computer/art~code will show the// file in a viewer.// - [x] Upload the file using this presigned url, via the client.// - Read: https://stackoverflow.com/a/28699269/8146077// - [x] Get presigned S3 URL from S3 bucket
// Next version?// - [] Use a multi-part uploader.
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";import { S3Client, PutObjectCommand, GetObjectCommand,} from "@aws-sdk/client-s3";import { authorize, userIDFromHandleOrEmail,} from "../../backend/authorization.mjs";import { respond } from "../../backend/http.mjs";
// Lazy-initialized S3 clients (created on first use to allow env vars to load)let s3Guest, s3Wand, s3User;
function getS3Clients() { if (!s3Guest) { // Credentials must be provided via environment variables // Support both ART_KEY and DO_SPACES_KEY naming conventions const accessKeyId = process.env.ART_KEY || process.env.DO_SPACES_KEY; const secretAccessKey = process.env.ART_SECRET || process.env.DO_SPACES_SECRET; const endpoint = "https://" + (process.env.ART_ENDPOINT || "sfo3.digitaloceanspaces.com"); // Validate credentials are available if (!accessKeyId || !secretAccessKey) { // In local dev mode, skip S3 client init — handled by local fallback. return null; } console.log("🔑 Using credentials:", { keySource: process.env.ART_KEY ? 'ART_KEY' : 'DO_SPACES_KEY', secretSource: process.env.ART_SECRET ? 'ART_SECRET' : 'DO_SPACES_SECRET', keyLength: accessKeyId?.length, secretLength: secretAccessKey?.length, endpoint, }); s3Guest = new S3Client({ endpoint, region: "us-east-1", credentials: { accessKeyId, secretAccessKey }, });
s3Wand = new S3Client({ endpoint: "https://" + (process.env.WAND_ENDPOINT || "sfo3.digitaloceanspaces.com"), region: "us-east-1", credentials: { accessKeyId, secretAccessKey }, });
s3User = new S3Client({ endpoint: "https://" + (process.env.USER_ENDPOINT || "sfo3.digitaloceanspaces.com"), region: "us-east-1", credentials: { accessKeyId, secretAccessKey }, }); console.log("✅ S3 Clients initialized successfully"); } return { s3Guest, s3Wand, s3User };}
export async function handler(event, context) { try { // Log environment variable availability for debugging console.log("🔍 Environment check:", { hasArtKey: !!process.env.ART_KEY, hasArtSecret: !!process.env.ART_SECRET, hasArtEndpoint: !!process.env.ART_ENDPOINT, hasUserEndpoint: !!process.env.USER_ENDPOINT, hasWandEndpoint: !!process.env.WAND_ENDPOINT, artKeyLength: process.env.ART_KEY?.length, artSecretLength: process.env.ART_SECRET?.length, }); // Only allow GET requests. if (event.httpMethod !== "GET") return respond(405, { error: "Wrong request type!" });
// Initialize S3 clients (lazy load to allow env vars to be set) const s3Clients = getS3Clients();
// Local dev fallback when S3 credentials are missing. if (!s3Clients) { const { customAlphabet } = await import("nanoid"); const nanoid = customAlphabet("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz", 8); const ext = event.path.slice(1).split("/")[1]; const code = nanoid(); const slug = code + "." + ext; console.log("📁 Local dev upload fallback:", slug); return respond(200, { uploadURL: `/local-upload/${slug}`, slug, code, }); }
const { s3Guest, s3Wand, s3User } = s3Clients;
if (event.path === "/presigned-download-url") { const queryParams = new URLSearchParams(event.queryStringParameters); const slug = queryParams.get("for");
if (!slug) { return respond(400, { error: "Missing filename query parameter." }); }
//❤️🔥 // TODO // Either choose s3 or s3User depending on the filename attribute... // https://aesthetic.computer/presigned-download-url
// TODO: Need to parse out the path.
let s3, bucket, filename = decodeURIComponent(slug); // Assume a guest / code lookup because emails or handles have an "@". if (slug.indexOf("@") === -1) { // Guest s3 = s3Guest; bucket = process.env.ART_SPACE_NAME; } else { // User s3 = s3User; bucket = process.env.USER_SPACE_NAME; // Replace `user` email or handle with their auth0 id. const path = filename.split("/"); path[0] = await userIDFromHandleOrEmail(path[0]); filename = path.join("/"); }
const getObjectParams = { Bucket: bucket, Key: filename, ResponseContentDisposition: `attachment; filename="${filename}"`, };
// Get download link that expires in an hour. (3600 seconds) const command = new GetObjectCommand(getObjectParams); const url = await getSignedUrl(s3, command, { expiresIn: 3600 }); return respond(200, { url }); }
// ➡️ Assume the path === "presigned-upload-url" let client; const { customAlphabet } = await import("nanoid"); const alphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; const nanoid = customAlphabet(alphabet, 8);
const extension = event.path.slice(1).split("/")[1]; let name = event.path.slice(1).split("/")[2]; const bucket = event.path.slice(1).split("/")[3];
if (bucket !== "wand") { // ^ Currently using a legacy name schema for wand. 23.05.01.21.59
// 🧠 Replace first prefix- with a prefix/ for folder sorting by media. // ⚠️ Only if name exists! Which it doesn't for the guest bucket. if (name?.indexOf("-") > -1) name = name.replace("-", "/"); }
let expiring = true; let subdirectory; // Sorted by user if necessary.
// TODO: This switch a little janky right now because I need // authentication. 22.11.15.07.16 if (bucket === "wand") { const wandBucket = process.env.WAND_SPACE_NAME || "wand-aesthetic-computer"; client = { s3: s3Wand, bucket: wandBucket }; } else if (bucket === "user") { const userBucket = process.env.USER_SPACE_NAME || "user-aesthetic-computer"; client = { s3: s3User, bucket: userBucket };
const user = await authorize(event.headers); if (user) { expiring = false; // Set the file not to expire. subdirectory = user.sub; // Sort this object into a user directory. } else { // Fail if the user is not logged in but an upload is attempted from // the client as if they are. // TODO: 🔴 Should I just use the guest bucket here? 23.04.30.17.59 return respond(401, { error: "Authorization failure..." }); } } else { // Assume the unauthorized, temporary "art" bucket. const guestBucket = process.env.ART_SPACE_NAME || "art-aesthetic-computer"; client = { s3: s3Guest, bucket: guestBucket }; // tagging = `activity=${activity}`; // Add activity tag. (Assume anonymous.) }
let mimeType; if (extension === "png") mimeType = "image/png"; if (extension === "zip") mimeType = "application/zip"; if (extension === "mjs") mimeType = "application/javascript; charset=utf-8"; if (extension === "lisp") mimeType = "text/x-lisp; charset=utf-8"; if (extension === "lua") mimeType = "text/x-lua; charset=utf-8"; if (extension === "mp4") mimeType = "video/mp4"; if (extension === "json") mimeType = "application/json"; if (extension === "gltf") mimeType = "model/gltf+json"; if (extension === "glb") mimeType = "model/gltf-binary"; if (extension === "obj") mimeType = "application/object";
if (!mimeType) { return respond(400, { message: "Invalid file extension." }); }
let loadCode = nanoid(); let fileName = name || loadCode + "." + extension;
// Check to see if this code has already been uploaded to blockStorage and if it has, // generate a new code. (This should almost never repeat.) See also: https://zelark.github.io/nano-id-cc while ((await fileExists(fileName, bucket)) === true) { if (name) { fileName = `${nanoid()}-${name}`; } else { loadCode = nanoid(); fileName = loadCode + "." + extension; } }
if (subdirectory) fileName = subdirectory + "/" + fileName;
const ContentDisposition = "inline";
const putObjectParams = { Bucket: client.bucket, Key: fileName, ContentType: mimeType, ContentDisposition, ACL: "public-read", // All media should be publicly readable }; // if (tagging) putObjectParams.Tagging = tagging; // Add object tags if they exist.
const command = new PutObjectCommand(putObjectParams); const uploadOptions = { ContentDisposition };
if (expiring) uploadOptions.expiresIn = 3600; // Set to expiring if we are in an anonymous bucket / not a logged in user. const uploadURL = await getSignedUrl(client.s3, command, uploadOptions);
return respond(200, { uploadURL: uploadURL, slug: fileName, code: loadCode, }); } catch (error) { console.error("❌ Presigned URL error:", error); console.error("❌ Error details:", { message: error.message, name: error.name, code: error.code, statusCode: error.statusCode, hasArtKey: !!process.env.ART_KEY, hasArtSecret: !!process.env.ART_SECRET, }); return respond(500, { error: error.message || "Internal server error", details: error.stack }); }}
// 📚 Library (Useful functions used throughout the file)
async function fileExists(filename) { try { const params = { Bucket: client.bucket, Key: filename, };
const headCode = await client.s3.headObject(params).promise(); console.error("File already exists:", filename); return true; } catch (headErr) { if (headErr.code === "NotFound") { // console.log("File doesn't exist yet:", filename); return false; } }}