Migrate backend from PocketBase to Directus

Full cutover: auth, users, collection and friendships now run on Directus
via @directus/sdk, with owner/visibility rules enforced by Directus
permissions.

- middleware: Directus session (directus_auth cookie) with token refresh
- /login is Directus (email normalized to lowercase), new /register page
  (first/last name, username) backed by public registration
- collection, friends and profile pages + all API endpoints ported;
  ownership and addressee-only accept/decline enforced by permissions
- display names use first_name/last_name everywhere (nav, friends, profile)
- PocketBase SDK, POCKETBASE_URL and pb_schema.json removed
- README and compose updated for DIRECTUS_URL
This commit is contained in:
2026-07-28 20:33:46 +02:00
parent a3a69e0eae
commit fc51371dad
37 changed files with 986 additions and 765 deletions
+30 -24
View File
@@ -1,7 +1,9 @@
import type { APIRoute } from "astro";
import { updateItem } from "@directus/sdk";
import { isLoggedIn } from "../../../lib/auth";
export const POST: APIRoute = async ({ request, locals }) => {
if (!locals.pb.authStore.isValid) {
if (!isLoggedIn(locals)) {
return new Response(JSON.stringify({ error: "Unauthorized" }), {
status: 401,
headers: { "Content-Type": "application/json" },
@@ -10,37 +12,41 @@ export const POST: APIRoute = async ({ request, locals }) => {
try {
const { friendshipId } = await request.json();
const id = Number(friendshipId);
if (!friendshipId) {
return new Response(JSON.stringify({ error: "Missing friendship id" }), {
status: 400,
headers: { "Content-Type": "application/json" },
});
if (!id || Number.isNaN(id)) {
return new Response(
JSON.stringify({ error: "Missing friendship id" }),
{
status: 400,
headers: { "Content-Type": "application/json" },
},
);
}
const friendship = await locals.pb.collection("friendships").getOne(friendshipId);
// Only the addressee can decline
if (friendship.addressee !== locals.pb.authStore.record!.id) {
return new Response(JSON.stringify({ error: "Forbidden" }), {
status: 403,
headers: { "Content-Type": "application/json" },
});
}
await locals.pb.collection("friendships").update(friendshipId, {
status: "declined",
});
// Only the addressee can decline — enforced by the Directus update
// permission (addressee = $CURRENT_USER, status field only).
await locals.directus!.request(
updateItem("friendships", id, { status: "declined" }),
);
return new Response(JSON.stringify({ success: true }), {
status: 200,
headers: { "Content-Type": "application/json" },
});
} catch (err) {
} catch (err: any) {
console.error("Decline friend failed:", err);
return new Response(JSON.stringify({ error: "Failed to decline" }), {
status: 500,
headers: { "Content-Type": "application/json" },
});
const forbidden =
err?.errors?.[0]?.extensions?.code === "FORBIDDEN" ||
err?.response?.status === 403;
return new Response(
JSON.stringify({
error: forbidden ? "Forbidden" : "Failed to decline",
}),
{
status: forbidden ? 403 : 500,
headers: { "Content-Type": "application/json" },
},
);
}
};