@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,634 @@
|
|
|
1
|
+
import { Hono } from 'hono'
|
|
2
|
+
import { randomUUID, createHash } from 'node:crypto'
|
|
3
|
+
import { config } from '../../config'
|
|
4
|
+
import { getDb } from '../../db/sqlite'
|
|
5
|
+
import { hashPassword, hashPasswordAsync, verifyPassword, verifyPasswordAsync } from '../../auth/password'
|
|
6
|
+
import {
|
|
7
|
+
issueSessionToken,
|
|
8
|
+
resolveSessionToken,
|
|
9
|
+
revokeSessionToken,
|
|
10
|
+
} from '../../auth/session'
|
|
11
|
+
import { buildSessionCookies, buildClearCookies } from '../../auth/cookie'
|
|
12
|
+
import { sessionAuth } from '../../auth/middleware'
|
|
13
|
+
import { SYSTEM_USER_ID } from '../../auth/constants'
|
|
14
|
+
|
|
15
|
+
const app = new Hono()
|
|
16
|
+
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// Login rate limiting (Task 2) — in-memory, per IP+username
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
const RATE_LIMIT_MAX_ATTEMPTS = 5
|
|
21
|
+
const RATE_LIMIT_LOCKOUT_MS = 5 * 60 * 1000 // 5 minutes
|
|
22
|
+
const RATE_LIMIT_CLEANUP_INTERVAL_MS = 10 * 60 * 1000 // 10 minutes
|
|
23
|
+
|
|
24
|
+
interface RateLimitEntry {
|
|
25
|
+
count: number
|
|
26
|
+
lockedUntil: number
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const loginAttempts = new Map<string, RateLimitEntry>()
|
|
30
|
+
|
|
31
|
+
/** Reset the rate limit map (exported for testing). */
|
|
32
|
+
export function _resetRateLimits(): void {
|
|
33
|
+
loginAttempts.clear()
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Periodically clean up stale entries to prevent memory leaks
|
|
37
|
+
const rateLimitCleanupTimer = setInterval(() => {
|
|
38
|
+
const now = Date.now()
|
|
39
|
+
for (const [key, entry] of loginAttempts) {
|
|
40
|
+
if (entry.lockedUntil < now && entry.count === 0) {
|
|
41
|
+
loginAttempts.delete(key)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}, RATE_LIMIT_CLEANUP_INTERVAL_MS)
|
|
45
|
+
|
|
46
|
+
// Allow Node/Bun to exit even if timer is active
|
|
47
|
+
if (rateLimitCleanupTimer.unref) {
|
|
48
|
+
rateLimitCleanupTimer.unref()
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function getRateLimitKey(ip: string, username: string): string {
|
|
52
|
+
return `${ip}:${username.toLowerCase()}`
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Returns true if the request is allowed, false if rate-limited.
|
|
57
|
+
*/
|
|
58
|
+
export function checkRateLimit(ip: string, username: string): boolean {
|
|
59
|
+
const key = getRateLimitKey(ip, username)
|
|
60
|
+
const entry = loginAttempts.get(key)
|
|
61
|
+
if (!entry) return true
|
|
62
|
+
if (entry.lockedUntil > Date.now()) return false
|
|
63
|
+
return true
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Record a failed login attempt. Returns true if now locked out.
|
|
68
|
+
*/
|
|
69
|
+
export function recordFailedAttempt(ip: string, username: string): boolean {
|
|
70
|
+
const key = getRateLimitKey(ip, username)
|
|
71
|
+
const now = Date.now()
|
|
72
|
+
const entry = loginAttempts.get(key) || { count: 0, lockedUntil: 0 }
|
|
73
|
+
|
|
74
|
+
// If a previous lockout has expired, reset counter for a fresh window.
|
|
75
|
+
// Only reset when lockedUntil was previously set (> 0) and has now expired.
|
|
76
|
+
if (entry.lockedUntil > 0 && entry.lockedUntil < now) {
|
|
77
|
+
entry.count = 0
|
|
78
|
+
entry.lockedUntil = 0
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
entry.count += 1
|
|
82
|
+
if (entry.count >= RATE_LIMIT_MAX_ATTEMPTS) {
|
|
83
|
+
entry.lockedUntil = now + RATE_LIMIT_LOCKOUT_MS
|
|
84
|
+
}
|
|
85
|
+
loginAttempts.set(key, entry)
|
|
86
|
+
return entry.lockedUntil > now
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Clear rate limit on successful login.
|
|
91
|
+
*/
|
|
92
|
+
function clearRateLimit(ip: string, username: string): void {
|
|
93
|
+
const key = getRateLimitKey(ip, username)
|
|
94
|
+
loginAttempts.delete(key)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Extract client IP from request (respects X-Forwarded-For).
|
|
99
|
+
*/
|
|
100
|
+
function getClientIp(c: import('hono').Context): string {
|
|
101
|
+
const forwarded = c.req.header('X-Forwarded-For')
|
|
102
|
+
if (forwarded) {
|
|
103
|
+
const first = forwarded.split(',')[0]?.trim()
|
|
104
|
+
if (first) return first
|
|
105
|
+
}
|
|
106
|
+
return c.req.header('X-Real-IP') || 'unknown'
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
interface UserRow {
|
|
110
|
+
id: string
|
|
111
|
+
username: string
|
|
112
|
+
password_hash: string
|
|
113
|
+
role: string
|
|
114
|
+
created_at: string
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Helper: issue tokens, set cookies, return JSON response.
|
|
119
|
+
*/
|
|
120
|
+
function issueAndRespond(
|
|
121
|
+
c: import('hono').Context,
|
|
122
|
+
userId: string,
|
|
123
|
+
isHttps: boolean,
|
|
124
|
+
) {
|
|
125
|
+
const db = getDb()
|
|
126
|
+
const { accessToken, refreshToken } = issueSessionToken(userId, db)
|
|
127
|
+
const cookies = buildSessionCookies(accessToken, refreshToken, {
|
|
128
|
+
secure: isHttps,
|
|
129
|
+
})
|
|
130
|
+
for (const cookie of cookies) {
|
|
131
|
+
c.header('Set-Cookie', cookie, { append: true })
|
|
132
|
+
}
|
|
133
|
+
return c.json({
|
|
134
|
+
accessToken,
|
|
135
|
+
refreshToken,
|
|
136
|
+
})
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// POST /setup — First-deploy admin bootstrap
|
|
140
|
+
app.post('/setup', async c => {
|
|
141
|
+
let body: Record<string, unknown>
|
|
142
|
+
try {
|
|
143
|
+
body = await c.req.json()
|
|
144
|
+
} catch {
|
|
145
|
+
return c.json({ error: 'Invalid JSON' }, 400)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const { apiKey, username, password } = body as {
|
|
149
|
+
apiKey?: string
|
|
150
|
+
username?: string
|
|
151
|
+
password?: string
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Validate API key
|
|
155
|
+
if (!apiKey || !config.apiKeys.includes(apiKey)) {
|
|
156
|
+
return c.json({ error: 'Invalid API key' }, 401)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Validate username
|
|
160
|
+
if (!username || typeof username !== 'string') {
|
|
161
|
+
return c.json({ error: 'Username is required' }, 400)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Validate password
|
|
165
|
+
if (!password || typeof password !== 'string') {
|
|
166
|
+
return c.json({ error: 'Password is required' }, 400)
|
|
167
|
+
}
|
|
168
|
+
if (password.length < 8) {
|
|
169
|
+
return c.json({ error: 'Password must be at least 8 characters' }, 400)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const db = getDb()
|
|
173
|
+
|
|
174
|
+
// Use BEGIN IMMEDIATE transaction to prevent concurrent setup race.
|
|
175
|
+
// Two simultaneous requests: only one can acquire the write lock and
|
|
176
|
+
// observe count=0; the other will see count>0 and throw.
|
|
177
|
+
const userId = `usr_${randomUUID().replace(/-/g, '')}`
|
|
178
|
+
const passwordHash = hashPassword(password)
|
|
179
|
+
const now = new Date().toISOString()
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
db.transaction(() => {
|
|
183
|
+
const count = db.query('SELECT COUNT(*) as count FROM users').get() as {
|
|
184
|
+
count: number
|
|
185
|
+
}
|
|
186
|
+
if (count.count > 0) {
|
|
187
|
+
throw new Error('setup_completed')
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
db.query(
|
|
191
|
+
`INSERT INTO users (id, username, password_hash, role, created_at)
|
|
192
|
+
VALUES ($id, $username, $passwordHash, $role, $now)`,
|
|
193
|
+
).run({
|
|
194
|
+
$id: userId,
|
|
195
|
+
$username: username,
|
|
196
|
+
$passwordHash: passwordHash,
|
|
197
|
+
$role: 'admin',
|
|
198
|
+
$now: now,
|
|
199
|
+
})
|
|
200
|
+
})()
|
|
201
|
+
} catch (err) {
|
|
202
|
+
if (err instanceof Error && err.message === 'setup_completed') {
|
|
203
|
+
return c.json({ error: 'Setup already completed' }, 409)
|
|
204
|
+
}
|
|
205
|
+
console.warn('[/setup] transaction failed:', err)
|
|
206
|
+
return c.json({ error: 'Setup failed' }, 500)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const isHttps = c.req.url.startsWith('https://')
|
|
210
|
+
return issueAndRespond(c, userId, isHttps)
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
// POST /login
|
|
214
|
+
app.post('/login', async c => {
|
|
215
|
+
let body: Record<string, unknown>
|
|
216
|
+
try {
|
|
217
|
+
body = await c.req.json()
|
|
218
|
+
} catch {
|
|
219
|
+
return c.json({ error: 'Invalid JSON' }, 400)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const { username, password } = body as {
|
|
223
|
+
username?: string
|
|
224
|
+
password?: string
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (!username || !password) {
|
|
228
|
+
return c.json({ error: 'Username and password are required' }, 400)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Rate limit by IP + username
|
|
232
|
+
const clientIp = getClientIp(c)
|
|
233
|
+
if (!checkRateLimit(clientIp, username)) {
|
|
234
|
+
return c.json(
|
|
235
|
+
{ error: 'Too many failed login attempts. Try again later.' },
|
|
236
|
+
429,
|
|
237
|
+
)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const db = getDb()
|
|
241
|
+
const user = db
|
|
242
|
+
.query('SELECT * FROM users WHERE username = $username')
|
|
243
|
+
.get({ $username: username }) as UserRow | null
|
|
244
|
+
|
|
245
|
+
// Generic error to avoid leaking user existence
|
|
246
|
+
if (!user || !verifyPassword(password, user.password_hash)) {
|
|
247
|
+
recordFailedAttempt(clientIp, username)
|
|
248
|
+
return c.json({ error: 'Invalid credentials' }, 401)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Successful login — clear rate limit
|
|
252
|
+
clearRateLimit(clientIp, username)
|
|
253
|
+
|
|
254
|
+
// Update last_login_at
|
|
255
|
+
db.query('UPDATE users SET last_login_at = $now WHERE id = $id').run({
|
|
256
|
+
$now: new Date().toISOString(),
|
|
257
|
+
$id: user.id,
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
const isHttps = c.req.url.startsWith('https://')
|
|
261
|
+
return issueAndRespond(c, user.id, isHttps)
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
// POST /refresh — rotate both access and refresh tokens (Task 4)
|
|
265
|
+
app.post('/refresh', async c => {
|
|
266
|
+
let body: Record<string, unknown> = {}
|
|
267
|
+
try {
|
|
268
|
+
body = await c.req.json()
|
|
269
|
+
} catch {
|
|
270
|
+
// body may be empty
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Get refresh token from body or cookie
|
|
274
|
+
const refreshToken =
|
|
275
|
+
(body.refreshToken as string) ||
|
|
276
|
+
(body.refresh_token as string) ||
|
|
277
|
+
parseCookieValue(c.req.header('Cookie'), 'rcs_refresh')
|
|
278
|
+
|
|
279
|
+
if (!refreshToken) {
|
|
280
|
+
return c.json({ error: 'Refresh token required' }, 401)
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const db = getDb()
|
|
284
|
+
const userId = resolveSessionToken(refreshToken, db, 'refresh')
|
|
285
|
+
|
|
286
|
+
if (!userId) {
|
|
287
|
+
return c.json({ error: 'Invalid or expired refresh token' }, 401)
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Revoke the old refresh token (rotation)
|
|
291
|
+
revokeSessionToken(refreshToken, db)
|
|
292
|
+
|
|
293
|
+
// Issue new access + refresh token pair
|
|
294
|
+
const isHttps = c.req.url.startsWith('https://')
|
|
295
|
+
const { accessToken, refreshToken: newRefreshToken } = issueSessionToken(
|
|
296
|
+
userId,
|
|
297
|
+
db,
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
const cookies = buildSessionCookies(accessToken, newRefreshToken, {
|
|
301
|
+
secure: isHttps,
|
|
302
|
+
})
|
|
303
|
+
for (const cookie of cookies) {
|
|
304
|
+
c.header('Set-Cookie', cookie, { append: true })
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return c.json({
|
|
308
|
+
accessToken,
|
|
309
|
+
refreshToken: newRefreshToken,
|
|
310
|
+
})
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
// POST /logout
|
|
314
|
+
app.post('/logout', async c => {
|
|
315
|
+
// Try to get the token for revocation
|
|
316
|
+
const authHeader = c.req.header('Authorization')
|
|
317
|
+
const bearerToken = authHeader?.startsWith('Bearer ')
|
|
318
|
+
? authHeader.slice('Bearer '.length)
|
|
319
|
+
: undefined
|
|
320
|
+
|
|
321
|
+
const cookieToken = parseCookieValue(c.req.header('Cookie'), 'rcs_access')
|
|
322
|
+
|
|
323
|
+
const token = bearerToken || cookieToken
|
|
324
|
+
|
|
325
|
+
if (token) {
|
|
326
|
+
try {
|
|
327
|
+
const db = getDb()
|
|
328
|
+
revokeSessionToken(token, db)
|
|
329
|
+
} catch (err) {
|
|
330
|
+
console.warn('[/logout] failed to revoke token:', err)
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Always clear cookies (idempotent)
|
|
335
|
+
const clearCookies = buildClearCookies()
|
|
336
|
+
for (const cookie of clearCookies) {
|
|
337
|
+
c.header('Set-Cookie', cookie, { append: true })
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return c.json({ ok: true })
|
|
341
|
+
})
|
|
342
|
+
|
|
343
|
+
// POST /join — Join via invitation token
|
|
344
|
+
app.post('/join', async c => {
|
|
345
|
+
let body: Record<string, unknown>
|
|
346
|
+
try {
|
|
347
|
+
body = await c.req.json()
|
|
348
|
+
} catch {
|
|
349
|
+
return c.json({ error: 'Invalid JSON' }, 400)
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const { inviteToken, username, password } = body as {
|
|
353
|
+
inviteToken?: string
|
|
354
|
+
username?: string
|
|
355
|
+
password?: string
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
if (!inviteToken || !username || !password) {
|
|
359
|
+
return c.json({ error: 'All fields are required' }, 400)
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (password.length < 8) {
|
|
363
|
+
return c.json({ error: 'Password must be at least 8 characters' }, 400)
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const db = getDb()
|
|
367
|
+
const tokenHash = createHash('sha256').update(inviteToken).digest('hex')
|
|
368
|
+
|
|
369
|
+
// Look up invitation (read-only, for validation)
|
|
370
|
+
const invitation = db
|
|
371
|
+
.query('SELECT * FROM invitations WHERE token_hash = $hash')
|
|
372
|
+
.get({ $hash: tokenHash }) as {
|
|
373
|
+
token_hash: string
|
|
374
|
+
team_id: string | null
|
|
375
|
+
role: string
|
|
376
|
+
expires_at: string
|
|
377
|
+
max_uses: number
|
|
378
|
+
uses: number
|
|
379
|
+
consumed_at: string | null
|
|
380
|
+
} | null
|
|
381
|
+
|
|
382
|
+
if (!invitation) {
|
|
383
|
+
return c.json({ error: 'Invalid invitation' }, 400)
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Check expiry
|
|
387
|
+
if (new Date(invitation.expires_at).getTime() < Date.now()) {
|
|
388
|
+
return c.json({ error: 'Invitation expired' }, 400)
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Check if already fully consumed (pre-check; atomic check below)
|
|
392
|
+
if (invitation.consumed_at || invitation.uses >= invitation.max_uses) {
|
|
393
|
+
return c.json({ error: 'Invitation already used' }, 400)
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Check username uniqueness
|
|
397
|
+
const existingUser = db
|
|
398
|
+
.query('SELECT id FROM users WHERE username = $username')
|
|
399
|
+
.get({ $username: username })
|
|
400
|
+
if (existingUser) {
|
|
401
|
+
return c.json({ error: 'Username already taken' }, 400)
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Create user + atomically consume invitation slot in a transaction.
|
|
405
|
+
// The UPDATE with `uses < max_uses AND consumed_at IS NULL` clause
|
|
406
|
+
// ensures that concurrent joins for a max_uses=1 invitation only
|
|
407
|
+
// succeed for one of them (affected rows = 0 for the loser).
|
|
408
|
+
const userId = `usr_${randomUUID().replace(/-/g, '')}`
|
|
409
|
+
const passwordHash = hashPassword(password)
|
|
410
|
+
const now = new Date().toISOString()
|
|
411
|
+
|
|
412
|
+
try {
|
|
413
|
+
db.transaction(() => {
|
|
414
|
+
// Re-check invitation state inside the transaction (may have changed)
|
|
415
|
+
const currentInv = db
|
|
416
|
+
.query('SELECT * FROM invitations WHERE token_hash = $hash')
|
|
417
|
+
.get({ $hash: tokenHash }) as typeof invitation | null
|
|
418
|
+
|
|
419
|
+
if (
|
|
420
|
+
!currentInv ||
|
|
421
|
+
currentInv.consumed_at ||
|
|
422
|
+
currentInv.uses >= currentInv.max_uses
|
|
423
|
+
) {
|
|
424
|
+
throw new Error('invitation_used')
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Insert user
|
|
428
|
+
db.query(
|
|
429
|
+
`INSERT INTO users (id, username, password_hash, role, created_at)
|
|
430
|
+
VALUES ($id, $username, $passwordHash, $role, $now)`,
|
|
431
|
+
).run({
|
|
432
|
+
$id: userId,
|
|
433
|
+
$username: username,
|
|
434
|
+
$passwordHash: passwordHash,
|
|
435
|
+
$role: invitation.role,
|
|
436
|
+
$now: now,
|
|
437
|
+
})
|
|
438
|
+
|
|
439
|
+
// Atomically increment uses — will fail (changes=0) if race condition
|
|
440
|
+
const result = db
|
|
441
|
+
.query(
|
|
442
|
+
`UPDATE invitations
|
|
443
|
+
SET uses = uses + 1, consumed_at = $now, consumed_by = $userId
|
|
444
|
+
WHERE token_hash = $hash
|
|
445
|
+
AND uses < max_uses
|
|
446
|
+
AND consumed_at IS NULL`,
|
|
447
|
+
)
|
|
448
|
+
.run({ $now: now, $userId: userId, $hash: tokenHash })
|
|
449
|
+
|
|
450
|
+
if (result.changes === 0) {
|
|
451
|
+
throw new Error('invitation_used')
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// Phase 2: If invitation is linked to a team, auto-add user to team
|
|
455
|
+
if (invitation.team_id) {
|
|
456
|
+
db.query(
|
|
457
|
+
`INSERT OR IGNORE INTO team_members (team_id, user_id, role, added_at, added_by)
|
|
458
|
+
VALUES ($teamId, $userId, $role, $now, $addedBy)`,
|
|
459
|
+
).run({
|
|
460
|
+
$teamId: invitation.team_id,
|
|
461
|
+
$userId: userId,
|
|
462
|
+
$role: invitation.role,
|
|
463
|
+
$now: now,
|
|
464
|
+
$addedBy: userId,
|
|
465
|
+
})
|
|
466
|
+
}
|
|
467
|
+
})()
|
|
468
|
+
} catch (err) {
|
|
469
|
+
if (err instanceof Error && err.message === 'invitation_used') {
|
|
470
|
+
return c.json({ error: 'Invitation already used or invalid' }, 400)
|
|
471
|
+
}
|
|
472
|
+
console.warn('[/join] transaction failed:', err)
|
|
473
|
+
return c.json({ error: 'Failed to process invitation' }, 500)
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
const isHttps = c.req.url.startsWith('https://')
|
|
477
|
+
return issueAndRespond(c, userId, isHttps)
|
|
478
|
+
})
|
|
479
|
+
|
|
480
|
+
// GET /setup-status — Check if initial setup is needed
|
|
481
|
+
app.get('/setup-status', c => {
|
|
482
|
+
try {
|
|
483
|
+
const db = getDb()
|
|
484
|
+
const count = db.query('SELECT COUNT(*) as count FROM users').get() as {
|
|
485
|
+
count: number
|
|
486
|
+
}
|
|
487
|
+
return c.json({ needsSetup: count.count === 0 })
|
|
488
|
+
} catch (err) {
|
|
489
|
+
console.warn('[/setup-status] DB check failed:', err)
|
|
490
|
+
return c.json({ needsSetup: true })
|
|
491
|
+
}
|
|
492
|
+
})
|
|
493
|
+
|
|
494
|
+
// POST /change-password — Change current user's password
|
|
495
|
+
app.post(
|
|
496
|
+
'/change-password',
|
|
497
|
+
async (c, next) => {
|
|
498
|
+
const result = await sessionAuth(c, next)
|
|
499
|
+
if (result) return result
|
|
500
|
+
},
|
|
501
|
+
async c => {
|
|
502
|
+
const userId = c.get('userId') as string
|
|
503
|
+
|
|
504
|
+
if (userId === SYSTEM_USER_ID) {
|
|
505
|
+
return c.json({ error: 'Cannot change system user password' }, 403)
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
let body: Record<string, unknown>
|
|
509
|
+
try {
|
|
510
|
+
body = await c.req.json()
|
|
511
|
+
} catch {
|
|
512
|
+
return c.json({ error: 'Invalid JSON' }, 400)
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
const { currentPassword, newPassword } = body as {
|
|
516
|
+
currentPassword?: string
|
|
517
|
+
newPassword?: string
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
if (!currentPassword || !newPassword) {
|
|
521
|
+
return c.json(
|
|
522
|
+
{ error: 'Current password and new password are required' },
|
|
523
|
+
400,
|
|
524
|
+
)
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
if (newPassword.length < 8) {
|
|
528
|
+
return c.json(
|
|
529
|
+
{ error: 'Password must be at least 8 characters' },
|
|
530
|
+
400,
|
|
531
|
+
)
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
const db = getDb()
|
|
535
|
+
const user = db
|
|
536
|
+
.query('SELECT password_hash FROM users WHERE id = $id')
|
|
537
|
+
.get({ $id: userId }) as { password_hash: string } | null
|
|
538
|
+
|
|
539
|
+
if (!user) {
|
|
540
|
+
return c.json({ error: 'User not found' }, 404)
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
const isValid = await verifyPasswordAsync(currentPassword, user.password_hash)
|
|
544
|
+
if (!isValid) {
|
|
545
|
+
return c.json({ error: 'Current password is incorrect' }, 401)
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
const newHash = await hashPasswordAsync(newPassword)
|
|
549
|
+
db.query('UPDATE users SET password_hash = $hash WHERE id = $id').run({
|
|
550
|
+
$hash: newHash,
|
|
551
|
+
$id: userId,
|
|
552
|
+
})
|
|
553
|
+
|
|
554
|
+
return c.json({ ok: true })
|
|
555
|
+
},
|
|
556
|
+
)
|
|
557
|
+
|
|
558
|
+
// POST /logout-all — Revoke all refresh tokens for the current user
|
|
559
|
+
app.post(
|
|
560
|
+
'/logout-all',
|
|
561
|
+
async (c, next) => {
|
|
562
|
+
const result = await sessionAuth(c, next)
|
|
563
|
+
if (result) return result
|
|
564
|
+
},
|
|
565
|
+
c => {
|
|
566
|
+
const userId = c.get('userId') as string
|
|
567
|
+
|
|
568
|
+
const db = getDb()
|
|
569
|
+
const now = new Date().toISOString()
|
|
570
|
+
db.query(
|
|
571
|
+
`UPDATE session_tokens SET revoked_at = $now WHERE user_id = $userId AND kind = 'refresh'`,
|
|
572
|
+
).run({ $now: now, $userId: userId })
|
|
573
|
+
|
|
574
|
+
// Also clear cookies
|
|
575
|
+
const clearCookies = buildClearCookies()
|
|
576
|
+
for (const cookie of clearCookies) {
|
|
577
|
+
c.header('Set-Cookie', cookie, { append: true })
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
return c.json({ ok: true })
|
|
581
|
+
},
|
|
582
|
+
)
|
|
583
|
+
|
|
584
|
+
// GET /me — Current user info (requires sessionAuth)
|
|
585
|
+
app.get(
|
|
586
|
+
'/me',
|
|
587
|
+
async (c, next) => {
|
|
588
|
+
const result = await sessionAuth(c, next)
|
|
589
|
+
if (result) return result
|
|
590
|
+
},
|
|
591
|
+
c => {
|
|
592
|
+
const userId = c.get('userId') as string
|
|
593
|
+
|
|
594
|
+
if (userId === SYSTEM_USER_ID) {
|
|
595
|
+
return c.json({
|
|
596
|
+
userId: SYSTEM_USER_ID,
|
|
597
|
+
username: 'system',
|
|
598
|
+
role: 'admin',
|
|
599
|
+
isAdmin: true,
|
|
600
|
+
})
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
const db = getDb()
|
|
604
|
+
const user = db
|
|
605
|
+
.query('SELECT id, username, role FROM users WHERE id = $id')
|
|
606
|
+
.get({ $id: userId }) as {
|
|
607
|
+
id: string
|
|
608
|
+
username: string
|
|
609
|
+
role: string
|
|
610
|
+
} | null
|
|
611
|
+
|
|
612
|
+
if (!user) {
|
|
613
|
+
return c.json({ error: 'User not found' }, 404)
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
return c.json({
|
|
617
|
+
userId: user.id,
|
|
618
|
+
username: user.username,
|
|
619
|
+
role: user.role,
|
|
620
|
+
isAdmin: user.role === 'admin',
|
|
621
|
+
})
|
|
622
|
+
},
|
|
623
|
+
)
|
|
624
|
+
|
|
625
|
+
function parseCookieValue(
|
|
626
|
+
cookieHeader: string | undefined,
|
|
627
|
+
name: string,
|
|
628
|
+
): string | undefined {
|
|
629
|
+
if (!cookieHeader) return undefined
|
|
630
|
+
const match = cookieHeader.match(new RegExp(`(?:^|;\\s*)${name}=([^;]+)`))
|
|
631
|
+
return match ? match[1] : undefined
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
export default app
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { log, error as logError } from '../../logger'
|
|
2
2
|
import { Hono } from 'hono'
|
|
3
|
-
import {
|
|
3
|
+
import { sessionAuth } from '../../auth/middleware'
|
|
4
4
|
import {
|
|
5
5
|
getSession,
|
|
6
6
|
isSessionClosedStatus,
|
|
@@ -26,7 +26,7 @@ function checkOwnership(
|
|
|
26
26
|
c: { get: (key: string) => string | undefined },
|
|
27
27
|
sessionId: string,
|
|
28
28
|
): OwnershipCheckResult {
|
|
29
|
-
const uuid = c.get('
|
|
29
|
+
const uuid = c.get('userId')!
|
|
30
30
|
const resolvedSessionId = resolveOwnedWebSessionId(sessionId, uuid)
|
|
31
31
|
if (!resolvedSessionId) {
|
|
32
32
|
return { error: true }
|
|
@@ -46,7 +46,7 @@ function closedSessionResponse(message: string) {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
/** POST /web/sessions/:id/events — Send user message to session */
|
|
49
|
-
app.post('/sessions/:id/events',
|
|
49
|
+
app.post('/sessions/:id/events', sessionAuth, async c => {
|
|
50
50
|
const requestedSessionId = c.req.param('id')!
|
|
51
51
|
const ownership = checkOwnership(c, requestedSessionId)
|
|
52
52
|
if (ownership.error) {
|
|
@@ -85,7 +85,7 @@ app.post('/sessions/:id/events', uuidAuth, requireWorkerToken, async c => {
|
|
|
85
85
|
})
|
|
86
86
|
|
|
87
87
|
/** POST /web/sessions/:id/control — Send control request (permission approval etc) */
|
|
88
|
-
app.post('/sessions/:id/control',
|
|
88
|
+
app.post('/sessions/:id/control', sessionAuth, async c => {
|
|
89
89
|
const requestedSessionId = c.req.param('id')!
|
|
90
90
|
const ownership = checkOwnership(c, requestedSessionId)
|
|
91
91
|
if (ownership.error) {
|
|
@@ -112,7 +112,7 @@ app.post('/sessions/:id/control', uuidAuth, requireWorkerToken, async c => {
|
|
|
112
112
|
})
|
|
113
113
|
|
|
114
114
|
/** POST /web/sessions/:id/interrupt — Interrupt session */
|
|
115
|
-
app.post('/sessions/:id/interrupt',
|
|
115
|
+
app.post('/sessions/:id/interrupt', sessionAuth, async c => {
|
|
116
116
|
const requestedSessionId = c.req.param('id')!
|
|
117
117
|
const ownership = checkOwnership(c, requestedSessionId)
|
|
118
118
|
if (ownership.error) {
|