aboutsummaryrefslogtreecommitdiffstats
path: root/packages/trpc/models/lists.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/trpc/models/lists.ts')
-rw-r--r--packages/trpc/models/lists.ts114
1 files changed, 50 insertions, 64 deletions
diff --git a/packages/trpc/models/lists.ts b/packages/trpc/models/lists.ts
index 2250819f..a0d9ca23 100644
--- a/packages/trpc/models/lists.ts
+++ b/packages/trpc/models/lists.ts
@@ -26,6 +26,7 @@ import { AuthedContext, Context } from "..";
import { buildImpersonatingAuthedContext } from "../lib/impersonate";
import { getBookmarkIdsFromMatcher } from "../lib/search";
import { Bookmark } from "./bookmarks";
+import { ListInvitation } from "./listInvitations";
interface ListCollaboratorEntry {
membershipId: string;
@@ -536,7 +537,7 @@ export abstract class List {
throw new TRPCError({ code: "NOT_FOUND" });
}
invariant(result[0].userId === this.ctx.user.id);
- // Fetch current collaborators count to update hasCollaborators
+ // Fetch current collaborators to update hasCollaborators
const collaboratorsCount =
await this.ctx.db.query.listCollaborators.findMany({
where: eq(listCollaborators.listId, this.list.id),
@@ -596,61 +597,24 @@ export abstract class List {
/**
* Add a collaborator to this list by email.
+ * Creates a pending invitation that must be accepted by the user.
+ * Returns the invitation ID.
*/
async addCollaboratorByEmail(
email: string,
role: "viewer" | "editor",
- ): Promise<void> {
+ ): Promise<string> {
this.ensureCanManage();
- // Look up the user by email
- const user = await this.ctx.db.query.users.findFirst({
- where: (users, { eq }) => eq(users.email, email),
- });
-
- if (!user) {
- throw new TRPCError({
- code: "NOT_FOUND",
- message: "No user found with that email address",
- });
- }
-
- // Check that the user is not adding themselves
- if (user.id === this.list.userId) {
- throw new TRPCError({
- code: "BAD_REQUEST",
- message: "Cannot add the list owner as a collaborator",
- });
- }
-
- // Check that the collaborator is not already added
- const existing = await this.ctx.db.query.listCollaborators.findFirst({
- where: and(
- eq(listCollaborators.listId, this.list.id),
- eq(listCollaborators.userId, user.id),
- ),
- });
-
- if (existing) {
- throw new TRPCError({
- code: "BAD_REQUEST",
- message: "User is already a collaborator on this list",
- });
- }
-
- // Only manual lists can be collaborative
- if (this.list.type !== "manual") {
- throw new TRPCError({
- code: "BAD_REQUEST",
- message: "Only manual lists can have collaborators",
- });
- }
-
- await this.ctx.db.insert(listCollaborators).values({
- listId: this.list.id,
- userId: user.id,
+ return await ListInvitation.inviteByEmail(this.ctx, {
+ email,
role,
- addedBy: this.ctx.user.id,
+ listId: this.list.id,
+ listName: this.list.name,
+ listType: this.list.type,
+ listOwnerId: this.list.userId,
+ inviterUserId: this.ctx.user.id,
+ inviterName: this.ctx.user.name ?? null,
});
}
@@ -738,23 +702,34 @@ export abstract class List {
}
/**
- * Get all collaborators for this list.
+ * Get all collaborators for this list, including pending invitations.
+ * For privacy, pending invitations show masked user info unless the invitation has been accepted.
*/
async getCollaborators() {
this.ensureCanView();
- const collaborators = await this.ctx.db.query.listCollaborators.findMany({
- where: eq(listCollaborators.listId, this.list.id),
- with: {
- user: {
- columns: {
- id: true,
- name: true,
- email: true,
+ const isOwner = this.list.userId === this.ctx.user.id;
+
+ const [collaborators, invitations] = await Promise.all([
+ this.ctx.db.query.listCollaborators.findMany({
+ where: eq(listCollaborators.listId, this.list.id),
+ with: {
+ user: {
+ columns: {
+ id: true,
+ name: true,
+ email: true,
+ },
},
},
- },
- });
+ }),
+ // Only show invitations for the owner
+ isOwner
+ ? ListInvitation.invitationsForList(this.ctx, {
+ listId: this.list.id,
+ })
+ : [],
+ ]);
// Get the owner information
const owner = await this.ctx.db.query.users.findFirst({
@@ -766,14 +741,24 @@ export abstract class List {
},
});
- return {
- collaborators: collaborators.map((c) => ({
+ const collaboratorEntries = collaborators.map((c) => {
+ return {
id: c.id,
userId: c.userId,
role: c.role,
+ status: "accepted" as const,
addedAt: c.addedAt,
- user: c.user,
- })),
+ invitedAt: c.addedAt,
+ user: {
+ id: c.user.id,
+ name: c.user.name,
+ email: c.user.email,
+ },
+ };
+ });
+
+ return {
+ collaborators: [...collaboratorEntries, ...invitations],
owner: owner
? {
id: owner.id,
@@ -786,6 +771,7 @@ export abstract class List {
/**
* Get all lists shared with the user (as a collaborator).
+ * Only includes lists where the invitation has been accepted.
*/
static async getSharedWithUser(
ctx: AuthedContext,