import { mutation, query } from "./_generated/server"; import { v } from "convex/values"; import { getAuthUserId } from "@convex-dev/auth/server"; import { nowIso } from "./_utils/time"; import type { Id } from "./_generated/dataModel"; const permission = v.union(v.literal("read"), v.literal("edit")); async function requireUserId(ctx: any): Promise { const userId = await getAuthUserId(ctx); if (userId === null) throw new Error("未登录"); return String(userId); } function normalizeUsername(raw: string): string { const username = String(raw ?? "").trim(); if (!username) throw new Error("用户名不能为空"); if (username.length < 2 || username.length > 32) throw new Error("用户名长度需为 2-32 个字符"); if (/\s/.test(username)) throw new Error("用户名不能包含空格"); return username; } async function requireOwnedDocument(ctx: any, documentId: string, userId: string) { const doc = await ctx.db .query("documents") .withIndex("by_document_id", (q: any) => q.eq("id", documentId)) .first(); if (!doc) throw new Error("页面不存在"); if (doc.user_id !== userId) throw new Error("无权限"); return doc; } async function resolveUserIdByUsername(ctx: any, rawUsername: string): Promise<{ userId: string; username: string }> { const username = normalizeUsername(rawUsername); // 说明:用户名存放在 Convex Auth 的 users.name;当前没有索引,这里用 filter 做扫描。 const user = await ctx.db .query("users") .filter((q: any) => q.eq(q.field("name"), username)) .first(); if (!user) throw new Error("未找到该用户(对方可能未注册或未设置用户名)"); return { userId: String(user._id), username: String(user.name ?? "") || username }; } export const listByDocument = query({ args: { documentId: v.string() }, handler: async (ctx, args) => { const userId = await requireUserId(ctx); const doc = await requireOwnedDocument(ctx, args.documentId, userId); const shares = await ctx.db .query("document_shares") .withIndex("by_document", (q: any) => q.eq("document_id", doc.id)) .collect(); const result = []; for (const share of shares) { const user = await ctx.db.get(share.shared_with_user_id as Id<"users">); result.push({ userId: share.shared_with_user_id, username: user?.name ?? null, permission: share.permission, includeDescendants: share.include_descendants, createdAt: share.created_at, updatedAt: share.updated_at, }); } return result; }, }); export const upsert = mutation({ args: { documentId: v.string(), username: v.string(), permission, includeDescendants: v.optional(v.boolean()), }, handler: async (ctx, args) => { const userId = await requireUserId(ctx); const doc = await requireOwnedDocument(ctx, args.documentId, userId); const { userId: sharedWithUserId } = await resolveUserIdByUsername(ctx, args.username); if (sharedWithUserId === userId) throw new Error("不能共享给自己"); const ts = nowIso(); const includeDescendants = Boolean(args.includeDescendants); // 确保对方成为 workspace member(否则无法看到该 workspace 的数据) const existingMember = await ctx.db .query("workspace_members") .withIndex("by_workspace_user", (q: any) => q.eq("workspace_id", doc.workspace_id).eq("user_id", sharedWithUserId)) .first(); if (!existingMember) { await ctx.db.insert("workspace_members", { workspace_id: doc.workspace_id, user_id: sharedWithUserId, role: "member", is_default: false, created_at: ts, }); } const existingShare = await ctx.db .query("document_shares") .withIndex("by_doc_user", (q: any) => q.eq("document_id", doc.id).eq("shared_with_user_id", sharedWithUserId), ) .first(); if (existingShare) { await ctx.db.patch(existingShare._id, { permission: args.permission, include_descendants: includeDescendants, updated_at: ts, }); } else { await ctx.db.insert("document_shares", { workspace_id: doc.workspace_id, document_id: doc.id, shared_with_user_id: sharedWithUserId, permission: args.permission, include_descendants: includeDescendants, created_by: userId, created_at: ts, updated_at: ts, }); } // 仅更新时间戳:共享不改变页面自身的 access_scope(避免把自己的页面从“私密”分区挪走) await ctx.db.patch(doc._id, { updated_at: ts }); return { ok: true }; }, }); export const remove = mutation({ args: { documentId: v.string(), sharedWithUserId: v.string() }, handler: async (ctx, args) => { const userId = await requireUserId(ctx); const doc = await requireOwnedDocument(ctx, args.documentId, userId); const existingShare = await ctx.db .query("document_shares") .withIndex("by_doc_user", (q: any) => q.eq("document_id", doc.id).eq("shared_with_user_id", args.sharedWithUserId), ) .first(); if (existingShare) { await ctx.db.delete(existingShare._id); } await ctx.db.patch(doc._id, { updated_at: nowIso() }); return { ok: true }; }, }); export const listShareRootsByWorkspace = query({ args: { workspaceId: v.string() }, handler: async (ctx, args) => { const userId = await requireUserId(ctx); // 仅允许查看自己所在 workspace 的共享摘要 const member = await ctx.db .query("workspace_members") .withIndex("by_workspace_user", (q: any) => q.eq("workspace_id", args.workspaceId).eq("user_id", userId)) .first(); if (!member) throw new Error("无权限"); const incoming = await ctx.db .query("document_shares") .withIndex("by_workspace_shared_with", (q: any) => q.eq("workspace_id", args.workspaceId).eq("shared_with_user_id", userId)) .collect(); const outgoing = await ctx.db .query("document_shares") .withIndex("by_workspace_created_by", (q: any) => q.eq("workspace_id", args.workspaceId).eq("created_by", userId)) .collect(); const outgoingByDoc = new Map(); for (const row of outgoing) { const existing = outgoingByDoc.get(row.document_id); if (!existing) { outgoingByDoc.set(row.document_id, { includeDescendants: Boolean(row.include_descendants), sharedWithCount: 1, }); } else { existing.includeDescendants = existing.includeDescendants || Boolean(row.include_descendants); existing.sharedWithCount += 1; } } return { incoming: incoming.map((r) => ({ documentId: r.document_id, permission: r.permission, includeDescendants: r.include_descendants, createdBy: r.created_by, updatedAt: r.updated_at, })), outgoing: Array.from(outgoingByDoc.entries()).map(([documentId, v]) => ({ documentId, includeDescendants: v.includeDescendants, sharedWithCount: v.sharedWithCount, })), }; }, });