Files
sexy/packages/backend/src/db/schema/users.ts
Sebastian Krüger 4d81266cb1 feat: add dedicated model photo separate from avatar
Adds a `photo` field to the users table (and a migration) that serves
as a dedicated profile/card image for models. This is now used in model
cards and on the model single page, while `avatar` is reserved for
comments, article authors, and the user profile page.

- DB: `photo` column on `users` with FK to `files`
- GraphQL: exposed on ModelType, UserType, AdminUserDetailType; photoId arg on adminUpdateUser
- Services: photo field in MODELS_QUERY, MODEL_BY_SLUG_QUERY, ADMIN_GET/UPDATE_USER
- Frontend: model cards and single page use `photo ?? avatar` fallback
- Admin: model photo upload section in user edit page

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-08 10:54:27 +01:00

65 lines
2.0 KiB
TypeScript

import {
pgTable,
text,
timestamp,
pgEnum,
boolean,
index,
uniqueIndex,
integer,
} from "drizzle-orm/pg-core";
import { files } from "./files";
export const roleEnum = pgEnum("user_role", ["model", "viewer", "admin"]);
export const users = pgTable(
"users",
{
id: text("id")
.primaryKey()
.$defaultFn(() => crypto.randomUUID()),
email: text("email").notNull(),
password_hash: text("password_hash").notNull(),
first_name: text("first_name"),
last_name: text("last_name"),
artist_name: text("artist_name"),
slug: text("slug"),
description: text("description"),
tags: text("tags").array().default([]),
role: roleEnum("role").notNull().default("viewer"),
avatar: text("avatar").references(() => files.id, { onDelete: "set null" }),
banner: text("banner").references(() => files.id, { onDelete: "set null" }),
photo: text("photo").references(() => files.id, { onDelete: "set null" }),
is_admin: boolean("is_admin").notNull().default(false),
email_verified: boolean("email_verified").notNull().default(false),
email_verify_token: text("email_verify_token"),
password_reset_token: text("password_reset_token"),
password_reset_expiry: timestamp("password_reset_expiry"),
date_created: timestamp("date_created").notNull().defaultNow(),
date_updated: timestamp("date_updated"),
},
(t) => [
uniqueIndex("users_email_idx").on(t.email),
uniqueIndex("users_slug_idx").on(t.slug),
index("users_role_idx").on(t.role),
],
);
export const user_photos = pgTable(
"user_photos",
{
id: integer("id").primaryKey().generatedAlwaysAsIdentity(),
user_id: text("user_id")
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
file_id: text("file_id")
.notNull()
.references(() => files.id, { onDelete: "cascade" }),
sort: integer("sort").default(0),
},
(t) => [index("user_photos_user_idx").on(t.user_id)],
);
export type User = typeof users.$inferSelect;
export type NewUser = typeof users.$inferInsert;