57c29f62b4
- user_blocks table (composite PK), Block/Unblock button on profiles. Blocking severs any existing follow relationship both ways and prevents the blocked party from following, commenting on the blocker's recipes, or the blocker from seeing their comments. - reports table (recipe/comment/user targets, pending/reviewed/ dismissed status). ReportButton on comments, admin review queue at /admin/reports with dismiss/mark-reviewed actions, audit-logged. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
99 lines
3.8 KiB
TypeScript
99 lines
3.8 KiB
TypeScript
import {
|
|
pgTable,
|
|
text,
|
|
timestamp,
|
|
integer,
|
|
boolean,
|
|
index,
|
|
unique,
|
|
pgEnum,
|
|
} from "drizzle-orm/pg-core";
|
|
import { relations } from "drizzle-orm";
|
|
import { users, tierEnum, userRoleEnum } from "./users";
|
|
|
|
export const tierDefinitions = pgTable("tier_definitions", {
|
|
tier: tierEnum("tier").primaryKey(),
|
|
maxRecipes: integer("max_recipes").notNull(),
|
|
aiCallsPerMonth: integer("ai_calls_per_month").notNull(),
|
|
storageMb: integer("storage_mb").notNull(),
|
|
maxPublicRecipes: integer("max_public_recipes").notNull(),
|
|
});
|
|
|
|
export const userUsage = pgTable("user_usage", {
|
|
id: text("id").primaryKey(),
|
|
userId: text("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
|
|
month: text("month").notNull(),
|
|
aiCallsUsed: integer("ai_calls_used").notNull().default(0),
|
|
recipeCount: integer("recipe_count").notNull().default(0),
|
|
storageUsedMb: integer("storage_used_mb").notNull().default(0),
|
|
}, (t) => [
|
|
index("user_usage_user_month_idx").on(t.userId, t.month),
|
|
unique("user_usage_user_month_uniq").on(t.userId, t.month),
|
|
]);
|
|
|
|
export const auditLogs = pgTable("audit_logs", {
|
|
id: text("id").primaryKey(),
|
|
userId: text("user_id").references(() => users.id, { onDelete: "set null" }),
|
|
action: text("action").notNull(),
|
|
targetType: text("target_type"),
|
|
targetId: text("target_id"),
|
|
metadata: text("metadata"),
|
|
createdAt: timestamp("created_at").notNull().defaultNow(),
|
|
}, (t) => [
|
|
index("audit_logs_created_idx").on(t.createdAt),
|
|
]);
|
|
|
|
export const siteSettings = pgTable("site_settings", {
|
|
key: text("key").primaryKey(),
|
|
value: text("value"),
|
|
isSecret: boolean("is_secret").notNull().default(false),
|
|
updatedAt: timestamp("updated_at").notNull().defaultNow(),
|
|
updatedById: text("updated_by_id").references(() => users.id, { onDelete: "set null" }),
|
|
});
|
|
|
|
export const invites = pgTable("invites", {
|
|
id: text("id").primaryKey(),
|
|
token: text("token").notNull(),
|
|
email: text("email"),
|
|
role: userRoleEnum("role").notNull().default("user"),
|
|
tier: tierEnum("tier").notNull().default("free"),
|
|
createdById: text("created_by_id").notNull().references(() => users.id, { onDelete: "cascade" }),
|
|
createdAt: timestamp("created_at").notNull().defaultNow(),
|
|
expiresAt: timestamp("expires_at"),
|
|
usedAt: timestamp("used_at"),
|
|
usedById: text("used_by_id").references(() => users.id, { onDelete: "set null" }),
|
|
}, (t) => [
|
|
unique("invites_token_uniq").on(t.token),
|
|
]);
|
|
|
|
export const userUsageRelations = relations(userUsage, ({ one }) => ({
|
|
user: one(users, { fields: [userUsage.userId], references: [users.id] }),
|
|
}));
|
|
|
|
export const invitesRelations = relations(invites, ({ one }) => ({
|
|
createdBy: one(users, { fields: [invites.createdById], references: [users.id] }),
|
|
usedBy: one(users, { fields: [invites.usedById], references: [users.id] }),
|
|
}));
|
|
|
|
export const reportTargetTypeEnum = pgEnum("report_target_type", ["recipe", "comment", "user"]);
|
|
export const reportStatusEnum = pgEnum("report_status", ["pending", "reviewed", "dismissed"]);
|
|
|
|
export const reports = pgTable("reports", {
|
|
id: text("id").primaryKey(),
|
|
reporterId: text("reporter_id").notNull().references(() => users.id, { onDelete: "cascade" }),
|
|
targetType: reportTargetTypeEnum("target_type").notNull(),
|
|
targetId: text("target_id").notNull(),
|
|
reason: text("reason").notNull(),
|
|
status: reportStatusEnum("status").notNull().default("pending"),
|
|
createdAt: timestamp("created_at").notNull().defaultNow(),
|
|
reviewedById: text("reviewed_by_id").references(() => users.id, { onDelete: "set null" }),
|
|
reviewedAt: timestamp("reviewed_at"),
|
|
}, (t) => [
|
|
index("reports_status_idx").on(t.status, t.createdAt),
|
|
]);
|
|
|
|
export const reportsRelations = relations(reports, ({ one }) => ({
|
|
reporter: one(users, { fields: [reports.reporterId], references: [users.id] }),
|
|
reviewedBy: one(users, { fields: [reports.reviewedById], references: [users.id] }),
|
|
}));
|