This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { query, mutation } from "./_generated/server";
|
||||
import { query, internalMutation } from "./_generated/server";
|
||||
import { paginationOptsValidator } from "convex/server";
|
||||
import { v } from "convex/values";
|
||||
import { getPublicStorageUrl } from "./storageUrl";
|
||||
@@ -225,7 +225,19 @@ export const searchScan = query({
|
||||
},
|
||||
});
|
||||
|
||||
export const send = mutation({
|
||||
// Plain text caps out at 4000 chars in the composer, which encodes to
|
||||
// well under 8KB of AES-GCM output. Attachments ride a separate
|
||||
// metadata-only JSON path, so 64KB leaves ~8× headroom for future
|
||||
// structured content without letting a rogue client write multi-MB
|
||||
// rows and bloat the database.
|
||||
const MAX_CIPHERTEXT_CHARS = 64 * 1024;
|
||||
|
||||
// Internal write. The public API is `messageActions.send`, which verifies
|
||||
// the caller's Ed25519 signature over (channelId, senderId, timestamp)
|
||||
// before invoking this. Never call this from a public mutation — it
|
||||
// trusts `senderId` completely and doing so would reintroduce the
|
||||
// spoofing vulnerability.
|
||||
export const sendInternal = internalMutation({
|
||||
args: {
|
||||
channelId: v.id("channels"),
|
||||
senderId: v.id("userProfiles"),
|
||||
@@ -237,6 +249,9 @@ export const send = mutation({
|
||||
},
|
||||
returns: v.object({ id: v.id("messages") }),
|
||||
handler: async (ctx, args) => {
|
||||
if (args.ciphertext.length > MAX_CIPHERTEXT_CHARS) {
|
||||
throw new Error("Message too large");
|
||||
}
|
||||
const id = await ctx.db.insert("messages", {
|
||||
channelId: args.channelId,
|
||||
senderId: args.senderId,
|
||||
@@ -250,7 +265,11 @@ export const send = mutation({
|
||||
},
|
||||
});
|
||||
|
||||
export const sendBatch = mutation({
|
||||
// Internal-only: there's no public caller and no way to verify every
|
||||
// senderId in a batch without a signature per message, which defeats
|
||||
// the point of batching. Kept as internalMutation so internal seed
|
||||
// scripts / migrations can still use it.
|
||||
export const sendBatchInternal = internalMutation({
|
||||
args: {
|
||||
messages: v.array(v.object({
|
||||
channelId: v.id("channels"),
|
||||
@@ -270,15 +289,24 @@ export const sendBatch = mutation({
|
||||
},
|
||||
});
|
||||
|
||||
export const edit = mutation({
|
||||
// Internal write — public entry point is `messageActions.edit`.
|
||||
// Enforces that only the original sender can edit. The action layer
|
||||
// has already verified the caller controls `userId`.
|
||||
export const editInternal = internalMutation({
|
||||
args: {
|
||||
id: v.id("messages"),
|
||||
userId: v.id("userProfiles"),
|
||||
ciphertext: v.string(),
|
||||
nonce: v.string(),
|
||||
signature: v.string(),
|
||||
},
|
||||
returns: v.null(),
|
||||
handler: async (ctx, args) => {
|
||||
const msg = await ctx.db.get(args.id);
|
||||
if (!msg) throw new Error("Message not found");
|
||||
if (msg.senderId !== args.userId) {
|
||||
throw new Error("Only the author can edit this message");
|
||||
}
|
||||
await ctx.db.patch(args.id, {
|
||||
ciphertext: args.ciphertext,
|
||||
nonce: args.nonce,
|
||||
@@ -289,13 +317,24 @@ export const edit = mutation({
|
||||
},
|
||||
});
|
||||
|
||||
export const pin = mutation({
|
||||
// Internal write — public entry point is `messageActions.pin`. Only
|
||||
// `manage_messages` role-holders can pin/unpin; the action layer has
|
||||
// already verified the caller controls `userId`.
|
||||
export const pinInternal = internalMutation({
|
||||
args: {
|
||||
id: v.id("messages"),
|
||||
userId: v.id("userProfiles"),
|
||||
pinned: v.boolean(),
|
||||
},
|
||||
returns: v.null(),
|
||||
handler: async (ctx, args) => {
|
||||
const roles = await getRolesForUser(ctx, args.userId);
|
||||
const canManage = roles.some(
|
||||
(role) => (role.permissions as Record<string, boolean>)?.manage_messages,
|
||||
);
|
||||
if (!canManage) {
|
||||
throw new Error("Not authorized to pin messages");
|
||||
}
|
||||
await ctx.db.patch(args.id, { pinned: args.pinned });
|
||||
return null;
|
||||
},
|
||||
@@ -460,7 +499,11 @@ export const listAfter = query({
|
||||
},
|
||||
});
|
||||
|
||||
export const remove = mutation({
|
||||
// Internal write — public entry point is `messageActions.remove`. The
|
||||
// existing isSender-or-manage_messages check stays here; the action
|
||||
// layer verifies the caller actually controls `userId` before we trust
|
||||
// that arg.
|
||||
export const removeInternal = internalMutation({
|
||||
args: { id: v.id("messages"), userId: v.id("userProfiles") },
|
||||
returns: v.null(),
|
||||
handler: async (ctx, args) => {
|
||||
|
||||
Reference in New Issue
Block a user