Add notes router
This commit is contained in:
parent
c176c14aa3
commit
63aa7505ea
6 changed files with 194 additions and 81 deletions
|
|
@ -1,6 +1,7 @@
|
|||
import { ListObjectsV2Command, PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
|
||||
import type { SupabaseClient } from "@supabase/supabase-js";
|
||||
import type { EventAndTablo } from "./types.ts";
|
||||
import type { Context, Next } from "hono";
|
||||
|
||||
export const generateICSFromEvents = (
|
||||
events: EventAndTablo[],
|
||||
|
|
@ -119,7 +120,7 @@ export const getTabloFileNames = async (s3_client: S3Client, tabloId: string) =>
|
|||
);
|
||||
};
|
||||
|
||||
export const isTabloMember = async (supabase: SupabaseClient, tabloId: string, userId: string) => {
|
||||
const isTabloMember = async (supabase: SupabaseClient, tabloId: string, userId: string) => {
|
||||
const { data: tabloAccess, error: isMemberError } = await supabase
|
||||
.from("tablo_access")
|
||||
.select("*")
|
||||
|
|
@ -134,7 +135,7 @@ export const isTabloMember = async (supabase: SupabaseClient, tabloId: string, u
|
|||
return tabloAccess?.length > 0;
|
||||
};
|
||||
|
||||
export const isTabloAdmin = async (supabase: SupabaseClient, tabloId: string, userId: string) => {
|
||||
const isTabloAdmin = async (supabase: SupabaseClient, tabloId: string, userId: string) => {
|
||||
const { data: tabloAccess, error: isAdminError } = await supabase
|
||||
.from("tablo_access")
|
||||
.select("*")
|
||||
|
|
@ -149,3 +150,25 @@ export const isTabloAdmin = async (supabase: SupabaseClient, tabloId: string, us
|
|||
|
||||
return tabloAccess?.length > 0;
|
||||
};
|
||||
|
||||
export const checkTabloMember = async (c: Context, next: Next) => {
|
||||
const supabase = c.get("supabase");
|
||||
const user = c.get("user");
|
||||
const tabloId = c.req.param("tabloId");
|
||||
const isMember = await isTabloMember(supabase, tabloId, user.id);
|
||||
if (!isMember) {
|
||||
return c.json({ error: "You are not a member of this tablo" }, 403);
|
||||
}
|
||||
await next();
|
||||
};
|
||||
|
||||
export const checkTabloAdmin = async (c: Context, next: Next) => {
|
||||
const supabase = c.get("supabase");
|
||||
const user = c.get("user");
|
||||
const tabloId = c.req.param("tabloId");
|
||||
const isAdmin = await isTabloAdmin(supabase, tabloId, user.id);
|
||||
if (!isAdmin) {
|
||||
return c.json({ error: "You are not an admin of this tablo" }, 403);
|
||||
}
|
||||
await next();
|
||||
};
|
||||
|
|
|
|||
|
|
@ -30,6 +30,14 @@ export const authMiddleware = async (c: Context, next: Next) => {
|
|||
await next();
|
||||
};
|
||||
|
||||
export const regularUserCheckMiddleware = async (c: Context, next: Next) => {
|
||||
const user = c.get("user");
|
||||
if (user.is_temporary) {
|
||||
return c.json({ error: "User is read only" }, 401);
|
||||
}
|
||||
await next();
|
||||
};
|
||||
|
||||
export const supabaseMiddleware = async (c: Context, next: Next) => {
|
||||
const supabase = createClient(
|
||||
process.env.SUPABASE_URL as string,
|
||||
|
|
|
|||
82
api/src/notes.ts
Normal file
82
api/src/notes.ts
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
import type { SupabaseClient, User } from "@supabase/supabase-js";
|
||||
import { Hono } from "hono";
|
||||
import { checkTabloMember } from "./helpers.js";
|
||||
import type { Database } from "./database.types.js";
|
||||
import { authMiddleware } from "./middleware.js";
|
||||
|
||||
export const notesRouter = new Hono<{
|
||||
Variables: {
|
||||
user: User;
|
||||
supabase: SupabaseClient;
|
||||
};
|
||||
}>();
|
||||
|
||||
type Note = Database["public"]["Tables"]["notes"]["Row"];
|
||||
|
||||
notesRouter.use(authMiddleware);
|
||||
|
||||
/**
|
||||
* Fetch notes shared with a specific tablo
|
||||
*/
|
||||
notesRouter.get("/:tabloId", checkTabloMember, async (c) => {
|
||||
const { tabloId } = c.req.param();
|
||||
|
||||
if (!tabloId) {
|
||||
return c.json({ error: "Tablo ID is required" }, 400);
|
||||
}
|
||||
|
||||
const supabase = c.get("supabase");
|
||||
|
||||
// Find the tablo owner
|
||||
const { data: tabloData, error: tabloError } = await supabase
|
||||
.from("tablos")
|
||||
.select("owner_id")
|
||||
.eq("id", tabloId)
|
||||
.single();
|
||||
|
||||
if (tabloError) {
|
||||
console.error("Error fetching tablo:", tabloError);
|
||||
return c.json({ error: "Failed to fetch tablo" }, 500);
|
||||
}
|
||||
|
||||
if (!tabloData) {
|
||||
return c.json({ error: "Tablo not found" }, 404);
|
||||
}
|
||||
|
||||
const tabloOwnerId = tabloData.owner_id;
|
||||
|
||||
// Find notes shared with this specific tablo or all tablos
|
||||
const { data, error } = await supabase
|
||||
.from("note_access")
|
||||
.select(`
|
||||
note_id,
|
||||
notes!inner (
|
||||
id,
|
||||
title,
|
||||
content,
|
||||
user_id,
|
||||
created_at,
|
||||
updated_at,
|
||||
deleted_at
|
||||
)
|
||||
`)
|
||||
.eq("is_active", true)
|
||||
.eq("user_id", tabloOwnerId)
|
||||
.or(`tablo_id.eq.${tabloId},tablo_id.is.null`)
|
||||
.is("notes.deleted_at", null);
|
||||
|
||||
if (error) {
|
||||
return c.json({ error: "An error occurred" }, 500);
|
||||
}
|
||||
|
||||
// Extract notes from the join result and remove duplicates
|
||||
type JoinedResult = { note_id: string; notes: Note[] };
|
||||
const extractedNotes = (data as JoinedResult[])
|
||||
.map((item) => (Array.isArray(item.notes) ? item.notes[0] : item.notes))
|
||||
.filter((note) => note !== null && note !== undefined);
|
||||
|
||||
// Remove duplicates by note id (in case a note is shared both with all tablos and this specific tablo)
|
||||
const uniqueNotes = Array.from(new Map(extractedNotes.map((note) => [note.id, note])).values());
|
||||
|
||||
return c.json({ notes: uniqueNotes });
|
||||
});
|
||||
|
|
@ -4,6 +4,7 @@ import { tabloRouter } from "./tablo.js";
|
|||
import { tabloDataRouter } from "./tablo_data.js";
|
||||
import { taskRouter } from "./tasks.js";
|
||||
import { userRouter } from "./user.js";
|
||||
import { notesRouter } from "./notes.js";
|
||||
|
||||
export const mainRouter = new Hono<{
|
||||
Bindings: {
|
||||
|
|
@ -34,3 +35,4 @@ mainRouter.route("/users", userRouter);
|
|||
mainRouter.route("/tablos", tabloRouter);
|
||||
mainRouter.route("/tasks", taskRouter);
|
||||
mainRouter.route("/tablo-data", tabloDataRouter);
|
||||
mainRouter.route("/notes", notesRouter);
|
||||
|
|
|
|||
|
|
@ -6,7 +6,12 @@ import type { StreamChat } from "stream-chat";
|
|||
import { config } from "./config.js";
|
||||
import type { Tables } from "./database.types.ts";
|
||||
import { writeCalendarFileToR2 } from "./helpers.js";
|
||||
import { authMiddleware, r2Middleware, streamChatMiddleware } from "./middleware.js";
|
||||
import {
|
||||
authMiddleware,
|
||||
r2Middleware,
|
||||
regularUserCheckMiddleware,
|
||||
streamChatMiddleware,
|
||||
} from "./middleware.js";
|
||||
import { generateToken } from "./token.js";
|
||||
import { transporter } from "./transporter.js";
|
||||
import type { EventInsertInTablo, TabloInsert } from "./types.ts";
|
||||
|
|
@ -41,7 +46,7 @@ type PostTablo = Omit<TabloInsert, "owner_id"> & {
|
|||
events?: EventInsertInTablo[];
|
||||
};
|
||||
|
||||
tabloRouter.post("/create", async (c) => {
|
||||
tabloRouter.post("/create", regularUserCheckMiddleware, async (c) => {
|
||||
const user = c.get("user");
|
||||
const supabase = c.get("supabase");
|
||||
const data = await c.req.json();
|
||||
|
|
@ -275,7 +280,7 @@ tabloRouter.post("/create-and-invite", async (c) => {
|
|||
return c.json({ id: tabloData.id });
|
||||
});
|
||||
|
||||
tabloRouter.patch("/update", async (c) => {
|
||||
tabloRouter.patch("/update", regularUserCheckMiddleware, async (c) => {
|
||||
const user = c.get("user");
|
||||
const supabase = c.get("supabase");
|
||||
const streamServerClient = c.get("streamServerClient");
|
||||
|
|
@ -356,7 +361,7 @@ tabloRouter.delete("/delete", async (c) => {
|
|||
return c.json({ message: "Tablo deleted successfully" });
|
||||
});
|
||||
|
||||
tabloRouter.post("/invite", async (c) => {
|
||||
tabloRouter.post("/invite", regularUserCheckMiddleware, async (c) => {
|
||||
const sender = c.get("user");
|
||||
const supabase = c.get("supabase");
|
||||
const { email: recipientmail, tablo_id } = await c.req.json();
|
||||
|
|
@ -545,7 +550,7 @@ tabloRouter.post("/leave", async (c) => {
|
|||
return c.json({ message: "Tablo left successfully" });
|
||||
});
|
||||
|
||||
tabloRouter.post("/webcal/generate-url", async (c) => {
|
||||
tabloRouter.post("/webcal/generate-url", regularUserCheckMiddleware, async (c) => {
|
||||
const user = c.get("user");
|
||||
const supabase = c.get("supabase");
|
||||
const s3_client = c.get("s3_client");
|
||||
|
|
|
|||
|
|
@ -1,8 +1,13 @@
|
|||
import { PutObjectCommand, type S3Client } from "@aws-sdk/client-s3";
|
||||
import type { SupabaseClient, User } from "@supabase/supabase-js";
|
||||
import { type Context, Hono, type Next } from "hono";
|
||||
import { getTabloFileNames, isTabloAdmin, isTabloMember } from "./helpers.js";
|
||||
import { authMiddleware, r2Middleware, streamChatMiddleware } from "./middleware.js";
|
||||
import { Hono } from "hono";
|
||||
import { checkTabloAdmin, checkTabloMember, getTabloFileNames } from "./helpers.js";
|
||||
import {
|
||||
authMiddleware,
|
||||
r2Middleware,
|
||||
regularUserCheckMiddleware,
|
||||
streamChatMiddleware,
|
||||
} from "./middleware.js";
|
||||
|
||||
export const tabloDataRouter = new Hono<{
|
||||
Variables: {
|
||||
|
|
@ -16,28 +21,6 @@ tabloDataRouter.use(authMiddleware);
|
|||
tabloDataRouter.use(streamChatMiddleware);
|
||||
tabloDataRouter.use(r2Middleware);
|
||||
|
||||
const checkTabloMember = async (c: Context, next: Next) => {
|
||||
const supabase = c.get("supabase");
|
||||
const user = c.get("user");
|
||||
const tabloId = c.req.param("tabloId");
|
||||
const isMember = await isTabloMember(supabase, tabloId, user.id);
|
||||
if (!isMember) {
|
||||
return c.json({ error: "You are not a member of this tablo" }, 403);
|
||||
}
|
||||
await next();
|
||||
};
|
||||
|
||||
const checkTabloAdmin = async (c: Context, next: Next) => {
|
||||
const supabase = c.get("supabase");
|
||||
const user = c.get("user");
|
||||
const tabloId = c.req.param("tabloId");
|
||||
const isAdmin = await isTabloAdmin(supabase, tabloId, user.id);
|
||||
if (!isAdmin) {
|
||||
return c.json({ error: "You are not an admin of this tablo" }, 403);
|
||||
}
|
||||
await next();
|
||||
};
|
||||
|
||||
// GET /tablo-data/:tabloId/filenames - Get all files for a tablo
|
||||
tabloDataRouter.get("/:tabloId/filenames", checkTabloMember, async (c) => {
|
||||
const tabloId = c.req.param("tabloId");
|
||||
|
|
@ -88,39 +71,44 @@ tabloDataRouter.get("/:tabloId/:fileName", checkTabloMember, async (c) => {
|
|||
});
|
||||
|
||||
// POST /tablo-data/:tabloId/:fileName - Create or update a file
|
||||
tabloDataRouter.post("/:tabloId/:fileName", checkTabloAdmin, async (c) => {
|
||||
const tabloId = c.req.param("tabloId");
|
||||
const fileName = c.req.param("fileName");
|
||||
tabloDataRouter.post(
|
||||
"/:tabloId/:fileName",
|
||||
regularUserCheckMiddleware,
|
||||
checkTabloAdmin,
|
||||
async (c) => {
|
||||
const tabloId = c.req.param("tabloId");
|
||||
const fileName = c.req.param("fileName");
|
||||
|
||||
const s3_client = c.get("s3_client");
|
||||
const s3_client = c.get("s3_client");
|
||||
|
||||
try {
|
||||
const body = await c.req.json();
|
||||
const { content, contentType = "text/plain" } = body;
|
||||
try {
|
||||
const body = await c.req.json();
|
||||
const { content, contentType = "text/plain" } = body;
|
||||
|
||||
if (!content) {
|
||||
return c.json({ error: "Content is required" }, 400);
|
||||
if (!content) {
|
||||
return c.json({ error: "Content is required" }, 400);
|
||||
}
|
||||
|
||||
await s3_client.send(
|
||||
new PutObjectCommand({
|
||||
Bucket: "tablo-data",
|
||||
Key: `${tabloId}/${fileName}`,
|
||||
Body: content,
|
||||
ContentType: contentType,
|
||||
})
|
||||
);
|
||||
|
||||
return c.json({
|
||||
message: "File uploaded successfully",
|
||||
fileName,
|
||||
tabloId,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error uploading file:", error);
|
||||
return c.json({ error: "Failed to upload file" }, 500);
|
||||
}
|
||||
|
||||
await s3_client.send(
|
||||
new PutObjectCommand({
|
||||
Bucket: "tablo-data",
|
||||
Key: `${tabloId}/${fileName}`,
|
||||
Body: content,
|
||||
ContentType: contentType,
|
||||
})
|
||||
);
|
||||
|
||||
return c.json({
|
||||
message: "File uploaded successfully",
|
||||
fileName,
|
||||
tabloId,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error uploading file:", error);
|
||||
return c.json({ error: "Failed to upload file" }, 500);
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
// // PUT /tablo-data/:tabloId/:fileName - Update a file
|
||||
// tabloDataRouter.put("/:tabloId/:fileName", async (c) => {
|
||||
|
|
@ -159,28 +147,33 @@ tabloDataRouter.post("/:tabloId/:fileName", checkTabloAdmin, async (c) => {
|
|||
// });
|
||||
|
||||
// DELETE /tablo-data/:tabloId/:fileName - Delete a file
|
||||
tabloDataRouter.delete("/:tabloId/:fileName", checkTabloAdmin, async (c) => {
|
||||
const tabloId = c.req.param("tabloId");
|
||||
const fileName = c.req.param("fileName");
|
||||
const s3_client = c.get("s3_client");
|
||||
tabloDataRouter.delete(
|
||||
"/:tabloId/:fileName",
|
||||
regularUserCheckMiddleware,
|
||||
checkTabloAdmin,
|
||||
async (c) => {
|
||||
const tabloId = c.req.param("tabloId");
|
||||
const fileName = c.req.param("fileName");
|
||||
const s3_client = c.get("s3_client");
|
||||
|
||||
try {
|
||||
const { DeleteObjectCommand } = await import("@aws-sdk/client-s3");
|
||||
try {
|
||||
const { DeleteObjectCommand } = await import("@aws-sdk/client-s3");
|
||||
|
||||
await s3_client.send(
|
||||
new DeleteObjectCommand({
|
||||
Bucket: "tablo-data",
|
||||
Key: `${tabloId}/${fileName}`,
|
||||
})
|
||||
);
|
||||
await s3_client.send(
|
||||
new DeleteObjectCommand({
|
||||
Bucket: "tablo-data",
|
||||
Key: `${tabloId}/${fileName}`,
|
||||
})
|
||||
);
|
||||
|
||||
return c.json({
|
||||
message: "File deleted successfully",
|
||||
fileName,
|
||||
tabloId,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error deleting file:", error);
|
||||
return c.json({ error: "Failed to delete file" }, 500);
|
||||
return c.json({
|
||||
message: "File deleted successfully",
|
||||
fileName,
|
||||
tabloId,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error deleting file:", error);
|
||||
return c.json({ error: "Failed to delete file" }, 500);
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in a new issue