All files / src/lib collection-access.ts

100% Statements 16/16
92.85% Branches 13/14
100% Functions 3/3
100% Lines 13/13

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56                                    48x 48x 41x 46x 46x 23x                       44x 44x 20x                       6x 2x 1x   1x    
import { redirect } from "next/navigation";
import type { Database } from "@/lib/db";
import type { Collection } from "@/types";
import { CollectionRepository } from "@/server/repositories/implementations/CollectionRepository";
import { MembershipRepository } from "@/server/repositories/implementations/MembershipRepository";
 
/**
 * Resolve a collection the user may access, accepting EITHER its public slug OR
 * its raw ULID id (so old ULID links keep working). Tries the slug column first,
 * then the id column — disambiguation is at the DB-column level, never string
 * parsing. Returns null if it doesn't exist or the user is neither member nor
 * owner. No redirects: callers decide.
 */
export async function resolveCollection(
  db: Database,
  slugOrId: string,
  userId: string,
): Promise<Collection | null> {
  const repo = new CollectionRepository(db);
  const collection = (await repo.findBySlug(slugOrId)) ?? (await repo.findById(slugOrId));
  if (!collection) return null;
  const membership = await new MembershipRepository(db).findByUser(userId, collection.id);
  if (!membership && collection.ownerId !== userId) return null;
  return collection;
}
 
/** Load a collection the current user may access (member or owner). Redirects to
 *  /collections if it doesn't exist or the user has no access. The single guard
 *  for every collection-scoped page — call it right after the session check.
 *  `slugOrId` is whatever is in the URL (a slug, or a legacy ULID). */
export async function requireCollectionAccess(
  db: Database,
  slugOrId: string,
  userId: string,
): Promise<Collection> {
  const collection = await resolveCollection(db, slugOrId, userId);
  if (!collection) redirect("/collections");
  return collection;
}
 
/** Owner-only page guard. Resolves the collection (member-or-owner first),
 *  then enforces ownership. Non-owners (curators) are redirected to the
 *  collection detail page; absent/no-access collections redirect to /collections.
 *  `slugOrId` accepts slug or legacy ULID. */
export async function requireCollectionOwnerPage(
  db: Database,
  slugOrId: string,
  userId: string,
): Promise<Collection> {
  const collection = await requireCollectionAccess(db, slugOrId, userId);
  if (collection.ownerId !== userId) {
    redirect(`/collections/${collection.slug ?? collection.id}`);
  }
  return collection;
}