Skip to content

Commit

Permalink
feat(api): add budget invitation APIs (#22)
Browse files Browse the repository at this point in the history
  • Loading branch information
duongdev committed Jun 8, 2024
1 parent 4daa0fe commit c778017
Show file tree
Hide file tree
Showing 2 changed files with 183 additions and 12 deletions.
185 changes: 173 additions & 12 deletions apps/api/v1/routes/budgets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,18 @@ import { BudgetUserPermissionSchema } from '@/prisma/generated/zod'
import { zValidator } from '@hono/zod-validator'
import { Hono } from 'hono'
import { z } from 'zod'
import { getAuthUserStrict } from '../middlewares/auth'
import { getAuthUser, getAuthUserStrict } from '../middlewares/auth'
import {
canUserDeleteBudgetInvitation,
canUserGenerateBudgetInvitation,
canUserInviteUserToBudget,
deleteBudgetInvitation,
findBudgetInvitation,
generateBudgetInvitation,
inviteUserToBudget,
respondToBudgetInvitation,
verifyBudgetInvitationToken,
} from '../services/budget-invitation.service'
import {
canUserCreateBudget,
canUserDeleteBudget,
Expand All @@ -14,14 +25,22 @@ import {
findBudgetsOfUser,
updateBudget,
} from '../services/budget.service'
import { zCreateBudget, zUpdateBudget } from '../validation'
import { zCreateBudget, zCreateUser, zUpdateBudget } from '../validation'

const router = new Hono()

const zBudgetIdParamValidator = zValidator(
const zBudgetParamValidator = zValidator(
'param',
z.object({
budgetId: z.string(),
}),
)

const zInvitationParamValidator = zValidator(
'param',
z.object({
budgetId: z.string(),
invitationId: z.string(),
}),
)

Expand All @@ -47,7 +66,7 @@ router.post('/', zValidator('json', zCreateBudget), async (c) => {
const user = getAuthUserStrict(c)

if (!(await canUserCreateBudget({ user }))) {
return c.json({ message: 'User cannot create budget' }, 403)
return c.json({ message: 'user cannot create budget' }, 403)
}

const createBudgetData = c.req.valid('json')
Expand All @@ -57,22 +76,22 @@ router.post('/', zValidator('json', zCreateBudget), async (c) => {
return c.json(budget, 201)
})

router.get('/:budgetId', zBudgetIdParamValidator, async (c) => {
router.get('/:budgetId', zBudgetParamValidator, async (c) => {
const user = getAuthUserStrict(c)
const { budgetId } = c.req.valid('param')

const budget = await findBudget({ budgetId })

if (!(budget && (await canUserReadBudget({ user, budget })))) {
return c.json(null, 404)
return c.json({ message: 'budget not found' }, 404)
}

return c.json(budget)
})

router.put(
'/:budgetId',
zBudgetIdParamValidator,
zBudgetParamValidator,
zValidator('json', zUpdateBudget),
async (c) => {
const user = getAuthUserStrict(c)
Expand All @@ -81,11 +100,11 @@ router.put(
const budget = await findBudget({ budgetId })

if (!(budget && (await canUserReadBudget({ user, budget })))) {
return c.json(null, 404)
return c.json({ message: 'budget not found' }, 404)
}

if (!(await canUserUpdateBudget({ user, budget }))) {
return c.json({ message: 'User cannot update budget' }, 403)
return c.json({ message: 'user cannot update budget' }, 403)
}

const updateBudgetData = c.req.valid('json')
Expand All @@ -99,23 +118,165 @@ router.put(
},
)

router.delete('/:budgetId', zBudgetIdParamValidator, async (c) => {
router.delete('/:budgetId', zBudgetParamValidator, async (c) => {
const user = getAuthUserStrict(c)
const { budgetId } = c.req.valid('param')

const budget = await findBudget({ budgetId })

if (!(budget && (await canUserReadBudget({ user, budget })))) {
return c.json(null, 404)
return c.json({ message: 'budget not found' }, 404)
}

if (!(await canUserDeleteBudget({ user, budget }))) {
return c.json({ message: 'User cannot delete budget' }, 403)
return c.json({ message: 'user cannot delete budget' }, 403)
}

await deleteBudget({ budgetId })

return c.json(budget)
})

/** Generate sharable invitation link */
router.post(
'/:budgetId/invitations/generate',
zBudgetParamValidator,
async (c) => {
const user = getAuthUserStrict(c)
const { budgetId } = c.req.valid('param')

const budget = await findBudget({ budgetId })

if (!(budget && (await canUserReadBudget({ user, budget })))) {
return c.json({ message: 'budget not found' }, 404)
}

if (!(await canUserGenerateBudgetInvitation({ user, budget }))) {
return c.json(
{ message: 'user cannot generate invite link to budget' },
403,
)
}

const invitation = await generateBudgetInvitation({
budgetId,
userId: user.id,
})

return c.json(invitation)
},
)

/** Invite user to budget by email */
router.post(
'/:budgetId/invitations',
zBudgetParamValidator,
zValidator(
'json',
z.object({
email: z.string().email(),
permission: BudgetUserPermissionSchema.optional(),
}),
),
async (c) => {
const user = getAuthUserStrict(c)
const { budgetId } = c.req.valid('param')

const budget = await findBudget({ budgetId })

if (!(budget && (await canUserReadBudget({ user, budget })))) {
return c.json({ message: 'budget not found' }, 404)
}

if (!(await canUserInviteUserToBudget({ user, budget }))) {
return c.json({ message: 'user cannot invite users to this budget' }, 403)
}

const { email, permission } = c.req.valid('json')

const invitation = await inviteUserToBudget({
inviter: user,
budget,
email,
permission,
})

return c.json(invitation, 201)
},
)

/** Delete/revoke invitation */
router.delete(
'/:budgetId/invitations/:invitationId',
zInvitationParamValidator,
async (c) => {
const user = getAuthUserStrict(c)
const { budgetId, invitationId } = c.req.valid('param')

const budget = await findBudget({ budgetId })

if (!(budget && (await canUserReadBudget({ user, budget })))) {
return c.json({ message: 'budget not found' }, 404)
}

const invitation = await findBudgetInvitation({ invitationId })

if (!invitation) {
return c.json({ message: 'invitation not found' }, 404)
}

if (!(await canUserDeleteBudgetInvitation({ user, invitation }))) {
return c.json({ message: 'user cannot delete this invitation' }, 403)
}

await deleteBudgetInvitation({ invitationId })

return c.json(invitation)
},
)

/** Join budget with token */
router.post(
'/response-invitation',
zValidator(
'json',
z.object({
token: z.string(),
userData: zCreateUser.optional(),
accept: z.boolean(),
}),
),
async (c) => {
const user = getAuthUser(c)
const { token, userData, accept } = c.req.valid('json')

if (!user && !userData) {
return c.json({ message: 'user data is required' }, 400)
}

const invitation = await verifyBudgetInvitationToken({ token })

if (!invitation) {
return c.json(
{
message: 'invalid or expired invitation token',
},
404,
)
}

const response = await respondToBudgetInvitation({
invitation,
accept,
userData: {
id: user?.id,
email: (invitation.email ?? user?.email ?? userData?.email)!,
name: (user?.name ?? userData?.name)!,
},
})

return c.json(response)
},
)

export default router
10 changes: 10 additions & 0 deletions apps/api/v1/services/budget-invitation.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,16 @@ export async function generateBudgetInvitation({
return invitation
}

export async function findBudgetInvitation({
invitationId,
}: {
invitationId: string
}): Promise<BudgetUserInvitation | null> {
return prisma.budgetUserInvitation.findUnique({
where: { id: invitationId },
})
}

export async function findBudgetInvitations({
budgetId,
permission,
Expand Down

0 comments on commit c778017

Please sign in to comment.