From d72963d5eb76b6512516c36baa77949e1055600c Mon Sep 17 00:00:00 2001 From: mimoham24 <69579255+mimoham24@users.noreply.github.com> Date: Wed, 6 Nov 2024 20:11:09 +0900 Subject: [PATCH] feat(server): implement limited scope and token management for public API (#655) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(web): unify settings (#1029) * rename save to save changes * wip: save settings when clicking on save button * rename save changes button * fix(server): integration api is not retrieving metdata item fields when metadata item id is passed (#1030) fix integration api get item bug * fix(web): metadata default value and item creating and updating (#1027) * fix: metadata default value * add: bool conversion * refactor: create and update * fix: the bug that prevented adding meta text and url * Apply suggestions from code review Co-authored-by: Nour Balaha * fix: dateConvert --------- Co-authored-by: Nour Balaha * fix(web): workspace settings in old workspaces (#1031) fix: workspace settings in old workspaces * fix(web): add missing translation (#1032) fix: add missing translation * chore(server): add trace to item (#1034) * add trace to item * fix lint * feat(web): make it possible to change tiles and terrain on the asset preview (#1033) * add: custom dropdown * add: default terrain * add: reflect settings * fix: dropdown styling * fix: missing type * add: no image * fix: mistype * fix(server): check group schema field before handling inner group fields (#1036) * check group schema field before handling inner group fields * fix operator * fix(web): update the item with a new group field (#1037) * fix: id bug * fix: styled warning * fix: check if value changed * fix: adding new group field * fix handling multiple value and refactor * add: useCallback * undo: styled-component warning * fix(web): enable to preview gltf/glb file (#1038) fix: gltf * fix(web): minor bugs in appearance, etc. (#1039) * fix: group icon color * fix: asset table column styling * fix: clickable range on group tab header * fix: leaving alert with single-option and multi-asset in group * fix: default terrain * fix: move the onClick functions --------- Co-authored-by: nourbalaha * perf(server): optimize mongo indices * chore(server): update reearthx package (#1042) update reearthx package * chore(server): fix go mod (#1043) fix * fix(web): not showing empty info box (#1041) * remove: unnecessary assertion * fix: not showing infoBox when selecting layer * refactor and fix * fix: showing mvt file info * wrap with useCallback * fix(web): the length of the view title's underline (#1045) fix: underline length * fix(worker,server): update decompressor (#1049) * chore(server): update go dependencies (#1047) * chore(server): update go dependencies * update reearthx * fix(web): the size of the point in mvt (#1053) * update cesium-mvt-imagery-provider * expand the size of the point * fix(web): mvt maximum zoom level (#1054) * add maximum zoom level * fix(web): change the default number of items (#1060) change the default number of items * chore(server): referenced items resolver (#1058) * ref items resolver * resolve notes revert worker sum changes * feat(web): implement referenced items resolver (#1059) * feat: implement referenced items resolver * feat(web): add e2e testing (#1040) * install playwright * wip: e2e config * add playwright to .git ignore * update vite.config * update gitignore files * add workspaceId * wip: e2e config * fix e2e action env * fix: removed unused env vars * fix: @playwright/test version * add gql to Reearth type * test(web): add unit tests for utils functions (#1065) add unit tests for utils folder * chore(server): go mod tidy * ci: fix decompressor deployment * fix(web): use assets data that is the result of the getItemQuery (#1066) * fix: use assets data of getItem's query * apply suggestions * refactor(web): comprehensive refactor (#1067) * Refactor Item page components * Refactor Asset page components * Refactor Schema page components * Refactor Integration page components * Refactor Request page components * Refactor Settings page components * Refactor Accessibility page components * Add unit tests * Remove inline styles * Use react hooks in the places that they should be used * Do the todo comments * Remove unnecessary comments * Remove unused queries * Add performance improvements --------- Co-authored-by: caichi <54824604+caichi-t@users.noreply.github.com> Co-authored-by: Kazuma Tsuchiya * fix(web): some quick fixing and improvements (#1071) * add: asset creator's name * fix: request creator name and reviewer's icon * add: setting icons to request modal * fix: multi reviewers style * fix: updating reviewers * fix: hiding three-dots icon * showing the notification when updating my integration * fix: swicth column width on Accessibility page * fix: project role setting * add: go back home button * apply suggestions * fix(web): asset value bugs (#1070) * fix: showing bug of asset default value * fix: asset name bug * fix: enable to select the same asset once unlinking * apply a suggestion * fix: getAsset logic * fix(web): reduce the queries to get assets (#1072) * unify GetAssets and GetAssetsItems * remove joinning asset's names * remove redundant codes * refactor: item fields * refactor: item metadata * Apply suggestions from code review Co-authored-by: Nour Balaha --------- Co-authored-by: Nour Balaha * feat(web): implement resizable columns on Asset and Request pages (#1074) make the columns stretchable on Asset and Request page * refactor(web): organize type definitions and data converters (#1075) * move model type * unify dataConverters * fix conflict * rename asset.type.ts to types.ts * delete thread.types.ts * refactor: move comment type * fix(web): include home tab in sidebar on project page (#1076) * revert: go back home button * add: home tab on project page * fix(web): reference field bugs (#1079) * fix: disable model change on update reference field * fix: disable unique validation in two-way reference fields * fix(server): cannot unset a schema field's title (#1073) * fix title unsetting bug * Update server/pkg/schema/schema.go Co-authored-by: yk-eukarya <81808708+yk-eukarya@users.noreply.github.com> --------- Co-authored-by: yk-eukarya <81808708+yk-eukarya@users.noreply.github.com> * fix(web): resolve issues with home tab and asset page comments (#1080) * fix: refetchQueries on comment panel * fix: when switching workspace, the home tab should always be selected * fix: synchronize account name with personal workspace name when updating account name * fix: the lang of the notification when updating language setting * add: required mark on webhook url * test(web): add simple, comprehensive, and minimum e2e tests (#1081) * add account and myIntegrations tests * add: logout test * add: integrations test * small fix * add: terrain test * add: member test * add: workspace test * add: project test * fix: terrain swtich test * add: overview test * add: accesibility test * add: asset test * add: request test * fix: comment on Asset test * add: schema test * move utils for workspace * move utils for project * move utils for model * move utils for group * move utils for comment * fix: missing import * move handleFieldForm * move createRequest func * add: content test * fix some codes * Revert "fix: synchronize account name with personal workspace name when updating account name" This reverts commit 388e67b5135da458c4bcc2ba20b12f56c551dc5c. * fix: request state * add: notification closing * fix: exclude path in vite.config * fix: integrations test * Apply suggestions from code review Co-authored-by: Nour Balaha --------- Co-authored-by: Nour Balaha * fix(web): request page bugs (#1082) * fix: reviewer selecting * add: reloading function of request modal * fix: reviewers column styling * fix: refetchQueries of createRequestMutation * feat(server): integration API to create asset upload (#1084) feat(server): add a new integration api to create asset upload * fix(server): integration API asset create cannot create an asset with token * fix(web): correct rendering of boolean field in content table (#1083) fix: boolean field render in content table * feat(web): increase asset file fragment depth (#1086) feat: increase asset file fragment depth * refactor: reference schema field (#1077) * ref field migration * fix lint * add missing translations * add some comments * add new error * fix migration * fix(server): ja translations (#1078) * fix ja translations * fix ja translation * migration test * fix: graphql * fix gql resolver * fix: reference item searching * fix migration --------- Co-authored-by: caichi <54824604+caichi-t@users.noreply.github.com> Co-authored-by: Kazuma Tsuchiya * chore(server): fix go mod (#1087) * fix go mod * fix go mod * ci: upgrade to golang 1.22.1 (#1088) * upgrade to golang 1.22.1 * fix * chore(worker): update golang dependencies (#1089) update * ci(web): add an action to update ion token (#1021) * ci: add an action to update ion token * update the Get Cesium Ion token run command * feat(server): support multiple tenants (#1068) * feat(server): support multiple tenants * test(server): fix broken tests * chore: update dependencies --------- Co-authored-by: rot1024 * feat(server): add WEB_CONFIG to config * feat(server): item search by referenced item title (#1090) * imp * fix dependency issue * ci: update github actions dependencies (#1052) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore: update go dependencies (#926) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(server): multi tennant was not working * chore(web): my workspace is optional * feat(server,web): support s3 multipart upload (#1091) * feat(server,web): support s3 multipart upload * update integration api * fix frontend * fix ci * fix ci * fix * fix ci * fix * fix * fix * add etag * Revert "add etag" This reverts commit a2f9738e58a2873b9c471c6f78106414a9600926. * fix --------- Co-authored-by: rot1024 * fix(server): workspace cloud not be created * fix(server): Fixed to use parameter uuid instead of generated uuid (#1093) * fix(web): add an editable icon on a metadata column header (#1092) * add: renderTitle * enable url metadata to edit from table * rename itemFormat.tsx to ItemFormat.tsx * fix(server): item search mongo pipeline (#1097) imp * feat(server,worker): add authentication for AWS SNS subscriptions (#1094) * feat(server,worker): add aws sns subcription * feat(server): change the authentication method of notify handler in the case of aws * fix(server,worker): error message * refactor --------- Co-authored-by: rot1024 * fix(web): adding e2e tests for items and optimizing queries (#1096) * fix: content test * add: text field test * add: asset test * add: date test * add: boolean test * add: option test * add: int test * fix: int test * add: url test * add; group test * add reference test and fix reference modal behavior * move files * add: text matadata test * small fix * add: tag test * add: boolean metadata test * add: checkbox test * add: date metadata test * add: url metadata test * fix: reduce api when updating item * fix: throw searchItem when opening the reference modal * fix: throw GetAssets when selecting asset field * fix: throw GetModalRequests when opening the add to Request modal * fix: unify GetAsset and GetAssetItem * fix: GetModels and GetModel * fix: tests * fix: refetchQueries name * fix: behavior after deleting model * fix: account settting test * fix group test * add: retries property to playwright.config * fix: randomizing project alias * add: beforeEach and afterEach * remove metadata tests * add: reverting back to the original username and email * fix: account name and email bug * chore(server): asset uc UpdateFiles should print more logs * fix(server): fix tests, return err from notify handler * fix(server): send event after saving an asset in asset creation * fix(server): fix warning * chore(server): update go.mod * feat(web,server): support multi-tenant login (#1102) * feat(web): support multi-tenant login * memoize * refactor type * fix cognito * fix bugs * pretty print config, mask db url * fix * fix(web): improve the performance related to the group (#1104) * remove redundant code * fix: jumping to ia new model when creating it * fix: unify model and group * remove redundant code * fix: getGroups and getGroup * add test for creating group * fix: createGroup test behavior * fix: default value bug * fix: remove graphql api from GroupItem * fix(web): table-related and small bugs (#1103) * fix: disable upload button when selecting nothing * add: clear btn on search input field on all tables * fix: the behavior of updating tag from the edit page * hide first column from table setting of table on modal * fix: make the columns resizable on modal table * fix: placeholder in test * refactor(web): model, project, and workspace types (#1105) * refactor: model type * refactor: workspace type * refactor: project type * fix: showPublishAction * fix(web): after deleting the first view, the current view is not changed (#1106) * move file * add: view test * fix: the behavior when removing first view * add: filter saving * add: saving column setting * add: test.setTimeout * fix: change test.timeout() to test.slow() * test(web): add the tests to reorder schema, model, tiles, and terrain by drag and drop (#1107) * add: schema reordering test * fix: where handleFieldForm is used * add: model reordering test * add: tiles and terrain reordring test * fix(web): improve the performance of updating metadata from the table (#1109) * remove searchItems query from refetchQueries * Revert "remove metadata tests" This reverts commit 055abc22553585cd7a32799d9654db42218e6392. * fix: tag test * fix: url updating feature * fix: url metadata test * fix(web): reference items bug (#1110) * fix: adding schema variable * add loading to reference modal table * refactor(web): move isItemReferenced query to organisms level (#1108) * refactor: move graphql to organisms level * fix: conflict * feat(server): asset file paths (#1099) * wip: adding file paths * wip: asset file paths * fix: filepaths * revert new file code * wip: move to adapter level * wip: getting asset files from adapter level * fix: memory * fix: test cases * fix: find file by id * fix: paths in repo layer * refactor: rename variables * revert repo and interactor changes * rename variables * fix: remove children from adapter * fix: convert asset unit test * refactor and comment unused code * fix: FindByID unit test * feat(web): asset file paths (#1100) * add file paths to gql * wip: unzip file list * remove children from asset file * fix: change constructFileTree's parameter name * fix path and remove unused props * refactor: get tree data * refactor * refactor 2 * refactor(web): e2e tests (#1111) * fix: closeNotification * add: locale setting * add: fullyParallel to config * remove: unnnecessary assertion * perf(web): asset file list (#1112) * perf: asset file list * fix asset file types * refactor * feat(web): migrate to antd v5 (#1113) * wip: migrate to antd 5 * wip: migrating to antd v5 * upgrade other antd deps * fix: console errors * remove deprecated method showArrow * wip: upgrading antd * fix: change moment to dayjs * fix: styling * update antd and @ant-design/pro-table * wip: fix stying * fix: table column * fix: control dropdown * fix: comment styling * fix: revert primary color change * fix: SearchItemQuery skip variable * update: ant-design libs * fix: multiple tag render * fix: e2e tests * revert: a tag styling * delete: any assertion * fix: ci error * yarn format --------- Co-authored-by: nourbalaha * fix(web): styling issues after updating antd and from long Input (#1117) * add: scroll-bar * fix: schema heading * fix: integration card title * fix: schema display * fix: reference modal * fix: request page * fix: field title * fix: test * Update web/src/components/molecules/Content/Form/FieldTitle.tsx Co-authored-by: Nour Balaha * add: test.slow() to asset field test --------- Co-authored-by: Nour Balaha * perf(web): optimize list items performance by minimizing query calls (#1119) * fix: move useGetViewsQuery to contentList hooks * add: default sort * fix: filter area styling * fix: the behavior when updating view's name * fix(web): styling of the close button on modal (#1120) fix: modal close btn * feat(web): implement resizable columns on Integration table (#1121) fix: table styling * chore(web): add a missing translation (#1123) chore: add missing translation * feat(web): add a sipnner to content and request pages (#1122) * add a spinner to content and request pages * remove unused code * feat(web): add a spinner when creating a new workspace (#1124) * feat: add a spinner to new workspace button * rename isLoading to setLoading * fix(server): item metadata resolver returns jumbled items (#1118) fix Co-authored-by: mimoham24 <69579255+mimoham24@users.noreply.github.com> * fix(server): filter out the deleted referenced items (#1116) filter out the nil items Co-authored-by: mimoham24 <69579255+mimoham24@users.noreply.github.com> * feat(web): implement workspace owners' ability to remove other owners (#1127) feat: workspace owners' ability to remove other owners * fix(web): updating metadata added later from edit page (#1125) * fix: updating metadata from detail * add: e2e test * chore(server): add environment example (#1101) * chore(server): log mongo query in dev env (#1098) imp Co-authored-by: mimoham24 <69579255+mimoham24@users.noreply.github.com> * feat(web): reduce the notificatoin duration (#1131) set the notification timeout to 2 seconds * feat(web): show notification when updating member’s role (#1129) * add: notification * fix: logic and typo * chore(web): update npm dependencies and configuration (#1126) * update minor versions * update patch versions * update major versions * update netlify deploy-preview version * make node 18 the minimum version to run cms * update @graphql-codegen * update @testing-library * upgrade apollo-upload-client --------- Co-authored-by: Kazuma Tsuchiya * chore(web): update auth0 version (#1132) * chore: upgrade auth0 to v2 * fix(server): remove the root file from asset files (#1134) * fix: filter the root file when finding file by id * fix: error checking if statement * fix: error checking in aws file * fix: update the commented code * move the filter logic to pkg and add a test * chore(web): add environment example (#1135) add: .env.example * chore(web): decrease the default items per page to 20 (#1136) fix: default pageSize * refactor(web): asset file tree and add unit tests (#1133) * refactor: move unzip file list to a folder * test: add unit tests * fix(server): deleting and updating item fails when it has a deleted reference (#1137) * fix * fix * refactor(server): data loaders generic implementation (#1130) * fix * imp --------- Co-authored-by: mimoham24 <69579255+mimoham24@users.noreply.github.com> * feat(web): check project alias availability (#1141) feat: check project alias availability * fix(server): increase disk size of cloud build * fix(web): clear fields on close in create project modal (#1143) fix: reset fields when closing create project modal * fix(web): populate create project form with initial values (#1144) fix: add initial values to create project form * feat(web): scrolling to selected model or group on Schema or Content page (#1139) feat: scroll to selected model or group Co-authored-by: Nour Balaha * feat(web): implement project alias update in settings page (#1145) * make project alias readonly in accessability page and add copy icon * wip: add project alias to update project form * add onProjectAliasCheck * refactor * feat(web): fill key or alias by name automatically (#1140) * feat: project alias * model key * model key * reference field * unify * fix: tests and validater setting * fix: e2e tests * fix(web): keep search word and selected view after going back to table from edit (#1138) * fix: keep searchTerm & currentView * fix: e2e tests * fix(web): format of standard date field (#1148) fix: format of date field * fix(web): fill key or alias automatically by lowercase (#1147) fix: auto filling by lowercase * fix(web): display all data on Request detail (#1146) * refactor values except for group * refactor Asset component & add reference title * fix: multiple boolean * fix: markdown styling * add: group field * fix: e2e tests * fix(web): filter drop-down closes by itself when the condition is selectable (#1149) fix: filter closing bug * fix(web): keep page number and page size when returning to table from detail (#1150) * fix: keep page and pageSize on Content * fix: asset page * fix: request * fix(web): view svg source code (#1151) fix: remove the cors header * chore(server): update content type handling for uploaded SVG images via URL (#1152) fix: content type * feat(server): implement ordering for multiple groups (#1153) * feat: support group ordering * add unit tests * add e2e test * feat(server): implement ordering for multiple views (#1154) * feat: add order to views * make order required * add unit tests * handle order in create and updateOrder functions * add e2e test * add more suitable error messages * make i18n * feat(web): make the project alias readonly instead of disabled (#1159) feat: make the project alias readonly instead of disabled * feat(web): add support for multi-group ordering (#1158) * feat: ordering * add e2e tests * yarn i18n * fix: remove on Group from gql query * chore(server): add missing translations (#1161) add missing translations * fix(server): add order to view document (#1162) * fix: add order to infrastructure layer * add unit test * feat(server): implement regenerate integration token functionality (#1156) * feat: implement regererate integration token * add unit test * add e2e test * feat(web): implement regenerate integration token functionality (#1157) * add regenerate token mutation to gql * implement regenerate token * add japanese translations * fix: translation --------- Co-authored-by: Kazuma Tsuchiya * feat(web): add support for multi-view ordering (#1163) * feat: ordering views * add e2e tests * fix: test name * fix: using Promise * feat(web): add the button to copy the value of Integration Token (#1164) feat: copying integration token * refactor(server): item reference (#1155) imp Co-authored-by: mimoham24 <69579255+mimoham24@users.noreply.github.com> * fix(server): webhook should not be triggered on meta data item creation (#1160) fix Co-authored-by: mimoham24 <69579255+mimoham24@users.noreply.github.com> * feat(server): add csv to preview type (#1165) feat: add csv to preview type * ci: migrate to GitHub Apps from PAT (#1167) feat: migrate to github apps Signed-off-by: KeisukeYamashita <19yamashita15@gmail.com> * fix(server): sort models, groups and views by order (#1166) * fix: sort models, groups and views by order * remove unnecessary code * add unit tests * handle errors * refactor * chore(web): upgrade cesium and resium versions (#1000) * upgrade resium to version 1.17.4 * upgrade cesium to version 1.117.0 * upgrade cesium-mvt-imagery-provider to version 1.4.1 --------- Co-authored-by: keiya01 * chore(web): update vite and fix vite.config (#1170) update vite + config * chore(web): migrate eslint to v9 (#1168) * migrate eslint to v9 * add config for playwright * add story book to eslint.config * add type: module to package.json * add prettier config and update to v 3.3.0 * feat(web): add csv preview support on asset page (#1169) * wip: add csv viewer * feat: showing point * yarn gql * rename * add: properties * yarn i18n * add: zoomTo entities * add: disableDepthTestDistance * apply suggestion * chore(web): update prettier config (#1171) * update prettier config * format files * refactor(web): resolve eslint update warnings (#1172) * wip: fix wrong type specification * fix: MyIntegrationDetails * fix: MyIntegrations * fix: Accessibility * fix: Settings * fix: AssetList * fix: asset * fix: modelsMenu * fix: overview * fix: Schema * fix: Request Details * fix: RequestList * fix: ContentList * fix: ViewsMenu * fix: contentDetail * fix: className * fix: fromGraphQLRequest * fix conflicts * yarn lint & fix manually * fix: e2e test * yarn fix * fix any * yarn format * yarn fix * add .prettierignore * fix: any assertion warnings * revert amplify changes * some fixes * add amplify to eslint ignores --------- Co-authored-by: nourbalaha * feat(server): enhance preview type detection for more file types (#1174) * feat: enhance preview type detection for more file types * add unit-test * fix(web): styling of request detail page (#1175) * fix: header dropdown * fix: styling * fix: accordion styling * yarn i18n * fix: hover color of remove button * feat(server): implement geo field's position (#1176) * wip: position value * refactor to value * refactor 2 * wip: unit tests * fix: mapToFloat64 * add Test_propertyPosition_ToValue * add propertyPosition to registry * add more unit tests * fix: float32 to float64 conversion * add decimal numbers to unit tests * fix: bad import in list_test * feat(server): implement geo-field's point (#1177) * add point field to the field schema * add point to convert schema * add field point to schema domain models * add unit tests * fix(web): disable all setting save buttons when nothing is changed (#1178) * fix: disable button * yarn fix * fix: test * fix * fix(web): styling of integration, myIntegration, and member page (#1180) * fix: MyIntegraiong, integration, delete * fix: member, integration page * add: minWidth * quick fix * feat(server): support field management in integration api (#1181) imp Co-authored-by: mimoham24 <69579255+mimoham24@users.noreply.github.com> * fix(web): disable members and integrations already added (#1182) * add flag to check if the user is member * remove already connected integration * refactor workspaceIntegrationMembers * fix: e2e test * fix: useCallback dependency * rename * refactor * fix(web): styling and translation issues (#1184) * fix traslations * apply suggestions * feat(server): support fetch projects in integration api (#1185) * imp * fix lint issue * feat(server): support search by Ids in item filter (#1183) imp * feat(server): add geometry field support (#1179) * wip: geometry field * test: fix the test cases * add ToGeometrySupportedType * wip: add validation to geo field * fix: TestFieldGeometry_Validate * fix: TestTypeProperty_Validate * fix: TestValue_Match * add FromGeometrySupportedType * fix(web): show loading for various actions (#1186) * wip * add: publish loading * fix: e2e tests * fix: uncomment t * remove as Field * apply suggestions * fix(web): ignore invalid characters when auto-filling the key and alias (#1187) * fix: ignore invalid characters * fix: use try-catch * ci: migrate GCR to GAR (#1190) * feat: migrate to github apps Signed-off-by: KeisukeYamashita <19yamashita15@gmail.com> * ci: migrate gcr to gar Signed-off-by: KeisukeYamashita <19yamashita15@gmail.com> --------- Signed-off-by: KeisukeYamashita <19yamashita15@gmail.com> * feat(server): support item search in integration api (#1192) * imp * add test cases * fix(web): remove useless key validating when updating some value other than key (#1191) * fix: project, model, and schema * fix: reference field validate * fix: model, project * fix: CorrespondingFieldKeyUnique * feat(server): implement geometry object and geometry editor fields (#1189) * add geometry to value * add geometry to mongodoc * wip: add geometry to integration api * wip: e2e * wip: e2e tests * fix: supported types * fix TestClearItemValues * make supported types uniq * add any supported type * add integration e2e tests * fix: TestIntegrationModelFilterAPI * format integration item test * add geometry editor type * remove unwanted code * rename the field geometry to geometry object * fix: test cases * remove any type from geometry object * rename test files * fix(server): geo field missing validations (#1188) * add validation supported type validation remove GeoJSON feature decoding * remove fmt * i18n gen * i18n gen * refactor rfc validation logic add geo collection support * add missing returns * position validation * is valid * support any * feat(web): preserve table conditions on asset and request page (#1193) * fix: preserve sort on asset * refactor * fix: preserve sort of request and columns * fix: type definition * fix: add missing repository URL argument for `configure-docker` command (#1195) * fix: add missing argument for `configure-docker` command Signed-off-by: KeisukeYamashita <19yamashita15@gmail.com> * fix: add missing `actions/checkout` before google cloud auth Signed-off-by: KeisukeYamashita <19yamashita15@gmail.com> --------- Signed-off-by: KeisukeYamashita <19yamashita15@gmail.com> * ci: add missing checkout before deploy server workflow (#1196) fix: add missing checkout before deploy server workflow Signed-off-by: KeisukeYamashita <19yamashita15@gmail.com> * fix(web): changes are not detected for some group fields (#1199) fix: the logic of detect changes in group field * feat(server): schemata in integration api (#1200) imp * fix(web): group field values sometimes disapper after refreshing page (#1202) fix * feat(server): export items as geojson and csv via integration api (#1194) * wip: items with project as geojson and csv * update the csv format and description in the integration schema * fix: csv and geojson bugs * add unit tests for group * update geojson content type * update Japanese translations * remove group and reference fields from export * add check if result is empty * feat(server): export items as geojson and csv via public api (#1198) * add geojson types to public api * support multiple values in csv * support multiple geo fields in geojson * add e2e test * improve error handling * make the files downloadable * feat(web): support geometry object and geometry editor fields (#1197) * wip: add point, polyline, polygon on cesium * add: geometry fields * fix: geometry field modal * wip: add zoom feature * change pin icons * feat: edit pin pos & fullscreen * feat: deleting points * wip * install monaco-editor * add icons * yarn gql * fix type definition * wip: imp UI * fix schema * fix icon * wip: move geometry field component * add: table render * impl field * add: default value * add: request * fix schema modal & add disable flag * feat: show input value on map * wip: fix validation * wip: integrate @reearth/core * render value on map * refactor * add: warning * add: search location * add: terrain setting * add: support group * i18n * fix: validate * add: e2e test * refactor * upgrage @reearth/core to render GeometryCollection * refactor * support tile setting * feat(web): add version suffix to cesiumBaseUrl (#1203) feat: add version suffix to cesiumBaseUrl * fix(web): cesium base url version (#1204) fix: cesium base url * feat(server): implement asset search in integrarion api (#1208) * add keyword param to integration schema * rename q to keyword * rename q to keyword in memory * fix test cases * add e2e * refactor * feat(server): firebase authentication (#1206) * add firebase to config * feat(web): firebase authentication (#1207) * add firebase auth * refactor * refactor 2 * update .env.example * rename firebase provider component * fix(web): improve geometry field (#1205) * make field resizable * fix: sketch color * fix: error showing * fix: search bar color bug * fix: delete button on editor * fix: validating * fix: camera pos when updating editor * fix: test * set --max_old_space_size * increase space size * support terrain setting * fix: group width * fix: placeholder position * fix: allocated memory * fix(web): upgrade @reearth/core (#1210) * upgrade core * fix: ci error * apply request * remove string-width * Revert "remove string-width" This reverts commit 2a30aa1a0511632b69b12d5ef818d1b7fb5c9563. * feat(server): add workspace id to project in integration api (#1209) imp * feat(server): schemata filter in integration api (#1212) imp * fix(server): internal error when the project has an active webhook (#1213) imp * fix(web): styling of model menu (#1211) * fix: title icon * fix: collapsed styling * feat: show tooltip * fix: keep collapsed * chore(web): upgrade eslint and eslint-config-reearth (#1215) * feat: upgrade eslint and reearth-eslint-config * fix: eslint error * add project name * fix(web): asset upload button is activated before all uploading process is completed (#1217) fix: asset uploading bug * chore(web): upgrade yarn dependencies (#1216) * upgrade patch versions * upgrade minor versions * upgrade major versions * generate gql and i18n * fix(web): publish status is not updated when creating a new request (#1221) fix: adding GetItem to refetchQueries * fix(web): disable closing modal when uploading asset (#1222) fix: disable closing when uploading * fix(web): using OpenLayers for geometry field (#1220) * update: using OpenLayers * fix: remove unnecessary props * remove @reearth/core * set max_old_space_size * increase max_old_space_siz * remove wrong line breaks * fix: map initilizing bug * Update web/src/components/molecules/Common/Form/GeometryItem/index.tsx Co-authored-by: Nour Balaha --------- Co-authored-by: Nour Balaha * chore(web): optimize vite config * fix(server): asset children are not returned in integration api (#1226) * fix * fix --------- Co-authored-by: mimoham24 <69579255+mimoham24@users.noreply.github.com> * fix(web): camera and validation message on geometry field (#1227) * fix: set default value on geometry editor * fix: zoom after searching * fix: error message on geometry object * fix(server): geojson validation bug (#1228) fix: multiple geo bug * fix(server): multiple default value bug (#1229) * fix: multiple default value bug * doc: fix api doc (#1232) fix api doc * feat(web): add item link to the request (#1230) * add: item link on request * add e2e test * feat(server): geoJson/json import via integration api (#1231) * imp * imp * wip: add e2e tests to geojson import * refactor e2e * fix geoJson handling * fix * fix lint * add i18n --------- Co-authored-by: nourbalaha Co-authored-by: mimoham24 <69579255+mimoham24@users.noreply.github.com> * fix(web): title of my integration and request page (#1234) * fix: background color * fix: page title * fix(server): set file size for zip asset's children files (#1237) fix * chore(web): show tooltip when pressing copy button (#1233) chore: show tooltip when pressing copy button * fix(web): screen flickers on member page (#1239) fix: screen flickers * fix(web): disable the buttons to change the state of an item based on the item's status (#1240) fix: disable publish buttons * fix(server): multiple bugs while importing json/geoJson (#1241) fix Co-authored-by: mimoham24 <69579255+mimoham24@users.noreply.github.com> * feat(server): enhance request filter to search by id (#1243) imp * feat(server): add requests resolver in item query (#1235) imp * feat(server): search request by item id (#1246) fix * feat(web): leave workspace (#1244) * impl * add modal to confirm leaving * fix(web): reference field updating bug (#1236) * fix: ref field bug * feat: toggle confirm button * improve test * chore: update go dependencies (#1142) * chore: update go dependencies * update go.work * fix * fix * fix * fix * ignore linter * ignore linter --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: yk-eukarya <81808708+yk-eukarya@users.noreply.github.com> * fix(server): accept all geo types when creating geometryObject field from integration api by default (#1247) fix * ci: update github actions dependencies (#1115) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: yk-eukarya <81808708+yk-eukarya@users.noreply.github.com> * feat(server): find by assets memory infrastructure (#649) * find by assets memory implementation and test case * fix --------- Co-authored-by: rot1024 Co-authored-by: yk-eukarya <81808708+yk-eukarya@users.noreply.github.com> * add api key * fix * fix * fix * fix * fix * implement token as standalone pkg * apply code suggestion * fix NewPublication * update TestToken_Value * add error handling to generate * remove token pkg and use lo.RandomString instead * fix ci * wip: e2e tests * lint + review changes * e2e * lint * gql project test * e2e * fix: auth client * e2e tests * e2e * refactor auth client * requested changes 1 * e2e * refactor * requested changes 2 * fix * fix * fix: possible nil check * add a comment * revert --------- Co-authored-by: Nour Balaha Co-authored-by: yk-eukarya <81808708+yk-eukarya@users.noreply.github.com> Co-authored-by: rot1024 Co-authored-by: Nao Yonashiro Co-authored-by: Kazuma Tsuchiya Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Katsuya Miyachi Co-authored-by: KeisukeYamashita <19yamashita15@gmail.com> Co-authored-by: keiya01 --- server/e2e/gql_porject_test.go | 64 ++++- server/e2e/integration_test.go | 8 +- server/e2e/publicapi_test.go | 149 +++++++++-- server/internal/adapter/gql/generated.go | 252 ++++++++++++++++-- .../adapter/gql/gqlmodel/convert_project.go | 6 + .../adapter/gql/gqlmodel/models_gen.go | 7 +- .../adapter/gql/resolver_integration.go | 4 +- .../internal/adapter/gql/resolver_project.go | 15 ++ .../internal/adapter/publicapi/controller.go | 15 +- server/internal/app/app.go | 2 +- server/internal/app/auth_client.go | 70 +++-- .../internal/infrastructure/memory/project.go | 16 ++ .../infrastructure/memory/project_test.go | 79 ++++++ .../infrastructure/mongo/mongodoc/project.go | 4 +- .../internal/infrastructure/mongo/project.go | 12 +- .../infrastructure/mongo/project_test.go | 75 ++++++ server/internal/usecase/interactor/project.go | 31 +++ server/internal/usecase/interfaces/project.go | 2 + server/internal/usecase/repo/project.go | 1 + server/pkg/integration/integration.go | 24 +- server/pkg/project/publication.go | 28 ++ server/pkg/project/publication_test.go | 33 ++- server/schemas/integration.graphql | 4 +- server/schemas/project.graphql | 6 + 24 files changed, 782 insertions(+), 125 deletions(-) diff --git a/server/e2e/gql_porject_test.go b/server/e2e/gql_porject_test.go index bbf7a63ddb..a5a508301b 100644 --- a/server/e2e/gql_porject_test.go +++ b/server/e2e/gql_porject_test.go @@ -88,14 +88,66 @@ func updateProject(e *httpexpect.Expect, pID, name, desc, alias, publicationScop return res.Path("$.data.updateProject.project.id").Raw().(string), res } -func TestCreateProject(t *testing.T) { - e := StartServer(t, &app.Config{}, true, baseSeederUser) +func regeneratePublicApiToken(e *httpexpect.Expect, pId string) *httpexpect.Value { + requestBody := GraphQLRequest{ + Query: `mutation RegeneratePublicApiToken($projectId: ID!) { + regeneratePublicApiToken(input: { projectId: $projectId }) { + project { + id + publication { + scope + assetPublic + token + } + } + } + }`, + Variables: map[string]any{ + "projectId": pId, + }, + } + + res := e.POST("/api/graphql"). + WithHeader("Origin", "https://example.com"). + WithHeader("X-Reearth-Debug-User", uId1.String()). + WithHeader("Content-Type", "application/json"). + WithJSON(requestBody). + Expect(). + Status(http.StatusOK). + JSON() + + return res +} - _, res := createProject(e, wId.String(), "test", "test", "test-1") +func TestProject(t *testing.T) { + e := StartServer(t, &app.Config{}, true, baseSeederUser) - res.Object(). + // create project + pId, p := createProject(e, wId.String(), "test1", "test1", "test1") + pp := p.Object(). Value("data").Object(). Value("createProject").Object(). - Value("project").Object(). - HasValue("name", "test") + Value("project").Object() + pp.HasValue("name", "test1") + pp.HasValue("description", "test1") + pp.HasValue("alias", "test1") + + // update project + _, res := updateProject(e, pId, "test2", "test2", "test2", "LIMITED", true) + pp = res.Object(). + Value("data").Object(). + Value("updateProject").Object(). + Value("project").Object() + pp.HasValue("name", "test2") + pp.HasValue("description", "test2") + pp.HasValue("alias", "test2") + pp.Value("publication").Object().HasValue("scope", "LIMITED") + pp.Value("publication").Object().HasValue("assetPublic", true) + + // regenerate public api token + res1 := regeneratePublicApiToken(e, pId) + token := res1.Path("$.data.regeneratePublicApiToken.project.publication.token") + res2 := regeneratePublicApiToken(e, pId) + newToken := res2.Path("$.data.regeneratePublicApiToken.project.publication.token") + token.NotEqual(newToken) } diff --git a/server/e2e/integration_test.go b/server/e2e/integration_test.go index f19d429fad..9e0658ecb8 100644 --- a/server/e2e/integration_test.go +++ b/server/e2e/integration_test.go @@ -56,8 +56,8 @@ func createIntegration(e *httpexpect.Expect, name, desc, logoUrl, iType string) func regenerateToken(e *httpexpect.Expect, iId string) (string, *httpexpect.Value) { requestBody := GraphQLRequest{ Query: ` - mutation regenerateToken($integrationId: ID!) { - regenerateToken(input: { integrationId: $integrationId }) { + mutation regenerateIntegrationToken($integrationId: ID!) { + regenerateIntegrationToken(input: { integrationId: $integrationId }) { integration { id name @@ -85,7 +85,7 @@ func regenerateToken(e *httpexpect.Expect, iId string) (string, *httpexpect.Valu Status(http.StatusOK). JSON() - return res.Path("$.data.regenerateToken.integration.config.token").Raw().(string), res + return res.Path("$.data.regenerateIntegrationToken.integration.config.token").Raw().(string), res } func TestRegenerateToken(t *testing.T) { @@ -102,7 +102,7 @@ func TestRegenerateToken(t *testing.T) { _, updatedIntegration := regenerateToken(e, iId) newToken := updatedIntegration.Object(). Value("data").Object(). - Value("regenerateToken").Object(). + Value("regenerateIntegrationToken").Object(). Value("integration").Object(). Value("config").Object(). Value("token") diff --git a/server/e2e/publicapi_test.go b/server/e2e/publicapi_test.go index 6506b1da19..bca973a4a6 100644 --- a/server/e2e/publicapi_test.go +++ b/server/e2e/publicapi_test.go @@ -22,27 +22,31 @@ import ( ) var ( - publicAPIProjectID = id.NewProjectID() - publicAPIModelID = id.NewModelID() - publicAPIItem1ID = id.NewItemID() - publicAPIItem2ID = id.NewItemID() - publicAPIItem3ID = id.NewItemID() - publicAPIItem4ID = id.NewItemID() - publicAPIItem6ID = id.NewItemID() - publicAPIItem7ID = id.NewItemID() - publicAPIAsset1ID = id.NewAssetID() - publicAPIAsset2ID = id.NewAssetID() - publicAPIAssetUUID = uuid.NewString() - publicAPIProjectAlias = "test-project" - publicAPIModelKey = "test-model" - publicAPIModelKey2 = "test-model-2" - publicAPIModelKey3 = "test-model-3" - publicAPIField1Key = "test-field-1" - publicAPIField2Key = "asset" - publicAPIField3Key = "test-field-2" - publicAPIField4Key = "asset2" - publicAPIField5Key = "geometry-object" - publicAPIField6Key = "geometry-editor" + publicAPIProjectID = id.NewProjectID() + publicAPIProjectID2 = id.NewProjectID() + publicAPIModelID = id.NewModelID() + publicAPIModelID2 = id.NewModelID() + publicAPIItem1ID = id.NewItemID() + publicAPIItem2ID = id.NewItemID() + publicAPIItem3ID = id.NewItemID() + publicAPIItem4ID = id.NewItemID() + publicAPIItem6ID = id.NewItemID() + publicAPIItem7ID = id.NewItemID() + publicAPIAsset1ID = id.NewAssetID() + publicAPIAsset2ID = id.NewAssetID() + publicAPIAssetUUID = uuid.NewString() + publicAPIProjectAlias = "test-project" + publicAPIProjectAlias2 = "test-project-2" + publicAPIModelKey = "test-model" + publicAPIModelKey2 = "test-model-2" + publicAPIModelKey3 = "test-model-3" + publicAPIModelKey4 = "test-model-4" + publicAPIField1Key = "test-field-1" + publicAPIField2Key = "asset" + publicAPIField3Key = "test-field-2" + publicAPIField4Key = "asset2" + publicAPIField5Key = "geometry-object" + publicAPIField6Key = "geometry-editor" ) func TestPublicAPI(t *testing.T) { @@ -213,7 +217,7 @@ func TestPublicAPI(t *testing.T) { "offset": 0, "page": 1, "results": []map[string]any{ - map[string]any{ + { "id": publicAPIAsset1ID.String(), "type": "asset", "url": fmt.Sprintf("https://example.com/assets/%s/%s/aaa.zip", publicAPIAssetUUID[:2], publicAPIAssetUUID[2:]), @@ -370,13 +374,109 @@ func TestPublicAPI(t *testing.T) { IsEqual(map[string]any{ "error": "not found", }) + + // make the project limited + prj.Publication().SetScope(project.PublicationScopeLimited) + prj.Publication().SetAssetPublic(true) + prj.Publication().GenerateToken() + token := prj.Publication().Token() + lo.Must0(repos.Project.Save(ctx, prj)) + + // invalid token + e.GET("/api/p/{project}/{model}", publicAPIProjectAlias, publicAPIModelKey). + WithHeader("Origin", "https://example.com"). + WithHeader("Authorization", "secret_abc"). + WithHeader("Content-Type", "application/json"). + Expect(). + Status(http.StatusUnauthorized). + JSON(). + IsEqual(map[string]interface{}{ + "error": "invalid token", + }) + + // valid token + e.GET("/api/p/{project}/{model}", publicAPIProjectAlias, publicAPIModelKey). + WithHeader("Origin", "https://example.com"). + WithHeader("Authorization", token). + WithHeader("Content-Type", "application/json"). + Expect(). + Status(http.StatusOK). + JSON(). + IsEqual(map[string]any{ + "results": []map[string]any{ + { + "id": publicAPIItem1ID.String(), + publicAPIField1Key: "aaa", + publicAPIField2Key: map[string]any{ + "type": "asset", + "id": publicAPIAsset1ID.String(), + "url": fmt.Sprintf("https://example.com/assets/%s/%s/aaa.zip", publicAPIAssetUUID[:2], publicAPIAssetUUID[2:]), + }, + }, + { + "id": publicAPIItem2ID.String(), + publicAPIField1Key: "bbb", + }, + { + "id": publicAPIItem3ID.String(), + publicAPIField1Key: "ccc", + publicAPIField3Key: []string{"aaa", "bbb", "ccc"}, + publicAPIField4Key: []any{ + map[string]any{ + "type": "asset", + "id": publicAPIAsset1ID.String(), + "url": fmt.Sprintf("https://example.com/assets/%s/%s/aaa.zip", publicAPIAssetUUID[:2], publicAPIAssetUUID[2:]), + }, + }, + }, + { + "id": publicAPIItem6ID.String(), + publicAPIField1Key: "ccc", + publicAPIField3Key: []string{"aaa", "bbb", "ccc"}, + publicAPIField4Key: []any{ + map[string]any{ + "type": "asset", + "id": publicAPIAsset1ID.String(), + "url": fmt.Sprintf("https://example.com/assets/%s/%s/aaa.zip", publicAPIAssetUUID[:2], publicAPIAssetUUID[2:]), + }, + }, + publicAPIField5Key: "{\n\"type\": \"Point\",\n\t\"coordinates\": [102.0, 0.5]\n}", + publicAPIField6Key: "{\"coordinates\":[[139.65439725962517,36.34793305387103],[139.61688622815393,35.910803456352724]],\"type\":\"LineString\"}", + }, + { + "id": publicAPIItem7ID.String(), + publicAPIField1Key: "ccc", + }, + }, + "totalCount": 5, + "hasMore": false, + "limit": 50, + "offset": 0, + "page": 1, + }) + + // different project in the same workspace with the same token + e.GET("/api/p/{project}/{model}", publicAPIProjectAlias2, publicAPIModelKey2). + WithHeader("Origin", "https://example.com"). + WithHeader("Authorization", token). + WithHeader("Content-Type", "application/json"). + Expect(). + Status(http.StatusBadRequest). + JSON(). + IsEqual(map[string]any{ + "error": "invalid project", + }) } func publicAPISeeder(ctx context.Context, r *repo.Container) error { uid := accountdomain.NewUserID() - p1 := project.New().ID(publicAPIProjectID).Workspace(accountdomain.NewWorkspaceID()).Alias(publicAPIProjectAlias).Publication( + wid := accountdomain.NewWorkspaceID() + p1 := project.New().ID(publicAPIProjectID).Workspace(wid).Alias(publicAPIProjectAlias).Publication( project.NewPublication(project.PublicationScopePublic, true), ).MustBuild() + p2 := project.New().ID(publicAPIProjectID2).Workspace(wid).Alias(publicAPIProjectAlias2).Publication( + project.NewPublicationWithToken(project.PublicationScopeLimited, true, "secret_abcdefghijklmnopqrstuvwxyz"), + ).MustBuild() a := asset.New().ID(publicAPIAsset1ID).Project(p1.ID()).CreatedByUser(uid).Size(1).Thread(id.NewThreadID()). FileName("aaa.zip").UUID(publicAPIAssetUUID).MustBuild() @@ -403,6 +503,7 @@ func publicAPISeeder(ctx context.Context, r *repo.Container) error { // m2 is not a public model m2 := model.New().ID(publicAPIModelID).Project(p1.ID()).Schema(s.ID()).Name(publicAPIModelKey2).Key(id.NewKey(publicAPIModelKey2)).Public(false).MustBuild() m3 := model.New().ID(publicAPIModelID).Project(p1.ID()).Schema(s2.ID()).Name(publicAPIModelKey3).Key(id.NewKey(publicAPIModelKey3)).Public(true).MustBuild() + m4 := model.New().ID(publicAPIModelID2).Project(p2.ID()).Schema(id.NewSchemaID()).Key(id.NewKey(publicAPIModelKey4)).Public(true).MustBuild() i1 := item.New().ID(publicAPIItem1ID).Model(m.ID()).Schema(s.ID()).Project(p1.ID()).Thread(id.NewThreadID()).User(uid).Fields([]*item.Field{ item.NewField(s.Fields()[0].ID(), value.TypeText.Value("aaa").AsMultiple(), nil), @@ -444,10 +545,12 @@ func publicAPISeeder(ctx context.Context, r *repo.Container) error { }).MustBuild() lo.Must0(r.Project.Save(ctx, p1)) + lo.Must0(r.Project.Save(ctx, p2)) lo.Must0(r.Asset.Save(ctx, a)) lo.Must0(r.AssetFile.Save(ctx, a.ID(), af)) lo.Must0(r.Schema.Save(ctx, s)) lo.Must0(r.Model.Save(ctx, m)) + lo.Must0(r.Model.Save(ctx, m4)) lo.Must0(r.Item.Save(ctx, i1)) lo.Must0(r.Item.Save(ctx, i2)) lo.Must0(r.Item.Save(ctx, i3)) diff --git a/server/internal/adapter/gql/generated.go b/server/internal/adapter/gql/generated.go index 3bb07ad41f..6284f6f87d 100644 --- a/server/internal/adapter/gql/generated.go +++ b/server/internal/adapter/gql/generated.go @@ -445,7 +445,8 @@ type ComplexityRoot struct { DeleteWorkspace func(childComplexity int, input gqlmodel.DeleteWorkspaceInput) int PublishItem func(childComplexity int, input gqlmodel.PublishItemInput) int PublishModel func(childComplexity int, input gqlmodel.PublishModelInput) int - RegenerateToken func(childComplexity int, input gqlmodel.RegenerateTokenInput) int + RegenerateIntegrationToken func(childComplexity int, input gqlmodel.RegenerateIntegrationTokenInput) int + RegeneratePublicAPIToken func(childComplexity int, input gqlmodel.RegeneratePublicAPITokenInput) int RemoveIntegrationFromWorkspace func(childComplexity int, input gqlmodel.RemoveIntegrationFromWorkspaceInput) int RemoveMultipleMembersFromWorkspace func(childComplexity int, input gqlmodel.RemoveMultipleMembersFromWorkspaceInput) int RemoveMyAuth func(childComplexity int, input gqlmodel.RemoveMyAuthInput) int @@ -531,6 +532,7 @@ type ComplexityRoot struct { ProjectPublication struct { AssetPublic func(childComplexity int) int Scope func(childComplexity int) int + Token func(childComplexity int) int } PublishItemPayload struct { @@ -958,7 +960,7 @@ type MutationResolver interface { CreateIntegration(ctx context.Context, input gqlmodel.CreateIntegrationInput) (*gqlmodel.IntegrationPayload, error) UpdateIntegration(ctx context.Context, input gqlmodel.UpdateIntegrationInput) (*gqlmodel.IntegrationPayload, error) DeleteIntegration(ctx context.Context, input gqlmodel.DeleteIntegrationInput) (*gqlmodel.DeleteIntegrationPayload, error) - RegenerateToken(ctx context.Context, input gqlmodel.RegenerateTokenInput) (*gqlmodel.IntegrationPayload, error) + RegenerateIntegrationToken(ctx context.Context, input gqlmodel.RegenerateIntegrationTokenInput) (*gqlmodel.IntegrationPayload, error) CreateWebhook(ctx context.Context, input gqlmodel.CreateWebhookInput) (*gqlmodel.WebhookPayload, error) UpdateWebhook(ctx context.Context, input gqlmodel.UpdateWebhookInput) (*gqlmodel.WebhookPayload, error) DeleteWebhook(ctx context.Context, input gqlmodel.DeleteWebhookInput) (*gqlmodel.DeleteWebhookPayload, error) @@ -979,6 +981,7 @@ type MutationResolver interface { CreateProject(ctx context.Context, input gqlmodel.CreateProjectInput) (*gqlmodel.ProjectPayload, error) UpdateProject(ctx context.Context, input gqlmodel.UpdateProjectInput) (*gqlmodel.ProjectPayload, error) DeleteProject(ctx context.Context, input gqlmodel.DeleteProjectInput) (*gqlmodel.DeleteProjectPayload, error) + RegeneratePublicAPIToken(ctx context.Context, input gqlmodel.RegeneratePublicAPITokenInput) (*gqlmodel.ProjectPayload, error) CreateRequest(ctx context.Context, input gqlmodel.CreateRequestInput) (*gqlmodel.RequestPayload, error) UpdateRequest(ctx context.Context, input gqlmodel.UpdateRequestInput) (*gqlmodel.RequestPayload, error) ApproveRequest(ctx context.Context, input gqlmodel.ApproveRequestInput) (*gqlmodel.RequestPayload, error) @@ -2767,17 +2770,29 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Mutation.PublishModel(childComplexity, args["input"].(gqlmodel.PublishModelInput)), true - case "Mutation.regenerateToken": - if e.complexity.Mutation.RegenerateToken == nil { + case "Mutation.regenerateIntegrationToken": + if e.complexity.Mutation.RegenerateIntegrationToken == nil { break } - args, err := ec.field_Mutation_regenerateToken_args(context.TODO(), rawArgs) + args, err := ec.field_Mutation_regenerateIntegrationToken_args(context.TODO(), rawArgs) if err != nil { return 0, false } - return e.complexity.Mutation.RegenerateToken(childComplexity, args["input"].(gqlmodel.RegenerateTokenInput)), true + return e.complexity.Mutation.RegenerateIntegrationToken(childComplexity, args["input"].(gqlmodel.RegenerateIntegrationTokenInput)), true + + case "Mutation.regeneratePublicApiToken": + if e.complexity.Mutation.RegeneratePublicAPIToken == nil { + break + } + + args, err := ec.field_Mutation_regeneratePublicApiToken_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Mutation.RegeneratePublicAPIToken(childComplexity, args["input"].(gqlmodel.RegeneratePublicAPITokenInput)), true case "Mutation.removeIntegrationFromWorkspace": if e.complexity.Mutation.RemoveIntegrationFromWorkspace == nil { @@ -3284,6 +3299,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.ProjectPublication.Scope(childComplexity), true + case "ProjectPublication.token": + if e.complexity.ProjectPublication.Token == nil { + break + } + + return e.complexity.ProjectPublication.Token(childComplexity), true + case "PublishItemPayload.items": if e.complexity.PublishItemPayload.Items == nil { break @@ -4780,7 +4802,8 @@ func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler { ec.unmarshalInputPagination, ec.unmarshalInputPublishItemInput, ec.unmarshalInputPublishModelInput, - ec.unmarshalInputRegenerateTokenInput, + ec.unmarshalInputRegenerateIntegrationTokenInput, + ec.unmarshalInputRegeneratePublicApiTokenInput, ec.unmarshalInputRemoveIntegrationFromWorkspaceInput, ec.unmarshalInputRemoveMultipleMembersFromWorkspaceInput, ec.unmarshalInputRemoveMyAuthInput, @@ -5655,7 +5678,7 @@ input DeleteIntegrationInput { integrationId: ID! } -input RegenerateTokenInput { +input RegenerateIntegrationTokenInput { integrationId: ID! } @@ -5674,7 +5697,7 @@ extend type Mutation { createIntegration(input: CreateIntegrationInput!): IntegrationPayload updateIntegration(input: UpdateIntegrationInput!): IntegrationPayload deleteIntegration(input: DeleteIntegrationInput!): DeleteIntegrationPayload - regenerateToken(input: RegenerateTokenInput!): IntegrationPayload + regenerateIntegrationToken(input: RegenerateIntegrationTokenInput!): IntegrationPayload } `, BuiltIn: false}, {Name: "../../../schemas/integration_webhook.graphql", Input: `type WebhookTrigger { @@ -6318,6 +6341,7 @@ enum ProjectPublicationScope { type ProjectPublication { scope: ProjectPublicationScope! assetPublic: Boolean! + token: String } type Project implements Node { @@ -6360,6 +6384,10 @@ input DeleteProjectInput { projectId: ID! } +input RegeneratePublicApiTokenInput { + projectId: ID! +} + # Payload type ProjectPayload { project: Project! @@ -6390,6 +6418,7 @@ extend type Mutation { createProject(input: CreateProjectInput!): ProjectPayload updateProject(input: UpdateProjectInput!): ProjectPayload deleteProject(input: DeleteProjectInput!): DeleteProjectPayload + regeneratePublicApiToken(input: RegeneratePublicApiTokenInput!): ProjectPayload } `, BuiltIn: false}, {Name: "../../../schemas/request.graphql", Input: `type Request implements Node { @@ -7931,35 +7960,67 @@ func (ec *executionContext) field_Mutation_publishModel_argsInput( return zeroVal, nil } -func (ec *executionContext) field_Mutation_regenerateToken_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { +func (ec *executionContext) field_Mutation_regenerateIntegrationToken_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { + var err error + args := map[string]interface{}{} + arg0, err := ec.field_Mutation_regenerateIntegrationToken_argsInput(ctx, rawArgs) + if err != nil { + return nil, err + } + args["input"] = arg0 + return args, nil +} +func (ec *executionContext) field_Mutation_regenerateIntegrationToken_argsInput( + ctx context.Context, + rawArgs map[string]interface{}, +) (gqlmodel.RegenerateIntegrationTokenInput, error) { + // We won't call the directive if the argument is null. + // Set call_argument_directives_with_null to true to call directives + // even if the argument is null. + _, ok := rawArgs["input"] + if !ok { + var zeroVal gqlmodel.RegenerateIntegrationTokenInput + return zeroVal, nil + } + + ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("input")) + if tmp, ok := rawArgs["input"]; ok { + return ec.unmarshalNRegenerateIntegrationTokenInput2githubᚗcomᚋreearthᚋreearthᚑcmsᚋserverᚋinternalᚋadapterᚋgqlᚋgqlmodelᚐRegenerateIntegrationTokenInput(ctx, tmp) + } + + var zeroVal gqlmodel.RegenerateIntegrationTokenInput + return zeroVal, nil +} + +func (ec *executionContext) field_Mutation_regeneratePublicApiToken_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { var err error args := map[string]interface{}{} - arg0, err := ec.field_Mutation_regenerateToken_argsInput(ctx, rawArgs) + arg0, err := ec.field_Mutation_regeneratePublicApiToken_argsInput(ctx, rawArgs) if err != nil { return nil, err } args["input"] = arg0 return args, nil } -func (ec *executionContext) field_Mutation_regenerateToken_argsInput( +func (ec *executionContext) field_Mutation_regeneratePublicApiToken_argsInput( ctx context.Context, rawArgs map[string]interface{}, -) (gqlmodel.RegenerateTokenInput, error) { +) (gqlmodel.RegeneratePublicAPITokenInput, error) { // We won't call the directive if the argument is null. // Set call_argument_directives_with_null to true to call directives // even if the argument is null. _, ok := rawArgs["input"] if !ok { - var zeroVal gqlmodel.RegenerateTokenInput + var zeroVal gqlmodel.RegeneratePublicAPITokenInput return zeroVal, nil } ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("input")) if tmp, ok := rawArgs["input"]; ok { - return ec.unmarshalNRegenerateTokenInput2githubᚗcomᚋreearthᚋreearthᚑcmsᚋserverᚋinternalᚋadapterᚋgqlᚋgqlmodelᚐRegenerateTokenInput(ctx, tmp) + return ec.unmarshalNRegeneratePublicApiTokenInput2githubᚗcomᚋreearthᚋreearthᚑcmsᚋserverᚋinternalᚋadapterᚋgqlᚋgqlmodelᚐRegeneratePublicAPITokenInput(ctx, tmp) } - var zeroVal gqlmodel.RegenerateTokenInput + var zeroVal gqlmodel.RegeneratePublicAPITokenInput return zeroVal, nil } @@ -20060,8 +20121,8 @@ func (ec *executionContext) fieldContext_Mutation_deleteIntegration(ctx context. return fc, nil } -func (ec *executionContext) _Mutation_regenerateToken(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_Mutation_regenerateToken(ctx, field) +func (ec *executionContext) _Mutation_regenerateIntegrationToken(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Mutation_regenerateIntegrationToken(ctx, field) if err != nil { return graphql.Null } @@ -20074,7 +20135,7 @@ func (ec *executionContext) _Mutation_regenerateToken(ctx context.Context, field }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return ec.resolvers.Mutation().RegenerateToken(rctx, fc.Args["input"].(gqlmodel.RegenerateTokenInput)) + return ec.resolvers.Mutation().RegenerateIntegrationToken(rctx, fc.Args["input"].(gqlmodel.RegenerateIntegrationTokenInput)) }) if err != nil { ec.Error(ctx, err) @@ -20088,7 +20149,7 @@ func (ec *executionContext) _Mutation_regenerateToken(ctx context.Context, field return ec.marshalOIntegrationPayload2ᚖgithubᚗcomᚋreearthᚋreearthᚑcmsᚋserverᚋinternalᚋadapterᚋgqlᚋgqlmodelᚐIntegrationPayload(ctx, field.Selections, res) } -func (ec *executionContext) fieldContext_Mutation_regenerateToken(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_Mutation_regenerateIntegrationToken(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ Object: "Mutation", Field: field, @@ -20109,7 +20170,7 @@ func (ec *executionContext) fieldContext_Mutation_regenerateToken(ctx context.Co } }() ctx = graphql.WithFieldContext(ctx, fc) - if fc.Args, err = ec.field_Mutation_regenerateToken_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + if fc.Args, err = ec.field_Mutation_regenerateIntegrationToken_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { ec.Error(ctx, err) return fc, err } @@ -21238,6 +21299,62 @@ func (ec *executionContext) fieldContext_Mutation_deleteProject(ctx context.Cont return fc, nil } +func (ec *executionContext) _Mutation_regeneratePublicApiToken(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Mutation_regeneratePublicApiToken(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().RegeneratePublicAPIToken(rctx, fc.Args["input"].(gqlmodel.RegeneratePublicAPITokenInput)) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*gqlmodel.ProjectPayload) + fc.Result = res + return ec.marshalOProjectPayload2ᚖgithubᚗcomᚋreearthᚋreearthᚑcmsᚋserverᚋinternalᚋadapterᚋgqlᚋgqlmodelᚐProjectPayload(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Mutation_regeneratePublicApiToken(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Mutation", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "project": + return ec.fieldContext_ProjectPayload_project(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type ProjectPayload", field.Name) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Mutation_regeneratePublicApiToken_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + func (ec *executionContext) _Mutation_createRequest(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { fc, err := ec.fieldContext_Mutation_createRequest(ctx, field) if err != nil { @@ -23265,6 +23382,8 @@ func (ec *executionContext) fieldContext_Project_publication(_ context.Context, return ec.fieldContext_ProjectPublication_scope(ctx, field) case "assetPublic": return ec.fieldContext_ProjectPublication_assetPublic(ctx, field) + case "token": + return ec.fieldContext_ProjectPublication_token(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type ProjectPublication", field.Name) }, @@ -23876,6 +23995,47 @@ func (ec *executionContext) fieldContext_ProjectPublication_assetPublic(_ contex return fc, nil } +func (ec *executionContext) _ProjectPublication_token(ctx context.Context, field graphql.CollectedField, obj *gqlmodel.ProjectPublication) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_ProjectPublication_token(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Token, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*string) + fc.Result = res + return ec.marshalOString2ᚖstring(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_ProjectPublication_token(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "ProjectPublication", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + func (ec *executionContext) _PublishItemPayload_items(ctx context.Context, field graphql.CollectedField, obj *gqlmodel.PublishItemPayload) (ret graphql.Marshaler) { fc, err := ec.fieldContext_PublishItemPayload_items(ctx, field) if err != nil { @@ -37862,8 +38022,8 @@ func (ec *executionContext) unmarshalInputPublishModelInput(ctx context.Context, return it, nil } -func (ec *executionContext) unmarshalInputRegenerateTokenInput(ctx context.Context, obj interface{}) (gqlmodel.RegenerateTokenInput, error) { - var it gqlmodel.RegenerateTokenInput +func (ec *executionContext) unmarshalInputRegenerateIntegrationTokenInput(ctx context.Context, obj interface{}) (gqlmodel.RegenerateIntegrationTokenInput, error) { + var it gqlmodel.RegenerateIntegrationTokenInput asMap := map[string]interface{}{} for k, v := range obj.(map[string]interface{}) { asMap[k] = v @@ -37889,6 +38049,33 @@ func (ec *executionContext) unmarshalInputRegenerateTokenInput(ctx context.Conte return it, nil } +func (ec *executionContext) unmarshalInputRegeneratePublicApiTokenInput(ctx context.Context, obj interface{}) (gqlmodel.RegeneratePublicAPITokenInput, error) { + var it gqlmodel.RegeneratePublicAPITokenInput + asMap := map[string]interface{}{} + for k, v := range obj.(map[string]interface{}) { + asMap[k] = v + } + + fieldsInOrder := [...]string{"projectId"} + for _, k := range fieldsInOrder { + v, ok := asMap[k] + if !ok { + continue + } + switch k { + case "projectId": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("projectId")) + data, err := ec.unmarshalNID2githubᚗcomᚋreearthᚋreearthᚑcmsᚋserverᚋinternalᚋadapterᚋgqlᚋgqlmodelᚐID(ctx, v) + if err != nil { + return it, err + } + it.ProjectID = data + } + } + + return it, nil +} + func (ec *executionContext) unmarshalInputRemoveIntegrationFromWorkspaceInput(ctx context.Context, obj interface{}) (gqlmodel.RemoveIntegrationFromWorkspaceInput, error) { var it gqlmodel.RemoveIntegrationFromWorkspaceInput asMap := map[string]interface{}{} @@ -44536,9 +44723,9 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { return ec._Mutation_deleteIntegration(ctx, field) }) - case "regenerateToken": + case "regenerateIntegrationToken": out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { - return ec._Mutation_regenerateToken(ctx, field) + return ec._Mutation_regenerateIntegrationToken(ctx, field) }) case "createWebhook": out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { @@ -44620,6 +44807,10 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { return ec._Mutation_deleteProject(ctx, field) }) + case "regeneratePublicApiToken": + out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { + return ec._Mutation_regeneratePublicApiToken(ctx, field) + }) case "createRequest": out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { return ec._Mutation_createRequest(ctx, field) @@ -45212,6 +45403,8 @@ func (ec *executionContext) _ProjectPublication(ctx context.Context, sel ast.Sel if out.Values[i] == graphql.Null { out.Invalids++ } + case "token": + out.Values[i] = ec._ProjectPublication_token(ctx, field, obj) default: panic("unknown field " + strconv.Quote(field.Name)) } @@ -50724,8 +50917,13 @@ func (ec *executionContext) unmarshalNPublishModelInput2githubᚗcomᚋreearth return res, graphql.ErrorOnPath(ctx, err) } -func (ec *executionContext) unmarshalNRegenerateTokenInput2githubᚗcomᚋreearthᚋreearthᚑcmsᚋserverᚋinternalᚋadapterᚋgqlᚋgqlmodelᚐRegenerateTokenInput(ctx context.Context, v interface{}) (gqlmodel.RegenerateTokenInput, error) { - res, err := ec.unmarshalInputRegenerateTokenInput(ctx, v) +func (ec *executionContext) unmarshalNRegenerateIntegrationTokenInput2githubᚗcomᚋreearthᚋreearthᚑcmsᚋserverᚋinternalᚋadapterᚋgqlᚋgqlmodelᚐRegenerateIntegrationTokenInput(ctx context.Context, v interface{}) (gqlmodel.RegenerateIntegrationTokenInput, error) { + res, err := ec.unmarshalInputRegenerateIntegrationTokenInput(ctx, v) + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) unmarshalNRegeneratePublicApiTokenInput2githubᚗcomᚋreearthᚋreearthᚑcmsᚋserverᚋinternalᚋadapterᚋgqlᚋgqlmodelᚐRegeneratePublicAPITokenInput(ctx context.Context, v interface{}) (gqlmodel.RegeneratePublicAPITokenInput, error) { + res, err := ec.unmarshalInputRegeneratePublicApiTokenInput(ctx, v) return res, graphql.ErrorOnPath(ctx, err) } diff --git a/server/internal/adapter/gql/gqlmodel/convert_project.go b/server/internal/adapter/gql/gqlmodel/convert_project.go index 7145ad31be..066fd436ca 100644 --- a/server/internal/adapter/gql/gqlmodel/convert_project.go +++ b/server/internal/adapter/gql/gqlmodel/convert_project.go @@ -29,9 +29,15 @@ func ToProjectPublication(p *project.Publication) *ProjectPublication { return nil } + token := lo.ToPtr(p.Token()) + if p.Scope() != project.PublicationScopeLimited { + token = nil + } + return &ProjectPublication{ Scope: ToProjectPublicationScope(p.Scope()), AssetPublic: p.AssetPublic(), + Token: token, } } diff --git a/server/internal/adapter/gql/gqlmodel/models_gen.go b/server/internal/adapter/gql/gqlmodel/models_gen.go index dae2e37e5f..cebafb0a12 100644 --- a/server/internal/adapter/gql/gqlmodel/models_gen.go +++ b/server/internal/adapter/gql/gqlmodel/models_gen.go @@ -784,6 +784,7 @@ type ProjectPayload struct { type ProjectPublication struct { Scope ProjectPublicationScope `json:"scope"` AssetPublic bool `json:"assetPublic"` + Token *string `json:"token,omitempty"` } type PublishItemInput struct { @@ -807,10 +808,14 @@ type PublishModelPayload struct { type Query struct { } -type RegenerateTokenInput struct { +type RegenerateIntegrationTokenInput struct { IntegrationID ID `json:"integrationId"` } +type RegeneratePublicAPITokenInput struct { + ProjectID ID `json:"projectId"` +} + type RemoveIntegrationFromWorkspaceInput struct { WorkspaceID ID `json:"workspaceId"` IntegrationID ID `json:"integrationId"` diff --git a/server/internal/adapter/gql/resolver_integration.go b/server/internal/adapter/gql/resolver_integration.go index 850c079b3a..1ff561d754 100644 --- a/server/internal/adapter/gql/resolver_integration.go +++ b/server/internal/adapter/gql/resolver_integration.go @@ -85,8 +85,8 @@ func (r *mutationResolver) DeleteIntegration(ctx context.Context, input gqlmodel }, nil } -// RegenerateToken is the resolver for the regenerateToken field. -func (r *mutationResolver) RegenerateToken(ctx context.Context, input gqlmodel.RegenerateTokenInput) (*gqlmodel.IntegrationPayload, error) { +// RegenerateIntegrationToken is the resolver for the regenerateToken field. +func (r *mutationResolver) RegenerateIntegrationToken(ctx context.Context, input gqlmodel.RegenerateIntegrationTokenInput) (*gqlmodel.IntegrationPayload, error) { iId, err := gqlmodel.ToID[id.Integration](input.IntegrationID) if err != nil { return nil, err diff --git a/server/internal/adapter/gql/resolver_project.go b/server/internal/adapter/gql/resolver_project.go index 3cb3fa9c1a..a0f8562c61 100644 --- a/server/internal/adapter/gql/resolver_project.go +++ b/server/internal/adapter/gql/resolver_project.go @@ -85,6 +85,21 @@ func (r *mutationResolver) DeleteProject(ctx context.Context, input gqlmodel.Del return &gqlmodel.DeleteProjectPayload{ProjectID: input.ProjectID}, nil } +// RegeneratePublicAPIToken is the resolver for the regeneratePublicApiToken field. +func (r *mutationResolver) RegeneratePublicAPIToken(ctx context.Context, input gqlmodel.RegeneratePublicAPITokenInput) (*gqlmodel.ProjectPayload, error) { + pid, err := gqlmodel.ToID[id.Project](input.ProjectID) + if err != nil { + return nil, err + } + + p, err := usecases(ctx).Project.RegenerateToken(ctx, pid, getOperator(ctx)) + if err != nil { + return nil, err + } + + return &gqlmodel.ProjectPayload{Project: gqlmodel.ToProject(p)}, nil +} + // Workspace is the resolver for the workspace field. func (r *projectResolver) Workspace(ctx context.Context, obj *gqlmodel.Project) (*gqlmodel.Workspace, error) { return dataloaders(ctx).Workspace.Load(obj.WorkspaceID) diff --git a/server/internal/adapter/publicapi/controller.go b/server/internal/adapter/publicapi/controller.go index 0bb8d432f7..c64e133b53 100644 --- a/server/internal/adapter/publicapi/controller.go +++ b/server/internal/adapter/publicapi/controller.go @@ -4,6 +4,7 @@ import ( "context" "errors" + "github.com/reearth/reearth-cms/server/internal/adapter" "github.com/reearth/reearth-cms/server/internal/usecase/interfaces" "github.com/reearth/reearth-cms/server/internal/usecase/repo" "github.com/reearth/reearth-cms/server/pkg/asset" @@ -37,17 +38,15 @@ func (c *Controller) checkProject(ctx context.Context, prj string) (*project.Pro return nil, ErrInvalidProject } - if p := pr.Publication(); p == nil || p.Scope() != project.PublicationScopePublic { + if p := pr.Publication(); p == nil || p.Scope() == project.PublicationScopePrivate { return nil, rerror.ErrNotFound } - // TODO: check token if the scope is limited - // if pr.Publication().Scope() == project.PublicationScopeLimited { - // t := pr.Publication().Token() - // if op := adapter.Operator(ctx); op == nil || t == "" || op.PublicAPIToken != t { - // return nil, ErrInvalidProject - // } - // } + if pr.Publication().Scope() == project.PublicationScopeLimited { + if op := adapter.Operator(ctx); op == nil || !op.IsReadableProject(pr.ID()) { + return nil, ErrInvalidProject + } + } return pr, nil } diff --git a/server/internal/app/app.go b/server/internal/app/app.go index 040daae81e..87f33b9e87 100644 --- a/server/internal/app/app.go +++ b/server/internal/app/app.go @@ -80,7 +80,7 @@ func initEcho(cfg *ServerConfig) *echo.Echo { ) api.POST("/signup", Signup(), usecaseMiddleware) - publicapi.Echo(api.Group("/p", PublicAPIAuthMiddleware(cfg), usecaseMiddleware)) + publicapi.Echo(api.Group("/p", publicAPIAuthMiddleware(cfg), usecaseMiddleware)) integration.RegisterHandlers(api.Group( "", authMiddleware(cfg), diff --git a/server/internal/app/auth_client.go b/server/internal/app/auth_client.go index 1e30c75e7c..53d99675c7 100644 --- a/server/internal/app/auth_client.go +++ b/server/internal/app/auth_client.go @@ -11,6 +11,7 @@ import ( "github.com/reearth/reearth-cms/server/internal/usecase" "github.com/reearth/reearth-cms/server/pkg/id" "github.com/reearth/reearth-cms/server/pkg/integration" + "github.com/reearth/reearth-cms/server/pkg/project" "github.com/reearth/reearthx/account/accountdomain" "github.com/reearth/reearthx/account/accountdomain/user" "github.com/reearth/reearthx/account/accountdomain/workspace" @@ -89,7 +90,7 @@ func attachUserOperator(ctx context.Context, req *http.Request, cfg *ServerConfi func attachIntegrationOperator(ctx context.Context, req *http.Request, cfg *ServerConfig) (context.Context, error) { var i *integration.Integration - if token := getIntegrationToken(req); token != "" { + if token := getToken(req); token != "" { var err error i, err = cfg.Repos.Integration.FindByToken(ctx, token) if err != nil { @@ -126,37 +127,36 @@ func attachIntegrationOperator(ctx context.Context, req *http.Request, cfg *Serv return ctx, nil } -func PublicAPIAuthMiddleware(cfg *ServerConfig) echo.MiddlewareFunc { +func publicAPIAuthMiddleware(cfg *ServerConfig) echo.MiddlewareFunc { return func(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { - // TODO: support limit publication scope - - // req := c.Request() - // ctx := req.Context() - // if token := req.Header.Get("Reearth-Token"); token != "" { - // ws, err := cfg.Repos.Project.FindByPublicAPIToken(ctx, token) - // if err != nil { - // if errors.Is(err, rerror.ErrNotFound) { - // return c.JSON(http.StatusUnauthorized, map[string]string{"error": "invalid token"}) - // } - // return err - // } - - // if !ws.IsPublic() { - // return c.JSON(http.StatusUnauthorized, map[string]string{"error": "invalid token"}) - // } - - // c.SetRequest(req.WithContext(adapter.AttachOperator(ctx, &usecase.Operator{ - // PublicAPIProject: ws.ID(), - // }))) - // } + req := c.Request() + ctx := req.Context() + if token := getToken(req); token != "" { + p, err := cfg.Repos.Project.FindByPublicAPIToken(ctx, token) + if err != nil { + if errors.Is(err, rerror.ErrNotFound) { + return c.JSON(http.StatusUnauthorized, map[string]string{"error": "invalid token"}) + } + return err + } + + if p.Publication() == nil || p.Publication().Scope() == project.PublicationScopePrivate { + return c.JSON(http.StatusUnauthorized, map[string]string{"error": "invalid project"}) + } + + defaultLang := req.Header.Get("Accept-Language") + op := generatePublicApiOperator(p, defaultLang) + ctx = adapter.AttachOperator(ctx, op) + c.SetRequest(req.WithContext(ctx)) + } return next(c) } } } -func getIntegrationToken(req *http.Request) string { +func getToken(req *http.Request) string { token := strings.TrimPrefix(req.Header.Get("authorization"), "Bearer ") if strings.HasPrefix(token, "secret_") { return token @@ -293,6 +293,28 @@ func generateIntegrationOperator(ctx context.Context, cfg *ServerConfig, i *inte }, nil } +func generatePublicApiOperator(p *project.Project, lang string) *usecase.Operator { + if p == nil { + return nil + } + + return &usecase.Operator{ + AcOperator: &accountusecase.Operator{ + User: nil, + ReadableWorkspaces: id.WorkspaceIDList{p.Workspace()}, + WritableWorkspaces: nil, + MaintainableWorkspaces: nil, + OwningWorkspaces: nil, + }, + Integration: nil, + Lang: lang, + ReadableProjects: id.ProjectIDList{p.ID()}, + WritableProjects: nil, + MaintainableProjects: nil, + OwningProjects: nil, + } +} + func AuthRequiredMiddleware() echo.MiddlewareFunc { return func(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { diff --git a/server/internal/infrastructure/memory/project.go b/server/internal/infrastructure/memory/project.go index bb4f699133..4d92522c40 100644 --- a/server/internal/infrastructure/memory/project.go +++ b/server/internal/infrastructure/memory/project.go @@ -129,6 +129,22 @@ func (r *Project) FindByPublicName(_ context.Context, name string) (*project.Pro return nil, rerror.ErrNotFound } + +func (r *Project) FindByPublicAPIToken(ctx context.Context, token string) (*project.Project, error) { + if r.err != nil { + return nil, r.err + } + + p := r.data.Find(func(_ id.ProjectID, v *project.Project) bool { + return v.Publication().Token() == token && r.f.CanRead(v.Workspace()) + }) + + if p != nil { + return p, nil + } + return nil, rerror.ErrNotFound +} + func (r *Project) CountByWorkspace(_ context.Context, workspace accountdomain.WorkspaceID) (c int, err error) { if r.err != nil { return 0, r.err diff --git a/server/internal/infrastructure/memory/project_test.go b/server/internal/infrastructure/memory/project_test.go index 448d131035..c99b948a75 100644 --- a/server/internal/infrastructure/memory/project_test.go +++ b/server/internal/infrastructure/memory/project_test.go @@ -939,3 +939,82 @@ func TestProjectRepo_Save(t *testing.T) { }) } } + +func TestProject_FindByPublicAPIToken(t *testing.T) { + mocknow := time.Now().Truncate(time.Millisecond).UTC() + tid1 := accountdomain.NewWorkspaceID() + id1 := id.NewProjectID() + pub := project.NewPublication(project.PublicationScopeLimited, false) + p1 := project.New(). + ID(id1). + Workspace(tid1). + UpdatedAt(mocknow). + Publication(pub). + MustBuild() + + tests := []struct { + name string + seeds project.List + arg string + want *project.Project + wantErr error + mockErr bool + }{ + { + name: "Not found in empty db", + seeds: project.List{}, + arg: "xyz123", + want: nil, + wantErr: rerror.ErrNotFound, + }, + { + name: "Not found", + seeds: project.List{ + p1, + }, + arg: "", + want: nil, + wantErr: rerror.ErrNotFound, + }, + { + name: "Found 1", + seeds: project.List{ + p1, + }, + arg: pub.Token(), + want: p1, + wantErr: nil, + }, + { + name: "must mock error", + wantErr: errors.New("test"), + mockErr: true, + }, + } + + for _, tc := range tests { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + r := NewProject() + if tc.mockErr { + SetProjectError(r, tc.wantErr) + } + defer MockProjectNow(r, mocknow)() + ctx := context.Background() + for _, p := range tc.seeds { + err := r.Save(ctx, p.Clone()) + assert.NoError(t, err) + } + + got, err := r.FindByPublicAPIToken(ctx, tc.arg) + if tc.wantErr != nil { + assert.ErrorIs(t, err, tc.wantErr) + return + } + assert.NoError(t, err) + assert.Equal(t, tc.want, got) + }) + } +} diff --git a/server/internal/infrastructure/mongo/mongodoc/project.go b/server/internal/infrastructure/mongo/mongodoc/project.go index 904cb3a270..47d26ac471 100644 --- a/server/internal/infrastructure/mongo/mongodoc/project.go +++ b/server/internal/infrastructure/mongo/mongodoc/project.go @@ -26,6 +26,7 @@ type ProjectDocument struct { type ProjectPublicationDocument struct { AssetPublic bool Scope string + Token string } func NewProject(project *project.Project) (*ProjectDocument, string) { @@ -57,6 +58,7 @@ func NewProjectPublication(p *project.Publication) *ProjectPublicationDocument { return &ProjectPublicationDocument{ AssetPublic: p.AssetPublic(), Scope: string(p.Scope()), + Token: p.Token(), } } @@ -94,7 +96,7 @@ func (d *ProjectPublicationDocument) Model() *project.Publication { if d == nil { return nil } - return project.NewPublication(project.PublicationScope(d.Scope), d.AssetPublic) + return project.NewPublicationWithToken(project.PublicationScope(d.Scope), d.AssetPublic, d.Token) } type ProjectConsumer = mongox.SliceFuncConsumer[*ProjectDocument, *project.Project] diff --git a/server/internal/infrastructure/mongo/project.go b/server/internal/infrastructure/mongo/project.go index 54731afbcd..97b8962377 100644 --- a/server/internal/infrastructure/mongo/project.go +++ b/server/internal/infrastructure/mongo/project.go @@ -16,7 +16,8 @@ import ( ) var ( - projectIndexes = []string{"workspace"} + // TODO: the `publication.token` should be unique, this should be fixed in the future + projectIndexes = []string{"workspace", "publication.token"} projectUniqueIndexes = []string{"id"} ) @@ -98,6 +99,15 @@ func (r *ProjectRepo) FindByPublicName(ctx context.Context, name string) (*proje }) } +func (r *ProjectRepo) FindByPublicAPIToken(ctx context.Context, token string) (*project.Project, error) { + if token == "" { + return nil, rerror.ErrNotFound + } + return r.findOne(ctx, bson.M{ + "publication.token": token, + }) +} + func (r *ProjectRepo) CountByWorkspace(ctx context.Context, workspace accountdomain.WorkspaceID) (int, error) { count, err := r.client.Count(ctx, bson.M{ "workspace": workspace.String(), diff --git a/server/internal/infrastructure/mongo/project_test.go b/server/internal/infrastructure/mongo/project_test.go index 1c3dd0914c..71c978b16c 100644 --- a/server/internal/infrastructure/mongo/project_test.go +++ b/server/internal/infrastructure/mongo/project_test.go @@ -892,3 +892,78 @@ func Test_projectRepo_Save(t *testing.T) { }) } } + +func TestProjectRepo_FindByPublicAPIToken(t *testing.T) { + tid1 := accountdomain.NewWorkspaceID() + id1 := id.NewProjectID() + now := time.Now().Truncate(time.Millisecond).UTC() + pub := project.NewPublication(project.PublicationScopeLimited, false) + p1 := project.New().ID(id1).Workspace(tid1).Publication(pub).UpdatedAt(now).MustBuild() + tests := []struct { + name string + seeds project.List + arg string + want *project.Project + wantErr error + }{ + { + name: "Not found in empty db", + seeds: project.List{}, + arg: pub.Token(), + want: nil, + wantErr: rerror.ErrNotFound, + }, + { + name: "Not found", + seeds: project.List{ + p1, + }, + arg: "xxx", + want: nil, + wantErr: rerror.ErrNotFound, + }, + { + name: "Found 1", + seeds: project.List{ + p1, + }, + arg: pub.Token(), + want: p1, + wantErr: nil, + }, + { + name: "Project exists but not public", + seeds: project.List{ + project.New().ID(id.NewProjectID()).Workspace(tid1).UpdatedAt(now).MustBuild(), + }, + arg: "some_token", + want: nil, + wantErr: rerror.ErrNotFound, + }, + } + + initDB := mongotest.Connect(t) + + for _, tc := range tests { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + client := mongox.NewClientWithDatabase(initDB(t)) + + r := NewProject(client) + ctx := context.Background() + for _, p := range tc.seeds { + err := r.Save(ctx, p) + assert.NoError(t, err) + } + + got, err := r.FindByPublicAPIToken(ctx, tc.arg) + if tc.wantErr != nil { + assert.ErrorIs(t, err, tc.wantErr) + return + } + assert.Equal(t, tc.want, got) + }) + } +} diff --git a/server/internal/usecase/interactor/project.go b/server/internal/usecase/interactor/project.go index 348da4902b..8b7fa633f6 100644 --- a/server/internal/usecase/interactor/project.go +++ b/server/internal/usecase/interactor/project.go @@ -3,6 +3,7 @@ package interactor import ( "context" "errors" + "time" "github.com/reearth/reearth-cms/server/internal/usecase" "github.com/reearth/reearth-cms/server/internal/usecase/gateway" @@ -164,3 +165,33 @@ func (i *Project) Delete(ctx context.Context, projectID id.ProjectID, operator * return nil }) } + +func (i *Project) RegenerateToken(ctx context.Context, pId id.ProjectID, operator *usecase.Operator) (*project.Project, error) { + if operator.AcOperator.User == nil { + return nil, interfaces.ErrInvalidOperator + } + return Run1(ctx, operator, i.repos, Usecase().Transaction(), + func(ctx context.Context) (*project.Project, error) { + p, err := i.repos.Project.FindByID(ctx, pId) + if err != nil { + return nil, err + } + + // check if the user is the owner of the project + if !operator.IsOwningProject(p.ID()) { + return nil, interfaces.ErrOperationDenied + } + + if p.Publication() == nil || p.Publication().Scope() != project.PublicationScopeLimited { + return nil, interfaces.ErrInvalidProject + } + + p.Publication().GenerateToken() + p.SetUpdatedAt(time.Now()) + if err := i.repos.Project.Save(ctx, p); err != nil { + return nil, err + } + + return p, nil + }) +} diff --git a/server/internal/usecase/interfaces/project.go b/server/internal/usecase/interfaces/project.go index e992af32ae..b521615e5c 100644 --- a/server/internal/usecase/interfaces/project.go +++ b/server/internal/usecase/interfaces/project.go @@ -38,6 +38,7 @@ type UpdateProjectPublicationParam struct { var ( ErrProjectAliasIsNotSet error = rerror.NewE(i18n.T("project alias is not set")) ErrProjectAliasAlreadyUsed error = rerror.NewE(i18n.T("project alias is already used by another project")) + ErrInvalidProject = rerror.NewE(i18n.T("invalid project")) ) type Project interface { @@ -48,4 +49,5 @@ type Project interface { Update(context.Context, UpdateProjectParam, *usecase.Operator) (*project.Project, error) CheckAlias(context.Context, string) (bool, error) Delete(context.Context, id.ProjectID, *usecase.Operator) error + RegenerateToken(context.Context, id.ProjectID, *usecase.Operator) (*project.Project, error) } diff --git a/server/internal/usecase/repo/project.go b/server/internal/usecase/repo/project.go index 3cb0133f11..46b4025aec 100644 --- a/server/internal/usecase/repo/project.go +++ b/server/internal/usecase/repo/project.go @@ -17,6 +17,7 @@ type Project interface { FindByWorkspaces(context.Context, accountdomain.WorkspaceIDList, *usecasex.Pagination) (project.List, *usecasex.PageInfo, error) FindByPublicName(context.Context, string) (*project.Project, error) CountByWorkspace(context.Context, accountdomain.WorkspaceID) (int, error) + FindByPublicAPIToken(context.Context, string) (*project.Project, error) Save(context.Context, *project.Project) error Remove(context.Context, id.ProjectID) error } diff --git a/server/pkg/integration/integration.go b/server/pkg/integration/integration.go index 5265f67ec1..8c35bc3610 100644 --- a/server/pkg/integration/integration.go +++ b/server/pkg/integration/integration.go @@ -1,8 +1,6 @@ package integration import ( - "crypto/rand" - "math/big" "net/url" "time" @@ -11,6 +9,8 @@ import ( "golang.org/x/exp/slices" ) +const charSet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + type Integration struct { id ID name string @@ -68,11 +68,7 @@ func (i *Integration) SetToken(token string) { } func (i *Integration) RandomToken() { - t, err := randomString(43) - if err != nil { - return - } - i.token = "secret_" + t + i.token = "secret_" + lo.RandomString(43, []rune(charSet)) } func (i *Integration) Developer() UserID { @@ -159,17 +155,3 @@ func (i *Integration) Clone() *Integration { updatedAt: i.updatedAt, } } - -func randomString(n int) (string, error) { - const letters = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" - result := make([]byte, n) - for i := 0; i < n; i++ { - randIndex, err := rand.Int(rand.Reader, big.NewInt(int64(len(letters)))) - if err != nil { - return "", err - } - result[i] = letters[randIndex.Int64()] - } - - return string(result), nil -} diff --git a/server/pkg/project/publication.go b/server/pkg/project/publication.go index 3ef30a6136..3d5a3e6890 100644 --- a/server/pkg/project/publication.go +++ b/server/pkg/project/publication.go @@ -1,16 +1,23 @@ package project +import ( + "github.com/samber/lo" +) + const ( PublicationScopePrivate PublicationScope = "private" PublicationScopeLimited PublicationScope = "limited" PublicationScopePublic PublicationScope = "public" ) +const charSet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + type PublicationScope string type Publication struct { scope PublicationScope assetPublic bool + token string } func NewPublication(scope PublicationScope, assetPublic bool) *Publication { @@ -20,6 +27,15 @@ func NewPublication(scope PublicationScope, assetPublic bool) *Publication { return p } +func NewPublicationWithToken(scope PublicationScope, assetPublic bool, token string) *Publication { + p := &Publication{ + token: token, + } + p.SetScope(scope) + p.SetAssetPublic(assetPublic) + return p +} + func (p *Publication) Scope() PublicationScope { if p.scope == "" { return PublicationScopePrivate @@ -31,10 +47,21 @@ func (p *Publication) AssetPublic() bool { return p.assetPublic } +func (p *Publication) Token() string { + return p.token +} + +func (p *Publication) GenerateToken() { + p.token = "secret_" + lo.RandomString(43, []rune(charSet)) +} + func (p *Publication) SetScope(scope PublicationScope) { if scope != PublicationScopePrivate && scope != PublicationScopeLimited && scope != PublicationScopePublic { scope = PublicationScopePrivate } + if scope == PublicationScopeLimited && p.token == "" { + p.GenerateToken() + } p.scope = scope } @@ -51,5 +78,6 @@ func (p *Publication) Clone() *Publication { return &Publication{ scope: p.scope, assetPublic: p.assetPublic, + token: p.token, } } diff --git a/server/pkg/project/publication_test.go b/server/pkg/project/publication_test.go index b09a1fd996..86869436a4 100644 --- a/server/pkg/project/publication_test.go +++ b/server/pkg/project/publication_test.go @@ -11,10 +11,12 @@ func TestNewPublication(t *testing.T) { scope: PublicationScopePrivate, assetPublic: false, }, NewPublication(PublicationScopePrivate, false)) - assert.Equal(t, &Publication{ - scope: PublicationScopeLimited, - assetPublic: true, - }, NewPublication(PublicationScopeLimited, true)) + + got := NewPublication(PublicationScopeLimited, true) + assert.Equal(t, PublicationScopeLimited, got.Scope()) + assert.Equal(t, true, got.AssetPublic()) + assert.NotEmpty(t, got.Token()) + assert.Equal(t, &Publication{ scope: PublicationScopePublic, assetPublic: false, @@ -47,6 +49,7 @@ func TestPublication_SetScope(t *testing.T) { p.SetScope(PublicationScopeLimited) assert.Equal(t, &Publication{ scope: PublicationScopeLimited, + token: p.Token(), }, p) p = &Publication{} @@ -92,3 +95,25 @@ func TestPublication_Clone(t *testing.T) { assert.NotSame(t, p, p2) assert.Nil(t, (*Publication)(nil).Clone()) } + +func TestNewPublicationWithToken(t *testing.T) { + assert.Equal(t, &Publication{ + scope: PublicationScopePrivate, + assetPublic: false, + token: "xxx", + }, NewPublicationWithToken(PublicationScopePrivate, false, "xxx")) + assert.Equal(t, &Publication{ + scope: PublicationScopeLimited, + assetPublic: true, + token: "xxx", + }, NewPublicationWithToken(PublicationScopeLimited, true, "xxx")) + assert.Equal(t, &Publication{ + scope: PublicationScopePublic, + assetPublic: false, + token: "xxx", + }, NewPublicationWithToken(PublicationScopePublic, false, "xxx")) + assert.Equal(t, &Publication{ + scope: PublicationScopePrivate, + assetPublic: true, + }, NewPublicationWithToken("", true, "")) +} diff --git a/server/schemas/integration.graphql b/server/schemas/integration.graphql index 27f1e1440f..0c61d731f1 100644 --- a/server/schemas/integration.graphql +++ b/server/schemas/integration.graphql @@ -41,7 +41,7 @@ input DeleteIntegrationInput { integrationId: ID! } -input RegenerateTokenInput { +input RegenerateIntegrationTokenInput { integrationId: ID! } @@ -60,5 +60,5 @@ extend type Mutation { createIntegration(input: CreateIntegrationInput!): IntegrationPayload updateIntegration(input: UpdateIntegrationInput!): IntegrationPayload deleteIntegration(input: DeleteIntegrationInput!): DeleteIntegrationPayload - regenerateToken(input: RegenerateTokenInput!): IntegrationPayload + regenerateIntegrationToken(input: RegenerateIntegrationTokenInput!): IntegrationPayload } diff --git a/server/schemas/project.graphql b/server/schemas/project.graphql index f0a31b95e5..e3a4d98c96 100644 --- a/server/schemas/project.graphql +++ b/server/schemas/project.graphql @@ -12,6 +12,7 @@ enum ProjectPublicationScope { type ProjectPublication { scope: ProjectPublicationScope! assetPublic: Boolean! + token: String } type Project implements Node { @@ -54,6 +55,10 @@ input DeleteProjectInput { projectId: ID! } +input RegeneratePublicApiTokenInput { + projectId: ID! +} + # Payload type ProjectPayload { project: Project! @@ -84,4 +89,5 @@ extend type Mutation { createProject(input: CreateProjectInput!): ProjectPayload updateProject(input: UpdateProjectInput!): ProjectPayload deleteProject(input: DeleteProjectInput!): DeleteProjectPayload + regeneratePublicApiToken(input: RegeneratePublicApiTokenInput!): ProjectPayload }