bump
Some checks failed
Build and Release / build-and-release (push) Has been cancelled

This commit is contained in:
Bryan1029384756
2026-04-20 17:09:04 -05:00
parent 82f8a12e27
commit b83360db35
48 changed files with 6079 additions and 49 deletions

View File

@@ -1,5 +1,8 @@
import { query, mutation } from "./_generated/server";
import { v } from "convex/values";
import { hasPermission } from "./roles";
import { AUDIT_ACTIONS, logAudit } from "./audit";
import { getPublicStorageUrl } from "./storageUrl";
/**
* Rotate the symmetric key for a DM channel. Inserts a brand-new
@@ -142,3 +145,138 @@ export const getKeysForUser = query({
}));
},
});
/**
* Admin-only query: list users who don't have a `channelKeys` row for
* the given (non-DM) channel. Powers the Channel Settings → Access
* panel where an admin grants missing keys to users who joined via a
* broken invite (only received one channel's key instead of all).
*
* The actor themselves is filtered out — you can't be missing your own
* key from your own POV, and the UI never needs to grant to self.
* Ghosts and users without a public key are filtered (can't log in
* anyway / nothing to encrypt against).
*/
export const getUsersMissingChannelKey = query({
args: {
actorId: v.id("userProfiles"),
channelId: v.id("channels"),
},
returns: v.array(
v.object({
userId: v.id("userProfiles"),
username: v.string(),
displayName: v.union(v.string(), v.null()),
avatarUrl: v.union(v.string(), v.null()),
userPublicKey: v.string(),
}),
),
handler: async (ctx, args) => {
if (!(await hasPermission(ctx, args.actorId, "manage_channels"))) {
throw new Error("Forbidden");
}
const channel = await ctx.db.get(args.channelId);
if (!channel) throw new Error("Channel not found");
if (channel.type === "dm") {
throw new Error("grantChannelAccess is not supported for DM channels");
}
const existing = await ctx.db
.query("channelKeys")
.withIndex("by_channel", (q) => q.eq("channelId", args.channelId))
.collect();
const have = new Set(existing.map((k) => k.userId as unknown as string));
const users = await ctx.db.query("userProfiles").collect();
const missing = users.filter(
(u) =>
!!u.publicIdentityKey &&
!u.isGhost &&
(u._id as unknown as string) !== (args.actorId as unknown as string) &&
!have.has(u._id as unknown as string),
);
const results = [];
for (const u of missing) {
let avatarUrl: string | null = null;
if (u.avatarStorageId) {
avatarUrl = await getPublicStorageUrl(ctx, u.avatarStorageId);
}
results.push({
userId: u._id,
username: u.username,
displayName: u.displayName ?? null,
avatarUrl,
userPublicKey: u.publicIdentityKey,
});
}
return results;
},
});
/**
* Admin-gated single-user grant. The admin's client has already
* decrypted its own channel-key bundle and re-encrypted the key against
* the target user's RSA public key — this mutation just upserts the
* row, re-checks auth, and writes an audit entry. Kept separate from
* `uploadKeys` on purpose: that path is used unauthenticated during
* register/invite-accept for the caller's own keys, and loosening it
* to accept cross-user writes would let any client grant themselves
* access to any channel.
*/
export const grantChannelAccess = mutation({
args: {
actorId: v.id("userProfiles"),
channelId: v.id("channels"),
userId: v.id("userProfiles"),
encryptedKeyBundle: v.string(),
keyVersion: v.number(),
},
returns: v.object({ success: v.boolean() }),
handler: async (ctx, args) => {
if (!(await hasPermission(ctx, args.actorId, "manage_channels"))) {
throw new Error("Forbidden");
}
const channel = await ctx.db.get(args.channelId);
if (!channel) throw new Error("Channel not found");
if (channel.type === "dm") {
throw new Error("grantChannelAccess is not supported for DM channels");
}
const target = await ctx.db.get(args.userId);
if (!target) throw new Error("Target user not found");
if (target.isGhost || !target.publicIdentityKey) {
throw new Error("Target user can't receive keys");
}
const existing = await ctx.db
.query("channelKeys")
.withIndex("by_channel_and_user", (q) =>
q.eq("channelId", args.channelId).eq("userId", args.userId),
)
.unique();
if (existing) {
await ctx.db.patch(existing._id, {
encryptedKeyBundle: args.encryptedKeyBundle,
keyVersion: args.keyVersion,
});
} else {
await ctx.db.insert("channelKeys", {
channelId: args.channelId,
userId: args.userId,
encryptedKeyBundle: args.encryptedKeyBundle,
keyVersion: args.keyVersion,
});
}
await logAudit(ctx, {
actorId: args.actorId,
action: AUDIT_ACTIONS.KEYS_GRANT,
targetType: "channel",
targetId: args.channelId as unknown as string,
targetName: channel.name,
metadata: { userId: args.userId, username: target.username },
});
return { success: true };
},
});