Skip to content

Commit 8cd72b4

Browse files
committed
feat: add API to delete shared device
See hello-nrfcloud/map#351
1 parent dfed1a8 commit 8cd72b4

File tree

7 files changed

+202
-45
lines changed

7 files changed

+202
-45
lines changed

cdk/BackendStack.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ export class BackendStack extends Stack {
146146
'PUT /user/device/{id}/sharing',
147147
shareAPI.extendDeviceSharingFn,
148148
)
149+
api.addRoute('DELETE /user/device/{id}', shareAPI.stopDeviceSharingFn)
149150

150151
const devicesAPI = new DevicesAPI(this, {
151152
baseLayer,

cdk/packBackendLambdas.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export type BackendLambdas = {
99
createDevice: PackedLambda
1010
listUserDevices: PackedLambda
1111
extendDeviceSharing: PackedLambda
12+
stopDeviceSharing: PackedLambda
1213
openSSL: PackedLambda
1314
apiHealthCheck: PackedLambda
1415
createCNAMERecord: PackedLambda
@@ -30,6 +31,7 @@ export const packBackendLambdas = async (): Promise<BackendLambdas> => ({
3031
createDevice: await pack('createDevice'),
3132
listUserDevices: await pack('listUserDevices'),
3233
extendDeviceSharing: await pack('extendDeviceSharing'),
34+
stopDeviceSharing: await pack('stopDeviceSharing'),
3335
openSSL: await pack('openSSL'),
3436
apiHealthCheck: await pack('apiHealthCheck'),
3537
createCNAMERecord: await packLambdaFromPath({

cdk/resources/ShareAPI.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export class ShareAPI extends Construct {
1111
public readonly sharingStatusFingerprintFn: Lambda.IFunction
1212
public readonly listUserDevicesFn: Lambda.IFunction
1313
public readonly extendDeviceSharingFn: Lambda.IFunction
14+
public readonly stopDeviceSharingFn: Lambda.IFunction
1415
constructor(
1516
parent: Construct,
1617
{
@@ -30,6 +31,7 @@ export class ShareAPI extends Construct {
3031
| 'deviceJwt'
3132
| 'listUserDevices'
3233
| 'extendDeviceSharing'
34+
| 'stopDeviceSharing'
3335
>
3436
},
3537
) {
@@ -132,5 +134,22 @@ export class ShareAPI extends Construct {
132134
publicDevices.publicDevicesTable.grantReadWriteData(
133135
this.extendDeviceSharingFn,
134136
)
137+
138+
this.stopDeviceSharingFn = new PackedLambdaFn(
139+
this,
140+
'stopDeviceSharingFn',
141+
lambdaSources.stopDeviceSharing,
142+
{
143+
description: 'Stop sharing a device',
144+
layers: [baseLayer],
145+
environment: {
146+
PUBLIC_DEVICES_TABLE_NAME: publicDevices.publicDevicesTable.tableName,
147+
PUBLIC_DEVICES_ID_INDEX_NAME: publicDevices.publicDevicesTableIdIndex,
148+
},
149+
},
150+
).fn
151+
publicDevices.publicDevicesTable.grantReadWriteData(
152+
this.stopDeviceSharingFn,
153+
)
135154
}
136155
}

features/StopSharing.feature.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
---
2+
exampleContext:
3+
API: https://api.nordicsemi.world/2024-04-15
4+
jwt: eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCIsImtpZCI6Ijg1NDdiNWIyLTdiNDctNDFlNC1iZjJkLTdjZGZmNDhiM2VhNCJ9.eyJAY29udGV4dCI6Imh0dHBzOi8vZ2l0aHViLmNvbS9oZWxsby1ucmZjbG91ZC9wcm90by1tYXAvdXNlci1qd3QiLCJlbWFpbCI6ImVkYjJiZDM3QGV4YW1wbGUuY29tIiwiaWF0IjoxNzIyODcxNTYyLCJleHAiOjE3MjI5NTc5NjIsImF1ZCI6ImhlbGxvLm5yZmNsb3VkLmNvbSJ9.ALiHjxR7HIjuYQBvPVh5-GMs-2f-pMGs_FTz-x0HGzQ4amLASeUGEZ7X_y-_mgZpYu8VKGm6be0LtIIx9DgYBff1ASfmQH327rub0a2-DjXW-JUJQn_6t6H6_JhvPZ9jWBSzy3Tbpp9NmTUNmHgEwzyoctnmgp0oo26VEwc4r6YGQWkZ
5+
publicDeviceId: outfling-swanherd-attaghan
6+
oldExpiry: "2024-08-13T08:56:51.280Z"
7+
needs:
8+
- Extend sharing
9+
---
10+
11+
# Stop sharing
12+
13+
> Users can stop sharing their device.
14+
15+
## List devices
16+
17+
Given the `Authorization` header of the next request is `Bearer ${jwt}`
18+
19+
When I `GET` `${API}/user/devices`
20+
21+
Then I store `$.devices[0].id` of the last response into `publicDeviceId`
22+
23+
## Extend sharing
24+
25+
Given the `Authorization` header of the next request is `Bearer ${jwt}`
26+
27+
When I `DELETE` to `${API}/user/device/${publicDeviceId}`
28+
29+
Then the status code of the last response should be `202`
30+
31+
## List devices again
32+
33+
> The device should have been deleted
34+
35+
Given the `Authorization` header of the next request is `Bearer ${jwt}`
36+
37+
When I `GET` `${API}/user/devices`
38+
39+
Then the status code of the last response should be `200`
40+
41+
And I should receive a
42+
`https://github.com/hello-nrfcloud/proto-map/user-devices` response
43+
44+
And `{"len": $count($.devices)}` of the last response should match
45+
46+
```json
47+
{ "len": 0 }
48+
```

lambda/stopDeviceSharing.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { DeleteItemCommand, DynamoDBClient } from '@aws-sdk/client-dynamodb'
2+
import { SSMClient } from '@aws-sdk/client-ssm'
3+
import { marshall } from '@aws-sdk/util-dynamodb'
4+
import { fromEnv } from '@bifravst/from-env'
5+
import { addVersionHeader } from '@hello.nrfcloud.com/lambda-helpers/addVersionHeader'
6+
import { aResponse } from '@hello.nrfcloud.com/lambda-helpers/aResponse'
7+
import { corsOPTIONS } from '@hello.nrfcloud.com/lambda-helpers/corsOPTIONS'
8+
import {
9+
ProblemDetailError,
10+
problemResponse,
11+
} from '@hello.nrfcloud.com/lambda-helpers/problemResponse'
12+
import { requestLogger } from '@hello.nrfcloud.com/lambda-helpers/requestLogger'
13+
import {
14+
validateInput,
15+
type ValidInput,
16+
} from '@hello.nrfcloud.com/lambda-helpers/validateInput'
17+
import { PublicDeviceId } from '@hello.nrfcloud.com/proto-map/api'
18+
import middy from '@middy/core'
19+
import { Type } from '@sinclair/typebox'
20+
import type {
21+
APIGatewayProxyEventV2,
22+
APIGatewayProxyResultV2,
23+
Context as LambdaContext,
24+
} from 'aws-lambda'
25+
import { STACK_NAME } from '../cdk/stackConfig.js'
26+
import { getDeviceId } from '../devices/getDeviceId.js'
27+
import { verifyUserToken } from '../jwt/verifyUserToken.js'
28+
import { getSettings } from '../settings/jwt.js'
29+
import { withUser, type WithUser } from './middleware/withUser.js'
30+
31+
const { stackName, TableName, idIndex, version } = fromEnv({
32+
stackName: 'STACK_NAME',
33+
TableName: 'PUBLIC_DEVICES_TABLE_NAME',
34+
idIndex: 'PUBLIC_DEVICES_ID_INDEX_NAME',
35+
version: 'VERSION',
36+
})({
37+
STACK_NAME,
38+
...process.env,
39+
})
40+
const db = new DynamoDBClient({})
41+
const byId = getDeviceId({ db, TableName, idIndex })
42+
const jwtSettings = await getSettings({ ssm: new SSMClient({}), stackName })
43+
44+
const InputSchema = Type.Object({
45+
id: PublicDeviceId,
46+
})
47+
48+
/**
49+
* Stop sharing a device
50+
*/
51+
const h = async (
52+
event: APIGatewayProxyEventV2,
53+
context: ValidInput<typeof InputSchema> & WithUser & LambdaContext,
54+
): Promise<APIGatewayProxyResultV2> => {
55+
const maybeDevice = await byId(context.validInput.id)
56+
57+
if ('error' in maybeDevice) {
58+
throw new ProblemDetailError({
59+
title: `Device ${context.validInput.id} not shared: ${maybeDevice.error}`,
60+
status: 404,
61+
})
62+
}
63+
64+
await db.send(
65+
new DeleteItemCommand({
66+
TableName,
67+
Key: marshall({ deviceId: maybeDevice.deviceId }),
68+
}),
69+
)
70+
71+
return aResponse(202)
72+
}
73+
74+
export const handler = middy()
75+
.use(addVersionHeader(version))
76+
.use(corsOPTIONS('DELETE'))
77+
.use(requestLogger())
78+
.use(validateInput(InputSchema))
79+
.use(
80+
withUser({
81+
verify: verifyUserToken(
82+
new Map([[jwtSettings.keyId, jwtSettings.publicKey]]),
83+
),
84+
}),
85+
)
86+
.use(problemResponse())
87+
.handler(h)

package-lock.json

Lines changed: 38 additions & 38 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)