diff --git a/packages/server/api.yaml b/packages/server/api.yaml index 20124557..30982696 100644 --- a/packages/server/api.yaml +++ b/packages/server/api.yaml @@ -577,6 +577,44 @@ paths: success: false error: 500 errorDescription: Internal error + /config/delete: + post: + tags: + - config + summary: 설정 삭제 + description: 학부의 설정을 삭제합니다. + security: + - UserSessionAuth: [ ] + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ConfigDeleteRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + required: + - success + - result + properties: + success: + type: boolean + result: + $ref: '#/components/schemas/ConfigDeleteRequest' + '500': + description: 내부 오류 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + example: + success: false + error: 500 + errorDescription: Internal error components: securitySchemes: UserLoginAuth: @@ -782,6 +820,15 @@ components: description: (서비스 설정 전용) 서비스에서 사용하는 건물의 목록입니다. 건물 번호를 키로, `Building` 을 값으로 하는 맵입니다. contact: type: string + ConfigDeleteRequest: + title: ConfigDeleteRequest + description: 설정 정보를 삭제하고자 할 때 사용합니다. + type: object + required: + - id + properties: + id: + type: string Building: title: Building description: 건물을 표현합니다. diff --git a/packages/server/src/config/data.ts b/packages/server/src/config/data.ts index ccc36943..69258c97 100644 --- a/packages/server/src/config/data.ts +++ b/packages/server/src/config/data.ts @@ -1,4 +1,5 @@ import type { + DeleteItemInput, ExpressionAttributeNameMap, ExpressionAttributeValueMap, GetItemInput, @@ -25,16 +26,16 @@ function toLockerSubsectionData(subsection: LockerSubsection): LockerSubsectionD function fromLockerSectionData(data: LockerSectionData): LockerSection { return { - subsections: data.s.L.map(subsectionData => fromLockerSubsectionData(subsectionData.M)), - disabled: data.d.L.map(disabled => disabled.S), + subsections: data.s.L.map((subsectionData) => fromLockerSubsectionData(subsectionData.M)), + disabled: data.d.L.map((disabled) => disabled.S), height: parseInt(data.h?.N ?? '0') }; } function toLockerSectionData(section: LockerSection): LockerSectionData { return { - s: { L: section.subsections.map(ss => ({ M: toLockerSubsectionData(ss) })) }, - d: { L: section.disabled.map(d => ({ S: d })) }, + s: { L: section.subsections.map((ss) => ({ M: toLockerSubsectionData(ss) })) }, + d: { L: section.disabled.map((d) => ({ S: d })) }, h: { N: `${section.height}` } }; } @@ -46,14 +47,19 @@ function toBuildingData(building: Building): BuildingData { l: { M: Object.fromEntries( Object.entries(building.lockers).map(([floor, lockerSectionMap]) => { - return [floor, + return [ + floor, { - M: - Object.fromEntries( - Object.entries(lockerSectionMap).map(([lockerName, section]) => [lockerName, { M: toLockerSectionData(section) }]) - ) - }]; - })) + M: Object.fromEntries( + Object.entries(lockerSectionMap).map(([lockerName, section]) => [ + lockerName, + { M: toLockerSectionData(section) } + ]) + ) + } + ]; + }) + ) } }; } @@ -64,10 +70,15 @@ function fromBuildingData(data: BuildingData): Building { name: data.n.S, lockers: Object.fromEntries( Object.entries(data.l.M).map(([floor, lockerSectionDataMap]) => { - return [floor, + return [ + floor, Object.fromEntries( - Object.entries(lockerSectionDataMap.M).map(([lockerName, sectionData]) => [lockerName, fromLockerSectionData(sectionData.M)]) - )]; + Object.entries(lockerSectionDataMap.M).map(([lockerName, sectionData]) => [ + lockerName, + fromLockerSectionData(sectionData.M) + ]) + ) + ]; }) ) }; @@ -95,14 +106,20 @@ function fromConfigDao(dao: ConfigDao): Config { function toServiceConfigDao(data: ServiceConfig): ServiceConfigDao { return { ...toConfigDao(data), - b: { M: Object.fromEntries(Object.entries(data.buildings).map(([s, b]) => [s, { M: toBuildingData(b) }])) } + b: { + M: Object.fromEntries( + Object.entries(data.buildings).map(([s, b]) => [s, { M: toBuildingData(b) }]) + ) + } }; } function fromServiceConfigDao(dao: ServiceConfigDao): ServiceConfig { return { ...fromConfigDao(dao), - buildings: Object.fromEntries(Object.entries(dao.b?.M ?? {}).map(([s, bd]) => [s, fromBuildingData(bd.M)])) + buildings: Object.fromEntries( + Object.entries(dao.b?.M ?? {}).map(([s, bd]) => [s, fromBuildingData(bd.M)]) + ) }; } @@ -120,7 +137,7 @@ function fromDepartmentConfigDao(dao: DepartmentConfigDao): DepartmentConfig { }; } -export const queryConfig = async function(startsWith = ''): Promise> { +export const queryConfig = async function (startsWith = ''): Promise> { let composedRes: Array = []; const req: QueryInput = { TableName, @@ -144,13 +161,17 @@ export const queryConfig = async function(startsWith = ''): Promise((v) => v.id.S === 'SERVICE' ? fromServiceConfigDao(v as unknown as ServiceConfigDao) : fromDepartmentConfigDao(v as unknown as DepartmentConfigDao)) + ...res.Items.map((v) => + v.id.S === 'SERVICE' + ? fromServiceConfigDao(v as unknown as ServiceConfigDao) + : fromDepartmentConfigDao(v as unknown as DepartmentConfigDao) + ) ]; } while (res.LastEvaluatedKey); return composedRes; }; -export const getConfig = async function(id: string): Promise { +export const getConfig = async function (id: string): Promise { const req: GetItemInput = { TableName, Key: { @@ -163,10 +184,12 @@ export const getConfig = async function(id: string): Promise { throw new NotFoundError(`Cannot find config of id ${id}`); } const dao: ConfigDao = res.Item as unknown as ConfigDao; - return dao.id.S === 'SERVICE' ? fromServiceConfigDao(dao as ServiceConfigDao) : fromDepartmentConfigDao(dao as DepartmentConfigDao); + return dao.id.S === 'SERVICE' + ? fromServiceConfigDao(dao as ServiceConfigDao) + : fromDepartmentConfigDao(dao as DepartmentConfigDao); }; -export const updateConfig = async function(config: ConfigUpdateRequest) { +export const updateConfig = async function (config: ConfigUpdateRequest) { const attributes: ExpressionAttributeValueMap = {}; const attributeNames: ExpressionAttributeNameMap = {}; let updateExp = ''; @@ -185,7 +208,11 @@ export const updateConfig = async function(config: ConfigUpdateRequest) { } if ((config as ServiceConfigUpdateRequest).buildings) { const buildings = (config as ServiceConfigUpdateRequest).buildings; - attributes[':buildings'] = { M: Object.fromEntries(Object.entries(buildings).map(([s, b]) => [s, { M: toBuildingData(b) }])) }; + attributes[':buildings'] = { + M: Object.fromEntries( + Object.entries(buildings).map(([s, b]) => [s, { M: toBuildingData(b) }]) + ) + }; updateExp += `${updateExp ? ',' : 'SET'} b = :buildings`; } if ((config as DepartmentConfigUpdateRequest).contact) { @@ -205,4 +232,16 @@ export const updateConfig = async function(config: ConfigUpdateRequest) { }; await dynamoDB.updateItem(req).promise(); return config; -}; \ No newline at end of file +}; + +export const deleteConfig = async function (id: string): Promise { + const req: DeleteItemInput = { + TableName, + Key: { + type: { S: 'config' }, + id: { S: id } + } + }; + await dynamoDB.deleteItem(req).promise(); + return id; +}; diff --git a/packages/server/src/config/handler/delete.ts b/packages/server/src/config/handler/delete.ts new file mode 100644 index 00000000..b98c50d2 --- /dev/null +++ b/packages/server/src/config/handler/delete.ts @@ -0,0 +1,61 @@ +import type { APIGatewayProxyHandler } from 'aws-lambda'; +import type { JwtPayload } from 'jsonwebtoken'; +import * as jwt from 'jsonwebtoken'; +import { JWT_SECRET } from '../../env'; +import { createResponse } from '../../common'; +import { assertAccessible } from '../../auth/data'; +import { deleteConfig } from '../data'; +import { errorResponse, isResponsibleError, ResponsibleError } from '../../util/error'; + +export const deleteConfigHandler: APIGatewayProxyHandler = async (event) => { + const token = (event.headers.Authorization ?? '').replace('Bearer ', ''); + let data: UserDeleteRequest; + try { + data = JSON.parse(event.body) as UserDeleteRequest; + } catch { + return createResponse(500, { + success: false, + error: 500, + errorDescription: 'Data body is malformed JSON' + }); + } + if (!data || !data.id) { + return createResponse(500, { + success: false, + error: 500, + errorDescription: 'Internal error' + }); + } + let payload: JwtPayload; + try { + payload = jwt.verify(token, JWT_SECRET) as JwtPayload; + } catch { + console.debug('malformed token'); + return createResponse(401, { + success: false, + error: 401, + errorDescription: 'Unauthorized' + }); + } + try { + const id = payload.aud as string; + await assertAccessible(id, token, true); + if (data.id.toUpperCase() === 'SERVICE') + return errorResponse( + new ResponsibleError(500, 'Internal error', "Can't delete SERVICE config.") + ); + const res = await deleteConfig(data.id); + return createResponse(200, { success: true, result: res }); + } catch (e) { + if (isResponsibleError(e)) { + return errorResponse(e as ResponsibleError); + } + console.error(e); + const res = { + success: false, + error: 500, + errorDescription: 'Internal error' + }; + return createResponse(500, res); + } +}; diff --git a/packages/server/src/handler.ts b/packages/server/src/handler.ts index ae7b7f1d..e93f1150 100644 --- a/packages/server/src/handler.ts +++ b/packages/server/src/handler.ts @@ -12,6 +12,7 @@ import { updateUserHandler } from './user/handler/update'; import { batchDeleteUserHandler } from './user/handler/batch/delete'; import { batchPutUserHandler } from './user/handler/batch/put'; import { unclaimLockerHandler } from './locker/handler/unclaim'; +import { deleteConfigHandler } from './config/handler/delete'; export const auth = { ssuLoginHandler, @@ -20,7 +21,8 @@ export const auth = { export const config = { getConfigHandler, - updateConfigHandler + updateConfigHandler, + deleteConfigHandler }; export const locker = { @@ -37,4 +39,4 @@ export const user = { updateUserHandler, batchDeleteUserHandler, batchPutUserHandler -}; \ No newline at end of file +}; diff --git a/packages/server/template.yaml b/packages/server/template.yaml index b99bf0d9..64040071 100644 --- a/packages/server/template.yaml +++ b/packages/server/template.yaml @@ -58,6 +58,11 @@ Resources: Properties: Path: /api/v1/config/update Method: options + ConfigDelete: + Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api + Properties: + Path: /api/v1/config/delete + Method: options LockerClaim: Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api Properties: @@ -198,6 +203,26 @@ Resources: Properties: Path: /api/v1/config/update Method: post + ConfigDeleteFunction: + Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction + Properties: + CodeUri: ./dist + Handler: handler.config.deleteConfigHandler + Runtime: nodejs14.x + Layers: + - !Ref DependenciesLayer + Policies: + - AWSLambdaExecute + - DynamoDBCrudPolicy: + TableName: !Ref TableName + Architectures: + - x86_64 + Events: + ConfigDelete: + Type: Api + Properties: + Path: /api/v1/config/delete + Method: post LockerClaimFunction: Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction Properties: