mirror of
https://github.com/willchen96/mike.git
synced 2026-06-30 21:59:37 +02:00
Add local repo contents
This commit is contained in:
parent
65739ef1ce
commit
d9690965b5
176 changed files with 68998 additions and 0 deletions
146
backend/src/lib/access.ts
Normal file
146
backend/src/lib/access.ts
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
/**
|
||||
* Project / document access helpers.
|
||||
*
|
||||
* Sharing makes the previous "scope by user_id" pattern incorrect — a doc
|
||||
* can belong to user A's project that A has shared with B's email, and B
|
||||
* must still be able to read/edit it. These helpers centralize the
|
||||
* "owner OR shared project member" check so every route uses the same
|
||||
* logic instead of re-implementing the join.
|
||||
*
|
||||
* Returned `isOwner` lets callers gate operations that should stay
|
||||
* owner-only (delete, rename, member management).
|
||||
*/
|
||||
|
||||
import type { createServerSupabase } from "./supabase";
|
||||
|
||||
type Db = ReturnType<typeof createServerSupabase>;
|
||||
|
||||
export type ProjectAccess =
|
||||
| {
|
||||
ok: true;
|
||||
isOwner: boolean;
|
||||
project: {
|
||||
id: string;
|
||||
user_id: string;
|
||||
shared_with: string[] | null;
|
||||
};
|
||||
}
|
||||
| { ok: false };
|
||||
|
||||
export async function checkProjectAccess(
|
||||
projectId: string,
|
||||
userId: string,
|
||||
userEmail: string | null | undefined,
|
||||
db: Db,
|
||||
): Promise<ProjectAccess> {
|
||||
const { data: project } = await db
|
||||
.from("projects")
|
||||
.select("id, user_id, shared_with")
|
||||
.eq("id", projectId)
|
||||
.single();
|
||||
if (!project) return { ok: false };
|
||||
const proj = project as {
|
||||
id: string;
|
||||
user_id: string;
|
||||
shared_with: string[] | null;
|
||||
};
|
||||
if (proj.user_id === userId) {
|
||||
return { ok: true, isOwner: true, project: proj };
|
||||
}
|
||||
const sharedWith = Array.isArray(proj.shared_with) ? proj.shared_with : [];
|
||||
const email = (userEmail ?? "").toLowerCase();
|
||||
if (
|
||||
email &&
|
||||
sharedWith.some((e) => (e ?? "").toLowerCase() === email)
|
||||
) {
|
||||
return { ok: true, isOwner: false, project: proj };
|
||||
}
|
||||
return { ok: false };
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the current user can access a document the caller has
|
||||
* already loaded (saves a round-trip vs. having the helper re-fetch).
|
||||
* Owner-of-doc passes immediately; otherwise we fall through to a
|
||||
* project-membership check via `shared_with`.
|
||||
*/
|
||||
export async function ensureDocAccess(
|
||||
doc: { user_id: string; project_id: string | null },
|
||||
userId: string,
|
||||
userEmail: string | null | undefined,
|
||||
db: Db,
|
||||
): Promise<{ ok: true; isOwner: boolean } | { ok: false }> {
|
||||
if (doc.user_id === userId) return { ok: true, isOwner: true };
|
||||
if (!doc.project_id) return { ok: false };
|
||||
const access = await checkProjectAccess(
|
||||
doc.project_id,
|
||||
userId,
|
||||
userEmail,
|
||||
db,
|
||||
);
|
||||
if (access.ok) return { ok: true, isOwner: false };
|
||||
return { ok: false };
|
||||
}
|
||||
|
||||
/**
|
||||
* Same shape as `ensureDocAccess`, for tabular_reviews. A review can be
|
||||
* shared in two ways:
|
||||
* 1. Indirectly — if `project_id` is set, everyone with project access
|
||||
* can read/operate on it.
|
||||
* 2. Directly — `tabular_reviews.shared_with` is a per-review email list
|
||||
* so standalone reviews (project_id null) can also be shared.
|
||||
* The owner (review.user_id) always has access.
|
||||
*/
|
||||
export async function ensureReviewAccess(
|
||||
review: {
|
||||
user_id: string;
|
||||
project_id: string | null;
|
||||
shared_with?: string[] | null;
|
||||
},
|
||||
userId: string,
|
||||
userEmail: string | null | undefined,
|
||||
db: Db,
|
||||
): Promise<{ ok: true; isOwner: boolean } | { ok: false }> {
|
||||
if (review.user_id === userId) return { ok: true, isOwner: true };
|
||||
const email = (userEmail ?? "").toLowerCase();
|
||||
if (email && Array.isArray(review.shared_with)) {
|
||||
if (review.shared_with.some((e) => (e ?? "").toLowerCase() === email)) {
|
||||
return { ok: true, isOwner: false };
|
||||
}
|
||||
}
|
||||
if (!review.project_id) return { ok: false };
|
||||
const access = await checkProjectAccess(
|
||||
review.project_id,
|
||||
userId,
|
||||
userEmail,
|
||||
db,
|
||||
);
|
||||
if (access.ok) return { ok: true, isOwner: false };
|
||||
return { ok: false };
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the set of project IDs the user can access — own projects plus
|
||||
* any project where their email is in `shared_with`. Used to scope chat
|
||||
* lists and similar collection queries.
|
||||
*/
|
||||
export async function listAccessibleProjectIds(
|
||||
userId: string,
|
||||
userEmail: string | null | undefined,
|
||||
db: Db,
|
||||
): Promise<string[]> {
|
||||
const [{ data: own }, { data: shared }] = await Promise.all([
|
||||
db.from("projects").select("id").eq("user_id", userId),
|
||||
userEmail
|
||||
? db
|
||||
.from("projects")
|
||||
.select("id")
|
||||
.contains("shared_with", [userEmail])
|
||||
.neq("user_id", userId)
|
||||
: Promise.resolve({ data: [] as { id: string }[] }),
|
||||
]);
|
||||
const ids = new Set<string>();
|
||||
for (const p of (own ?? []) as { id: string }[]) ids.add(p.id);
|
||||
for (const p of (shared ?? []) as { id: string }[]) ids.add(p.id);
|
||||
return [...ids];
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue