"use node"; import { action } from "./_generated/server"; import { internal } from "./_generated/api"; import { v } from "convex/values"; import { requireAuth } from "./authGuard"; /** * Signed profile update. The canonical message binds userId + timestamp; * the 5-minute replay window limits damage if a signature leaks. See * `authGuard.requireAuth` for the full verification flow. */ export const updateProfile = action({ args: { userId: v.id("userProfiles"), displayName: v.optional(v.string()), aboutMe: v.optional(v.string()), avatarStorageId: v.optional(v.id("_storage")), customStatus: v.optional(v.string()), joinSoundStorageId: v.optional(v.id("_storage")), removeJoinSound: v.optional(v.boolean()), accentColor: v.optional(v.string()), bannerStorageId: v.optional(v.id("_storage")), removeBanner: v.optional(v.boolean()), authTimestamp: v.number(), authSignature: v.string(), }, returns: v.null(), handler: async (ctx, args): Promise => { const canonical = `updateProfile:${args.userId}:${args.authTimestamp}`; await requireAuth(ctx, args.userId, args.authTimestamp, args.authSignature, canonical); await ctx.runMutation(internal.auth.updateProfileInternal, { userId: args.userId, displayName: args.displayName, aboutMe: args.aboutMe, avatarStorageId: args.avatarStorageId, customStatus: args.customStatus, joinSoundStorageId: args.joinSoundStorageId, removeJoinSound: args.removeJoinSound, accentColor: args.accentColor, bannerStorageId: args.bannerStorageId, removeBanner: args.removeBanner, }); return null; }, }); export const updateStatus = action({ args: { userId: v.id("userProfiles"), status: v.string(), authTimestamp: v.number(), authSignature: v.string(), }, returns: v.null(), handler: async (ctx, args): Promise => { const canonical = `updateStatus:${args.userId}:${args.status}:${args.authTimestamp}`; await requireAuth(ctx, args.userId, args.authTimestamp, args.authSignature, canonical); await ctx.runMutation(internal.auth.updateStatusInternal, { userId: args.userId, status: args.status, }); return null; }, }); export const setNickname = action({ args: { actorUserId: v.id("userProfiles"), targetUserId: v.id("userProfiles"), displayName: v.string(), authTimestamp: v.number(), authSignature: v.string(), }, returns: v.null(), handler: async (ctx, args): Promise => { const canonical = `setNickname:${args.actorUserId}:${args.targetUserId}:${args.authTimestamp}`; await requireAuth(ctx, args.actorUserId, args.authTimestamp, args.authSignature, canonical); await ctx.runMutation(internal.auth.setNicknameInternal, { actorUserId: args.actorUserId, targetUserId: args.targetUserId, displayName: args.displayName, }); return null; }, }); export const deleteUser = action({ args: { requestingUserId: v.id("userProfiles"), targetUserId: v.id("userProfiles"), authTimestamp: v.number(), authSignature: v.string(), }, returns: v.object({ success: v.boolean(), error: v.optional(v.string()) }), handler: async ( ctx, args, ): Promise<{ success: boolean; error?: string }> => { const canonical = `deleteUser:${args.requestingUserId}:${args.targetUserId}:${args.authTimestamp}`; await requireAuth( ctx, args.requestingUserId, args.authTimestamp, args.authSignature, canonical, ); return await ctx.runMutation(internal.auth.deleteUserInternal, { requestingUserId: args.requestingUserId, targetUserId: args.targetUserId, }); }, });