c434015829
Deploy to route.crispygoat.com / deploy (push) Failing after 17s
- 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
297 lines
11 KiB
TypeScript
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; |