Skip to content

Commit

Permalink
feat(api): add budget invitation service (#21)
Browse files Browse the repository at this point in the history
  • Loading branch information
duongdev authored Jun 8, 2024
1 parent 10b0e3d commit 4daa0fe
Show file tree
Hide file tree
Showing 4 changed files with 199 additions and 2 deletions.
24 changes: 23 additions & 1 deletion apps/api/prisma/generated/zod/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1123,18 +1123,35 @@ export const BudgetUserInvitationOrderByWithRelationInputSchema: z.ZodType<Prism
export const BudgetUserInvitationWhereUniqueInputSchema: z.ZodType<Prisma.BudgetUserInvitationWhereUniqueInput> = z.union([
z.object({
id: z.string().cuid(),
token_budgetId: z.lazy(() => BudgetUserInvitationTokenBudgetIdCompoundUniqueInputSchema)
token_budgetId: z.lazy(() => BudgetUserInvitationTokenBudgetIdCompoundUniqueInputSchema),
email_budgetId: z.lazy(() => BudgetUserInvitationEmailBudgetIdCompoundUniqueInputSchema)
}),
z.object({
id: z.string().cuid(),
token_budgetId: z.lazy(() => BudgetUserInvitationTokenBudgetIdCompoundUniqueInputSchema),
}),
z.object({
id: z.string().cuid(),
email_budgetId: z.lazy(() => BudgetUserInvitationEmailBudgetIdCompoundUniqueInputSchema),
}),
z.object({
id: z.string().cuid(),
}),
z.object({
token_budgetId: z.lazy(() => BudgetUserInvitationTokenBudgetIdCompoundUniqueInputSchema),
email_budgetId: z.lazy(() => BudgetUserInvitationEmailBudgetIdCompoundUniqueInputSchema),
}),
z.object({
token_budgetId: z.lazy(() => BudgetUserInvitationTokenBudgetIdCompoundUniqueInputSchema),
}),
z.object({
email_budgetId: z.lazy(() => BudgetUserInvitationEmailBudgetIdCompoundUniqueInputSchema),
}),
])
.and(z.object({
id: z.string().cuid().optional(),
token_budgetId: z.lazy(() => BudgetUserInvitationTokenBudgetIdCompoundUniqueInputSchema).optional(),
email_budgetId: z.lazy(() => BudgetUserInvitationEmailBudgetIdCompoundUniqueInputSchema).optional(),
AND: z.union([ z.lazy(() => BudgetUserInvitationWhereInputSchema),z.lazy(() => BudgetUserInvitationWhereInputSchema).array() ]).optional(),
OR: z.lazy(() => BudgetUserInvitationWhereInputSchema).array().optional(),
NOT: z.union([ z.lazy(() => BudgetUserInvitationWhereInputSchema),z.lazy(() => BudgetUserInvitationWhereInputSchema).array() ]).optional(),
Expand Down Expand Up @@ -2601,6 +2618,11 @@ export const BudgetUserInvitationTokenBudgetIdCompoundUniqueInputSchema: z.ZodTy
budgetId: z.string()
}).strict();

export const BudgetUserInvitationEmailBudgetIdCompoundUniqueInputSchema: z.ZodType<Prisma.BudgetUserInvitationEmailBudgetIdCompoundUniqueInput> = z.object({
email: z.string(),
budgetId: z.string()
}).strict();

export const BudgetUserInvitationCountOrderByAggregateInputSchema: z.ZodType<Prisma.BudgetUserInvitationCountOrderByAggregateInput> = z.object({
id: z.lazy(() => SortOrderSchema).optional(),
createdAt: z.lazy(() => SortOrderSchema).optional(),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/*
Warnings:
- A unique constraint covering the columns `[email,budgetId]` on the table `BudgetUserInvitation` will be added. If there are existing duplicate values, this will fail.
*/
-- CreateIndex
CREATE UNIQUE INDEX "BudgetUserInvitation_email_budgetId_key" ON "BudgetUserInvitation"("email", "budgetId");
1 change: 1 addition & 0 deletions apps/api/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ model BudgetUserInvitation {
responses BudgetUserInvitationResponse[]
@@unique([token, budgetId])
@@unique([email, budgetId])
}

model BudgetUserInvitationResponse {
Expand Down
168 changes: 167 additions & 1 deletion apps/api/v1/services/budget-invitation.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import {
BudgetUserPermission,
type User,
} from '@prisma/client'
import { isUserBudgetOwner } from './budget.service'
import type { CreateUser } from '../validation'
import { createBudgetUser, isUserBudgetOwner } from './budget.service'
import { createUser, findUserByEmail } from './user.service'

export async function canUserInviteUserToBudget({
user,
Expand All @@ -17,6 +19,16 @@ export async function canUserInviteUserToBudget({
return isUserBudgetOwner({ user, budget })
}

export async function canUserReadBudgetInvitations({
user,
budget,
}: {
user: User
budget: Budget
}): Promise<boolean> {
return isUserBudgetOwner({ user, budget })
}

export async function inviteUserToBudget({
inviter,
budget,
Expand Down Expand Up @@ -62,3 +74,157 @@ export async function inviteUserToBudget({

return invitation
}

export async function canUserGenerateBudgetInvitation({
user,
budget,
}: {
user: User
budget: Budget
}): Promise<boolean> {
return isUserBudgetOwner({ user, budget })
}

export async function generateBudgetInvitation({
budgetId,
userId,
}: { budgetId: string; userId: string }) {
let invitation = await prisma.budgetUserInvitation.findFirst({
where: {
budgetId,
email: null,
},
})

if (invitation) {
invitation = await prisma.budgetUserInvitation.update({
where: { id: invitation.id },
data: {
expiresAt: new Date(Date.now() + 5 * 365 * 24 * 60 * 60 * 1000), // 5 years
token: Math.random().toString(36).substring(2), // uuid-like
},
})

return invitation
}

invitation = await prisma.budgetUserInvitation.create({
data: {
budgetId,
token: Math.random().toString(36).substring(2), // uuid-like
createdByUserId: userId,
expiresAt: new Date(Date.now() + 5 * 365 * 24 * 60 * 60 * 1000), // 5 years
permission: BudgetUserPermission.MEMBER,
},
})

return invitation
}

export async function findBudgetInvitations({
budgetId,
permission,
}: { budgetId: string; permission?: BudgetUserPermission }) {
return prisma.budgetUserInvitation.findMany({
where: {
budgetId,
permission,
},
})
}

export async function canUserDeleteBudgetInvitation({
user,
invitation,
}: {
user: User
invitation: BudgetUserInvitation
}): Promise<boolean> {
const budget = await prisma.budget.findUnique({
where: { id: invitation.budgetId },
})

if (!budget) {
return false
}

return isUserBudgetOwner({ user, budget })
}

export async function deleteBudgetInvitation({
invitationId,
}: {
invitationId: string
}) {
return prisma.budgetUserInvitation.delete({
where: { id: invitationId },
})
}

export async function verifyBudgetInvitationToken({
token,
}: {
token: string
}): Promise<BudgetUserInvitation | null> {
const invitation = await prisma.budgetUserInvitation.findFirst({
where: {
token,
},
})

if (!invitation) {
return null
}

if (invitation.expiresAt < new Date()) {
return null
}

return invitation
}

export async function respondToBudgetInvitation({
invitation,
accept,
userData,
}: {
invitation: BudgetUserInvitation
accept: boolean
userData: CreateUser
}) {
if (!accept) {
const declinedAt = new Date()

await prisma.budgetUserInvitationResponse.create({
data: {
invitationId: invitation.id,
declinedAt,
},
})

return { accepted: false, declinedAt }
}

// Create find or new user
const user =
(await findUserByEmail(userData.email)) || (await createUser(userData))

// Create invitation response
const acceptedAt = new Date()
await prisma.budgetUserInvitationResponse.create({
data: {
invitationId: invitation.id,
createdUserId: user.id,
acceptedAt,
},
})

// Add user to budget
const budgetUser = await createBudgetUser({
userId: user.id,
budgetId: invitation.budgetId,
permission: invitation.permission || BudgetUserPermission.MEMBER,
})

return { accepted: true, acceptedAt, user, budgetUser }
}

0 comments on commit 4daa0fe

Please sign in to comment.