Files
route-commerce/src/lib/storage.ts
T
openclaw 916ad39176
Deploy to route.crispygoat.com / deploy (push) Failing after 3m1s
feat(storage): MinIO object storage, Neon Auth, Supabase removal
- Add MinIO/S3-compatible storage client (src/lib/storage.ts) with uploadObject,
  deleteObject, presigned URL helpers, and BUCKETS constant
- Wire product images, brand logos, and water log photos to MinIO via the
  new storage client
- Migrate forgot-password to Neon Auth (remove Supabase /auth/v1/recover call)
- Migrate send-scheduled cron to direct Postgres + Resend (remove Supabase Edge
  Function proxy)
- Add logoUrl to email types (OrderReceipt, Welcome, PasswordReset) and pass
  brand_settings.logo_url from all call sites
- Update email templates to use dynamic logoUrl instead of hardcoded Supabase
  bucket URLs
- Remove hardcoded Supabase URLs from TuxedoVideoHero, TuxedoAboutPage,
  TimeTrackingFieldClient; use brand_settings props + local public/ fallback
- Download brand logos (3) and tuxedo-hero.mp4 (36MB) from Supabase bucket to
  public/ for local development
- Add MinIO env vars to .env.example (endpoint, access key, secret, buckets)
- Fix TimeTrackingFieldClient to destructure logoUrl and brandAccent props
- Fix admin/users.ts logoUrl type (null → undefined for optional string)
- Remove stale sb- cookie from wholesale-auth
- Migrate tuxedo/about page to remove supabase import and use pool query for
  wholesale_settings lookup
2026-06-09 12:23:37 -06:00

141 lines
5.2 KiB
TypeScript

/**
* MinIO / S3-compatible object storage client.
*
* Env vars (set in .env.local):
* MINIO_ENDPOINT — host:port, e.g. s3.crispygoat.com (no https://)
* MINIO_REGION — e.g. us-east-1 (default)
* MINIO_ACCESS_KEY — MinIO access key
* MINIO_SECRET_KEY — MinIO secret key
* MINIO_USE_SSL — "true" to use HTTPS (default: true if endpoint ends in .com/.io/etc.)
* MINIO_BUCKET_PRODUCTS — bucket for product images
* MINIO_BUCKET_BRAND_LOGOS — bucket for brand logos
* MINIO_BUCKET_WATER_LOGS — bucket for water log photos
*
* Public URL base — used to construct public-facing URLs after upload:
* MINIO_PUBLIC_URL — e.g. https://s3.crispygoat.com (default derived from endpoint)
*/
import { S3Client, PutObjectCommand, DeleteObjectCommand, GetObjectCommand } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
// ── Client singleton ─────────────────────────────────────────────────────────
let _client: S3Client | null = null;
export function getStorageClient(): S3Client {
if (_client) return _client;
const endpoint = process.env.MINIO_ENDPOINT ?? "s3.crispygoat.com";
const ssl = process.env.MINIO_USE_SSL !== "false";
const region = process.env.MINIO_REGION ?? "us-east-1";
_client = new S3Client({
endpoint: `${ssl ? "https" : "http"}://${endpoint}`,
region,
credentials: {
accessKeyId: process.env.MINIO_ACCESS_KEY ?? "",
secretAccessKey: process.env.MINIO_SECRET_KEY ?? "",
},
forcePathStyle: true, // Required for MinIO — bucket is not part of the host
});
return _client;
}
// ── Public URL helper ─────────────────────────────────────────────────────────
/**
* Returns the public base URL for the given bucket.
* Override with MINIO_PUBLIC_URL if your public endpoint differs from the endpoint.
*/
export function getPublicUrl(bucket: string, key: string): string {
const base = process.env.MINIO_PUBLIC_URL ?? `https://${process.env.MINIO_ENDPOINT ?? "s3.crispygoat.com"}`;
return `${base}/${bucket}/${key}`;
}
// ── Upload helpers ───────────────────────────────────────────────────────────
export type UploadOptions = {
bucket: string;
key: string;
body: Buffer | Uint8Array;
contentType: string;
/**
* Cache-Control value. Defaults to "public, max-age=31536000, immutable"
* for immutable uploaded assets.
*/
cacheControl?: string;
};
export async function uploadObject(opts: UploadOptions): Promise<string> {
const client = getStorageClient();
await client.send(
new PutObjectCommand({
Bucket: opts.bucket,
Key: opts.key,
Body: opts.body,
ContentType: opts.contentType,
CacheControl: opts.cacheControl ?? "public, max-age=31536000, immutable",
})
);
return getPublicUrl(opts.bucket, opts.key);
}
export async function deleteObject(bucket: string, key: string): Promise<void> {
const client = getStorageClient();
await client.send(new DeleteObjectCommand({ Bucket: bucket, Key: key }));
}
// ── Presigned URL helpers ─────────────────────────────────────────────────────
/**
* Generate a short-lived presigned PUT URL for direct browser → MinIO upload.
* Useful when you want the browser to upload bytes directly without routing
* them through the Next.js server.
*
* @param bucket Target bucket name
* @param key Desired object key
* @param contentType Expected MIME type
* @param expiresSeconds URL expiry (default 5 minutes)
*/
export async function getPresignedPutUrl(
bucket: string,
key: string,
contentType: string,
expiresSeconds = 300
): Promise<string> {
const client = getStorageClient();
return getSignedUrl(
client,
new PutObjectCommand({ Bucket: bucket, Key: key, ContentType: contentType }),
{ expiresIn: expiresSeconds }
);
}
/**
* Generate a short-lived presigned GET URL for private objects.
* @param bucket Bucket name
* @param key Object key
* @param expiresSeconds URL expiry (default 1 hour)
*/
export async function getPresignedGetUrl(
bucket: string,
key: string,
expiresSeconds = 3600
): Promise<string> {
const client = getStorageClient();
return getSignedUrl(
client,
new GetObjectCommand({ Bucket: bucket, Key: key }),
{ expiresIn: expiresSeconds }
);
}
// ── Bucket constants ─────────────────────────────────────────────────────────
export const BUCKETS = {
PRODUCTS: process.env.MINIO_BUCKET_PRODUCTS ?? "route-products",
BRAND_LOGOS: process.env.MINIO_BUCKET_BRAND_LOGOS ?? "route-brand-logos",
WATER_LOGS: process.env.MINIO_BUCKET_WATER_LOGS ?? "route-water-logs",
} as const;