@saluzi/saluzi-edu 0.2.9 → 0.2.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +336 -333
- package/dist/rcs-web/assets/Dashboard-BwotBWlJ.js +1 -0
- package/dist/rcs-web/assets/Join-4yrksbdX.js +1 -0
- package/dist/rcs-web/assets/Login-Cxm_yEeE.js +1 -0
- package/dist/rcs-web/assets/SessionDetail-DNdfPOYy.js +50 -0
- package/dist/rcs-web/assets/Setup-BiAAxugt.js +1 -0
- package/dist/rcs-web/assets/ShareLanding-CP3Z6ckr.js +1 -0
- package/dist/rcs-web/assets/TeamCreate-DgdCwRSb.js +1 -0
- package/dist/rcs-web/assets/TeamDetail-CkNTFoMB.js +11 -0
- package/dist/rcs-web/assets/TeamList-Bf0sQ5RQ.js +1 -0
- package/dist/rcs-web/assets/TeamSettings-BaNAfXU3.js +1 -0
- package/dist/rcs-web/assets/UserSettings-CsutQjxG.js +1 -0
- package/{packages/remote-control-server/web/dist/assets/arc-D90597o-.js → dist/rcs-web/assets/arc-BxxmNQO6.js} +1 -1
- package/dist/rcs-web/assets/architectureDiagram-3BPJPVTR-DBjkRdbd.js +36 -0
- package/dist/rcs-web/assets/{blockDiagram-GPEHLZMM-DHd8IbOs.js → blockDiagram-GPEHLZMM-BF6COWlM.js} +2 -2
- package/dist/rcs-web/assets/{c4Diagram-AAUBKEIU-CjFkQJ9h.js → c4Diagram-AAUBKEIU-DHOf390v.js} +2 -2
- package/dist/rcs-web/assets/channel-JsvbMQjW.js +1 -0
- package/dist/rcs-web/assets/{chunk-2J33WTMH-C21XWnGZ.js → chunk-2J33WTMH-CeJN8yjM.js} +1 -1
- package/dist/rcs-web/assets/{chunk-4BX2VUAB-B3SXiJlt.js → chunk-4BX2VUAB-jP76_P2A.js} +1 -1
- package/dist/rcs-web/assets/{chunk-55IACEB6-DjImv45F.js → chunk-55IACEB6-BsFA6emB.js} +1 -1
- package/{packages/remote-control-server/web/dist/assets/chunk-727SXJPM-DtnaqsyD.js → dist/rcs-web/assets/chunk-727SXJPM-BF4HrQjZ.js} +1 -1
- package/dist/rcs-web/assets/{chunk-AQP2D5EJ-BVFgH5mS.js → chunk-AQP2D5EJ-DToJ74Kc.js} +1 -1
- package/dist/rcs-web/assets/{chunk-FMBD7UC4-Bl8jgb57.js → chunk-FMBD7UC4-DVTARgsa.js} +1 -1
- package/dist/rcs-web/assets/{chunk-ND2GUHAM-BpK111zh.js → chunk-ND2GUHAM-C50Gqv9c.js} +1 -1
- package/dist/rcs-web/assets/{chunk-QZHKN3VN-ruhIHBim.js → chunk-QZHKN3VN-EsIIh6zV.js} +1 -1
- package/dist/rcs-web/assets/classDiagram-4FO5ZUOK-BXn8Uc4c.js +1 -0
- package/dist/rcs-web/assets/classDiagram-v2-Q7XG4LA2-BXn8Uc4c.js +1 -0
- package/dist/rcs-web/assets/code-block-IT6T5CEO-BnufHNTT.js +2 -0
- package/dist/rcs-web/assets/cose-bilkent-S5V4N54A-M6tNt14I.js +1 -0
- package/dist/rcs-web/assets/dagre-BM42HDAG-DaTm29qG.js +4 -0
- package/dist/rcs-web/assets/diagram-2AECGRRQ-fqoeOvq2.js +43 -0
- package/dist/rcs-web/assets/diagram-5GNKFQAL-6s2CiaGG.js +10 -0
- package/dist/rcs-web/assets/{diagram-KO2AKTUF-Uy3PDP5S.js → diagram-KO2AKTUF-C0aw_N95.js} +2 -2
- package/dist/rcs-web/assets/diagram-LMA3HP47-C8_Rv3rB.js +24 -0
- package/dist/rcs-web/assets/diagram-OG6HWLK6-Dae4u8Uc.js +24 -0
- package/{packages/remote-control-server/web/dist/assets/erDiagram-TEJ5UH35-sre_MTeK.js → dist/rcs-web/assets/erDiagram-TEJ5UH35-CosS5VWr.js} +13 -13
- package/dist/rcs-web/assets/{flowDiagram-I6XJVG4X-DwrWaLoe.js → flowDiagram-I6XJVG4X-D_Mxs46-.js} +2 -2
- package/dist/rcs-web/assets/ganttDiagram-6RSMTGT7-B0jy8Am_.js +292 -0
- package/dist/rcs-web/assets/gitGraphDiagram-PVQCEYII-CSdu2HVI.js +106 -0
- package/dist/rcs-web/assets/i18n-Dbt-vEQ3.js +1 -0
- package/dist/rcs-web/assets/infoDiagram-5YYISTIA-Dcbc9L1S.js +2 -0
- package/{packages/remote-control-server/web/dist/assets/ishikawaDiagram-YF4QCWOH-SO0nn3--.js → dist/rcs-web/assets/ishikawaDiagram-YF4QCWOH-BLMjFE4t.js} +2 -2
- package/dist/rcs-web/assets/{journeyDiagram-JHISSGLW-D4igFUd4.js → journeyDiagram-JHISSGLW-Dw4nh1xI.js} +2 -2
- package/dist/rcs-web/assets/{kanban-definition-UN3LZRKU-Cm_yKsZC.js → kanban-definition-UN3LZRKU-DkU3j_ly.js} +2 -2
- package/dist/rcs-web/assets/{linear-4IMGll_E.js → linear-N5Cify7M.js} +1 -1
- package/dist/rcs-web/assets/main-30YqUvTd.js +238 -0
- package/dist/rcs-web/assets/main-EP6Lac2m.css +1 -0
- package/{packages/remote-control-server/web/dist/assets/mermaid-VLURNSYL-D_yQ7nAd.js → dist/rcs-web/assets/mermaid-VLURNSYL-gfiJUpl_.js} +5 -5
- package/dist/rcs-web/assets/{mermaid.core-B1qOUdiU.js → mermaid.core-B_Xc65Bf.js} +5 -5
- package/dist/rcs-web/assets/{mindmap-definition-RKZ34NQL-yETom5qt.js → mindmap-definition-RKZ34NQL-DUavHfNu.js} +2 -2
- package/{packages/remote-control-server/web/dist/assets/pieDiagram-4H26LBE5-KIhqXGT6.js → dist/rcs-web/assets/pieDiagram-4H26LBE5-DSJaToTU.js} +2 -2
- package/{packages/remote-control-server/web/dist/assets/quadrantDiagram-W4KKPZXB-DISqmoA6.js → dist/rcs-web/assets/quadrantDiagram-W4KKPZXB-BFp6TnHj.js} +2 -2
- package/dist/rcs-web/assets/{requirementDiagram-4Y6WPE33-DsgT4ptk.js → requirementDiagram-4Y6WPE33-eil6F3-O.js} +2 -2
- package/{packages/remote-control-server/web/dist/assets/sankeyDiagram-5OEKKPKP-Dn67kbn0.js → dist/rcs-web/assets/sankeyDiagram-5OEKKPKP-DbGhoepK.js} +2 -2
- package/{packages/remote-control-server/web/dist/assets/sequenceDiagram-3UESZ5HK-CtWaXZHl.js → dist/rcs-web/assets/sequenceDiagram-3UESZ5HK-B0YrN1ud.js} +14 -14
- package/dist/rcs-web/assets/stateDiagram-AJRCARHV-CofFPj3N.js +1 -0
- package/dist/rcs-web/assets/stateDiagram-v2-BHNVJYJU-DPlcvPn1.js +1 -0
- package/{packages/remote-control-server/web/dist/assets/timeline-definition-PNZ67QCA-Dk5WUenC.js → dist/rcs-web/assets/timeline-definition-PNZ67QCA-Bx4dusy8.js} +2 -2
- package/dist/rcs-web/assets/useTeams-MDI4djXg.js +1 -0
- package/{packages/remote-control-server/web/dist/assets/vennDiagram-CIIHVFJN-TY8EiRrC.js → dist/rcs-web/assets/vennDiagram-CIIHVFJN-B_-3l-MF.js} +2 -2
- package/dist/rcs-web/assets/{wardleyDiagram-YWT4CUSO-BK3oi47X.js → wardleyDiagram-YWT4CUSO-DNSn-pGp.js} +2 -2
- package/dist/rcs-web/assets/workerType-OM09y7Lk.js +1 -0
- package/{packages/remote-control-server/web/dist/assets/xychartDiagram-2RQKCTM6-DhsxqX67.js → dist/rcs-web/assets/xychartDiagram-2RQKCTM6-TqUbNp0j.js} +2 -2
- package/dist/rcs-web/index.html +2 -3
- package/package.json +2 -1
- package/packages/remote-control-server/src/__tests__/acp-ws-handler.test.ts +7 -19
- package/packages/remote-control-server/src/__tests__/auth-login.test.ts +561 -0
- package/packages/remote-control-server/src/__tests__/auth-security.test.ts +547 -0
- package/packages/remote-control-server/src/__tests__/auth.test.ts +7 -18
- package/packages/remote-control-server/src/__tests__/bind-replacement.test.ts +251 -0
- package/packages/remote-control-server/src/__tests__/cookie.test.ts +155 -0
- package/packages/remote-control-server/src/__tests__/data-layer-fixes.test.ts +147 -0
- package/packages/remote-control-server/src/__tests__/db.test.ts +582 -0
- package/packages/remote-control-server/src/__tests__/design-gaps.test.ts +440 -0
- package/packages/remote-control-server/src/__tests__/disconnect-monitor.test.ts +2 -18
- package/packages/remote-control-server/src/__tests__/helpers/db.ts +43 -0
- package/packages/remote-control-server/src/__tests__/helpers/fixtures.ts +94 -0
- package/packages/remote-control-server/src/__tests__/helpers/mock-config.ts +31 -0
- package/packages/remote-control-server/src/__tests__/middleware.test.ts +4 -112
- package/packages/remote-control-server/src/__tests__/password.test.ts +76 -0
- package/packages/remote-control-server/src/__tests__/phase4-cleanup.test.ts +290 -0
- package/packages/remote-control-server/src/__tests__/phase4-integration.test.ts +224 -0
- package/packages/remote-control-server/src/__tests__/routes.test.ts +292 -207
- package/packages/remote-control-server/src/__tests__/services.test.ts +2 -18
- package/packages/remote-control-server/src/__tests__/session-visibility.test.ts +452 -0
- package/packages/remote-control-server/src/__tests__/session.test.ts +216 -0
- package/packages/remote-control-server/src/__tests__/shares.test.ts +1419 -0
- package/packages/remote-control-server/src/__tests__/sse-writer.test.ts +2 -18
- package/packages/remote-control-server/src/__tests__/teams.test.ts +2089 -0
- package/packages/remote-control-server/src/__tests__/work-dispatch.test.ts +2 -18
- package/packages/remote-control-server/src/__tests__/worker-token-removal.test.ts +487 -0
- package/packages/remote-control-server/src/__tests__/ws-handler.test.ts +2 -18
- package/packages/remote-control-server/src/auth/constants.ts +13 -0
- package/packages/remote-control-server/src/auth/cookie.ts +75 -0
- package/packages/remote-control-server/src/auth/middleware.ts +75 -165
- package/packages/remote-control-server/src/auth/password.ts +116 -0
- package/packages/remote-control-server/src/auth/session.ts +130 -0
- package/packages/remote-control-server/src/db/schema.sql +116 -0
- package/packages/remote-control-server/src/db/sqlite.ts +99 -0
- package/packages/remote-control-server/src/routes/acp/index.ts +6 -11
- package/packages/remote-control-server/src/routes/v1/environments.ts +111 -8
- package/packages/remote-control-server/src/routes/web/auth-routes.ts +634 -0
- package/packages/remote-control-server/src/routes/web/control.ts +5 -5
- package/packages/remote-control-server/src/routes/web/environments.ts +5 -27
- package/packages/remote-control-server/src/routes/web/sessions.ts +19 -41
- package/packages/remote-control-server/src/routes/web/shares.ts +371 -0
- package/packages/remote-control-server/src/routes/web/teams.ts +850 -0
- package/packages/remote-control-server/src/services/environment.ts +1 -4
- package/packages/remote-control-server/src/services/session-visibility.ts +79 -0
- package/packages/remote-control-server/src/services/session.ts +0 -13
- package/packages/remote-control-server/src/store.ts +150 -56
- package/packages/remote-control-server/src/types/api.ts +2 -8
- package/packages/remote-control-server/web/dist/assets/Dashboard-BwotBWlJ.js +1 -0
- package/packages/remote-control-server/web/dist/assets/Join-4yrksbdX.js +1 -0
- package/packages/remote-control-server/web/dist/assets/Login-Cxm_yEeE.js +1 -0
- package/packages/remote-control-server/web/dist/assets/SessionDetail-DNdfPOYy.js +50 -0
- package/packages/remote-control-server/web/dist/assets/Setup-BiAAxugt.js +1 -0
- package/packages/remote-control-server/web/dist/assets/ShareLanding-CP3Z6ckr.js +1 -0
- package/packages/remote-control-server/web/dist/assets/TeamCreate-DgdCwRSb.js +1 -0
- package/packages/remote-control-server/web/dist/assets/TeamDetail-CkNTFoMB.js +11 -0
- package/packages/remote-control-server/web/dist/assets/TeamList-Bf0sQ5RQ.js +1 -0
- package/packages/remote-control-server/web/dist/assets/TeamSettings-BaNAfXU3.js +1 -0
- package/packages/remote-control-server/web/dist/assets/UserSettings-CsutQjxG.js +1 -0
- package/{dist/rcs-web/assets/arc-D90597o-.js → packages/remote-control-server/web/dist/assets/arc-BxxmNQO6.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/architectureDiagram-3BPJPVTR-DBjkRdbd.js +36 -0
- package/packages/remote-control-server/web/dist/assets/{blockDiagram-GPEHLZMM-DHd8IbOs.js → blockDiagram-GPEHLZMM-BF6COWlM.js} +2 -2
- package/packages/remote-control-server/web/dist/assets/{c4Diagram-AAUBKEIU-CjFkQJ9h.js → c4Diagram-AAUBKEIU-DHOf390v.js} +2 -2
- package/packages/remote-control-server/web/dist/assets/channel-JsvbMQjW.js +1 -0
- package/packages/remote-control-server/web/dist/assets/{chunk-2J33WTMH-C21XWnGZ.js → chunk-2J33WTMH-CeJN8yjM.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{chunk-4BX2VUAB-B3SXiJlt.js → chunk-4BX2VUAB-jP76_P2A.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{chunk-55IACEB6-DjImv45F.js → chunk-55IACEB6-BsFA6emB.js} +1 -1
- package/{dist/rcs-web/assets/chunk-727SXJPM-DtnaqsyD.js → packages/remote-control-server/web/dist/assets/chunk-727SXJPM-BF4HrQjZ.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{chunk-AQP2D5EJ-BVFgH5mS.js → chunk-AQP2D5EJ-DToJ74Kc.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{chunk-FMBD7UC4-Bl8jgb57.js → chunk-FMBD7UC4-DVTARgsa.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{chunk-ND2GUHAM-BpK111zh.js → chunk-ND2GUHAM-C50Gqv9c.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{chunk-QZHKN3VN-ruhIHBim.js → chunk-QZHKN3VN-EsIIh6zV.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/classDiagram-4FO5ZUOK-BXn8Uc4c.js +1 -0
- package/packages/remote-control-server/web/dist/assets/classDiagram-v2-Q7XG4LA2-BXn8Uc4c.js +1 -0
- package/packages/remote-control-server/web/dist/assets/code-block-IT6T5CEO-BnufHNTT.js +2 -0
- package/packages/remote-control-server/web/dist/assets/cose-bilkent-S5V4N54A-M6tNt14I.js +1 -0
- package/packages/remote-control-server/web/dist/assets/dagre-BM42HDAG-DaTm29qG.js +4 -0
- package/packages/remote-control-server/web/dist/assets/diagram-2AECGRRQ-fqoeOvq2.js +43 -0
- package/packages/remote-control-server/web/dist/assets/diagram-5GNKFQAL-6s2CiaGG.js +10 -0
- package/packages/remote-control-server/web/dist/assets/{diagram-KO2AKTUF-Uy3PDP5S.js → diagram-KO2AKTUF-C0aw_N95.js} +2 -2
- package/packages/remote-control-server/web/dist/assets/diagram-LMA3HP47-C8_Rv3rB.js +24 -0
- package/packages/remote-control-server/web/dist/assets/diagram-OG6HWLK6-Dae4u8Uc.js +24 -0
- package/{dist/rcs-web/assets/erDiagram-TEJ5UH35-sre_MTeK.js → packages/remote-control-server/web/dist/assets/erDiagram-TEJ5UH35-CosS5VWr.js} +13 -13
- package/packages/remote-control-server/web/dist/assets/{flowDiagram-I6XJVG4X-DwrWaLoe.js → flowDiagram-I6XJVG4X-D_Mxs46-.js} +2 -2
- package/packages/remote-control-server/web/dist/assets/ganttDiagram-6RSMTGT7-B0jy8Am_.js +292 -0
- package/packages/remote-control-server/web/dist/assets/gitGraphDiagram-PVQCEYII-CSdu2HVI.js +106 -0
- package/packages/remote-control-server/web/dist/assets/i18n-Dbt-vEQ3.js +1 -0
- package/packages/remote-control-server/web/dist/assets/infoDiagram-5YYISTIA-Dcbc9L1S.js +2 -0
- package/{dist/rcs-web/assets/ishikawaDiagram-YF4QCWOH-SO0nn3--.js → packages/remote-control-server/web/dist/assets/ishikawaDiagram-YF4QCWOH-BLMjFE4t.js} +2 -2
- package/packages/remote-control-server/web/dist/assets/{journeyDiagram-JHISSGLW-D4igFUd4.js → journeyDiagram-JHISSGLW-Dw4nh1xI.js} +2 -2
- package/packages/remote-control-server/web/dist/assets/{kanban-definition-UN3LZRKU-Cm_yKsZC.js → kanban-definition-UN3LZRKU-DkU3j_ly.js} +2 -2
- package/packages/remote-control-server/web/dist/assets/{linear-4IMGll_E.js → linear-N5Cify7M.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/main-30YqUvTd.js +238 -0
- package/packages/remote-control-server/web/dist/assets/main-EP6Lac2m.css +1 -0
- package/{dist/rcs-web/assets/mermaid-VLURNSYL-D_yQ7nAd.js → packages/remote-control-server/web/dist/assets/mermaid-VLURNSYL-gfiJUpl_.js} +5 -5
- package/packages/remote-control-server/web/dist/assets/{mermaid.core-B1qOUdiU.js → mermaid.core-B_Xc65Bf.js} +5 -5
- package/packages/remote-control-server/web/dist/assets/{mindmap-definition-RKZ34NQL-yETom5qt.js → mindmap-definition-RKZ34NQL-DUavHfNu.js} +2 -2
- package/{dist/rcs-web/assets/pieDiagram-4H26LBE5-KIhqXGT6.js → packages/remote-control-server/web/dist/assets/pieDiagram-4H26LBE5-DSJaToTU.js} +2 -2
- package/{dist/rcs-web/assets/quadrantDiagram-W4KKPZXB-DISqmoA6.js → packages/remote-control-server/web/dist/assets/quadrantDiagram-W4KKPZXB-BFp6TnHj.js} +2 -2
- package/packages/remote-control-server/web/dist/assets/{requirementDiagram-4Y6WPE33-DsgT4ptk.js → requirementDiagram-4Y6WPE33-eil6F3-O.js} +2 -2
- package/{dist/rcs-web/assets/sankeyDiagram-5OEKKPKP-Dn67kbn0.js → packages/remote-control-server/web/dist/assets/sankeyDiagram-5OEKKPKP-DbGhoepK.js} +2 -2
- package/{dist/rcs-web/assets/sequenceDiagram-3UESZ5HK-CtWaXZHl.js → packages/remote-control-server/web/dist/assets/sequenceDiagram-3UESZ5HK-B0YrN1ud.js} +14 -14
- package/packages/remote-control-server/web/dist/assets/stateDiagram-AJRCARHV-CofFPj3N.js +1 -0
- package/packages/remote-control-server/web/dist/assets/stateDiagram-v2-BHNVJYJU-DPlcvPn1.js +1 -0
- package/{dist/rcs-web/assets/timeline-definition-PNZ67QCA-Dk5WUenC.js → packages/remote-control-server/web/dist/assets/timeline-definition-PNZ67QCA-Bx4dusy8.js} +2 -2
- package/packages/remote-control-server/web/dist/assets/useTeams-MDI4djXg.js +1 -0
- package/{dist/rcs-web/assets/vennDiagram-CIIHVFJN-TY8EiRrC.js → packages/remote-control-server/web/dist/assets/vennDiagram-CIIHVFJN-B_-3l-MF.js} +2 -2
- package/packages/remote-control-server/web/dist/assets/{wardleyDiagram-YWT4CUSO-BK3oi47X.js → wardleyDiagram-YWT4CUSO-DNSn-pGp.js} +2 -2
- package/packages/remote-control-server/web/dist/assets/workerType-OM09y7Lk.js +1 -0
- package/{dist/rcs-web/assets/xychartDiagram-2RQKCTM6-DhsxqX67.js → packages/remote-control-server/web/dist/assets/xychartDiagram-2RQKCTM6-TqUbNp0j.js} +2 -2
- package/packages/remote-control-server/web/dist/index.html +2 -3
- package/dist/rcs-web/assets/Dashboard-Cyj-Bb86.js +0 -2
- package/dist/rcs-web/assets/SessionDetail-SA9QBkyj.js +0 -51
- package/dist/rcs-web/assets/architectureDiagram-3BPJPVTR-CG9q9_bk.js +0 -36
- package/dist/rcs-web/assets/channel-CQFGglg-.js +0 -1
- package/dist/rcs-web/assets/classDiagram-4FO5ZUOK-C5rBdShD.js +0 -1
- package/dist/rcs-web/assets/classDiagram-v2-Q7XG4LA2-C5rBdShD.js +0 -1
- package/dist/rcs-web/assets/code-block-IT6T5CEO-BLGFrBw3.js +0 -2
- package/dist/rcs-web/assets/cose-bilkent-S5V4N54A-DBKEXjqM.js +0 -1
- package/dist/rcs-web/assets/dagre-BM42HDAG-DB_SWlAe.js +0 -4
- package/dist/rcs-web/assets/diagram-2AECGRRQ-Gbm8G3Xz.js +0 -43
- package/dist/rcs-web/assets/diagram-5GNKFQAL-BBuWqfDc.js +0 -10
- package/dist/rcs-web/assets/diagram-LMA3HP47-DyUwNrlg.js +0 -24
- package/dist/rcs-web/assets/diagram-OG6HWLK6-BXpvRwkJ.js +0 -24
- package/dist/rcs-web/assets/ganttDiagram-6RSMTGT7-ByDYviDl.js +0 -292
- package/dist/rcs-web/assets/gitGraphDiagram-PVQCEYII-BUnOPw74.js +0 -106
- package/dist/rcs-web/assets/infoDiagram-5YYISTIA-B1RpwJHG.js +0 -2
- package/dist/rcs-web/assets/main-0A4a3FDU.css +0 -1
- package/dist/rcs-web/assets/main-Dom16YJk.js +0 -283
- package/dist/rcs-web/assets/qr-C0jP9HcD.js +0 -8
- package/dist/rcs-web/assets/qr-scanner-worker.min-D85Z9gVD.js +0 -98
- package/dist/rcs-web/assets/stateDiagram-AJRCARHV-D6xp64f3.js +0 -1
- package/dist/rcs-web/assets/stateDiagram-v2-BHNVJYJU-D76vZCyQ.js +0 -1
- package/packages/remote-control-server/web/dist/assets/Dashboard-Cyj-Bb86.js +0 -2
- package/packages/remote-control-server/web/dist/assets/SessionDetail-SA9QBkyj.js +0 -51
- package/packages/remote-control-server/web/dist/assets/architectureDiagram-3BPJPVTR-CG9q9_bk.js +0 -36
- package/packages/remote-control-server/web/dist/assets/channel-CQFGglg-.js +0 -1
- package/packages/remote-control-server/web/dist/assets/classDiagram-4FO5ZUOK-C5rBdShD.js +0 -1
- package/packages/remote-control-server/web/dist/assets/classDiagram-v2-Q7XG4LA2-C5rBdShD.js +0 -1
- package/packages/remote-control-server/web/dist/assets/code-block-IT6T5CEO-BLGFrBw3.js +0 -2
- package/packages/remote-control-server/web/dist/assets/cose-bilkent-S5V4N54A-DBKEXjqM.js +0 -1
- package/packages/remote-control-server/web/dist/assets/dagre-BM42HDAG-DB_SWlAe.js +0 -4
- package/packages/remote-control-server/web/dist/assets/diagram-2AECGRRQ-Gbm8G3Xz.js +0 -43
- package/packages/remote-control-server/web/dist/assets/diagram-5GNKFQAL-BBuWqfDc.js +0 -10
- package/packages/remote-control-server/web/dist/assets/diagram-LMA3HP47-DyUwNrlg.js +0 -24
- package/packages/remote-control-server/web/dist/assets/diagram-OG6HWLK6-BXpvRwkJ.js +0 -24
- package/packages/remote-control-server/web/dist/assets/ganttDiagram-6RSMTGT7-ByDYviDl.js +0 -292
- package/packages/remote-control-server/web/dist/assets/gitGraphDiagram-PVQCEYII-BUnOPw74.js +0 -106
- package/packages/remote-control-server/web/dist/assets/infoDiagram-5YYISTIA-B1RpwJHG.js +0 -2
- package/packages/remote-control-server/web/dist/assets/main-0A4a3FDU.css +0 -1
- package/packages/remote-control-server/web/dist/assets/main-Dom16YJk.js +0 -283
- package/packages/remote-control-server/web/dist/assets/qr-C0jP9HcD.js +0 -8
- package/packages/remote-control-server/web/dist/assets/qr-scanner-worker.min-D85Z9gVD.js +0 -98
- package/packages/remote-control-server/web/dist/assets/stateDiagram-AJRCARHV-D6xp64f3.js +0 -1
- package/packages/remote-control-server/web/dist/assets/stateDiagram-v2-BHNVJYJU-D76vZCyQ.js +0 -1
|
@@ -0,0 +1,850 @@
|
|
|
1
|
+
import { Hono } from 'hono'
|
|
2
|
+
import { randomUUID, createHash } from 'node:crypto'
|
|
3
|
+
import { getDb } from '../../db/sqlite'
|
|
4
|
+
import { sessionAuth } from '../../auth/middleware'
|
|
5
|
+
import { SYSTEM_USER_ID } from '../../auth/constants'
|
|
6
|
+
|
|
7
|
+
const app = new Hono()
|
|
8
|
+
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// Helpers
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
|
|
13
|
+
const SLUG_RE = /^[a-z0-9-]{3,32}$/
|
|
14
|
+
|
|
15
|
+
function isValidSlug(slug: string): boolean {
|
|
16
|
+
return SLUG_RE.test(slug)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface UserInfo {
|
|
20
|
+
userId: string
|
|
21
|
+
isAdmin: boolean
|
|
22
|
+
role: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Resolve the current user's info from context.
|
|
27
|
+
* System admin (via API key bypass) gets userId=SYSTEM_USER_ID.
|
|
28
|
+
* Also checks DB role for users who logged in as admin.
|
|
29
|
+
*/
|
|
30
|
+
function getUserInfo(c: import('hono').Context): UserInfo {
|
|
31
|
+
const userId = c.get('userId') as string
|
|
32
|
+
const isAdminCtx = c.get('isAdmin') as boolean
|
|
33
|
+
|
|
34
|
+
if (userId === SYSTEM_USER_ID || isAdminCtx) {
|
|
35
|
+
return { userId: SYSTEM_USER_ID, isAdmin: true, role: 'admin' }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Look up user role from DB
|
|
39
|
+
const db = getDb()
|
|
40
|
+
const row = db
|
|
41
|
+
.query('SELECT role FROM users WHERE id = $id')
|
|
42
|
+
.get({ $id: userId }) as { role: string } | null
|
|
43
|
+
|
|
44
|
+
const role = row?.role || 'guest'
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
userId,
|
|
48
|
+
isAdmin: role === 'admin',
|
|
49
|
+
role,
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Get the user's role within a specific team.
|
|
55
|
+
* Returns null if user is not a member.
|
|
56
|
+
* System admins get 'owner' implicitly.
|
|
57
|
+
*/
|
|
58
|
+
function getTeamRole(teamId: string, user: UserInfo): string | null {
|
|
59
|
+
if (user.isAdmin) return 'owner'
|
|
60
|
+
|
|
61
|
+
const db = getDb()
|
|
62
|
+
const row = db
|
|
63
|
+
.query(
|
|
64
|
+
'SELECT role FROM team_members WHERE team_id = $teamId AND user_id = $userId',
|
|
65
|
+
)
|
|
66
|
+
.get({ $teamId: teamId, $userId: user.userId }) as {
|
|
67
|
+
role: string
|
|
68
|
+
} | null
|
|
69
|
+
|
|
70
|
+
return row?.role || null
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function getTeamOr404(c: import('hono').Context, teamId: string) {
|
|
74
|
+
const db = getDb()
|
|
75
|
+
const team = db
|
|
76
|
+
.query('SELECT * FROM teams WHERE id = $id AND deleted_at IS NULL')
|
|
77
|
+
.get({ $id: teamId }) as Record<string, unknown> | null
|
|
78
|
+
if (!team) {
|
|
79
|
+
return { error: c.json({ error: 'Team not found' }, 404) }
|
|
80
|
+
}
|
|
81
|
+
return { team }
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
// Auth middleware wrapper
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
|
|
88
|
+
async function withAuth(c: import('hono').Context, next: import('hono').Next) {
|
|
89
|
+
const result = await sessionAuth(c, next)
|
|
90
|
+
if (result) return result
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
// POST /teams — Create team
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
|
|
97
|
+
app.post('/teams', withAuth, async c => {
|
|
98
|
+
const user = getUserInfo(c)
|
|
99
|
+
|
|
100
|
+
// Guest cannot create teams
|
|
101
|
+
if (!user.isAdmin && user.role === 'guest') {
|
|
102
|
+
return c.json({ error: 'Guests cannot create teams' }, 403)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
let body: Record<string, unknown>
|
|
106
|
+
try {
|
|
107
|
+
body = await c.req.json()
|
|
108
|
+
} catch {
|
|
109
|
+
return c.json({ error: 'Invalid JSON' }, 400)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const { name, slug, description } = body as {
|
|
113
|
+
name?: string
|
|
114
|
+
slug?: string
|
|
115
|
+
description?: string
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (!name || typeof name !== 'string') {
|
|
119
|
+
return c.json({ error: 'Name is required' }, 400)
|
|
120
|
+
}
|
|
121
|
+
if (!slug || typeof slug !== 'string') {
|
|
122
|
+
return c.json({ error: 'Slug is required' }, 400)
|
|
123
|
+
}
|
|
124
|
+
if (!isValidSlug(slug)) {
|
|
125
|
+
return c.json(
|
|
126
|
+
{
|
|
127
|
+
error:
|
|
128
|
+
'Invalid slug: must be 3-32 lowercase letters, digits, or hyphens',
|
|
129
|
+
},
|
|
130
|
+
400,
|
|
131
|
+
)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const db = getDb()
|
|
135
|
+
|
|
136
|
+
// Check slug uniqueness
|
|
137
|
+
const existing = db
|
|
138
|
+
.query('SELECT id FROM teams WHERE slug = $slug AND deleted_at IS NULL')
|
|
139
|
+
.get({ $slug: slug })
|
|
140
|
+
if (existing) {
|
|
141
|
+
return c.json({ error: 'Slug already taken' }, 409)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const teamId = `team_${randomUUID().replace(/-/g, '')}`
|
|
145
|
+
const now = new Date().toISOString()
|
|
146
|
+
|
|
147
|
+
// For system admin, use the first admin user as created_by
|
|
148
|
+
let createdBy = user.userId
|
|
149
|
+
if (user.isAdmin) {
|
|
150
|
+
const adminUser = db
|
|
151
|
+
.query("SELECT id FROM users WHERE role = 'admin' LIMIT 1")
|
|
152
|
+
.get() as { id: string } | null
|
|
153
|
+
createdBy = adminUser?.id || user.userId
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
db.query(
|
|
157
|
+
`INSERT INTO teams (id, name, slug, description, created_by, created_at, updated_at)
|
|
158
|
+
VALUES ($id, $name, $slug, $desc, $by, $now, $now)`,
|
|
159
|
+
).run({
|
|
160
|
+
$id: teamId,
|
|
161
|
+
$name: name,
|
|
162
|
+
$slug: slug,
|
|
163
|
+
$desc: description || null,
|
|
164
|
+
$by: createdBy,
|
|
165
|
+
$now: now,
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
// Creator becomes owner
|
|
169
|
+
db.query(
|
|
170
|
+
`INSERT INTO team_members (team_id, user_id, role, added_at, added_by)
|
|
171
|
+
VALUES ($teamId, $userId, 'owner', $now, $addedBy)`,
|
|
172
|
+
).run({ $teamId: teamId, $userId: createdBy, $addedBy: createdBy, $now: now })
|
|
173
|
+
|
|
174
|
+
return c.json(
|
|
175
|
+
{
|
|
176
|
+
id: teamId,
|
|
177
|
+
name,
|
|
178
|
+
slug,
|
|
179
|
+
description: description || null,
|
|
180
|
+
created_by: createdBy,
|
|
181
|
+
created_at: now,
|
|
182
|
+
updated_at: now,
|
|
183
|
+
},
|
|
184
|
+
201,
|
|
185
|
+
)
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
// ---------------------------------------------------------------------------
|
|
189
|
+
// GET /teams — List teams
|
|
190
|
+
// ---------------------------------------------------------------------------
|
|
191
|
+
|
|
192
|
+
app.get('/teams', withAuth, async c => {
|
|
193
|
+
const user = getUserInfo(c)
|
|
194
|
+
const db = getDb()
|
|
195
|
+
|
|
196
|
+
if (user.isAdmin) {
|
|
197
|
+
// Admin sees all non-deleted teams
|
|
198
|
+
const teams = db
|
|
199
|
+
.query('SELECT * FROM teams WHERE deleted_at IS NULL ORDER BY name')
|
|
200
|
+
.all()
|
|
201
|
+
return c.json(teams, 200)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (user.role === 'guest') {
|
|
205
|
+
return c.json([], 200)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Member sees only teams they belong to
|
|
209
|
+
const teams = db
|
|
210
|
+
.query(
|
|
211
|
+
`SELECT t.* FROM teams t
|
|
212
|
+
JOIN team_members tm ON tm.team_id = t.id
|
|
213
|
+
WHERE tm.user_id = $userId AND t.deleted_at IS NULL
|
|
214
|
+
ORDER BY t.name`,
|
|
215
|
+
)
|
|
216
|
+
.all({ $userId: user.userId })
|
|
217
|
+
|
|
218
|
+
return c.json(teams, 200)
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
// ---------------------------------------------------------------------------
|
|
222
|
+
// GET /teams/:id — Get team details
|
|
223
|
+
// ---------------------------------------------------------------------------
|
|
224
|
+
|
|
225
|
+
app.get('/teams/:id', withAuth, async c => {
|
|
226
|
+
const teamId = c.req.param('id')!
|
|
227
|
+
const user = getUserInfo(c)
|
|
228
|
+
const result = getTeamOr404(c, teamId)
|
|
229
|
+
if ('error' in result) return result.error
|
|
230
|
+
|
|
231
|
+
const teamRole = getTeamRole(teamId, user)
|
|
232
|
+
if (!teamRole) {
|
|
233
|
+
return c.json({ error: 'Not a team member' }, 403)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return c.json(result.team, 200)
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
// ---------------------------------------------------------------------------
|
|
240
|
+
// PATCH /teams/:id — Update team
|
|
241
|
+
// ---------------------------------------------------------------------------
|
|
242
|
+
|
|
243
|
+
app.patch('/teams/:id', withAuth, async c => {
|
|
244
|
+
const teamId = c.req.param('id')!
|
|
245
|
+
const user = getUserInfo(c)
|
|
246
|
+
const result = getTeamOr404(c, teamId)
|
|
247
|
+
if ('error' in result) return result.error
|
|
248
|
+
|
|
249
|
+
const teamRole = getTeamRole(teamId, user)
|
|
250
|
+
|
|
251
|
+
// Only owner (or system admin) can modify team info
|
|
252
|
+
if (!user.isAdmin && teamRole !== 'owner') {
|
|
253
|
+
return c.json({ error: 'Only team owner can modify team info' }, 403)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
let body: Record<string, unknown>
|
|
257
|
+
try {
|
|
258
|
+
body = await c.req.json()
|
|
259
|
+
} catch {
|
|
260
|
+
return c.json({ error: 'Invalid JSON' }, 400)
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const { name, slug, description } = body as {
|
|
264
|
+
name?: string
|
|
265
|
+
slug?: string
|
|
266
|
+
description?: string
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const db = getDb()
|
|
270
|
+
|
|
271
|
+
// If changing slug, validate and check uniqueness
|
|
272
|
+
if (slug !== undefined) {
|
|
273
|
+
if (!isValidSlug(slug)) {
|
|
274
|
+
return c.json({ error: 'Invalid slug format' }, 400)
|
|
275
|
+
}
|
|
276
|
+
const existingSlug = db
|
|
277
|
+
.query(
|
|
278
|
+
'SELECT id FROM teams WHERE slug = $slug AND id != $id AND deleted_at IS NULL',
|
|
279
|
+
)
|
|
280
|
+
.get({ $slug: slug, $id: teamId })
|
|
281
|
+
if (existingSlug) {
|
|
282
|
+
return c.json({ error: 'Slug already taken' }, 409)
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const now = new Date().toISOString()
|
|
287
|
+
const updates: string[] = ['updated_at = $now']
|
|
288
|
+
const params: Record<string, string | null> = {
|
|
289
|
+
$id: teamId,
|
|
290
|
+
$now: now,
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (name !== undefined) {
|
|
294
|
+
updates.push('name = $name')
|
|
295
|
+
params.$name = name
|
|
296
|
+
}
|
|
297
|
+
if (slug !== undefined) {
|
|
298
|
+
updates.push('slug = $slug')
|
|
299
|
+
params.$slug = slug
|
|
300
|
+
}
|
|
301
|
+
if (description !== undefined) {
|
|
302
|
+
updates.push('description = $desc')
|
|
303
|
+
params.$desc = description
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
db.query(`UPDATE teams SET ${updates.join(', ')} WHERE id = $id`).run(params)
|
|
307
|
+
|
|
308
|
+
const updated = db
|
|
309
|
+
.query('SELECT * FROM teams WHERE id = $id')
|
|
310
|
+
.get({ $id: teamId })
|
|
311
|
+
return c.json(updated, 200)
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
// ---------------------------------------------------------------------------
|
|
315
|
+
// DELETE /teams/:id — Soft delete team
|
|
316
|
+
// ---------------------------------------------------------------------------
|
|
317
|
+
|
|
318
|
+
app.delete('/teams/:id', withAuth, async c => {
|
|
319
|
+
const teamId = c.req.param('id')!
|
|
320
|
+
const user = getUserInfo(c)
|
|
321
|
+
const result = getTeamOr404(c, teamId)
|
|
322
|
+
if ('error' in result) return result.error
|
|
323
|
+
|
|
324
|
+
const teamRole = getTeamRole(teamId, user)
|
|
325
|
+
|
|
326
|
+
// Only owner (or system admin) can delete
|
|
327
|
+
if (!user.isAdmin && teamRole !== 'owner') {
|
|
328
|
+
return c.json({ error: 'Only team owner can delete team' }, 403)
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const db = getDb()
|
|
332
|
+
|
|
333
|
+
// Check for un-transferred environments
|
|
334
|
+
const envCount = db
|
|
335
|
+
.query(
|
|
336
|
+
'SELECT COUNT(*) as count FROM team_environments WHERE team_id = $teamId',
|
|
337
|
+
)
|
|
338
|
+
.get({ $teamId: teamId }) as { count: number }
|
|
339
|
+
if (envCount.count > 0) {
|
|
340
|
+
return c.json(
|
|
341
|
+
{
|
|
342
|
+
error: 'Team has environments that must be transferred first',
|
|
343
|
+
},
|
|
344
|
+
400,
|
|
345
|
+
)
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const now = new Date().toISOString()
|
|
349
|
+
db.query('UPDATE teams SET deleted_at = $now WHERE id = $id').run({
|
|
350
|
+
$now: now,
|
|
351
|
+
$id: teamId,
|
|
352
|
+
})
|
|
353
|
+
|
|
354
|
+
// Remove team ownership from sessions (orphan them)
|
|
355
|
+
db.query(
|
|
356
|
+
"DELETE FROM session_owners WHERE owner_type = 'team' AND owner_id = $teamId",
|
|
357
|
+
).run({ $teamId: teamId })
|
|
358
|
+
|
|
359
|
+
return c.json({ ok: true }, 200)
|
|
360
|
+
})
|
|
361
|
+
|
|
362
|
+
// ---------------------------------------------------------------------------
|
|
363
|
+
// GET /teams/:id/members — List members
|
|
364
|
+
// ---------------------------------------------------------------------------
|
|
365
|
+
|
|
366
|
+
app.get('/teams/:id/members', withAuth, async c => {
|
|
367
|
+
const teamId = c.req.param('id')!
|
|
368
|
+
const user = getUserInfo(c)
|
|
369
|
+
const result = getTeamOr404(c, teamId)
|
|
370
|
+
if ('error' in result) return result.error
|
|
371
|
+
|
|
372
|
+
const teamRole = getTeamRole(teamId, user)
|
|
373
|
+
if (!teamRole) {
|
|
374
|
+
return c.json({ error: 'Not a team member' }, 403)
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const db = getDb()
|
|
378
|
+
const members = db
|
|
379
|
+
.query(
|
|
380
|
+
`SELECT tm.*, u.username FROM team_members tm
|
|
381
|
+
LEFT JOIN users u ON u.id = tm.user_id
|
|
382
|
+
WHERE tm.team_id = $teamId`,
|
|
383
|
+
)
|
|
384
|
+
.all({ $teamId: teamId })
|
|
385
|
+
|
|
386
|
+
return c.json(members, 200)
|
|
387
|
+
})
|
|
388
|
+
|
|
389
|
+
// ---------------------------------------------------------------------------
|
|
390
|
+
// POST /teams/:id/members — Add member
|
|
391
|
+
// ---------------------------------------------------------------------------
|
|
392
|
+
|
|
393
|
+
app.post('/teams/:id/members', withAuth, async c => {
|
|
394
|
+
const teamId = c.req.param('id')!
|
|
395
|
+
const user = getUserInfo(c)
|
|
396
|
+
const result = getTeamOr404(c, teamId)
|
|
397
|
+
if ('error' in result) return result.error
|
|
398
|
+
|
|
399
|
+
const teamRole = getTeamRole(teamId, user)
|
|
400
|
+
|
|
401
|
+
// Only owner/admin (or system admin) can add members
|
|
402
|
+
if (!user.isAdmin && teamRole !== 'owner' && teamRole !== 'admin') {
|
|
403
|
+
return c.json({ error: 'Only team owner/admin can add members' }, 403)
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
let body: Record<string, unknown>
|
|
407
|
+
try {
|
|
408
|
+
body = await c.req.json()
|
|
409
|
+
} catch {
|
|
410
|
+
return c.json({ error: 'Invalid JSON' }, 400)
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const { userId, role } = body as {
|
|
414
|
+
userId?: string
|
|
415
|
+
role?: string
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
if (!userId) {
|
|
419
|
+
return c.json({ error: 'userId is required' }, 400)
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const validRoles = ['owner', 'admin', 'member']
|
|
423
|
+
const memberRole = role || 'member'
|
|
424
|
+
if (!validRoles.includes(memberRole)) {
|
|
425
|
+
return c.json({ error: 'Invalid role' }, 400)
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const db = getDb()
|
|
429
|
+
|
|
430
|
+
// Check user exists
|
|
431
|
+
const targetUser = db
|
|
432
|
+
.query('SELECT id FROM users WHERE id = $id')
|
|
433
|
+
.get({ $id: userId })
|
|
434
|
+
if (!targetUser) {
|
|
435
|
+
return c.json({ error: 'User not found' }, 404)
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// Check not already a member
|
|
439
|
+
const existing = db
|
|
440
|
+
.query(
|
|
441
|
+
'SELECT user_id FROM team_members WHERE team_id = $teamId AND user_id = $userId',
|
|
442
|
+
)
|
|
443
|
+
.get({ $teamId: teamId, $userId: userId })
|
|
444
|
+
if (existing) {
|
|
445
|
+
return c.json({ error: 'User is already a member' }, 409)
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const now = new Date().toISOString()
|
|
449
|
+
const addedBy = user.isAdmin
|
|
450
|
+
? (result.team.created_by as string)
|
|
451
|
+
: user.userId
|
|
452
|
+
|
|
453
|
+
db.query(
|
|
454
|
+
`INSERT INTO team_members (team_id, user_id, role, added_at, added_by)
|
|
455
|
+
VALUES ($teamId, $userId, $role, $now, $addedBy)`,
|
|
456
|
+
).run({
|
|
457
|
+
$teamId: teamId,
|
|
458
|
+
$userId: userId,
|
|
459
|
+
$role: memberRole,
|
|
460
|
+
$now: now,
|
|
461
|
+
$addedBy: addedBy,
|
|
462
|
+
})
|
|
463
|
+
|
|
464
|
+
return c.json({ ok: true, role: memberRole }, 201)
|
|
465
|
+
})
|
|
466
|
+
|
|
467
|
+
// ---------------------------------------------------------------------------
|
|
468
|
+
// PATCH /teams/:id/members/:userId — Change member role
|
|
469
|
+
// ---------------------------------------------------------------------------
|
|
470
|
+
|
|
471
|
+
app.patch('/teams/:id/members/:userId', withAuth, async c => {
|
|
472
|
+
const teamId = c.req.param('id')!
|
|
473
|
+
const targetUserId = c.req.param('userId')!
|
|
474
|
+
const user = getUserInfo(c)
|
|
475
|
+
const result = getTeamOr404(c, teamId)
|
|
476
|
+
if ('error' in result) return result.error
|
|
477
|
+
|
|
478
|
+
const teamRole = getTeamRole(teamId, user)
|
|
479
|
+
|
|
480
|
+
// Must be at least admin to change roles
|
|
481
|
+
if (!user.isAdmin && teamRole !== 'owner' && teamRole !== 'admin') {
|
|
482
|
+
return c.json({ error: 'Insufficient permissions' }, 403)
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
const db = getDb()
|
|
486
|
+
|
|
487
|
+
// Team admin cannot change owner/admin roles
|
|
488
|
+
if (!user.isAdmin && teamRole === 'admin') {
|
|
489
|
+
const targetMember = db
|
|
490
|
+
.query(
|
|
491
|
+
'SELECT role FROM team_members WHERE team_id = $teamId AND user_id = $userId',
|
|
492
|
+
)
|
|
493
|
+
.get({ $teamId: teamId, $userId: targetUserId }) as {
|
|
494
|
+
role: string
|
|
495
|
+
} | null
|
|
496
|
+
if (
|
|
497
|
+
targetMember &&
|
|
498
|
+
(targetMember.role === 'owner' || targetMember.role === 'admin')
|
|
499
|
+
) {
|
|
500
|
+
return c.json(
|
|
501
|
+
{ error: 'Team admin cannot change owner/admin roles' },
|
|
502
|
+
403,
|
|
503
|
+
)
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
let body: Record<string, unknown>
|
|
508
|
+
try {
|
|
509
|
+
body = await c.req.json()
|
|
510
|
+
} catch {
|
|
511
|
+
return c.json({ error: 'Invalid JSON' }, 400)
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
const { role } = body as { role?: string }
|
|
515
|
+
if (!role) {
|
|
516
|
+
return c.json({ error: 'role is required' }, 400)
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
const validRoles = ['owner', 'admin', 'member']
|
|
520
|
+
if (!validRoles.includes(role)) {
|
|
521
|
+
return c.json({ error: 'Invalid role' }, 400)
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// Check target is actually a member
|
|
525
|
+
const currentMember = db
|
|
526
|
+
.query(
|
|
527
|
+
'SELECT role FROM team_members WHERE team_id = $teamId AND user_id = $userId',
|
|
528
|
+
)
|
|
529
|
+
.get({ $teamId: teamId, $userId: targetUserId }) as {
|
|
530
|
+
role: string
|
|
531
|
+
} | null
|
|
532
|
+
if (!currentMember) {
|
|
533
|
+
return c.json({ error: 'User is not a member' }, 404)
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// If changing from owner to something else, check there are other owners
|
|
537
|
+
if (currentMember.role === 'owner' && role !== 'owner') {
|
|
538
|
+
const ownerCount = db
|
|
539
|
+
.query(
|
|
540
|
+
`SELECT COUNT(*) as count FROM team_members
|
|
541
|
+
WHERE team_id = $teamId AND role = 'owner'`,
|
|
542
|
+
)
|
|
543
|
+
.get({ $teamId: teamId }) as { count: number }
|
|
544
|
+
if (ownerCount.count <= 1) {
|
|
545
|
+
return c.json({ error: 'Cannot demote the last owner' }, 400)
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
db.query(
|
|
550
|
+
`UPDATE team_members SET role = $role WHERE team_id = $teamId AND user_id = $userId`,
|
|
551
|
+
).run({ $role: role, $teamId: teamId, $userId: targetUserId })
|
|
552
|
+
|
|
553
|
+
return c.json({ ok: true, role }, 200)
|
|
554
|
+
})
|
|
555
|
+
|
|
556
|
+
// ---------------------------------------------------------------------------
|
|
557
|
+
// DELETE /teams/:id/members/:userId — Remove member
|
|
558
|
+
// ---------------------------------------------------------------------------
|
|
559
|
+
|
|
560
|
+
app.delete('/teams/:id/members/:userId', withAuth, async c => {
|
|
561
|
+
const teamId = c.req.param('id')!
|
|
562
|
+
const targetUserId = c.req.param('userId')!
|
|
563
|
+
const user = getUserInfo(c)
|
|
564
|
+
const result = getTeamOr404(c, teamId)
|
|
565
|
+
if ('error' in result) return result.error
|
|
566
|
+
|
|
567
|
+
const teamRole = getTeamRole(teamId, user)
|
|
568
|
+
const db = getDb()
|
|
569
|
+
|
|
570
|
+
// Check target is a member
|
|
571
|
+
const targetMember = db
|
|
572
|
+
.query(
|
|
573
|
+
'SELECT role FROM team_members WHERE team_id = $teamId AND user_id = $userId',
|
|
574
|
+
)
|
|
575
|
+
.get({ $teamId: teamId, $userId: targetUserId }) as {
|
|
576
|
+
role: string
|
|
577
|
+
} | null
|
|
578
|
+
if (!targetMember) {
|
|
579
|
+
return c.json({ error: 'User is not a member' }, 404)
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// Members can only remove themselves
|
|
583
|
+
if (!user.isAdmin && teamRole === 'member') {
|
|
584
|
+
if (targetUserId !== user.userId) {
|
|
585
|
+
return c.json({ error: 'Members can only remove themselves' }, 403)
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// Owner/admin can remove anyone (except constraints below)
|
|
590
|
+
if (
|
|
591
|
+
!user.isAdmin &&
|
|
592
|
+
teamRole !== 'owner' &&
|
|
593
|
+
teamRole !== 'admin' &&
|
|
594
|
+
targetUserId !== user.userId
|
|
595
|
+
) {
|
|
596
|
+
return c.json({ error: 'Insufficient permissions' }, 403)
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// Cannot remove the last owner
|
|
600
|
+
if (targetMember.role === 'owner') {
|
|
601
|
+
const ownerCount = db
|
|
602
|
+
.query(
|
|
603
|
+
`SELECT COUNT(*) as count FROM team_members
|
|
604
|
+
WHERE team_id = $teamId AND role = 'owner'`,
|
|
605
|
+
)
|
|
606
|
+
.get({ $teamId: teamId }) as { count: number }
|
|
607
|
+
if (ownerCount.count <= 1) {
|
|
608
|
+
return c.json({ error: 'Cannot remove the last owner' }, 400)
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
db.query(
|
|
613
|
+
'DELETE FROM team_members WHERE team_id = $teamId AND user_id = $userId',
|
|
614
|
+
).run({ $teamId: teamId, $userId: targetUserId })
|
|
615
|
+
|
|
616
|
+
return c.json({ ok: true }, 200)
|
|
617
|
+
})
|
|
618
|
+
|
|
619
|
+
// ---------------------------------------------------------------------------
|
|
620
|
+
// POST /teams/:id/invitations — Create invitation
|
|
621
|
+
// ---------------------------------------------------------------------------
|
|
622
|
+
|
|
623
|
+
app.post('/teams/:id/invitations', withAuth, async c => {
|
|
624
|
+
const teamId = c.req.param('id')!
|
|
625
|
+
const user = getUserInfo(c)
|
|
626
|
+
const result = getTeamOr404(c, teamId)
|
|
627
|
+
if ('error' in result) return result.error
|
|
628
|
+
|
|
629
|
+
const teamRole = getTeamRole(teamId, user)
|
|
630
|
+
|
|
631
|
+
// Only owner/admin can create invitations
|
|
632
|
+
if (!user.isAdmin && teamRole !== 'owner' && teamRole !== 'admin') {
|
|
633
|
+
return c.json(
|
|
634
|
+
{ error: 'Only team owner/admin can create invitations' },
|
|
635
|
+
403,
|
|
636
|
+
)
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
let body: Record<string, unknown>
|
|
640
|
+
try {
|
|
641
|
+
body = await c.req.json()
|
|
642
|
+
} catch {
|
|
643
|
+
return c.json({ error: 'Invalid JSON' }, 400)
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
const { role, expiresIn, maxUses } = body as {
|
|
647
|
+
role?: string
|
|
648
|
+
expiresIn?: number
|
|
649
|
+
maxUses?: number
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
const inviteRole = role || 'member'
|
|
653
|
+
const validRoles = ['admin', 'member']
|
|
654
|
+
if (!validRoles.includes(inviteRole)) {
|
|
655
|
+
return c.json({ error: 'Invalid role' }, 400)
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
const token = `inv_${randomUUID().replace(/-/g, '')}`
|
|
659
|
+
const tokenHash = createHash('sha256').update(token).digest('hex')
|
|
660
|
+
const now = new Date()
|
|
661
|
+
const expiresAt = new Date(
|
|
662
|
+
now.getTime() + (expiresIn || 86400) * 1000,
|
|
663
|
+
).toISOString()
|
|
664
|
+
const maxUsesValue = maxUses || 1
|
|
665
|
+
|
|
666
|
+
const db = getDb()
|
|
667
|
+
const createdBy = user.isAdmin
|
|
668
|
+
? (result.team.created_by as string)
|
|
669
|
+
: user.userId
|
|
670
|
+
|
|
671
|
+
db.query(
|
|
672
|
+
`INSERT INTO invitations (token_hash, team_id, role, expires_at, max_uses, uses, created_by)
|
|
673
|
+
VALUES ($hash, $teamId, $role, $exp, $maxUses, 0, $by)`,
|
|
674
|
+
).run({
|
|
675
|
+
$hash: tokenHash,
|
|
676
|
+
$teamId: teamId,
|
|
677
|
+
$role: inviteRole,
|
|
678
|
+
$exp: expiresAt,
|
|
679
|
+
$maxUses: maxUsesValue,
|
|
680
|
+
$by: createdBy,
|
|
681
|
+
})
|
|
682
|
+
|
|
683
|
+
return c.json(
|
|
684
|
+
{
|
|
685
|
+
token,
|
|
686
|
+
inviteToken: token,
|
|
687
|
+
role: inviteRole,
|
|
688
|
+
expiresAt,
|
|
689
|
+
maxUses: maxUsesValue,
|
|
690
|
+
teamId,
|
|
691
|
+
},
|
|
692
|
+
201,
|
|
693
|
+
)
|
|
694
|
+
})
|
|
695
|
+
|
|
696
|
+
// ---------------------------------------------------------------------------
|
|
697
|
+
// DELETE /teams/:id/invitations/:token — Revoke invitation
|
|
698
|
+
// ---------------------------------------------------------------------------
|
|
699
|
+
|
|
700
|
+
app.delete('/teams/:id/invitations/:token', withAuth, async c => {
|
|
701
|
+
const teamId = c.req.param('id')!
|
|
702
|
+
const token = c.req.param('token')!
|
|
703
|
+
const user = getUserInfo(c)
|
|
704
|
+
const result = getTeamOr404(c, teamId)
|
|
705
|
+
if ('error' in result) return result.error
|
|
706
|
+
|
|
707
|
+
const teamRole = getTeamRole(teamId, user)
|
|
708
|
+
|
|
709
|
+
// Only owner/admin can revoke invitations
|
|
710
|
+
if (!user.isAdmin && teamRole !== 'owner' && teamRole !== 'admin') {
|
|
711
|
+
return c.json(
|
|
712
|
+
{ error: 'Only team owner/admin can revoke invitations' },
|
|
713
|
+
403,
|
|
714
|
+
)
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
const db = getDb()
|
|
718
|
+
const tokenHash = createHash('sha256').update(token).digest('hex')
|
|
719
|
+
|
|
720
|
+
// Verify invitation belongs to this team
|
|
721
|
+
const invitation = db
|
|
722
|
+
.query(
|
|
723
|
+
'SELECT * FROM invitations WHERE token_hash = $hash AND team_id = $teamId',
|
|
724
|
+
)
|
|
725
|
+
.get({ $hash: tokenHash, $teamId: teamId })
|
|
726
|
+
if (!invitation) {
|
|
727
|
+
return c.json({ error: 'Invitation not found' }, 404)
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
// Delete the invitation (revoke)
|
|
731
|
+
db.query('DELETE FROM invitations WHERE token_hash = $hash').run({
|
|
732
|
+
$hash: tokenHash,
|
|
733
|
+
})
|
|
734
|
+
|
|
735
|
+
return c.json({ ok: true }, 200)
|
|
736
|
+
})
|
|
737
|
+
|
|
738
|
+
// ---------------------------------------------------------------------------
|
|
739
|
+
// GET /teams/:id/environments — List team environments
|
|
740
|
+
// ---------------------------------------------------------------------------
|
|
741
|
+
|
|
742
|
+
app.get('/teams/:id/environments', withAuth, async c => {
|
|
743
|
+
const teamId = c.req.param('id')!
|
|
744
|
+
const user = getUserInfo(c)
|
|
745
|
+
const result = getTeamOr404(c, teamId)
|
|
746
|
+
if ('error' in result) return result.error
|
|
747
|
+
|
|
748
|
+
const teamRole = getTeamRole(teamId, user)
|
|
749
|
+
if (!teamRole) {
|
|
750
|
+
return c.json({ error: 'Not a team member' }, 403)
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
const db = getDb()
|
|
754
|
+
const envs = db
|
|
755
|
+
.query(
|
|
756
|
+
`SELECT te.environment_id as id, te.environment_id
|
|
757
|
+
FROM team_environments te
|
|
758
|
+
WHERE te.team_id = $teamId`,
|
|
759
|
+
)
|
|
760
|
+
.all({ $teamId: teamId })
|
|
761
|
+
|
|
762
|
+
return c.json(envs, 200)
|
|
763
|
+
})
|
|
764
|
+
|
|
765
|
+
// ---------------------------------------------------------------------------
|
|
766
|
+
// POST /environments/:id/transfer — Transfer environment to another team
|
|
767
|
+
// ---------------------------------------------------------------------------
|
|
768
|
+
|
|
769
|
+
app.post('/environments/:id/transfer', withAuth, async c => {
|
|
770
|
+
const envId = c.req.param('id')!
|
|
771
|
+
const user = getUserInfo(c)
|
|
772
|
+
|
|
773
|
+
let body: Record<string, unknown>
|
|
774
|
+
try {
|
|
775
|
+
body = await c.req.json()
|
|
776
|
+
} catch {
|
|
777
|
+
return c.json({ error: 'Invalid JSON' }, 400)
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
const { toTeamId } = body as { toTeamId?: string }
|
|
781
|
+
if (!toTeamId) {
|
|
782
|
+
return c.json({ error: 'toTeamId is required' }, 400)
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
const db = getDb()
|
|
786
|
+
|
|
787
|
+
// Check target team exists
|
|
788
|
+
const targetTeam = db
|
|
789
|
+
.query('SELECT * FROM teams WHERE id = $id AND deleted_at IS NULL')
|
|
790
|
+
.get({ $id: toTeamId })
|
|
791
|
+
if (!targetTeam) {
|
|
792
|
+
return c.json({ error: 'Target team not found' }, 404)
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
// Find source team
|
|
796
|
+
const sourceRow = db
|
|
797
|
+
.query(
|
|
798
|
+
'SELECT team_id FROM team_environments WHERE environment_id = $envId',
|
|
799
|
+
)
|
|
800
|
+
.get({ $envId: envId }) as { team_id: string } | null
|
|
801
|
+
|
|
802
|
+
if (sourceRow) {
|
|
803
|
+
const teamRole = getTeamRole(sourceRow.team_id, user)
|
|
804
|
+
// Only owner/admin (or system admin) can transfer
|
|
805
|
+
if (!user.isAdmin && teamRole !== 'owner' && teamRole !== 'admin') {
|
|
806
|
+
return c.json(
|
|
807
|
+
{
|
|
808
|
+
error: 'Only team owner/admin can transfer environments',
|
|
809
|
+
},
|
|
810
|
+
403,
|
|
811
|
+
)
|
|
812
|
+
}
|
|
813
|
+
} else if (!user.isAdmin) {
|
|
814
|
+
// Environment not in any team — only admin can transfer
|
|
815
|
+
return c.json({ error: 'Environment not found in any team' }, 404)
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
// Remove from source team
|
|
819
|
+
db.query('DELETE FROM team_environments WHERE environment_id = $envId').run({
|
|
820
|
+
$envId: envId,
|
|
821
|
+
})
|
|
822
|
+
|
|
823
|
+
// Add to target team
|
|
824
|
+
db.query(
|
|
825
|
+
`INSERT OR REPLACE INTO team_environments (team_id, environment_id)
|
|
826
|
+
VALUES ($teamId, $envId)`,
|
|
827
|
+
).run({ $teamId: toTeamId, $envId: envId })
|
|
828
|
+
|
|
829
|
+
// Transfer session ownership for sessions in this environment
|
|
830
|
+
// Update session_owners: remove old team owners, add new team owner
|
|
831
|
+
if (sourceRow) {
|
|
832
|
+
db.query(
|
|
833
|
+
`DELETE FROM session_owners
|
|
834
|
+
WHERE owner_type = 'team' AND owner_id = $oldTeamId
|
|
835
|
+
AND session_id IN (
|
|
836
|
+
SELECT id FROM sessions WHERE environment_id = $envId
|
|
837
|
+
)`,
|
|
838
|
+
).run({ $oldTeamId: sourceRow.team_id, $envId: envId })
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
// Add new team as owner for sessions in this environment
|
|
842
|
+
db.query(
|
|
843
|
+
`INSERT OR IGNORE INTO session_owners (session_id, owner_type, owner_id)
|
|
844
|
+
SELECT id, 'team', $teamId FROM sessions WHERE environment_id = $envId`,
|
|
845
|
+
).run({ $teamId: toTeamId, $envId: envId })
|
|
846
|
+
|
|
847
|
+
return c.json({ ok: true }, 200)
|
|
848
|
+
})
|
|
849
|
+
|
|
850
|
+
export default app
|