Skip to content

Commit 237b91a

Browse files
authored
feat(wallet/backend): add endpoints for view/change pin (#1659)
* Add getPin endpoint * Add changePin endpoint, tests * Fix tests * Small fixes * Update status code for change pin
1 parent d3e3236 commit 237b91a

6 files changed

Lines changed: 318 additions & 16 deletions

File tree

packages/wallet/backend/src/app.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,8 @@ export class App {
315315
router.get('/cards/:cardId/details', isAuth, cardController.getCardDetails)
316316
router.put('/cards/:cardId/lock', isAuth, cardController.lock)
317317
router.put('/cards/:cardId/unlock', isAuth, cardController.unlock)
318+
router.get('/cards/:cardId/pin', isAuth, cardController.getPin)
319+
router.post('/cards/:cardId/change-pin', isAuth, cardController.changePin)
318320

319321
// Return an error for invalid routes
320322
router.use('*', (req: Request, res: CustomResponse) => {

packages/wallet/backend/src/card/controller.ts

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,17 @@ import {
1414
getCardsByCustomerSchema,
1515
getCardDetailsSchema,
1616
lockCardSchema,
17-
unlockCardSchema
17+
unlockCardSchema,
18+
changePinSchema
1819
} from './validation'
1920

2021
export interface ICardController {
2122
getCardsByCustomer: Controller<ICardDetailsResponse[]>
2223
getCardDetails: Controller<ICardResponse>
2324
lock: Controller<ICardResponse>
2425
unlock: Controller<ICardResponse>
26+
getPin: Controller<ICardResponse>
27+
changePin: Controller<void>
2528
}
2629

2730
export class CardController implements ICardController {
@@ -50,9 +53,9 @@ export class CardController implements ICardController {
5053
) => {
5154
try {
5255
const userId = req.session.user.id
53-
const { params, body } = await validate(getCardDetailsSchema, req)
56+
const { params, query } = await validate(getCardDetailsSchema, req)
5457
const { cardId } = params
55-
const { publicKeyBase64 } = body
58+
const { publicKeyBase64 } = query
5659

5760
const requestBody: ICardDetailsRequest = { cardId, publicKeyBase64 }
5861
const cardDetails = await this.cardService.getCardDetails(
@@ -97,4 +100,37 @@ export class CardController implements ICardController {
97100
next(error)
98101
}
99102
}
103+
104+
public getPin = async (req: Request, res: Response, next: NextFunction) => {
105+
try {
106+
const userId = req.session.user.id
107+
const { params, query } = await validate(getCardDetailsSchema, req)
108+
const { cardId } = params
109+
const { publicKeyBase64 } = query
110+
111+
const requestBody: ICardDetailsRequest = { cardId, publicKeyBase64 }
112+
const cardPin = await this.cardService.getPin(userId, requestBody)
113+
res.status(200).json(toSuccessResponse(cardPin))
114+
} catch (error) {
115+
next(error)
116+
}
117+
}
118+
119+
public changePin = async (
120+
req: Request,
121+
res: Response,
122+
next: NextFunction
123+
) => {
124+
try {
125+
const userId = req.session.user.id
126+
const { params, body } = await validate(changePinSchema, req)
127+
const { cardId } = params
128+
const { cypher } = body
129+
130+
const result = await this.cardService.changePin(userId, cardId, cypher)
131+
res.status(201).json(toSuccessResponse(result))
132+
} catch (error) {
133+
next(error)
134+
}
135+
}
100136
}

packages/wallet/backend/src/card/service.ts

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,18 +25,31 @@ export class CardService {
2525
requestBody: ICardDetailsRequest
2626
): Promise<ICardDetailsResponse> {
2727
const { cardId } = requestBody
28-
29-
const walletAddress = await this.walletAddressService.getByCardId(
30-
userId,
31-
cardId
32-
)
33-
if (!walletAddress) {
34-
throw new NotFound('Card not found or not associated with the user.')
35-
}
28+
await this.ensureWalletAddressExists(userId, cardId)
3629

3730
return this.gateHubClient.getCardDetails(requestBody)
3831
}
3932

33+
async getPin(
34+
userId: string,
35+
requestBody: ICardDetailsRequest
36+
): Promise<ICardDetailsResponse> {
37+
const { cardId } = requestBody
38+
await this.ensureWalletAddressExists(userId, cardId)
39+
40+
return this.gateHubClient.getPin(requestBody)
41+
}
42+
43+
async changePin(
44+
userId: string,
45+
cardId: string,
46+
cypher: string
47+
): Promise<void> {
48+
await this.ensureWalletAddressExists(userId, cardId)
49+
50+
await this.gateHubClient.changePin(cardId, cypher)
51+
}
52+
4053
async lock(
4154
cardId: string,
4255
reasonCode: LockReasonCode,
@@ -51,4 +64,17 @@ export class CardService {
5164
): Promise<ICardResponse> {
5265
return this.gateHubClient.unlockCard(cardId, requestBody)
5366
}
67+
68+
private async ensureWalletAddressExists(
69+
userId: string,
70+
cardId: string
71+
): Promise<void> {
72+
const walletAddress = await this.walletAddressService.getByCardId(
73+
userId,
74+
cardId
75+
)
76+
if (!walletAddress) {
77+
throw new NotFound('Card not found or not associated with the user.')
78+
}
79+
}
5480
}

packages/wallet/backend/src/card/validation.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export const getCardDetailsSchema = z.object({
1010
params: z.object({
1111
cardId: z.string()
1212
}),
13-
body: z.object({
13+
query: z.object({
1414
publicKeyBase64: z.string()
1515
})
1616
})
@@ -42,3 +42,12 @@ export const unlockCardSchema = z.object({
4242
note: z.string()
4343
})
4444
})
45+
46+
export const changePinSchema = z.object({
47+
params: z.object({
48+
cardId: z.string()
49+
}),
50+
body: z.object({
51+
cypher: z.string()
52+
})
53+
})

packages/wallet/backend/src/gatehub/client.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,70 @@ export class GateHubClient {
411411
)
412412
}
413413

414+
async getPin(
415+
requestBody: ICardDetailsRequest
416+
): Promise<ICardDetailsResponse> {
417+
const url = `${this.apiUrl}/token/pin`
418+
419+
const response = await this.request<ILinksResponse>(
420+
'POST',
421+
url,
422+
JSON.stringify(requestBody),
423+
{
424+
cardAppId: this.env.GATEHUB_CARD_APP_ID
425+
}
426+
)
427+
428+
const token = response.token
429+
if (!token) {
430+
throw new Error('Failed to obtain token for card pin retrieval')
431+
}
432+
433+
// TODO change this to direct call to card managing entity
434+
// Will get this from the GateHub proxy for now
435+
const cardPinUrl = `${this.apiUrl}/v1/proxy/client-device/pin`
436+
const cardPinResponse = await this.request<ICardDetailsResponse>(
437+
'GET',
438+
cardPinUrl,
439+
undefined,
440+
{
441+
token
442+
}
443+
)
444+
445+
return cardPinResponse
446+
}
447+
448+
async changePin(cardId: string, cypher: string): Promise<void> {
449+
const url = `${this.apiUrl}/token/pin-change`
450+
451+
const response = await this.request<ILinksResponse>(
452+
'POST',
453+
url,
454+
JSON.stringify({ cardId: cardId }),
455+
{
456+
cardAppId: this.env.GATEHUB_CARD_APP_ID
457+
}
458+
)
459+
460+
const token = response.token
461+
if (!token) {
462+
throw new Error('Failed to obtain token for card pin retrieval')
463+
}
464+
465+
// TODO change this to direct call to card managing entity
466+
// Will get this from the GateHub proxy for now
467+
const cardPinUrl = `${this.apiUrl}/v1/proxy/client-device/pin`
468+
await this.request<void>(
469+
'POST',
470+
cardPinUrl,
471+
JSON.stringify({ cypher: cypher }),
472+
{
473+
token
474+
}
475+
)
476+
}
477+
414478
private async request<T>(
415479
method: HTTP_METHODS,
416480
url: string,

0 commit comments

Comments
 (0)