feat(ui): add Button, Modal, Spinner, Toast, and Tooltip components with styles
All checks were successful
Build and Release / build-and-release (push) Successful in 13m12s
All checks were successful
Build and Release / build-and-release (push) Successful in 13m12s
- Implemented Button component with various props for customization. - Created Modal component with header, content, and footer subcomponents. - Added Spinner component for loading indicators. - Developed Toast component for displaying notifications. - Introduced Tooltip component for contextual hints with keyboard shortcuts. - Added corresponding CSS modules for styling each component. - Updated index file to export new components. - Configured TypeScript settings for the UI package.
This commit is contained in:
@@ -1,6 +1,78 @@
|
||||
import { query, mutation } from "./_generated/server";
|
||||
import { v } from "convex/values";
|
||||
|
||||
/**
|
||||
* Rotate the symmetric key for a DM channel. Inserts a brand-new
|
||||
* versioned row for each participant — existing rows are left alone
|
||||
* so previously-encrypted messages remain decryptable.
|
||||
*
|
||||
* The caller proves they're a DM participant by passing their own
|
||||
* userId; the server cross-checks against `dmParticipants` for the
|
||||
* channel. Every recipient userId in `entries` must also be a
|
||||
* participant — no leaking keys to random users.
|
||||
*
|
||||
* The new rows are tagged with `maxExistingVersion + 1`.
|
||||
*/
|
||||
export const rotateDMKey = mutation({
|
||||
args: {
|
||||
channelId: v.id("channels"),
|
||||
initiatorUserId: v.id("userProfiles"),
|
||||
entries: v.array(
|
||||
v.object({
|
||||
userId: v.id("userProfiles"),
|
||||
encryptedKeyBundle: v.string(),
|
||||
}),
|
||||
),
|
||||
},
|
||||
returns: v.object({ keyVersion: v.number() }),
|
||||
handler: async (ctx, args) => {
|
||||
const channel = await ctx.db.get(args.channelId);
|
||||
if (!channel) throw new Error("Channel not found");
|
||||
if (channel.type !== "dm") {
|
||||
throw new Error("rotateDMKey is only supported for DM channels");
|
||||
}
|
||||
|
||||
// Verify every (initiator + entries) userId is in dmParticipants.
|
||||
const participants = await ctx.db
|
||||
.query("dmParticipants")
|
||||
.withIndex("by_channel", (q) => q.eq("channelId", args.channelId))
|
||||
.collect();
|
||||
const participantSet = new Set(participants.map((p) => p.userId as string));
|
||||
if (!participantSet.has(args.initiatorUserId as unknown as string)) {
|
||||
throw new Error("Not a participant in this DM");
|
||||
}
|
||||
for (const entry of args.entries) {
|
||||
if (!participantSet.has(entry.userId as unknown as string)) {
|
||||
throw new Error("Target userId is not a participant in this DM");
|
||||
}
|
||||
}
|
||||
|
||||
// Find the current max keyVersion for this channel. New rows go
|
||||
// one above that. If no rows exist yet, start at 2 so legacy
|
||||
// messages tagged version 1 still hit their original key.
|
||||
const existing = await ctx.db
|
||||
.query("channelKeys")
|
||||
.withIndex("by_channel", (q) => q.eq("channelId", args.channelId))
|
||||
.collect();
|
||||
const maxVersion = existing.reduce(
|
||||
(m, k) => (k.keyVersion > m ? k.keyVersion : m),
|
||||
0,
|
||||
);
|
||||
const newVersion = maxVersion + 1;
|
||||
|
||||
for (const entry of args.entries) {
|
||||
await ctx.db.insert("channelKeys", {
|
||||
channelId: args.channelId,
|
||||
userId: entry.userId,
|
||||
encryptedKeyBundle: entry.encryptedKeyBundle,
|
||||
keyVersion: newVersion,
|
||||
});
|
||||
}
|
||||
|
||||
return { keyVersion: newVersion };
|
||||
},
|
||||
});
|
||||
|
||||
// Batch upsert encrypted key bundles
|
||||
export const uploadKeys = mutation({
|
||||
args: {
|
||||
|
||||
Reference in New Issue
Block a user