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:
+17
-81
@@ -3,9 +3,6 @@
|
||||
import "server-only";
|
||||
import { cookies } from "next/headers";
|
||||
import { pool, query } from "@/lib/db";
|
||||
import { getMockTableData, mockBrands } from "@/lib/mock-data";
|
||||
|
||||
const useMockData = process.env.NEXT_PUBLIC_USE_MOCK_DATA === "true";
|
||||
|
||||
export type AdminUserRow = {
|
||||
id: string;
|
||||
@@ -112,16 +109,30 @@ async function sendWelcomeEmailSafe(input: {
|
||||
name: string;
|
||||
role: "platform_admin" | "brand_admin" | "store_employee";
|
||||
password: string;
|
||||
brandId?: string;
|
||||
}): Promise<void> {
|
||||
try {
|
||||
const { sendWelcomeEmail } = await import("@/lib/email-service");
|
||||
const { getBrandSettings } = await import("@/actions/brand-settings");
|
||||
const emailRole = input.role === "platform_admin" ? "brand_admin" : input.role;
|
||||
|
||||
let logoUrl: string | null = null;
|
||||
let brandName = "Route Commerce";
|
||||
if (input.brandId) {
|
||||
const settings = await getBrandSettings(input.brandId);
|
||||
if (settings.success && settings.settings) {
|
||||
logoUrl = settings.settings.logo_url ?? null;
|
||||
brandName = settings.settings.brand_name ?? brandName;
|
||||
}
|
||||
}
|
||||
|
||||
await sendWelcomeEmail({
|
||||
to: input.to,
|
||||
name: input.name,
|
||||
role: emailRole as "brand_admin" | "wholesale_buyer" | "store_employee",
|
||||
brandName: "Tuxedo Corn",
|
||||
brandName,
|
||||
tempPassword: input.password,
|
||||
logoUrl: logoUrl ?? undefined,
|
||||
});
|
||||
} catch {
|
||||
// welcome email is best-effort; never block user creation
|
||||
@@ -131,14 +142,6 @@ async function sendWelcomeEmailSafe(input: {
|
||||
// ─── Public actions ─────────────────────────────────────────────────────────
|
||||
|
||||
export async function getAdminUsers(brandId?: string): Promise<{ users: AdminUserRow[]; error: string | null }> {
|
||||
if (useMockData) {
|
||||
const mockUsers = getMockTableData("users") as AdminUserRow[];
|
||||
return {
|
||||
users: brandId ? mockUsers.filter((u) => u.brand_id === brandId) : mockUsers,
|
||||
error: null,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const sql = brandId
|
||||
? `SELECT au.id, au.user_id, au.display_name, au.email, au.phone_number,
|
||||
@@ -168,35 +171,6 @@ export async function getAdminUsers(brandId?: string): Promise<{ users: AdminUse
|
||||
}
|
||||
|
||||
export async function createAdminUser(input: CreateAdminUserInput): Promise<{ user: AdminUserRow | null; error: string | null }> {
|
||||
if (useMockData) {
|
||||
const mockUsers = getMockTableData("users") as AdminUserRow[];
|
||||
const newRow: AdminUserRow = {
|
||||
id: `mock-${Date.now()}`,
|
||||
user_id: null,
|
||||
display_name: input.display_name ?? input.email.split("@")[0],
|
||||
email: input.email,
|
||||
phone_number: input.phone_number ?? null,
|
||||
role: input.role,
|
||||
brand_id: input.brand_id,
|
||||
brand_name: null,
|
||||
can_manage_products: input.flags.can_manage_products ?? false,
|
||||
can_manage_stops: input.flags.can_manage_stops ?? false,
|
||||
can_manage_orders: input.flags.can_manage_orders ?? false,
|
||||
can_manage_pickup: input.flags.can_manage_pickup ?? false,
|
||||
can_manage_messages: input.flags.can_manage_messages ?? false,
|
||||
can_manage_refunds: input.flags.can_manage_refunds ?? false,
|
||||
can_manage_users: input.flags.can_manage_users ?? false,
|
||||
can_manage_water_log: input.flags.can_manage_water_log ?? false,
|
||||
can_manage_reports: input.flags.can_manage_reports ?? false,
|
||||
active: true,
|
||||
must_change_password: input.mustChangePassword ?? true,
|
||||
created_at: new Date().toISOString(),
|
||||
last_login: null,
|
||||
};
|
||||
mockUsers.push(newRow);
|
||||
return { user: newRow, error: null };
|
||||
}
|
||||
|
||||
try {
|
||||
// No Supabase Auth — `user_id` stays NULL until the user signs in
|
||||
// via Auth.js and `get_admin_user_for_session` matches them by
|
||||
@@ -242,6 +216,7 @@ export async function createAdminUser(input: CreateAdminUserInput): Promise<{ us
|
||||
name: input.display_name ?? input.email.split("@")[0],
|
||||
role: input.role,
|
||||
password: input.password,
|
||||
brandId: input.brand_id ?? undefined,
|
||||
});
|
||||
|
||||
return { user: mapUserRow(rows[0]), error: null };
|
||||
@@ -251,25 +226,6 @@ export async function createAdminUser(input: CreateAdminUserInput): Promise<{ us
|
||||
}
|
||||
|
||||
export async function updateAdminUser(input: UpdateAdminUserInput): Promise<{ user: AdminUserRow | null; error: string | null }> {
|
||||
if (useMockData) {
|
||||
const mockUsers = getMockTableData("users") as AdminUserRow[];
|
||||
const idx = mockUsers.findIndex((u) => u.id === input.id);
|
||||
if (idx === -1) return { user: null, error: "User not found" };
|
||||
const merged: AdminUserRow = { ...mockUsers[idx] };
|
||||
if (input.role !== undefined) merged.role = input.role;
|
||||
if (input.brand_id !== undefined) merged.brand_id = input.brand_id;
|
||||
if (input.active !== undefined) merged.active = input.active;
|
||||
if (input.display_name !== undefined) merged.display_name = input.display_name;
|
||||
if (input.phone_number !== undefined) merged.phone_number = input.phone_number;
|
||||
if (input.flags) {
|
||||
for (const [k, v] of Object.entries(input.flags)) {
|
||||
if (v !== undefined) (merged as Record<string, unknown>)[k] = v;
|
||||
}
|
||||
}
|
||||
mockUsers[idx] = merged;
|
||||
return { user: merged, error: null };
|
||||
}
|
||||
|
||||
try {
|
||||
// Build a partial SET clause. Each `can_manage_*` column is set
|
||||
// individually — the input's `flags` partial is spread across them.
|
||||
@@ -306,14 +262,6 @@ export async function updateAdminUser(input: UpdateAdminUserInput): Promise<{ us
|
||||
}
|
||||
|
||||
export async function deleteAdminUser(id: string): Promise<{ success: boolean; error: string | null }> {
|
||||
if (useMockData) {
|
||||
const mockUsers = getMockTableData("users") as AdminUserRow[];
|
||||
const idx = mockUsers.findIndex((u) => u.id === id);
|
||||
if (idx === -1) return { success: false, error: "User not found" };
|
||||
mockUsers.splice(idx, 1);
|
||||
return { success: true, error: null };
|
||||
}
|
||||
|
||||
try {
|
||||
// No Supabase Auth — nothing to delete from the auth service.
|
||||
const { rowCount } = await query(`DELETE FROM admin_users WHERE id = $1`, [id]);
|
||||
@@ -324,14 +272,6 @@ export async function deleteAdminUser(id: string): Promise<{ success: boolean; e
|
||||
}
|
||||
|
||||
export async function setMustChangePassword(userId: string): Promise<{ success: boolean; error: string | null }> {
|
||||
if (useMockData) {
|
||||
const mockUsers = getMockTableData("users") as AdminUserRow[];
|
||||
const u = mockUsers.find((m) => m.id === userId);
|
||||
if (!u) return { success: false, error: "User not found" };
|
||||
u.must_change_password = true;
|
||||
return { success: true, error: null };
|
||||
}
|
||||
|
||||
try {
|
||||
const { rowCount } = await query(
|
||||
`UPDATE admin_users SET must_change_password = true WHERE id = $1`,
|
||||
@@ -358,14 +298,10 @@ export async function sendPasswordResetEmail(_email: string): Promise<{ success:
|
||||
}
|
||||
|
||||
export async function getBrands(): Promise<{ brands: { id: string; name: string }[]; error: string | null }> {
|
||||
if (useMockData) {
|
||||
return { brands: mockBrands.map((b) => ({ id: b.id, name: b.name })), error: null };
|
||||
}
|
||||
try {
|
||||
// The SaaS rebuild renamed `brands` → `tenants` (see db/schema/tenants.ts).
|
||||
// Sort by name so the platform admin's brand picker stays stable.
|
||||
const { rows } = await query<{ id: string; name: string }>(
|
||||
`SELECT id, name FROM tenants ORDER BY name`,
|
||||
`SELECT id, name FROM brands ORDER BY name`,
|
||||
);
|
||||
return { brands: rows, error: null };
|
||||
} catch (err) {
|
||||
|
||||
Reference in New Issue
Block a user