import { pgTable, uuid, text, boolean, timestamp, integer, } from "drizzle-orm/pg-core"; export const users = pgTable("users", { id: uuid("id").primaryKey().defaultRandom(), displayName: text("display_name").notNull(), email: text("email").notNull().unique(), isAdmin: boolean("is_admin").notNull().default(false), watchingNowTvSlots: integer("watching_now_tv_slots").notNull().default(5), watchingNowMovieSlots: integer("watching_now_movie_slots").notNull().default(10), createdAt: timestamp("created_at").notNull().defaultNow(), updatedAt: timestamp("updated_at").notNull().defaultNow(), }); // A linked Plex account. // // Linking does two things at once, which is why it is worth a table of its own // rather than another row in user_identities: it grants the person access to // the Plex libraries, and it makes their Plex watchlist readable as demand. // Neither is possible without knowing which Plex account belongs to which user. // // NO TOKEN IS STORED. The link is proved by the PIN flow -- the user signs in // at plex.tv, we exchange the PIN for a token, ask Plex who it belongs to, and // then throw the token away. Reading their watchlist needs the OWNER's token // and the account uuid, both of which we already have, so keeping a second // person's credential would buy nothing and be one more thing to leak. export const plexAccounts = pgTable("plex_accounts", { id: uuid("id").primaryKey().defaultRandom(), userId: uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" }).unique(), // Plex's numeric account id, and the uuid the community API keys watchlists on. plexUserId: text("plex_user_id").notNull().unique(), plexUuid: text("plex_uuid"), plexUsername: text("plex_username").notNull(), plexEmail: text("plex_email"), // Does this account own the Plex server? // // The owner cannot be invited to their own libraries -- Plex refuses with // "You cannot send an invitation to yourself." Without this column that // refusal is indistinguishable from a share that genuinely failed, and the // owner is told forever that their libraries did not share and to try again. // They already have every library; there is nothing to grant. isServerOwner: boolean("is_server_owner").notNull().default(false), // When the libraries were shared, and which ones. Recorded so a failed or // partial share is visible rather than being assumed to have worked. // Both stay null for the owner, who was never invited to anything. librariesSharedAt: timestamp("libraries_shared_at"), sharedSectionIds: text("shared_section_ids").array(), linkedAt: timestamp("linked_at").notNull().defaultNow(), updatedAt: timestamp("updated_at").notNull().defaultNow(), }); // Maps Authentik (or other OIDC) external identities to local users. export const userIdentities = pgTable("user_identities", { id: uuid("id").primaryKey().defaultRandom(), userId: uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" }), provider: text("provider").notNull(), // e.g. "authentik" externalId: text("external_id").notNull(), // sub claim from OIDC createdAt: timestamp("created_at").notNull().defaultNow(), });