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);
|
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.
|
* 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.
|
* Applies user-level read/update/delete permissions so only the owner can access it by default.
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
type RemoteSnapshot,
|
type RemoteSnapshot,
|
||||||
upsertSpaceSnapshot,
|
upsertSpaceSnapshot,
|
||||||
} from "@/lib/appwrite-db";
|
} from "@/lib/appwrite-db";
|
||||||
|
import { doesUserOwnSpace } from "@/lib/appwrite-db";
|
||||||
import { authClient } from "@/lib/auth-client";
|
import { authClient } from "@/lib/auth-client";
|
||||||
import { AIImageShapeUtil, AITextResultShapeUtil } from "@/lib/tldraw/ai-shapes";
|
import { AIImageShapeUtil, AITextResultShapeUtil } from "@/lib/tldraw/ai-shapes";
|
||||||
import { createImageShapeFromFile, setupFileDropHandler, generateText } from "@/lib/tldraw/processing";
|
import { createImageShapeFromFile, setupFileDropHandler, generateText } from "@/lib/tldraw/processing";
|
||||||
@@ -44,6 +45,7 @@ function SpaceRoute() {
|
|||||||
const { id } = Route.useSearch();
|
const { id } = Route.useSearch();
|
||||||
const { data: session, isPending } = authClient.useSession();
|
const { data: session, isPending } = authClient.useSession();
|
||||||
const navigate = Route.useNavigate();
|
const navigate = Route.useNavigate();
|
||||||
|
const [authorized, setAuthorized] = useState<boolean | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!((session || isPending) && id)) {
|
if (!((session || isPending) && id)) {
|
||||||
@@ -53,6 +55,32 @@ function SpaceRoute() {
|
|||||||
}
|
}
|
||||||
}, [session, isPending, navigate, id]);
|
}, [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
|
// Create a stable store instance once
|
||||||
const store = useMemo(
|
const store = useMemo(
|
||||||
() =>
|
() =>
|
||||||
@@ -71,11 +99,11 @@ function SpaceRoute() {
|
|||||||
// Track latest saved version hash to suppress redundant saves
|
// Track latest saved version hash to suppress redundant saves
|
||||||
const lastSavedRef = useRef<string>("");
|
const lastSavedRef = useRef<string>("");
|
||||||
|
|
||||||
// Load initial snapshot from Appwrite
|
// Load initial snapshot from Appwrite (only after authorization)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
async function load(_id: string | undefined) {
|
async function load(_id: string | undefined) {
|
||||||
if (!(_id && session)) {
|
if (!(_id && session && authorized)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const remote = await getLatestSpaceSnapshot(_id, session.$id);
|
const remote = await getLatestSpaceSnapshot(_id, session.$id);
|
||||||
@@ -95,15 +123,20 @@ function SpaceRoute() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
setStoreWithStatus({ status: "not-synced", store });
|
setStoreWithStatus({ status: "not-synced", store });
|
||||||
load(id);
|
if (authorized) {
|
||||||
|
load(id);
|
||||||
|
}
|
||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
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 />;
|
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
|
// Provide a custom Toolbar that injects our upload buttons into the bottom toolbar
|
||||||
const components: TLComponents = {
|
const components: TLComponents = {
|
||||||
@@ -318,7 +351,7 @@ function SpaceRoute() {
|
|||||||
let timeout: number | undefined;
|
let timeout: number | undefined;
|
||||||
const unlisten = editor.store.listen(
|
const unlisten = editor.store.listen(
|
||||||
() => {
|
() => {
|
||||||
if (!(id && session)) {
|
if (!(id && session && authorized)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
window.clearTimeout(timeout);
|
window.clearTimeout(timeout);
|
||||||
|
|||||||
Reference in New Issue
Block a user