feat(storage): MinIO object storage, Neon Auth, Supabase removal
Deploy to route.crispygoat.com / deploy (push) Failing after 3m1s
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
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
/**
|
||||
* 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;
|
||||
Reference in New Issue
Block a user