Files
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

297 lines
11 KiB
TypeScript

/**
* Support tables: referrals, changelogs, onboarding, api_keys,
* notifications, operational events, audit logs.
* Source: `db/migrations/0001_init.sql`.
*/
import {
pgTable,
uuid,
text,
varchar,
boolean,
timestamp,
jsonb,
integer,
numeric,
time,
index,
uniqueIndex,
} from "drizzle-orm/pg-core";
import { brands, adminUsers } from "./brands";
// ── Referrals ──────────────────────────────────────────────────────────────
export const referralCodes = pgTable(
"referral_codes",
{
id: uuid("id").primaryKey().defaultRandom(),
brandId: uuid("brand_id").references(() => brands.id, {
onDelete: "cascade",
}),
referrerUserId: uuid("referrer_user_id").notNull(),
referralCode: varchar("referral_code", { length: 50 }).notNull().unique(),
referrerEmail: varchar("referrer_email", { length: 255 }).notNull(),
createdAt: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
expiresAt: timestamp("expires_at", { withTimezone: true }),
maxUses: integer("max_uses").default(1),
currentUses: integer("current_uses").default(0),
isActive: boolean("is_active").default(true),
rewardType: varchar("reward_type", { length: 50 }).default("percentage"),
rewardValue: numeric("reward_value", { precision: 10, scale: 2 }).default("20.00"),
metadata: jsonb("metadata").default({}),
},
);
export const referralRedemptions = pgTable(
"referral_redemptions",
{
id: uuid("id").primaryKey().defaultRandom(),
referralCodeId: uuid("referral_code_id").references(
() => referralCodes.id,
{ onDelete: "cascade" },
),
brandId: uuid("brand_id").references(() => brands.id, {
onDelete: "cascade",
}),
referredUserId: uuid("referred_user_id").notNull(),
referredEmail: varchar("referred_email", { length: 255 }).notNull(),
redeemedAt: timestamp("redeemed_at", { withTimezone: true })
.notNull()
.defaultNow(),
rewardType: varchar("reward_type", { length: 50 }),
rewardValue: numeric("reward_value", { precision: 10, scale: 2 }),
isCreditApplied: boolean("is_credit_applied").default(false),
signupPlan: varchar("signup_plan", { length: 50 }),
signupValue: numeric("signup_value", { precision: 10, scale: 2 }),
metadata: jsonb("metadata").default({}),
},
);
// ── Changelogs ─────────────────────────────────────────────────────────────
export const changelogs = pgTable(
"changelogs",
{
id: uuid("id").primaryKey().defaultRandom(),
brandId: uuid("brand_id").references(() => brands.id, {
onDelete: "cascade",
}),
version: varchar("version", { length: 50 }).notNull(),
title: varchar("title", { length: 255 }).notNull(),
description: text("description"),
content: jsonb("content").notNull().default([]),
releasedAt: timestamp("released_at", { withTimezone: true })
.notNull()
.defaultNow(),
isPublished: boolean("is_published").default(false),
featureType: varchar("feature_type", { length: 50 }).default("general"),
metadata: jsonb("metadata").default({}),
},
(t) => ({
brandVersionIdx: uniqueIndex("changelogs_brand_version_idx").on(
t.brandId,
t.version,
),
}),
);
export const changelogReads = pgTable(
"changelog_reads",
{
id: uuid("id").primaryKey().defaultRandom(),
userId: uuid("user_id").notNull(),
changelogId: uuid("changelog_id").references(() => changelogs.id, {
onDelete: "cascade",
}),
readAt: timestamp("read_at", { withTimezone: true })
.notNull()
.defaultNow(),
dismissed: boolean("dismissed").default(false),
},
(t) => ({
userChangelogIdx: uniqueIndex("changelog_reads_user_changelog_idx").on(
t.userId,
t.changelogId,
),
}),
);
// ── Onboarding ─────────────────────────────────────────────────────────────
export const onboardingProgress = pgTable(
"onboarding_progress",
{
id: uuid("id").primaryKey().defaultRandom(),
brandId: uuid("brand_id").references(() => brands.id, {
onDelete: "cascade",
}),
userId: uuid("user_id").notNull(),
currentStep: varchar("current_step", { length: 50 }).notNull(),
completedSteps: jsonb("completed_steps").default([]),
skippedSteps: jsonb("skipped_steps").default([]),
data: jsonb("data").default({}),
startedAt: timestamp("started_at", { withTimezone: true })
.notNull()
.defaultNow(),
completedAt: timestamp("completed_at", { withTimezone: true }),
isCompleted: boolean("is_completed").default(false),
metadata: jsonb("metadata").default({}),
},
(t) => ({
brandUserIdx: uniqueIndex("onboarding_progress_brand_user_idx").on(
t.brandId,
t.userId,
),
}),
);
// ── API Keys ────────────────────────────────────────────────────────────────
export const apiKeys = pgTable(
"api_keys",
{
id: uuid("id").primaryKey().defaultRandom(),
brandId: uuid("brand_id").references(() => brands.id, {
onDelete: "cascade",
}),
name: varchar("name", { length: 255 }).notNull(),
keyHash: varchar("key_hash", { length: 255 }).notNull(),
keyPrefix: varchar("key_prefix", { length: 20 }),
permissions: jsonb("permissions").default(["read"]),
rateLimit: integer("rate_limit").default(1000),
lastUsedAt: timestamp("last_used_at", { withTimezone: true }),
expiresAt: timestamp("expires_at", { withTimezone: true }),
isActive: boolean("is_active").default(true),
createdAt: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
createdBy: uuid("created_by").notNull(),
},
(t) => ({
brandIdx: index("api_keys_brand_idx").on(t.brandId),
}),
);
// ── Notification Preferences ───────────────────────────────────────────────
export const notificationPreferences = pgTable(
"notification_preferences",
{
id: uuid("id").primaryKey().defaultRandom(),
userId: uuid("user_id").notNull(),
brandId: uuid("brand_id").references(() => brands.id, {
onDelete: "cascade",
}),
emailOrders: boolean("email_orders").default(true),
emailMarketing: boolean("email_marketing").default(false),
emailReports: boolean("email_reports").default(true),
emailBilling: boolean("email_billing").default(true),
smsOrders: boolean("sms_orders").default(false),
smsMarketing: boolean("sms_marketing").default(false),
pushOrders: boolean("push_orders").default(true),
pushMarketing: boolean("push_marketing").default(false),
quietHoursStart: time("quiet_hours_start"),
quietHoursEnd: time("quiet_hours_end"),
timezone: varchar("timezone", { length: 50 }).default("America/New_York"),
metadata: jsonb("metadata").default({}),
updatedAt: timestamp("updated_at", { withTimezone: true })
.notNull()
.defaultNow(),
},
(t) => ({
userBrandIdx: uniqueIndex("notification_preferences_user_brand_idx").on(
t.userId,
t.brandId,
),
}),
);
// ── Operational Events ─────────────────────────────────────────────────────
export const operationalEvents = pgTable(
"operational_events",
{
id: uuid("id").primaryKey().defaultRandom(),
brandId: uuid("brand_id")
.notNull()
.references(() => brands.id, { onDelete: "cascade" }),
eventType: text("event_type").notNull(),
entityType: text("entity_type"),
entityId: uuid("entity_id"),
actorType: text("actor_type"),
actorId: uuid("actor_id"),
source: text("source").notNull().default("system"),
payload: jsonb("payload").notNull().default({}),
createdAt: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
},
(t) => ({
brandIdx: index("operational_events_brand_idx").on(t.brandId),
typeIdx: index("operational_events_type_idx").on(t.brandId, t.eventType),
}),
);
// ── Audit Logs ─────────────────────────────────────────────────────────────
export const auditLogs = pgTable(
"audit_logs",
{
id: uuid("id").primaryKey().defaultRandom(),
userId: uuid("user_id"),
brandId: uuid("brand_id").references(() => brands.id, {
onDelete: "set null",
}),
action: text("action").notNull(),
entityType: text("entity_type"),
entityId: uuid("entity_id"),
details: jsonb("details"),
ipAddress: text("ip_address"),
createdAt: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
},
(t) => ({
brandIdx: index("audit_logs_brand_idx").on(t.brandId),
userIdx: index("audit_logs_user_idx").on(t.userId),
createdIdx: index("audit_logs_created_idx").on(t.createdAt),
}),
);
export const adminActionLogs = pgTable(
"admin_action_logs",
{
id: uuid("id").primaryKey().defaultRandom(),
adminUserId: uuid("admin_user_id")
.notNull()
.references(() => adminUsers.id),
brandId: uuid("brand_id").references(() => brands.id, {
onDelete: "set null",
}),
action: text("action").notNull(),
targetType: text("target_type"),
targetId: uuid("target_id"),
metadata: jsonb("metadata"),
ipAddress: text("ip_address"),
createdAt: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
},
(t) => ({
adminIdx: index("admin_action_logs_admin_idx").on(t.adminUserId),
createdIdx: index("admin_action_logs_created_idx").on(t.createdAt),
}),
);
export type ReferralCode = typeof referralCodes.$inferSelect;
export type ReferralRedemption = typeof referralRedemptions.$inferSelect;
export type Changelog = typeof changelogs.$inferSelect;
export type ChangelogRead = typeof changelogReads.$inferSelect;
export type OnboardingProgress = typeof onboardingProgress.$inferSelect;
export type ApiKey = typeof apiKeys.$inferSelect;
export type NotificationPreference = typeof notificationPreferences.$inferSelect;
export type OperationalEvent = typeof operationalEvents.$inferSelect;
export type AuditLog = typeof auditLogs.$inferSelect;
export type AdminActionLog = typeof adminActionLogs.$inferSelect;