mirror of
https://github.com/FranP-code/Reflecto.git
synced 2025-10-13 00:43:31 +00:00
feat: add space ownership check and authorization guard for routes
This commit is contained in:
@@ -278,6 +278,27 @@ export async function deleteSpace(args: {
|
||||
await databases.deleteDocument(DATABASE_ID, COLLECTION_ID, existing.$id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a space exists and is owned by the current (or provided) user.
|
||||
*/
|
||||
export async function doesUserOwnSpace(
|
||||
spaceId: string,
|
||||
userId?: string
|
||||
): Promise<boolean> {
|
||||
ensureEnv();
|
||||
const uid = userId ?? (await getCurrentUserId());
|
||||
if (!uid) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const res = (await databases.listDocuments(DATABASE_ID, COLLECTION_ID, [
|
||||
Query.equal("spaceId", spaceId),
|
||||
Query.equal("userId", uid),
|
||||
])) as Models.DocumentList<Models.Document>;
|
||||
|
||||
return (res.documents?.length ?? 0) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload an image file to Appwrite Storage and return its identifiers.
|
||||
* Applies user-level read/update/delete permissions so only the owner can access it by default.
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
type RemoteSnapshot,
|
||||
upsertSpaceSnapshot,
|
||||
} from "@/lib/appwrite-db";
|
||||
import { doesUserOwnSpace } from "@/lib/appwrite-db";
|
||||
import { authClient } from "@/lib/auth-client";
|
||||
import { AIImageShapeUtil, AITextResultShapeUtil } from "@/lib/tldraw/ai-shapes";
|
||||
import { createImageShapeFromFile, setupFileDropHandler, generateText } from "@/lib/tldraw/processing";
|
||||
@@ -44,6 +45,7 @@ function SpaceRoute() {
|
||||
const { id } = Route.useSearch();
|
||||
const { data: session, isPending } = authClient.useSession();
|
||||
const navigate = Route.useNavigate();
|
||||
const [authorized, setAuthorized] = useState<boolean | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!((session || isPending) && id)) {
|
||||
@@ -53,6 +55,32 @@ function SpaceRoute() {
|
||||
}
|
||||
}, [session, isPending, navigate, id]);
|
||||
|
||||
// Check that the space exists and is owned by the current user BEFORE anything else
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
async function check() {
|
||||
if (!(id && session)) return;
|
||||
setAuthorized(null);
|
||||
try {
|
||||
const ok = await doesUserOwnSpace(id, session.$id);
|
||||
if (!cancelled) setAuthorized(!!ok);
|
||||
} catch {
|
||||
if (!cancelled) setAuthorized(false);
|
||||
}
|
||||
}
|
||||
check();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [id, session]);
|
||||
|
||||
// If unauthorized, redirect to dashboard
|
||||
useEffect(() => {
|
||||
if (authorized === false) {
|
||||
navigate({ to: "/dashboard" });
|
||||
}
|
||||
}, [authorized, navigate]);
|
||||
|
||||
// Create a stable store instance once
|
||||
const store = useMemo(
|
||||
() =>
|
||||
@@ -71,11 +99,11 @@ function SpaceRoute() {
|
||||
// Track latest saved version hash to suppress redundant saves
|
||||
const lastSavedRef = useRef<string>("");
|
||||
|
||||
// Load initial snapshot from Appwrite
|
||||
// Load initial snapshot from Appwrite (only after authorization)
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
async function load(_id: string | undefined) {
|
||||
if (!(_id && session)) {
|
||||
if (!(_id && session && authorized)) {
|
||||
return;
|
||||
}
|
||||
const remote = await getLatestSpaceSnapshot(_id, session.$id);
|
||||
@@ -95,15 +123,20 @@ function SpaceRoute() {
|
||||
}
|
||||
}
|
||||
setStoreWithStatus({ status: "not-synced", store });
|
||||
load(id);
|
||||
if (authorized) {
|
||||
load(id);
|
||||
}
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [id, session, store]);
|
||||
}, [id, session, store, authorized]);
|
||||
|
||||
if (!(id && session)) {
|
||||
// Show loader while waiting for session/id or auth check
|
||||
if (!(id && session) || authorized === null) {
|
||||
return <Loader />;
|
||||
}
|
||||
// If explicitly unauthorized, render nothing (we redirect)
|
||||
if (authorized === false) return null;
|
||||
|
||||
// Provide a custom Toolbar that injects our upload buttons into the bottom toolbar
|
||||
const components: TLComponents = {
|
||||
@@ -318,7 +351,7 @@ function SpaceRoute() {
|
||||
let timeout: number | undefined;
|
||||
const unlisten = editor.store.listen(
|
||||
() => {
|
||||
if (!(id && session)) {
|
||||
if (!(id && session && authorized)) {
|
||||
return;
|
||||
}
|
||||
window.clearTimeout(timeout);
|
||||
|
||||
Reference in New Issue
Block a user