916ad39176
Deploy to route.crispygoat.com / deploy (push) Failing after 3m1s
- 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
141 lines
5.2 KiB
TypeScript
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;
|