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,297 @@
|
||||
/**
|
||||
* 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;
|
||||
Reference in New Issue
Block a user