114 lines
3.7 KiB
TypeScript
114 lines
3.7 KiB
TypeScript
"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<null> => {
|
|
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<null> => {
|
|
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<null> => {
|
|
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,
|
|
});
|
|
},
|
|
});
|