Skip to content

Commit

Permalink
feat(wallet): ask for user password on certain card actions (#1770)
Browse files Browse the repository at this point in the history
Ask for password on certain card actions
  • Loading branch information
raducristianpopa authored Oct 24, 2024
1 parent 64b6ac4 commit fc4cc3a
Show file tree
Hide file tree
Showing 10 changed files with 231 additions and 99 deletions.
4 changes: 2 additions & 2 deletions packages/wallet/backend/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,7 @@ export class App {

// Cards
router.get('/customers/cards', isAuth, cardController.getCardsByCustomer)
router.get('/cards/:cardId/details', isAuth, cardController.getCardDetails)
router.post('/cards/:cardId/details', isAuth, cardController.getCardDetails)
router.get(
'/cards/:cardId/transactions',
this.ensureGateHubProductionEnv,
Expand All @@ -348,7 +348,7 @@ export class App {
isAuth,
cardController.createOrOverrideCardLimits
)
router.get(
router.post(
'/cards/:cardId/pin',
this.ensureGateHubProductionEnv,
isAuth,
Expand Down
13 changes: 10 additions & 3 deletions packages/wallet/backend/src/card/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,16 +83,18 @@ export class CardController implements ICardController {
) => {
try {
const userId = req.session.user.id
const { params, query } = await validate(getCardDetailsSchema, req)
const { params, query, body } = await validate(getCardDetailsSchema, req)
const { cardId } = params
const { publicKeyBase64 } = query
const { password } = body

const requestBody: ICardDetailsRequest = {
cardId,
publicKey: publicKeyBase64
}
const cardDetails = await this.cardService.getCardDetails(
userId,
password,
requestBody
)
res.status(200).json(toSuccessResponse(cardDetails))
Expand Down Expand Up @@ -171,15 +173,20 @@ export class CardController implements ICardController {
public getPin = async (req: Request, res: Response, next: NextFunction) => {
try {
const userId = req.session.user.id
const { params, query } = await validate(getCardDetailsSchema, req)
const { params, query, body } = await validate(getCardDetailsSchema, req)
const { cardId } = params
const { publicKeyBase64 } = query
const { password } = body

const requestBody: ICardDetailsRequest = {
cardId,
publicKey: publicKeyBase64
}
const cardPin = await this.cardService.getPin(userId, requestBody)
const cardPin = await this.cardService.getPin(
userId,
password,
requestBody
)
res.status(200).json(toSuccessResponse(cardPin))
} catch (error) {
next(error)
Expand Down
34 changes: 24 additions & 10 deletions packages/wallet/backend/src/card/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
} from './types'
import { IGetTransactionsResponse } from '@wallet/shared/src'
import { LockReasonCode } from '@wallet/shared/src'
import { InternalServerError, NotFound } from '@shared/backend'
import { BadRequest, InternalServerError, NotFound } from '@shared/backend'
import { AccountService } from '@/account/service'
import { ICardResponse } from '@wallet/shared'
import { UserService } from '@/user/service'
Expand All @@ -33,7 +33,7 @@ export class CardService {
if (!user) {
throw new NotFound()
}
const gateHubUserId = await this.ensureGatehubUserUuid(userId)
const { gateHubUserId } = await this.ensureGatehubUserUuid(userId)

const cards = await this.gateHubClient.getCardsByCustomer(
customerId,
Expand All @@ -50,12 +50,18 @@ export class CardService {

async getCardDetails(
userId: string,
password: string,
requestBody: ICardDetailsRequest
): Promise<ICardDetailsResponse> {
const { cardId } = requestBody
await this.ensureAccountExists(userId, cardId)

const gateHubUserId = await this.ensureGatehubUserUuid(userId)
const { user, gateHubUserId } = await this.ensureGatehubUserUuid(userId)

const isValidPassword = await user.verifyPassword(password)
if (!isValidPassword) {
throw new BadRequest('Password is not valid')
}

return await this.gateHubClient.getCardDetails(gateHubUserId, requestBody)
}
Expand Down Expand Up @@ -92,19 +98,25 @@ export class CardService {

async getPin(
userId: string,
password: string,
requestBody: ICardDetailsRequest
): Promise<ICardDetailsResponse> {
const { cardId } = requestBody
await this.ensureAccountExists(userId, cardId)
const gateHubUserId = await this.ensureGatehubUserUuid(userId)
const { user, gateHubUserId } = await this.ensureGatehubUserUuid(userId)

const isValidPassword = await user.verifyPassword(password)
if (!isValidPassword) {
throw new BadRequest('Password is not valid')
}

return this.gateHubClient.getPin(gateHubUserId, requestBody)
}

async getTokenForPinChange(userId: string, cardId: string): Promise<string> {
await this.ensureAccountExists(userId, cardId)

const gateHubUserId = await this.ensureGatehubUserUuid(userId)
const { gateHubUserId } = await this.ensureGatehubUserUuid(userId)
const token = await this.gateHubClient.getTokenForPinChange(
gateHubUserId,
cardId
Expand Down Expand Up @@ -134,7 +146,7 @@ export class CardService {
): Promise<ICardResponse> {
await this.ensureAccountExists(userId, cardId)

const gateHubUserId = await this.ensureGatehubUserUuid(userId)
const { gateHubUserId } = await this.ensureGatehubUserUuid(userId)

return this.gateHubClient.lockCard(
cardId,
Expand All @@ -151,7 +163,7 @@ export class CardService {
): Promise<ICardResponse> {
await this.ensureAccountExists(userId, cardId)

const gateHubUserId = await this.ensureGatehubUserUuid(userId)
const { gateHubUserId } = await this.ensureGatehubUserUuid(userId)

return this.gateHubClient.unlockCard(cardId, gateHubUserId, requestBody)
}
Expand All @@ -163,7 +175,7 @@ export class CardService {
): Promise<void> {
await this.ensureAccountExists(userId, cardId)

const gateHubUserId = await this.ensureGatehubUserUuid(userId)
const { gateHubUserId } = await this.ensureGatehubUserUuid(userId)

await this.gateHubClient.closeCard(gateHubUserId, cardId, reasonCode)
}
Expand All @@ -178,7 +190,9 @@ export class CardService {
}
}

private async ensureGatehubUserUuid(userId: string): Promise<string> {
private async ensureGatehubUserUuid(
userId: string
): Promise<{ user: User; gateHubUserId: string }> {
const user = await this.userService.getById(userId)

if (!user) {
Expand All @@ -191,6 +205,6 @@ export class CardService {
throw new InternalServerError()
}

return user.gateHubUserId
return { user, gateHubUserId: user.gateHubUserId }
}
}
3 changes: 3 additions & 0 deletions packages/wallet/backend/src/card/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ export const getCardDetailsSchema = z.object({
}),
query: z.object({
publicKeyBase64: z.string()
}),
body: z.object({
password: z.string()
})
})

Expand Down
77 changes: 77 additions & 0 deletions packages/wallet/frontend/src/components/dialogs/PasswordDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import {
Dialog,
DialogPanel,
DialogTitle,
Transition,
TransitionChild
} from '@headlessui/react'
import { Fragment } from 'react'
import type { DialogProps } from '@/lib/types/dialog'
import { useZodForm } from '@/lib/hooks/useZodForm'
import { passwordSchema } from '@/lib/api/user'
import { Form } from '@/ui/forms/Form'
import { Input } from '@/ui/forms/Input'
import { Button } from '@/ui/Button'

type PasswordDialogProps = Pick<DialogProps, 'onClose'> & {
title: string
onSubmit: (password: string) => Promise<void>
}

export const PasswordDialog = ({
title,
onClose,
onSubmit
}: PasswordDialogProps) => {
const form = useZodForm({
schema: passwordSchema
})
return (
<Transition show={true} as={Fragment} appear={true}>
<Dialog as="div" className="relative z-10" onClose={onClose}>
<TransitionChild
as={Fragment}
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-green-modal/75 transition-opacity dark:bg-black/75" />
</TransitionChild>

<div className="fixed inset-0 z-10 overflow-y-auto">
<div className="flex min-h-full items-center justify-center p-4">
<TransitionChild
as={Fragment}
leave="ease-in duration-200"
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-4"
>
<DialogPanel className="relative w-full max-w-sm space-y-4 overflow-hidden rounded-lg bg-white p-8 shadow-xl dark:bg-purple">
<DialogTitle as="h3" className="text-center text-2xl font-bold">
{title}
</DialogTitle>
<Form
form={form}
onSubmit={async (data) => {
await onSubmit(data.password)
}}
>
<Input
required
type="password"
{...form.register('password')}
error={form.formState.errors.password?.message}
label="Password"
/>
<Button type="submit" intent="primary" aria-label={title}>
Submit
</Button>
</Form>
</DialogPanel>
</TransitionChild>
</div>
</div>
</Dialog>
</Transition>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import type { DialogProps } from '@/lib/types/dialog'
import { UserCardFront } from '@/components/userCards/UserCard'
import { ICardResponse } from '@wallet/shared'

type UserCardPINDialogProos = Pick<DialogProps, 'onClose'> & {
type UserCardPINDialogProps = Pick<DialogProps, 'onClose'> & {
card: ICardResponse
pin: string
}
Expand All @@ -19,7 +19,7 @@ export const UserCardPINDialog = ({
card,
pin,
onClose
}: UserCardPINDialogProos) => {
}: UserCardPINDialogProps) => {
// Initial time in seconds (1 hour)
const initialTime = 10
const [timeRemaining, setTimeRemaining] = useState(initialTime)
Expand Down Expand Up @@ -51,9 +51,6 @@ export const UserCardPINDialog = ({
<Dialog as="div" className="relative z-10" onClose={onClose}>
<TransitionChild
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
Expand All @@ -65,9 +62,6 @@ export const UserCardPINDialog = ({
<div className="flex min-h-full items-center justify-center p-4">
<TransitionChild
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 translate-y-4"
enterTo="opacity-100 translate-y-0"
leave="ease-in duration-200"
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-4"
Expand Down
Loading

0 comments on commit fc4cc3a

Please sign in to comment.