feat(storage): MinIO object storage, Neon Auth, Supabase removal
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:
openclaw
2026-06-09 11:32:17 -06:00
parent bb464bda65
commit 916ad39176
349 changed files with 6706 additions and 3343 deletions
+142 -141
View File
@@ -1,153 +1,59 @@
"use client";
import { useState, useTransition } from "react";
import { signInWithGoogle, signInWithCredentials } from "@/actions/auth-actions";
import { useState } from "react";
type LoginClientProps = {
hasGoogle: boolean;
hasCredentials: boolean;
/** Server-rendered error message, if any (from ?error=...) */
error: string | null;
/** Pre-fill the email in dev (for the seeded admin). */
seededEmail?: string;
/** Where to send the user after a successful sign-in. */
redirectTo?: string;
};
function GoogleSignIn({ hasGoogle }: { hasGoogle: boolean }) {
if (!hasGoogle) {
return (
<div className="rounded-xl border border-stone-200/80 bg-stone-50 p-4 text-sm text-stone-700">
<p className="font-medium">Google sign-in is not configured.</p>
<p className="mt-1 text-stone-600">
Add <code className="font-mono text-xs">AUTH_GOOGLE_ID</code> and{" "}
<code className="font-mono text-xs">AUTH_GOOGLE_SECRET</code> to your
environment to enable it.
</p>
</div>
);
}
const REDIRECT_URL = "/admin";
return (
<form action={signInWithGoogle}>
<button
type="submit"
className="w-full flex items-center justify-center gap-3 rounded-xl bg-white border border-stone-200/80 px-6 py-3.5 text-sm font-semibold text-stone-800 hover:bg-stone-50 active:scale-[0.98] transition-all shadow-sm"
style={{ fontFamily: "'Plus Jakarta Sans', system-ui, sans-serif" }}
>
<svg className="w-5 h-5" viewBox="0 0 24 24" aria-hidden="true">
<path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" />
<path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.99.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84A10.99 10.99 0 0012 23z" />
<path fill="#FBBC05" d="M5.84 14.1A6.6 6.6 0 015.5 12c0-.73.13-1.44.34-2.1V7.07H2.18A10.99 10.99 0 001 12c0 1.77.43 3.45 1.18 4.93l3.66-2.83z" />
<path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.83C6.71 7.31 9.14 5.38 12 5.38z" />
</svg>
Continue with Google
</button>
</form>
);
}
// Development mode detection
const isDev = process.env.NODE_ENV !== "production";
function CredentialsForm({
seededEmail,
error,
}: {
seededEmail?: string;
error: string | null;
}) {
const [isPending, startTransition] = useTransition();
export default function LoginClient({ error }: LoginClientProps) {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [loading, setLoading] = useState(false);
const [localError, setLocalError] = useState<string | null>(null);
return (
<form
action={(formData) => {
setLocalError(null);
startTransition(async () => {
try {
await signInWithCredentials(formData);
} catch (err) {
// Auth.js throws NEXT_REDIRECT on success — that's the normal
// flow. We only care about non-redirect errors here.
const msg = err instanceof Error ? err.message : String(err);
if (!msg.includes("NEXT_REDIRECT")) {
setLocalError("Sign-in failed. Please try again.");
}
}
});
}}
className="space-y-3"
>
<label className="block">
<span className="block text-xs font-medium text-stone-700 mb-1.5" style={{ fontFamily: "'Plus Jakarta Sans', system-ui, sans-serif" }}>
Email
</span>
<input
name="email"
type="email"
required
autoComplete="username"
defaultValue={seededEmail ?? ""}
className="w-full rounded-xl border border-stone-200/80 bg-white px-4 py-3 text-sm text-stone-900 placeholder:text-stone-400 focus:outline-none focus:ring-2 focus:ring-[#6b8f71]/40 focus:border-[#6b8f71]"
placeholder="you@example.com"
/>
</label>
<label className="block">
<span className="block text-xs font-medium text-stone-700 mb-1.5" style={{ fontFamily: "'Plus Jakarta Sans', system-ui, sans-serif" }}>
Password
</span>
<input
name="password"
type="password"
required
autoComplete="current-password"
className="w-full rounded-xl border border-stone-200/80 bg-white px-4 py-3 text-sm text-stone-900 placeholder:text-stone-400 focus:outline-none focus:ring-2 focus:ring-[#6b8f71]/40 focus:border-[#6b8f71]"
placeholder="••••••••"
/>
</label>
{(error || localError) && (
<p className="text-sm text-rose-700 bg-rose-50 border border-rose-200 rounded-lg px-3 py-2">
{error ?? localError}
</p>
)}
<button
type="submit"
disabled={isPending}
className="w-full rounded-xl px-6 py-3.5 text-sm font-semibold text-white shadow-sm transition-all disabled:opacity-60 disabled:cursor-not-allowed active:scale-[0.98]"
style={{
fontFamily: "'Plus Jakarta Sans', system-ui, sans-serif",
background: isPending
? "linear-gradient(135deg, #6b8f71 0%, #7ba085 100%)"
: "linear-gradient(135deg, #1a4d2e 0%, #2d6a45 100%)",
boxShadow: "0 8px 24px rgba(26, 77, 46, 0.20)",
}}
>
{isPending ? "Signing in…" : "Sign in with email"}
</button>
</form>
);
}
async function handleSignIn() {
if (!email || !password) {
setLocalError("Email and password are required.");
return;
}
setLocalError(null);
setLoading(true);
function Divider() {
return (
<div className="flex items-center gap-3 my-5" aria-hidden="true">
<div className="flex-1 h-px bg-stone-200/80" />
<span className="text-xs uppercase tracking-wider text-stone-400" style={{ fontFamily: "'Plus Jakarta Sans', system-ui, sans-serif" }}>
or
</span>
<div className="flex-1 h-px bg-stone-200/80" />
</div>
);
}
try {
const res = await fetch("/api/auth/sign-in", {
method: "POST",
credentials: "include",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email: email.trim().toLowerCase(), password: password.trim() }),
});
const data = await res.json();
if (!res.ok || data.error) {
setLocalError(data.message ?? "Sign-in failed. Please check your email and password.");
setLoading(false);
return;
}
// Cookie is set server-side via next/headers — just redirect on success.
window.location.href = REDIRECT_URL;
} catch {
setLocalError("Sign-in failed. Please try again.");
setLoading(false);
}
}
function handleDevLogin(role: string) {
// Set the dev_session cookie for local development bypass
document.cookie = `dev_session=${role}; path=/; max-age=86400`;
window.location.href = REDIRECT_URL;
}
export default function LoginClient({
hasGoogle,
hasCredentials,
error,
seededEmail,
}: LoginClientProps) {
// Render the Google button first (or a setup message), then the divider,
// then the credentials form if dev login is enabled. The order is the
// most-common-first progression: prod users see Google; dev users
// see Google at top, email/password below as the fast path.
return (
<main
className="min-h-screen flex flex-col relative overflow-hidden"
@@ -196,13 +102,108 @@ export default function LoginClient({
</p>
</div>
<GoogleSignIn hasGoogle={hasGoogle} />
{hasCredentials && hasGoogle && <Divider />}
{hasCredentials && <CredentialsForm seededEmail={seededEmail} error={error} />}
<div className="space-y-3">
<div>
<label className="block text-xs font-medium text-stone-700 mb-1.5" style={{ fontFamily: "'Plus Jakarta Sans', system-ui, sans-serif" }}>
Email
</label>
<input
type="email"
required
autoComplete="username"
className="w-full rounded-xl border border-stone-200/80 bg-white px-4 py-3 text-sm text-stone-900 placeholder:text-stone-400 focus:outline-none focus:ring-2 focus:ring-[#6b8f71]/40 focus:border-[#6b8f71]"
placeholder="you@example.com"
value={email}
onChange={(e) => setEmail(e.target.value)}
disabled={loading}
onKeyDown={(e) => { if (e.key === "Enter") handleSignIn(); }}
/>
</div>
<div>
<label className="block text-xs font-medium text-stone-700 mb-1.5" style={{ fontFamily: "'Plus Jakarta Sans', system-ui, sans-serif" }}>
Password
</label>
<input
type="password"
required
autoComplete="current-password"
className="w-full rounded-xl border border-stone-200/80 bg-white px-4 py-3 text-sm text-stone-900 placeholder:text-stone-400 focus:outline-none focus:ring-2 focus:ring-[#6b8f71]/40 focus:border-[#6b8f71]"
placeholder="••••••••"
value={password}
onChange={(e) => setPassword(e.target.value)}
disabled={loading}
onKeyDown={(e) => { if (e.key === "Enter") handleSignIn(); }}
/>
</div>
{(error || localError) && (
<p className="text-sm text-rose-700 bg-rose-50 border border-rose-200 rounded-lg px-3 py-2">
{error ?? localError}
</p>
)}
<button
type="button"
onClick={handleSignIn}
disabled={loading}
className="w-full rounded-xl px-6 py-3.5 text-sm font-semibold text-white shadow-sm transition-all disabled:opacity-60 disabled:cursor-not-allowed active:scale-[0.98]"
style={{
fontFamily: "'Plus Jakarta Sans', system-ui, sans-serif",
background: loading
? "linear-gradient(135deg, #6b8f71 0%, #7ba085 100%)"
: "linear-gradient(135deg, #1a4d2e 0%, #2d6a45 100%)",
boxShadow: "0 8px 24px rgba(26, 77, 46, 0.20)",
}}
>
{loading ? "Signing in…" : "Sign in with email"}
</button>
</div>
{/* Development bypass buttons */}
{isDev && (
<div className="mt-6 pt-6 border-t border-stone-200">
<p className="text-xs text-stone-500 text-center mb-3" style={{ fontFamily: "'Plus Jakarta Sans', system-ui, sans-serif" }}>
Dev Mode Quick Access
</p>
<div className="grid grid-cols-3 gap-2">
<button
type="button"
onClick={() => handleDevLogin("platform_admin")}
className="rounded-lg px-3 py-2 text-xs font-medium text-white transition-all hover:opacity-90"
style={{
fontFamily: "'Plus Jakarta Sans', system-ui, sans-serif",
background: "#1a4d2e",
}}
>
Platform Admin
</button>
<button
type="button"
onClick={() => handleDevLogin("brand_admin")}
className="rounded-lg px-3 py-2 text-xs font-medium text-white transition-all hover:opacity-90"
style={{
fontFamily: "'Plus Jakarta Sans', system-ui, sans-serif",
background: "#2d6a45",
}}
>
Brand Admin
</button>
<button
type="button"
onClick={() => handleDevLogin("store_employee")}
className="rounded-lg px-3 py-2 text-xs font-medium text-white transition-all hover:opacity-90"
style={{
fontFamily: "'Plus Jakarta Sans', system-ui, sans-serif",
background: "#6b8f71",
}}
>
Store Employee
</button>
</div>
</div>
)}
</div>
</div>
</div>
</div>
</main>
);
}
}