commit 43ee2813a06315d0b8c88ac290cb1a37b75a3f20 Author: Sebastian Zell Date: Sun Jan 25 18:55:34 2026 +0100 Probleme mit der Installation in DOcker gelösst diff --git a/.abacus.donotdelete b/.abacus.donotdelete new file mode 100644 index 0000000..8369c20 --- /dev/null +++ b/.abacus.donotdelete @@ -0,0 +1 @@ +gAAAAABpdkLcuZ-miUqWjdB0Cd3dpyc1YEmQ8RIAGa-n_3eNbVkstw2BD1X53OMGz8gtf0FxtiCFoIcHfOQ5nT8jAV_c78JdPQ7TgTrfC2RhHeFhq5KBcmeY1CDhHwM8Y8Hn-VVzQyxAFfD89ch5n8zPcsMIVdmBH5y-r5LYVF7MvHrYVV6PfcBD0NNhn6rNJRHQThtYrhFm3Ln94HF7fvlhWwNogMHQ_6-BE7cqgjGQGMf2hzJIBl2Ljuw2ViSDHmIXixr4bMD4CRvbEr2oSAlsaDcIFNdw2sy5Jena9YW_t7qe4Qhy2J84fIF9oewlTuW-jYw3xiVvYUxKRfQNhYUU0mHqP0t14g6gPgow7gZjIBOD9ZhvZsf_Swc4XqUdRfRESziHnWh9na_mlBFeMmpFzUy9bzNkz-H_v9RmkGusKBqNPDtJkupRdX95ZEjSnh7zmmoNECCz6f2NVBq3Z47fvPA4C1QbImQuHbZkFFkrRUXr6XGRaEh0e-K23itcC1AtOd1V0MmDEecTfkw0HeYtQ3x-cWQmkaZp62l2A9l338P48lhCSSDloh_xOT851SDpkf3CBionDgKO7YSiMykTASUYJs9-s8KR2bJx_bq-Mkofa-nCqdVdivG3gocd0jShpu1Pkt7AplciUFAKIResUHzGBhpk6FhxVfzAy9mIQWBsiXrKI1sDBdKXxbE7FOLwVl2p9XIZFscL9R_v4QJVK4ANih_ysgTUIQDnM561asQcLMCRL5_k5rhfzVdB5uDibKqlM5TjMo0oI7Wj5N6qbpW3GdOKhItgYz_uOvg-2D3ix_IIYmQ62pfl5RcTEyZvKxdsbfvK5XZW5oFvgmjjoYazPYPjq2N2meEOXmU63ftGgv79g1IgjqxWXotBgeAZ9rh78J3JYvH_-9G2fBxWcHbzHfQZ4YTUrKdra4SV4RR9HBs3pPBQKHSWre9GcZz3bp7MtOg_gOeE91tftTKXcanILJmUNFyX18M3c3rZK6nWBhcdn7IRExi2wOmaVNBUSFCD_cd_PNOziO1IVsHrZPrPPRA2Jyp-LAeAjRYoa3FVibqhMWdQ9Yu-C0Q_Re5JFiMfavfZShhbAFkDsffXkUVfBxJILLS-wFt3SLyhatxfbOih-01-Q38d5XFREOTQg0ZLLEO-liGKrAs_bK6N9LvTGpAId7AI7vN9GCTq3F6GtWXfHDx9a3zPRPXdkh22oBFAwakamgr5EbCB6mGKypGiENpbfGF10KKqNKE7iDslJ7Ucacc_fSwceWub2DHPQPOU5f6YGPaPgD0sFLCQKsR7QYZRfbBLZyAirnMWfcNDAQHB8HPSB-P5EVGi7djhrvJaIGSN_JK05t4_ZhOg4NA7zXkfWtGLtu6VQrEj_Vza9UbEEO50MN3JdPB5IbJOjBVqpgdYfCuvY_NxPWkCAeQXZyVrGCqJ4TrCwW6PpSZRcrnfxyvw2GkNwR2zpq9DbXR4lXXsTL0CUzi_iE9-zX8VnRBbKNpnt_WOTjMmOig_wvTPnk43LQiWnUGGX8bJl87L4lTR6LsXLtwE-YD51aDydfpE-jbe3xNj9ZO4B0x31-GDt8WJsD-ZBhXN0QWnf2QA2pm1i6nDQkSU51FOD0NexU2vMKpmkQO1ClhjqrcBBaKbtI9V-o3bTRokqYbnGvH_LnegFJ-Yp94hfd-T-X6U2Hv1qL8AzKauHs0Sr7niFVR_-mgkZLoeT93OtXB7t61QE4v_1POBFJweMq_Nc1-4HSX7lVHqvEznpkgkg09_giV1EC8AN8w0MxiDOLVmEMRdg1Y1ybJJ3SV93g05lxSpbMAa7TW5pp6Kne8fCu9lwDFBp3kVoxouqaUOdAvp6lawkKvDvEvee1b9nbHsApvO4WzPcMAdnUMHyBSfpHRpQrXybKY8nb8yzB6QUyvtkYtgZtaJu6Vh-V2RkCsqIxBBTjUO-JE07Ix7R4M7az0q60zjKKMehijvqNsKkXlOX3IDQjk6QC6AwhwOJeaej4YlooxZFPl3pQxxhleY7-ivruJbT-mjav-vz7nunYfeRbjz7tWajvWOL-3svt2g7HSIuIhiiG9MKgJTeEKThGfXznYdvtN3fLQ_ef7B2lIxaB8ZJIqQuFNh5QY0g4rIL30B2FMbOqB7lIJeFtc-Y2enyu4oJxWm5I1bkolzPdoDGK0QWcrHyv8oS7RUhirTOESsH2bca4ynFd9I9DZfKwfCb8gl66uXDv_dmSBYVz9pQeteDrjUECO7aV2VSoqqj7zIA-1E2XTwQjwqhdXIU9ne3h3WZzhArJJstRNp6B2raJX_AGzEe2E2PC-GdT1fAMo2FB894dp2KDlJ3wr50ESzZunnSUNPIeRQez92SM9A6JufDs_JrPgrK7F3PlsYGCkZ7PsP3GFn3Dv7VE9JDRfQU3tBFBqfKGFEx6-rIeyw9CQROBNnmNYSbNJHM8Pb0Ci1dn10fy_Ais6N5O9B-9Ly0hdvADkUD1U7EpEHrWmGgtfn8V8keWRv-q8m72NnVbmvw90z5TeHc-tULIPJbLHBRbnumAOdK7XiMtYqyOauI5ZVq8QokfsfD0g_tuZOsxY4HYSIDPdpak-dJzCXbS5kY1NMYR9M9bGg7v60r7gGaJUl_vZn71p80NeKiU_08H_V589N1PLVyzT3UAb2wvPLvayz4eUEmsjqduPEGdVV1-fG6bHC9plJHpA5c9ppNrXJWav_HpramfcCekj9IXXMIitP-4Q4OBQdY8Vq7uQM9QK4mRv7hw6qCfEEaOoFDYkW7wB75y16s9KLzFQWvmRUnDrNy1uPz_r1ojWvwwq-iK6ik-YLLEhzpIrS5w2eR2OFuQ14qKdeafkt7QyYBo7W8PK7PaDt7mtU8-r6HSdAQkS1pL7LO39z5WO6PNLVTIButdLPp3DmL1j4TMV8_yZOKlft3aYrvJxrQw9IqQ0_LxY2fEASdtltR-0fKr2QRSwOPHA0PXJPaJSYIVzFiVEXOeio3FbDnq4iP2CeyIqNJlC4iik_O4Yy4ZXaemOfuiW8lG6IrSBS1iy6xLVNXc65bZAR6Yq4PbWC1iMXK3FgWL0F9ft2ajE7Ncs46XOIE_55yJPusJQi7dujB7l0KxYqYAd44QrenIFFdAUFCbp_yd8dMfaBF15fCvIBA0i78aZa1IZzPZXHlEto1g-lBwoKmBHzB7nSaAVErnz1YKW8EEMOjb_KnOOKEwFVHH8vMCxm4IazIfehwO8ZuPSluQ9a-NLVzp1yqDh8f33EaWWiGmysAcnGm3Kaj3GebcyjHaGU6slg40ZJxgoGNGV7vP7N49EYtaXMpmuVlP7fMOMNVRFUD0v7strBeVmp9n8Y5V_i91pYJj_n1zo9WUBliPkDbLmMwg5TFrPY6z6obFbvoS7XOZ5996P9G7-doCdQcP9xjOpV2oJIdMUgu_GljC_orQZZXuT9Y6egxtq1w7NytaRbDbcrPy-_zIpKcEF6NN5iVbBukar5O6a2k5YhsHV0hq-SMYDpyR4t_jl3zdkJ8UHvCxRypOrTxhxzhmTRDe7MShyJNe6W0yFCGepJp_ljnBN7zdFLS5KjRfTqcU8tTOs3WDPp21ZKG3aTDgKLcj6TDMsUBagY4btp5gXATT6SM2jI1V3x1mspe9W4pC3hY4lqtN7hpn8Fz3v_nk-kQamI3FJNnW8c5BBbctFXK5mnxMq07Npws_QBzG5mgjwub4w0aXy96yCbx8o2-4t1amiGSKtKL7sTRUDK0dOlXMjB7wHcP27HSItg8RRs_Y8_DEUCt_v4ss3N4lbtz4QjWPhMsi4dhMn8CRtbtzCsFrSfZ3PEoMIpPVfBuCNBIgd75aimV1CpLTy9Oi3cFaUgE30vuX_ItVncIUI0ORRbgoxlRAW553LF3QOMRPebnfkV_HyMRnGFwuWyVW13CTm_PbrvwuMQUJqyyjcl1I651ClblJm1vxq9duUtAMHTyfKZApyjOEUWF-rGMdbYERXHbILg21c8q05WYwFR1OV79VBw4nZNAVXP5-55IvcA1agAwHhFs0fvRPdMYaKO6iMoYXAb5YcYFlzZMCcIq3dRf8pOwiyWo11D-iUHh6RxZPAPlmVI4osqO3-1Q5LDGPV1mT0Gi4VLpjHvKO6Jpn3SZMEq9mw6OJcpXDOPLpV3WvI4RshV6OQWRYrevU8GTRs6tH-k7Dm1J_cWVboeBb5FU_r1ueF9hYXUgzs30riHc-JePWELqJs19eI5gZ4_NMabkVHErjb4Q8nHFn5uwkVb4GoKrptDdkxdJXrZ_d1iW_DIilKvd0_Ye47HD9GaOkO9ib2jlySfIIpnckpHeH-2D2lRRxX7j1fxIBQBbcTLq8UjITdojMP-WTPZGieZU2bbdgP0ZbFk59v7Fkbxl2mOAKTCDIAZDH4pRtkHQ6EaxwzyvtLVxvGYXXYVPVupfwYO5KLj74YHKrysjlQHbamfyAEq7hGovGgvFeBHMdynOw62TMMC0ot0xJOsio7nmDOvzWRKmI9xBuzgMaeTFnw5RaTRPMoQ66BnPfjP74qWp4yLYdEgU7EnVDlISlRAiHPc2oJ8IRC84Ez9dg38R_hE2gWPgWVQhmdSc18WIZkK7Un87TNgRFB1h6EB3tnMxXtZJ9pZ-heQCPz1fWZVjaUXkMeepCr6VqPFM9zIkyGgvvG11tthOUtiTjhxH7JMh7idsisdSSexlz4vHg7mz0t-wV4q3PRUXcZcij08SNAcwfC-lbnZ5S9jLpowUVy43dkRE32ML6ca8sV8mwNWeLE01VV0mRnt6ooIOcQXDlaDWFwCWKKmRyqYdQ0xyHdFHyLVHlnp9ElFxFkEYf5x8ysuY1_HWUZJ7oBfmX6MNNgGiJhY3VOZMhDsWEvAlF9aFeVvizKr2OldDvByS9XsoI3v0-vsrhGsuetTBf9cZt4zTeimZpnko34bPpAMN9BWg7OU3RXvk-XX4yY5jsieXbK1Q5CvA8ld92BI4DfzJv_ho3jPpN-Kpyu_wIoQz_C1E2T4fdoMHuqIwWh0Kr6j3Q-Y0loofvT0zCmn19FZ6c-o3qiKM_T72LUAMFjRy8TqPIjwCLlqd9IPTGdSkCm-f9qmNB5i3FY7kejd4I-r410N9G4_ZlLNhaElTYETFbtq0EOjVI1ORHOkS9gN2KGx6AU_PvQ4_EVFLnaRMMV-uF2ZCIkUfQsw9hDFE0Y14d4kFFo7uT9k4CaN-ic6k9emn9ga0p_qaPslu7C1B2jR4qI6hi6Xi7QiwoYatKDSq9lJyS3qzmaQllV1NIKOEVHVYNwBMlcGE9sdDz8RAr82wOcqkXCF58D740Ij74NYz2Z05Lwi4tAXpLK4IhEX8Ach9SXJdq3IEmIGFTjMMFWfVKiIB0Ode2014Prk2XmGYAKk58wO9DmIVwfCYZHz8Uno1uCFBIPbt2cK0zaE5rdxaE6FGaIcaqe2zP5VqDfO-9bu-X3x5pS2VTSU4DtaX2i4aBgdNwPxySeXecqRW_fh1aFaBzbhh-pMYcF3X-ZdjRqxCg93cC1BXnDWl5MaJDI4Kf6j5CHs2mYPLoLXrJzfg3u0ZgkbgpMSSZIhbwsINFEEUtiLtCdqpfADOv8iTtFpkNZymO9FhJBjq_BUryqx9PU6cYeTfFDI218XImJcdaPuTg2pqwHeaOL4SreyElVTfLUqUUNyDO22QSiWNkin3qLZM3wnTfiVIig-gWXd9W55FDObWv6kw0voSlncpFmKQ4NIQQLfCFXZuSrFL34x0D6pcisRvd1JKic0hGwwCgt_nJLqGABLgq1F6HChD5Mm-m5DEUYPVV4nwoFh411OzIcHdILcTTzdY8xmSViqZ2gYImsMN3ZwMDY4W0sZWo3EQSGmDIV2t6QK_IH90ovLQR6SkGL3YZ_yPU9F1mnAAhNqZ4dqfx1I9x5SFlbw4ZtrCufmuf2poeyGHz9EiHfXJzbe62kcLrB8uZs-6GQS3VIk_CfLMjbzbiDVumRgQafGX_hJkpD0o1S9DLern4FuT4K7GzRCG9UQ06APmWJy6SXXqpdBOLr632j9KE5AttwKm2Xuf1I8ebjnQvm9EwfOeGE5YVTcI5VxTpFAw9LaY2rBMxORjGkqct-9kjVSRpoKGXC4N-dhBPcxko599svijefu_Zri2fiMeTPSe-Ol6aKYPnVcI86shuD4M_F0nS2KL-t8pQEUJxXggmBkQYRJW0GvZYXO1Z40VrFQYi8k6pxySbx9gbBlpE2Mx3Zq7j0M4pXmB93Fyh0Rk1zuMdKp93ic5pZ2JKT_cPtuEUP2uMz4jC_nVA9rU2ZzvuDSRlCJl0OGUKbItqm43H3P4S81jCd_hFfSUc7f4FrIp9KJcwIbryUM66764innMpT1xHRVEAWDpvt4uDni5rNzsVf_7Wlu3eGsd_ZcMbpNOI1dSD-O7dIXb64KYNSPbTPLfYA9IGVWw8-IfyqZaVaC7hcEq2_ww2OiZIT8E_HIehC2ErKk3YFOvxek0D4IR7Y2OWmzML5Ju_inMs63LvSsQzlp3HjZ9cmNufxToguyOobn8kQEP1EQ3sxYVNmLRAxyfpstRFyMs2_SXquIdz0eo0MmLEZ4U0qqPlkM3D6aSHAv1kdzfJvF-ILhO-D-PV4HPt7fAJCtJhORX2bTREK1o4liT0Z_Lx9--WmzlcRbLuQ6aF94BztdqQLYLgtzh9URoKtligISbQuj4tGEaqCKJoJS2LKstNJxrGAHzprbfdLssdRBtC5x16T-1kaJwJVPLEjFiq0vZusBDa0a9_DS1zKbJoktpUuve1ncryuWZP08Ml0BDOWt0sAP6Nqiy5EGP_G9mC79mrc3FKzfqdb-RJ_I_qap9SjuW2O9iBLoo735eCM8_7GinEEq4Q1YWgS_cPQG-ckioL8BZGIvHnAEIhMDTmDB9GCBfQmegqym0AIokKPhFv_0xSB_cKYpxiGlUtAg1YfTtisB7N4MEBEMVebHRryoeTuA71hKaDbFBvz-28c0XdueWm2XhU9yHM_kNpBFV961bhGcLAB-pkmV2DnMPQ6-tmVrr1-9KJpxYTrkBH3MsC6Rp4W2oUhh_2J_KYGGLZIDTbV4C7TM0nroksIJZC0xoO5clnwDy0A_QsHGRsi2yfv_QT03DdQg4ZxbYZA4zyHxMP8v0lpYVeuoCPlnvb2cKB5iVMZghpXh8BMkjx6uFQGH7zyn7h0so2ZxnP7NLM5Y4fQ_nxC1cbuJk9aRB2A0ABFF7t9CJ8Gw6Tu4Iu99vZ1JCq6vdv5VrYpHA7rwFOQDQ606QPHFJpaaVTy6ROVscVdo7KnRTraJadR4x21KyHlWtDKZ1GGoCX7CxPyT4Zy56_-qvJTBro4zffafVe5DTxL4IypErNp7Q0Acnh6HyFzDui-3KhmF-rUclkqPHA4-0B7CXnbIO91dYmQVsJJPUCDz8PFP7so1J564vrDp5tq6YM8pCmfa7IhIU9rOr0bRGxTiB9VGHYzDJ3WV15FEgV0V2pX16pYr9MLRGBioJ1CdzC5YCFOZRlLyBNbvNxs5ve6b9kEJpwcBtfMYfF2iW56qOOpJ3NcPI9StPrRmXTPVYl6AnnLnysLcw5qch-v5-XEtZxn9ZEm1SuCwvOdvlLrBnJGT27MGcSrY488V4mFabNLV3uJRMz_MwD9yu3u2Ptgx2hWy-MUjeIeiNhIZ0USmbIiHfCTV1zDbc0__6EVbKZtZlbZUO4z2w9ZUeO1yemViq1cxLDTKFYcbVOeixHg-slGBZvFiXybA1yxVkGI7ioTbVZb6JAPGGSsn3Kr5319166kNK0j-XVxpbpdXKY2-VcCFcqVXgUrzMMN_uj5XUqwQ4rdLFE5xpz_Y-zFNE4bPumyiCep059BHDeU6D3rm1v4Uwzip3djB2wiRN_jLc4z_DXBQP2ZWxc_6Y7QgWw9ZiNDevLn6utn-69CZnltxR70SLgZew2Pok9MC8mLtMqdOSSU9LOtrqBV1a-CmI5aYqOB3iKBrDCYoFBMyjmueCntzGO0BrOIeBOM0sUtqsc4gkkeFlAO8GgiHdAFVr5Pk4CdClhAWUA7jFIvn7_eMlxHHlZfkMPOXW4pWEmiqDMX5DSMbifN9KoXkxOa2gGB8sNfeVurOy8b9JLBSaYUXUz-5VJMxQ7_YZqFsY8ADEVZor-pnb0xmeoNl7ZsesfVAJ33igXFep28g9pd410AgptePtTGrIFFa52HHkANEX-PFFKW1ZFvZW9bhtcTb4sDiAoEjUH3Ev2MOYdBF1WxrdTywwnL4MaCDK7eSJFXxDcQVaAUJxStey9guiR8atJHRopi_we6--Mpq2AuHdaEq0knQ6i5iOHCyYakNLHUqZX5suFXZmoOqTL4ULNHf4xxNGFeALAluwUEWJPcKLWbfClbI_1qXEE2CZ2u4Cn3WZ3loW03_idvi2waQjYa2mAji_lcwStzeoJAs-xUwMyWdmyuApbKFykHSmQCeA47aQYVG7Oc0I4PA1EJLgtw8C8PdDBBQC2RlhxwLe50YTdNu1UeB1USLHYNrL0XnCG93wUbabdO7bnQC0-nAOgEwUjlnLseBWWVcqh6FyhFpcZiqcD7iylZnNB8rKE8eoDB6vtUWEo3GB2blgto9xDKiuwsZ1Wa_5O_WP2sttEhYdDvfh9m3FC2pwlxJK8gpeADWJ5sJc8B-QAYVtCiM-8DaM3GeONu6b5gqnn_yINq63bxHfc1P638NDz27TACZ0oporSxIi481k_XEMiO1v0h2agHIOSAXGqnR-w0_ZgmzOBZ6csaX2B3dDFOOonGaKDpH1l9HtcK6Bur_gVB0XxVmVf671WQXyGUzAwJNGrSEyHaRPxka4N36OqLSff8M063e9KC_-sQnM3Z9zIlVuhX09pejC1FvCUwHNE2VK6kLZWzx4A1Hovr3P8WAta6SDZQvgnGYmvtls9gKJgRLqfrsj07I2HSPfSN0ZKq5M05RaMwDTdMj5uC0ZjEX161wiG9PNSLfnpt9il8zI9m9KTuK369plzgtJ0s01m9J-L3766usr_qjWLuKuuRZCOzSv47yJnL2dKruU3_0I0dibICXaVL5qQamidRbq_f-wspuB6C5oGM3H-iz0pzQP9dNAvGKKfhsiIrPDdNmwcx0DyUlFfOIgVicJ1HM_4PPZJAW9_v1J3av2h0s0fLowKWSqJ5UAwgbyHPqYBxxmYmzSmRGrmvzRKswL6GFtQxE-SxY3R5t8DlD5X30nVMRpnOQbs-0AKuL1m_-Lq5XfiON0zUpGKQD5etU_LA8PMS3Qesfrdlo2VtmIAw65srdiZcMOl_u3k-GEdeFyyqO-FRl-b7IsvgeNMFRITkfHZ4gm0Hyb8wGHB9BYy0iWPup6I5QAMp-SNv_wcwUz2R-QGTYsOCpcjxxwkf3GH0nmSMMJKlX3dDqabywyni4wj20mK9QOQqZ33FBgA4Xb2LwH90je_A15KOEmbMrP-ChnazyQX_tV2dF3_lkabmyG7UQJCA1qO2_QszIf4kI4FbfEKXkBkH9uvLeVscfsKEq7YRBkU0moNT3gt9TbHjOfmSW0EYOEBjXGyTAxb19RFT5R0rTQpG1XynzLOdh5FrQWBCaoTRRAJhRaTwm915RG5xxbn9e4_PYKwdwSxGFzvjIkW9URpl2Ru8qiu62Gw0mgfsqFh7SaJlve6m6zXswfD0QStkVSM-G82duLxRsjub-RLaHGgIAjYfC5LZOCIeNsXBYJOOdi7EhPJ4X4p3yg5FK7Qz68N5Crs5bNVm9akINfHymWVQBuWbv_zBk0THdIwxAucxsOf0LeeBfEJCRG0hoBUGzJVsU4A_6rMn6TChS5n8efdrgUV1zNM1_-ofZOao0VzGY9PwCX7Wwg3UTzJKAeHtvpulXYXZs8o-6X3wUhnX602ACQaYAGFJI2TejTKU0NINQSNjj2DV8pxGz0rVoYDyxdlG3TIfPXax3wQ0x6b67gKRb-i_uz-5wTqYCRTq1LLHZaX3pbsmNR81DkWKxdNylvDS9pIuAWrut7ENj_OvFGhIo7yGYbujE1OJki3CSpqP2wMCa0lJuAIlt7SREHDGKm8T8dt0y6zGd1V87pmlVNkmdV9slnAz4Lg24IP_uWyobSDhAtwkDzoSFZxguAj2rkMK66lTD167pYL-dxHsjlfWss4DB_JXdVh8dDfxwtwjkv4dxCqWyoRTq_LqnAERW4GGX2xPWwvQDbw8PTKQNkUG-8RydKAbk0P3O63gzkuhRlAm9AkJLb1nlKGPxw6uz_tBwPlr-ZvoRBD3uosg-rejj0LWUmIy_6OPvELmOhSok-vMBWNYm5EEQOqWDpPIPxKB3FpQKj5HMlDzOvZ54pSh_ojn6YPXdFZHi8kvuj38Tg7yUm9mK2F183gss368r9os-AYORqaWaOVaiLiKaTF_Epld8RW-nik36TweTaf0vMft3rirGk2ZMsgL6B_wWXIoq6zYxzqSEzKlKYolaFRXJUKr4QYmZUrZWIuZkdiMycvumWuPwkRUMJuI0kuFWqZMEYYKKT2cvHTVih90_XuV9lnQHRz6wDsrks6aFPVWtt1sV1rMBarifY4RBsOgFX-yD_sg03okhCeU8_bCG9VgLvWz8G86r1WWVfgh5yxqMh8S-TrE8p74wxUQI3XJol0ELQSB-UaJ2vlfoat6yMGk69EllN0gs22Yjm69xffyJwJTn2S8ifh871mNzYqVEDH_rpfD_7zZF-JZnTnJZwyyMkL70EZjeb55P6Rf5XCVlOiR_hYy6jqM11ksYUoc13oZf95NGy7382khIOisKzGpQISfcB3Wpi4mmQGoMy_Dyo5r1b14lT86MLTVT5RlThTvLEMpiVwv2PnCqcwmkVkq-RVlBG3v5T4Q4Ykl5DuN_Jj8ERiRWtaHdJOqBHIzaUp1PluJW5RFsdK47kI_ehe5XFMV-8LNzQAQRFyTKQI-QaJfCafzSKJzzjTFgsrvRSZ16WpP0srm0xyJ8xLFuF2MAEBq7UMWRdjcrj7lqxuqRWz70p3aZ7ZWCxRgPBU-nXS1FsfDffkUY58cTm6wbchEgYCVqs_yh8NOvu56_ncVc_3V3PAHRr9tlZmM0guR2Bp9jrVScZP9in_GZBDXbyUZkXl2q12LDdWUF7oDUi9R2URfHFeX48hTbwUE87cA7uAvn-y3o7GmQ4xdjnoa6UDRGpShHCFbBGHaFk7rWnqfZvhQZGJI7NXlMfcZsqziPYg0LTgUEu40GSvFsVFs5-I7vfDiiOVwIGbfB6kI3DUsVBXetHT2JkdrIB1-8N4gJ8NdgU7CZ7Rq37-YVC_zcE_rcmqWvuc36yvmCG-JzA_CeGzhvoP21f1NllqbNcgf4qsrR8MpXDOJX9UIWcxj58Y-xmaww-jwwYeCJDf28Opg06FrIUijHy9qY_c2jQkGriEOehltyJZjljdKf3yX3i_0IDtCEKuakzMo-dJGuqZ5J0gcsxX9sdNdas3wRfZrUwBCP_jS5eh2rVjPwSZbZQ94Iej-3c6TK-yzX2Dt1DTY5W6aXIKTh7P7g2b6JASEMe5au-uWiMaYrK1Sd2dLVyqpG6STTvOerWI-VeboLNIhWSQ6zP2ysu1k6AtMgxnaUIl2Ld6ZIlZPdkKASAPu8RETsXfKIxGkeVYIXFgbR-muCh5oehRvCRf_YXbv1yoFp5zCqhxs_QpOlu-sB_GJK7OwxDV44_QY0lNvYqNb81YbKN1SjZ8gn-5NoZGov8uU8NlO9B46Vh03oSRrjXEaSh5Rg2euNCRoboaGKfPh_ayC3KHQoateEc25g9iIDHTkZPNx6pxdz8qgoSKQMgHAdvDMaNFGeSTiCc4shr9aw_6vVyTLoXo-5k_mxrFJ33PN6y2LeNKc1USjaI1TFb3mgnquCfDVxrMhHH6W24IWmXIXZX9TQddfjYS4ttZ3Kck0DsucyOVHMNmckU64YGiS8cT_RxomVJvXv0DGFaqFtbCa8iXigAXCA6XAeeMakvyzboCSu5SAlhuTIeu3MXjhG1-UjPKfC7D4owTACXVBhDXqrGgIuZwZqjn43OI6gZYp0JNaCfikZCrw2pz1pTI8Ud5FRNMDvcd_QaHjGr_g0DEgNjYrBmnlJ47QsEdSHk5_8SKx6HqoDJIEBXsyJ0b7YdGfqrfuY_j7mA4uO7oEPvi6WXAUUIYFvzWXY8HmKUTt7cnVG5KeczDcP5yJXwcNgRHxAcSa85-0sxgy1HhwtsJryR70HeQGoU3ecdG3f63qVZpmupgEHtwJ_2Vu6P6sgHR2N_4kOtYZWcUImBo6snuLBizdoz-r94nPY53JXU5zxaUXvmqso_wyEIwRQ0X4iPC9WIj-LUxfPpZtStS1Ogpwool2vYK-F669nbw0-s6YJErCrOV9UGzpulaeDiUK6BC1TYgMF2KCnHN2dRl6HK7xMg6K-EX5QZqkUYnnpEPte-tfpka8xtoRsjMvyTkWzozJ1LGxhNeArmXqTY5hCiBRu9e6wMlgz9RYenmCrPxaOKBvDxIkGeavjKYqMRpk4hljoFbFb-DEfYUnhjWzPNRe1ZbvAgHby2eokZEeX9IwyKxTIhjJFQe_ufaN5-irunhE7QAAqMQ6cuQSP_QAj9CdYUXIz7cqiURAjQXkllpTNFG7V7WNXrNlO3vf6deQCJDkiCnJ70O6RNo36h8MlqUj0AFmc7nirC_CWEPP-PPbph5FzSD94ejGHljvRUSjRXtggu9BMv17fq2jDPsCF-xDnb0_e92cd-QnFB9O-IYEIRsFMDS58DnBf4pmQv-1l6WBv_f2rVOaD5nvE-k8vTsTfAP6dci53nJLkOYZCyCxGzixfwASTlorDdBoBqE_bUH4ECDEoZKKhsEJfc_7NXr_b8FQim4oyOCXQfGlfdeFqKoL83SAdraDWh468nlfPF6rDYX25l6z61hPAYx9l4r-6sm0RiwGKd7MjtttdbuTpf06OoWZR5B9LgOFLDb3pwed_jJDtf7XWWo5cslYs3Q1M2rkCpgMrudaBcH1aIpCWUnDLbkAmKwnpvj6bpfRYGxOqATtFf1xpcoPS_4JB19bLjdjbXMnDcoe1wgrQZ0bWxzBy6bviRMyFp6C701hJ0NRHAcGiIGlSk5z5uBxOXjQen-OUM-nDlpplFO_bv6WWep8sCHW_VPSzEbRNlSAYuRI3oTBMmzIF7m-QI6eO_r4NaAH-L5KJth4lK9dfOluKZGFI1tFuXK0ZAUmXUz3oT75wtt-_0KQxJhkQkGdoQgO3otlFLPVW_p1aMWFzn9Iif1-Bt1WCbIfv44eujh2AN0JUnrsJSdE-3Kpg54SKwKYghmT4nAOewvetpPfe7BuVAEsKJ85zVoctzNqg7Ap-AUQF5h3yskwyCq39eAVhNkQ2mcfRsEjBbtlethMW99OYlVwsOxah7lQJSJIBxBxJcwCTtC3ivENfsGG8pmZGqma9Wv7bogGQVpZyRbW9ilAljPBBrCIU6glbhUNxfJmjGNM1X6gbVpC7I7n8kgYp7Lqih3UCqnTQMVaRtI9jxM5fLtN3ARRvRLL-hPisniopKwumy0pRGIB0_lupkCTLit-83p7KJxggpjE55lVab-KghlaOb_sKbKdaGqEI3oWpMmo-8CmOoXpo6Ob402iZsp6Bx49eJ56YpPjHhHtWieiwj3bBL44q5g7gW14HMx7IYZRHUEUvIc_7gPI6OrjkEiAVdhdM8S5tyINnGhzgKa8_YIdMbEYaU2I8ape7s_RchcPF1_umcHy_ifWWr9EflI7yS8bwCj75pcYKL5NIb7cnxz3pNAWH8IUoayCkyDowQIxF0Qs26QB82LdJIiRLEBO8Z4YV7bevWMJg4vzjBAxaA-_hHqoT64du3l9AIaipsYAUAK8UIY6CRTD8gm46-tSPhUZJaBdDRricf3qSj28W-435y-zad8mqjO4EM2cgCFoyLwVpHqErUTKunInfpjZXCQ62nU0-boT2teOJVYckBvVajInAFdUnknUCrX9HhjPrLuW9vH1aRy4OeihYDkgOFziKw8J5v9QpvtxMfG6cGfr__SdILuhY-TIaE4eIRIYNHPkeroU34Xir8xqFIIR1EaG3p-Foi_vnuH9g51M8difQTyqFUdYd4qVS1NkhF50yswSzFhYSOpUkiSxDYx9f1s4DBacaLJn-nRCalFKxxnVMwI0L8lkhU3nzGiphUHG07eNsSd94Z6d85kX37HmDsSBmb4liIDXa6NOvnrUCVRL3F_OFgqPt1O-lfSgJD1yCeemyLChNKe8g4--rr9AHEy5cjUnZ-Yd774z8eok1xnFeJiIsYRwMkJgqUhid9RUBSFGqqANoLYN4RHpLQwfgj2-eJP3Wp_MSjk3uYCpoWyCYiAfU66VbiIJyP7J6062wCdZg4HH0ebRjvbnHpYkrAtKAhR6u2qIRT5rWNlUFpuo1HjoCbNlsLl3aBIPlXK3zPEoJPtSW_gQQ7Pv4gbC0QjjB0o3I3XM7NQMhl4aLFF4zTDcG0bGbb9A9P2Vq8SLELDgF2n2jKN-ue9BoHy-QSXOeM3pOEtlQtHCgn4BosgPwf-TVDkvvI3qA7-hJC-ZbPhGwkRj7XfmhyXhnod6zCTe8xr9m_itXbMEgMvHIlXnE6dWDEOKNbAwp-MV8rh3eThyj6PGKoqDSVmTqFlAzJoTMq2ubxktdJ-7jMX9Uxba2R7aT92Ijz-sgt6W6OggTUoeIA4Wwc9tUN0D40kOneGhVo4t8lPS778WG6ZOtepuT2tNwmKrJ8FbPuW25fcCmOBMFMB98Dx2CQzVuaMABy5VnUPGAx6cbG49MoTxtoD9oH4MfCsaIfZqsZ4epwEzHTjW-mJTk0DDziICXI1ljxIro6175e7NGsLBDkELU97g55ouOGiKrmju0T7HWURVwpHHnrOCvnrb-iJv0MX1BvxFrMkxgE1e8SQqbwsW3giM8coXeN0h1j291vOUc9yGHO0CzkQ_nnW_fCqqH0PfNXuQzktH7Au0uH2HNUucUS9FdLRhVK862b744mhKtd_K8BprWHWatKRA9-CMMTTs7QShEbVowO8S45GPNrYA7v2tNIq9r_Mh5gr0yqjpwnvoavt0apyl3vipq6t57cLGz8ShlOH9Qy2Br93hi5nn1jsICMB3Olx-PFsHutC4woDk519bZKG36DfaRZsMAXK4FlpEM_Y-TCBeUV1w3LZakbpC7SJSOo2D0Rq-sGdl-OhxFonUWcb0CqwIu13KF64FD-GTYloX1PbsBI4ro3x1dKLqoQ2IsiwyDPDGHCSrRHSqqxgexAaFLBq-lKeUm0nRmcldct3HrJ-agNRJnYqOMD7fLu02wU9EQ3Di6YvAEfbsFNF_fnPVeq7CbIQxBxMqBwGY-tmZ7V3_lcuNr5KARTNsCC21rXn7G8X-lr-9zAGbUelHCQ0ifvCzcMHO7c-2LvGnkQ6L1lmufUdbBTY4zHjpQACim06moCqjfR__gepEJoVvAvo_B6RrpSNL0nFNctjZ30GA2k-gUJlkdMHYSbiCTiMtcuKnhqeya8MARHVtfwrkR_OiKLE2-0Ci6vyxIxYbxOW-ap8W5Bfk_8ygbc6UhAyaMCDawXQkBgjXHGe3TEbbqWgsQjCkT_MrUTtndiQv7yRAm2FFdNOjG4b4MyE1Hklo5FFrFxTleDKXDC4GZ939-vTHbK4aSUEw16kqvyu3DsBcvNZRPWAkzh5FAz-56-fRSRAim-xTwyFHIFBjYNlcssSO9btj4BagWi-lh_BMHN04o9-zLFtB-CG6tL_yk931XLiHICYVmY30AQNYsJnLheSuzpFszs-LWhAqxhiWLdpPCJQmmeAUga4hHjciN2HEteXCdaQ2L9c5KMSu6ue37Den4QzcySPzOZ7v8YiBpDtVpAd2cZOIlYWZ4f5LDVQnyWJ_W2WcU3h7U3ofYa4rl8G0z1jAyzjh5Ly-poKhF6rM6Wtj9SZdFG6GKzJyPmgI3EnT5SU3C_bqhJx0ah9TGnz5hhdGoIqQwmFu63XmOwY-sezMk9AX6-t-vdsZ_uw8a7GAwZHKRvfee-Vjx-QqbZbU1w7ZCWy2zw1CScw9vbsN8mnFz-F7ne1rLgZYdjTfdoMCT9ucnu6cR-1LxikY3VUpR_pWClWWLJFdJwTysr-ybowevDweIpigvwLEMkt1yNwIj9EHUDlXE4B7jjzGfhA3IXxPg6Eclo--WdFjosC_8qn5LydaKPSf37yKYWDE0iVAVJEc9odozGr50ZGgEfVFn6IjfpgJgmklMTNYXWKLck_CFsio_Eezd_dVFqrY_3xqqBlVXLi_tDLLFCF26DDR05P3XUdVzgeToYnVd1PRLNUjnNdqq0s6LTBdeTX3XoG4QPwqMiMGu4Wk3wOlwhtvpQINzd3V7FofMZo21semW9RRKavUWzhgF4Z0PuE87BG7wREWUrICqTkeHdxxIyaaLwnQ_7u5ry5gaz3EOdme1tZnpxpfmVUcAtqaVWfGx3iSiBLB_mv8dxAbEpmjL2CIE-0AO3QJikdlw7uhuoJtTbwFyUiaUw_RfuD7wWNlmamlEwrHsBClAYZnaK28DrjmrdRkxgOJgzX5tI60ZVnJJEsCdfWOB9gA8nSURlozjGimUdN0mxRFz2Jxk3HpD1wun3ZW4vcp-fYUPXh-ClIWeRuoK0Q3_vV_qNMttAxCSNo0M3pNR5lwpE-XqgbLqzj1iDwH0gmT68m9aae8fsLxkjvOPRhFHooT8y4YXqBV_H13F7RyIy0iRX4l-qoSs8Hoj4odF-G3PWPPEGkfQqLykcmFPlsQMAbvIzF8ovhlgGwSqs6aBtf4y2g4J8QL5_jKBEy9zDxAbIDViWA0yxDtiIUiVBPtGoLWI8q9j8Hg9-dbfRXAzkKBkMFgaWqprN__Itc-uPMyVmyPDycJkKpEgKxwUmo-zuVpnwjMsh2ZHCugptrkvw0_MPOixVOLlveIuz3oqUns9Ve26QyS09NXuiDUCVndLFHzXlbs3omkgQa-FbBZL03xEMD7z-mtQYtB8A-cQZl0Hs-kbiBPB4H_T_4wyIQLPCWQke76ZdoZEe96Ncs0_hsS1L-aMxN7W3GyE6Ije2BQiCcYJkBoYDcm2AaaDel5VU5bFcxe-wrcFPbSIcOjWY_L3G3_XQ3KBM42jSOlIuYUHJ6DNpCXexOrHw9K3ESuvqoVJm2fnfEKuigfB7dPOHxXCijiDqxElvnBl10uK2Z269FdwjKXu0Hn6tHY4Tfy_xkOgn9nrB2m6RE2W-y4rHwGBh0HohTo7R_EtjaXC-6-D5VlIjTFbgZpC6q_vEVyiNhlkjpVY_CYX0-ZG3Abr8HvMMP40cG63GvQLDSFiW3aol340zkN44-oLJaWKHiA93ce8gzMXPRULcj6YCn0eBMa8Wg1Xqt4sbM1PRXVs8-hofr04dvLS4asHs9N06S9yoj41cCFOnAvTICzmRtXmCcwHKrIEmai2jyKnbNExwenDxAtYvE9E3sbbJvw9lx3kXxgXccnXz9berMIuE6heNLdFyuEMo42VM17OCMw_n9xnes-yIW2IVUXv4Pf2gBoMNiIbWrdUC8nta4yLyQRap_OQ2D7O6wnqdg7J3igxwWM8spmea5zyqeHph9g4rpMUGqKShSGryelE4RvgFwRJeQSq6ccgixsf252_tZqIjYqllHO6gDddYPf5-6-ykmLdagw7Jkw87BhE3r_Gfx_m9dhk4D7V7pbPT9fUnO9jrUbUqgp-Q5Wm7FRVhD0SvwQX3bgOJRgcqI_YAxHoPg22CX1zSKr_nWNfPwbhIPGYanOVoePT1N3zhygLCYvsXvAtyHBjgIM2LyXDbwMa2VNUBnfILtntuToZYh7zagT8Bnh61qAc8glqBvVK-7SUhU0yfNoeFlGW6SCCBoktbZ5NwxYZx14YWcdQ9biWYcAKa6lqCuz9SzMReMTB-pmt_2ESRifYTuU-ccJFjshG47tF5Wf6jTWzt4BHMeIn8wSQS96SYj4KyldPeT45umMIAtfIIBl7cn80mhieXjunlDd1HTcbxZRaAg8dF4aS6ClxSg4jik9tzCGV5_yrIiXdxiT8TimiKh1jyEoaahSq-9bTt6nIAdvzwNpC1gdS7lp0kT83u9AX13L-8HdInua4r1PCdvATILbBlReBs-X2m1w_ilIRbs8OiCVC6nuiJxDzfD5TTrdR9S6rOIT33CT-gZ_eY-rU4_oUcbvO2ECF53D_op0iGXOqHuGeHsda_QZksM_2LKpFSVK2xKf_GS2VvZ5BICHblwnGOm2N6huKum1bfktwNBdBQE2_m-YUKVSU2PQNQnuCRmQdcw8aL6SiF0TgDIwXF8nUVig-g0GCbwe1Of9W3vpI5PGsbQge8fLTLtXw3y4LK041-xLbFmuseP0AIuKwXl5REzEB_u38dsgBo38PBMmEjL3nmKvwx32jBtw0XIiVyVBBni6j2umU03s60gm40GvTk0Tsbj4wWGRr2_ir36sR4a-1JQcMwLxXcp2bOUOOwDCL26O_D2LWEgMsMZOzD3IguYnotTLN-JdqtIGeaZvhA7c9PGgLQV7XNtMA6ySpF3hc_1vJnrhjhp1QE7JOJD8dgPoEgWEFv2xJjwwD2SYU_YZL7rFx6wRINasosQU-6Qcmks2fbCS53KQWAFS-VyrSnVc0pk-uTTURY7HnkNV5DHq2qQOUMR7kKtmerFNOOmLx5nWasCoYU6f3tvsMGdK4CPzy60moDM_FgfZzQ5QU0aMjCjdnFkYurSSZ8ddEDUR3zYrOfpMJNFUcZGlJy3XMdKmBZdlnR9jjzbCrSSqFv_CDaDDDj3XemueL9FymAanKhrRMb2_41ESQp_eYTjNVhf9xo69UapCext5Zc2tVpfxJEsMlQ37Yr9Rc-jYamjW8abvYAFfa36NMsHlPH9el7-XVEP7TQFc5mSBZF1zmS-B7hntP4syldSVGLfsDPoWE_HSv5mICUEzmQAD5mZG6AvOvXtZtlwaNS7TWejh4Q2ZIYi0NFrIjYIGrU9KZqVyYPEqCCXs8NGyI5VbhsogyTl4rbWgpAFJjMCPACDRNCQpH1hY9ynTkz6vKkvJUaSNABlRaGRK9J8S9pfHOZ4PZwo022pQcSSA_Xo_qwzrJPl3emTwYb76xP3T7toW1PXBhi5L1M_STEq2lNpwY0IpMInw6RikVvCx50sQEMR6YlxE5WAfPNkJ81fwcIwbgmgOS0UZv3Rr5sQCJBIkeD6FwcLDr3cF1AQmfFPk51O6_hlbwoLtLIQO8T6sX7uvTqBJuodEOIcMdSguOzFFHzrnlIkQavgA5-X2IUWMZ1tsf6HbIOcMx2Po61rGqiypWwxQXyDwoR4vZ-s7JxeM7_TSnTOniyQjWIp8pRfyYLevmu1HCc4bk13YrtuXYEVTWalF0wUb00AN5UjrGLWyBzrVVE5Xfqt8RKHFinRHJoTw1HxyHoG3IP14RhgwmtG4tBDhJ3moqjYSMXDBhPJ3OhU-lRTZNcjPHfRX5vSKcE2KDje_LXIS37mH2o7Bg2qFSeVAuaCO4bxstJZurDhG9-ggavLExpMCUlKndblXQzGgzEAuzHfhT0VpfiCCMTyQJN0rOTZthvSb45i6b7Mrto1ZsYdSphXlDlz54g6e0QP4iR-JFg63_Say0_4EN7tKAxbUON3_zdFORuync1J_0XIx_tbhueLOCIkyOdejhUe1sq7opWd-92dxZc5b-UgEjhBlz50oO6vCpwH8ck0LvbkRsB6Poryu9o0fEm8Eex4jFcZwpLJGnFtavaPymYahJOF2oWffJ6WfmSJbgdGmA97bmr7sfefP2wJ1f6wlQ4CzcVpBjrJ6yt19XPL0zwQj9jyqcAd7KXe2VK-sfvGfLn8A_73ExGBFr_LEncMTK4aJRxeToWdNEYMYamsyychWqgxrSXulKJKYR7K237xBTXV5Vf0R2FyYY-soAoIbBm-Hu_dvOAdZ3O1ZzvwFW0QgAWyzPoi15OIBW1ZmWNKoA7aAPYDPFzZ0x1KzaiclaPdq4906SA-F4Xzbo9q4DK3F0uDvHJgI3dW3ONOHZHaMZo1_EpNA6wQ8h0j1ZYdf3doDuN1xHcskMvgfR38_U0vL_c1FB2b-oouuz70pNfV9aZ804jYDE5-9FAtfJz8S_9gZ7GqqegOqTANuJvcnVk8SBaEQjgSf80Sw7ccfeEuUa15gp0RiAg90dsdNTmHVSjMZo2U4eLUmjbKs4I-MgT46J6HW2nVYOki_7eGJ0RjXjcHPgvlQlW4RFb22TyY2xpjk1UvYOoi94XUtGU3pPWp0CLX6oMaThpTL6MGjuIPZpT5j5UCAA1TU36N3RtF6X83PGTuoz4p3aCb1WjfVFu1gX0c3JGqlL075dnPoynJ89BnB_1Pj0N9BVbjvI6_SGW8ewnAFIKWUsm3fFbzRzkGAnrGyBuBZ8PRwYPlx-ovnaJThwQP15l88zOf4EmY2saCk12QJba4ySBAyaWfv_8em8CvXG89EumPllFBy7HvqR5W9l2v6YOLJb7AHa265tDgrSk2sBqqxSwQYOR3VsVq6Mc9RuPHyI2OXad6xwwYf7S2HrbT1nxeVcevz-gC6Q-_FtqrxKIcNWPJuflx5lNHVCwf7umbiFZCqa5j8iDYCi6O3C87P-CtMbt7e_-HFrpH7t6UeAqpebR-DrCE0G0yISs-gms2EzamUXi7MgPVpanaCoyt65HdqX369WansuUt1Loxex-ohJNE0pd7ZKX0Xzr4JLU_OEou-uD_p6vhg4VEFcZAcxZnuj6zHBSCYJvM6eJWKum93o0eQqUfg4gcs08FBjW1U6cwB4c18cee-DfAy_Zexb0eRWPKz258o_s--HmIhNb8GIR4lilwcoq_aKWbUP0Pk1KRLyUpLoSKgO6oCaEhAyWf0XUlW0NphFqwi1ZzRZfcR2vSpSHAcjleApAwHzfjpurwqRrsdQnVWsP4yDGpysaCVrJupfyEZPTM3zSMHDLLR_AeWHJpyK6ipRZVJtByRXi0hjSgbiElfndb8mSUeIYFJyFRkMBbe59RtokoytcYHqeLzzvMX1y9GrxrgCpYxZJ8JGEe4ZGP8CveiU7ZVmlWpc0bTBYr2-E_Yy5IzBKFakWF8b9K77mOo-aEGVcjkbSRNXpumM1HoSvUg5lZ2vaYcyBzf9T2bRNZfrkmkvzz7OmOIkRkChHchwUeCKcnH46P4sVtRa_MxLv3g_rvPEaGOk-n1V6if9r7T4nJ1KGGV65NrUO7NMEvc4NyrPmC4gE5zIvvmk9NzFZ1BkO88SqRDtEZ1a-rjoR5N_wiS8z6__fZLljxL21gKhuQ5nWBap0LjbFWrEl1xF2LjtOi-c0UnE2y75yNctNPvsfkyXuEF3wzDijnlPPV7r9Qn6riXCqH4NNfb6f3lunLvmJr2nirNU8BdwDA5I1-as7UgGXvvFwZivmaxGCUCOt4FMio6GaioxsYhYFfFSRcfmJajANtuVmBAICHhGjTVmhzpD4ic0lTmeBv2wY0hAzQ75Ss1RLatRo9gKw21scUgjhEssXhb2p7cfqxObixssFRFnUA2EIOnKUBHkRmf7YUZhF2EdzqTEnJC6_NqMLOegHpXt9z1l-lL4KFnm8G5TJyv9zAN0hEZrB81MZ1gE1e7nLMV9D4EXYr98eyijEKuZtrfpUU8FuP9ub2ubMBBuuEyjh_nMGq_0iC-hcz3Tnh4ynzqcfuCwUsb9V2I9mLninsmImrz8Lr5-bM0mDm-4Eb1z-xaRTrYvvtlpE6C6xafHL9lV0cE572MxtFW8OrfgCyGbdJS-s3aRDY-a4yvARgG6OFuod7zOHmbLZX4-reWeuOxZ40qjHnAne6Nx2xL8dPDoO7S03sYMPfA3XrT3EETo2g4ZzIcHnwFv1Tp9xWI7Sv0IDUYPNXVOzoFX8Wdr3Z6-YVUpJTaXgS4oaFBJJYQwu09DoUR00Q7H8F_SsRRCMaECTd_OH1Xn9tDu_XJ6X-1ajn74W-OFSmf1wqoQAtH3CwYLOvRok6H-_NDgAliUt9heb8hcLR5Arg7BAA0kBcpnq0xufy2dLZ3GQhf7EhR3QprszOGArchNCn3hFn_-fHEnk6FQnIX_jCOycXDitVi9vPGUgIViMw5D3uaCbd6lBNFty1PquRGTq1wgJ9pCDRv1GdO2cougJbR5H77i_MEoji_wQRQfL4vXUJjWKOm3r-eaJVsvyx9K6LBHhJOA85l8noSH3USYpZNjXNmM4PhdG0zY3NeYZseTcM8AJR2veVpVD-2c1ZfNL3kmRMqPvKc-_RqkOG9B8_jJ14J47cC5036SPbpdNSNuX3JqDAL4XjJ_OTSJhNCakaR3tZLOWvf1zmL7XqOgLfo0QHRUboRgjmMTMexszSSUC_B9mYHgG1jq-eijRhtBVxaWHMhmYipi59qGRdq0wNjm9IaGjIuWbNKZsQnIqVC6pwCLTQIUyXfJjj8q4sd5uuOcuPwX0ANV6lJl7nTTtM-npLuTqrB0D_H453HLl5dqmIDssgqAceLLhRl-Zs_PWvE-Sr87oAXeZc3RDSZkYpK9DMK0hWJuDjjQoV9iOg8FxY3Nq6HBPqylUkPv7Vd8vZM838O9xkc72mW-uvJwEbGaSRw9g-PaW6lvwKTSX-oJL_PggmUSnKBANZp_JPcQfHVhqhtUydkCYrAy3Li3pp0h9jPUomY7K9VgzUnjP4e3Kss5cRHtzy4450xXLZcBZWrimUOga5vUQXlqslg2wmD04QPEgHQ-7kD1pxeF9RzkoAL9IEzBTi2vJMgzTpCg1qrHprEZsfnnCKsEeeS9Tzxd2j28QUqrEW__1ICsFLoLckRZXa7-QwmsOtH1FotceS7h4gM7cnxwZg-HbhwXUcrVwfXCdDMo8rerBagpuERsHFlKxe3TKQoKWZaT-cox75K5H56-UoUBe3NCN3hf9NzEK2h180WT63vIGuVWz2n5ZH44Tfuoa_VupdN-UZY2fFOGzBjsnO54dBb2T12ZudrfaMlTSaoHX-6gCscxEhJFDBQ4JwUWLQABEqAt06m9yuP8f-ltogEcXPD2Iic1hdJeNFeLwslIvrll_TxxOQgc0kcX0feHFOZXpEiHNJW6M5r6P5JESnBgHoNvDcvojwCgD2Po2zprJ0XRAQqepyTGcY4EEFS1B8pb1TxXz45wBlJ0YIbkxT-XkIWFeCxc8yvo7RWsOcCUfKPVcEs2D9vr_OgQ7ASCllwZdOaCDsXayIRmmOfzkzf1ssmrLWhEDLsXn3prVXZtfZPmuOu4Y07EakPQH5XqBEgnrQT7TggS6p1J-dxebzOhVcUSC5sxzqQ96tLtnFrPhnm_UwWgSvnoTbagoOV-mArej5rFJggzcOMQpUJtKn-Z8O_Kr_Unc7AmzzJ8BA9hfv1ksUdQsc9bO9paasOxci7CJr-TlmsBQeXB_gIq8E_l-AI3Nz4keTZgAdM6kGeSK1ggaenGcdJni-nQodP6dLwT78DFsshyW1MS4Fl5dRdk4caBp8FZ9nhPrWgI4kAz6eVsOXGHar84PtBtfZIB52ex7oUtpca1nHMo-M_alGf7W1i0e_iApOlSvjJ__TANb2FY0gmbgg8JYcGVigMw5E-zJ4WIOdVhp26rgAzyEGFCQ5VcW0aK3lPtA7iGBVNHd4gFaWpUnbXTh0_6MBR4vQ5w9xfEEyDd2bXGMj9eQWRuhHX_aN-uln9yTBWa0N9cMtT2ucP2YSnyoOyV_JV5WQRTq9QxDFL6hxE5_sg3Z90sy8Fwbxi6jAoyGC4Vm7_ajSXBup4s1qpUabHkXsR1UizyxkslshvoptvVIRZIy6BrOEBoGSvK5iv-dU-In1hwMwBk9vaxkpAmd2hypCDET4IhllNLNKgH5GEDg9pL9rG5WJIiPIaemX-pROSnO_9tEb0cz7606VRDQzPdjerbYWjLoiOH_cudws9Oa47Frbuk2uARX0zLGgtX5N5myy_tewcx6ji4YVLESqKGNwDa5oS6JIXbruji7MmXROr16SJuLO4kZ5U4EOZYDLrcXgGwf6lZCWBMmHCKyuEpFgA2jf69Fjx0uZ3CV6QY1Yo_J3r7HAZklcFSmWw3EPMg_y09zBzqFhPHy0aFyKe43XuO26j-uouRra0pR6wXTJL8wX4xio7rvw08judqy52zdyLfROQXIdlFetFVOD3XlpC_JVx1-6Ef5aYJ9VdCLN9Kl4agGHs2V2NSPNPcT8iZakMJXA5MLpPc4HU3EyFl9dia9juAfgmn7WOitehGql-WqeU-MhuqLGh0NR8EpOvctTwz9LjDNvL672dnK5p-k2aTUo0rVA8QnI15Q_cWA2S1-a5hj3sF92yhgqirr6HAqkSLjzksdzkThOJT7e1feJAE13BpBXV3GpHxZ2LljhaM_zTrDLNNaRtIf-jncJcHMCOMiiASSj0Y5otF2JM93ai5SzWCZzwWo7ifjq5bDymQrTaZx2IYPNMPC8-NSd-yNtnxIL86VF_PB0aoR1P7a8p1GAi29PN3bPxADyfm2ltOv0WUQclMo2RSsz4sCLe1bJ9HJPjVjPCBPOk1eyAvTWDBb8gQJhazA6aK0UORHCRPwTK56HxCTRdgAfvqZHTZlv1xWa3necdPA30cG7YdgLRR3eKWZK6uhtRuiav3GgaT1Kj1z1SEzEKT7M7IqzbAKRi5n3eP4P21Uadahi7HzLftseUqv0e4ZHoerorTgxh9mPImStJ-Rkfu3vA9H1Kzr9jl8HE4BhlNAf5WrhQhurlDV0nj92nt4FbEUwRz3n899EodWxF45nHL2c17QEwCuvH9SbIc24H6xe0BN5DdCVrAkQZz6_TWrrjWImSiavaTv1EWkpG28tik4ROmVtPaeFpd3dzxpbfZwjiBPbun0kjOUk6lOGcWkFEH5A3GH1ZmfK5XiB0IQnj3L3QPa60zZT73l2aj1Z45LB5_iU55w6YEXh_3EmGblnoUbtF-9ixFQBBpJqI38aZQFgFAA_6nKZSQ_6b_kGPmUk8pLsmk_xwGKuZJrwB3XU1rAfXnF5bhPypEKEYaGE4T60NYafISIxEGEGsex--BrOGyjvn_hRFSBf_3UsLVFcng2nD_ZrwD2QMoL9MuITqCbaaiqnpqhHJzW4rr6BYZ6r2zH1SPPH_m6UxvZObBhEYRvzpAcFwmug6oFNxLl19qEatw3c5aL8bQvPJd1CT0JDB4QbZ4NZAEdPyM2MbwUt9Ka8eYooh4hn8mn_Y5PdXQQmeCi22Kt3XgulIm7LMKFj0YkYttJvWkzoMzt12PYQU3kDetTfmrPu0ldzVJh8tI4hUeU2b11cywqOowD3pxZFC0FjeUZcprS9m3699NRi6ioCTR2q_d4f7JmWHuEGkg-sGNhnezsIX5KLG2v4pCv2FKXKeETYSG_4jDeyL-qlqB7nhKY2XQ3ByJ5iexhyi0kDPKGXNx15Y3abNahl_08GWSN6uwr44FDftHhUVQrId9yt17PgzTTY6UyWkFx7-i2RZ9F1EaYvNE5bKbWWG9sU1m3NEQMJWMkHoCk922vOT213aXD9MKWtHbmIFApcVbnWvwXyr9XWkG7-0bZp_ZQzWUe-YA8c6ZANrXdGitilsuPlrA-ropRkA78FO1Yw3yIutpgajshFtjYb9a5vsWWBfSri2KiZJlezT-VbKev3mhlcSPnwIL5vHD8Yf-wxkca_OBaCoe7-93LviwS4jVoqLEYEUe9-aatDRPe7Dk6dE7nO5cadaXO-seyfREDoUJeH3GrjDDt8dcOlvDHY_XDyRaDnPADuO0P-WSrDFgEO7qTC1fIueY8MkfuWH1OYpVRsT6zQX_e-fCej51tAaRRqoLsw-k-yI51BjFzU1mGx3PmiJCzc0L9eRp5ZN92F9oL1TPAutDiHFTNGZVrbkQFZkTBbxX0kfmU7ISgN0QuuET0dckk3Go6ADmwYWEF-fvfSiRsbUtnQVOd4QNjQA8Ua76Lcda8I_z0rz3ZTlEyQLOuz325pwT4bmilqE0n0hCH8UTZNYyajq1H26_NwtyVPh1VfHOsSrvX6GMH_rp8eONPrJboajJX6iFz1STw2lwTtQbYxM6NqWzh7fR4pWTdmM2llBZWk7InUy3lxsk9zeuhLKo7A73MT_SQG6cn50juMtR1Zbv7ydcOrTsWZSJ_MU2jJkDLwJ1ut5kVChGus84Ba5rNN2MxPFIpHy9F8xJBWcWo4AqDNLsgq3MqqKZrTuVHHVIS4fEX-4L-ThjfDMDGH--PAVF-d23tnecA5BhpRVc0Dl4ho_BJmTmlsP4RunSvbWbyVEEKCVb4Acr4NSWJY0r4RUJ0yagdiO1jPVCeS_Px0UAB78zWNlSW6vAZIMn45lrBntweiIJr0CMAuscXDoXgHy-TBq2AbakN_U0XHMccBhlXLIvi6hwjtAhEqbrJks2KPzraPEvRPOJ6FzzFFkEIc6Hf4k5aMFf0z4hPgvQsVwrEncKu1H-IXAB3wKe2NYT_meGkV8qyyMT-b9BhW6QN8RskA2BqlU4kmgsqIJVk_rCYIVPnMqxpBf2YquHAbBURnQWubnYfDYlCCFlVZ4bUji5SoVaDdYoDO_Fxqn9amEV86dhUOiz7IhkgoJygrfQ5ohBT2uuXgU5PdgdEaOwjGIQC6lChI66Z_P-4ae9YC2HYnv4xgfcFP5u3q4US2FL-cZAeJNNaEaNmQpbZy6TiLaCESS5HwZ0qYMpDjCXJJzZxx_Y9Dg03dU17RvQ76HjMmjT5lsxfK40g8IiJTZPq5IqdNbUI6NFYdyGTKKWILHb57K4sarOcWBFbvoSPvo2SpWB4kn_1yL7-cwmFzdAign6VE2oKsjWJZYblf-VntMBNHiEfD06vyLUlKmgwz2Jc4IfCffZGVILcOZ2XaUpdCr0N-863VKc_TT3LA3obCGgrS8hmZoenXADDTNflm6-HpHfP5boHQUhEGcDUD5i1OvNCywPB_FLqZnK33gf_YV2fT8trAnVYAh6d377z6UThxhB9o3Sd8854pnAw80uI-lTZ9DZbvubDvxC2YqUpocZUUN9wcl_GY4uHupknrN4NKDn1FCogvX1TCWXYTFuIduOJ8LJkrnF5xQqhlX-NDFLN_zgJvxxJU_8egbY0fEsR5v6ybPgfnEte_yeVbNVfRCcQqOlwaD8vjkYbu0nuLgQPchMVS4AQZ-MytarToGPwPMXvXkGGL6hHMlhf5JzbWawa068dtLh9KQmBCnaX0kUneRqcM3G9PiRcl2-rzOAY3x33eYYiEQXHTBJ3o_vy5_9YV7l2DMlogv1UWnZaVOjBKJLAN4yNCzmWiWMWkxH_FfWoiIlk0AsV50Dtg41q_Q2mwXSyeCa2ZPsIQIoN8DWmlq3VSrI2wd5bIIFTcT-t3ZbST01ysemyGJX3rq7OlRbWMzHhJ81KUGiFNoOc1Xgyrs2Foj3aHycxtYTLsqtvfcnpau00YvQgE2Nhy5jDYfj0ZZjAciW3kGA_FzznheVGOomaM7sH6D6jHsjB0bjSl4pU7doulMAhPPXq36C_i4GN5nq-z9vjUkwyLIvAYMN0SRJlliPP5zZ72ElpZnxsDkGHKC5e6IRceHv_isXAllPY2eqlPzQtAKGEzBB8bUnVAE_agGq8U_XYwLKgJ46YpLF3Pte0GonSpN0qKeJEcGJlfmIEbxo-h8sca-f9d5H7XfXTPHk9CBrxglR3GhmPS6iEtNRZrCOl97BdChIiDWRuJb7scaDOVmbapeCBBw24F8rfp2p5RwgnVBOqSmntD6XIyfUrk1SIbr7-vexGZbtdD_mBAHmxYoUHez9uM_H64kRr1AWuXbr4omtLAQrHsSQsxujQoJkjxd6aG10CLHxfqejTNmmiEQe4sut9Oeo0fKPOzTDAuM47WTNBqc79PZCBvw8uk4hTaO6apttMtOT2TCrc3WgWPmYxelWb6LkcRyzKjqMCHDTkJXJAYgjBQe7nyrFzvchtsT0AtZgV8Ro_U8aa7AvSkbd3k2_HGbP3chg5n8jgzDzaPSbReO5Jt33h0PeXMr1OrA5NxC_kEwAUQd5buKXfPE8fMyTToyXAd1GDVlD1-yuuX3pja8WzUPBJv62fGsvGWWXrQijnuz8PUID1CAoO2ZLwls9jT4xqFfJvLSvwQwdG8R8w-htXsi-h1wS1tirmp54U0qWbaSVS2v514Hm9X7GN8hVW7vAEjiwuo_iVFN5HIOqo1C8BtGut5AVhHHdakmdbmOscC1McLwe7WnoAl9D7di8GToMNCSjtd9EgVh1Pd77e1vbQplB0wMQ5iXTuBTxdrL69Vn6e3sa8WLcaQGP3rE5mwtKcW9pDRx3dcefwxVjsduqaJWyTJnLNbTJG7DYdWXHgnHmsCkFlMXEQJ0sRTdqALZ2UPGgprDJqY00r2fD3jHXhoyGDpZGF5tu-ZJUwjOrG0YoeCl_6amTyhNL7s53LuJKQBnuJr35-qNXBy-QBZ4WTBDuqUo-8jxDRn7O3rpcmEpq0qCNRPvMEP2VMjAe4mwpEJB2XC8A8lUuiYZsQ2N22f8T5nqWDsaR1tt_ol99Qtg3BL2MwREPHgYRi_QIrcIKS52tjIAj4AOZq6QViblmRQXfx0g5BUTQNjoWNCFf7aDdKt67S1cFGRlM-ekS3sM90cNKKgEpWqB3PcbDMuWsGD8kZ9zq0ZW86GOUYq2b1KcljH8dwalJQw7-IygkLvefjPbXXtrTYGOCJBB7PORVrxpQ7espp0VPspQ9xF8kGJkC4Fuu5PF-mTkRqFuTlmRSX94kHphJjcHMgmhXmtY05bRHTAptujncyby9Rl5SI1qiyjH_5bM1GgPPxzDGeF7xBCNqEqWfxJyl_nvoBLHTGR0FRvOoQZ_rCPgE2U9s86AgJhZ2SN7ru4zlFhZh9TxoUF3fS9xicQGmoFyOhkOupPxy-Nt6qwUsDNhk61iIsx2dvASFbfVvu0AJgMft2TR59fnifoDXX3c9iZ3zVLqirYP4xB_rr8XcxiYWUvwyK3kTFgvFaT8d8VfhT31NWStK2WXzuah32Ek02Hn2sWISBqBtSxgfY0lWBVulUHBXqmbdDCgptjM-n85Nxp3-iXlAIk4_gBp4xluUf_Q0hkNuj0obur8unS-ZR60baWk3ESUCbpjmv2-eqr7LV2RSfjgcTt_2FTyMQTfn-y5aYhfbYQ0LQI45ZjJhOm4s35eJAtU8nD_2VtVdZ-IArCQF7LR1F4FVskXB8EJOB6Vaud9pt-bjUpvNBadf8k0jmfXZAogTP0I8YW8AfnspYE9anICD3p-n5_P4SPqCqkkl71veWVgLAhVeTT2wIHIjckVG92p6VUPiV8McRNycSK77r1Swr9Syw08ECkwCd39eG17Habod1iY6rRMXgnaBt9D8TY466oMkGPm1KsCJyuoOKaSuBOZ8rdYHd8QLcaaemuIePezQsKt1-uCk7PPT-eiuxi9wNhWl_lOS0aa_erEYgsI0S73nWuRRnj3TjmPp4MsTpF94QB_nP9JzlLeknAYcd2j8PLxELYjurcYrDHmGaSE3GOclWIGwRjw6f6SKhSucoQtDbdTBpAdpA17WckioCRrma4SzuOJIa7YD6q9LtfBDhPpVtIEcFX8eEd9xgx9sulU6wa9IyiF4Y3LALBerlS9nPE8G4H-uetKBDO1nO_ep2ZszviH8cUjXL4IJwfeq5EBY4Aq6l8m78b2xVkl_1ePeXMRnXFPpgyfzr_139xxqTwGBch9dYFgNRueZClDcCxVTrYhFhzQjQh_AjQNowb119VWF1bf-sQDQvZgMNrGR5lD9U9Of6cl3GJcdAL6CTLFbQDeqUivFJmqYm0OjUVIZKWqfuca1tf-EzLY5TZXhXeVFHZlswl0iIfIevCgimg5tVacq4ZLpX2pK3DskT7Nnr965Gs6joZ3_4NftRn0z85vLPUrrSSjt4RysMCSz3RN3RDi8mCPdCsVTR04q3Fs90Kb8uA0PFIKyCkwDsEKIFitDVaMz063jGJ82lYPTworlBeLBzyu9YigEaPgCrHjowf7cGRrABgyPwW6x3hVakhxfFuuGtm3Cmpne4nZzm9JOCsBk9QgmDZZjQ1Y6B0Q7JD5zKJgYSSbpI3vfUngcZ0iZXy4KgB9cT5-A9LxPJLMaK_abkMWBdcCNMrtIsXWXSmhKP7lebdNHQENPnCVQQeKQjj9pDh0IrAnav-JPNFA7wT5jkdg31-kzV_ur-JEMkbmJy8gOVbqwJRZg9otZqZZVoxf5wXOm9zzI7hbhBj37q73YsUc-xOd0q5F42UJoXcCO81uBTspLd_n-jLU_X3Uov38mIyHeaDT1OoIYswXrE7Ms-2HFo22_TD5TTc52GMDVTbt7EFckAThgaO_OW5hCQU7B2nXr7_KqkocultJUQY9p1MuN6YJAX1hmBtJ4s933uSz2ATj1dovgPrLDdntyDB5oFdmaDDQpuozQTweG3BFQXUIjzZX9Wn_PU1MX_5_A6rpCjrSyMS-CvUpSGhQ3IfbnQfrlpm4SqHyqpcPDFssGIcMWQm8sF5_4LDbTHNmEI3-S6JAZeio89mEYyuZ_msUweNLFZ_hEWi-t21r1i6w2ew2PAuaYohu3B7d0zbSVMYDGglmAbVTEQvlgje1zXYH6wTqNrQxCHwz28gj-mFTXP8bBBdnnh-BpOeRcn9IyD4hUVU6KzzkQDuapEPfi3EJgymUkE-RlceXB5ZShzRW7GmYHvXX03k0gyGaKF8ibycZN8brCRaefUMdtjp1WthunWhSqGyn9v8Pts-fULclgIFiMrkBU1A_nvsQA8_V2kJhtTMBgDEQLk6FBjeXnMb8x0Vaf1LMmxwkjugnsUaCppw5geMWAs_aIGQ_G3_3rHQDNFI1zFJmM_ZVEZ12_o-d070pONOoe201KqP5a5iot_OTTtb9yfTkzu5VSM3SQOVezprs-sVvf-lY7Rr7EmVC-JXTjh0KI6W7WGJP21OPTYeFRqvYzjY_lBMvPXdskqUor_M-nXVWGsZ8OU3gJu-Tftl2OxjlXeKcr3qTjGGRcjYpWhC84nDhcxvxXkGQJuzpL7045R1oCXw7ta8C5uZcrOTCZzb4suOCs5uHHtgkqd5SO9hhue2otqZQm-fHBVOpzN7kMlQ8EEpJfmzradyA7OAs1rQqm7__f5OYoJ4J0zE93KWR1MAEK42AhGVQv4fUGZ8vbA5dPgzGrDvvrXElXKwnzjgVx20BLdzX9ezRdHBrCkEn1t76WvY88I0JWM7Ucqxd32u33Oza9BFyzcrp6Js_ESrqAbJOC5jMAXYF7sVaS-i3R6CEy-hwDzzzHmybMOQmw0yD14tbP9_jdGRjKnq2Vi5tdpsXnTiaN18bHqaVlNWQs7QxUENcr_eE967qOz0xok9t00HApEKUroIyq0HBjL7u4FfdOUTgxJCwROBvrbFhuYN9FTQ-YCk22HShQML7Mj3tCRsS2GWb2c2GdwUl5-oMYnyAc25-PywB39uHZHFVYLpesYpkegn5cAQ8HLYIEp8Ocysk6rJO9wbJuK5UE-1BHKy6xUhd896xaKHsdv-cmB3bwesJA-Tq4R2Kf6SxqdzB2r96-DXLkyYo17iAScOK3kKLDcuuaiDBzD7EKepxLdvUTb50FOolu2trzdXzymcPwqvUKnEBk75T62juzwu2F5bI4uKeCGhZ2ycuPxmfDpmtzpWdrUWAQTAgsotMV3Lg4wx0hmDU7GxGDMXRHcchecuNzo8sgzDScQ8pAj4NeBGj6ctiH3cJRvAw012fbKOiZvn_MqAzWgYmr15a5MEXCn4YKGSk-EBG-2kVVPWszgyXwAHP311uYNuQ86Ts7oF_KuQT3PPN4hNHCgLE-p1prA8jPZMBzItDcD2CZf0exOy1DJSSx8Zd96elyB0AP7RGSpYeoEPxydylZje8WCEIESH-EU_28XwWr0q_kP6q6bxlN911_Dxn9o2V6Vxci3jgiEzuGGDflvyHO6fgGx60yw_lRUFW4Rr0rLwBd-NqQxy7rqZOISiH_VM6vq_bR_Gz1VyFqMMbqNXxwTfTtagnQACqa7bM0-AG9kz5J0hY8pbB2XK_cTvFvT-AG6VN0RazlgV7rpAAVD0N1TGCVN-v9mrVxGuqrmr_8fnQbA2vYkxXD0e0TWsBYvRU-YwgC6qlC65dlFXMR87F3L1Y_p5G7tIYObg-sENUty8bkVawKFg-JALC9VqYXgqtXQonZX_wABA7jfT7PSHiw4qivAEIXmWy_nolO3LxkcGcGQuG8bV_-EFk3KEgju9hKnNs4wbTmehvKPGvyMlnSO95G0KOhKE7j8AFB-QRAukSabh8o9QuI_NFGwM7qDJKcLDcKnVCgUwjOEF5MBUtx5-mN36_w_0r0UZWKndGIPvAesMwTV64EV1mStkF5i2RXaInrqdBcyLrb7ne-4apYfD1_UFsORA3bM65UVPWY8fEPo400pRr-vXmZP6MHLvd1VmwxqCIFHkwmeZvQK-iIlzTaj8PzsuMJKa2CrmoxqDN6q5cst1AfI9CyzkhprFAPeWIxgZaDlcOtjLdGwlDNWRPN-7Qu5VHL8NdefPDaRaUvXvnzkklENtJTaU0xczZiqSMEqiXm34AOodl9gzru6AzGGWYRuG40Jx338sgyV34fTRKtnqEZ8pBOepc6978y-EpjJPGn4r6uYk714nSSNqGG6pT-rmSlZ3MVLzxJUidJSTPXC3lB3muKc7jr982ojvmhZsZ1T7cj-kvzgrmL4XXmiVs_3JMQ_VxvRlJkCr22asNzWxWqapO_iZmost2yaO8M9RuG9lxlvKd_VGXBQEWx7dRqhr-cjsFIU54OJf1UIl9vppk_eYEnQVPX9VAgFLy0MVT-3JE45Xsf_tHdWQaWqAGfSaseA_-aY49hbuvDxrdnfBiv4jBwJU88EIPEYVg2O7whC9YiK94vHVaqXMhOZ0hq9XCc7vaLiVxmrS0DBc3PpW0goLBI_IEipl5ZNz6EBbHfFlJpt6mEzzjeliivvWLvHPSu3bH2kLqnHNXlr7_phTqWsBEKrA2V7F8C_8T6HSqKblyBBHLNUxSGvFfNnQD1TyBdB6trkemWRBJWHCxX70u0MzhKeCzH-ac1AXGiyMYuTUTB5cFNx6t03GrJIBynovLwoJlX2T4PAqNDJ9fgzxwwT8l8B60hNoWQuUGXWyBbR5b5SG1qpltSk6s-VaVuVYPitctyl-p71Zkgy1gmxWh14AmtuYapJ1Etzhsu5154XmI6EWPocbh31diczEm-hLtPNrHO6mfxxHcIVYp-a_s5k5czXc09RAhaA_HOeE_ocqQXOm-k-BFfStojWzfhSROIapKJJ6vGjMa27VPB_zj8Qgrs8MI7kIEEnmTtBY6QJbnEQsfySU-ex7skYbpZT5YPV2Ch3B4T4ob4VhhElxdQC7Rn1uIPObfAcFkQQbt4500IkOix-o9SLqjAUTiOb20Ps1Qw3Tlt7DT1OMDOWokLBASnDvHLWfT_IiU1eHA2nR4ltTTOjdQsMJeuzInJinGe9iFO6PHUIfcIAbqnJlRX7xysakcxgkA0RZ9MRgayYbBmjhah25AbGZ99WhiN_djH5MP4QN0Re-Yycoahf0lt_XGal6_Yb9UuWuw5N7UuJmsHXYlADsQwcg6i07vHJ4UJ6BU0ckFxCbu5By8zErtbTWD8lfYZiURHc2zOSpY7oDV4e0dqVC-VspqiETDJcfqDHp_2NbHgN3bs38bNutPR7l05W8RmCyCTAAqhzs4pFEMFOyHbIMAymGinylGAdt97qiy0957HXLAgMBrViB7WqUPXrqpjhx_Jar_krywxxgfs1aev9hq0g1Y_bf89l_P5PqRSAVfcCX1N94mDOuLpA4hke9RIeTaSpHV3t9uDQq45zogYSSsCQpDhmlbC9ZytoqnzkCQgXfe22WQSpYYKQdpLgdD_k6n0f2-QMR8284JPkFf3Pr6oPNlzor0ib9yNW1T9-vf4NMjLWBC6ajR7Uk02iGjeHclvbUc1TtU2vLJqu1NKSefUS7K0FAJPl8FwLJaseykDy7BpivjceWdEeP9NfxleN5wfHWPR066MZjlz9JWipxmO8oFA-N4KP3YTob9kacBo8lxSj_J2QIiv6OpIv6YEHPzyNFNjrsTgmmQ7k3r9neO3zP8FLiKFWsQiZ4DAJWpx4zXkJcfIinzBLNjHT6fWElhGZgGVrCJzzkUXVzT7xgMwzKCxK_dIUBcxWRqMf1-mfOAvZ3Ru1RdRmkcehsgtDLdvXTKGlMbTsYSUw37CN4PZHd_V9oBth2CVRuYSCCD2eCOCPoTO93kNlLObx_FepNCqLAckNfyQGYfA6u7eKUSkS1LBrlLc9DhOmqOU4-PGd_6tI_Yql2fUJgCTGmpG2q6sizQIRB93n9yz8Ae11sGUtDNS1iFOibht123W7QhlIx9oypFUQ0OdEpHhDtBwGclgpK2VaxJa1o-vxxYf1GwvAkZbH32d2weebF6BQEzuyqJXKX6I-2Txk-YCrAD-PsZHRwcZJRwXi4XEOlV9Do1BeIoTKWeaMiWdLqjnbJq6oRlTvWMM7WRA7Ebt5JhboVZmLMdoppnIKnhhBILbFqmqTLPWeYqNKNTVGfl5wkFGj2oIozExq4ru5-Y5Eua0yLQ7XhTx3Kvz1AcLtdWd84g1GNCJzNI8P_sEaWeQvMCUIxu60CGPjkUD4p0oT9gW_ADdII_TRnD9UncIH0neajQ3K6nmExZzOWSmYp1kcqI3LNseC0ssbMitTidZkwMN_FLHB0HA1G-OjPPjqCzzE_3JCBcLxJKc05RYfI9hUXFlk-LbrkeNIE-sD7UJELY41_vQSxI4yTnAtNgcCbKszYmSLk45ja7_tThFXoq5838fDvcKDf9D95Em4Wfk3KT1tZk0XMYLmfeWsB8KtCXzuDf3xNkU8GTiMomHuzKurtevGFkel2c0vcHEoWGMx3YofWTDMd5nIOZxTt0PnEuxptgb_PTGzDSlfks86V_xIdNANeaevYKnR8OrDi9TUgH34T6QOaxD8Cr35flpHmBbdBWXO0Oa5rPjFFVzf-J-RuAXxRkZHKWgNL5kybAtaaYykR8pgXj5ew9pla4H2pjdD02RpxnlmibKWUuU1uSGnvLxlij64ajc2cehXC1L_I3MkFrNtyrI_voM4gV-fqn5eG4ru6-GKr0w4XciwcJbFWuUGHQeHDvqUIFTDLfhdyQMrFC3RPRVMzai_FI3OzIpJrwRKSi2_TUSjcWGR9YsOE2Myb_pyDsUW_P4Oh6KsaUQip1RjsbzArGrSR6reuHlH1teKbBVOCLkERsVXC6RAm-AY5v21UFvimEtwfxlQ3V-l27u_FuNNpz3HzpbB2j0kkPiRYK6xPW6CHSLdkdm25f9pHrdEOGr4LiHUAI9Wl2JzvrqMKxVyToQJ54Z7QA5MAllz7xkJvl-JQ_GpeejvMBELQNXkXznW6wL2avdLL1VmNewKVYJjb9Gl021FGi1_pwV-EY53PU-rnzWOLIH0n-RWZcqTEuDgMB39uY1IvgsvGKP4JVLU1xZcywKsIoXDEEf23prlckSSlU6m63y5zd_p-KeAO-Sv5BPrhBMtzJmc8GAjusUqdYw95Fyq0jSFNptp1YU_QoB8weYggSYAyv23nupGFZDS6q6t3keyfeUVtKKcVNn4EojxrS54eZ31u0t03LqTWcQwJUq5ehR9FtUuN_cPjIzsujkEiNUkNcNSuPoSNyEbRFpjEsUfVE_5dmRl7O4tCiTmlNT55B8Y2CsjPfdOxbORZNwjLW6_ai1z3-YqNuowlUEEqbK1ksTVkoLuA7hNfrxLVnWK3xEITw0e855MxkiW6sRaVoWv2bq-aXmXjcY4nDiRKxh49inn1dN_i78tS3ZaxSRMcdsDYCUyllPS1HZLb-pRZwkfXrMQfazG4fCQixSMsbeOy1Jfzk3Djmi03EfPO9wyDVVhGgWq7iWey1JlFY8sUprp6T_cYybo4CXIKvbjhXft-l_FwCtb-8EOSebeXFgHjIadktaclMROqKqL1yE_ZSLVsdqd4M8tmS9ArwIX59tKhhn0lIAB-FfOiXinzmQnKoGRaJ2fsY87mabNb3flIw2rgQuXOeLA8mQZf5jY0e-S-dpqp9hhBV9aQsTrh5S_QYpTCcSLPvCTAr0hvmCWLjGWCpVvYUonKKOjpEAi90FSXvevYjmE9t9wJQKO8hXz2LtwO_jElrKvl9o0h7Jjti5Q_uUO5vokLD8JJHH6EiIQucFE6AQVN7ixwXIY_mGe--i3xY3Tv5ca5vGJgUwPxGG4_1cFtXGVxRiwRSKNIDWzQJoALA7aKRLEgpg14f2nSXYd2uQ5Z-F9C38_yFUzVUOdcd5oCJb2xRtLY3FYtfVXzOHf6I9oAyA7-t0224mGAAUx08R-E5H0hURdQCZu6TkHuWStaeF8iecpuC7BqxFb6iGDdwSRAPWFiUzjUA0b40Hl7MrJ1-ODFzZZYJtV4d7r4WUSkEiRzcrYf1GBRrHEB-XjnMCA_DX74tYXsEewp-rAJ0qFrTQJXLTHih_efu41g6so50XPv4hV2sL8NOR4CM62QC7vlEy-prlDt5EnkPjgkTKv_VYOODsemIk8lowAxmoCHxBqY9Z-h7SsXD7tOZoUqSDaEyz6VCnLVy6xJCjIuRtVXWoLOBqpP6E1Ob6l0o6lo4pyonvniS4XXljKC2nnAp71eW0ca-rkK-_emxB_IcdUWgRgfbC297wWYKIeWg_AjaHfdnwrEaBXbbFIcDpzdKuLDfbC2jiEmLiU887hs2rqx1C438ZyfoD-Yj-jjGzXLBNZ8yt10by8BwpzWXG3tMgURQer3qFUs5t4aQnyO5jtB9QovJL3EKyZ6oiNcPJj77RX5RQCzmjxi5Ol148p4lqdhl86RRtgoZbtWNI4EfzYuR3-vGzhopjQK9TgumGoUxoNuToZedlUYJDH1xalprD-lQC3GgEerUBtXlrATUj-DCOjuSY4n_Z3phNuFdKL3kcjpe2eiLlHFkey2fujfM9jRP-Vh4cW0cW9ByLcMoa8n7FRMymaZjlyKslSM-K8fs8ZR8-XxMaOM334581gOH7ymAUgQuaEu5ZHyXAJWBGjD0tklFrarymXpiTKAcduwvy_pZtph-c7U_wRdgFZ-YxczaheliRlGHmf-N8XhGEfiWH8uLgdRd1RFGaVFe8y-7i9lrNSy1F3G8DEPr8B6CJaEHACOF2SGKBQb06SjKfdUmp4OP5nGSV49JdmXtLw5EVLmb94uVHfTn2-DL3_8YW_BCfHQK4xpCHMQb9ct1wvI8SocWc3eCz8M2aOnazPkELr46aWqPNiEd8GD_R2_mYFXbWVHk-l6-Hzdik_MukjgsJZHPhCmQpxOptIHHtdtmaZCupDJDVJ3bqtGTUpfzz92YStA0S0FlJUZBnGby4ZZSG5RHu3C-YcT-n4R7VFmDyhn-9ovHpg6PZ7OWvXxMinV0HReoKq_1faES-vuVt2lrOiK-L05v0eOh-xz5yDAx9pYyc5KnOldfeiAX8nKBEhMji-jDShiZOKWh_4_H-Xm48tHbstCwAls1dw3fcRLHoWxi-uBSvdZDXbWBhMH5to6hIxyTXqz69AnpTrPNCBTZIFBvasHHSORRl1XtfrkK-beqVQDuxfpv6jPnmrygvtfDbNBS4G2dtx0N1sCuAUqTCKXJdGbXFxqCYcxzRpnWsrtr0VeiMlAL0tqGnPn2ru2eAwGUlI3tLZwRV4AdizZEXfH2dSWbIQ9eIvTcTvvPhm4-gYjuLof5IxuPh3VsDNGVU1TPJzKbLPfZOWzqZ5fjcAIqXTIXbVzHhWo_qPuP9zrC0OktoOGY3jvELUtBgqb0G7C8HugxlbKnb8YFEgZgNJWGn3C6yD89QLmfjYNQE_T-gtVvII2UBnn_KXrSwg1b72nEg6dj2pNxZ14z31kqrb1HG3F6yQ2nVhSeOg-AuMpBzRar37uIpopMypJRfvQywdfAm6jERBjzzJWWk-fGZNWBD_lATcov6N6POQFtZuitfqI6SYdlJkIMc22ZNshXGZEKEbs7d9POixzdCqNJI_JyjQJvspayKf4HKiFuvNY7P_UXG_Lhx5YrC9eYzSx_7fpIl0Ns-VE-urIZWU3ZKzZFlagelDBmnxdPBksyieaBPDjTqVEuQsQq2V7-ZEQGkkA54P3p14WSziyb-ZI_JslydezujnQJnwY_sXx5ZMl3Q6sxtCveLecWezWHQ6i2PfzusjlEWgINS0s1vS1C6bmA9tyswDNEgKC3L29caZmFByAYSaq9WomUPSUhLdH7WgxgXbFGw7QSkgRds2y7YlpWDV-uNxx2aC4wGfJldTbB-RLz8n4mJpYoByrxfW9e71HRo6pHRk3lu6n7LnnHcWRctPiZKt-InuWLw6GCAejRD2z1EiZJrrQrrMerU1DfzSeESBzmKRuRKTqEUDwrQcgO7t_1i1cB3OfpJuMpSzgWojbl1gCTO5WtTYi3o_Mq_Zy4C-u7px18XdM9dkGvOatdNNgG61gMkPFVex85m6ufYFgJY3UggoQfbj7YR6KMfmmvsRPZOZZ2Z7YEhElQEbnPbFbJ64ZpWF6pLKbMCM0tPKIms56spUBcgO6GsWd1VbvdkvTe_-vjLL1xM24L802jAbXmBuKWYRn-v4TgTIyOJuDJ_OvEixj90TV4MRt-FO5MBxS6op2a6a1jnN_6bT2ixKt3ITTTmLU_ZeCu7mCGw8XDLYA_X9qoRhOQ_w1gXD9cK5sSyAxy7lbt0CIjzdEJML-3K9UW_A2b91leWwfqcvMC7NXUStf_I0xDcFMbzvL1GVMipAJ3ig3UziBr8WXd1POyhtFHXP2o4DRUsyA-0-6ILJRXgq3dM3I1QswMbT_9w9D3OFcJKL-RorXhCd-clRH3ZylErJxm1-eaRaHe6vS6sjZTSzShPZU5PM9wccbHAj83CpeI5Kz9g6w0SzMvFtks4nu8zWKwhNlaZZwTJg92QU-6oS4hGxQdHYKuniBCdHrAybdXb8mEEEctWTjRMZOM87Lenbtd70wkzvM1tsawtWPeIbnRKgkJHoKeRbZuhUM8dqDDoNPPBuYTiTtrLWPmtyrjJUraylgokiwkTauQHD5HaA3CiAZFRxDUHdzFBew56QPY69Lp1NSSzHEMXrtHZiS0ix3ky87ddugu48KnRK82f4NgByhGVgp6fbXjItX50cbAPXXKooyv0JSnulW2db_33AvFiICefRIt9zeG5VyjQ2K9k8Cx4FjweAX7PqfMKyyjdSfzFb-IIomPb6lVIEdP7UO_OdixkSFKaGgz1IuP89tlc3ZEHBsQlBDzqBccNHRN2-bozCGfBs7nPizn3vMH53uMDHybWl2KwavzmKu3pcy0BjFfJtS_bryQK2dm0BhdHgBNHXXXGSqjegTNmpLFadbzUHFfcIgB8MBKObxp-iIKFn6PVgt73f3alItSrqIfHqSDNxeaKFLPAgjyX9fbN6F0jam-90vzjJvf8WXc5l7TU4FkshCUCN0g9HzS9RuIQPPmf0DO5TIScrw1FHpqprRHTAIxXqDqqV71Veg628GEkbH5LdHrKpy6Jp4S-O5yMZECwWed3KdYqoCAZA81Ntny-7UK1e-s33D45UpgBf5URKuJZNXxgRb2-w6eByTB-izpKcNmZ8KfoYd36sYLYNhXrg7eWQ9IciFCkQf8v1WeTkJ3cvI-f-57G6Zr6WcboFVWq7n9-QdEoxRc6YpAPPNpuZlddBuOG8cBQpZ4aDjrUkJgfi8e5dv0UbsJyXfrLqsp0TXnWOs41x2Ug3fRPaduyvvCWhAWXJa_RB_J04KXpI0Z0TyDyrLqQkSy53U2mHF_38oQuS-t47Na3mjJqLRAry9FKwhqEnOh33sKlYmigoDlZV-vynKksFrcJ2AZf5E6W4WywcunnRrMaA_VWcpdx3Wk9114G0GdriKD05Uuw1Im9FnE-ovL7TMqccodFVneqO-qRMcGufwNzx8jhYXqxcZFyUK42K9FoGbBSfFaSf7BcBom816VnbrzDdLNbT7aDu7nRS3eXLhmCBzSNnOfvIXXtVxCa0KNPPw-k0q9veups_znSuXIupIstolwcIgBA_C2R9SyJN5scGjso1ns6W184FijugKZud_SN2GwiPj7k0SLgx8E1cB7-UcNvJXPk2Wl5KDJBqo2EIWgSkeXPKpW0E2D0R01Xlxrpy1I_1Ln7T3pFXxjkMEjGKBcTZEY-4Fyg3lN7nTUy6h-ulUQ8ueO48Ql5Fi2VnmKS6YfN56-8AkDATkcTcWt_VQZ3PTL1S8DXyyAc6IYBasPCRU7-RzwNYWCDHziOUCpMUF6BZqmlf99wBdHS5GRBSBl0iTptFlAMyjzIDETDe4wDCbOSeWXnTRlwiKvIfT3PwUAo8K9BMG-eBtDmMZhoZMqFASeZbNxDo8YBhOh1yH7QBewjvF6xcPwQXJFL6Sk6Yt7CC63m1g_yQAZv8PQQOJzM1bMhpSdQyAUC2Cc-dc2sWUkQ9zd_PclYFFIRnFFFuywNPb3jd8_wtIZySdxPgJ3l-g9_nhgzDTjbeBKWqySpabnu8If-XLqlqi6OE1HHutR1UlWdblloQKu6SDxbLDfJT0siLhmZPVjCcXmIo05fiuD1pNqQdxDBQoJn9dA3RaOVdaao1D0xVA5lFL_NucYiRFNhQu52ROEE1uM7yac1pWMNdyav6xqrtIa4cGD9vwCA6Dj2UCay7vU1rcY6NIehORL0ToVi3ImsVeCg9_1dW1OqzQoukbDJJSN61gkyEkOvGwqYFlb7CxEMmaiLC2rYI6hZFXaVBm3icl8-Ygd3ZrIJOLuzwZY1Kjj4zkhY1CCg5fN0Vn8doA68BkrJmo0s3TBjod-zSxnOiUdQJtncMEnh1AmgN3sFEJLIcx--3wtxMDsbdSjnJq0skHtGNZw8q3FY6V9UJ8aJ5y_x5fkJRpJKeBbw45qhasb_GZBsqUzwMTiuZddkyMcjdtHZvzWRfbLltv0JGFoiBtcP1p6NYQJfV9ese9fL5mx6YscpnfG8aMMZaFkMTPbm-V4OBi_mwtbUvgfpfCAKSHp7xZ9faKopZ6W3vU-mzusC8cGnbqNJoLGbbbCYvGb9WDt9GHq9_o2FwLfSm6PF0PpbT4iB5epys1uvYwDuYy3PyCsxwofMT1ffFagxDVahT0Iealrncb0mzFbegBC66UDwgc9yB22DsNGs1nsE17tFdqE7U7E18cW-o3p8OIVICFxN6GuoPYXJ_bCOrW6awLD3ilufLUTzJgwhaai4D_bZOb57ZWzMcdMnvXWHS4kqq3kSTkMYgBcb_HGc44tLcr43HhLGYqcisRtdhszaCxOatAKBezTl1KNqduq5ry3jfbj210sgoWgK12_UvggRdkFrstbmlqpiROYNKQWtcLli5-8zZWexAdtzV_XYEwIpt-daqmEMHvh81pZt59N1iA68eH2-bx-RhQLbDb1b3Ca8XitnEuXFAmeEw8jngrTJn1OtrX8JW3OFLAYwNjGRCQ3cIo0Bd460WhkAgfcpbDqXqislOAoo2EMEmJO5DvEtAymWQNR5Ssx_u_IDfbCf2cdKMvCC_ZEI4A94RDnxMFYXtlQXDXocGsFwg1NpE2d0frhmfyR6dchBfn-OYlhlN9Y43u6EXlapp8ZY5nIm9C0aARHRuT1IX3Qrx_B73aOWL3T8fhC_teYPnskInKvA2qNNh22ndIQWo-sQWbcK6xJJUcc8wzsDVNENj2ZgiN9vRACi9GamX5mth1CmjdtqNQ4hF54m0pAVC8OKgOxmM86ZgjXLmDj-xyVUMMwlzUbFR5jaiL7BzbXzCnLLTt_4kTc1QLo8jj-xyYhlGAQj_HUQJQZX81o3-X_HgEuJ26XalLfxt3qp9wnMsQf9do8eUQGH4ywBTlk3dH2mloXhLSAnWQARPOQ1yscGArq8FiUc8OglXz4E04nwnXfUVpjyxdQTfaEOic2USTRENshxJvD6ikNE6zzyPE0Pki9ZxTPy9oPoCg5IaSrQ7lF3bHdUS8i7vtRX9W5JyOXRuh1EaFhbRWRU7efkZVuQ_BmTR6Lo1OOhN5xuVw9lDUQ8m-EkWwPzgbQtttnLH9yYMd8rtN8FzVba-gXE1gLfGOsAD9QiyWyAki6MfSYhPnNkg-65R3Lz-tKgkf-LXQG01WDZXida2oNW8P7DEvPLglRVvqS2JtSCfOhNvJiLgkEnuvcbcXkXQtTgqyYRCAS2I3ux7PCB2nxoKYlFlsZBb3KnuGCneKm4OYYMhv-fkbJkjTgieBZkVrtidJby8TQnv09wIUGn-UVfEeK8LkE9vs0vUag1cEqNz-yA1HPBUh6WQtBdCtSwMZsnBV6KQSyeuybb0uXlTdE0IyQmDiMKwbf3tQpkjmKEA2YzWgAQjNto0GekyvMUTDt4_G5mu2msYf_ytgROilU_3L5mRMpxkRf7B2cZjRcNVzUkJrZySjcQ_K4lpuMt_vxGefFt8vPPFeEUPeZaEWaywSCHBMhuvXTPvvrx0cXRmXhKL-x-LgfkY2HDnclDh524JzOmtjjSqTRvo0xCvLClFmDAxByrODCUl6Ff8hlrYYgrVcL0TnOrMcFt2PHHj4HAnxik_B1Wv5IfvaT-XOhpfDAmRLWueHO3wavY8ZYRs_rc36Ujc_feDa1rech12jmzwByME2CGC1quDoI3YRSXQNd6OpGzlVViNuXH_sf9BO0co56ssGS0M5uQjpvp6uAqCrsz33nVyeN5JpILHn2geFPc8jcwdCYLkRmr5upDaMtyudGY81NxyJ1WNLOrWXth9ofmKaYFkuK9OBGNXSOYqA1uA7rcRDXONMfeyWE-OCsuqkufBJeetQBeOVG8cXJH-e5OAEh00RvpgpVbsTCCxkUqC76kemudnv6RtM2BUfugc6z9xpKFYO__IUPzT56p7cfSCCfuKNzVQIr2tXkSh0upIqeOshwwnyrRf-BIFNomTwtvTYpmkk5WsyoKAoa44TvT6BY0Ozr_PkoQl1hswL8RYUxXSb1XFl4z7CuWitz1cWp8YP7XfedRV2mrsQAgXVkBaI5JzSD8GW5GQuRns1z_UxBHsSLBfK4l0VJBKSthp3kCrzlGNj_LUWQEyW1DYc9M0RBYBRkTinMn8VTrjhn-_7uTRn1wUjMivRO41J6wGN_RlowP8qFSjnqc40E4l3rXJjdL8dL8TIWhDxj01e5GlHTIBh3zGBwiPHiXqMtj1SmURa3WMVNiGSi2Kb-6FYZUuKkN1lrKf7v7tyfi_nQkLJDdOEnpRWPRcrKF5pTKXZSXMjj6zJW8bnj6UCMEAt6q0s7Xyvcnh-NRo4cZsKkOO5fFs-JAHZ3FIvBriHgHdpGEceCQUqvbCm50ErB3F0yImIhOiffFT4moiFBvUj4PNB5VircZcaSKzu6QdCmYg4Anm8I0IaUyfq6CQEQMTEHrrk-v7E_h7fkeYIy1XiJLp4-eWKs8rQh56y9IulQ6cipoYJaBcRQFvb4l57MgA0IVa6NU1Lh1Z6iMLKIm6rrIMIcnsd88fi1oRHRk4mjq4jMSP_tw5PYJy2P_DpQM1UDubWCVyjKeZxtFXLBC8DON2tP8FciTV6safKYead_UZBLCytd8w1TLG9DCmBI_68cz3952dUI10kNwrEObHLPu-3r9PTwD4gP27qPS-9ndX5VmWXRoD7ftTsL21tUHK72bQ7eh1ggQ1NRQyCQgNOks_eyEN9MjMX6f7A8kioDIyDAc9wpE43tEl7oboMfQx1BtxrzcozvVWa0YIiKYyYULHX2ZBxqIge6-WQkzNCzN7cBZrWoPv6CK8isM8AUkeuMAOav1UwHc9LtpN_e3Tcw72pKPh2UqfUFuDDMJk5v8Z4_TT-zO2TaysSRxmtHHdHeCILBAWBnyosTujwF01vyqc-eMGBKkogvBKhJP3ecALAO7L2gjNYI0jHQPkHsc9WkHVNPF30UF0o2qCd_T6950dZ2zJ8VX69gw5MnEoXTopI0WzlNAG0xEWH_YqUBLjJpdrOSwv_Ick0MlT_KuprlsbOjpjSh0uE2Jn8F5tCEZU5jmBzZS5Z5-QuyC-Ip9OPFLWqE8o8D7yyKb0vKo66XqJ2TrQhVopCrVhdkAfbngaulLGRrZnydIwjBQqLTYdsWPW-Ui2mza_KIdcSMn0sQ7RR_M9cozpnLLA-Yv3rk4RqxEAuA8RVSfNzEAnVxyZJFyWk0-Yir98JVZ6zZO0H1Oo6NQtrM6zf7bJsAK46zkc7Q8vIo72NJyBqIns8anNtNOMmGo4PsDx6rlbK0_gKJ_XbB_dwfOyi3Uf0__o_wFLiIP9iXinYI65kbRJJVmaDorJC61X0Hcj15hk6Ac_20FF3I7Z6cWRBeAf2bXBHFkSuLN3XeXDDntD_1V32PwtJ7E5xTV_7xSMMUvn4KFJkvjvtynPJEXd1z-4b7Y2IHgGRZzyjOzfT6Ts47QyFc5nnWnzWAbsxsD65gFcUgcyGZd2xz5iIYrwNDFUpZYkh9Hh9cQ8VYZm13ChH44C_nv1itgvS_uVTck4DIZq7CTXaSpcj549NxXln387Ftie0HE_qV_ALVAQdGq0xx_EvYtli1KNgoj5LN0luHT_YyvVg9gqwLjRnUCEX9E4qH5naSMJl7i8QiBivk7lgINwQ114rSctrv2cUglIxWY5VGG-WqNaf12znrOTRw2zXjI0167jH_0XFZchQMrU8cCwEM1BkNAX-oO-zTx-vdFDfMmd6lWrceiJ23_u_ATVu_RiauAhivfCT-pie-lNFPxCwvcG79MXv3H55pFgy6BGyr6L9eTF5PpqM72TUq-YIi4gxBybZFL_8Kr4sUuvZKJ5XSLpVkBgUJDczW7uL4HIc7P-2K-gOXjyMdTKP-tz582F5bfsJlBFulDHR24b7ACmILg-7NpCpE1SFlm_nYqjNF1EUVX-jTEZrP-nULcWcBqi8S0NIuDhh27gx2wSfxa7t6vVU-unmMHUhGSkbN3HiycQUU0bU2rjcxJ6JoVkqMDxUoTL0xQg9FUItAR51zPZipaSY5lC-O58f7fXT92lfI6lb4LspJohgmy5VJNeFNDO4VWvtuCJo3_jcgMLGD_8Hr9Kqi69NmUyDsGmCNKJXVYmxjT43wWOVVtmXgW-VZiSpcZcdLwZHrxy-1xPP-6-qovoQDiScrqHaCMzbgOaXB_KQ20pReLAwUqmnD1NNy43F64c1MGib6dm79WjwOWyVbKbAfUJZGNUF0V3Ga_s5YUTVtb8yyCLBX1JUKrEAE162VQoaL5G_iEwUPsGe98ympskGCsk3WC9oU62ITD8nTZ8sA4ymNNXvSwb8WCJ5zNry0nyN5fRXd17m_ZZ_zYrlLlfn1MsuxYu5RaYW6wnupgTiTWU55ig85Bhnn-koDdO6po52L6JWFOvZoJYbXu-i-xwvkQnoar3rvva6iFrPEssGNKVRIEqpy7_LeErfDl0dQTdVno5sAeZFI_D2NZq1bpnO_DRWJ-SWzfkBhCOgypWv9iIGPH6y7WMC4Z7idoM4pXVUjTw9YJYAwVO3QZ7yiZjuouXEtOyJlqLUPQvUsWDhH6Fi1L2XMGiqYnMFHWzBoy1ffzkksbrQbaoAH620yGVmNi514nJmx48sIm8SNRp19t8brBdGaosluOLTCzXtSKSIIRuzRp54gm_6hWw4y0FgmRc9yZtcJTHHacj5oW4cwzQgvejxaItavdTEcbcekaHoOSGWaWGZCIgO1nWnR1EvicgAI1FESK3ZYrqGI5gOHqRgBZMTN0VNuY5eydjQZEiOgRWdVvCDV-0hKntgwR_E6SZylYKjtcPBsLIaGOmIX_TbrOJ3dpVT-671soPSe5ieBaT5afd3OjLZQg1AeATwpEsEenQmWnjsxnhmZS_RwqvmEEQ58VepqVYu3kOPPGgycZI3DlyUYYqAGkEUJ5dyWscc-5AGcqw1XFf0m6qI0fg3wa1NuwHPBUzBzcGFn7kvQNuit2qD3Y344D8gtrsziYr3H2HtbOlagB98p84ri1ogkdMGVcbGAZxK8KCwb-GxQC-afIaSYGmvRlGbSEXNn0bRo7eEWBP1vHf_LvjEzEbC4PasLjSHzLoRh4cEL6CuROlIUzghkNl0viGINoKd0S0dRw7bixRzvMCVz5QTrnb5YEFRGFPv68Hmtqp39e7cWfEMa6pEzbQz52h3NSvVnKA2gjfB8g8BikHz9bd_akC-areKrWxvV9OHh_9_pdKBghcOOCE1kFhwmVGt0HzYh4zacV-soVTDxKMtJRjMZpHWpnRcjj9B1oU5xgNSXqgHdiX3bbqWbssUpUXZbzojmyqRJB8SKHg_hi3nQZja1Vzan_JQw3cgDL_fpSFncJ3l5CB4UocEftjQrW6PaonJk0nyGktOT48TNpJ41Q1Yq9vjdXohT0o3yXSkUCkZEXwx422vuSY6qB8bbV_0kAgzyQKUrLANsJQL4kzylP2jBxL0qlF-GjCs-YUIWtx2uhofq0nLP4CqajVJZcxrcOg874-l2fILsTRjVcqIdG4I4QHneiDFHfNVRFR7k3g6rW9Gme8eZKMBLuASEG7a_jIrZFz8KjNxI0EC-nhtO8QClIYhDXdVcP1KLOlgZ4bgysbkiqdJT94QPsnfWPxuYWW5doWSalHa9xBEs6OU_NTkQ3T-XF1XpfoJsYGhe9T2MnAp4SY2WmcnAd7PIuIY7QpRmbOYpi4L4YxlO_8uDPYFMZ5_kZBq2cGdLlR4IzjXmI9fz27uc96SJPu1JqB_gyPgPlbPiedtGKK6AsFdo8xPfXDNbfecymO0F11YF0UVTo72Xbu4tmJcQ2Ek6Hd0EfYvhLnCIBGINeETuoOPcdGmcDt0P_zNIUEMEkkGldvzJ5j9IPqZ7koeyH4qDcLPkAM-rewnqL-vZeC1JAOi5XYR450kIORhbLY_T29LW-d7yAd7_UjSs0SFT6KgEjUmmKerLnRIvGSQhgynCfBO2Lxu9lMpkuOQycB9dg7UnUrtNQ_M-aKdmEDmFKbd2-SmngNMm2fdQSSMxDyZLd6GPuK-FtpWcIgenNLAJpZ6ttPf4xTP7lTjvWN-QjtCBZZgI2r4a00WAitEVcSdbR1sC85wnBmNJdBgqlAezCNb4VZZvQWNrd6RAvjgDhG7mfAborXQ_2D5_QcwC90pYcOP2eHQJzWjfcMahAr2DvKgg7-TuM_sNQXH6K9wmncIQHygITUdx7bQnEp12yoklTsozDrrsbWUgbxH-sqAW2nnsDbNS6bSyRzKCpTQKVWTsIeJu_hrOg9ZD9yrwZGUdTb43pGOdssF4B1toPbEBBsU3aVMxUhlLeCVz04qEQKlC4SjFj0lr_SCNkHbDMBXFRlIeUUs_Fxzb5ekp9OsYrXogKid0HOasgvgGSomRSyChlDkrPlrKM9TRPjGrD8Ihb5_OnZZoZJ_WMdR-uu5ZIbNU85PCZrb6LVHV7l39PoMLonbJZjfn9QjdMv8fJc0TxcJ2Dh42giIOEu2Q2gT42k0kbq7_AGLukogKd8QPl7845OAfF2YoPeRSMI07mbefIJyL4L1r1SITOMOSsqNPHd-W2AS1ZPThxQWfHgULH-PAzL8mubzUsSeJTsDL9COOK420ds8GaKaoL9BDYGd8KlQ5Pn5US_aFG_5wNbjDvj3quxIpbHD6F6QSOMaWy3ymKbDpn3bm3LMNjO7myBqRh3pOZSxnJZW7o2PfNmxcNnIDRyItYWtHXUAPy7v-qb25hS0c_H1yWdTrFJ1I6F38AZRYQigvmyddHxhtpsjlXtCvbQiJuz3Gs5tWlgo21m7F6_fd9S6oPmkUM4k05Nqh0NY5HMElBIBtocM334q-JDuWAQPnUv0CGWyN8hmh6u2BSi-wYI8Vun7LKMJyHONfkhUTOa_4ZDY0c_3kOnSA8aPlObVZONt2swM1HPrJIlFO1KOhio3GMp2MDI1pUYYdQbygH-YtlMMH2Q7Hq-jaRSUjMEmpT17itWBmWI29f81kj3jc3aCq0ra7iHULF3gWl5EZ7OOoAFFJF2L6irJkSc4Sq4S3Rqq4v9dVy4sjoxLc9ILlJRAL51AEz16uvZStnHagnc8-k6PplDAUu2r1ueEN2f0-Ks1kbi39a-Tm7zAtRhyJyRPd3VDxlcPeWpVvQpFDNPOnqD2Zz1_n2ozHYfkXSZgn1bk3ZchxoLVwU1_mt1wA_2sbFguBogJNNlHZ_czhZwh_B0tHgTnlQEQhkFs0vHIrkaIQ4P51eFVW_PtROnplptqbcOQhgJ3n5iS1NYwDOGWIbkWyorSWk6SRQcJKSy5-tbiiegZDKuVtKmI9Jgxaaw7oZoOeJfr8l88X2PutP1YsebbZOaRtKX1fR3_Dz61BAyd8cJaw8YZ869U3X7NbYfQA-Q5ZakgGmkdqDCReOngU1XHIvFunqFVEF2N976ovS6x4Tghmy6QOri7nI5rxlc9_bFLTiRR8VO-b4e42Av1x_ZWABkzjMkVFYX4ocVrfOkqo6vSUDWeY3XM4vyAIceZN6kuZ9t5qFb4vdKZEtUnrM0uevRq8IQajGTyWKmczXrw_6M7F6NvoXX_XopE4495__X7lfFs5TzESnUROhReivfWEUGSsHq3YRKcyIKkfUffH-Its37TYrb34Av1g5_9q-qIigs1a6xHLIB8XUUqJllY7Tvd0rY-InhEo-AirX7sS-SMpQHYA96vGFi3jWv_IOhGte70OT8MamIsmHY5vL-ixysEtV4pCjboUO-y2jPLshXUpfbwy3tydtwiPiHuC1PYkPeLXHZmFHhZkhMdPqc2glMB2-Bt66T34QXuJpzBhQw8g0IcaDfg3B5YvQeqTHUziK77o052DkDa0vi0fgKcM_JQ4FtA9Ek3mStsEQ0tlK9cXBJiVzzlyQqLrHA-IKoiU-UzojqKNCV2hQZN-soU4qF-3boabZ6Wq1pSKURPLRvy3cCuS7iCuIya2XmS7DUQhXgnaI73WsaOS14TthMCVefBTAY3P76p5YTyqJNUrKzFOBS6VFLIMkttPPjRo1HQGaDxtJ22rgPY1mA35FFIxuCLK_d3-tWk6cutVoYwwEgy3tmbCDtlebbwsEH8tZSh6Q2EeecWfM_bcyHD0aQrCqsrlQnCYeIEdDQB_F49NDWHN8W9S3TqPouTeZ2K-gKGuK6jiMF4gwJu0KPKO1MS9nZ7SJud8QBoC5xwC4kPOwEokfbVqxitUriK678KZgKBSaDVQWDekY21zktepLRGS0n8YzEPPEhuN_2Ygwf1KxZ9DmCrzEQnsnYlX0EjqPvbs2OrR3Dg9paVU8Cm--iQa_CDiyUo2LWwS7OV2ez0hCGbAV1AM9TrekrcCit8Fl9JY_jMzo-LstHuBXOshNK3P9y011S8UuU20shcaUmlS2R6vtagfCIiICAZJF9LKDBLTQWTeeeyyMQJ14ZM_BbLG5-ymcNeLx6KFn7hLefajaKAKCMQHbQ31f-YpnyxayR6rsKkK5ckCQl0NwnPeHwaYYVJ5ln2VwpqOcht8LymiDs5xR9JbqqrZVEjqtXiBYvmkyDxUJuAGggQ-OnjDBBgRQSRGb8cFZqeArtxItpGqJnZpJr_85BHwcFeaZIHPbJ9IjNqDkLUxMvehiB0eqjWmF2B5v8VBfE5_tl23M-sBqhSDwkc_GQ1WJG9wzSsJQqH_H1Gp0aZ8-9wBQ2mT-AoPvzHX2jDWb02y4Rd-0T-cZ5ofJDMapq_c_crNiC_j7ISJ2bA9aZ2NoRE1UcV89H4x0F4kVFrDYcY_dJmsSG0-YaRo3ti8UmPLMtp_TPDHViZxwYiHd0TlyxcbdSxi36vISxcwNwhMf8ZVMM0JuLYReSQT8LUgB8JHjX_R_7n1wMxlySDLBLNUrYnu_oOnkdUmWG4aTbF_SXloG5-9IqwYy2GLsqForEXkW9Txxc0OC4spKE4MN8d0Kuhkv4OIwEPIjajEfkdMG4z0fs9CD3M5wEPA1TumOCOXxxraue_uuPkaPEznqtWhZHLTXgOfoEB4v4JH4QULPgFfn1xkkYz6nqHZZ0T8SPrJR3uexUi1rJ053lk8FW7-Ipd0MPkWyGta3DykqTSwCE0U3HgdAEdW9HI6nfwZjbTsxLCaIYz4oMoFGKCzar7SEn9RF1RIYGayG5T1Dn0XIeRjENg1F7By2944YIZYa2hEfT-JyEOhNBF4YzwTr5t_5ml71DKSQdI6ZZBi-qcIFYkxG6hc22-lX83jkvlcDX0dx4rVRsPh8lB-wIzqMJ4qL-E2Gu_VD_OGCegI7TQR0hmplUhMWOSJkkPuDKbEcas2e7vf_SiurDEm3-l6OvixrGWXMQ5zK97vDX1VTnRovI6QGiCIPAN0gkB2-eOqpwUyzH7ZvnUP704vZooltGIlusQRQWo-CK_ldv4py--ZyuMm37j1EhzAhsY2poF8z_gou3ogPxDBEysvmnSuc8K4p3pmqdJYq0Kg6RYyNKK9RKDwgnkPf69Lfj3Rje3Jee2PIJ1jyB3bVYfYXqQ813v58vFxkUF0pWudOlftL2FowEPDXGZGzM2cVHa6M7CTEE0F_llzyY7Zzh4Orpf7WD4Qsrl-EW5Isr8kmw7nLEkq3DEzEh7eHPxFfdJnzD8VaiYSiNtPIoseaxBKBra6v6PdIjQOtWazjj2fsk4tVHxWuQtboLV0t0C53PdXFkmSY_VStdtpb7df72CkXotAPzZIp3cSjNMt7SHFK1brOfA5Rn1Thh_PXcsty8Fngj4fwF5QqrmcYfLNNV53dhG2_w2rXVvUrX2qIHkf1vGInQX1eBpSs1lAMX0z_rDlR6yuPvXil0wA1MKFuoX26gw_yf4LFrn4aJ4sp54Xgeccpem2GMUGBtZXKA7CRpc48IvYcPYsnYGV8ZeHbonq3cBmOTJNvcR74Fk4D9sVvhtjeNg7B6HefApjDffi3M4f86MOOZn5goVsDm-AdMTN0Xr_hpscROEoLdXrV3BxwcnC7F1GIEPfP-Mc3HAcn5djcQTjltUaYvKy86ShgT_DdOZkFdyJ5FErE_X_zX8QtRRKCcrdk9h6Kon7uJyWVtXaVAxhaYLi2PtJvGhl8w_LFtSUQVeOy_vyXh7TCdAFYeCHq6dyWfRY2dj1ZY3zYbEYv4XMdlz9TAKjfVkayREorwry3aelri__CMNiO8mJH5_OUX7AxnQCLoOhf-S1ZC2sQoTAIv9lYlichhqHxQx6ue3cR6YA6OdCQamEzeVApHPnnq_74UC8pVV0ULvqsQBbE_tdAU_8yzNU0AJJQhHqlFJjg_Rjxw6S-dK5XRDrKkS47asXnBXCK6lAJ_E_j6u7W7dkvxItW2s-HxtjRSxcXVMAq6vylPLttw0osoFWbQi7W4CxRT_YMdXBftGEBpA65kGFvpeT7DjI4JUi_KmOgl679rnhsj5Lk848ysbHVvl0bIPJ4UNC9IKHmAuFhb7m9yJnw2j112T3TGCYhNPTB8-rplYUdJACa2pBJfGhqGrxXVgIv-RPas4EwfP49D-SyATSWFZnVwRCjb2SLScrJLl7NAdd5uiRjzXTBLLRKkfCYvr8Rd_N97YUxTxC407_JYZt4v0PfBSUpbXcXSb_OdxUpvrqCII-fi8Wt1bBW0ZiSqIOBzciw_Q9YDGPGj5oO49a31CPK7ltoigpAqzjpM-wMEHihGWy5rjYNkQgM3s_Q3P3WVI2iLFWAG8ZQJTplovIqH1efr-hqXfMsca-1Z9hLQZlwWgrEPXkLZbkmfaThcLkiPjNqln7_nxR5byC0K2FY5ZJQYeF59mFknD4c_PurZ6jD6MERIXZ_alUHyMVTYRumkZ8qRJCwD6isJzVG20ZUiHOHe--T72YyUVvI181iWJRVGU4uYtGyvMIjI8Bdpai0rH7fzduHWCcIQJrLmdua5YW-_V0Ti2qNzdNMBgRAvGWd_iItsrTcy3BEkmB-XE25uyvzGEbcTRi2SE_JMylgHo5kqLotLQkUxo7XktNWiykiziK19OTk9lZcHAP-X5ATCHWhPxcqgRO2SlSde4qecPecdgf4LUs_WjeZx1zpjL8yr1bETv5zfHlQQwTqfVxOcEser-DUl7q3AMExd6WksBxLPoB6FAvJ-mGZHya5-HpUEwcX9R6kO4yOk2oFxJhI792g1RefJg9PRWQ1kCSnfYJ5E04manc1JPbNm71GAY5YVv-3y5FNsuHZ8AJHXJFyOC0D2mZwnrMlsZydjp3BNp0QTO-ZYik7r_gmlX0-AuiMgUajoTmFn4kYB04SqcBUeX_mZGIvJgwoXPWR9NNRuJ-rwCFE6PcWKAwh9lQwvc02DE5BN35_aIUX1FAmxrA7eXfuAaBSNsClfFCVyueZnysCvWXgy90z76lscUlAjB52GscgoxPlrWzcw0cF5wh77s2AifHqy-shkPS05F-4S-49P2WbHB9Yx87S426LXr0hwN9EDC8w0nSynBmDR59zZCuHTw_e_w837GvgLLsK0-4mpPpIoREDTWn65MO5WyqeRfKYD-3ssJ21YFMNJWPgoZM9Mzo6JtYfKAKq_GYgC1-pNnrKHq0redXiu3B8kYMSgbpUqpTSiPKgf44dE59UCFnYun3ZGT6QYtCfTad3DyXFuO8twnbyL5_s1WXPYQHuUO6k6CvFJg9UplQXc3v5PNgciD26rWIHImBrmtkTLgrM5MqrKAk3ssZTKbQfS_2CzHsuhmfyaHdUI3pFkRp3nUUW8IOUr99rhGgwHgyfKLeE0doZ3f9waULqgRRL1W-3-Ax4s0NouOngOsJrLHJV6Jj2K6RUYY45vfFeOPphJ_VFb_fFZ6C5yaO_IR-WXxwYBEqbx9-TC48kLuscE44RdIcR1SEXxeZgrtm1FKblFqPt4WI0WVwUw2T3C8kI5jW9trAVeQ5tEnGA3y0tKoqPiNWyGnmQfmUS5RAr9iaWDL6vXKQ-pIPCOPm6zWSKv0Uq67jAUfB5XmrMT130KehzHdeXh2IzzdBHKMHD70jWxxjCG0g7MFsCk1NGd6Vj6G1TwAb_UBxoI8cssIFWlSTFlj56KTdM9MtZ1DWaAD9EUyr6xWjtTSWXgaA_h8PdejxEl7ZKQaMqBbV8_cYtmameNb3ZEt8wp6FKFoRVfJEqwiCtKZZlOyGfnVai7_aXXGiUqK00FCTZ_0n5Qxa8SosIJ8Dx7WLV_QjR-T_RqaUs6BDIm3oB-k-yfyFbkCw2lNGDTe9rjuBCu63VKUEnRmpukH0m2pW3ilkYfB-wDb6OoGS1Go-dfRW-eXaVGNZtnFlv9MzPn1oeQWcyWvUP5_OSvV9-6oprmkC6yFJjMnFjegwzAtdIHKwpzbsyH83BRxQut3pXdY_Z8YIOnoNDOTv4PbWYCYYlZcD8CErmW_-S-brZmfZWvQiF427zLfi-4Qcm4pd630ae3L4WPheJBofSkZoQ3TKcBNFp_moIrpdX0W_g0tMvwrswrIlPAbwzhq-Nde_zpNjYHemUf2O1fDrCMq1byyxDCwzaNJNonKxE2C2AzDumxFZZPqxTwkeqjHKkm8Oxot2TZtyxwYkmQDhORWTn1hIKdv0EfWEA7Ap3bw9Tktzq_-BCxC8CRZPkCSFr-U-Yzd0Uo3zQObAirLIBvTmcij2xC9uiacNo2YGhkR3ds59QLZw4-_ffoA-MmTAPntmgdN7FSgWrcM61WDrK8Z8qwpMlUA4Xa2VErCrV3Mze-JxWrmZjNwUNQ8tEB5uaFBJ4KnWpHTkIDhIFRQohiwyjU7j3Gr9dugTCmn17bwkfJSAbffVulsrxx91zbNsvBkNhSSRxMM_6_G8Ck7p-U8HtrDjzKgCAQkSy6XDFVGVV51oUsDQvV3LGrC4Zg8RHkqcpXyLr2pVAA-kFP1hQ4g4m_CTfHhEZkBGUbg2_YaWk95mbUdrTsfPyUO_7Psh5or79ui7jvAgj5HRynbQwSmaVn4iAVR_wVdVQM_O0b3YDeOCq-DqJfTFrxCBPYpXqaWQJPt5JgLU_SmK6www3mBPUndqktZVbunGF9cvcGqzBKth7ksRCPYg3Uer1DGbNmY9ZnX97l0J1cAeNDrWZ-AzYYB_EEi-uJN-Lpp8y-f6GWuav5_xSDlQdvF7nBuTTfpANjA2AVva_ChNT8h88lvLczTmzXVY-NoQp77coPlly2rsfDrXy8jIKIbJ3ieYNZ-h37DloRboUd75Nv_yI5RTDQnH7cLEegi08EGBhXV6fvW89UNfAeZHsaOy-x7sBHyJmou_BuNn327IDUEWABt91pF_U9afT6ubdajyUyiMjLoFCE0GyOMTEALDtea9KVKk_hhoYkvuUHf62kUmA54Qz7RR05p3mtLoo50NUZ4pzyhsDMIqfO1m7OyHBDcX3O4ECbuIi34lE6r3lYH33Y2OexHhqnzZfkKIWlMrORhdI2TpEDPsI2cGhWsQ-5gZTcnx8SzGm6JzFxGUbILJVrdN9lObA4gZcFQqaeJCKdjgnps7Gk0ykg9X8LZXyt3A1h7xC9APhC8ByiY-RTMUPUpBRfxXoHOmKdzERU-4kreemHZCMo9Hk47gIaQqKHzzXXOE1dmTmC4pZSXAlsKJQY3rlNI0L_BU2EQ02oNJZAaKYQKdX23Uc5hmTSCBmGF15UOVd86Z-jJxzza9lS6hPGVR_uKwWBJ8GC-nZhfwUe6N_MFuKoio9KmFuUAOd9a-8fo-PtFus8p8TZbJoakMChJmdd3mvSbFSxXqR1W32wZdymM-qaGaog4z96_j_6L14pyFbK-mc51b7nfJxcXPYtLEyiY-dqZCRACGjrKgbD-pvaf_A8av1N0v2rZUJMwbwqDIgmNoBvTwnb4HVM-z6VHOsrMqoZMkHYOmx2e9RNI3daxsjX6d5V_mlSoIMIjOa75mYbjLkHnKcYA748C47K4CB7Eu5LjM9E17NPp184tegLmhnN66m9c_5mc3L-CGAsyCfzEXBCdW0-rJVupoBB4j-5AoYaQKM2UVK9QLEztlSXCSnRhuxJOs6XJBoHbpn2ds1mdmqgXk7qqXJUC53LQ884ak566YcfwtykKvsLyv63YaGz-ZCMxnbTVdylmgSJXqr_nh5Stpd_AztDjCGHHSvL20-t0EnKqKiZFuOvF9MEqoD0CJC5L5ydblDxcZGK0mL2CNx6Uv07bvYnmtC7x8Clu7udT8UkMKXPXtGseZdYhkIQjiY3Be7gQDIOFvUGyxKccbi-p9hqviJ6x3cNkgyFmHPEfR_-tzHZvVEvnVmCduDCOiVBfA7DhrGFTuRFzDXHvyDXh7Ie15QFAjKk55StwYYbp6j3JXJB5D9hSnUR3tRAf9y8_PhsUmXhYizJR7pEqCi1ko0zXInl2VIUg2ACxcaQyAaugbqAFbMBokYCuGyUskPiOZ4Yp7HA2cEbqelspJpLG5jnsfzzN3XVPXqGA1teYEPHxIbjS8opofut_7hVqAAv2qQ4wkg9E30HKvWcxSdgdf3oFPaeTKpFpr_exVWlSRb_7Z1MMu5jHqpzNOACCsjkPJToPxwJ_5oEgfvHB0BysyoIhLhWNiyykBBR8TTto3nmRr4NxSAbrkmjo8pxsR1U_d99mPqwg3k2KZo60AXDgpOu5Gsqy_KsiG0On8KX7f4S0q4JrGw9xT5cetYa6ZijdH2iwnZaGz5WTxYVC_6QTIcizoAuUsapJAGtKgPH83gT9lzfvYOytdyMVw4OcfujG28dJTT3kuTMnCO5SYet3AJ7C6QTWoqavFaKm0G4DJKQU7fWiboYTZK2_O5vJX80b2-TM5P_q2eF84l0B5r0fcLgOhRNW3APh5VikGuh0UwYWQsuDzOM6xT-CTPlJ4VsnYTKacoPEYn3iBlzfTcAyI1oG6CrL4vBlpPQDNjXSuNCRLDZT-oDE4qhK3f0GxyMwH9fPtru2AbIgjxzf5V1KPDJedfIOug0u_H-rG7SLblWbNookOuKZMaDX4W7eMnl9htdcecnvhYgBdqVQ0m3cBV85gmyBz_X6mEi2nzvEK-m3NEV9tITnCg45EOofmuMCR74WSdX37F3DZSwDzuDusFqlQOu19pBc205Omp6AJd27gJUMQIX8oPlvSZrGiRS-6z0qX0ZHC1ohBMau8edOw09oP1AOtiixfq-NSqxS3or-qOu1kF644iSVWiDV_2RIv65wy8cGdMTf2getLLX04FVqzpwNov70OqH_CWyMab3nLbJzhhiGhTe_qHLFQLoTWxjkkTYR6V9Os2oRMo9Sk8lfDUbT7UrZAaYCcG07Kr7V9q0K3-D7EMaf6gpdkIUTrHeO9DK7Ws9x7dZLtC_S1xQP8f5x0B5iUWksPS3ETv1qglU2RQ4uE0RAdkQjCf4fzsk5uU3SjBIfmZsSu_gGwvELsNsBZYju96lzsHx5oZv47uVOqcvelQUUWRddFHu0gnCIY9ueTm_tv031C1TWIsqkciEp4IDuIxfOVY7ZkY5DFMLley_p-5o5DCYpLinPiplUGYNI8uZ1mHzMGV-OJv26-L59qtDr6Ptf7JNAZ7NLrXsKWCNm-nw2eAs5rfjZddlaaAHwy3wn6UaMFXAvbW2Jb-gdNDtJIv8MKsOTvjKYxkrBgjtFoUzdHcLP-ryBT7Y3TjK6BECe5rRIbFC1o6B6RoXDqKGu7DSDtdbnikwhxoHQjFCPU8YN648sWlAqhlTQ9cy3LgUA65XBiD1Yss39aJrSNrF-h2N_vpUm3eA69lVOgOqx7UeHmQOhMe6AgfIFeS7MPciUXvgTvpHedfEhlNtTJsTxGr1Eu0Fp9eDbSsAGYZXgG6wQ4RXNkO5USShBSXfMAdPSE5R6GRvDd7QayqDItBI0SoOzrUHNmZrv2k0MFixGZXgO619I872nfuUvd4TEA0944PfcXbKCBEotRnDPpPCSiZRg8F2nQCn2zK9oLtCftokAAbC8L4PJouji2nsKvwC-OHP8RhXgfVm9hqe-Cx2mYJxaBiE1PcM8gxA8lZGJEX869FzZ4rUZYdHItDs8tRly6RIbtL8ArSqpIsFepW5sDPLwqDZiA7mCfmbMQaNsR_VODbowvvbE7bCLNr0tmX_H7953JM0jB0ZHXev1PmTrw4CO-qxpB7MrsoYrBGpKwvBjAIEY1OL8YFhZg0WH-DXBURfAHUXGWPMqhCeuORU2_mm0sJbXjmUY2imzgQIi6sxvl2u37fD7mfU_r7DcM2hJz65Yp9P9FAhT1dN7-lgUeVaicxM68fHzl5nygq66XW1ohe8JqCml1pntl8gvgSVsc-aS4_yqPYOn7vH_Rdq1M2kr6FPauhXsyVx7a3Pjrll0jIoLs4C0Knklr0rENrTTbvk-16FU7S7EK6Qh8AqWPwOE4avnkcN97ENcy6m1iFbMEEBGJEKAHL4-dqFu1FcYoEl9K8Z8y3XHvaHzAYwDA4nGngz9-TV9EPb6t1uGc4g0vsbJxAHM4heBOMCsoVVfNQAsOHM9DMvx46m5LMVqrFKt3s_cLaAyOX-MiEuKOCXfrkJ0KIgUWQtSLel-PRsQ8ou4lFUD1EHe2SuZ9-6NkMf4taDf5QTDI6mglbJLf6Mz6c7x8eMQOM0VT-5UwOYZF6mqdUX2-bHBSXv6PXRrTsxRKzo66XaU2Uu0-T-QNfEhweGleLbR46BNXk6MhC2IlWeDvmySU1NlU6FKoaycDBkK41s1rhCKpzcRzHQjIyXiAW7ynVv_QglVhWDXhZl1ROdan4uvy9lt0eXB-iwavGuyOcT6kODfyp198TEBoGnsBuuiHMibPhLxYETJHkcZi-sFbTbdsG2tw3m65RgNnmzD8V3ZbfaI-64odOVgQ6ADDYsj3VIattyxg4ni2OWpgxSEyPz7hfKANFQ3CMmdX1FwfjXuWZpoI1PHQcLU9B8-lT-9O4wftprhVwm32ESJS8RsC4L8rp5HdDmo054ypiwgkhNBYK4eohE3Yd07elgmrcveMUVpOI3_ftUZOagX6UEGbyt3RymJZ71OEwtSVekrkCY1iqbyR8M4_4Zonz9m5j09SBe99NLxqJrIMfzAjThfidoD_2DzyTE8FeAZYCyg9-xVjAcvQf4dBcHXejCCGHQ7LD_kQG8poRZWurwS3Epq43gfUi_blxl0G1MONmUsEBOyUI-8Q8HJtx0HaNnM8UKnI8GyvwsvkcP1WYqKNa-XDD8Yvdb_PU76UjDOHNT0sAYX3F8TnDtcngjcgLujoaCcyyvvycm2ACnIHgr4qkL0fkX8-aLTxvN33aVVvULeOfB-fX4tk3WAoxPpPAQu7S7GKtpFE4vrJf6xDAywt1kmmlxUcaF0WwOypZ44saRS0siEWnxwPYWImVTMAcirV6CZHh_NiLRphadg7QBMIWMMxnLT7CMTjxo-ptaqR9vcyzynl1TntDSeNBkoUM-0ZEVDvVPrphuc6Pcpg4G3kxmc7Tw_kVWsSIQJR9dbJZTYO9cdfntSAnxt-H1c-rshhNIi-e5FGNcKQIxb_lFwIKiSeDsNv_xr7YebGGOn_uxSxgAZwxCuVd6iFu8KrBbwDJ_oktH2CVl8ykrFWxUWsmjfLW-T3nhHu4Dk5SCAhcaUDR-8O5SxdRssk-zJNzcSTd1JJUOEcaba9fW3OaxPtgWbbBnhMzj80w3qT083IXAiHFW-I87kSvcljo0bld_jkGkcvivQly4z-w2u-N6kWofkbEzG8wCrDKOUItAiUUB-9yNvcCVtBRZ0XaaxBExulGQhwsPETa6LG23Tm4gbPRiQgvvHA8mW5Ra8dKQ-6lAhCB_mLtrcw4OioQLOlMkLNPOQJQKkab67fcq2iDLJL7mMKDUDpEKBOdYlDWa7-NCIC6g55f3H6iuR4jQ9NHHbBAVjRn-060kQ2RBXIkGSGXSv8VcwXcOWL15LWEdNR25Zv3Uj3kf-3eVEiYdGOO2WDXxtKmBAlTzTOKgljWH0sUnkdB2sfC_ZGMbqxOFWrXAXUN4iUnFdJyvp9hlMNIb-cokwIMZh1glqvrDhvwlVOUL-3NaprZ_qCYSiRgZyKJjcLOZ_nukr-9PMRirjdRuQMD9qLjE4DKSSj2Ie2Wc-H3xGrLJDArnKt_b_2ozZtL_pQ0GRh-HosGJX8gZ6pulUsmrSN6CZrjFQpN_UUAzpPJBQziRML-6KoU1iZpaIH4oCp3mFlt-o2qxsV6uAeOR230UX9HScGkRtzgbIdY6Dc3RZkKKzzjfpL4A7sQPPm2sxr1lkwoWypoFv3NYl-pArVfrxCiqohcRN98q528P09j1ug_s8zYYbYZIwpNuvJA3XFARL6I9gf507JCNZzSvvCNpNGX4IUwPfgg2hds_9sT5MZbIH96be5wFAgDs1H7Kk-oBLtRU1MLkeKJ_dGpkjAnBJtwfJcKgl-OboUr7E74ukOsIl0nmCFLs9eZX8BBGRYSVnkNbnxFpwJYNOOE3HuaZ8Q76FE7_c0xQYxk7SDK8s2aD4iWCkH_gz2dHNyCXtlL-Le4kZ4YczODhqml1-o9ccj6qnIiMZPdzuxotk7bofDnMKLoSPZNtzha4_LmZ8UF68NZRwWO-VDaIEnPZxQYRRc2adUEUAG0KhcGb1LaL-_RwaLtFhcA5K7VzgDFFL4lV9_gNo2iZ0em49Gu9Wy8Q9U7r2wa4EZpt3k9PEl7CqvJOwqZwfa3UebXjYxJStCFrAZNUq9j4BB8a3ICiYwTwMwW1jfUpjo9q1jTYD2XW-LOZgg0WK07rZbAELjKWWE1rV4n0BIvUpwUq7c5YhhQ3BjbTROgcEZaf69AyJoeYyT17YcFmdj87LzsQ0vFswcxiez4B2EhAXXppmiyL6WZm1Epep-oTenLGG_t21hV4XjuZzdzNBmKyoVZUp9QXP2d-1u5MIqCWPsfiAHdqMD8zz5RbPIOq2rX2-JkfkZBEGWQT1PPa_rIVu9Ngqy_YF1z8qzfsc3KWUaohUGS4KC1aD-fRM2NXZf7aIQL1-waGXS-gO2l20-XFNZLO3r78L3myqPGa4vxgMoMbMIL0tP7IsPj6w5L6eoPbgLGxsjILqqQe33153qM79IQ9l48WtVHNQfRtW87NEdqkSO9dgO6fDSfF-vSHoQcwFl2U8Z0Mk7ZKb-HgRH-aFmeHrpa2qM-J7k_Q5ew9Z402J6WF7InJaufHX7DfZB2IYVgQQ7hKuHmx5Wj5GRnqxnIMACPtW8GvrgD9ARq3J_xJ8aohpv7MsH6pFtsgxPYhA2wokaW77A72NNVnxtj3C1W_bW5ewLXYGbZfamwXSNidox8BK7pY3LNiVfBJ3iioE2B3V68V2MZ8B5GFMP0ggwM9OfVQP5b97cYpoyxSBAPA-wnqRtftcxNBMJvGzEfa6OT7ZpyL6aMJkEzjcaWBgZiITqcr27dQsz8fbPcVgEsfBeYcOphodf_VndEWmxpEv-c_7WmH4pEC1Iw0QaNlOKwx7Adzq5NzDbLplxlxjB0uG28jRhVPu239tENv_s-2LHqVfyzMiGLtCRv9H1Rw8GJjiNfXeVZrKsQ1CpCxL6znaRx1GR3raf2bSO4Z85ot3CEGR_MP4s86q7PZAMv5us7Q6QFdT06EzDA4wyFG3ok_olaN0ksxJLVKbphBVHRf5Ci6ebe3fZrifJcb06Q4lTO4Aqje-mL6q2Pzg8-R5h3geKIXEfzra5dszM52uYf2mFS-UU9u_sKG_OgJpCP2pz6JLEiuyMfpy384HZMW4uTnrW3XZ3Lr8lSwwcqEI5piuofXZTZgL5ZTsFjH8gVHgNXCDlxT7i6UGEsEumbRbM-xhJvltUKRuGvj57l0rRLbH0qd1DAr2st_0PMEz6LQ6kYGOlOFkkZvRaM3wAoSkgprwHsVGfaAfE8AjejRV1Rcgaa5YFYN23xuOBkgpKslnHHwPYiwCihKxPlxDX9ZnLnRY5bQZJIJiXZfp_bZVBXdXNjzkvEkGltsa8P0DicHKOSYzybGn1Ut0qu9avoHWX6b9CPuMgr-nxrvwcqRF0sG-uTMZ7n_nE-dB2EGJSIZwutyWDkTtDqPphupLJeOvVVXAwx_3wVVn8wI2y_8lg3WnnPNY2VGtnu8A3dDfRmojRygEUD7rl84M7iwh2MIYIJkNpmhUAejd8kkfZB4EVBITdGyagl_XdEoEcHD6KwFKBWn3dsNrvkJaFfLJYJ9dI1PAp251Wbkp9JZ64pTzD9OwRT7m3Y0Ws2N2EVUUL-5yDv6enFqLHYSRzn0zDOaf9NoBFg3s-ZwRpqDXtXqksv5xz7zXSNm7zPoturwelvdpi0bdGuwRv4gY1kTeaGt_TfCn7HdgmYi4n-1zqwL7YhdbwqtxQAPAvY3exd4xyPeJD4DnlXdtNNgW9FnTxNDXmCYxth5A0tan4IVVHbfJShX6PeY7EQDy9wGm2qpabtzsZW5cFw2z9BFXlC1nAupktJVO5yu50nEuejtiDIRAEeBIWTpOynFCnodPZV3lKyQ4EzaE5Z7_z_IB90bYpiGbEONRqs0h5xK5gJ-SJlzvU4Cc98zXUaUvlVN773uEbFmf2yCN92lfA-KV7cfXpYMTD3uwgdc7lhApmO2lzdglpAASGltrXbW758WJY6tMKi56D2iv9lx8WzehHOq7Bv2rv7ksYWQbUr2GqaLv_-0s5YBW7_F4xpEDQv2Qel82Biyl8sZIVC-_cxaMRwMqmX2NuXZDlo_r2tf6h3nIY6N1xvl7rwvO5DbeaDeq9w9yPihhuheXe86Ge8G6CUPz256IVSIH6cvMrRYFRtZGCG3EHNrRYZ7yb4NTf-pob5juGRnyWQ71Z0mQSmiHO17ZoJ_vCqbcOqOsBCRpnvIT_WWffft-Ax-B-wo5H6I3Ifxl-peOyqye4We3_QvTH5kC9N4XRLA_Uff79UP4RTLc9XFsO-Lpo1x_qAK1ryrB7WSRnPF1yMMwM_79NNr2J4qCF3d2_5IpI47XImcHjIjII8JVf5ebyQA6Vl4TlgBlUE_CX4Qx5C5XmoJfDza-u4aXixrCJYMvquPc6MfVjiccmb9r04U4SnRq0fWnZ8wjPKApGK1cugJi-JW-bI3c9E9oXE4Hsrd_BKUu1oTP86cR5RZmsEm3roN1L_80lw2ISW_IcSGDIDqWF4s0Hxk0wwweKXNsvltMQKmnvT4svXbcndhnD-dEhrXYkCPV89GkO8dUC6-ehsX4PknC9Ae9XArkxbgJXxiKyq1mlkM2rQfgtjLgP5iDWjq4fQlXJ8og_BYoJIucv8pyOiaAVV16Lfk11GdegL1yue1601w45DW1zyuFhN-ncAw-d-cPhNaid1xkb1WiTQAfWIREFZzPBO8HGIWHiPCNXzTall3CIgwGTbIOV40B2Cq4vqHHTZPcmvK1MblmFfWp7xDUodJQ5G_IIFVCRxwFs8h1z3WNJzBQafqSs47Vqx583gfCzaIaxS6DbS23IomfNr2ZdsvFEsd3L2Jg-GC9-YOP30QUAWsEkpc6L_HC4R-_yI5-bhN9bkBVCpXRL-CRdCOoBrYISoEdme5x_CG1U5ez2En4AUAkq6Foed6h5OWIoXZ_yxTW_VyYueYkTXK07gloU_kFZpLwDTE89xiY-Q0hjYlvRu9q9KLWshD835iKpv-8sw2yrE2w4= \ No newline at end of file diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..4874e04 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,55 @@ +# Git +.git +.gitignore + +# Node.js +node_modules +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Build-Ausgabe (wird im Container neu gebaut) +dist + +# Test-Dateien +test +*.test.ts +*.spec.ts +coverage + +# IDE und Editor +.idea +.vscode +*.swp +*.swo +*~ + +# OS-spezifische Dateien +.DS_Store +Thumbs.db + +# Dokumentation (nicht im Container benötigt) +*.md +!README.md +docs + +# Beispiel-Workflows (werden als Volume gemountet) +workflows + +# Umgebungsvariablen +.env +.env.* + +# Archive +*.tar.gz +*.zip + +# Logs +*.log +logs + +# Temporäre Dateien +tmp +temp +.tmp +.cache diff --git a/.env.docker b/.env.docker new file mode 100644 index 0000000..47bd35c --- /dev/null +++ b/.env.docker @@ -0,0 +1,62 @@ +# Docker Umgebungsvariablen für n8n mit LibreBooking Node +# Kopiere diese Datei nach .env und passe die Werte an + +# ============================================ +# n8n Basis-Konfiguration +# ============================================ + +# Host und Port +N8N_HOST=localhost +N8N_PORT=5678 +N8N_PROTOCOL=http + +# Webhook URL (für externe Webhooks) +# Für Produktion: https://your-domain.com/ +WEBHOOK_URL=http://localhost:5678/ + +# ============================================ +# Authentifizierung (für Produktion aktivieren!) +# ============================================ + +N8N_BASIC_AUTH_ACTIVE=false +N8N_BASIC_AUTH_USER=admin +N8N_BASIC_AUTH_PASSWORD=changeme_secure_password + +# ============================================ +# Zeitzone +# ============================================ + +TZ=Europe/Berlin + +# ============================================ +# Logging +# ============================================ + +# Mögliche Werte: silent, error, warn, info, debug +N8N_LOG_LEVEL=info + +# ============================================ +# PostgreSQL (optional, für Produktion empfohlen) +# Aktivieren mit: docker-compose --profile with-postgres up -d +# ============================================ + +POSTGRES_USER=n8n +POSTGRES_PASSWORD=n8n_secure_password +POSTGRES_DB=n8n + +# Wenn PostgreSQL aktiv, diese Variable in docker-compose.yml hinzufügen: +# DB_TYPE=postgresdb +# DB_POSTGRESDB_HOST=postgres +# DB_POSTGRESDB_PORT=5432 +# DB_POSTGRESDB_DATABASE=${POSTGRES_DB} +# DB_POSTGRESDB_USER=${POSTGRES_USER} +# DB_POSTGRESDB_PASSWORD=${POSTGRES_PASSWORD} + +# ============================================ +# LibreBooking Konfiguration (Optional) +# Diese können auch direkt in n8n als Credentials angelegt werden +# ============================================ + +# LIBREBOOKING_URL=https://booking.example.com +# LIBREBOOKING_USER=api_user +# LIBREBOOKING_PASSWORD=api_password diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..71bdef4 --- /dev/null +++ b/.env.example @@ -0,0 +1,33 @@ +# n8n LibreBooking Node - Umgebungsvariablen +# +# Kopiere diese Datei nach .env und passe die Werte an: +# cp .env.example .env +# + +# n8n Authentifizierung +# WICHTIG: Ändere diese Werte für Produktion! +N8N_BASIC_AUTH_USER=admin +N8N_BASIC_AUTH_PASSWORD=changeme + +# Webhook-URL (für Produktion anpassen) +# Beispiel: https://n8n.deine-domain.de/ +WEBHOOK_URL=http://localhost:5678/ + +# Zeitzone +TZ=Europe/Berlin + +# Log-Level (debug, info, warn, error) +N8N_LOG_LEVEL=info + +# Optional: Datenbank (Standard: SQLite) +# DB_TYPE=postgresdb +# DB_POSTGRESDB_HOST=localhost +# DB_POSTGRESDB_PORT=5432 +# DB_POSTGRESDB_DATABASE=n8n +# DB_POSTGRESDB_USER=n8n +# DB_POSTGRESDB_PASSWORD=password + +# Optional: Executions +# EXECUTIONS_DATA_SAVE_ON_ERROR=all +# EXECUTIONS_DATA_SAVE_ON_SUCCESS=all +# EXECUTIONS_DATA_SAVE_MANUAL_EXECUTIONS=true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..80ccd10 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +node_modules/ +dist/ +.env +*.log + +# Generated PDFs +*.pdf diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..1a02188 --- /dev/null +++ b/.npmignore @@ -0,0 +1,70 @@ +# Source files (nur dist wird veröffentlicht) +*.ts +!*.d.ts +tsconfig.json + +# Git +.git +.gitignore +.gitattributes + +# Tests +test/ +*.test.ts +*.spec.ts +coverage/ +jest.config.js + +# Docker +Dockerfile +docker-compose.yml +.dockerignore + +# Entwicklung +.eslintrc.js +.eslintrc.json +.prettierrc +.prettierrc.json +.editorconfig +.vscode/ +.idea/ + +# Dokumentation (README bleibt) +CONTRIBUTING.md +CHANGELOG.md +INSTALLATION.md +SCHNELLSTART.md +ARCHIV-INFO.md +docs/ + +# Beispiele +workflows/ +examples/ + +# Skripte +install.sh +install.ps1 + +# OS-spezifisch +.DS_Store +Thumbs.db + +# Archive +*.tar.gz +*.zip + +# Logs und temp +*.log +logs/ +tmp/ +temp/ +.tmp/ +.cache/ + +# Umgebungsvariablen +.env +.env.* +!.env.example + +# node_modules (sowieso ignoriert, aber sicherheitshalber) +node_modules/ diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..5860d70 --- /dev/null +++ b/.npmrc @@ -0,0 +1,16 @@ +# npm Configuration für LibreBooking n8n Node +# Diese Datei konfiguriert npm für dieses Projekt + +# Audit-Warnungen deaktivieren +# Die Vulnerabilities kommen von n8n-workflow Dependencies und sind +# in diesem Kontext nicht kritisch (siehe SECURITY.md) +audit=false + +# Fund-Nachrichten deaktivieren +fund=false + +# Optional: Legacy Peer Dependencies (für ältere n8n Versionen) +# legacy-peer-deps=true + +# Optional: Engine-Strict deaktivieren +# engine-strict=false diff --git a/ARCHIV-INFO.md b/ARCHIV-INFO.md new file mode 100644 index 0000000..f6070b2 --- /dev/null +++ b/ARCHIV-INFO.md @@ -0,0 +1,123 @@ +# LibreBooking n8n Node - Archiv-Information + +Dieses Archiv enthält den vollständigen LibreBooking n8n Node. + +## Archiv entpacken + +### Linux/macOS + +```bash +# .tar.gz Archiv entpacken +tar -xzf n8n-nodes-librebooking.tar.gz +cd n8n-nodes-librebooking +``` + +### Windows + +```powershell +# .zip Archiv entpacken +Expand-Archive -Path n8n-nodes-librebooking.zip -DestinationPath . +cd n8n-nodes-librebooking +``` + +Oder: Rechtsklick → "Alle extrahieren..." + +## Installation + +### Schnellste Methode (Linux/Mac) + +```bash +chmod +x install.sh +./install.sh +n8n start +``` + +### Schnellste Methode (Windows) + +```powershell +Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass +.\install.ps1 +n8n start +``` + +### Mit Docker + +```bash +docker-compose up -d +# Browser öffnen: http://localhost:5678 +``` + +## Enthaltene Dateien + +``` +n8n-nodes-librebooking/ +├── credentials/ # API-Credentials Definition +├── nodes/ # Node-Implementierungen +│ ├── LibreBooking/ # Haupt-Node +│ └── LibreBookingTrigger/ # Trigger-Node +├── custom-nodes/ # Für Docker-Integration (eigenständig) +│ ├── credentials/ +│ ├── nodes/ +│ ├── package.json +│ └── README.md +├── workflows/ # Beispiel-Workflows +├── test/ # Test-Scripts +├── Dockerfile # Docker Image Definition +├── Dockerfile.custom-nodes # Für Custom Nodes Integration +├── docker-compose.yml # Docker Compose Konfiguration +├── docker-compose.override.yml # Override für bestehende Installationen +├── docker-compose.example.yml # Vollständiges Beispiel +├── install.sh # Installations-Skript (Linux/Mac) +├── install.ps1 # Installations-Skript (Windows) +├── install-docker.sh # Docker-Integration Skript +├── nginx.conf # Reverse Proxy Beispiel +├── .env.docker # Docker Umgebungsvariablen +├── package.json # npm Paket-Definition +├── tsconfig.json # TypeScript Konfiguration +├── README.md # Hauptdokumentation +├── INSTALLATION.md # Detaillierte Installationsanleitung +├── DOCKER-INTEGRATION.md # Docker-Integration Anleitung +├── SCHNELLSTART.md # Kurzanleitung +├── SCHNELLSTART-DOCKER.md # Docker Kurzanleitung +├── CHANGELOG.md # Versionshistorie +├── CONTRIBUTING.md # Entwickler-Anleitung +└── LICENSE # MIT Lizenz +``` + +## Docker-Integration (NEU) + +Für bestehende n8n Docker-Installationen: + +```bash +# Automatisch +./install-docker.sh -p /pfad/zu/n8n + +# Oder manuell +cp -r custom-nodes /pfad/zu/n8n/ +cd /pfad/zu/n8n/custom-nodes && npm install && npm run build +docker-compose restart n8n +``` + +📖 Siehe **DOCKER-INTEGRATION.md** für ausführliche Anleitung. + +## Dokumentation + +- **README.md** - Übersicht und Schnellstart +- **INSTALLATION.md** - Detaillierte Installationsanleitung +- **DOCKER-INTEGRATION.md** - Anleitung für bestehende Docker-Installationen +- **SCHNELLSTART.md** - Ultra-Kurzanleitung für Experten +- **SCHNELLSTART-DOCKER.md** - Docker-Kurzanleitung +- **CONTRIBUTING.md** - Anleitung für Entwickler + +## Support + +Bei Fragen oder Problemen: +- GitHub Issues: https://github.com/your-org/n8n-nodes-librebooking/issues + +## Lizenz + +MIT License - siehe LICENSE Datei + +--- + +*LibreBooking n8n Node v1.0.0* diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..e651f0c --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,35 @@ +# Changelog + +Alle wichtigen Änderungen werden hier dokumentiert. + +## [1.1.0] - 2026-01-25 + +### Geändert +- ⭐ **Vereinfachte Installation**: Fokus auf "auf dem Host bauen" +- Aktualisierte Dokumentation mit funktionierender Methode +- Neue npm scripts: `docker:deploy`, `docker:copy`, `docker:restart` + +### Hinzugefügt +- `quick-install.sh` - Ultra-einfache Installation +- `update-node.sh` - Für Updates +- `git-commit.sh` - Git Commit Helper +- `git-cleanup.sh` - Cleanup alter Dateien +- `create-release.sh` - Release-Erstellung +- `GIT-COMMANDS.md` - Git-Befehlsreferenz + +### Behoben +- TypeScript Installation Problem gelöst ("tsc not found") +- Read-only Volume Problem dokumentiert und gelöst +- npm audit Vulnerabilities dokumentiert + +## [1.0.0] - 2026-01-24 + +### Hinzugefügt +- Vollständige LibreBooking API Integration +- 8 Ressourcen: Reservierung, Ressource, Zeitplan, Benutzer, Konto, Gruppe, Zubehör, Attribut +- Trigger Node für neue/geänderte Reservierungen +- Docker Support mit docker-compose.yml +- Automatische Installationsskripte +- Umfangreiche Dokumentation auf Deutsch +- Beispiel-Workflows +- Test-Skripte diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..07bd3e9 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,238 @@ +# Beitragen zum LibreBooking n8n Node + +Vielen Dank für dein Interesse, zu diesem Projekt beizutragen! 🎉 + +## Inhaltsverzeichnis + +- [Code of Conduct](#code-of-conduct) +- [Wie kann ich beitragen?](#wie-kann-ich-beitragen) +- [Entwicklungsumgebung einrichten](#entwicklungsumgebung-einrichten) +- [Code-Richtlinien](#code-richtlinien) +- [Pull Request Prozess](#pull-request-prozess) +- [Bug Reports](#bug-reports) +- [Feature Requests](#feature-requests) + +## Code of Conduct + +Dieses Projekt folgt einem [Code of Conduct](CODE_OF_CONDUCT.md). Mit deiner Teilnahme erklärst du dich einverstanden, diesen einzuhalten. + +## Wie kann ich beitragen? + +### Bugs melden + +- Überprüfe zunächst, ob der Bug bereits gemeldet wurde +- Erstelle ein Issue mit einer klaren Beschreibung +- Füge Schritte zur Reproduktion hinzu +- Gib deine Umgebung an (OS, Node.js Version, n8n Version) + +### Features vorschlagen + +- Erstelle ein Issue mit dem Label "enhancement" +- Beschreibe den Use Case +- Erkläre, warum diese Funktion nützlich wäre + +### Code beitragen + +1. Forke das Repository +2. Erstelle einen Feature-Branch +3. Implementiere deine Änderungen +4. Schreibe Tests (falls möglich) +5. Erstelle einen Pull Request + +## Entwicklungsumgebung einrichten + +### Voraussetzungen + +- Node.js 18.x oder höher +- npm 8.x oder höher +- n8n (global installiert) +- Git + +### Setup + +```bash +# Repository klonen +git clone https://github.com/DEIN-REPO/n8n-nodes-librebooking.git +cd n8n-nodes-librebooking + +# Dependencies installieren +npm install + +# Build ausführen +npm run build + +# Für Entwicklung: Watch-Modus +npm run dev +``` + +### Lokales Testen + +```bash +# Node mit n8n verlinken +npm link + +# In n8n-Verzeichnis verlinken +cd $(npm root -g)/n8n +npm link n8n-nodes-librebooking + +# n8n starten +n8n start +``` + +### Mit Docker testen + +```bash +docker-compose up --build +``` + +## Code-Richtlinien + +### TypeScript + +- Verwende strenge Typisierung (`strict: true`) +- Vermeide `any` wo möglich +- Dokumentiere komplexe Funktionen mit JSDoc + +### Formatierung + +```bash +# Code formatieren +npm run format + +# Linting prüfen +npm run lint + +# Linting mit automatischer Korrektur +npm run lintfix +``` + +### Commit Messages + +Wir folgen [Conventional Commits](https://www.conventionalcommits.org/): + +``` +feat: Neue Funktion hinzugefügt +fix: Bug behoben +docs: Dokumentation aktualisiert +style: Formatierung geändert (kein Code) +refactor: Code umstrukturiert +test: Tests hinzugefügt/geändert +chore: Build-Prozess/Tools geändert +``` + +Beispiele: +``` +feat(reservation): Check-In Operation hinzugefügt +fix(auth): Session-Token wird jetzt korrekt erneuert +docs: Installationsanleitung aktualisiert +``` + +### Projektstruktur + +``` +n8n-nodes-librebooking/ +├── credentials/ # Credential-Definitionen +│ └── LibreBookingApi.credentials.ts +├── nodes/ # Node-Definitionen +│ ├── LibreBooking/ +│ │ ├── LibreBooking.node.ts +│ │ └── librebooking.svg +│ └── LibreBookingTrigger/ +│ ├── LibreBookingTrigger.node.ts +│ └── librebooking.svg +├── test/ # Tests +├── workflows/ # Beispiel-Workflows +├── dist/ # Kompilierte Dateien (generiert) +└── package.json +``` + +## Pull Request Prozess + +1. **Branch erstellen:** + ```bash + git checkout -b feature/meine-funktion + ``` + +2. **Änderungen implementieren:** + - Halte dich an die Code-Richtlinien + - Aktualisiere die Dokumentation + - Füge Tests hinzu (falls sinnvoll) + +3. **Testen:** + ```bash + npm run lint + npm run build + # Manuell in n8n testen + ``` + +4. **Commit und Push:** + ```bash + git add . + git commit -m "feat: Meine neue Funktion" + git push origin feature/meine-funktion + ``` + +5. **Pull Request erstellen:** + - Beschreibe deine Änderungen + - Referenziere relevante Issues + - Warte auf Review + +### PR Checkliste + +- [ ] Code folgt den Richtlinien +- [ ] Linting/Formatting bestanden +- [ ] Build erfolgreich +- [ ] Dokumentation aktualisiert +- [ ] CHANGELOG.md aktualisiert +- [ ] Keine Secrets/Credentials im Code + +## Bug Reports + +### Template + +```markdown +## Beschreibung +[Klare Beschreibung des Bugs] + +## Schritte zur Reproduktion +1. ... +2. ... +3. ... + +## Erwartetes Verhalten +[Was sollte passieren?] + +## Tatsächliches Verhalten +[Was passiert stattdessen?] + +## Umgebung +- OS: [z.B. Ubuntu 22.04] +- Node.js: [z.B. 20.10.0] +- n8n: [z.B. 1.20.0] +- LibreBooking: [z.B. 2.8.5] + +## Logs/Screenshots +[Falls vorhanden] +``` + +## Feature Requests + +### Template + +```markdown +## Beschreibung +[Beschreibe die gewünschte Funktion] + +## Use Case +[Warum wird diese Funktion benötigt?] + +## Vorgeschlagene Lösung +[Falls du eine Idee hast] + +## Alternativen +[Andere Möglichkeiten, die du in Betracht gezogen hast] +``` + +--- + +Vielen Dank für deinen Beitrag! 🙏 diff --git a/DOCKER-INTEGRATION.md b/DOCKER-INTEGRATION.md new file mode 100644 index 0000000..0bb0681 --- /dev/null +++ b/DOCKER-INTEGRATION.md @@ -0,0 +1,92 @@ +# Docker Integration + +## Empfohlene Methode: Auf dem Host bauen + +Die zuverlässigste Methode für Docker-Installationen. + +### Schritt-für-Schritt + +```bash +# 1. Auf dem Host +cd /pfad/zu/n8n-nodes-librebooking +npm install +npm run build + +# 2. In Container kopieren +docker cp dist n8n:/home/node/.n8n/custom/n8n-nodes-librebooking/ +docker cp package.json n8n:/home/node/.n8n/custom/n8n-nodes-librebooking/ +docker cp node_modules n8n:/home/node/.n8n/custom/n8n-nodes-librebooking/ + +# 3. Neustarten +docker restart n8n +``` + +### Mit Skript + +```bash +./quick-install.sh n8n +``` + +### Mit npm + +```bash +npm run docker:deploy +``` + +--- + +## docker-compose.yml Beispiel + +```yaml +version: '3.8' + +services: + n8n: + image: n8nio/n8n:latest + container_name: n8n + restart: unless-stopped + ports: + - "5678:5678" + environment: + - N8N_CUSTOM_EXTENSIONS=/home/node/.n8n/custom + - N8N_COMMUNITY_NODES_ENABLED=true + - TZ=Europe/Berlin + volumes: + - n8n_data:/home/node/.n8n + # Optional: Custom Nodes Verzeichnis + # - ./custom-nodes:/home/node/.n8n/custom + +volumes: + n8n_data: +``` + +--- + +## Für Updates + +```bash +./update-node.sh n8n + +# Oder +npm run docker:deploy +``` + +--- + +## Verifikation + +```bash +# Dateien prüfen +docker exec n8n ls -la /home/node/.n8n/custom/n8n-nodes-librebooking/ + +# Sollte zeigen: +# dist/ +# package.json +# node_modules/ +``` + +--- + +## Probleme? + +Siehe [TROUBLESHOOTING.md](TROUBLESHOOTING.md) diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..fa2c457 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,41 @@ +# Dockerfile für n8n mit LibreBooking Node +# Basiert auf dem offiziellen n8n Docker Image + +FROM n8nio/n8n:latest + +# Als Root-Benutzer für Installation +USER root + +# Arbeitsverzeichnis für den Custom Node +WORKDIR /home/node/.n8n/custom + +# Kopiere Node-Dateien +COPY package*.json ./ +COPY tsconfig.json ./ +COPY index.ts ./ +COPY credentials/ ./credentials/ +COPY nodes/ ./nodes/ + +# Installiere Dependencies und baue den Node +RUN npm install && \ + npm run build && \ + chown -R node:node /home/node/.n8n + +# Zurück zum node-Benutzer +USER node + +# Arbeitsverzeichnis auf n8n Standard setzen +WORKDIR /home/node + +# n8n wird automatisch den Custom Node laden +ENV N8N_CUSTOM_EXTENSIONS="/home/node/.n8n/custom" + +# Healthcheck +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ + CMD wget -q --spider http://localhost:5678/healthz || exit 1 + +# Standard n8n Port +EXPOSE 5678 + +# Startbefehl +CMD ["n8n", "start"] diff --git a/Dockerfile.custom-nodes b/Dockerfile.custom-nodes new file mode 100644 index 0000000..1463d62 --- /dev/null +++ b/Dockerfile.custom-nodes @@ -0,0 +1,48 @@ +# Dockerfile für Custom Nodes Integration +# Verwendet das offizielle n8n Image und fügt den LibreBooking Node hinzu +# +# Build: docker build -f Dockerfile.custom-nodes -t n8n-librebooking . +# Run: docker run -p 5678:5678 n8n-librebooking + +ARG N8N_VERSION=latest +FROM n8nio/n8n:${N8N_VERSION} + +# Wechsle zu root für Installationen +USER root + +# Erstelle Custom Nodes Verzeichnis +RUN mkdir -p /home/node/.n8n/custom/n8n-nodes-librebooking && \ + chown -R node:node /home/node/.n8n/custom + +# Arbeitsverzeichnis setzen +WORKDIR /home/node/.n8n/custom/n8n-nodes-librebooking + +# Kopiere Custom Node Dateien +COPY --chown=node:node custom-nodes/package.json . +COPY --chown=node:node custom-nodes/tsconfig.json . +COPY --chown=node:node custom-nodes/index.ts . +COPY --chown=node:node custom-nodes/credentials/ ./credentials/ +COPY --chown=node:node custom-nodes/nodes/ ./nodes/ + +# Installiere Dependencies und baue den Node +RUN npm install && npm run build + +# Wechsle zurück zum node User +USER node + +# Arbeitsverzeichnis für n8n setzen +WORKDIR /home/node + +# Umgebungsvariablen +ENV N8N_CUSTOM_EXTENSIONS=/home/node/.n8n/custom \ + N8N_COMMUNITY_NODES_ENABLED=true + +# n8n Port +EXPOSE 5678 + +# Healthcheck +HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \ + CMD wget -qO- http://localhost:5678/healthz || exit 1 + +# Startbefehl +CMD ["n8n", "start"] diff --git a/GIT-COMMANDS.md b/GIT-COMMANDS.md new file mode 100644 index 0000000..d5599be --- /dev/null +++ b/GIT-COMMANDS.md @@ -0,0 +1,65 @@ +# Git-Befehle für LibreBooking n8n Node + +## Schnellbefehle + +```bash +# Alle Änderungen committen +git add . +git commit -m "fix: Vereinfachte Installation" + +# Push +git push origin main +``` + +## Release erstellen + +```bash +# 1. Version in package.json anpassen (z.B. 1.1.0) + +# 2. Alte Archive löschen +rm ../n8n-nodes-librebooking*.tar.gz +rm ../n8n-nodes-librebooking*.zip + +# 3. Neue Archive erstellen +git archive --format=tar.gz --prefix=n8n-nodes-librebooking/ --output=../n8n-nodes-librebooking-v1.1.0.tar.gz HEAD +git archive --format=zip --prefix=n8n-nodes-librebooking/ --output=../n8n-nodes-librebooking-v1.1.0.zip HEAD + +# 4. Tag erstellen +git tag -a v1.1.0 -m "Version 1.1.0 - Vereinfachte Installation" + +# 5. Push +git push origin main +git push origin v1.1.0 +``` + +## Mit Skripten + +```bash +# Commit +./git-commit.sh "fix: Beschreibung der Änderung" + +# Cleanup +./git-cleanup.sh + +# Release +./create-release.sh +``` + +## Nützliche Befehle + +```bash +# Status anzeigen +git status + +# Änderungen anzeigen +git diff + +# Log anzeigen +git log --oneline -10 + +# Tags anzeigen +git tag -l + +# Remote anzeigen +git remote -v +``` diff --git a/INSTALLATION.md b/INSTALLATION.md new file mode 100644 index 0000000..e225a3c --- /dev/null +++ b/INSTALLATION.md @@ -0,0 +1,101 @@ +# Installation + +## Methode 1: Auf dem Host bauen (EMPFOHLEN) ⭐ + +Die zuverlässigste Methode für Docker-Installationen. + +### Voraussetzungen + +- Node.js 18+ +- npm +- Docker + +### Installation + +```bash +# 1. Klonen +git clone https://github.com/your-org/n8n-nodes-librebooking.git +cd n8n-nodes-librebooking + +# 2. Dependencies installieren +npm install + +# 3. Bauen +npm run build + +# 4. In Container kopieren +docker cp dist n8n:/home/node/.n8n/custom/n8n-nodes-librebooking/ +docker cp package.json n8n:/home/node/.n8n/custom/n8n-nodes-librebooking/ +docker cp node_modules n8n:/home/node/.n8n/custom/n8n-nodes-librebooking/ + +# 5. Container neustarten +docker restart n8n +``` + +### Mit Skript + +```bash +./quick-install.sh n8n +``` + +### Mit npm scripts + +```bash +npm install +npm run docker:deploy +``` + +--- + +## Methode 2: Docker mit vorgebautem dist/ + +Für Read-only Volumes. + +```bash +# 1. Bauen +npm install +npm run build + +# 2. Kopieren +cp -r dist package.json node_modules /pfad/zu/custom-nodes/n8n-nodes-librebooking/ + +# 3. docker-compose.yml +volumes: + - ./custom-nodes:/home/node/.n8n/custom:ro # Read-only möglich! +``` + +--- + +## Methode 3: Native Installation (ohne Docker) + +```bash +# 1. Installieren +npm install +npm run build +npm link + +# 2. n8n starten +n8n start +``` + +--- + +## Verifikation + +Nach der Installation: + +1. Öffne n8n: http://localhost:5678 +2. Erstelle neuen Workflow +3. Suche nach "LibreBooking" +4. Wenn der Node erscheint → ✅ Installation erfolgreich + +## Deinstallation + +```bash +# Docker +docker exec n8n rm -rf /home/node/.n8n/custom/n8n-nodes-librebooking +docker restart n8n + +# Native +npm unlink -g n8n-nodes-librebooking +``` diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..840fafe --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 LibreBooking n8n Node Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/MANUELLE-INSTALLATION-CONTAINER.md b/MANUELLE-INSTALLATION-CONTAINER.md new file mode 100644 index 0000000..559b94e --- /dev/null +++ b/MANUELLE-INSTALLATION-CONTAINER.md @@ -0,0 +1,328 @@ +# Manuelle Installation im Docker Container + +Diese Anleitung beschreibt, wie Sie den LibreBooking Node manuell im Docker Container installieren, wenn die Dateien bereits kopiert wurden, aber der Node nicht in n8n erscheint. + +--- + +## Situation + +Sie haben die Dateien in den Container kopiert (z.B. nach `/opt/n8n/custom-nodes` oder `/home/node/.n8n/custom/n8n-nodes-librebooking`), aber: + +- Der LibreBooking Node erscheint nicht in der n8n Node-Suche +- Die TypeScript-Dateien wurden nicht kompiliert +- Das `dist/` Verzeichnis fehlt oder ist leer + +--- + +## Diagnose + +### Schnell-Check mit einem Befehl + +```bash +# Prüft Dateien und Status im Container +docker exec n8n sh -c "ls -la /home/node/.n8n/custom/*/dist/ 2>/dev/null || echo 'dist/ nicht gefunden'" +``` + +### Ausführlicher Check mit Skript + +```bash +# Check-Skript in Container kopieren und ausführen +docker cp check-installation.sh n8n:/tmp/ +docker exec n8n sh /tmp/check-installation.sh +``` + +### Manuell im Container prüfen + +```bash +# In den Container einloggen +docker exec -it n8n sh + +# Prüfen, was vorhanden ist +ls -la /home/node/.n8n/custom/ +ls -la /home/node/.n8n/custom/n8n-nodes-librebooking/ + +# Umgebungsvariablen prüfen +env | grep N8N + +# Container verlassen +exit +``` + +--- + +## Lösung 1: Automatisch mit Skript (empfohlen) + +Das einfachste ist das All-in-One-Skript: + +```bash +# Auf dem Host ausführen +./fix-node-installation.sh +``` + +Oder mit spezifischem Container-Namen: + +```bash +./fix-node-installation.sh -c mein-n8n-container +``` + +### Alternative: Nur das Installations-Skript + +```bash +# Skript in Container kopieren +docker cp install-in-container.sh n8n:/tmp/ + +# Skript im Container ausführen +docker exec -it n8n sh /tmp/install-in-container.sh + +# Container neustarten +docker restart n8n +``` + +--- + +## Lösung 2: Manuelle Schritte im Container + +### Schritt 1: In den Container einloggen + +```bash +docker exec -it n8n sh +``` + +### Schritt 2: Zum Custom-Node-Verzeichnis wechseln + +```bash +# Standard-Pfad (offizielles n8n Image) +cd /home/node/.n8n/custom/n8n-nodes-librebooking + +# Oder falls an anderem Ort: +cd /opt/n8n/custom-nodes +# oder +cd /data/custom-nodes +``` + +### Schritt 3: Prüfen ob Dateien vorhanden sind + +```bash +ls -la +# Sollte zeigen: package.json, tsconfig.json, nodes/, credentials/ +``` + +### Schritt 4: Dependencies installieren + +```bash +npm install +``` + +Erwartete Ausgabe: +``` +added 50 packages in 10s +``` + +### Schritt 5: Node kompilieren + +```bash +npm run build +``` + +Erwartete Ausgabe: +``` +> n8n-nodes-librebooking@1.0.0 build +> tsc && npm run copy:icons +``` + +### Schritt 6: Prüfen ob Build erfolgreich war + +```bash +ls -la dist/ +ls -la dist/nodes/LibreBooking/ +``` + +Sollte zeigen: +- `dist/nodes/LibreBooking/LibreBooking.node.js` +- `dist/nodes/LibreBookingTrigger/LibreBookingTrigger.node.js` +- `dist/credentials/LibreBookingApi.credentials.js` + +### Schritt 7: Container verlassen und neustarten + +```bash +# Container verlassen +exit + +# Container neustarten (auf dem Host) +docker restart n8n +``` + +--- + +## Lösung 3: Direkt mit docker exec (Ein-Befehl-Lösung) + +Wenn Sie schnell zum Ziel kommen wollen: + +```bash +# Alles in einem Befehl +docker exec n8n sh -c "cd /home/node/.n8n/custom/n8n-nodes-librebooking && npm install && npm run build" + +# Container neustarten +docker restart n8n +``` + +Für anderen Pfad: + +```bash +docker exec n8n sh -c "cd /opt/n8n/custom-nodes && npm install && npm run build" +docker restart n8n +``` + +--- + +## Verifizierung + +### 1. Logs prüfen + +```bash +# Nach LibreBooking in den Logs suchen +docker logs n8n 2>&1 | grep -i "librebooking\|custom\|node" +``` + +### 2. In n8n prüfen + +1. Öffnen Sie n8n im Browser (z.B. `http://localhost:5678`) +2. Erstellen Sie einen neuen Workflow oder öffnen Sie einen bestehenden +3. Klicken Sie auf `+` um einen neuen Node hinzuzufügen +4. Suchen Sie nach "LibreBooking" +5. Es sollten zwei Nodes erscheinen: + - **LibreBooking** - Hauptnode für alle Operationen + - **LibreBooking Trigger** - Trigger für Reservierungen + +### 3. Node-Dateien im Container prüfen + +```bash +# Prüfe ob .node.js Dateien existieren +docker exec n8n find /home/node/.n8n/custom -name "*.node.js" 2>/dev/null +``` + +--- + +## Troubleshooting + +### Problem: npm nicht gefunden + +**Symptom:** +``` +sh: npm: not found +``` + +**Lösung:** +Das verwendete Docker-Image enthält kein npm. Verwenden Sie ein Image mit Node.js: + +```bash +# Prüfen Sie das Image +docker inspect n8n --format='{{.Config.Image}}' + +# Das offizielle n8n Image (n8nio/n8n) enthält npm +# Falls Sie ein minimales Image verwenden, wechseln Sie zu n8nio/n8n +``` + +### Problem: Permission denied + +**Symptom:** +``` +EACCES: permission denied +npm ERR! could not create a lockfile +``` + +**Lösung:** +n8n läuft als User `node` (UID 1000). Setzen Sie die Berechtigungen: + +```bash +# Auf dem Host (vor dem Kopieren) +sudo chown -R 1000:1000 custom-nodes/ + +# Oder im Container als root +docker exec -u root n8n chown -R node:node /home/node/.n8n/custom/ +``` + +### Problem: Build schlägt fehl + +**Symptom:** +``` +error TS2307: Cannot find module 'n8n-workflow' +``` + +**Lösung:** +```bash +# Im Container +cd /home/node/.n8n/custom/n8n-nodes-librebooking +rm -rf node_modules package-lock.json +npm install +npm run build +``` + +### Problem: Node erscheint nach Neustart immer noch nicht + +**Mögliche Ursachen:** + +1. **Falscher Pfad**: N8N_CUSTOM_EXTENSIONS zeigt nicht auf das richtige Verzeichnis + ```bash + docker exec n8n env | grep N8N_CUSTOM + # Sollte zeigen: N8N_CUSTOM_EXTENSIONS=/home/node/.n8n/custom + ``` + +2. **package.json n8n-Sektion fehlt**: Die `n8n` Konfiguration in package.json muss korrekt sein + ```bash + docker exec n8n cat /home/node/.n8n/custom/n8n-nodes-librebooking/package.json | grep -A10 '"n8n"' + ``` + +3. **Container-Cache**: Container komplett neu erstellen + ```bash + docker compose down + docker compose up -d + ``` + +4. **n8n Version zu alt**: Der Node benötigt n8n >= 1.0.0 + ```bash + docker exec n8n n8n --version + ``` + +### Problem: Fehler bei npm install (Netzwerk) + +**Symptom:** +``` +npm ERR! network request failed +``` + +**Lösung:** +```bash +# DNS prüfen +docker exec n8n cat /etc/resolv.conf + +# npm Registry prüfen +docker exec n8n npm config get registry + +# Ggf. Registry setzen +docker exec n8n npm config set registry https://registry.npmjs.org/ +``` + +--- + +## Schnellreferenz + +| Aktion | Befehl | +|--------|--------| +| Status prüfen | `docker exec n8n sh /tmp/check-installation.sh` | +| Installieren | `docker exec n8n sh -c "cd /home/node/.n8n/custom/n8n-nodes-librebooking && npm install && npm run build"` | +| Neustarten | `docker restart n8n` | +| Logs prüfen | `docker logs n8n 2>&1 \| grep -i libre` | +| Im Container | `docker exec -it n8n sh` | + +--- + +## Weiterführende Dokumentation + +- [DOCKER-INTEGRATION.md](DOCKER-INTEGRATION.md) - Vollständige Docker-Anleitung +- [TROUBLESHOOTING.md](TROUBLESHOOTING.md) - Alle Troubleshooting-Themen +- [SCHNELLSTART-DOCKER.md](SCHNELLSTART-DOCKER.md) - Docker-Schnellstart + +--- + +*Letzte Aktualisierung: Januar 2026* diff --git a/README.md b/README.md new file mode 100644 index 0000000..ef4abac --- /dev/null +++ b/README.md @@ -0,0 +1,102 @@ +# LibreBooking n8n Node + +Integration von LibreBooking in n8n für automatisierte Reservierungs- und Ressourcenverwaltung. + +## ⚡ Schnellstart (EMPFOHLEN) + +**Die einfachste Methode: Auf dem Host bauen, in Docker kopieren** + +```bash +# 1. Repository klonen +git clone https://github.com/your-org/n8n-nodes-librebooking.git +cd n8n-nodes-librebooking + +# 2. Bauen und installieren +./quick-install.sh n8n + +# Fertig! ✔ +``` + +**Oder manuell:** + +```bash +# Dependencies & Build +npm install +npm run build + +# In Container kopieren +docker cp dist n8n:/home/node/.n8n/custom/n8n-nodes-librebooking/ +docker cp package.json n8n:/home/node/.n8n/custom/n8n-nodes-librebooking/ +docker cp node_modules n8n:/home/node/.n8n/custom/n8n-nodes-librebooking/ + +# Container neustarten +docker restart n8n +``` + +## 🛠️ npm Scripts + +```bash +npm run build # Baut den Node +npm run docker:deploy # Baut, kopiert & startet Container neu +npm run docker:copy # Kopiert in Container +npm run docker:restart # Startet Container neu +``` + +## 📚 Dokumentation + +- **[INSTALLATION.md](INSTALLATION.md)** - Alle Installationsmethoden +- **[SCHNELLSTART.md](SCHNELLSTART.md)** - Ultra-kurze Anleitung +- **[TROUBLESHOOTING.md](TROUBLESHOOTING.md)** - Problemlösung +- **[DOCKER-INTEGRATION.md](DOCKER-INTEGRATION.md)** - Docker-spezifische Anleitung + +## 🔑 Credentials einrichten + +1. Öffne n8n: http://localhost:5678 +2. Gehe zu: **Einstellungen** → **Credentials** → **Add Credential** +3. Suche: **LibreBooking API** +4. Eingabe: + - **URL**: `https://deine-librebooking-url.de` + - **Benutzername**: Admin-Benutzer + - **Passwort**: Passwort + +## 🌟 Features + +### LibreBooking Node +- Reservierungen erstellen, bearbeiten, löschen +- Ressourcen und Verfügbarkeit verwalten +- Benutzer und Gruppen administrieren +- Zeitpläne und Zubehör konfigurieren + +### LibreBooking Trigger Node +- Neue Reservierungen überwachen +- Geänderte Reservierungen erfassen +- Filter nach Ressource/Zeitplan/Benutzer + +## 🔄 Updates + +```bash +# Nach Änderungen oder git pull +./update-node.sh n8n + +# Oder mit npm +npm run docker:deploy +``` + +## ❓ Problemlösung + +### tsc not found? +→ **Lösung**: Auf dem Host bauen (siehe Schnellstart) + +### Read-only Volume? +→ **Lösung**: dist/ in Container kopieren statt npm im Container + +### npm audit Vulnerabilities? +→ Sind non-critical Dependencies von n8n-workflow. Siehe [SECURITY.md](SECURITY.md) + +## 📄 Lizenz + +MIT - Siehe [LICENSE](LICENSE) + +## 🤝 Beitragen + +Beiträge sind willkommen! Siehe [CONTRIBUTING.md](CONTRIBUTING.md) diff --git a/SCHNELLSTART-DOCKER.md b/SCHNELLSTART-DOCKER.md new file mode 100644 index 0000000..7092986 --- /dev/null +++ b/SCHNELLSTART-DOCKER.md @@ -0,0 +1,147 @@ +# Docker Schnellstart - LibreBooking n8n Node + +Schnelle Befehle für erfahrene Docker-Benutzer. + +--- + +## Neue Installation + +### Option A: Mit docker-compose (empfohlen) + +```bash +# 1. Starten +cd /pfad/zu/librebooking_n8n_node +docker compose up -d + +# 2. Browser öffnen +open http://localhost:5678 +``` + +### Option B: In bestehende n8n Installation integrieren + +```bash +# 1. Custom Nodes kopieren +cp -r custom-nodes /pfad/zu/n8n/ + +# 2. Bauen +cd /pfad/zu/n8n/custom-nodes +npm install && npm run build + +# 3. docker-compose.override.yml erstellen +cat > docker-compose.override.yml << 'EOF' +version: '3.8' +services: + n8n: + volumes: + - ./custom-nodes:/home/node/.n8n/custom/n8n-nodes-librebooking:ro + environment: + - N8N_CUSTOM_EXTENSIONS=/home/node/.n8n/custom + - N8N_COMMUNITY_NODES_ENABLED=true +EOF + +# 4. Neustarten +docker compose restart n8n +``` + +--- + +## Node im Container bauen/reparieren + +### Quick-Fix (Ein Befehl) + +```bash +docker exec n8n sh -c "cd /home/node/.n8n/custom/n8n-nodes-librebooking && npm install && npm run build" && docker restart n8n +``` + +### Mit Auto-Fix Skript + +```bash +./fix-node-installation.sh +``` + +### Status prüfen + +```bash +# Check-Skript ausführen +docker cp check-installation.sh n8n:/tmp/ +docker exec n8n sh /tmp/check-installation.sh +``` + +--- + +## Häufige Befehle + +| Aktion | Befehl | +|--------|--------| +| Container starten | `docker compose up -d` | +| Container stoppen | `docker compose down` | +| Container neustarten | `docker restart n8n` | +| Logs anzeigen | `docker logs -f n8n` | +| In Container einloggen | `docker exec -it n8n sh` | +| Node bauen | `docker exec n8n sh -c "cd /home/node/.n8n/custom/n8n-nodes-librebooking && npm install && npm run build"` | +| Status prüfen | `docker exec n8n sh /tmp/check-installation.sh` | + +--- + +## Pfade im Container + +| Beschreibung | Pfad | +|--------------|------| +| n8n Home | `/home/node/.n8n` | +| Custom Nodes | `/home/node/.n8n/custom` | +| LibreBooking Node | `/home/node/.n8n/custom/n8n-nodes-librebooking` | +| Daten-Verzeichnis | `/home/node/.n8n` | + +--- + +## Umgebungsvariablen + +```yaml +environment: + - N8N_CUSTOM_EXTENSIONS=/home/node/.n8n/custom + - N8N_COMMUNITY_NODES_ENABLED=true + - N8N_LOG_LEVEL=info +``` + +--- + +## Troubleshooting + +### Node erscheint nicht + +```bash +# 1. Build prüfen +docker exec n8n ls /home/node/.n8n/custom/n8n-nodes-librebooking/dist/ + +# 2. Falls leer - neu bauen +docker exec n8n sh -c "cd /home/node/.n8n/custom/n8n-nodes-librebooking && npm install && npm run build" + +# 3. Neustarten +docker restart n8n +``` + +### Permission denied + +```bash +sudo chown -R 1000:1000 custom-nodes/ +``` + +### docker-compose: distutils Fehler (Python 3.12) + +```bash +# Lösung: Docker Compose v2 verwenden +sudo apt-get install docker-compose-plugin +docker compose up -d # Beachte: ohne Bindestrich +``` + +--- + +## Detaillierte Anleitungen + +- [DOCKER-INTEGRATION.md](DOCKER-INTEGRATION.md) - Vollständige Docker-Dokumentation +- [MANUELLE-INSTALLATION-CONTAINER.md](MANUELLE-INSTALLATION-CONTAINER.md) - Manuelle Installation +- [TROUBLESHOOTING.md](TROUBLESHOOTING.md) - Alle Probleme und Lösungen + +--- + +*Schnellstart-Guide für Docker-Profis* diff --git a/SCHNELLSTART.md b/SCHNELLSTART.md new file mode 100644 index 0000000..28b3f17 --- /dev/null +++ b/SCHNELLSTART.md @@ -0,0 +1,40 @@ +# Schnellstart + +## ⚡ Installation in 4 Zeilen + +```bash +cd /pfad/zu/n8n-nodes-librebooking +npm install +npm run build +./quick-install.sh n8n +``` + +**Oder mit npm scripts:** + +```bash +npm install +npm run docker:deploy +``` + +## 🔑 Credentials + +n8n → Einstellungen → Credentials → Add → "LibreBooking API" + +| Feld | Wert | +|------|------| +| URL | `https://deine-librebooking-url.de` | +| Benutzername | Admin-Benutzer | +| Passwort | Passwort | + +## 🔄 Updates + +```bash +git pull +./update-node.sh n8n +``` + +## ❓ Probleme? + +- **tsc not found**: Auf dem Host bauen (npm install && npm run build) +- **Read-only Volume**: `docker cp` verwenden +- Mehr: [TROUBLESHOOTING.md](TROUBLESHOOTING.md) diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..8b1cbbd --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,234 @@ +# Sicherheitshinweise - LibreBooking n8n Node + +Dieses Dokument erklärt die npm audit Vulnerabilities und wie man damit umgeht. + +## Inhaltsverzeichnis + +- [Übersicht der Vulnerabilities](#übersicht-der-vulnerabilities) +- [Warum diese Vulnerabilities existieren](#warum-diese-vulnerabilities-existieren) +- [Risikoeinschätzung](#risikoeinschätzung) +- [Empfehlungen](#empfehlungen) +- [Wie man sie beheben kann](#wie-man-sie-beheben-kann) +- [Produktionsumgebungen](#produktionsumgebungen) + +--- + +## Übersicht der Vulnerabilities + +Beim Ausführen von `npm audit` werden möglicherweise folgende Vulnerabilities angezeigt: + +### Critical: form-data + +``` +form-data <4.0.1 +Severity: critical +Prototype Pollution in form-data +https://github.com/advisories/GHSA-xxx +``` + +### Moderate: lodash + +``` +lodash <4.17.21 +Severity: moderate +Prototype Pollution in lodash +https://github.com/advisories/GHSA-xxx +``` + +--- + +## Warum diese Vulnerabilities existieren + +Diese Vulnerabilities kommen **nicht direkt aus diesem Projekt**, sondern sind **transitive Dependencies** von `n8n-workflow` und `n8n-core`. + +### Dependency-Kette: + +``` +n8n-nodes-librebooking + └── n8n-workflow (devDependency für Typen) + └── axios + └── form-data (vulnerable version) + └── lodash (vulnerable version) +``` + +### Wichtig zu verstehen: + +1. **n8n-workflow** ist nur als `devDependency` und `peerDependency` deklariert +2. In Produktion verwendet n8n seine **eigene** n8n-workflow Version +3. Die vulnerable Dependencies werden nur beim **Entwickeln** installiert +4. Diese Package werden **nicht** in das finale dist/ Verzeichnis gebündelt + +--- + +## Risikoeinschätzung + +### Für dieses Projekt: **NIEDRIGES RISIKO** + +| Aspekt | Risiko | Begründung | +|--------|--------|------------| +| Entwicklung | Niedrig | form-data/lodash werden nicht direkt verwendet | +| Produktion | Sehr niedrig | Keine transtiven Dependencies werden deployed | +| n8n Runtime | Abhängig von n8n | n8n selbst muss die Vulnerabilities beheben | + +### Warum niedriges Risiko: + +1. **form-data Vulnerability:** + - Betrifft nur das Parsen von multipart/form-data + - Dieser Node verwendet keine File-Uploads über form-data + - Die LibreBooking API verwendet JSON für alle Requests + +2. **lodash Vulnerability:** + - Betrifft `_.set()` und `_.setWith()` Funktionen + - Dieser Node verwendet keine direkten lodash Aufrufe + - Die Vulnerability erfordert Angreifer-kontrollierten Input + +--- + +## Empfehlungen + +### Für Entwickler: + +1. **Warnungen ignorieren** (wenn nicht kritisch): + ```bash + npm install --ignore-scripts + ``` + +2. **Audit bei npm install deaktivieren:** + ```bash + # Einmalig: + npm install --no-audit + + # Permanent via .npmrc: + echo "audit=false" >> .npmrc + ``` + +3. **Overrides verwenden** (in package.json): + ```json + "overrides": { + "form-data": "^4.0.1", + "lodash": "^4.17.21" + } + ``` + +### Für Produktionsumgebungen: + +1. **n8n aktuell halten:** Die n8n-Entwickler aktualisieren regelmäßig ihre Dependencies +2. **Nur vertrauenswürdige Inputs:** Keine ungeprüften Daten an die Nodes übergeben +3. **Network Isolation:** n8n Container im isolierten Netzwerk betreiben + +--- + +## Wie man sie beheben kann + +### Option 1: Overrides in package.json (empfohlen) + +Die package.json enthält bereits Overrides für bekannte Vulnerabilities: + +```json +"overrides": { + "form-data": "^4.0.1", + "lodash": "^4.17.21" +} +``` + +### Option 2: npm audit fix (begrenzt) + +```bash +# Automatische Fixes (nur kompatible Updates) +npm audit fix + +# Force Fixes (VORSICHT: kann Breaking Changes einführen) +npm audit fix --force +``` + +**Hinweis:** `npm audit fix` kann transitive Dependencies nur begrenzt beheben. + +### Option 3: Update-Skript verwenden + +```bash +./update-dependencies.sh +``` + +Das Skript: +- Führt `npm audit fix` aus +- Aktualisiert alle Dependencies +- Testet ob alles funktioniert +- Gibt einen Report + +### Option 4: Resolutions (für yarn/pnpm) + +Wenn Sie yarn statt npm verwenden: + +```json +"resolutions": { + "form-data": "^4.0.1", + "lodash": "^4.17.21" +} +``` + +--- + +## Produktionsumgebungen + +### Best Practices: + +1. **Docker Image aktuell halten:** + ```bash + docker pull n8nio/n8n:latest + ``` + +2. **Regelmäßige Updates:** + ```bash + docker compose pull + docker compose up -d + ``` + +3. **Security Scanning:** + ```bash + # Image auf Vulnerabilities prüfen + docker scan n8nio/n8n:latest + # Oder mit Trivy: + trivy image n8nio/n8n:latest + ``` + +4. **Netzwerk-Isolation:** + - n8n nicht direkt im Internet exponieren + - Reverse Proxy mit TLS verwenden + - Firewall-Regeln setzen + +5. **Zugriffskontrollen:** + - Starke Passwörter verwenden + - Basic Auth oder OAuth aktivieren + - API-Keys für LibreBooking sicher speichern + +### Sicherheits-Checkliste: + +- [ ] n8n Version aktuell? +- [ ] Docker Image aktuell? +- [ ] TLS/HTTPS aktiviert? +- [ ] Starke Passwörter? +- [ ] Netzwerk isoliert? +- [ ] Regelmäßige Backups? + +--- + +## Weiterführende Links + +- [n8n Security Best Practices](https://docs.n8n.io/hosting/security/) +- [npm audit Documentation](https://docs.npmjs.com/cli/v8/commands/npm-audit) +- [OWASP Dependency Check](https://owasp.org/www-project-dependency-check/) +- [Snyk Vulnerability Database](https://snyk.io/vuln/) + +--- + +## Meldung von Sicherheitsproblemen + +Wenn Sie eine Sicherheitslücke **direkt in diesem Projekt** (nicht in Dependencies) finden: + +1. **Nicht öffentlich melden** (kein GitHub Issue) +2. Kontaktieren Sie uns direkt per E-Mail +3. Geben Sie Zeit für einen Fix bevor öffentliche Disclosure + +--- + +*Letzte Aktualisierung: Januar 2026* diff --git a/TROUBLESHOOTING.md b/TROUBLESHOOTING.md new file mode 100644 index 0000000..c8ad009 --- /dev/null +++ b/TROUBLESHOOTING.md @@ -0,0 +1,141 @@ +# Troubleshooting + +## Inhaltsverzeichnis + +1. [tsc not found](#tsc-not-found) +2. [Read-only Volume](#read-only-volume) +3. [npm audit Vulnerabilities](#npm-audit-vulnerabilities) +4. [Node nicht sichtbar](#node-nicht-sichtbar) +5. [Authentifizierung fehlgeschlagen](#authentifizierung-fehlgeschlagen) + +--- + +## tsc not found + +**Symptom:** +``` +sh: 1: tsc: not found +npm error code 127 +``` + +**Ursache:** TypeScript ist nicht installiert (im Container oft nicht verfügbar). + +**Lösung:** Auf dem Host bauen! + +```bash +# Auf dem Host (nicht im Container) +npm install +npm run build + +# Dann in Container kopieren +docker cp dist n8n:/home/node/.n8n/custom/n8n-nodes-librebooking/ +docker cp package.json n8n:/home/node/.n8n/custom/n8n-nodes-librebooking/ +docker cp node_modules n8n:/home/node/.n8n/custom/n8n-nodes-librebooking/ +docker restart n8n +``` + +**Oder mit Skript:** +```bash +./quick-install.sh n8n +``` + +--- + +## Read-only Volume + +**Symptom:** +``` +EROFS: read-only file system +npm error EACCES: permission denied +``` + +**Ursache:** Volume ist mit `:ro` gemountet. + +**Lösung 1:** Volume ohne `:ro` mounten + +```yaml +# docker-compose.yml +volumes: + - ./custom-nodes:/home/node/.n8n/custom # Ohne :ro +``` + +**Lösung 2:** Auf dem Host bauen und kopieren + +```bash +npm install && npm run build +docker cp dist n8n:/home/node/.n8n/custom/n8n-nodes-librebooking/ +``` + +--- + +## npm audit Vulnerabilities + +**Symptom:** +``` +found 2 vulnerabilities (1 moderate, 1 critical) +``` + +**Erklärung:** Diese kommen von `n8n-workflow` und sind für dieses Projekt nicht kritisch. + +**Lösung:** Ignorieren oder `.npmrc` verwenden: + +```bash +# .npmrc bereits konfiguriert +audit=false +``` + +Siehe [SECURITY.md](SECURITY.md) für Details. + +--- + +## Node nicht sichtbar + +**Lösung:** + +1. Container neustarten: + ```bash + docker restart n8n + ``` + +2. Dateien prüfen: + ```bash + docker exec n8n ls -la /home/node/.n8n/custom/n8n-nodes-librebooking/ + ``` + +3. Environment prüfen: + ```yaml + environment: + - N8N_CUSTOM_EXTENSIONS=/home/node/.n8n/custom + ``` + +4. Logs prüfen: + ```bash + docker logs n8n | grep -i librebooking + ``` + +--- + +## Authentifizierung fehlgeschlagen + +**Symptom:** "401 Unauthorized" oder "Invalid credentials" + +**Lösung:** + +1. URL prüfen (ohne `/Web/` am Ende) +2. Benutzer muss Admin sein +3. API muss in LibreBooking aktiviert sein + +Test: +```bash +curl -X POST https://librebooking.example.com/Web/Services/Authentication/Authenticate \ + -H 'Content-Type: application/json' \ + -d '{"username": "admin", "password": "pass"}' +``` + +--- + +## Weitere Hilfe + +- [README.md](README.md) - Übersicht +- [INSTALLATION.md](INSTALLATION.md) - Installationsanleitung +- [DOCKER-INTEGRATION.md](DOCKER-INTEGRATION.md) - Docker Details diff --git a/build-on-host.sh b/build-on-host.sh new file mode 100755 index 0000000..7fa0e3c --- /dev/null +++ b/build-on-host.sh @@ -0,0 +1,183 @@ +#!/bin/bash +# ============================================================================ +# build-on-host.sh - Baut den Node auf dem Host (außerhalb des Containers) +# +# Verwendung: Wenn das Volume als read-only gemountet werden soll +# +# Dieses Skript: +# 1. Installiert Dependencies auf dem Host +# 2. Baut den Node auf dem Host +# 3. Kopiert nur die fertigen Dateien in den Container +# 4. Das Volume kann dann read-only gemountet werden +# ============================================================================ + +set -e + +# Farben +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +echo -e "${BLUE}============================================${NC}" +echo -e "${BLUE} LibreBooking Node - Build auf Host${NC}" +echo -e "${BLUE}============================================${NC}" +echo "" + +# Hilfe anzeigen +show_help() { + echo "Verwendung: $0 [OPTIONEN]" + echo "" + echo "Dieses Skript baut den Node auf dem Host, sodass er als" + echo "read-only Volume gemountet werden kann." + echo "" + echo "Optionen:" + echo " -o, --output DIR Ausgabeverzeichnis (Standard: ./dist-for-docker)" + echo " -c, --copy-to PATH Kopiert direkt zu einem Pfad (z.B. custom-nodes/)" + echo " -h, --help Diese Hilfe anzeigen" + echo "" + echo "Beispiele:" + echo " $0 # Baut in ./dist-for-docker" + echo " $0 -c ../n8n/custom-nodes/n8n-nodes-librebooking" + echo "" + echo "Danach in docker-compose.yml:" + echo " volumes:" + echo " - ./dist-for-docker:/home/node/.n8n/custom/n8n-nodes-librebooking:ro" + exit 0 +} + +OUTPUT_DIR="$SCRIPT_DIR/dist-for-docker" +COPY_TO="" + +# Parameter parsen +while [[ $# -gt 0 ]]; do + case $1 in + -o|--output) + OUTPUT_DIR="$2" + shift 2 + ;; + -c|--copy-to) + COPY_TO="$2" + shift 2 + ;; + -h|--help) + show_help + ;; + *) + echo -e "${RED}Unbekannte Option: $1${NC}" + show_help + ;; + esac +done + +log() { + echo -e "${GREEN}[INFO]${NC} $1" +} + +warn() { + echo -e "${YELLOW}[WARN]${NC} $1" +} + +error() { + echo -e "${RED}[ERROR]${NC} $1" + exit 1 +} + +# Schritt 1: Prüfe Voraussetzungen +log "Prüfe Voraussetzungen..." + +if ! command -v node &>/dev/null; then + error "Node.js ist nicht installiert!\n\n Installieren Sie Node.js 18+: https://nodejs.org/" +fi + +NODE_VERSION=$(node -v | cut -d'v' -f2 | cut -d'.' -f1) +if [ "$NODE_VERSION" -lt 18 ]; then + error "Node.js Version $NODE_VERSION ist zu alt. Mindestens v18 erforderlich." +fi + +if ! command -v npm &>/dev/null; then + error "npm ist nicht installiert!" +fi + +log "Node.js: $(node -v)" +log "npm: $(npm -v)" + +# Schritt 2: Dependencies installieren +log "Installiere Dependencies..." +cd "$SCRIPT_DIR" + +if npm install 2>&1 | tail -5; then + log "Dependencies installiert ✓" +else + error "npm install fehlgeschlagen!" +fi + +# Schritt 3: Bauen +log "Baue den Node..." + +if npm run build 2>&1 | tail -10; then + log "Build erfolgreich ✓" +else + error "npm run build fehlgeschlagen!" +fi + +# Schritt 4: Prüfe Build +if [ ! -d "$SCRIPT_DIR/dist" ]; then + error "dist/ Verzeichnis wurde nicht erstellt!" +fi + +NODE_COUNT=$(find "$SCRIPT_DIR/dist" -name "*.node.js" | wc -l) +if [ "$NODE_COUNT" -eq 0 ]; then + error "Keine .node.js Dateien im dist/ Verzeichnis!" +fi + +log "Gefunden: $NODE_COUNT Node-Datei(en)" + +# Schritt 5: Erstelle Ausgabeverzeichnis +log "Erstelle Ausgabeverzeichnis: $OUTPUT_DIR" + +mkdir -p "$OUTPUT_DIR" + +# Kopiere alle notwendigen Dateien +cp -r "$SCRIPT_DIR/dist" "$OUTPUT_DIR/" +cp "$SCRIPT_DIR/package.json" "$OUTPUT_DIR/" +cp -r "$SCRIPT_DIR/node_modules" "$OUTPUT_DIR/" 2>/dev/null || true + +# Optional: Nur essentielle Dateien für minimales Image +log "Räume auf..." +rm -rf "$OUTPUT_DIR/node_modules/.cache" 2>/dev/null || true + +# Schritt 6: Optional kopieren +if [ -n "$COPY_TO" ]; then + log "Kopiere zu: $COPY_TO" + mkdir -p "$COPY_TO" + cp -r "$OUTPUT_DIR/"* "$COPY_TO/" + log "Kopiert ✓" +fi + +echo "" +echo -e "${GREEN}============================================${NC}" +echo -e "${GREEN} Build abgeschlossen!${NC}" +echo -e "${GREEN}============================================${NC}" +echo "" +log "Ausgabe in: $OUTPUT_DIR" +echo "" +echo "Nächste Schritte:" +echo "" +echo "1. Kopieren Sie das Verzeichnis zu Ihrer n8n Installation:" +echo " cp -r $OUTPUT_DIR /pfad/zu/n8n/custom-nodes/n8n-nodes-librebooking" +echo "" +echo "2. Oder verwenden Sie es direkt in docker-compose.yml:" +echo "" +echo " services:" +echo " n8n:" +echo " volumes:" +echo " # Read-only möglich, da bereits gebaut!" +echo " - $OUTPUT_DIR:/home/node/.n8n/custom/n8n-nodes-librebooking:ro" +echo "" +echo "3. Starten Sie n8n neu:" +echo " docker compose restart n8n" +echo "" diff --git a/check-installation.sh b/check-installation.sh new file mode 100755 index 0000000..97f751c --- /dev/null +++ b/check-installation.sh @@ -0,0 +1,281 @@ +#!/bin/sh +# ============================================================================ +# check-installation.sh - Debug-Skript für LibreBooking Node Installation +# Kann sowohl im Container als auch auf dem Host ausgeführt werden +# ============================================================================ + +echo "============================================" +echo " LibreBooking Node - Installation Check" +echo "============================================" +echo "" +echo "Ausgeführt: $(date)" +echo "Hostname: $(hostname)" +echo "" + +# Status-Zähler +OK_COUNT=0 +WARN_COUNT=0 +ERROR_COUNT=0 + +ok() { + echo "[✓] $1" + OK_COUNT=$((OK_COUNT + 1)) +} + +warn() { + echo "[!] $1" + WARN_COUNT=$((WARN_COUNT + 1)) +} + +error() { + echo "[✗] $1" + ERROR_COUNT=$((ERROR_COUNT + 1)) +} + +info() { + echo "[i] $1" +} + +echo "============================================" +echo " 1. UMGEBUNG" +echo "============================================" + +# Node.js/npm prüfen +if command -v node >/dev/null 2>&1; then + ok "Node.js installiert: $(node --version)" +else + error "Node.js nicht gefunden" +fi + +if command -v npm >/dev/null 2>&1; then + ok "npm installiert: $(npm --version)" +else + error "npm nicht gefunden" +fi + +# Aktueller Benutzer +info "Aktueller Benutzer: $(whoami) (UID: $(id -u))" + +echo "" +echo "============================================" +echo " 2. UMGEBUNGSVARIABLEN" +echo "============================================" + +if [ -n "$N8N_CUSTOM_EXTENSIONS" ]; then + ok "N8N_CUSTOM_EXTENSIONS: $N8N_CUSTOM_EXTENSIONS" +else + warn "N8N_CUSTOM_EXTENSIONS nicht gesetzt" + info " Empfehlung: N8N_CUSTOM_EXTENSIONS=/home/node/.n8n/custom" +fi + +if [ "$N8N_COMMUNITY_NODES_ENABLED" = "true" ]; then + ok "N8N_COMMUNITY_NODES_ENABLED: true" +else + warn "N8N_COMMUNITY_NODES_ENABLED ist nicht 'true'" + info " Aktueller Wert: ${N8N_COMMUNITY_NODES_ENABLED:-}" +fi + +# Weitere n8n Variablen +if [ -n "$N8N_LOG_LEVEL" ]; then + info "N8N_LOG_LEVEL: $N8N_LOG_LEVEL" +fi + +echo "" +echo "============================================" +echo " 3. VERZEICHNISSE PRÜFEN" +echo "============================================" + +# Mögliche Pfade +POSSIBLE_PATHS=" +/home/node/.n8n/custom/n8n-nodes-librebooking +/home/node/.n8n/custom +/opt/n8n/custom-nodes +/data/custom-nodes +" + +FOUND_PATH="" + +for path in $POSSIBLE_PATHS; do + if [ -d "$path" ]; then + if [ -f "$path/package.json" ]; then + ok "Custom Node Verzeichnis: $path" + FOUND_PATH="$path" + break + elif [ -f "$path/n8n-nodes-librebooking/package.json" ]; then + ok "Custom Node Verzeichnis: $path/n8n-nodes-librebooking" + FOUND_PATH="$path/n8n-nodes-librebooking" + break + else + info "Verzeichnis existiert (ohne package.json): $path" + fi + fi +done + +if [ -z "$FOUND_PATH" ]; then + error "Kein Custom Node Verzeichnis mit package.json gefunden!" + info " Geprüfte Pfade:" + for path in $POSSIBLE_PATHS; do + info " - $path" + done +fi + +echo "" +echo "============================================" +echo " 4. NODE-DATEIEN PRÜFEN" +echo "============================================" + +if [ -n "$FOUND_PATH" ]; then + cd "$FOUND_PATH" 2>/dev/null || true + + # package.json + if [ -f "package.json" ]; then + ok "package.json vorhanden" + info " Name: $(grep '"name":' package.json | head -1 | cut -d'"' -f4)" + info " Version: $(grep '"version":' package.json | head -1 | cut -d'"' -f4)" + else + error "package.json fehlt" + fi + + # node_modules + if [ -d "node_modules" ]; then + MODULE_COUNT=$(ls -1 node_modules 2>/dev/null | wc -l) + ok "node_modules vorhanden ($MODULE_COUNT Pakete)" + else + error "node_modules fehlt - führen Sie 'npm install' aus" + fi + + # dist Verzeichnis + if [ -d "dist" ]; then + ok "dist/ Verzeichnis vorhanden" + + # .node.js Dateien suchen + echo "" + info "Gefundene Node-Dateien:" + NODE_FILES=$(find dist -name "*.node.js" 2>/dev/null) + if [ -n "$NODE_FILES" ]; then + echo "$NODE_FILES" | while read -r f; do + if [ -f "$f" ]; then + ok " $f ($(ls -lh "$f" | awk '{print $5}'))" + fi + done + else + error " Keine .node.js Dateien gefunden!" + fi + + # Credentials + echo "" + info "Gefundene Credential-Dateien:" + CRED_FILES=$(find dist -name "*.credentials.js" 2>/dev/null) + if [ -n "$CRED_FILES" ]; then + echo "$CRED_FILES" | while read -r f; do + if [ -f "$f" ]; then + ok " $f" + fi + done + else + error " Keine .credentials.js Dateien gefunden!" + fi + else + error "dist/ Verzeichnis fehlt - führen Sie 'npm run build' aus" + fi + + # Icons + echo "" + info "Icon-Dateien:" + find . -name "*.svg" 2>/dev/null | while read -r f; do + info " $f" + done +fi + +echo "" +echo "============================================" +echo " 5. BERECHTIGUNGEN" +echo "============================================" + +# Funktion: Prüft ob ein Verzeichnis read-only ist +check_readonly() { + local dir="$1" + local test_file="$dir/.write_test_$$" + + # Versuche eine Test-Datei zu erstellen + if touch "$test_file" 2>/dev/null; then + rm -f "$test_file" 2>/dev/null + return 0 # Schreibbar + else + return 1 # Read-only + fi +} + +if [ -n "$FOUND_PATH" ]; then + # Prüfe Schreibrechte (inkl. read-only Volume Check) + if check_readonly "$FOUND_PATH"; then + ok "Schreibrechte auf $FOUND_PATH" + else + error "KEINE Schreibrechte auf $FOUND_PATH (Read-only Volume?)" + echo "" + info " PROBLEM: Das Verzeichnis ist möglicherweise als :ro gemountet." + info " LÖSUNG 1: Volume ohne :ro mounten:" + info " - ./custom-nodes:/home/node/.n8n/custom/n8n-nodes-librebooking" + info " LÖSUNG 2: Auf dem Host bauen mit ./build-on-host.sh" + info " Siehe: TROUBLESHOOTING.md" + echo "" + fi + + # Besitzer prüfen + OWNER=$(ls -ld "$FOUND_PATH" | awk '{print $3}') + GROUP=$(ls -ld "$FOUND_PATH" | awk '{print $4}') + info "Besitzer: $OWNER:$GROUP" + + # n8n läuft als node (UID 1000) + if [ "$(id -u)" = "1000" ]; then + ok "Läuft als UID 1000 (Standard für n8n)" + else + warn "Läuft nicht als UID 1000 (aktuell: $(id -u))" + fi +fi + +echo "" +echo "============================================" +echo " ZUSAMMENFASSUNG" +echo "============================================" +echo "" +echo "Ergebnisse:" +echo " ✓ OK: $OK_COUNT" +echo " ! Warnung: $WARN_COUNT" +echo " ✗ Fehler: $ERROR_COUNT" +echo "" + +if [ "$ERROR_COUNT" -gt 0 ]; then + echo "============================================" + echo " EMPFOHLENE AKTIONEN" + echo "============================================" + echo "" + + if [ ! -d "node_modules" ] 2>/dev/null; then + echo "1. Dependencies installieren:" + echo " cd $FOUND_PATH && npm install" + echo "" + fi + + if [ ! -d "dist" ] 2>/dev/null; then + echo "2. Node bauen:" + echo " cd $FOUND_PATH && npm run build" + echo "" + fi + + echo "3. Container neustarten:" + echo " docker restart n8n" + echo "" + + exit 1 +elif [ "$WARN_COUNT" -gt 0 ]; then + echo "Status: TEILWEISE OK (Warnungen beachten)" + exit 0 +else + echo "Status: ALLES OK ✓" + echo "" + echo "Falls der Node trotzdem nicht erscheint:" + echo " 1. Starten Sie n8n neu: docker restart n8n" + echo " 2. Prüfen Sie die Logs: docker logs n8n | grep -i libre" + exit 0 +fi diff --git a/create-release.sh b/create-release.sh new file mode 100755 index 0000000..af08448 --- /dev/null +++ b/create-release.sh @@ -0,0 +1,89 @@ +#!/bin/bash +# ============================================================================ +# create-release.sh - Erstellt neue Archive und Git Tag +# ============================================================================ + +set -e + +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +NC='\033[0m' + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PARENT_DIR="$(dirname "$SCRIPT_DIR")" + +# Version aus package.json lesen +VERSION=$(grep '"version"' "$SCRIPT_DIR/package.json" | head -1 | sed 's/.*"version": "\([^"]*\)".*/\1/') + +echo -e "${GREEN}=== Release erstellen v$VERSION ===${NC}\n" + +cd "$SCRIPT_DIR" + +# 1. Prüfe, ob alles committet ist +if [ -n "$(git status --porcelain)" ]; then + echo -e "${YELLOW}Warnung: Es gibt uncommittete Änderungen!${NC}" + git status --short + echo "" + read -p "Trotzdem fortfahren? (j/N) " -n 1 -r + echo + if [[ ! $REPLY =~ ^[Jj]$ ]]; then + exit 1 + fi +fi + +# 2. Alte Archive löschen +echo "[1/4] Lösche alte Archive..." +./git-cleanup.sh 2>/dev/null || true + +# 3. Build prüfen +echo "" +echo "[2/4] Prüfe Build..." +if [ ! -d "dist" ] || [ -z "$(find dist -name '*.node.js' 2>/dev/null)" ]; then + echo "Baue Node..." + npm install --silent + npm run build --silent +fi +echo " ✓ Build OK" + +# 4. Archive erstellen +echo "" +echo "[3/4] Erstelle Archive..." + +TAR_FILE="$PARENT_DIR/n8n-nodes-librebooking-v${VERSION}.tar.gz" +ZIP_FILE="$PARENT_DIR/n8n-nodes-librebooking-v${VERSION}.zip" + +git archive --format=tar.gz --prefix=n8n-nodes-librebooking/ --output="$TAR_FILE" HEAD +echo " ✓ $TAR_FILE" + +git archive --format=zip --prefix=n8n-nodes-librebooking/ --output="$ZIP_FILE" HEAD +echo " ✓ $ZIP_FILE" + +# 5. Git Tag (optional) +echo "" +echo "[4/4] Git Tag..." + +if git tag | grep -q "v$VERSION"; then + echo -e " ${YELLOW}Tag v$VERSION existiert bereits${NC}" +else + read -p "Git Tag v$VERSION erstellen? (j/N) " -n 1 -r + echo + if [[ $REPLY =~ ^[Jj]$ ]]; then + git tag -a "v$VERSION" -m "Version $VERSION - Vereinfachte Installation" + echo " ✓ Tag v$VERSION erstellt" + fi +fi + +echo "" +echo -e "${GREEN}============================================${NC}" +echo -e "${GREEN} Release v$VERSION erstellt!${NC}" +echo -e "${GREEN}============================================${NC}" +echo "" +echo "Archive:" +echo " $TAR_FILE" +echo " $ZIP_FILE" +echo "" +echo "Zum Pushen:" +echo " git push origin main" +echo " git push origin v$VERSION" +echo "" diff --git a/credentials/LibreBookingApi.credentials.ts b/credentials/LibreBookingApi.credentials.ts new file mode 100644 index 0000000..ca0de1c --- /dev/null +++ b/credentials/LibreBookingApi.credentials.ts @@ -0,0 +1,65 @@ +import { + IAuthenticateGeneric, + ICredentialTestRequest, + ICredentialType, + INodeProperties, +} from 'n8n-workflow'; + +/** + * LibreBooking API Credentials + * + * LibreBooking verwendet Session-basierte Authentifizierung. + * Der Node holt bei jeder Ausführung einen neuen Session-Token. + */ +export class LibreBookingApi implements ICredentialType { + name = 'libreBookingApi'; + displayName = 'LibreBooking API'; + documentationUrl = 'https://librebooking.org/docs/api'; + + properties: INodeProperties[] = [ + { + displayName: 'LibreBooking URL', + name: 'url', + type: 'string', + default: '', + placeholder: 'https://booking.example.com', + required: true, + description: 'Die Basis-URL Ihrer LibreBooking-Installation (ohne /Web/Services)', + }, + { + displayName: 'Benutzername', + name: 'username', + type: 'string', + default: '', + required: true, + description: 'Ihr LibreBooking-Benutzername oder E-Mail-Adresse', + }, + { + displayName: 'Passwort', + name: 'password', + type: 'string', + typeOptions: { + password: true, + }, + default: '', + required: true, + description: 'Ihr LibreBooking-Passwort', + }, + ]; + + // Test-Request um die Credentials zu validieren + test: ICredentialTestRequest = { + request: { + baseURL: '={{$credentials.url}}', + url: '/Web/Services/index.php/Authentication/Authenticate', + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: { + username: '={{$credentials.username}}', + password: '={{$credentials.password}}', + }, + }, + }; +} diff --git a/custom-nodes/README.md b/custom-nodes/README.md new file mode 100644 index 0000000..867cd96 --- /dev/null +++ b/custom-nodes/README.md @@ -0,0 +1,42 @@ +# LibreBooking n8n Node - Custom Nodes Version + +Diese Version ist speziell für die Integration in bestehende n8n Docker-Installationen optimiert. + +## Schnellinstallation + +### 1. Verzeichnis in n8n Custom Nodes kopieren + +```bash +cp -r custom-nodes /pfad/zu/n8n/.n8n/custom/n8n-nodes-librebooking +``` + +### 2. Dependencies installieren und bauen + +```bash +cd /pfad/zu/n8n/.n8n/custom/n8n-nodes-librebooking +npm install +npm run build +``` + +### 3. n8n neustarten + +```bash +docker-compose restart n8n +``` + +## Enthaltene Dateien + +- `credentials/` - API Credentials Definition +- `nodes/` - LibreBooking und LibreBookingTrigger Nodes +- `package.json` - Vereinfachte Package-Konfiguration +- `tsconfig.json` - TypeScript Konfiguration + +## Weitere Informationen + +Siehe die ausführliche Dokumentation: +- `DOCKER-INTEGRATION.md` - Detaillierte Docker-Anleitung +- `README.md` - Vollständige Dokumentation + +## Lizenz + +MIT License diff --git a/custom-nodes/credentials/LibreBookingApi.credentials.ts b/custom-nodes/credentials/LibreBookingApi.credentials.ts new file mode 100644 index 0000000..ca0de1c --- /dev/null +++ b/custom-nodes/credentials/LibreBookingApi.credentials.ts @@ -0,0 +1,65 @@ +import { + IAuthenticateGeneric, + ICredentialTestRequest, + ICredentialType, + INodeProperties, +} from 'n8n-workflow'; + +/** + * LibreBooking API Credentials + * + * LibreBooking verwendet Session-basierte Authentifizierung. + * Der Node holt bei jeder Ausführung einen neuen Session-Token. + */ +export class LibreBookingApi implements ICredentialType { + name = 'libreBookingApi'; + displayName = 'LibreBooking API'; + documentationUrl = 'https://librebooking.org/docs/api'; + + properties: INodeProperties[] = [ + { + displayName: 'LibreBooking URL', + name: 'url', + type: 'string', + default: '', + placeholder: 'https://booking.example.com', + required: true, + description: 'Die Basis-URL Ihrer LibreBooking-Installation (ohne /Web/Services)', + }, + { + displayName: 'Benutzername', + name: 'username', + type: 'string', + default: '', + required: true, + description: 'Ihr LibreBooking-Benutzername oder E-Mail-Adresse', + }, + { + displayName: 'Passwort', + name: 'password', + type: 'string', + typeOptions: { + password: true, + }, + default: '', + required: true, + description: 'Ihr LibreBooking-Passwort', + }, + ]; + + // Test-Request um die Credentials zu validieren + test: ICredentialTestRequest = { + request: { + baseURL: '={{$credentials.url}}', + url: '/Web/Services/index.php/Authentication/Authenticate', + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: { + username: '={{$credentials.username}}', + password: '={{$credentials.password}}', + }, + }, + }; +} diff --git a/custom-nodes/index.ts b/custom-nodes/index.ts new file mode 100644 index 0000000..13abd91 --- /dev/null +++ b/custom-nodes/index.ts @@ -0,0 +1,4 @@ +// LibreBooking n8n Node - Entry Point +export { LibreBooking } from './nodes/LibreBooking/LibreBooking.node'; +export { LibreBookingTrigger } from './nodes/LibreBookingTrigger/LibreBookingTrigger.node'; +export { LibreBookingApi } from './credentials/LibreBookingApi.credentials'; diff --git a/custom-nodes/nodes/LibreBooking/LibreBooking.node.ts b/custom-nodes/nodes/LibreBooking/LibreBooking.node.ts new file mode 100644 index 0000000..675b4ad --- /dev/null +++ b/custom-nodes/nodes/LibreBooking/LibreBooking.node.ts @@ -0,0 +1,1203 @@ +import { + IExecuteFunctions, + INodeExecutionData, + INodeType, + INodeTypeDescription, + IHttpRequestMethods, + NodeApiError, + NodeOperationError, +} from 'n8n-workflow'; + +interface LibreBookingSession { + sessionToken: string; + userId: number; + sessionExpires: string; +} + +/** + * Authentifizierung bei LibreBooking + */ +async function authenticate( + executeFunctions: IExecuteFunctions, + baseUrl: string, + username: string, + password: string, +): Promise { + try { + const response = await executeFunctions.helpers.httpRequest({ + method: 'POST', + url: `${baseUrl}/Web/Services/index.php/Authentication/Authenticate`, + headers: { 'Content-Type': 'application/json' }, + body: { username, password }, + json: true, + }); + + if (!response.isAuthenticated) { + throw new NodeOperationError( + executeFunctions.getNode(), + 'Authentifizierung fehlgeschlagen. Bitte überprüfen Sie Ihre Credentials.', + ); + } + + return { + sessionToken: response.sessionToken, + userId: response.userId, + sessionExpires: response.sessionExpires, + }; + } catch (error: any) { + throw new NodeApiError(executeFunctions.getNode(), error, { + message: 'Authentifizierung fehlgeschlagen', + description: 'Überprüfen Sie die LibreBooking URL und Ihre Zugangsdaten.', + }); + } +} + +/** + * Abmeldung von LibreBooking + */ +async function signOut( + executeFunctions: IExecuteFunctions, + baseUrl: string, + session: LibreBookingSession, +): Promise { + try { + await executeFunctions.helpers.httpRequest({ + method: 'POST', + url: `${baseUrl}/Web/Services/index.php/Authentication/SignOut`, + headers: { 'Content-Type': 'application/json' }, + body: { + userId: session.userId, + sessionToken: session.sessionToken, + }, + json: true, + }); + } catch (error) { + // Ignoriere SignOut-Fehler + } +} + +/** + * API-Request mit Session-Authentifizierung + */ +async function makeApiRequest( + executeFunctions: IExecuteFunctions, + baseUrl: string, + session: LibreBookingSession, + method: IHttpRequestMethods, + endpoint: string, + body?: any, + qs?: any, +): Promise { + const options: any = { + method, + url: `${baseUrl}/Web/Services/index.php${endpoint}`, + headers: { + 'Content-Type': 'application/json', + 'X-Booked-SessionToken': session.sessionToken, + 'X-Booked-UserId': session.userId.toString(), + }, + json: true, + }; + + if (body && Object.keys(body).length > 0) { + options.body = body; + } + + if (qs && Object.keys(qs).length > 0) { + options.qs = qs; + } + + try { + return await executeFunctions.helpers.httpRequest(options); + } catch (error: any) { + if (error.statusCode === 401) { + throw new NodeApiError(executeFunctions.getNode(), error, { + message: 'Authentifizierung abgelaufen', + description: 'Der Session-Token ist abgelaufen. Bitte erneut ausführen.', + }); + } else if (error.statusCode === 403) { + throw new NodeApiError(executeFunctions.getNode(), error, { + message: 'Zugriff verweigert', + description: 'Sie haben keine Berechtigung für diese Operation. Admin-Rechte erforderlich?', + }); + } else if (error.statusCode === 404) { + throw new NodeApiError(executeFunctions.getNode(), error, { + message: 'Nicht gefunden', + description: 'Die angeforderte Ressource wurde nicht gefunden.', + }); + } + throw new NodeApiError(executeFunctions.getNode(), error, { + message: `API-Fehler: ${error.message}`, + }); + } +} + +/** + * Hilfsfunktion: String zu Array von Zahlen + */ +function parseIdList(value: string | undefined): number[] { + if (!value || value.trim() === '') return []; + return value.split(',').map(id => parseInt(id.trim(), 10)).filter(id => !isNaN(id)); +} + +/** + * LibreBooking n8n Node + * + * Vollständige Integration für die LibreBooking API. + * Unterstützt alle wichtigen Ressourcen und Operationen. + */ +export class LibreBooking implements INodeType { + description: INodeTypeDescription = { + displayName: 'LibreBooking', + name: 'libreBooking', + icon: 'file:librebooking.svg', + group: ['transform'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Verwalten Sie Reservierungen, Ressourcen, Benutzer und mehr mit LibreBooking', + defaults: { + name: 'LibreBooking', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'libreBookingApi', + required: true, + }, + ], + properties: [ + // ===================================================== + // RESOURCE SELECTOR + // ===================================================== + { + displayName: 'Ressource', + name: 'resource', + type: 'options', + noDataExpression: true, + options: [ + { + name: 'Reservierung', + value: 'reservation', + description: 'Reservierungen verwalten', + }, + { + name: 'Ressource', + value: 'resource', + description: 'Ressourcen (Räume, Equipment) verwalten', + }, + { + name: 'Zeitplan', + value: 'schedule', + description: 'Zeitpläne abrufen', + }, + { + name: 'Benutzer', + value: 'user', + description: 'Benutzer verwalten (Admin-Rechte erforderlich)', + }, + { + name: 'Konto', + value: 'account', + description: 'Eigenes Konto verwalten', + }, + { + name: 'Gruppe', + value: 'group', + description: 'Benutzergruppen verwalten', + }, + { + name: 'Zubehör', + value: 'accessory', + description: 'Zubehör abrufen', + }, + { + name: 'Attribut', + value: 'attribute', + description: 'Benutzerdefinierte Attribute verwalten', + }, + ], + default: 'reservation', + }, + + // ===================================================== + // RESERVATION OPERATIONS + // ===================================================== + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { + show: { + resource: ['reservation'], + }, + }, + options: [ + { name: 'Erstellen', value: 'create', description: 'Neue Reservierung erstellen', action: 'Reservierung erstellen' }, + { name: 'Abrufen', value: 'get', description: 'Reservierung abrufen', action: 'Reservierung abrufen' }, + { name: 'Alle Abrufen', value: 'getAll', description: 'Alle Reservierungen abrufen', action: 'Alle Reservierungen abrufen' }, + { name: 'Aktualisieren', value: 'update', description: 'Reservierung aktualisieren', action: 'Reservierung aktualisieren' }, + { name: 'Löschen', value: 'delete', description: 'Reservierung löschen', action: 'Reservierung löschen' }, + { name: 'Genehmigen', value: 'approve', description: 'Ausstehende Reservierung genehmigen', action: 'Reservierung genehmigen' }, + { name: 'Check-In', value: 'checkIn', description: 'In Reservierung einchecken', action: 'In Reservierung einchecken' }, + { name: 'Check-Out', value: 'checkOut', description: 'Aus Reservierung auschecken', action: 'Aus Reservierung auschecken' }, + ], + default: 'getAll', + }, + + // ===================================================== + // RESOURCE OPERATIONS + // ===================================================== + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { show: { resource: ['resource'] } }, + options: [ + { name: 'Alle Abrufen', value: 'getAll', description: 'Alle Ressourcen abrufen', action: 'Alle Ressourcen abrufen' }, + { name: 'Abrufen', value: 'get', description: 'Ressource abrufen', action: 'Ressource abrufen' }, + { name: 'Verfügbarkeit Prüfen', value: 'getAvailability', description: 'Verfügbarkeit von Ressourcen prüfen', action: 'Verfügbarkeit prüfen' }, + { name: 'Gruppen Abrufen', value: 'getGroups', description: 'Ressourcen-Gruppen abrufen', action: 'Ressourcen-Gruppen abrufen' }, + { name: 'Typen Abrufen', value: 'getTypes', description: 'Ressourcen-Typen abrufen', action: 'Ressourcen-Typen abrufen' }, + { name: 'Status Abrufen', value: 'getStatuses', description: 'Verfügbare Status abrufen', action: 'Status abrufen' }, + { name: 'Erstellen', value: 'create', description: 'Neue Ressource erstellen (Admin)', action: 'Ressource erstellen' }, + { name: 'Aktualisieren', value: 'update', description: 'Ressource aktualisieren (Admin)', action: 'Ressource aktualisieren' }, + { name: 'Löschen', value: 'delete', description: 'Ressource löschen (Admin)', action: 'Ressource löschen' }, + ], + default: 'getAll', + }, + + // ===================================================== + // SCHEDULE OPERATIONS + // ===================================================== + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { show: { resource: ['schedule'] } }, + options: [ + { name: 'Alle Abrufen', value: 'getAll', description: 'Alle Zeitpläne abrufen', action: 'Alle Zeitpläne abrufen' }, + { name: 'Abrufen', value: 'get', description: 'Zeitplan abrufen', action: 'Zeitplan abrufen' }, + { name: 'Slots Abrufen', value: 'getSlots', description: 'Verfügbare Slots abrufen', action: 'Slots abrufen' }, + ], + default: 'getAll', + }, + + // ===================================================== + // USER OPERATIONS + // ===================================================== + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { show: { resource: ['user'] } }, + options: [ + { name: 'Alle Abrufen', value: 'getAll', description: 'Alle Benutzer abrufen', action: 'Alle Benutzer abrufen' }, + { name: 'Abrufen', value: 'get', description: 'Benutzer abrufen', action: 'Benutzer abrufen' }, + { name: 'Erstellen', value: 'create', description: 'Neuen Benutzer erstellen (Admin)', action: 'Benutzer erstellen' }, + { name: 'Aktualisieren', value: 'update', description: 'Benutzer aktualisieren (Admin)', action: 'Benutzer aktualisieren' }, + { name: 'Passwort Ändern', value: 'updatePassword', description: 'Benutzer-Passwort ändern (Admin)', action: 'Passwort ändern' }, + { name: 'Löschen', value: 'delete', description: 'Benutzer löschen (Admin)', action: 'Benutzer löschen' }, + ], + default: 'getAll', + }, + + // ===================================================== + // ACCOUNT OPERATIONS + // ===================================================== + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { show: { resource: ['account'] } }, + options: [ + { name: 'Abrufen', value: 'get', description: 'Eigene Kontoinformationen abrufen', action: 'Konto abrufen' }, + { name: 'Erstellen', value: 'create', description: 'Neues Konto erstellen (Registrierung)', action: 'Konto erstellen' }, + { name: 'Aktualisieren', value: 'update', description: 'Eigenes Konto aktualisieren', action: 'Konto aktualisieren' }, + { name: 'Passwort Ändern', value: 'updatePassword', description: 'Eigenes Passwort ändern', action: 'Passwort ändern' }, + ], + default: 'get', + }, + + // ===================================================== + // GROUP OPERATIONS + // ===================================================== + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { show: { resource: ['group'] } }, + options: [ + { name: 'Alle Abrufen', value: 'getAll', description: 'Alle Gruppen abrufen', action: 'Alle Gruppen abrufen' }, + { name: 'Abrufen', value: 'get', description: 'Gruppe abrufen', action: 'Gruppe abrufen' }, + { name: 'Erstellen', value: 'create', description: 'Neue Gruppe erstellen (Admin)', action: 'Gruppe erstellen' }, + { name: 'Aktualisieren', value: 'update', description: 'Gruppe aktualisieren (Admin)', action: 'Gruppe aktualisieren' }, + { name: 'Löschen', value: 'delete', description: 'Gruppe löschen (Admin)', action: 'Gruppe löschen' }, + { name: 'Rollen Ändern', value: 'changeRoles', description: 'Gruppenrollen ändern (Admin)', action: 'Rollen ändern' }, + { name: 'Berechtigungen Ändern', value: 'changePermissions', description: 'Gruppenberechtigungen ändern (Admin)', action: 'Berechtigungen ändern' }, + { name: 'Benutzer Ändern', value: 'changeUsers', description: 'Gruppenbenutzer ändern (Admin)', action: 'Benutzer ändern' }, + ], + default: 'getAll', + }, + + // ===================================================== + // ACCESSORY OPERATIONS + // ===================================================== + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { show: { resource: ['accessory'] } }, + options: [ + { name: 'Alle Abrufen', value: 'getAll', description: 'Alle Zubehörteile abrufen', action: 'Alle Zubehörteile abrufen' }, + { name: 'Abrufen', value: 'get', description: 'Zubehörteil abrufen', action: 'Zubehörteil abrufen' }, + ], + default: 'getAll', + }, + + // ===================================================== + // ATTRIBUTE OPERATIONS + // ===================================================== + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { show: { resource: ['attribute'] } }, + options: [ + { name: 'Abrufen', value: 'get', description: 'Attribut abrufen', action: 'Attribut abrufen' }, + { name: 'Nach Kategorie Abrufen', value: 'getByCategory', description: 'Attribute einer Kategorie abrufen', action: 'Attribute nach Kategorie abrufen' }, + { name: 'Erstellen', value: 'create', description: 'Neues Attribut erstellen (Admin)', action: 'Attribut erstellen' }, + { name: 'Aktualisieren', value: 'update', description: 'Attribut aktualisieren (Admin)', action: 'Attribut aktualisieren' }, + { name: 'Löschen', value: 'delete', description: 'Attribut löschen (Admin)', action: 'Attribut löschen' }, + ], + default: 'getByCategory', + }, + + // ===================================================== + // RESERVATION PARAMETERS + // ===================================================== + { + displayName: 'Referenznummer', + name: 'referenceNumber', + type: 'string', + required: true, + displayOptions: { show: { resource: ['reservation'], operation: ['get', 'update', 'delete', 'approve', 'checkIn', 'checkOut'] } }, + default: '', + description: 'Die eindeutige Referenznummer der Reservierung', + }, + { + displayName: 'Ressourcen-ID', + name: 'resourceId', + type: 'number', + required: true, + displayOptions: { show: { resource: ['reservation'], operation: ['create'] } }, + default: 1, + description: 'Die ID der zu reservierenden Ressource', + }, + { + displayName: 'Startzeit', + name: 'startDateTime', + type: 'dateTime', + required: true, + displayOptions: { show: { resource: ['reservation'], operation: ['create', 'update'] } }, + default: '', + description: 'Startzeitpunkt der Reservierung (ISO 8601 Format)', + }, + { + displayName: 'Endzeit', + name: 'endDateTime', + type: 'dateTime', + required: true, + displayOptions: { show: { resource: ['reservation'], operation: ['create', 'update'] } }, + default: '', + description: 'Endzeitpunkt der Reservierung (ISO 8601 Format)', + }, + { + displayName: 'Titel', + name: 'title', + type: 'string', + displayOptions: { show: { resource: ['reservation'], operation: ['create', 'update'] } }, + default: '', + description: 'Titel der Reservierung', + }, + { + displayName: 'Aktualisierungsbereich', + name: 'updateScope', + type: 'options', + displayOptions: { show: { resource: ['reservation'], operation: ['update', 'delete'] } }, + options: [ + { name: 'Nur Diese', value: 'this', description: 'Nur diese Instanz ändern' }, + { name: 'Zukünftige', value: 'future', description: 'Diese und alle zukünftigen Instanzen ändern' }, + { name: 'Alle', value: 'full', description: 'Alle Instanzen der Serie ändern' }, + ], + default: 'this', + }, + { + displayName: 'Zusätzliche Felder', + name: 'additionalFields', + type: 'collection', + placeholder: 'Feld hinzufügen', + default: {}, + displayOptions: { show: { resource: ['reservation'], operation: ['create', 'update'] } }, + options: [ + { displayName: 'Beschreibung', name: 'description', type: 'string', default: '' }, + { displayName: 'Benutzer-ID', name: 'userId', type: 'number', default: '' }, + { displayName: 'Zusätzliche Ressourcen', name: 'resources', type: 'string', default: '', description: 'Komma-getrennte Liste' }, + { displayName: 'Teilnehmer', name: 'participants', type: 'string', default: '', description: 'Komma-getrennte Benutzer-IDs' }, + { displayName: 'Eingeladene', name: 'invitees', type: 'string', default: '', description: 'Komma-getrennte Benutzer-IDs' }, + { displayName: 'Teilnahme Erlauben', name: 'allowParticipation', type: 'boolean', default: true }, + { displayName: 'Nutzungsbedingungen Akzeptiert', name: 'termsAccepted', type: 'boolean', default: true }, + ], + }, + { + displayName: 'Filter', + name: 'filters', + type: 'collection', + placeholder: 'Filter hinzufügen', + default: {}, + displayOptions: { show: { resource: ['reservation'], operation: ['getAll'] } }, + options: [ + { displayName: 'Benutzer-ID', name: 'userId', type: 'number', default: '' }, + { displayName: 'Ressourcen-ID', name: 'resourceId', type: 'number', default: '' }, + { displayName: 'Zeitplan-ID', name: 'scheduleId', type: 'number', default: '' }, + { displayName: 'Startzeit', name: 'startDateTime', type: 'dateTime', default: '' }, + { displayName: 'Endzeit', name: 'endDateTime', type: 'dateTime', default: '' }, + ], + }, + + // ===================================================== + // RESOURCE PARAMETERS + // ===================================================== + { + displayName: 'Ressourcen-ID', + name: 'resourceIdParam', + type: 'number', + required: true, + displayOptions: { show: { resource: ['resource'], operation: ['get', 'update', 'delete'] } }, + default: 1, + }, + { + displayName: 'Ressourcen-ID (Optional)', + name: 'resourceIdOptional', + type: 'number', + displayOptions: { show: { resource: ['resource'], operation: ['getAvailability'] } }, + default: '', + }, + { + displayName: 'Datum/Zeit', + name: 'availabilityDateTime', + type: 'dateTime', + displayOptions: { show: { resource: ['resource'], operation: ['getAvailability'] } }, + default: '', + }, + { + displayName: 'Ressourcen-Name', + name: 'resourceName', + type: 'string', + required: true, + displayOptions: { show: { resource: ['resource'], operation: ['create', 'update'] } }, + default: '', + }, + { + displayName: 'Zeitplan-ID', + name: 'scheduleIdForResource', + type: 'number', + required: true, + displayOptions: { show: { resource: ['resource'], operation: ['create'] } }, + default: 1, + }, + { + displayName: 'Ressourcen-Optionen', + name: 'resourceOptions', + type: 'collection', + placeholder: 'Option hinzufügen', + default: {}, + displayOptions: { show: { resource: ['resource'], operation: ['create', 'update'] } }, + options: [ + { displayName: 'Standort', name: 'location', type: 'string', default: '' }, + { displayName: 'Kontakt', name: 'contact', type: 'string', default: '' }, + { displayName: 'Beschreibung', name: 'description', type: 'string', default: '' }, + { displayName: 'Notizen', name: 'notes', type: 'string', default: '' }, + { displayName: 'Max. Teilnehmer', name: 'maxParticipants', type: 'number', default: 0 }, + { displayName: 'Genehmigung Erforderlich', name: 'requiresApproval', type: 'boolean', default: false }, + { displayName: 'Mehrtägig Erlauben', name: 'allowMultiday', type: 'boolean', default: false }, + { displayName: 'Check-In Erforderlich', name: 'requiresCheckIn', type: 'boolean', default: false }, + { displayName: 'Auto-Release Minuten', name: 'autoReleaseMinutes', type: 'number', default: 0 }, + { displayName: 'Farbe', name: 'color', type: 'string', default: '' }, + { displayName: 'Status-ID', name: 'statusId', type: 'options', options: [{ name: 'Versteckt', value: 0 }, { name: 'Verfügbar', value: 1 }, { name: 'Nicht Verfügbar', value: 2 }], default: 1 }, + ], + }, + + // ===================================================== + // SCHEDULE PARAMETERS + // ===================================================== + { + displayName: 'Zeitplan-ID', + name: 'scheduleId', + type: 'number', + required: true, + displayOptions: { show: { resource: ['schedule'], operation: ['get', 'getSlots'] } }, + default: 1, + }, + { + displayName: 'Slots-Filter', + name: 'slotsFilters', + type: 'collection', + placeholder: 'Filter hinzufügen', + default: {}, + displayOptions: { show: { resource: ['schedule'], operation: ['getSlots'] } }, + options: [ + { displayName: 'Ressourcen-ID', name: 'resourceId', type: 'number', default: '' }, + { displayName: 'Startzeit', name: 'startDateTime', type: 'dateTime', default: '' }, + { displayName: 'Endzeit', name: 'endDateTime', type: 'dateTime', default: '' }, + ], + }, + + // ===================================================== + // USER PARAMETERS + // ===================================================== + { + displayName: 'Benutzer-ID', + name: 'userId', + type: 'number', + required: true, + displayOptions: { show: { resource: ['user'], operation: ['get', 'update', 'updatePassword', 'delete'] } }, + default: 1, + }, + { + displayName: 'E-Mail', + name: 'emailAddress', + type: 'string', + required: true, + displayOptions: { show: { resource: ['user'], operation: ['create'] } }, + default: '', + }, + { + displayName: 'Benutzername', + name: 'userName', + type: 'string', + required: true, + displayOptions: { show: { resource: ['user'], operation: ['create'] } }, + default: '', + }, + { + displayName: 'Passwort', + name: 'password', + type: 'string', + typeOptions: { password: true }, + required: true, + displayOptions: { show: { resource: ['user'], operation: ['create', 'updatePassword'] } }, + default: '', + }, + { + displayName: 'Vorname', + name: 'firstName', + type: 'string', + required: true, + displayOptions: { show: { resource: ['user'], operation: ['create', 'update'] } }, + default: '', + }, + { + displayName: 'Nachname', + name: 'lastName', + type: 'string', + required: true, + displayOptions: { show: { resource: ['user'], operation: ['create', 'update'] } }, + default: '', + }, + { + displayName: 'Benutzer-Filter', + name: 'userFilters', + type: 'collection', + placeholder: 'Filter hinzufügen', + default: {}, + displayOptions: { show: { resource: ['user'], operation: ['getAll'] } }, + options: [ + { displayName: 'Benutzername', name: 'username', type: 'string', default: '' }, + { displayName: 'E-Mail', name: 'email', type: 'string', default: '' }, + { displayName: 'Vorname', name: 'firstName', type: 'string', default: '' }, + { displayName: 'Nachname', name: 'lastName', type: 'string', default: '' }, + { displayName: 'Organisation', name: 'organization', type: 'string', default: '' }, + ], + }, + { + displayName: 'Benutzer-Optionen', + name: 'userOptions', + type: 'collection', + placeholder: 'Option hinzufügen', + default: {}, + displayOptions: { show: { resource: ['user'], operation: ['create', 'update'] } }, + options: [ + { displayName: 'Zeitzone', name: 'timezone', type: 'string', default: 'Europe/Berlin' }, + { displayName: 'Sprache', name: 'language', type: 'string', default: 'de_de' }, + { displayName: 'Telefon', name: 'phone', type: 'string', default: '' }, + { displayName: 'Organisation', name: 'organization', type: 'string', default: '' }, + { displayName: 'Position', name: 'position', type: 'string', default: '' }, + { displayName: 'Gruppen', name: 'groups', type: 'string', default: '', description: 'Komma-getrennte Gruppen-IDs' }, + ], + }, + + // ===================================================== + // ACCOUNT PARAMETERS + // ===================================================== + { + displayName: 'Benutzer-ID', + name: 'accountUserId', + type: 'number', + required: true, + displayOptions: { show: { resource: ['account'], operation: ['get', 'update', 'updatePassword'] } }, + default: '', + }, + { + displayName: 'Account-Daten', + name: 'accountData', + type: 'collection', + placeholder: 'Feld hinzufügen', + default: {}, + displayOptions: { show: { resource: ['account'], operation: ['create', 'update'] } }, + options: [ + { displayName: 'E-Mail', name: 'emailAddress', type: 'string', default: '' }, + { displayName: 'Benutzername', name: 'userName', type: 'string', default: '' }, + { displayName: 'Passwort', name: 'password', type: 'string', typeOptions: { password: true }, default: '' }, + { displayName: 'Vorname', name: 'firstName', type: 'string', default: '' }, + { displayName: 'Nachname', name: 'lastName', type: 'string', default: '' }, + { displayName: 'Zeitzone', name: 'timezone', type: 'string', default: 'Europe/Berlin' }, + { displayName: 'Sprache', name: 'language', type: 'string', default: 'de_de' }, + { displayName: 'Telefon', name: 'phone', type: 'string', default: '' }, + { displayName: 'Organisation', name: 'organization', type: 'string', default: '' }, + { displayName: 'Position', name: 'position', type: 'string', default: '' }, + { displayName: 'AGB Akzeptiert', name: 'acceptTermsOfService', type: 'boolean', default: true }, + ], + }, + { + displayName: 'Passwort-Änderung', + name: 'passwordChange', + type: 'fixedCollection', + default: {}, + displayOptions: { show: { resource: ['account'], operation: ['updatePassword'] } }, + options: [ + { + name: 'passwords', + displayName: 'Passwörter', + values: [ + { displayName: 'Aktuelles Passwort', name: 'currentPassword', type: 'string', typeOptions: { password: true }, default: '' }, + { displayName: 'Neues Passwort', name: 'newPassword', type: 'string', typeOptions: { password: true }, default: '' }, + ], + }, + ], + }, + + // ===================================================== + // GROUP PARAMETERS + // ===================================================== + { + displayName: 'Gruppen-ID', + name: 'groupId', + type: 'number', + required: true, + displayOptions: { show: { resource: ['group'], operation: ['get', 'update', 'delete', 'changeRoles', 'changePermissions', 'changeUsers'] } }, + default: 1, + }, + { + displayName: 'Gruppen-Name', + name: 'groupName', + type: 'string', + required: true, + displayOptions: { show: { resource: ['group'], operation: ['create', 'update'] } }, + default: '', + }, + { + displayName: 'Standard-Gruppe', + name: 'isDefault', + type: 'boolean', + displayOptions: { show: { resource: ['group'], operation: ['create', 'update'] } }, + default: false, + }, + { + displayName: 'Rollen-IDs', + name: 'roleIds', + type: 'string', + displayOptions: { show: { resource: ['group'], operation: ['changeRoles'] } }, + default: '', + description: '1=Gruppenadmin, 2=App-Admin, 3=Ressourcen-Admin, 4=Zeitplan-Admin', + }, + { + displayName: 'Ressourcen-IDs', + name: 'permissionResourceIds', + type: 'string', + displayOptions: { show: { resource: ['group'], operation: ['changePermissions'] } }, + default: '', + }, + { + displayName: 'Benutzer-IDs', + name: 'groupUserIds', + type: 'string', + displayOptions: { show: { resource: ['group'], operation: ['changeUsers'] } }, + default: '', + }, + + // ===================================================== + // ACCESSORY PARAMETERS + // ===================================================== + { + displayName: 'Zubehör-ID', + name: 'accessoryId', + type: 'number', + required: true, + displayOptions: { show: { resource: ['accessory'], operation: ['get'] } }, + default: 1, + }, + + // ===================================================== + // ATTRIBUTE PARAMETERS + // ===================================================== + { + displayName: 'Attribut-ID', + name: 'attributeId', + type: 'number', + required: true, + displayOptions: { show: { resource: ['attribute'], operation: ['get', 'update', 'delete'] } }, + default: 1, + }, + { + displayName: 'Kategorie-ID', + name: 'categoryId', + type: 'options', + required: true, + displayOptions: { show: { resource: ['attribute'], operation: ['getByCategory', 'create'] } }, + options: [ + { name: 'Reservierung', value: 1 }, + { name: 'Benutzer', value: 2 }, + { name: 'Ressource', value: 4 }, + { name: 'Ressourcen-Typ', value: 5 }, + ], + default: 1, + }, + { + displayName: 'Attribut-Label', + name: 'attributeLabel', + type: 'string', + required: true, + displayOptions: { show: { resource: ['attribute'], operation: ['create', 'update'] } }, + default: '', + }, + { + displayName: 'Attribut-Typ', + name: 'attributeType', + type: 'options', + required: true, + displayOptions: { show: { resource: ['attribute'], operation: ['create', 'update'] } }, + options: [ + { name: 'Einzeilig', value: 1 }, + { name: 'Mehrzeilig', value: 2 }, + { name: 'Auswahlliste', value: 3 }, + { name: 'Checkbox', value: 4 }, + { name: 'Datum/Zeit', value: 5 }, + ], + default: 1, + }, + { + displayName: 'Attribut-Optionen', + name: 'attributeOptions', + type: 'collection', + placeholder: 'Option hinzufügen', + default: {}, + displayOptions: { show: { resource: ['attribute'], operation: ['create', 'update'] } }, + options: [ + { displayName: 'Erforderlich', name: 'required', type: 'boolean', default: false }, + { displayName: 'Nur Admin', name: 'adminOnly', type: 'boolean', default: false }, + { displayName: 'Privat', name: 'isPrivate', type: 'boolean', default: false }, + { displayName: 'Sortierung', name: 'sortOrder', type: 'number', default: 0 }, + { displayName: 'Regex-Validierung', name: 'regex', type: 'string', default: '' }, + { displayName: 'Mögliche Werte', name: 'possibleValues', type: 'string', default: '', description: 'Komma-getrennt' }, + ], + }, + ], + }; + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const returnData: INodeExecutionData[] = []; + + const credentials = await this.getCredentials('libreBookingApi'); + const baseUrl = (credentials.url as string).replace(/\/$/, ''); + const username = credentials.username as string; + const pw = credentials.password as string; + + const session = await authenticate(this, baseUrl, username, pw); + + try { + for (let i = 0; i < items.length; i++) { + try { + const resource = this.getNodeParameter('resource', i) as string; + const operation = this.getNodeParameter('operation', i) as string; + let responseData: any; + + // RESERVATION + if (resource === 'reservation') { + if (operation === 'getAll') { + const filters = this.getNodeParameter('filters', i, {}) as any; + const qs: any = {}; + if (filters.userId) qs.userId = filters.userId; + if (filters.resourceId) qs.resourceId = filters.resourceId; + if (filters.scheduleId) qs.scheduleId = filters.scheduleId; + if (filters.startDateTime) qs.startDateTime = filters.startDateTime; + if (filters.endDateTime) qs.endDateTime = filters.endDateTime; + responseData = await makeApiRequest(this, baseUrl, session, 'GET', '/Reservations/', undefined, qs); + } else if (operation === 'get') { + const referenceNumber = this.getNodeParameter('referenceNumber', i) as string; + responseData = await makeApiRequest(this, baseUrl, session, 'GET', `/Reservations/${referenceNumber}`); + } else if (operation === 'create') { + const resourceId = this.getNodeParameter('resourceId', i) as number; + const startDateTime = this.getNodeParameter('startDateTime', i) as string; + const endDateTime = this.getNodeParameter('endDateTime', i) as string; + const title = this.getNodeParameter('title', i, '') as string; + const additionalFields = this.getNodeParameter('additionalFields', i, {}) as any; + const body: any = { resourceId, startDateTime: new Date(startDateTime).toISOString(), endDateTime: new Date(endDateTime).toISOString() }; + if (title) body.title = title; + if (additionalFields.description) body.description = additionalFields.description; + if (additionalFields.userId) body.userId = additionalFields.userId; + if (additionalFields.resources) body.resources = parseIdList(additionalFields.resources); + if (additionalFields.participants) body.participants = parseIdList(additionalFields.participants); + if (additionalFields.invitees) body.invitees = parseIdList(additionalFields.invitees); + if (additionalFields.allowParticipation !== undefined) body.allowParticipation = additionalFields.allowParticipation; + if (additionalFields.termsAccepted !== undefined) body.termsAccepted = additionalFields.termsAccepted; + responseData = await makeApiRequest(this, baseUrl, session, 'POST', '/Reservations/', body); + } else if (operation === 'update') { + const referenceNumber = this.getNodeParameter('referenceNumber', i) as string; + const startDateTime = this.getNodeParameter('startDateTime', i) as string; + const endDateTime = this.getNodeParameter('endDateTime', i) as string; + const title = this.getNodeParameter('title', i, '') as string; + const updateScope = this.getNodeParameter('updateScope', i, 'this') as string; + const additionalFields = this.getNodeParameter('additionalFields', i, {}) as any; + const body: any = { startDateTime: new Date(startDateTime).toISOString(), endDateTime: new Date(endDateTime).toISOString() }; + if (title) body.title = title; + if (additionalFields.description) body.description = additionalFields.description; + if (additionalFields.resourceId) body.resourceId = additionalFields.resourceId; + if (additionalFields.userId) body.userId = additionalFields.userId; + if (additionalFields.resources) body.resources = parseIdList(additionalFields.resources); + if (additionalFields.participants) body.participants = parseIdList(additionalFields.participants); + if (additionalFields.invitees) body.invitees = parseIdList(additionalFields.invitees); + responseData = await makeApiRequest(this, baseUrl, session, 'POST', `/Reservations/${referenceNumber}?updateScope=${updateScope}`, body); + } else if (operation === 'delete') { + const referenceNumber = this.getNodeParameter('referenceNumber', i) as string; + const updateScope = this.getNodeParameter('updateScope', i, 'this') as string; + responseData = await makeApiRequest(this, baseUrl, session, 'DELETE', `/Reservations/${referenceNumber}?updateScope=${updateScope}`); + } else if (operation === 'approve') { + const referenceNumber = this.getNodeParameter('referenceNumber', i) as string; + responseData = await makeApiRequest(this, baseUrl, session, 'POST', `/Reservations/${referenceNumber}/Approval`); + } else if (operation === 'checkIn') { + const referenceNumber = this.getNodeParameter('referenceNumber', i) as string; + responseData = await makeApiRequest(this, baseUrl, session, 'POST', `/Reservations/${referenceNumber}/CheckIn`); + } else if (operation === 'checkOut') { + const referenceNumber = this.getNodeParameter('referenceNumber', i) as string; + responseData = await makeApiRequest(this, baseUrl, session, 'POST', `/Reservations/${referenceNumber}/CheckOut`); + } + } + + // RESOURCE + else if (resource === 'resource') { + if (operation === 'getAll') { + responseData = await makeApiRequest(this, baseUrl, session, 'GET', '/Resources/'); + } else if (operation === 'get') { + const resourceIdParam = this.getNodeParameter('resourceIdParam', i) as number; + responseData = await makeApiRequest(this, baseUrl, session, 'GET', `/Resources/${resourceIdParam}`); + } else if (operation === 'getAvailability') { + const resourceIdOptional = this.getNodeParameter('resourceIdOptional', i, '') as number | ''; + const availabilityDateTime = this.getNodeParameter('availabilityDateTime', i, '') as string; + let endpoint = '/Resources/Availability'; + if (resourceIdOptional) endpoint = `/Resources/${resourceIdOptional}/Availability`; + const qs: any = {}; + if (availabilityDateTime) qs.dateTime = new Date(availabilityDateTime).toISOString(); + responseData = await makeApiRequest(this, baseUrl, session, 'GET', endpoint, undefined, qs); + } else if (operation === 'getGroups') { + responseData = await makeApiRequest(this, baseUrl, session, 'GET', '/Resources/Groups'); + } else if (operation === 'getTypes') { + responseData = await makeApiRequest(this, baseUrl, session, 'GET', '/Resources/Types'); + } else if (operation === 'getStatuses') { + responseData = await makeApiRequest(this, baseUrl, session, 'GET', '/Resources/Status'); + } else if (operation === 'create') { + const resourceName = this.getNodeParameter('resourceName', i) as string; + const scheduleIdForResource = this.getNodeParameter('scheduleIdForResource', i) as number; + const resourceOptions = this.getNodeParameter('resourceOptions', i, {}) as any; + const body: any = { name: resourceName, scheduleId: scheduleIdForResource }; + if (resourceOptions.location) body.location = resourceOptions.location; + if (resourceOptions.contact) body.contact = resourceOptions.contact; + if (resourceOptions.description) body.description = resourceOptions.description; + if (resourceOptions.notes) body.notes = resourceOptions.notes; + if (resourceOptions.maxParticipants) body.maxParticipants = resourceOptions.maxParticipants; + if (resourceOptions.requiresApproval !== undefined) body.requiresApproval = resourceOptions.requiresApproval; + if (resourceOptions.allowMultiday !== undefined) body.allowMultiday = resourceOptions.allowMultiday; + if (resourceOptions.requiresCheckIn !== undefined) body.requiresCheckIn = resourceOptions.requiresCheckIn; + if (resourceOptions.autoReleaseMinutes) body.autoReleaseMinutes = resourceOptions.autoReleaseMinutes; + if (resourceOptions.color) body.color = resourceOptions.color; + if (resourceOptions.statusId !== undefined) body.statusId = resourceOptions.statusId; + responseData = await makeApiRequest(this, baseUrl, session, 'POST', '/Resources/', body); + } else if (operation === 'update') { + const resourceIdParam = this.getNodeParameter('resourceIdParam', i) as number; + const resourceName = this.getNodeParameter('resourceName', i) as string; + const resourceOptions = this.getNodeParameter('resourceOptions', i, {}) as any; + const body: any = { name: resourceName }; + if (resourceOptions.location) body.location = resourceOptions.location; + if (resourceOptions.contact) body.contact = resourceOptions.contact; + if (resourceOptions.description) body.description = resourceOptions.description; + if (resourceOptions.notes) body.notes = resourceOptions.notes; + if (resourceOptions.maxParticipants) body.maxParticipants = resourceOptions.maxParticipants; + if (resourceOptions.requiresApproval !== undefined) body.requiresApproval = resourceOptions.requiresApproval; + if (resourceOptions.allowMultiday !== undefined) body.allowMultiday = resourceOptions.allowMultiday; + if (resourceOptions.requiresCheckIn !== undefined) body.requiresCheckIn = resourceOptions.requiresCheckIn; + if (resourceOptions.autoReleaseMinutes) body.autoReleaseMinutes = resourceOptions.autoReleaseMinutes; + if (resourceOptions.color) body.color = resourceOptions.color; + if (resourceOptions.statusId !== undefined) body.statusId = resourceOptions.statusId; + responseData = await makeApiRequest(this, baseUrl, session, 'POST', `/Resources/${resourceIdParam}`, body); + } else if (operation === 'delete') { + const resourceIdParam = this.getNodeParameter('resourceIdParam', i) as number; + responseData = await makeApiRequest(this, baseUrl, session, 'DELETE', `/Resources/${resourceIdParam}`); + } + } + + // SCHEDULE + else if (resource === 'schedule') { + if (operation === 'getAll') { + responseData = await makeApiRequest(this, baseUrl, session, 'GET', '/Schedules/'); + } else if (operation === 'get') { + const scheduleId = this.getNodeParameter('scheduleId', i) as number; + responseData = await makeApiRequest(this, baseUrl, session, 'GET', `/Schedules/${scheduleId}`); + } else if (operation === 'getSlots') { + const scheduleId = this.getNodeParameter('scheduleId', i) as number; + const slotsFilters = this.getNodeParameter('slotsFilters', i, {}) as any; + const qs: any = {}; + if (slotsFilters.resourceId) qs.resourceId = slotsFilters.resourceId; + if (slotsFilters.startDateTime) qs.startDateTime = new Date(slotsFilters.startDateTime).toISOString(); + if (slotsFilters.endDateTime) qs.endDateTime = new Date(slotsFilters.endDateTime).toISOString(); + responseData = await makeApiRequest(this, baseUrl, session, 'GET', `/Schedules/${scheduleId}/Slots`, undefined, qs); + } + } + + // USER + else if (resource === 'user') { + if (operation === 'getAll') { + const userFilters = this.getNodeParameter('userFilters', i, {}) as any; + const qs: any = {}; + if (userFilters.username) qs.username = userFilters.username; + if (userFilters.email) qs.email = userFilters.email; + if (userFilters.firstName) qs.firstName = userFilters.firstName; + if (userFilters.lastName) qs.lastName = userFilters.lastName; + if (userFilters.organization) qs.organization = userFilters.organization; + responseData = await makeApiRequest(this, baseUrl, session, 'GET', '/Users/', undefined, qs); + } else if (operation === 'get') { + const userId = this.getNodeParameter('userId', i) as number; + responseData = await makeApiRequest(this, baseUrl, session, 'GET', `/Users/${userId}`); + } else if (operation === 'create') { + const emailAddress = this.getNodeParameter('emailAddress', i) as string; + const userName = this.getNodeParameter('userName', i) as string; + const pw = this.getNodeParameter('password', i) as string; + const firstName = this.getNodeParameter('firstName', i) as string; + const lastName = this.getNodeParameter('lastName', i) as string; + const userOptions = this.getNodeParameter('userOptions', i, {}) as any; + const body: any = { emailAddress, userName, password: pw, firstName, lastName }; + if (userOptions.timezone) body.timezone = userOptions.timezone; + if (userOptions.language) body.language = userOptions.language; + if (userOptions.phone) body.phone = userOptions.phone; + if (userOptions.organization) body.organization = userOptions.organization; + if (userOptions.position) body.position = userOptions.position; + if (userOptions.groups) body.groups = parseIdList(userOptions.groups); + responseData = await makeApiRequest(this, baseUrl, session, 'POST', '/Users/', body); + } else if (operation === 'update') { + const userId = this.getNodeParameter('userId', i) as number; + const firstName = this.getNodeParameter('firstName', i) as string; + const lastName = this.getNodeParameter('lastName', i) as string; + const userOptions = this.getNodeParameter('userOptions', i, {}) as any; + const body: any = { firstName, lastName }; + if (userOptions.timezone) body.timezone = userOptions.timezone; + if (userOptions.language) body.language = userOptions.language; + if (userOptions.phone) body.phone = userOptions.phone; + if (userOptions.organization) body.organization = userOptions.organization; + if (userOptions.position) body.position = userOptions.position; + if (userOptions.groups) body.groups = parseIdList(userOptions.groups); + responseData = await makeApiRequest(this, baseUrl, session, 'POST', `/Users/${userId}`, body); + } else if (operation === 'updatePassword') { + const userId = this.getNodeParameter('userId', i) as number; + const pw = this.getNodeParameter('password', i) as string; + responseData = await makeApiRequest(this, baseUrl, session, 'POST', `/Users/${userId}/Password`, { password: pw }); + } else if (operation === 'delete') { + const userId = this.getNodeParameter('userId', i) as number; + responseData = await makeApiRequest(this, baseUrl, session, 'DELETE', `/Users/${userId}`); + } + } + + // ACCOUNT + else if (resource === 'account') { + if (operation === 'get') { + const accountUserId = this.getNodeParameter('accountUserId', i) as number; + responseData = await makeApiRequest(this, baseUrl, session, 'GET', `/Accounts/${accountUserId}`); + } else if (operation === 'create') { + const accountData = this.getNodeParameter('accountData', i, {}) as any; + const body: any = {}; + if (accountData.emailAddress) body.emailAddress = accountData.emailAddress; + if (accountData.userName) body.userName = accountData.userName; + if (accountData.password) body.password = accountData.password; + if (accountData.firstName) body.firstName = accountData.firstName; + if (accountData.lastName) body.lastName = accountData.lastName; + if (accountData.timezone) body.timezone = accountData.timezone; + if (accountData.language) body.language = accountData.language; + if (accountData.phone) body.phone = accountData.phone; + if (accountData.organization) body.organization = accountData.organization; + if (accountData.position) body.position = accountData.position; + if (accountData.acceptTermsOfService !== undefined) body.acceptTermsOfService = accountData.acceptTermsOfService; + responseData = await makeApiRequest(this, baseUrl, session, 'POST', '/Accounts/', body); + } else if (operation === 'update') { + const accountUserId = this.getNodeParameter('accountUserId', i) as number; + const accountData = this.getNodeParameter('accountData', i, {}) as any; + const body: any = {}; + if (accountData.emailAddress) body.emailAddress = accountData.emailAddress; + if (accountData.userName) body.userName = accountData.userName; + if (accountData.firstName) body.firstName = accountData.firstName; + if (accountData.lastName) body.lastName = accountData.lastName; + if (accountData.timezone) body.timezone = accountData.timezone; + if (accountData.language) body.language = accountData.language; + if (accountData.phone) body.phone = accountData.phone; + if (accountData.organization) body.organization = accountData.organization; + if (accountData.position) body.position = accountData.position; + responseData = await makeApiRequest(this, baseUrl, session, 'POST', `/Accounts/${accountUserId}`, body); + } else if (operation === 'updatePassword') { + const accountUserId = this.getNodeParameter('accountUserId', i) as number; + const passwordChange = this.getNodeParameter('passwordChange', i, {}) as any; + const passwords = passwordChange.passwords || {}; + responseData = await makeApiRequest(this, baseUrl, session, 'POST', `/Accounts/${accountUserId}/Password`, { + currentPassword: passwords.currentPassword, + newPassword: passwords.newPassword, + }); + } + } + + // GROUP + else if (resource === 'group') { + if (operation === 'getAll') { + responseData = await makeApiRequest(this, baseUrl, session, 'GET', '/Groups/'); + } else if (operation === 'get') { + const groupId = this.getNodeParameter('groupId', i) as number; + responseData = await makeApiRequest(this, baseUrl, session, 'GET', `/Groups/${groupId}`); + } else if (operation === 'create') { + const groupName = this.getNodeParameter('groupName', i) as string; + const isDefault = this.getNodeParameter('isDefault', i, false) as boolean; + responseData = await makeApiRequest(this, baseUrl, session, 'POST', '/Groups/', { name: groupName, isDefault }); + } else if (operation === 'update') { + const groupId = this.getNodeParameter('groupId', i) as number; + const groupName = this.getNodeParameter('groupName', i) as string; + const isDefault = this.getNodeParameter('isDefault', i, false) as boolean; + responseData = await makeApiRequest(this, baseUrl, session, 'POST', `/Groups/${groupId}`, { name: groupName, isDefault }); + } else if (operation === 'delete') { + const groupId = this.getNodeParameter('groupId', i) as number; + responseData = await makeApiRequest(this, baseUrl, session, 'DELETE', `/Groups/${groupId}`); + } else if (operation === 'changeRoles') { + const groupId = this.getNodeParameter('groupId', i) as number; + const roleIds = this.getNodeParameter('roleIds', i, '') as string; + responseData = await makeApiRequest(this, baseUrl, session, 'POST', `/Groups/${groupId}/Roles`, { roleIds: parseIdList(roleIds) }); + } else if (operation === 'changePermissions') { + const groupId = this.getNodeParameter('groupId', i) as number; + const permissionResourceIds = this.getNodeParameter('permissionResourceIds', i, '') as string; + responseData = await makeApiRequest(this, baseUrl, session, 'POST', `/Groups/${groupId}/Permissions`, { resourceIds: parseIdList(permissionResourceIds) }); + } else if (operation === 'changeUsers') { + const groupId = this.getNodeParameter('groupId', i) as number; + const groupUserIds = this.getNodeParameter('groupUserIds', i, '') as string; + responseData = await makeApiRequest(this, baseUrl, session, 'POST', `/Groups/${groupId}/Users`, { userIds: parseIdList(groupUserIds) }); + } + } + + // ACCESSORY + else if (resource === 'accessory') { + if (operation === 'getAll') { + responseData = await makeApiRequest(this, baseUrl, session, 'GET', '/Accessories/'); + } else if (operation === 'get') { + const accessoryId = this.getNodeParameter('accessoryId', i) as number; + responseData = await makeApiRequest(this, baseUrl, session, 'GET', `/Accessories/${accessoryId}`); + } + } + + // ATTRIBUTE + else if (resource === 'attribute') { + if (operation === 'get') { + const attributeId = this.getNodeParameter('attributeId', i) as number; + responseData = await makeApiRequest(this, baseUrl, session, 'GET', `/Attributes/${attributeId}`); + } else if (operation === 'getByCategory') { + const categoryId = this.getNodeParameter('categoryId', i) as number; + responseData = await makeApiRequest(this, baseUrl, session, 'GET', `/Attributes/Category/${categoryId}`); + } else if (operation === 'create') { + const attributeLabel = this.getNodeParameter('attributeLabel', i) as string; + const attributeType = this.getNodeParameter('attributeType', i) as number; + const categoryId = this.getNodeParameter('categoryId', i) as number; + const attributeOptions = this.getNodeParameter('attributeOptions', i, {}) as any; + const body: any = { label: attributeLabel, type: attributeType, categoryId }; + if (attributeOptions.required !== undefined) body.required = attributeOptions.required; + if (attributeOptions.adminOnly !== undefined) body.adminOnly = attributeOptions.adminOnly; + if (attributeOptions.isPrivate !== undefined) body.isPrivate = attributeOptions.isPrivate; + if (attributeOptions.sortOrder !== undefined) body.sortOrder = attributeOptions.sortOrder; + if (attributeOptions.regex) body.regex = attributeOptions.regex; + if (attributeOptions.possibleValues) body.possibleValues = attributeOptions.possibleValues.split(',').map((v: string) => v.trim()); + responseData = await makeApiRequest(this, baseUrl, session, 'POST', '/Attributes/', body); + } else if (operation === 'update') { + const attributeId = this.getNodeParameter('attributeId', i) as number; + const attributeLabel = this.getNodeParameter('attributeLabel', i) as string; + const attributeType = this.getNodeParameter('attributeType', i) as number; + const attributeOptions = this.getNodeParameter('attributeOptions', i, {}) as any; + const body: any = { label: attributeLabel, type: attributeType }; + if (attributeOptions.required !== undefined) body.required = attributeOptions.required; + if (attributeOptions.adminOnly !== undefined) body.adminOnly = attributeOptions.adminOnly; + if (attributeOptions.isPrivate !== undefined) body.isPrivate = attributeOptions.isPrivate; + if (attributeOptions.sortOrder !== undefined) body.sortOrder = attributeOptions.sortOrder; + if (attributeOptions.regex) body.regex = attributeOptions.regex; + if (attributeOptions.possibleValues) body.possibleValues = attributeOptions.possibleValues.split(',').map((v: string) => v.trim()); + responseData = await makeApiRequest(this, baseUrl, session, 'POST', `/Attributes/${attributeId}`, body); + } else if (operation === 'delete') { + const attributeId = this.getNodeParameter('attributeId', i) as number; + responseData = await makeApiRequest(this, baseUrl, session, 'DELETE', `/Attributes/${attributeId}`); + } + } + + // Process response + if (responseData) { + if (Array.isArray(responseData)) { + returnData.push(...responseData.map(item => ({ json: item }))); + } else if (responseData.reservations) { + returnData.push(...responseData.reservations.map((item: any) => ({ json: item }))); + } else if (responseData.resources) { + returnData.push(...responseData.resources.map((item: any) => ({ json: item }))); + } else if (responseData.schedules) { + returnData.push(...responseData.schedules.map((item: any) => ({ json: item }))); + } else if (responseData.users) { + returnData.push(...responseData.users.map((item: any) => ({ json: item }))); + } else if (responseData.groups) { + returnData.push(...responseData.groups.map((item: any) => ({ json: item }))); + } else if (responseData.accessories) { + returnData.push(...responseData.accessories.map((item: any) => ({ json: item }))); + } else if (responseData.attributes) { + returnData.push(...responseData.attributes.map((item: any) => ({ json: item }))); + } else { + returnData.push({ json: responseData }); + } + } + + } catch (error: any) { + if (this.continueOnFail()) { + returnData.push({ json: { error: error.message } }); + continue; + } + throw error; + } + } + } finally { + await signOut(this, baseUrl, session); + } + + return [returnData]; + } +} diff --git a/custom-nodes/nodes/LibreBooking/librebooking.svg b/custom-nodes/nodes/LibreBooking/librebooking.svg new file mode 100644 index 0000000..81306a9 --- /dev/null +++ b/custom-nodes/nodes/LibreBooking/librebooking.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/custom-nodes/nodes/LibreBookingTrigger/LibreBookingTrigger.node.ts b/custom-nodes/nodes/LibreBookingTrigger/LibreBookingTrigger.node.ts new file mode 100644 index 0000000..5e2e9ae --- /dev/null +++ b/custom-nodes/nodes/LibreBookingTrigger/LibreBookingTrigger.node.ts @@ -0,0 +1,352 @@ +import { + INodeType, + INodeTypeDescription, + IPollFunctions, + INodeExecutionData, + NodeApiError, + NodeOperationError, +} from 'n8n-workflow'; + +interface LibreBookingSession { + sessionToken: string; + userId: number; + sessionExpires: string; +} + +interface ReservationData { + referenceNumber: string; + startDate: string; + endDate: string; + title: string; + resourceId: number; + userId: number; + [key: string]: any; +} + +/** + * Authentifizierung bei LibreBooking + */ +async function authenticateTrigger( + pollFunctions: IPollFunctions, + baseUrl: string, + username: string, + password: string, +): Promise { + try { + const response = await pollFunctions.helpers.httpRequest({ + method: 'POST', + url: `${baseUrl}/Web/Services/index.php/Authentication/Authenticate`, + headers: { 'Content-Type': 'application/json' }, + body: { username, password }, + json: true, + }); + + if (!response.isAuthenticated) { + throw new NodeOperationError( + pollFunctions.getNode(), + 'Authentifizierung fehlgeschlagen', + ); + } + + return { + sessionToken: response.sessionToken, + userId: response.userId, + sessionExpires: response.sessionExpires, + }; + } catch (error: any) { + throw new NodeApiError(pollFunctions.getNode(), error, { + message: 'Authentifizierung fehlgeschlagen', + }); + } +} + +/** + * Abmeldung + */ +async function signOutTrigger( + pollFunctions: IPollFunctions, + baseUrl: string, + session: LibreBookingSession, +): Promise { + try { + await pollFunctions.helpers.httpRequest({ + method: 'POST', + url: `${baseUrl}/Web/Services/index.php/Authentication/SignOut`, + headers: { 'Content-Type': 'application/json' }, + body: { + userId: session.userId, + sessionToken: session.sessionToken, + }, + json: true, + }); + } catch (error) { + // Ignoriere SignOut-Fehler + } +} + +/** + * Reservierungen abrufen + */ +async function getReservations( + pollFunctions: IPollFunctions, + baseUrl: string, + session: LibreBookingSession, + startDateTime: string, + endDateTime: string, + filters: any, +): Promise { + const qs: any = { + startDateTime, + endDateTime, + }; + + if (filters.resourceId) qs.resourceId = filters.resourceId; + if (filters.scheduleId) qs.scheduleId = filters.scheduleId; + if (filters.userId) qs.userId = filters.userId; + + const response = await pollFunctions.helpers.httpRequest({ + method: 'GET', + url: `${baseUrl}/Web/Services/index.php/Reservations/`, + headers: { + 'Content-Type': 'application/json', + 'X-Booked-SessionToken': session.sessionToken, + 'X-Booked-UserId': session.userId.toString(), + }, + qs, + json: true, + }); + + return response.reservations || []; +} + +/** + * Detaillierte Reservierungsdaten abrufen + */ +async function getReservationDetails( + pollFunctions: IPollFunctions, + baseUrl: string, + session: LibreBookingSession, + referenceNumber: string, +): Promise { + const response = await pollFunctions.helpers.httpRequest({ + method: 'GET', + url: `${baseUrl}/Web/Services/index.php/Reservations/${referenceNumber}`, + headers: { + 'Content-Type': 'application/json', + 'X-Booked-SessionToken': session.sessionToken, + 'X-Booked-UserId': session.userId.toString(), + }, + json: true, + }); + + return response; +} + +/** + * Zeitfenster berechnen + */ +function getTimeWindow(timeWindow: string): { start: string; end: string } { + const now = new Date(); + const start = now.toISOString(); + + let endDate = new Date(now); + switch (timeWindow) { + case '7days': + endDate.setDate(endDate.getDate() + 7); + break; + case '14days': + endDate.setDate(endDate.getDate() + 14); + break; + case '30days': + endDate.setDate(endDate.getDate() + 30); + break; + case '90days': + endDate.setDate(endDate.getDate() + 90); + break; + default: + endDate.setDate(endDate.getDate() + 14); + } + + return { + start, + end: endDate.toISOString(), + }; +} + +/** + * Eindeutigen Schlüssel für Reservierung generieren + */ +function getReservationKey(reservation: ReservationData): string { + return `${reservation.referenceNumber}_${reservation.startDate}_${reservation.endDate}_${reservation.title || ''}`; +} + +/** + * LibreBooking Trigger Node + */ +export class LibreBookingTrigger implements INodeType { + description: INodeTypeDescription = { + displayName: 'LibreBooking Trigger', + name: 'libreBookingTrigger', + icon: 'file:librebooking.svg', + group: ['trigger'], + version: 1, + description: 'Wird bei neuen oder geänderten Reservierungen in LibreBooking ausgelöst', + subtitle: '={{$parameter["event"]}}', + defaults: { + name: 'LibreBooking Trigger', + }, + inputs: [], + outputs: ['main'], + credentials: [ + { + name: 'libreBookingApi', + required: true, + }, + ], + polling: true, + properties: [ + { + displayName: 'Event', + name: 'event', + type: 'options', + options: [ + { name: 'Neue Reservierung', value: 'newReservation', description: 'Wird bei neuen Reservierungen ausgelöst' }, + { name: 'Geänderte Reservierung', value: 'updatedReservation', description: 'Wird bei geänderten Reservierungen ausgelöst' }, + { name: 'Alle Reservierungen', value: 'allReservations', description: 'Wird bei neuen und geänderten Reservierungen ausgelöst' }, + ], + default: 'newReservation', + }, + { + displayName: 'Filter', + name: 'filters', + type: 'collection', + placeholder: 'Filter hinzufügen', + default: {}, + options: [ + { displayName: 'Ressourcen-ID', name: 'resourceId', type: 'number', default: '' }, + { displayName: 'Zeitplan-ID', name: 'scheduleId', type: 'number', default: '' }, + { displayName: 'Benutzer-ID', name: 'userId', type: 'number', default: '' }, + ], + }, + { + displayName: 'Zeitfenster', + name: 'timeWindow', + type: 'options', + options: [ + { name: 'Nächste 7 Tage', value: '7days' }, + { name: 'Nächste 14 Tage', value: '14days' }, + { name: 'Nächste 30 Tage', value: '30days' }, + { name: 'Nächste 90 Tage', value: '90days' }, + ], + default: '14days', + }, + { + displayName: 'Optionen', + name: 'options', + type: 'collection', + placeholder: 'Option hinzufügen', + default: {}, + options: [ + { displayName: 'Detaillierte Daten Abrufen', name: 'fetchDetails', type: 'boolean', default: false }, + ], + }, + ], + }; + + async poll(this: IPollFunctions): Promise { + const credentials = await this.getCredentials('libreBookingApi'); + const baseUrl = (credentials.url as string).replace(/\/$/, ''); + const username = credentials.username as string; + const password = credentials.password as string; + + const event = this.getNodeParameter('event') as string; + const filters = this.getNodeParameter('filters', {}) as any; + const timeWindow = this.getNodeParameter('timeWindow', '14days') as string; + const options = this.getNodeParameter('options', {}) as any; + + const workflowStaticData = this.getWorkflowStaticData('node'); + const previousReservations = (workflowStaticData.reservations as Record) || {}; + + let session: LibreBookingSession; + try { + session = await authenticateTrigger(this, baseUrl, username, password); + } catch (error) { + throw error; + } + + try { + const { start, end } = getTimeWindow(timeWindow); + + const reservations = await getReservations( + this, + baseUrl, + session, + start, + end, + filters, + ); + + const returnData: INodeExecutionData[] = []; + const currentReservations: Record = {}; + + for (const reservation of reservations) { + const refNumber = reservation.referenceNumber; + const reservationKey = getReservationKey(reservation); + currentReservations[refNumber] = reservationKey; + + const isNew = !previousReservations[refNumber]; + const isUpdated = previousReservations[refNumber] && previousReservations[refNumber] !== reservationKey; + + let shouldTrigger = false; + let eventType = ''; + + if (event === 'newReservation' && isNew) { + shouldTrigger = true; + eventType = 'new'; + } else if (event === 'updatedReservation' && isUpdated) { + shouldTrigger = true; + eventType = 'updated'; + } else if (event === 'allReservations' && (isNew || isUpdated)) { + shouldTrigger = true; + eventType = isNew ? 'new' : 'updated'; + } + + if (shouldTrigger) { + let reservationData = reservation; + + if (options.fetchDetails) { + try { + reservationData = await getReservationDetails( + this, + baseUrl, + session, + refNumber, + ); + } catch (error) { + reservationData = reservation; + } + } + + returnData.push({ + json: { + ...reservationData, + _eventType: eventType, + _triggeredAt: new Date().toISOString(), + }, + }); + } + } + + workflowStaticData.reservations = currentReservations; + + if (returnData.length === 0) { + return null; + } + + return [returnData]; + + } finally { + await signOutTrigger(this, baseUrl, session); + } + } +} diff --git a/custom-nodes/nodes/LibreBookingTrigger/librebooking.svg b/custom-nodes/nodes/LibreBookingTrigger/librebooking.svg new file mode 100644 index 0000000..81306a9 --- /dev/null +++ b/custom-nodes/nodes/LibreBookingTrigger/librebooking.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/custom-nodes/package.json b/custom-nodes/package.json new file mode 100644 index 0000000..17c52f3 --- /dev/null +++ b/custom-nodes/package.json @@ -0,0 +1,35 @@ +{ + "name": "n8n-nodes-librebooking", + "version": "1.0.0", + "description": "LibreBooking n8n Community Node - Custom Nodes Version für Docker Integration", + "keywords": [ + "n8n-community-node-package", + "librebooking", + "booking", + "reservation" + ], + "main": "dist/index.js", + "scripts": { + "build": "tsc && npm run copy-icons", + "copy-icons": "mkdir -p dist/nodes/LibreBooking dist/nodes/LibreBookingTrigger && cp nodes/LibreBooking/*.svg dist/nodes/LibreBooking/ && cp nodes/LibreBookingTrigger/*.svg dist/nodes/LibreBookingTrigger/", + "clean": "rm -rf dist", + "rebuild": "npm run clean && npm run build" + }, + "n8n": { + "n8nNodesApiVersion": 1, + "credentials": [ + "dist/credentials/LibreBookingApi.credentials.js" + ], + "nodes": [ + "dist/nodes/LibreBooking/LibreBooking.node.js", + "dist/nodes/LibreBookingTrigger/LibreBookingTrigger.node.js" + ] + }, + "devDependencies": { + "typescript": "^5.3.0" + }, + "peerDependencies": { + "n8n-workflow": ">=1.0.0" + }, + "license": "MIT" +} diff --git a/custom-nodes/tsconfig.json b/custom-nodes/tsconfig.json new file mode 100644 index 0000000..88c33d7 --- /dev/null +++ b/custom-nodes/tsconfig.json @@ -0,0 +1,31 @@ +{ + "compilerOptions": { + "target": "ES2019", + "module": "commonjs", + "lib": ["ES2019", "ES2020.Promise"], + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "outDir": "./dist", + "rootDir": ".", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "moduleResolution": "node", + "noImplicitAny": true, + "noImplicitReturns": true, + "noUnusedLocals": false, + "noUnusedParameters": false + }, + "include": [ + "nodes/**/*.ts", + "credentials/**/*.ts" + ], + "exclude": [ + "node_modules", + "dist", + "test" + ] +} diff --git a/docker-compose.example.yml b/docker-compose.example.yml new file mode 100644 index 0000000..10e4bb4 --- /dev/null +++ b/docker-compose.example.yml @@ -0,0 +1,131 @@ +# Vollständige Docker Compose Beispiel-Konfiguration +# n8n mit LibreBooking Node - Ready to Use +# +# Verwendung: +# 1. cp docker-compose.example.yml docker-compose.yml +# 2. cp .env.docker .env +# 3. .env Datei anpassen +# 4. docker-compose up -d + +version: '3.8' + +services: + n8n: + # Verwende das vorgefertigte Image mit LibreBooking Node + build: + context: . + dockerfile: Dockerfile.custom-nodes + args: + N8N_VERSION: latest + + # Oder nutze das offizielle Image mit Volume-Mount: + # image: n8nio/n8n:latest + + container_name: n8n-librebooking + restart: unless-stopped + + ports: + - "${N8N_PORT:-5678}:5678" + + environment: + # Basis-Konfiguration + - N8N_HOST=${N8N_HOST:-localhost} + - N8N_PORT=5678 + - N8N_PROTOCOL=${N8N_PROTOCOL:-http} + - WEBHOOK_URL=${WEBHOOK_URL:-http://localhost:5678/} + + # Authentifizierung (aktivieren für Produktion!) + - N8N_BASIC_AUTH_ACTIVE=${N8N_BASIC_AUTH_ACTIVE:-false} + - N8N_BASIC_AUTH_USER=${N8N_BASIC_AUTH_USER:-admin} + - N8N_BASIC_AUTH_PASSWORD=${N8N_BASIC_AUTH_PASSWORD:-changeme} + + # Custom Nodes + - N8N_CUSTOM_EXTENSIONS=/home/node/.n8n/custom + - N8N_COMMUNITY_NODES_ENABLED=true + + # Zeitzone + - GENERIC_TIMEZONE=${TZ:-Europe/Berlin} + - TZ=${TZ:-Europe/Berlin} + + # Logging + - N8N_LOG_LEVEL=${N8N_LOG_LEVEL:-info} + + # Execution Settings + - EXECUTIONS_DATA_SAVE_ON_ERROR=all + - EXECUTIONS_DATA_SAVE_ON_SUCCESS=all + - EXECUTIONS_DATA_SAVE_MANUAL_EXECUTIONS=true + + volumes: + # Persistente n8n Daten + - n8n_data:/home/node/.n8n + + # Custom Nodes (wenn nicht im Image gebaut) + # - ./custom-nodes:/home/node/.n8n/custom/n8n-nodes-librebooking:ro + + healthcheck: + test: ["CMD", "wget", "-qO-", "http://localhost:5678/healthz"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 30s + + networks: + - n8n_network + + # Optional: PostgreSQL Datenbank für n8n + # Für Produktion empfohlen statt SQLite + postgres: + image: postgres:15-alpine + container_name: n8n-postgres + restart: unless-stopped + profiles: + - with-postgres + + environment: + - POSTGRES_USER=${POSTGRES_USER:-n8n} + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-n8n_password} + - POSTGRES_DB=${POSTGRES_DB:-n8n} + + volumes: + - postgres_data:/var/lib/postgresql/data + + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-n8n}"] + interval: 10s + timeout: 5s + retries: 5 + + networks: + - n8n_network + + # Optional: Redis für Queue Mode + redis: + image: redis:7-alpine + container_name: n8n-redis + restart: unless-stopped + profiles: + - with-redis + + volumes: + - redis_data:/data + + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 5s + retries: 5 + + networks: + - n8n_network + +volumes: + n8n_data: + driver: local + postgres_data: + driver: local + redis_data: + driver: local + +networks: + n8n_network: + driver: bridge diff --git a/docker-compose.override.yml b/docker-compose.override.yml new file mode 100644 index 0000000..4a4a01e --- /dev/null +++ b/docker-compose.override.yml @@ -0,0 +1,21 @@ +# Docker Compose Override für LibreBooking n8n Node +# +# WICHTIG: custom-nodes Volume NICHT als read-only (:ro) mounten! +# Der Node benötigt Schreibrechte für npm install/build. +# +# Für read-only Volumes siehe: docker-compose.readonly.yml +# und führen Sie vorher build-on-host.sh aus. + +version: '3.8' + +services: + n8n: + volumes: + # LibreBooking Custom Node + # OHNE :ro - Der Node muss beim Start gebaut werden können! + - ./custom-nodes:/home/node/.n8n/custom/n8n-nodes-librebooking + + environment: + # Custom Nodes aktivieren + - N8N_CUSTOM_EXTENSIONS=/home/node/.n8n/custom + - N8N_COMMUNITY_NODES_ENABLED=true diff --git a/docker-compose.readonly.yml b/docker-compose.readonly.yml new file mode 100644 index 0000000..dab572c --- /dev/null +++ b/docker-compose.readonly.yml @@ -0,0 +1,25 @@ +# Docker Compose Override für READ-ONLY Volume Mount +# +# VORAUSSETZUNG: Node wurde auf dem Host gebaut! +# Führen Sie zuerst aus: ./build-on-host.sh +# +# Verwendung: +# docker compose -f docker-compose.yml -f docker-compose.readonly.yml up -d +# +# Oder setzen Sie in Ihrer Umgebung: +# export COMPOSE_FILE=docker-compose.yml:docker-compose.readonly.yml + +version: '3.8' + +services: + n8n: + volumes: + # LibreBooking Custom Node - READ-ONLY + # Das dist-for-docker Verzeichnis wurde mit build-on-host.sh erstellt + # und enthält bereits alle kompilierten Dateien. + - ./dist-for-docker:/home/node/.n8n/custom/n8n-nodes-librebooking:ro + + environment: + # Custom Nodes aktivieren + - N8N_CUSTOM_EXTENSIONS=/home/node/.n8n/custom + - N8N_COMMUNITY_NODES_ENABLED=true diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..a37c5b8 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,65 @@ +# Docker Compose für n8n mit LibreBooking Node +# +# Verwendung: +# docker-compose up -d # Im Hintergrund starten +# docker-compose logs -f # Logs anzeigen +# docker-compose down # Stoppen und entfernen +# docker-compose build --no-cache # Neu bauen + +version: '3.8' + +services: + n8n: + build: + context: . + dockerfile: Dockerfile + container_name: n8n-librebooking + restart: unless-stopped + ports: + - "5678:5678" + environment: + # Basis-Konfiguration + - N8N_BASIC_AUTH_ACTIVE=true + - N8N_BASIC_AUTH_USER=${N8N_BASIC_AUTH_USER:-admin} + - N8N_BASIC_AUTH_PASSWORD=${N8N_BASIC_AUTH_PASSWORD:-changeme} + + # Webhook-URL (für Produktionsumgebung anpassen) + - WEBHOOK_URL=${WEBHOOK_URL:-http://localhost:5678/} + + # Timezone + - GENERIC_TIMEZONE=${TZ:-Europe/Berlin} + - TZ=${TZ:-Europe/Berlin} + + # Custom Extensions + - N8N_CUSTOM_EXTENSIONS=/home/node/.n8n/custom + + # Optional: Logging + - N8N_LOG_LEVEL=${N8N_LOG_LEVEL:-info} + + # Optional: Executions + - EXECUTIONS_DATA_SAVE_ON_ERROR=all + - EXECUTIONS_DATA_SAVE_ON_SUCCESS=all + - EXECUTIONS_DATA_SAVE_MANUAL_EXECUTIONS=true + volumes: + # Persistente Daten + - n8n_data:/home/node/.n8n + # Workflow-Dateien (optional) + - ./workflows:/home/node/workflows:ro + networks: + - n8n-network + healthcheck: + test: ["CMD", "wget", "-q", "--spider", "http://localhost:5678/healthz"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 30s + +volumes: + n8n_data: + driver: local + name: n8n-librebooking-data + +networks: + n8n-network: + driver: bridge + name: n8n-librebooking-network diff --git a/fix-node-installation.sh b/fix-node-installation.sh new file mode 100755 index 0000000..af8e266 --- /dev/null +++ b/fix-node-installation.sh @@ -0,0 +1,268 @@ +#!/bin/bash +# ============================================================================ +# fix-node-installation.sh - All-in-One Lösung für Node-Installation im Container +# Führt alle Schritte automatisch aus (auf dem HOST ausführen!) +# ============================================================================ + +set -e + +# Farben +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +# Standard-Werte +CONTAINER_NAME="n8n" +CONTAINER_PATH="/home/node/.n8n/custom/n8n-nodes-librebooking" +SKIP_RESTART=false +VERBOSE=false + +echo -e "${BLUE}============================================${NC}" +echo -e "${BLUE} LibreBooking Node - Auto-Fix Installation${NC}" +echo -e "${BLUE}============================================${NC}" +echo "" + +# Hilfe anzeigen +show_help() { + echo "Verwendung: $0 [OPTIONEN]" + echo "" + echo "Dieses Skript installiert den LibreBooking Node automatisch im Docker Container." + echo "" + echo "Optionen:" + echo " -c, --container NAME Container-Name (Standard: n8n)" + echo " -p, --path PATH Pfad im Container (Standard: /home/node/.n8n/custom/n8n-nodes-librebooking)" + echo " -n, --no-restart Container nicht neustarten" + echo " -v, --verbose Ausführliche Ausgabe" + echo " -h, --help Diese Hilfe anzeigen" + echo "" + echo "Beispiele:" + echo " $0 # Standard-Installation" + echo " $0 -c mein-n8n # Anderer Container-Name" + echo " $0 -p /opt/n8n/custom-nodes # Anderer Pfad" + echo " $0 -n # Ohne Neustart" + exit 0 +} + +# Parameter parsen +while [[ $# -gt 0 ]]; do + case $1 in + -c|--container) + CONTAINER_NAME="$2" + shift 2 + ;; + -p|--path) + CONTAINER_PATH="$2" + shift 2 + ;; + -n|--no-restart) + SKIP_RESTART=true + shift + ;; + -v|--verbose) + VERBOSE=true + shift + ;; + -h|--help) + show_help + ;; + *) + echo -e "${RED}Unbekannte Option: $1${NC}" + show_help + ;; + esac +done + +log() { + echo -e "${GREEN}[INFO]${NC} $1" +} + +warn() { + echo -e "${YELLOW}[WARN]${NC} $1" +} + +error() { + echo -e "${RED}[ERROR]${NC} $1" + exit 1 +} + +# Funktion: Prüft ob ein Verzeichnis im Container read-only ist +check_readonly_container() { + local container="$1" + local dir="$2" + local test_file="$dir/.write_test_$$" + + # Versuche eine Test-Datei im Container zu erstellen + if docker exec "$container" touch "$test_file" 2>/dev/null; then + docker exec "$container" rm -f "$test_file" 2>/dev/null + return 0 # Schreibbar + else + return 1 # Read-only + fi +} + +print_readonly_solution() { + echo "" + echo -e "${RED}============================================${NC}" + echo -e "${RED} PROBLEM: Read-only Volume erkannt!${NC}" + echo -e "${RED}============================================${NC}" + echo "" + echo "Das custom-nodes Verzeichnis ist als read-only gemountet." + echo "npm install und npm run build benötigen Schreibrechte." + echo "" + echo "LÖSUNGEN:" + echo "" + echo "1. Volume OHNE :ro mounten (empfohlen):" + echo " In docker-compose.yml oder docker-compose.override.yml:" + echo "" + echo " volumes:" + echo " - ./custom-nodes:/home/node/.n8n/custom/n8n-nodes-librebooking" + echo " # ENTFERNEN: :ro am Ende!" + echo "" + echo "2. Auf dem Host bauen (für read-only Volumes):" + echo " ./build-on-host.sh" + echo " # Dann docker-compose.readonly.yml verwenden" + echo "" + echo "3. docker-compose.override.yml anpassen:" + echo " cp docker-compose.override.yml docker-compose.override.yml.bak" + echo " # Entfernen Sie ':ro' aus der Volume-Definition" + echo "" + echo "Siehe: TROUBLESHOOTING.md und DOCKER-INTEGRATION.md" +} + +# Schritt 1: Docker prüfen +log "Prüfe Docker..." +if ! command -v docker &>/dev/null; then + error "Docker ist nicht installiert!" +fi + +# Schritt 2: Container prüfen +log "Prüfe Container '$CONTAINER_NAME'..." +if ! docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then + # Prüfe ob Container existiert aber nicht läuft + if docker ps -a --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then + warn "Container '$CONTAINER_NAME' existiert aber läuft nicht." + log "Starte Container..." + docker start "$CONTAINER_NAME" + sleep 3 + else + error "Container '$CONTAINER_NAME' nicht gefunden! Prüfen Sie den Namen mit: docker ps -a" + fi +fi +log "Container '$CONTAINER_NAME' läuft ✓" + +# Schritt 3: Prüfe ob Pfad im Container existiert +log "Prüfe Pfad im Container: $CONTAINER_PATH" +if ! docker exec "$CONTAINER_NAME" test -d "$CONTAINER_PATH"; then + # Versuche alternative Pfade + ALTERNATIVE_PATHS=( + "/home/node/.n8n/custom/n8n-nodes-librebooking" + "/home/node/.n8n/custom" + "/opt/n8n/custom-nodes" + "/data/custom-nodes" + ) + + FOUND_PATH="" + for alt_path in "${ALTERNATIVE_PATHS[@]}"; do + if docker exec "$CONTAINER_NAME" test -f "$alt_path/package.json" 2>/dev/null; then + FOUND_PATH="$alt_path" + break + fi + done + + if [ -n "$FOUND_PATH" ]; then + warn "Angegebener Pfad nicht gefunden, verwende: $FOUND_PATH" + CONTAINER_PATH="$FOUND_PATH" + else + error "Kein Custom-Node-Verzeichnis mit package.json gefunden!\n\n Bitte stellen Sie sicher, dass die Dateien korrekt kopiert wurden.\n Beispiel: docker cp custom-nodes/. $CONTAINER_NAME:/home/node/.n8n/custom/n8n-nodes-librebooking/" + fi +fi +log "Pfad gefunden: $CONTAINER_PATH ✓" + +# Schritt 3.5: Prüfe ob Verzeichnis schreibbar ist (read-only Volume?) +log "Prüfe Schreibrechte im Container..." +if ! check_readonly_container "$CONTAINER_NAME" "$CONTAINER_PATH"; then + print_readonly_solution + exit 1 +fi +log "Schreibrechte vorhanden ✓" + +# Schritt 4: Skript in Container kopieren +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +if [ -f "$SCRIPT_DIR/install-in-container.sh" ]; then + log "Kopiere install-in-container.sh in Container..." + docker cp "$SCRIPT_DIR/install-in-container.sh" "$CONTAINER_NAME:/tmp/" +else + warn "install-in-container.sh nicht gefunden, erstelle inline..." +fi + +# Schritt 5: npm install und build im Container +log "Führe npm install aus..." +if $VERBOSE; then + docker exec -w "$CONTAINER_PATH" "$CONTAINER_NAME" npm install +else + if ! docker exec -w "$CONTAINER_PATH" "$CONTAINER_NAME" npm install 2>&1 | tail -5; then + error "npm install fehlgeschlagen!" + fi +fi +log "Dependencies installiert ✓" + +log "Führe npm run build aus..." +if $VERBOSE; then + docker exec -w "$CONTAINER_PATH" "$CONTAINER_NAME" npm run build +else + if ! docker exec -w "$CONTAINER_PATH" "$CONTAINER_NAME" npm run build 2>&1 | tail -10; then + error "npm run build fehlgeschlagen!" + fi +fi +log "Build erfolgreich ✓" + +# Schritt 6: Prüfe Ergebnis +log "Prüfe Build-Ergebnis..." +NODE_COUNT=$(docker exec "$CONTAINER_NAME" find "$CONTAINER_PATH/dist" -name "*.node.js" 2>/dev/null | wc -l) +if [ "$NODE_COUNT" -gt 0 ]; then + log "$NODE_COUNT Node-Datei(en) gefunden ✓" +else + error "Keine Node-Dateien nach Build gefunden!" +fi + +# Schritt 7: Container neustarten +if $SKIP_RESTART; then + warn "Container-Neustart übersprungen (-n Option)" + echo "" + echo -e "${YELLOW}Bitte starten Sie den Container manuell neu:${NC}" + echo " docker restart $CONTAINER_NAME" +else + log "Starte Container neu..." + docker restart "$CONTAINER_NAME" + log "Container neugestartet ✓" + + # Warte kurz auf Start + sleep 5 +fi + +# Schritt 8: Abschluss-Check (optional) +echo "" +echo -e "${GREEN}============================================${NC}" +echo -e "${GREEN} Installation abgeschlossen!${NC}" +echo -e "${GREEN}============================================${NC}" +echo "" +log "Der LibreBooking Node sollte jetzt in n8n verfügbar sein." +echo "" +echo "Nächste Schritte:" +echo " 1. Öffnen Sie n8n im Browser" +echo " 2. Erstellen Sie einen neuen Workflow" +echo " 3. Suchen Sie nach 'LibreBooking'" +echo "" +echo "Falls der Node nicht erscheint:" +echo " - Prüfen Sie die Logs: docker logs $CONTAINER_NAME 2>&1 | grep -i libre" +echo " - Führen Sie das Check-Skript aus: docker exec $CONTAINER_NAME sh /tmp/check-installation.sh" +echo "" + +# Optional: Check-Skript kopieren und ausführen +if [ -f "$SCRIPT_DIR/check-installation.sh" ]; then + docker cp "$SCRIPT_DIR/check-installation.sh" "$CONTAINER_NAME:/tmp/" + echo "Tipp: Führen Sie für einen detaillierten Status aus:" + echo " docker exec $CONTAINER_NAME sh /tmp/check-installation.sh" +fi diff --git a/git-cleanup.sh b/git-cleanup.sh new file mode 100755 index 0000000..f7184b2 --- /dev/null +++ b/git-cleanup.sh @@ -0,0 +1,32 @@ +#!/bin/bash +# ============================================================================ +# git-cleanup.sh - Löscht alte Archive und temporäre Dateien +# ============================================================================ + +set -e + +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PARENT_DIR="$(dirname "$SCRIPT_DIR")" + +echo -e "${GREEN}=== Git Cleanup ===${NC}\n" + +# Alte Archive im übergeordneten Verzeichnis löschen +echo "Lösche alte Archive in $PARENT_DIR..." +find "$PARENT_DIR" -maxdepth 1 -name "n8n-nodes-librebooking*.tar.gz" -delete 2>/dev/null && echo " ✓ .tar.gz gelöscht" || echo " - Keine .tar.gz gefunden" +find "$PARENT_DIR" -maxdepth 1 -name "n8n-nodes-librebooking*.zip" -delete 2>/dev/null && echo " ✓ .zip gelöscht" || echo " - Keine .zip gefunden" + +# Temporäre Dateien im Projekt löschen +echo "" +echo "Lösche temporäre Dateien..." +rm -rf "$SCRIPT_DIR/dist-for-docker" 2>/dev/null && echo " ✓ dist-for-docker/ gelöscht" || true +rm -rf "$SCRIPT_DIR/.tsbuildinfo" 2>/dev/null && echo " ✓ .tsbuildinfo gelöscht" || true +find "$SCRIPT_DIR" -name "*.log" -delete 2>/dev/null && echo " ✓ .log Dateien gelöscht" || true +find "$SCRIPT_DIR" -name ".DS_Store" -delete 2>/dev/null || true + +echo "" +echo -e "${GREEN}✓ Cleanup abgeschlossen${NC}" +echo "" diff --git a/git-commit.sh b/git-commit.sh new file mode 100755 index 0000000..7f5abec --- /dev/null +++ b/git-commit.sh @@ -0,0 +1,36 @@ +#!/bin/bash +# ============================================================================ +# git-commit.sh - Committet alle Änderungen +# ============================================================================ + +set -e + +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$SCRIPT_DIR" + +MESSAGE="${1:-fix: Vereinfachte Installation - auf dem Host bauen}" + +echo -e "${GREEN}=== Git Commit ===${NC}\n" + +# Status anzeigen +echo "Geänderte Dateien:" +git status --short +echo "" + +# Alles hinzufügen +git add . + +# Commit +echo "Commit mit Nachricht: $MESSAGE" +git commit -m "$MESSAGE" + +echo "" +echo -e "${GREEN}✓ Commit erfolgreich${NC}" +echo "" +echo "Zum Pushen:" +echo " git push origin main" +echo "" diff --git a/index.ts b/index.ts new file mode 100644 index 0000000..c94c56e --- /dev/null +++ b/index.ts @@ -0,0 +1,6 @@ +// n8n-nodes-librebooking +// Entry point for the LibreBooking n8n node package + +export * from './nodes/LibreBooking/LibreBooking.node'; +export * from './nodes/LibreBookingTrigger/LibreBookingTrigger.node'; +export * from './credentials/LibreBookingApi.credentials'; diff --git a/install-docker-manual.sh b/install-docker-manual.sh new file mode 100755 index 0000000..9b57de4 --- /dev/null +++ b/install-docker-manual.sh @@ -0,0 +1,238 @@ +#!/bin/bash + +# ============================================================================= +# LibreBooking n8n Node - Manuelle Docker Installation (ohne docker-compose) +# ============================================================================= +# Diese Alternative verwendet nur "docker" Befehle für maximale Kompatibilität. +# Nutzen Sie dieses Skript wenn docker-compose Probleme macht. +# +# Verwendung: ./install-docker-manual.sh [OPTIONS] +# +# Optionen: +# -p, --port PORT n8n Port (Standard: 5678) +# -n, --name NAME Container Name (Standard: n8n-librebooking) +# -f, --force Bestehenden Container ersetzen +# -h, --help Diese Hilfe anzeigen +# +# ============================================================================= + +set -e + +# Farben für Ausgabe +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Standardwerte +N8N_PORT=5678 +CONTAINER_NAME="n8n-librebooking" +FORCE=false +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Hilfsfunktionen +print_info() { + echo -e "${BLUE}[INFO]${NC} $1" +} + +print_success() { + echo -e "${GREEN}[✓]${NC} $1" +} + +print_warning() { + echo -e "${YELLOW}[!]${NC} $1" +} + +print_error() { + echo -e "${RED}[FEHLER]${NC} $1" +} + +show_help() { + echo "LibreBooking n8n Node - Manuelle Docker Installation" + echo "" + echo "Verwendung: $0 [OPTIONS]" + echo "" + echo "Optionen:" + echo " -p, --port PORT n8n Port (Standard: 5678)" + echo " -n, --name NAME Container Name (Standard: n8n-librebooking)" + echo " -f, --force Bestehenden Container ersetzen" + echo " -h, --help Diese Hilfe anzeigen" + echo "" + echo "Beispiele:" + echo " $0 # Standard-Installation" + echo " $0 -p 8080 # Anderer Port" + echo " $0 -n mein-n8n -p 9000 # Eigener Name und Port" + exit 0 +} + +# Argumente parsen +while [[ $# -gt 0 ]]; do + case $1 in + -p|--port) + N8N_PORT="$2" + shift 2 + ;; + -n|--name) + CONTAINER_NAME="$2" + shift 2 + ;; + -f|--force) + FORCE=true + shift + ;; + -h|--help) + show_help + ;; + *) + print_error "Unbekannte Option: $1" + show_help + ;; + esac +done + +echo "" +echo "=============================================" +echo " LibreBooking n8n Node - Manuelle Installation" +echo " (ohne docker-compose)" +echo "=============================================" +echo "" + +# ============================================================================= +# Voraussetzungen prüfen +# ============================================================================= + +print_info "Prüfe Voraussetzungen..." + +# Docker prüfen +if ! command -v docker &> /dev/null; then + print_error "Docker ist nicht installiert!" + echo " Bitte installieren Sie Docker: https://docs.docker.com/get-docker/" + exit 1 +fi +print_success "Docker gefunden: $(docker --version)" + +# Prüfen ob Container bereits existiert +if docker ps -a --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then + if [ "$FORCE" = true ]; then + print_warning "Bestehender Container '$CONTAINER_NAME' wird entfernt..." + docker stop "$CONTAINER_NAME" 2>/dev/null || true + docker rm "$CONTAINER_NAME" 2>/dev/null || true + else + print_error "Container '$CONTAINER_NAME' existiert bereits!" + echo " Nutzen Sie -f um ihn zu ersetzen, oder -n für einen anderen Namen." + exit 1 + fi +fi + +# ============================================================================= +# Volume und Verzeichnisse vorbereiten +# ============================================================================= + +print_info "Bereite Volumes vor..." + +# Docker Volume für n8n Daten erstellen +VOLUME_NAME="${CONTAINER_NAME}_data" +if ! docker volume ls --format '{{.Name}}' | grep -q "^${VOLUME_NAME}$"; then + docker volume create "$VOLUME_NAME" + print_success "Volume '$VOLUME_NAME' erstellt" +else + print_info "Volume '$VOLUME_NAME' existiert bereits" +fi + +# Custom Nodes Verzeichnis vorbereiten +CUSTOM_NODES_DIR="$SCRIPT_DIR/custom-nodes" +if [ ! -d "$CUSTOM_NODES_DIR" ]; then + print_error "custom-nodes Verzeichnis nicht gefunden: $CUSTOM_NODES_DIR" + exit 1 +fi + +# ============================================================================= +# Node bauen (wenn nicht bereits gebaut) +# ============================================================================= + +if [ ! -d "$CUSTOM_NODES_DIR/dist" ]; then + print_info "Baue LibreBooking Node..." + + if command -v npm &> /dev/null; then + cd "$CUSTOM_NODES_DIR" + npm install 2>/dev/null || print_warning "npm install hatte Warnungen" + npm run build 2>/dev/null || { + print_warning "Build fehlgeschlagen, versuche alternativen Ansatz..." + npx tsc 2>/dev/null || true + mkdir -p dist/nodes/LibreBooking dist/nodes/LibreBookingTrigger + cp nodes/LibreBooking/*.svg dist/nodes/LibreBooking/ 2>/dev/null || true + cp nodes/LibreBookingTrigger/*.svg dist/nodes/LibreBookingTrigger/ 2>/dev/null || true + } + cd "$SCRIPT_DIR" + print_success "Node erfolgreich gebaut" + else + print_warning "npm nicht gefunden - Node wird im Container gebaut" + fi +fi + +# ============================================================================= +# n8n Container starten +# ============================================================================= + +print_info "Starte n8n Container..." + +# Berechtigungen setzen +if [ "$(id -u)" = "0" ]; then + chown -R 1000:1000 "$CUSTOM_NODES_DIR" 2>/dev/null || true +else + sudo chown -R 1000:1000 "$CUSTOM_NODES_DIR" 2>/dev/null || print_warning "Konnte Berechtigungen nicht setzen" +fi + +# Container starten +docker run -d \ + --name "$CONTAINER_NAME" \ + --restart unless-stopped \ + -p "${N8N_PORT}:5678" \ + -v "${VOLUME_NAME}:/home/node/.n8n" \ + -v "${CUSTOM_NODES_DIR}:/home/node/.n8n/custom/n8n-nodes-librebooking:ro" \ + -e N8N_CUSTOM_EXTENSIONS=/home/node/.n8n/custom \ + -e N8N_COMMUNITY_NODES_ENABLED=true \ + -e TZ=Europe/Berlin \ + -e GENERIC_TIMEZONE=Europe/Berlin \ + n8nio/n8n:latest + +if [ $? -eq 0 ]; then + print_success "Container gestartet!" +else + print_error "Container konnte nicht gestartet werden!" + exit 1 +fi + +# Warten auf Start +print_info "Warte auf n8n Start..." +sleep 5 + +# Status prüfen +if docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then + print_success "n8n Container läuft!" +else + print_error "Container läuft nicht - prüfen Sie: docker logs $CONTAINER_NAME" + exit 1 +fi + +# ============================================================================= +# Abschluss +# ============================================================================= + +echo "" +echo "=============================================" +print_success "Installation abgeschlossen!" +echo "=============================================" +echo "" +echo "n8n ist erreichbar unter: http://localhost:${N8N_PORT}" +echo "" +echo "Nützliche Befehle:" +echo " docker logs $CONTAINER_NAME # Logs anzeigen" +echo " docker stop $CONTAINER_NAME # Container stoppen" +echo " docker start $CONTAINER_NAME # Container starten" +echo " docker restart $CONTAINER_NAME # Container neustarten" +echo " docker rm -f $CONTAINER_NAME # Container löschen" +echo "" +echo "Bei Problemen siehe: TROUBLESHOOTING.md" +echo "" diff --git a/install-docker.sh b/install-docker.sh new file mode 100755 index 0000000..fd81d55 --- /dev/null +++ b/install-docker.sh @@ -0,0 +1,454 @@ +#!/bin/bash + +# ============================================================================= +# LibreBooking n8n Node - Docker Integration Script +# ============================================================================= +# Automatische Integration des LibreBooking Nodes in bestehende n8n Docker-Installationen +# +# Verwendung: ./install-docker.sh [OPTIONS] +# +# Optionen: +# -p, --path PATH Pfad zur n8n Docker-Installation (Standard: aktuelles Verzeichnis) +# -b, --build Node im Container bauen statt vorgebaut kopieren +# -f, --force Bestehende Installation überschreiben +# -h, --help Diese Hilfe anzeigen +# +# ============================================================================= + +set -e + +# Farben für Ausgabe +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Standardwerte +N8N_PATH="." +FORCE=false +BUILD_IN_CONTAINER=false +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Hilfsfunktionen +print_info() { + echo -e "${BLUE}[INFO]${NC} $1" +} + +print_success() { + echo -e "${GREEN}[✓]${NC} $1" +} + +print_warning() { + echo -e "${YELLOW}[!]${NC} $1" +} + +print_error() { + echo -e "${RED}[FEHLER]${NC} $1" +} + +show_help() { + echo "LibreBooking n8n Node - Docker Integration Script" + echo "" + echo "Verwendung: $0 [OPTIONS]" + echo "" + echo "Optionen:" + echo " -p, --path PATH Pfad zur n8n Docker-Installation" + echo " -b, --build Node im Container bauen" + echo " -f, --force Bestehende Installation überschreiben" + echo " -h, --help Diese Hilfe anzeigen" + echo "" + echo "Beispiele:" + echo " $0 # Installation im aktuellen Verzeichnis" + echo " $0 -p /opt/n8n # Installation in /opt/n8n" + echo " $0 -f -p /home/user/n8n # Installation mit Überschreiben" + exit 0 +} + +# Argumente parsen +while [[ $# -gt 0 ]]; do + case $1 in + -p|--path) + N8N_PATH="$2" + shift 2 + ;; + -b|--build) + BUILD_IN_CONTAINER=true + shift + ;; + -f|--force) + FORCE=true + shift + ;; + -h|--help) + show_help + ;; + *) + print_error "Unbekannte Option: $1" + show_help + ;; + esac +done + +echo "" +echo "=========================================" +echo " LibreBooking n8n Node - Docker Setup" +echo "=========================================" +echo "" + +# ============================================================================= +# Voraussetzungen prüfen +# ============================================================================= + +print_info "Prüfe Voraussetzungen..." + +# Docker prüfen +if ! command -v docker &> /dev/null; then + print_error "Docker ist nicht installiert!" + echo " Bitte installieren Sie Docker: https://docs.docker.com/get-docker/" + exit 1 +fi +print_success "Docker gefunden: $(docker --version)" + +# Docker Compose prüfen - v2 bevorzugen +detect_compose_command() { + # Zuerst docker compose (v2/Plugin) prüfen - bevorzugt + if docker compose version &> /dev/null 2>&1; then + COMPOSE_CMD="docker compose" + COMPOSE_VERSION="v2" + return 0 + fi + + # Dann docker-compose (v1) prüfen + if command -v docker-compose &> /dev/null; then + # Prüfen ob es tatsächlich funktioniert (distutils Problem bei Python 3.12) + if docker-compose --version &> /dev/null 2>&1; then + COMPOSE_CMD="docker-compose" + COMPOSE_VERSION="v1" + return 0 + else + # docker-compose existiert aber funktioniert nicht + print_warning "docker-compose (v1) ist installiert, funktioniert aber nicht!" + print_warning "Dies liegt wahrscheinlich am fehlenden 'distutils' Modul (Python 3.12+)" + echo "" + echo " Mögliche Lösungen:" + echo " 1. Docker Compose v2 installieren (empfohlen):" + echo " sudo apt-get update && sudo apt-get install docker-compose-plugin" + echo "" + echo " 2. distutils für Python installieren (Workaround):" + echo " sudo apt-get install python3-distutils" + echo " # Oder für neuere Systeme:" + echo " pip3 install setuptools" + echo "" + echo " Siehe TROUBLESHOOTING.md für weitere Details." + echo "" + return 1 + fi + fi + + return 1 +} + +# Compose Command ermitteln +if detect_compose_command; then + if [ "$COMPOSE_VERSION" = "v2" ]; then + print_success "Docker Compose v2 (Plugin) gefunden: $(docker compose version --short 2>/dev/null || docker compose version)" + else + print_success "docker-compose v1 gefunden: $(docker-compose --version)" + print_warning "Empfehlung: Upgrade zu Docker Compose v2 für bessere Kompatibilität" + fi +else + print_error "Docker Compose ist nicht installiert oder funktioniert nicht!" + echo "" + echo " Installation von Docker Compose v2 (empfohlen):" + echo " sudo apt-get update && sudo apt-get install docker-compose-plugin" + echo "" + echo " Oder siehe: https://docs.docker.com/compose/install/" + echo "" + echo " Alternativ: Verwenden Sie install-docker-manual.sh für Installation ohne docker-compose" + exit 1 +fi + +# Hilfsfunktion für Compose-Befehle +run_compose() { + $COMPOSE_CMD "$@" +} + +# ============================================================================= +# Funktion: Prüft ob ein Verzeichnis read-only ist +# ============================================================================= +check_readonly() { + local dir="$1" + local test_file="$dir/.write_test_$$" + + # Versuche eine Test-Datei zu erstellen + if touch "$test_file" 2>/dev/null; then + rm -f "$test_file" 2>/dev/null + return 0 # Schreibbar + else + return 1 # Read-only + fi +} + +print_readonly_warning() { + local dir="$1" + print_error "Das Verzeichnis '$dir' ist read-only!" + echo "" + echo " Das custom-nodes Verzeichnis benötigt Schreibrechte für:" + echo " - npm install (Dependencies)" + echo " - npm run build (Kompilierung)" + echo "" + echo " LÖSUNGEN:" + echo "" + echo " 1. Volume OHNE :ro mounten (empfohlen):" + echo " volumes:" + echo " - ./custom-nodes:/home/node/.n8n/custom/n8n-nodes-librebooking" + echo " # NICHT: - ./custom-nodes:/...:ro" + echo "" + echo " 2. Auf dem Host bauen (für read-only Volumes):" + echo " ./build-on-host.sh" + echo " # Dann docker-compose.readonly.yml verwenden" + echo "" + echo " 3. Berechtigungen prüfen:" + echo " ls -la $dir" + echo " sudo chown -R 1000:1000 $dir" + echo "" + echo " Siehe: TROUBLESHOOTING.md und SECURITY.md" +} + +# Zielpfad prüfen +if [ ! -d "$N8N_PATH" ]; then + print_error "Verzeichnis existiert nicht: $N8N_PATH" + exit 1 +fi + +N8N_PATH=$(cd "$N8N_PATH" && pwd) +print_info "Zielverzeichnis: $N8N_PATH" + +# docker-compose.yml prüfen +if [ ! -f "$N8N_PATH/docker-compose.yml" ] && [ ! -f "$N8N_PATH/docker-compose.yaml" ]; then + print_warning "Keine docker-compose.yml gefunden in $N8N_PATH" + echo "" + read -p "Soll eine Beispiel-Konfiguration erstellt werden? (j/n): " CREATE_EXAMPLE + if [[ "$CREATE_EXAMPLE" =~ ^[jJyY]$ ]]; then + print_info "Kopiere Beispiel-Konfiguration..." + cp "$SCRIPT_DIR/docker-compose.example.yml" "$N8N_PATH/docker-compose.yml" + cp "$SCRIPT_DIR/.env.docker" "$N8N_PATH/.env" + print_success "Beispiel-Konfiguration erstellt" + print_warning "Bitte $N8N_PATH/.env anpassen!" + else + print_error "Abbruch: Keine docker-compose.yml vorhanden" + exit 1 + fi +fi + +# ============================================================================= +# n8n Status prüfen +# ============================================================================= + +print_info "Prüfe n8n Container Status..." + +cd "$N8N_PATH" + +N8N_RUNNING=false +if $COMPOSE_CMD ps 2>/dev/null | grep -q "n8n.*Up"; then + N8N_RUNNING=true + print_success "n8n Container läuft" +else + print_warning "n8n Container läuft nicht (wird später gestartet)" +fi + +# ============================================================================= +# Custom Nodes Verzeichnis vorbereiten +# ============================================================================= + +print_info "Bereite Custom Nodes Verzeichnis vor..." + +CUSTOM_NODES_DIR="$N8N_PATH/custom-nodes" + +# Prüfen ob bereits installiert +if [ -d "$CUSTOM_NODES_DIR" ]; then + if [ "$FORCE" = true ]; then + print_warning "Bestehendes custom-nodes Verzeichnis wird überschrieben" + rm -rf "$CUSTOM_NODES_DIR" + else + print_warning "custom-nodes Verzeichnis existiert bereits" + read -p "Überschreiben? (j/n): " OVERWRITE + if [[ "$OVERWRITE" =~ ^[jJyY]$ ]]; then + rm -rf "$CUSTOM_NODES_DIR" + else + print_info "Behalte bestehendes Verzeichnis" + fi + fi +fi + +# Custom Nodes kopieren +if [ ! -d "$CUSTOM_NODES_DIR" ]; then + cp -r "$SCRIPT_DIR/custom-nodes" "$CUSTOM_NODES_DIR" + print_success "Custom Nodes kopiert nach: $CUSTOM_NODES_DIR" +fi + +# Prüfe ob Verzeichnis schreibbar ist +if ! check_readonly "$CUSTOM_NODES_DIR"; then + print_readonly_warning "$CUSTOM_NODES_DIR" + echo "" + read -p "Trotzdem fortfahren? (j/n): " CONTINUE_RO + if [[ ! "$CONTINUE_RO" =~ ^[jJyY]$ ]]; then + print_error "Abbruch wegen read-only Verzeichnis" + exit 1 + fi + print_warning "Fortsetzen trotz read-only - npm install wird fehlschlagen!" +fi + +# ============================================================================= +# Node bauen (wenn nicht bereits gebaut) +# ============================================================================= + +if [ ! -d "$CUSTOM_NODES_DIR/dist" ] || [ "$BUILD_IN_CONTAINER" = true ]; then + print_info "Baue LibreBooking Node..." + + cd "$CUSTOM_NODES_DIR" + + # Prüfen ob node/npm vorhanden + if command -v npm &> /dev/null; then + npm install 2>/dev/null || print_warning "npm install hatte Warnungen" + npm run build 2>/dev/null || { + print_warning "Build fehlgeschlagen, versuche alternativen Ansatz..." + # Manueller Build + npx tsc 2>/dev/null || true + mkdir -p dist/nodes/LibreBooking dist/nodes/LibreBookingTrigger + cp nodes/LibreBooking/*.svg dist/nodes/LibreBooking/ 2>/dev/null || true + cp nodes/LibreBookingTrigger/*.svg dist/nodes/LibreBookingTrigger/ 2>/dev/null || true + } + print_success "Node erfolgreich gebaut" + else + print_warning "npm nicht gefunden - Node wird beim Container-Start gebaut" + print_info "Erstelle Build-Skript für Container..." + cat > "$CUSTOM_NODES_DIR/build.sh" << 'BUILDEOF' +#!/bin/sh +cd /home/node/.n8n/custom/n8n-nodes-librebooking +npm install +npm run build +BUILDEOF + chmod +x "$CUSTOM_NODES_DIR/build.sh" + fi + + cd "$N8N_PATH" +fi + +# ============================================================================= +# Docker Compose Override erstellen/aktualisieren +# ============================================================================= + +print_info "Erstelle docker-compose.override.yml..." + +OVERRIDE_FILE="$N8N_PATH/docker-compose.override.yml" + +if [ -f "$OVERRIDE_FILE" ]; then + print_warning "docker-compose.override.yml existiert bereits" + + # Prüfen ob LibreBooking bereits konfiguriert + if grep -q "n8n-nodes-librebooking" "$OVERRIDE_FILE"; then + print_success "LibreBooking bereits in override.yml konfiguriert" + else + print_info "Füge LibreBooking Konfiguration hinzu..." + # Backup erstellen + cp "$OVERRIDE_FILE" "${OVERRIDE_FILE}.backup" + + # Hinzufügen (vereinfacht - für komplexe Fälle manuell anpassen) + cat >> "$OVERRIDE_FILE" << 'OVERRIDEEOF' + +# LibreBooking Node (automatisch hinzugefügt) +# Falls Konflikte auftreten, bitte manuell anpassen +# Siehe DOCKER-INTEGRATION.md für Details +OVERRIDEEOF + print_warning "Bitte $OVERRIDE_FILE manuell prüfen!" + fi +else + # Neue Override-Datei erstellen + cat > "$OVERRIDE_FILE" << 'OVERRIDEEOF' +# Docker Compose Override für LibreBooking n8n Node +# Automatisch generiert von install-docker.sh + +version: '3.8' + +services: + n8n: + volumes: + # LibreBooking Custom Node + - ./custom-nodes:/home/node/.n8n/custom/n8n-nodes-librebooking:ro + + environment: + # Custom Nodes aktivieren + - N8N_CUSTOM_EXTENSIONS=/home/node/.n8n/custom + - N8N_COMMUNITY_NODES_ENABLED=true +OVERRIDEEOF + print_success "docker-compose.override.yml erstellt" +fi + +# ============================================================================= +# Berechtigungen setzen +# ============================================================================= + +print_info "Setze Berechtigungen..." + +# n8n läuft oft als node User mit UID 1000 +if [ "$(id -u)" = "0" ]; then + chown -R 1000:1000 "$CUSTOM_NODES_DIR" 2>/dev/null || print_warning "Konnte Berechtigungen nicht setzen" +else + # Versuche mit sudo falls verfügbar + if command -v sudo &> /dev/null; then + sudo chown -R 1000:1000 "$CUSTOM_NODES_DIR" 2>/dev/null || print_warning "Konnte Berechtigungen nicht setzen (sudo fehlgeschlagen)" + fi +fi + +print_success "Berechtigungen konfiguriert" + +# ============================================================================= +# Container neustarten +# ============================================================================= + +echo "" +read -p "Soll der n8n Container jetzt neu gestartet werden? (j/n): " RESTART + +if [[ "$RESTART" =~ ^[jJyY]$ ]]; then + print_info "Starte n8n Container neu..." + + if [ "$N8N_RUNNING" = true ]; then + $COMPOSE_CMD restart n8n 2>/dev/null || $COMPOSE_CMD restart + else + $COMPOSE_CMD up -d n8n 2>/dev/null || $COMPOSE_CMD up -d + fi + + # Warten bis Container bereit + print_info "Warte auf Container-Start..." + sleep 5 + + # Status prüfen + if $COMPOSE_CMD ps | grep -q "n8n.*Up"; then + print_success "n8n Container läuft!" + else + print_warning "Container-Status unklar - bitte manuell prüfen" + fi +else + print_info "Container nicht neu gestartet" + echo " Führen Sie später aus: cd $N8N_PATH && $COMPOSE_CMD restart" +fi + +# ============================================================================= +# Abschluss +# ============================================================================= + +echo "" +echo "=========================================" +print_success "Installation abgeschlossen!" +echo "=========================================" +echo "" +echo "Nächste Schritte:" +echo "1. Öffnen Sie n8n in Ihrem Browser" +echo "2. Gehen Sie zu: Settings > Community Nodes" +echo "3. Der 'LibreBooking' Node sollte sichtbar sein" +echo "4. Erstellen Sie neue Credentials für LibreBooking" +echo "" +echo "Bei Problemen siehe: DOCKER-INTEGRATION.md" +echo "" diff --git a/install-in-container.sh b/install-in-container.sh new file mode 100755 index 0000000..ff87675 --- /dev/null +++ b/install-in-container.sh @@ -0,0 +1,191 @@ +#!/bin/sh +# ============================================================================ +# install-in-container.sh - Installiert den LibreBooking Node IM Docker Container +# Dieses Skript wird INNERHALB des Containers ausgeführt! +# ============================================================================ + +set -e + +echo "============================================" +echo " LibreBooking Node - Container Installation" +echo "============================================" +echo "" + +# Funktion: Prüft ob ein Verzeichnis read-only ist +check_readonly() { + local dir="$1" + local test_file="$dir/.write_test_$$" + + # Versuche eine Test-Datei zu erstellen + if touch "$test_file" 2>/dev/null; then + rm -f "$test_file" 2>/dev/null + return 0 # Schreibbar (exit code 0 = true) + else + return 1 # Read-only (exit code 1 = false) + fi +} + +print_readonly_error() { + local dir="$1" + echo "" + echo "============================================" + echo " FEHLER: Read-only Volume!" + echo "============================================" + echo "" + echo "Das Verzeichnis '$dir' ist read-only gemountet." + echo "npm install und npm run build benötigen Schreibrechte." + echo "" + echo "LÖSUNGEN:" + echo "" + echo "1. Volume OHNE :ro mounten:" + echo " In docker-compose.yml oder docker-compose.override.yml:" + echo "" + echo " volumes:" + echo " - ./custom-nodes:/home/node/.n8n/custom/n8n-nodes-librebooking" + echo " # NICHT:" + echo " - ./custom-nodes:/home/node/.n8n/custom/n8n-nodes-librebooking:ro" + echo "" + echo "2. Auf dem Host bauen:" + echo " Führen Sie auf dem HOST aus: ./build-on-host.sh" + echo " Dann verwenden Sie: docker-compose.readonly.yml" + echo "" + echo "Siehe: TROUBLESHOOTING.md" + exit 1 +} + +# Mögliche Pfade für custom-nodes +POSSIBLE_PATHS=" +/home/node/.n8n/custom/n8n-nodes-librebooking +/home/node/.n8n/custom +/opt/n8n/custom-nodes +/data/custom-nodes +" + +CUSTOM_NODE_PATH="" + +# Finde den custom-nodes Pfad +echo "[1/5] Suche custom-nodes Verzeichnis..." +for path in $POSSIBLE_PATHS; do + if [ -d "$path" ]; then + # Prüfe ob package.json vorhanden ist + if [ -f "$path/package.json" ]; then + CUSTOM_NODE_PATH="$path" + echo " ✓ Gefunden: $path" + break + elif [ -f "$path/n8n-nodes-librebooking/package.json" ]; then + CUSTOM_NODE_PATH="$path/n8n-nodes-librebooking" + echo " ✓ Gefunden: $CUSTOM_NODE_PATH" + break + fi + fi +done + +if [ -z "$CUSTOM_NODE_PATH" ]; then + echo " ✗ Kein custom-nodes Verzeichnis mit package.json gefunden!" + echo "" + echo " Geprüfte Pfade:" + for path in $POSSIBLE_PATHS; do + if [ -d "$path" ]; then + echo " - $path (existiert, aber keine package.json)" + else + echo " - $path (existiert nicht)" + fi + done + echo "" + echo " Bitte stellen Sie sicher, dass die Dateien korrekt kopiert wurden." + exit 1 +fi + +# Wechsle ins Verzeichnis +echo "[2/5] Wechsle ins Verzeichnis: $CUSTOM_NODE_PATH" +cd "$CUSTOM_NODE_PATH" +echo " ✓ Aktuelles Verzeichnis: $(pwd)" + +# Prüfe ob npm verfügbar ist +echo "[3/5] Prüfe npm..." +if ! command -v npm >/dev/null 2>&1; then + echo " ✗ npm ist nicht installiert!" + echo "" + echo " Im n8n Docker-Image sollte npm vorhanden sein." + echo " Falls nicht, verwenden Sie ein Image mit Node.js." + exit 1 +fi +echo " ✓ npm Version: $(npm --version)" +echo " ✓ node Version: $(node --version)" + +# Prüfe Schreibrechte (read-only Volume?) +echo "[3.5/5] Prüfe Schreibrechte..." +if ! check_readonly "$CUSTOM_NODE_PATH"; then + print_readonly_error "$CUSTOM_NODE_PATH" +fi +echo " ✓ Schreibrechte vorhanden" + +# Dependencies installieren +echo "[4/5] Installiere Dependencies..." +echo " Führe 'npm install' aus..." +if npm install 2>&1; then + echo " ✓ Dependencies installiert" +else + echo " ✗ npm install fehlgeschlagen!" + echo "" + echo " Mögliche Lösungen:" + echo " - Prüfen Sie die Berechtigungen" + echo " - Prüfen Sie die Internetverbindung" + echo " - Führen Sie 'npm cache clean --force' aus" + exit 1 +fi + +# Build ausführen +echo "[5/5] Baue den Node..." +echo " Führe 'npm run build' aus..." +if npm run build 2>&1; then + echo " ✓ Build erfolgreich" +else + echo " ✗ Build fehlgeschlagen!" + echo "" + echo " Prüfen Sie die TypeScript-Fehler in der Ausgabe oben." + exit 1 +fi + +# Prüfe Ergebnis +echo "" +echo "============================================" +echo " Prüfe Installation..." +echo "============================================" + +if [ -d "$CUSTOM_NODE_PATH/dist" ]; then + echo "✓ dist/ Verzeichnis existiert" + + # Prüfe auf .node.js Dateien + NODE_FILES=$(find "$CUSTOM_NODE_PATH/dist" -name "*.node.js" 2>/dev/null | wc -l) + if [ "$NODE_FILES" -gt 0 ]; then + echo "✓ $NODE_FILES Node-Datei(en) gefunden:" + find "$CUSTOM_NODE_PATH/dist" -name "*.node.js" -exec echo " - {}" \; + else + echo "✗ Keine .node.js Dateien im dist/ Verzeichnis gefunden!" + fi + + # Prüfe credentials + CRED_FILES=$(find "$CUSTOM_NODE_PATH/dist" -name "*.credentials.js" 2>/dev/null | wc -l) + if [ "$CRED_FILES" -gt 0 ]; then + echo "✓ $CRED_FILES Credential-Datei(en) gefunden" + fi +else + echo "✗ dist/ Verzeichnis wurde nicht erstellt!" + exit 1 +fi + +echo "" +echo "============================================" +echo " Installation abgeschlossen!" +echo "============================================" +echo "" +echo "Nächste Schritte:" +echo " 1. Verlassen Sie den Container: exit" +echo " 2. Starten Sie n8n neu: docker restart n8n" +echo " 3. Öffnen Sie n8n im Browser und suchen Sie nach 'LibreBooking'" +echo "" +echo "Falls der Node nicht erscheint:" +echo " - Prüfen Sie die Umgebungsvariable N8N_CUSTOM_EXTENSIONS" +echo " - Führen Sie ./check-installation.sh aus" +echo "" diff --git a/install.ps1 b/install.ps1 new file mode 100644 index 0000000..3fc8ca4 --- /dev/null +++ b/install.ps1 @@ -0,0 +1,250 @@ +# +# LibreBooking n8n Node - Installations-Skript für Windows (PowerShell) +# +# Verwendung: +# .\install.ps1 +# +# Optionen: +# -NoLink Überspringt npm link (nur Build) +# -Global Installiert global statt npm link +# -Help Zeigt diese Hilfe an +# + +param( + [switch]$NoLink, + [switch]$Global, + [switch]$Help +) + +# Konfiguration +$MIN_NODE_VERSION = 18 +$ErrorActionPreference = "Stop" + +# Funktionen +function Write-ColorOutput { + param( + [string]$Message, + [string]$Color = "White" + ) + Write-Host $Message -ForegroundColor $Color +} + +function Write-Header { + Write-ColorOutput "" "Blue" + Write-ColorOutput "=============================================" "Blue" + Write-ColorOutput " LibreBooking n8n Node Installer" "Blue" + Write-ColorOutput "=============================================" "Blue" + Write-ColorOutput "" "Blue" +} + +function Write-Success { + param([string]$Message) + Write-ColorOutput "✓ $Message" "Green" +} + +function Write-Warning-Msg { + param([string]$Message) + Write-ColorOutput "⚠ $Message" "Yellow" +} + +function Write-Error-Msg { + param([string]$Message) + Write-ColorOutput "✗ $Message" "Red" +} + +function Write-Info { + param([string]$Message) + Write-ColorOutput "ℹ $Message" "Cyan" +} + +function Show-Help { + Write-Host "Verwendung: .\install.ps1 [OPTIONEN]" + Write-Host "" + Write-Host "Optionen:" + Write-Host " -NoLink Überspringt npm link (nur Build)" + Write-Host " -Global Installiert global mit npm install -g" + Write-Host " -Help Zeigt diese Hilfe an" + Write-Host "" + Write-Host "Beispiele:" + Write-Host " .\install.ps1 # Standard-Installation mit npm link" + Write-Host " .\install.ps1 -NoLink # Nur Dependencies installieren und Build" + Write-Host " .\install.ps1 -Global # Globale Installation" + exit 0 +} + +function Test-Command { + param([string]$Command) + try { + $null = Get-Command $Command -ErrorAction Stop + return $true + } + catch { + return $false + } +} + +function Get-NodeVersion { + $version = (node -v) -replace 'v', '' + return [int]($version.Split('.')[0]) +} + +# Hilfe anzeigen +if ($Help) { + Show-Help +} + +# Start +Write-Header + +# 1. Node.js prüfen +Write-Host "" +Write-Info "Prüfe Voraussetzungen..." +Write-Host "" + +if (-not (Test-Command "node")) { + Write-Error-Msg "Node.js ist nicht installiert!" + Write-Host " Bitte installiere Node.js v$MIN_NODE_VERSION oder höher:" + Write-Host " https://nodejs.org/" + exit 1 +} + +$nodeVersion = Get-NodeVersion +if ($nodeVersion -lt $MIN_NODE_VERSION) { + Write-Error-Msg "Node.js Version $nodeVersion ist zu alt!" + Write-Host " Mindestens Version $MIN_NODE_VERSION benötigt." + exit 1 +} + +$nodeFullVersion = (node -v) -replace 'v', '' +Write-Success "Node.js v$nodeFullVersion gefunden" + +# 2. npm prüfen +if (-not (Test-Command "npm")) { + Write-Error-Msg "npm ist nicht installiert!" + exit 1 +} + +$npmVersion = npm -v +Write-Success "npm v$npmVersion gefunden" + +# 3. n8n prüfen (optional) +if (Test-Command "n8n") { + try { + $n8nVersion = n8n --version 2>$null + Write-Success "n8n $n8nVersion gefunden" + } + catch { + Write-Success "n8n installiert" + } +} +else { + Write-Warning-Msg "n8n ist nicht global installiert." + Write-Host " Für npm link muss n8n global installiert sein:" + Write-Host " npm install -g n8n" + Write-Host "" + + if (-not $NoLink -and -not $Global) { + $response = Read-Host "Möchtest du trotzdem fortfahren? (j/N)" + if ($response -notmatch '^[Jj]$') { + exit 1 + } + } +} + +# 4. Dependencies installieren +Write-Host "" +Write-Info "Installiere Dependencies..." + +try { + npm install + Write-Success "Dependencies installiert" +} +catch { + Write-Error-Msg "Fehler bei npm install: $_" + exit 1 +} + +# 5. TypeScript kompilieren +Write-Host "" +Write-Info "Kompiliere TypeScript..." + +try { + npm run build + Write-Success "Build erfolgreich" +} +catch { + Write-Error-Msg "Fehler beim Build: $_" + exit 1 +} + +# 6. npm link oder global install +if ($Global) { + Write-Host "" + Write-Info "Installiere global..." + + try { + npm install -g . + Write-Success "Global installiert" + } + catch { + Write-Error-Msg "Fehler bei globaler Installation: $_" + exit 1 + } +} +elseif (-not $NoLink) { + Write-Host "" + Write-Info "Verlinke mit npm link..." + + try { + npm link + Write-Success "npm link erfolgreich" + + # Versuche mit n8n zu verlinken + if (Test-Command "n8n") { + $n8nPath = Join-Path (npm root -g) "n8n" + if (Test-Path $n8nPath) { + Write-Info "Verlinke mit n8n..." + Push-Location $n8nPath + try { + npm link n8n-nodes-librebooking 2>$null + Write-Success "Mit n8n verlinkt" + } + catch { + Write-Warning-Msg "Konnte nicht automatisch mit n8n verlinken" + } + finally { + Pop-Location + } + } + } + } + catch { + Write-Error-Msg "Fehler bei npm link: $_" + exit 1 + } +} + +# Abschluss +Write-Host "" +Write-ColorOutput "=============================================" "Green" +Write-ColorOutput " Installation erfolgreich abgeschlossen!" "Green" +Write-ColorOutput "=============================================" "Green" +Write-Host "" +Write-Host "Nächste Schritte:" +Write-Host "" + +if ($NoLink) { + Write-Host " 1. Führe 'npm link' aus, um den Node zu verlinken" + Write-Host " 2. Starte n8n neu: n8n start" +} +else { + Write-Host " 1. Starte n8n neu: n8n start" + Write-Host " (oder mit Docker: docker-compose restart)" +} + +Write-Host "" +Write-Host " 2. Öffne n8n im Browser: http://localhost:5678" +Write-Host " 3. Der LibreBooking Node sollte verfügbar sein" +Write-Host "" +Write-Host "Bei Problemen siehe INSTALLATION.md oder README.md" +Write-Host "" diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..69b459c --- /dev/null +++ b/install.sh @@ -0,0 +1,209 @@ +#!/bin/bash +# +# LibreBooking n8n Node - Installations-Skript für Linux/Mac +# +# Verwendung: +# chmod +x install.sh +# ./install.sh +# +# Optionen: +# --no-link Überspringt npm link (nur Build) +# --global Installiert global statt npm link +# --help Zeigt diese Hilfe an +# + +set -e + +# Farben für Ausgabe +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Konfiguration +MIN_NODE_VERSION=18 +REQUIRED_NPM_VERSION=8 + +# Optionen +SKIP_LINK=false +GLOBAL_INSTALL=false + +# Funktionen +print_header() { + echo -e "${BLUE}" + echo "=============================================" + echo " LibreBooking n8n Node Installer" + echo "=============================================" + echo -e "${NC}" +} + +print_success() { + echo -e "${GREEN}✓ $1${NC}" +} + +print_warning() { + echo -e "${YELLOW}⚠ $1${NC}" +} + +print_error() { + echo -e "${RED}✗ $1${NC}" +} + +print_info() { + echo -e "${BLUE}ℹ $1${NC}" +} + +show_help() { + echo "Verwendung: ./install.sh [OPTIONEN]" + echo "" + echo "Optionen:" + echo " --no-link Überspringt npm link (nur Build)" + echo " --global Installiert global mit npm install -g" + echo " --help Zeigt diese Hilfe an" + echo "" + echo "Beispiele:" + echo " ./install.sh # Standard-Installation mit npm link" + echo " ./install.sh --no-link # Nur Dependencies installieren und Build" + echo " ./install.sh --global # Globale Installation" + exit 0 +} + +check_command() { + if command -v $1 &> /dev/null; then + return 0 + else + return 1 + fi +} + +get_node_version() { + node -v | sed 's/v//' | cut -d. -f1 +} + +get_npm_version() { + npm -v | cut -d. -f1 +} + +# Parameter verarbeiten +for arg in "$@"; do + case $arg in + --no-link) + SKIP_LINK=true + ;; + --global) + GLOBAL_INSTALL=true + ;; + --help|-h) + show_help + ;; + *) + print_error "Unbekannte Option: $arg" + show_help + ;; + esac +done + +# Start +print_header + +# 1. Node.js prüfen +echo "" +print_info "Prüfe Voraussetzungen..." +echo "" + +if ! check_command node; then + print_error "Node.js ist nicht installiert!" + echo " Bitte installiere Node.js v${MIN_NODE_VERSION} oder höher:" + echo " https://nodejs.org/" + exit 1 +fi + +NODE_VERSION=$(get_node_version) +if [ "$NODE_VERSION" -lt "$MIN_NODE_VERSION" ]; then + print_error "Node.js Version $NODE_VERSION ist zu alt!" + echo " Mindestens Version ${MIN_NODE_VERSION} benötigt." + exit 1 +fi +print_success "Node.js v$(node -v | sed 's/v//') gefunden" + +# 2. npm prüfen +if ! check_command npm; then + print_error "npm ist nicht installiert!" + exit 1 +fi +print_success "npm v$(npm -v) gefunden" + +# 3. n8n prüfen (optional, aber empfohlen) +if check_command n8n; then + print_success "n8n $(n8n --version 2>/dev/null || echo 'installiert') gefunden" +else + print_warning "n8n ist nicht global installiert." + echo " Für npm link muss n8n global installiert sein:" + echo " npm install -g n8n" + echo "" + if [ "$SKIP_LINK" = false ] && [ "$GLOBAL_INSTALL" = false ]; then + read -p "Möchtest du trotzdem fortfahren? (j/N) " -n 1 -r + echo + if [[ ! $REPLY =~ ^[Jj]$ ]]; then + exit 1 + fi + fi +fi + +# 4. Dependencies installieren +echo "" +print_info "Installiere Dependencies..." +npm install +print_success "Dependencies installiert" + +# 5. TypeScript kompilieren +echo "" +print_info "Kompiliere TypeScript..." +npm run build +print_success "Build erfolgreich" + +# 6. npm link oder global install +if [ "$GLOBAL_INSTALL" = true ]; then + echo "" + print_info "Installiere global..." + npm install -g . + print_success "Global installiert" +elif [ "$SKIP_LINK" = false ]; then + echo "" + print_info "Verlinke mit npm link..." + npm link + print_success "npm link erfolgreich" + + # Prüfen ob n8n vorhanden und linken + if check_command n8n; then + N8N_PATH=$(npm root -g)/n8n + if [ -d "$N8N_PATH" ]; then + print_info "Verlinke mit n8n..." + cd "$N8N_PATH" 2>/dev/null && npm link n8n-nodes-librebooking 2>/dev/null && cd - > /dev/null + print_success "Mit n8n verlinkt" + fi + fi +fi + +# Abschluss +echo "" +echo -e "${GREEN}=============================================${NC}" +echo -e "${GREEN} Installation erfolgreich abgeschlossen!${NC}" +echo -e "${GREEN}=============================================${NC}" +echo "" +echo "Nächste Schritte:" +echo "" +if [ "$SKIP_LINK" = true ]; then + echo " 1. Führe 'npm link' aus, um den Node zu verlinken" + echo " 2. Starte n8n neu: n8n start" +else + echo " 1. Starte n8n neu: n8n start" + echo " (oder mit Docker: docker-compose restart)" +fi +echo "" +echo " 2. Öffne n8n im Browser: http://localhost:5678" +echo " 3. Der LibreBooking Node sollte verfügbar sein" +echo "" +echo "Bei Problemen siehe INSTALLATION.md oder README.md" +echo "" diff --git a/n8n-nodes-librebooking-v1.0.0.tar.gz b/n8n-nodes-librebooking-v1.0.0.tar.gz new file mode 100644 index 0000000..e6c52bc Binary files /dev/null and b/n8n-nodes-librebooking-v1.0.0.tar.gz differ diff --git a/n8n-nodes-librebooking-v1.0.0.zip b/n8n-nodes-librebooking-v1.0.0.zip new file mode 100644 index 0000000..38b91aa Binary files /dev/null and b/n8n-nodes-librebooking-v1.0.0.zip differ diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..6251781 --- /dev/null +++ b/nginx.conf @@ -0,0 +1,122 @@ +# Nginx Reverse Proxy Konfiguration für n8n mit LibreBooking +# Beispielkonfiguration für HTTPS Zugang +# +# Verwendung: +# 1. Diese Datei nach /etc/nginx/sites-available/n8n kopieren +# 2. ln -s /etc/nginx/sites-available/n8n /etc/nginx/sites-enabled/ +# 3. SSL-Zertifikate einrichten (z.B. mit Certbot) +# 4. nginx -t && systemctl reload nginx + +# Upstream für n8n +upstream n8n_backend { + server 127.0.0.1:5678; + keepalive 32; +} + +# HTTP -> HTTPS Redirect +server { + listen 80; + listen [::]:80; + server_name n8n.example.com; + + # ACME Challenge für Let's Encrypt + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } + + location / { + return 301 https://$host$request_uri; + } +} + +# HTTPS Server +server { + listen 443 ssl http2; + listen [::]:443 ssl http2; + server_name n8n.example.com; + + # SSL-Zertifikate (Let's Encrypt Beispiel) + ssl_certificate /etc/letsencrypt/live/n8n.example.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/n8n.example.com/privkey.pem; + + # SSL-Einstellungen (moderne Konfiguration) + ssl_protocols TLSv1.2 TLSv1.3; + ssl_prefer_server_ciphers off; + ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384; + + # HSTS + add_header Strict-Transport-Security "max-age=63072000" always; + + # Logging + access_log /var/log/nginx/n8n_access.log; + error_log /var/log/nginx/n8n_error.log; + + # Proxy-Einstellungen + location / { + proxy_pass http://n8n_backend; + proxy_http_version 1.1; + + # WebSocket Support (wichtig für n8n Editor) + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + + # Standard Proxy Headers + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Forwarded-Port $server_port; + + # Timeouts (erhöht für lange Workflow-Ausführungen) + proxy_connect_timeout 60s; + proxy_send_timeout 300s; + proxy_read_timeout 300s; + + # Buffer-Einstellungen + proxy_buffering off; + proxy_buffer_size 4k; + + # Client-Upload Limit (anpassen nach Bedarf) + client_max_body_size 50M; + } + + # Webhook-spezifische Einstellungen + location /webhook/ { + proxy_pass http://n8n_backend; + proxy_http_version 1.1; + + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # Erhöhte Timeouts für Webhooks + proxy_connect_timeout 60s; + proxy_send_timeout 600s; + proxy_read_timeout 600s; + + proxy_buffering off; + client_max_body_size 100M; + } + + # Health Check Endpoint + location /healthz { + proxy_pass http://n8n_backend/healthz; + proxy_http_version 1.1; + proxy_set_header Host $host; + access_log off; + } +} + +# Optional: Monitoring/Metrics Server Block +# server { +# listen 127.0.0.1:9090; +# server_name localhost; +# +# location /nginx_status { +# stub_status on; +# allow 127.0.0.1; +# deny all; +# } +# } diff --git a/nodes/LibreBooking/LibreBooking.node.ts b/nodes/LibreBooking/LibreBooking.node.ts new file mode 100644 index 0000000..675b4ad --- /dev/null +++ b/nodes/LibreBooking/LibreBooking.node.ts @@ -0,0 +1,1203 @@ +import { + IExecuteFunctions, + INodeExecutionData, + INodeType, + INodeTypeDescription, + IHttpRequestMethods, + NodeApiError, + NodeOperationError, +} from 'n8n-workflow'; + +interface LibreBookingSession { + sessionToken: string; + userId: number; + sessionExpires: string; +} + +/** + * Authentifizierung bei LibreBooking + */ +async function authenticate( + executeFunctions: IExecuteFunctions, + baseUrl: string, + username: string, + password: string, +): Promise { + try { + const response = await executeFunctions.helpers.httpRequest({ + method: 'POST', + url: `${baseUrl}/Web/Services/index.php/Authentication/Authenticate`, + headers: { 'Content-Type': 'application/json' }, + body: { username, password }, + json: true, + }); + + if (!response.isAuthenticated) { + throw new NodeOperationError( + executeFunctions.getNode(), + 'Authentifizierung fehlgeschlagen. Bitte überprüfen Sie Ihre Credentials.', + ); + } + + return { + sessionToken: response.sessionToken, + userId: response.userId, + sessionExpires: response.sessionExpires, + }; + } catch (error: any) { + throw new NodeApiError(executeFunctions.getNode(), error, { + message: 'Authentifizierung fehlgeschlagen', + description: 'Überprüfen Sie die LibreBooking URL und Ihre Zugangsdaten.', + }); + } +} + +/** + * Abmeldung von LibreBooking + */ +async function signOut( + executeFunctions: IExecuteFunctions, + baseUrl: string, + session: LibreBookingSession, +): Promise { + try { + await executeFunctions.helpers.httpRequest({ + method: 'POST', + url: `${baseUrl}/Web/Services/index.php/Authentication/SignOut`, + headers: { 'Content-Type': 'application/json' }, + body: { + userId: session.userId, + sessionToken: session.sessionToken, + }, + json: true, + }); + } catch (error) { + // Ignoriere SignOut-Fehler + } +} + +/** + * API-Request mit Session-Authentifizierung + */ +async function makeApiRequest( + executeFunctions: IExecuteFunctions, + baseUrl: string, + session: LibreBookingSession, + method: IHttpRequestMethods, + endpoint: string, + body?: any, + qs?: any, +): Promise { + const options: any = { + method, + url: `${baseUrl}/Web/Services/index.php${endpoint}`, + headers: { + 'Content-Type': 'application/json', + 'X-Booked-SessionToken': session.sessionToken, + 'X-Booked-UserId': session.userId.toString(), + }, + json: true, + }; + + if (body && Object.keys(body).length > 0) { + options.body = body; + } + + if (qs && Object.keys(qs).length > 0) { + options.qs = qs; + } + + try { + return await executeFunctions.helpers.httpRequest(options); + } catch (error: any) { + if (error.statusCode === 401) { + throw new NodeApiError(executeFunctions.getNode(), error, { + message: 'Authentifizierung abgelaufen', + description: 'Der Session-Token ist abgelaufen. Bitte erneut ausführen.', + }); + } else if (error.statusCode === 403) { + throw new NodeApiError(executeFunctions.getNode(), error, { + message: 'Zugriff verweigert', + description: 'Sie haben keine Berechtigung für diese Operation. Admin-Rechte erforderlich?', + }); + } else if (error.statusCode === 404) { + throw new NodeApiError(executeFunctions.getNode(), error, { + message: 'Nicht gefunden', + description: 'Die angeforderte Ressource wurde nicht gefunden.', + }); + } + throw new NodeApiError(executeFunctions.getNode(), error, { + message: `API-Fehler: ${error.message}`, + }); + } +} + +/** + * Hilfsfunktion: String zu Array von Zahlen + */ +function parseIdList(value: string | undefined): number[] { + if (!value || value.trim() === '') return []; + return value.split(',').map(id => parseInt(id.trim(), 10)).filter(id => !isNaN(id)); +} + +/** + * LibreBooking n8n Node + * + * Vollständige Integration für die LibreBooking API. + * Unterstützt alle wichtigen Ressourcen und Operationen. + */ +export class LibreBooking implements INodeType { + description: INodeTypeDescription = { + displayName: 'LibreBooking', + name: 'libreBooking', + icon: 'file:librebooking.svg', + group: ['transform'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Verwalten Sie Reservierungen, Ressourcen, Benutzer und mehr mit LibreBooking', + defaults: { + name: 'LibreBooking', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'libreBookingApi', + required: true, + }, + ], + properties: [ + // ===================================================== + // RESOURCE SELECTOR + // ===================================================== + { + displayName: 'Ressource', + name: 'resource', + type: 'options', + noDataExpression: true, + options: [ + { + name: 'Reservierung', + value: 'reservation', + description: 'Reservierungen verwalten', + }, + { + name: 'Ressource', + value: 'resource', + description: 'Ressourcen (Räume, Equipment) verwalten', + }, + { + name: 'Zeitplan', + value: 'schedule', + description: 'Zeitpläne abrufen', + }, + { + name: 'Benutzer', + value: 'user', + description: 'Benutzer verwalten (Admin-Rechte erforderlich)', + }, + { + name: 'Konto', + value: 'account', + description: 'Eigenes Konto verwalten', + }, + { + name: 'Gruppe', + value: 'group', + description: 'Benutzergruppen verwalten', + }, + { + name: 'Zubehör', + value: 'accessory', + description: 'Zubehör abrufen', + }, + { + name: 'Attribut', + value: 'attribute', + description: 'Benutzerdefinierte Attribute verwalten', + }, + ], + default: 'reservation', + }, + + // ===================================================== + // RESERVATION OPERATIONS + // ===================================================== + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { + show: { + resource: ['reservation'], + }, + }, + options: [ + { name: 'Erstellen', value: 'create', description: 'Neue Reservierung erstellen', action: 'Reservierung erstellen' }, + { name: 'Abrufen', value: 'get', description: 'Reservierung abrufen', action: 'Reservierung abrufen' }, + { name: 'Alle Abrufen', value: 'getAll', description: 'Alle Reservierungen abrufen', action: 'Alle Reservierungen abrufen' }, + { name: 'Aktualisieren', value: 'update', description: 'Reservierung aktualisieren', action: 'Reservierung aktualisieren' }, + { name: 'Löschen', value: 'delete', description: 'Reservierung löschen', action: 'Reservierung löschen' }, + { name: 'Genehmigen', value: 'approve', description: 'Ausstehende Reservierung genehmigen', action: 'Reservierung genehmigen' }, + { name: 'Check-In', value: 'checkIn', description: 'In Reservierung einchecken', action: 'In Reservierung einchecken' }, + { name: 'Check-Out', value: 'checkOut', description: 'Aus Reservierung auschecken', action: 'Aus Reservierung auschecken' }, + ], + default: 'getAll', + }, + + // ===================================================== + // RESOURCE OPERATIONS + // ===================================================== + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { show: { resource: ['resource'] } }, + options: [ + { name: 'Alle Abrufen', value: 'getAll', description: 'Alle Ressourcen abrufen', action: 'Alle Ressourcen abrufen' }, + { name: 'Abrufen', value: 'get', description: 'Ressource abrufen', action: 'Ressource abrufen' }, + { name: 'Verfügbarkeit Prüfen', value: 'getAvailability', description: 'Verfügbarkeit von Ressourcen prüfen', action: 'Verfügbarkeit prüfen' }, + { name: 'Gruppen Abrufen', value: 'getGroups', description: 'Ressourcen-Gruppen abrufen', action: 'Ressourcen-Gruppen abrufen' }, + { name: 'Typen Abrufen', value: 'getTypes', description: 'Ressourcen-Typen abrufen', action: 'Ressourcen-Typen abrufen' }, + { name: 'Status Abrufen', value: 'getStatuses', description: 'Verfügbare Status abrufen', action: 'Status abrufen' }, + { name: 'Erstellen', value: 'create', description: 'Neue Ressource erstellen (Admin)', action: 'Ressource erstellen' }, + { name: 'Aktualisieren', value: 'update', description: 'Ressource aktualisieren (Admin)', action: 'Ressource aktualisieren' }, + { name: 'Löschen', value: 'delete', description: 'Ressource löschen (Admin)', action: 'Ressource löschen' }, + ], + default: 'getAll', + }, + + // ===================================================== + // SCHEDULE OPERATIONS + // ===================================================== + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { show: { resource: ['schedule'] } }, + options: [ + { name: 'Alle Abrufen', value: 'getAll', description: 'Alle Zeitpläne abrufen', action: 'Alle Zeitpläne abrufen' }, + { name: 'Abrufen', value: 'get', description: 'Zeitplan abrufen', action: 'Zeitplan abrufen' }, + { name: 'Slots Abrufen', value: 'getSlots', description: 'Verfügbare Slots abrufen', action: 'Slots abrufen' }, + ], + default: 'getAll', + }, + + // ===================================================== + // USER OPERATIONS + // ===================================================== + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { show: { resource: ['user'] } }, + options: [ + { name: 'Alle Abrufen', value: 'getAll', description: 'Alle Benutzer abrufen', action: 'Alle Benutzer abrufen' }, + { name: 'Abrufen', value: 'get', description: 'Benutzer abrufen', action: 'Benutzer abrufen' }, + { name: 'Erstellen', value: 'create', description: 'Neuen Benutzer erstellen (Admin)', action: 'Benutzer erstellen' }, + { name: 'Aktualisieren', value: 'update', description: 'Benutzer aktualisieren (Admin)', action: 'Benutzer aktualisieren' }, + { name: 'Passwort Ändern', value: 'updatePassword', description: 'Benutzer-Passwort ändern (Admin)', action: 'Passwort ändern' }, + { name: 'Löschen', value: 'delete', description: 'Benutzer löschen (Admin)', action: 'Benutzer löschen' }, + ], + default: 'getAll', + }, + + // ===================================================== + // ACCOUNT OPERATIONS + // ===================================================== + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { show: { resource: ['account'] } }, + options: [ + { name: 'Abrufen', value: 'get', description: 'Eigene Kontoinformationen abrufen', action: 'Konto abrufen' }, + { name: 'Erstellen', value: 'create', description: 'Neues Konto erstellen (Registrierung)', action: 'Konto erstellen' }, + { name: 'Aktualisieren', value: 'update', description: 'Eigenes Konto aktualisieren', action: 'Konto aktualisieren' }, + { name: 'Passwort Ändern', value: 'updatePassword', description: 'Eigenes Passwort ändern', action: 'Passwort ändern' }, + ], + default: 'get', + }, + + // ===================================================== + // GROUP OPERATIONS + // ===================================================== + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { show: { resource: ['group'] } }, + options: [ + { name: 'Alle Abrufen', value: 'getAll', description: 'Alle Gruppen abrufen', action: 'Alle Gruppen abrufen' }, + { name: 'Abrufen', value: 'get', description: 'Gruppe abrufen', action: 'Gruppe abrufen' }, + { name: 'Erstellen', value: 'create', description: 'Neue Gruppe erstellen (Admin)', action: 'Gruppe erstellen' }, + { name: 'Aktualisieren', value: 'update', description: 'Gruppe aktualisieren (Admin)', action: 'Gruppe aktualisieren' }, + { name: 'Löschen', value: 'delete', description: 'Gruppe löschen (Admin)', action: 'Gruppe löschen' }, + { name: 'Rollen Ändern', value: 'changeRoles', description: 'Gruppenrollen ändern (Admin)', action: 'Rollen ändern' }, + { name: 'Berechtigungen Ändern', value: 'changePermissions', description: 'Gruppenberechtigungen ändern (Admin)', action: 'Berechtigungen ändern' }, + { name: 'Benutzer Ändern', value: 'changeUsers', description: 'Gruppenbenutzer ändern (Admin)', action: 'Benutzer ändern' }, + ], + default: 'getAll', + }, + + // ===================================================== + // ACCESSORY OPERATIONS + // ===================================================== + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { show: { resource: ['accessory'] } }, + options: [ + { name: 'Alle Abrufen', value: 'getAll', description: 'Alle Zubehörteile abrufen', action: 'Alle Zubehörteile abrufen' }, + { name: 'Abrufen', value: 'get', description: 'Zubehörteil abrufen', action: 'Zubehörteil abrufen' }, + ], + default: 'getAll', + }, + + // ===================================================== + // ATTRIBUTE OPERATIONS + // ===================================================== + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { show: { resource: ['attribute'] } }, + options: [ + { name: 'Abrufen', value: 'get', description: 'Attribut abrufen', action: 'Attribut abrufen' }, + { name: 'Nach Kategorie Abrufen', value: 'getByCategory', description: 'Attribute einer Kategorie abrufen', action: 'Attribute nach Kategorie abrufen' }, + { name: 'Erstellen', value: 'create', description: 'Neues Attribut erstellen (Admin)', action: 'Attribut erstellen' }, + { name: 'Aktualisieren', value: 'update', description: 'Attribut aktualisieren (Admin)', action: 'Attribut aktualisieren' }, + { name: 'Löschen', value: 'delete', description: 'Attribut löschen (Admin)', action: 'Attribut löschen' }, + ], + default: 'getByCategory', + }, + + // ===================================================== + // RESERVATION PARAMETERS + // ===================================================== + { + displayName: 'Referenznummer', + name: 'referenceNumber', + type: 'string', + required: true, + displayOptions: { show: { resource: ['reservation'], operation: ['get', 'update', 'delete', 'approve', 'checkIn', 'checkOut'] } }, + default: '', + description: 'Die eindeutige Referenznummer der Reservierung', + }, + { + displayName: 'Ressourcen-ID', + name: 'resourceId', + type: 'number', + required: true, + displayOptions: { show: { resource: ['reservation'], operation: ['create'] } }, + default: 1, + description: 'Die ID der zu reservierenden Ressource', + }, + { + displayName: 'Startzeit', + name: 'startDateTime', + type: 'dateTime', + required: true, + displayOptions: { show: { resource: ['reservation'], operation: ['create', 'update'] } }, + default: '', + description: 'Startzeitpunkt der Reservierung (ISO 8601 Format)', + }, + { + displayName: 'Endzeit', + name: 'endDateTime', + type: 'dateTime', + required: true, + displayOptions: { show: { resource: ['reservation'], operation: ['create', 'update'] } }, + default: '', + description: 'Endzeitpunkt der Reservierung (ISO 8601 Format)', + }, + { + displayName: 'Titel', + name: 'title', + type: 'string', + displayOptions: { show: { resource: ['reservation'], operation: ['create', 'update'] } }, + default: '', + description: 'Titel der Reservierung', + }, + { + displayName: 'Aktualisierungsbereich', + name: 'updateScope', + type: 'options', + displayOptions: { show: { resource: ['reservation'], operation: ['update', 'delete'] } }, + options: [ + { name: 'Nur Diese', value: 'this', description: 'Nur diese Instanz ändern' }, + { name: 'Zukünftige', value: 'future', description: 'Diese und alle zukünftigen Instanzen ändern' }, + { name: 'Alle', value: 'full', description: 'Alle Instanzen der Serie ändern' }, + ], + default: 'this', + }, + { + displayName: 'Zusätzliche Felder', + name: 'additionalFields', + type: 'collection', + placeholder: 'Feld hinzufügen', + default: {}, + displayOptions: { show: { resource: ['reservation'], operation: ['create', 'update'] } }, + options: [ + { displayName: 'Beschreibung', name: 'description', type: 'string', default: '' }, + { displayName: 'Benutzer-ID', name: 'userId', type: 'number', default: '' }, + { displayName: 'Zusätzliche Ressourcen', name: 'resources', type: 'string', default: '', description: 'Komma-getrennte Liste' }, + { displayName: 'Teilnehmer', name: 'participants', type: 'string', default: '', description: 'Komma-getrennte Benutzer-IDs' }, + { displayName: 'Eingeladene', name: 'invitees', type: 'string', default: '', description: 'Komma-getrennte Benutzer-IDs' }, + { displayName: 'Teilnahme Erlauben', name: 'allowParticipation', type: 'boolean', default: true }, + { displayName: 'Nutzungsbedingungen Akzeptiert', name: 'termsAccepted', type: 'boolean', default: true }, + ], + }, + { + displayName: 'Filter', + name: 'filters', + type: 'collection', + placeholder: 'Filter hinzufügen', + default: {}, + displayOptions: { show: { resource: ['reservation'], operation: ['getAll'] } }, + options: [ + { displayName: 'Benutzer-ID', name: 'userId', type: 'number', default: '' }, + { displayName: 'Ressourcen-ID', name: 'resourceId', type: 'number', default: '' }, + { displayName: 'Zeitplan-ID', name: 'scheduleId', type: 'number', default: '' }, + { displayName: 'Startzeit', name: 'startDateTime', type: 'dateTime', default: '' }, + { displayName: 'Endzeit', name: 'endDateTime', type: 'dateTime', default: '' }, + ], + }, + + // ===================================================== + // RESOURCE PARAMETERS + // ===================================================== + { + displayName: 'Ressourcen-ID', + name: 'resourceIdParam', + type: 'number', + required: true, + displayOptions: { show: { resource: ['resource'], operation: ['get', 'update', 'delete'] } }, + default: 1, + }, + { + displayName: 'Ressourcen-ID (Optional)', + name: 'resourceIdOptional', + type: 'number', + displayOptions: { show: { resource: ['resource'], operation: ['getAvailability'] } }, + default: '', + }, + { + displayName: 'Datum/Zeit', + name: 'availabilityDateTime', + type: 'dateTime', + displayOptions: { show: { resource: ['resource'], operation: ['getAvailability'] } }, + default: '', + }, + { + displayName: 'Ressourcen-Name', + name: 'resourceName', + type: 'string', + required: true, + displayOptions: { show: { resource: ['resource'], operation: ['create', 'update'] } }, + default: '', + }, + { + displayName: 'Zeitplan-ID', + name: 'scheduleIdForResource', + type: 'number', + required: true, + displayOptions: { show: { resource: ['resource'], operation: ['create'] } }, + default: 1, + }, + { + displayName: 'Ressourcen-Optionen', + name: 'resourceOptions', + type: 'collection', + placeholder: 'Option hinzufügen', + default: {}, + displayOptions: { show: { resource: ['resource'], operation: ['create', 'update'] } }, + options: [ + { displayName: 'Standort', name: 'location', type: 'string', default: '' }, + { displayName: 'Kontakt', name: 'contact', type: 'string', default: '' }, + { displayName: 'Beschreibung', name: 'description', type: 'string', default: '' }, + { displayName: 'Notizen', name: 'notes', type: 'string', default: '' }, + { displayName: 'Max. Teilnehmer', name: 'maxParticipants', type: 'number', default: 0 }, + { displayName: 'Genehmigung Erforderlich', name: 'requiresApproval', type: 'boolean', default: false }, + { displayName: 'Mehrtägig Erlauben', name: 'allowMultiday', type: 'boolean', default: false }, + { displayName: 'Check-In Erforderlich', name: 'requiresCheckIn', type: 'boolean', default: false }, + { displayName: 'Auto-Release Minuten', name: 'autoReleaseMinutes', type: 'number', default: 0 }, + { displayName: 'Farbe', name: 'color', type: 'string', default: '' }, + { displayName: 'Status-ID', name: 'statusId', type: 'options', options: [{ name: 'Versteckt', value: 0 }, { name: 'Verfügbar', value: 1 }, { name: 'Nicht Verfügbar', value: 2 }], default: 1 }, + ], + }, + + // ===================================================== + // SCHEDULE PARAMETERS + // ===================================================== + { + displayName: 'Zeitplan-ID', + name: 'scheduleId', + type: 'number', + required: true, + displayOptions: { show: { resource: ['schedule'], operation: ['get', 'getSlots'] } }, + default: 1, + }, + { + displayName: 'Slots-Filter', + name: 'slotsFilters', + type: 'collection', + placeholder: 'Filter hinzufügen', + default: {}, + displayOptions: { show: { resource: ['schedule'], operation: ['getSlots'] } }, + options: [ + { displayName: 'Ressourcen-ID', name: 'resourceId', type: 'number', default: '' }, + { displayName: 'Startzeit', name: 'startDateTime', type: 'dateTime', default: '' }, + { displayName: 'Endzeit', name: 'endDateTime', type: 'dateTime', default: '' }, + ], + }, + + // ===================================================== + // USER PARAMETERS + // ===================================================== + { + displayName: 'Benutzer-ID', + name: 'userId', + type: 'number', + required: true, + displayOptions: { show: { resource: ['user'], operation: ['get', 'update', 'updatePassword', 'delete'] } }, + default: 1, + }, + { + displayName: 'E-Mail', + name: 'emailAddress', + type: 'string', + required: true, + displayOptions: { show: { resource: ['user'], operation: ['create'] } }, + default: '', + }, + { + displayName: 'Benutzername', + name: 'userName', + type: 'string', + required: true, + displayOptions: { show: { resource: ['user'], operation: ['create'] } }, + default: '', + }, + { + displayName: 'Passwort', + name: 'password', + type: 'string', + typeOptions: { password: true }, + required: true, + displayOptions: { show: { resource: ['user'], operation: ['create', 'updatePassword'] } }, + default: '', + }, + { + displayName: 'Vorname', + name: 'firstName', + type: 'string', + required: true, + displayOptions: { show: { resource: ['user'], operation: ['create', 'update'] } }, + default: '', + }, + { + displayName: 'Nachname', + name: 'lastName', + type: 'string', + required: true, + displayOptions: { show: { resource: ['user'], operation: ['create', 'update'] } }, + default: '', + }, + { + displayName: 'Benutzer-Filter', + name: 'userFilters', + type: 'collection', + placeholder: 'Filter hinzufügen', + default: {}, + displayOptions: { show: { resource: ['user'], operation: ['getAll'] } }, + options: [ + { displayName: 'Benutzername', name: 'username', type: 'string', default: '' }, + { displayName: 'E-Mail', name: 'email', type: 'string', default: '' }, + { displayName: 'Vorname', name: 'firstName', type: 'string', default: '' }, + { displayName: 'Nachname', name: 'lastName', type: 'string', default: '' }, + { displayName: 'Organisation', name: 'organization', type: 'string', default: '' }, + ], + }, + { + displayName: 'Benutzer-Optionen', + name: 'userOptions', + type: 'collection', + placeholder: 'Option hinzufügen', + default: {}, + displayOptions: { show: { resource: ['user'], operation: ['create', 'update'] } }, + options: [ + { displayName: 'Zeitzone', name: 'timezone', type: 'string', default: 'Europe/Berlin' }, + { displayName: 'Sprache', name: 'language', type: 'string', default: 'de_de' }, + { displayName: 'Telefon', name: 'phone', type: 'string', default: '' }, + { displayName: 'Organisation', name: 'organization', type: 'string', default: '' }, + { displayName: 'Position', name: 'position', type: 'string', default: '' }, + { displayName: 'Gruppen', name: 'groups', type: 'string', default: '', description: 'Komma-getrennte Gruppen-IDs' }, + ], + }, + + // ===================================================== + // ACCOUNT PARAMETERS + // ===================================================== + { + displayName: 'Benutzer-ID', + name: 'accountUserId', + type: 'number', + required: true, + displayOptions: { show: { resource: ['account'], operation: ['get', 'update', 'updatePassword'] } }, + default: '', + }, + { + displayName: 'Account-Daten', + name: 'accountData', + type: 'collection', + placeholder: 'Feld hinzufügen', + default: {}, + displayOptions: { show: { resource: ['account'], operation: ['create', 'update'] } }, + options: [ + { displayName: 'E-Mail', name: 'emailAddress', type: 'string', default: '' }, + { displayName: 'Benutzername', name: 'userName', type: 'string', default: '' }, + { displayName: 'Passwort', name: 'password', type: 'string', typeOptions: { password: true }, default: '' }, + { displayName: 'Vorname', name: 'firstName', type: 'string', default: '' }, + { displayName: 'Nachname', name: 'lastName', type: 'string', default: '' }, + { displayName: 'Zeitzone', name: 'timezone', type: 'string', default: 'Europe/Berlin' }, + { displayName: 'Sprache', name: 'language', type: 'string', default: 'de_de' }, + { displayName: 'Telefon', name: 'phone', type: 'string', default: '' }, + { displayName: 'Organisation', name: 'organization', type: 'string', default: '' }, + { displayName: 'Position', name: 'position', type: 'string', default: '' }, + { displayName: 'AGB Akzeptiert', name: 'acceptTermsOfService', type: 'boolean', default: true }, + ], + }, + { + displayName: 'Passwort-Änderung', + name: 'passwordChange', + type: 'fixedCollection', + default: {}, + displayOptions: { show: { resource: ['account'], operation: ['updatePassword'] } }, + options: [ + { + name: 'passwords', + displayName: 'Passwörter', + values: [ + { displayName: 'Aktuelles Passwort', name: 'currentPassword', type: 'string', typeOptions: { password: true }, default: '' }, + { displayName: 'Neues Passwort', name: 'newPassword', type: 'string', typeOptions: { password: true }, default: '' }, + ], + }, + ], + }, + + // ===================================================== + // GROUP PARAMETERS + // ===================================================== + { + displayName: 'Gruppen-ID', + name: 'groupId', + type: 'number', + required: true, + displayOptions: { show: { resource: ['group'], operation: ['get', 'update', 'delete', 'changeRoles', 'changePermissions', 'changeUsers'] } }, + default: 1, + }, + { + displayName: 'Gruppen-Name', + name: 'groupName', + type: 'string', + required: true, + displayOptions: { show: { resource: ['group'], operation: ['create', 'update'] } }, + default: '', + }, + { + displayName: 'Standard-Gruppe', + name: 'isDefault', + type: 'boolean', + displayOptions: { show: { resource: ['group'], operation: ['create', 'update'] } }, + default: false, + }, + { + displayName: 'Rollen-IDs', + name: 'roleIds', + type: 'string', + displayOptions: { show: { resource: ['group'], operation: ['changeRoles'] } }, + default: '', + description: '1=Gruppenadmin, 2=App-Admin, 3=Ressourcen-Admin, 4=Zeitplan-Admin', + }, + { + displayName: 'Ressourcen-IDs', + name: 'permissionResourceIds', + type: 'string', + displayOptions: { show: { resource: ['group'], operation: ['changePermissions'] } }, + default: '', + }, + { + displayName: 'Benutzer-IDs', + name: 'groupUserIds', + type: 'string', + displayOptions: { show: { resource: ['group'], operation: ['changeUsers'] } }, + default: '', + }, + + // ===================================================== + // ACCESSORY PARAMETERS + // ===================================================== + { + displayName: 'Zubehör-ID', + name: 'accessoryId', + type: 'number', + required: true, + displayOptions: { show: { resource: ['accessory'], operation: ['get'] } }, + default: 1, + }, + + // ===================================================== + // ATTRIBUTE PARAMETERS + // ===================================================== + { + displayName: 'Attribut-ID', + name: 'attributeId', + type: 'number', + required: true, + displayOptions: { show: { resource: ['attribute'], operation: ['get', 'update', 'delete'] } }, + default: 1, + }, + { + displayName: 'Kategorie-ID', + name: 'categoryId', + type: 'options', + required: true, + displayOptions: { show: { resource: ['attribute'], operation: ['getByCategory', 'create'] } }, + options: [ + { name: 'Reservierung', value: 1 }, + { name: 'Benutzer', value: 2 }, + { name: 'Ressource', value: 4 }, + { name: 'Ressourcen-Typ', value: 5 }, + ], + default: 1, + }, + { + displayName: 'Attribut-Label', + name: 'attributeLabel', + type: 'string', + required: true, + displayOptions: { show: { resource: ['attribute'], operation: ['create', 'update'] } }, + default: '', + }, + { + displayName: 'Attribut-Typ', + name: 'attributeType', + type: 'options', + required: true, + displayOptions: { show: { resource: ['attribute'], operation: ['create', 'update'] } }, + options: [ + { name: 'Einzeilig', value: 1 }, + { name: 'Mehrzeilig', value: 2 }, + { name: 'Auswahlliste', value: 3 }, + { name: 'Checkbox', value: 4 }, + { name: 'Datum/Zeit', value: 5 }, + ], + default: 1, + }, + { + displayName: 'Attribut-Optionen', + name: 'attributeOptions', + type: 'collection', + placeholder: 'Option hinzufügen', + default: {}, + displayOptions: { show: { resource: ['attribute'], operation: ['create', 'update'] } }, + options: [ + { displayName: 'Erforderlich', name: 'required', type: 'boolean', default: false }, + { displayName: 'Nur Admin', name: 'adminOnly', type: 'boolean', default: false }, + { displayName: 'Privat', name: 'isPrivate', type: 'boolean', default: false }, + { displayName: 'Sortierung', name: 'sortOrder', type: 'number', default: 0 }, + { displayName: 'Regex-Validierung', name: 'regex', type: 'string', default: '' }, + { displayName: 'Mögliche Werte', name: 'possibleValues', type: 'string', default: '', description: 'Komma-getrennt' }, + ], + }, + ], + }; + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const returnData: INodeExecutionData[] = []; + + const credentials = await this.getCredentials('libreBookingApi'); + const baseUrl = (credentials.url as string).replace(/\/$/, ''); + const username = credentials.username as string; + const pw = credentials.password as string; + + const session = await authenticate(this, baseUrl, username, pw); + + try { + for (let i = 0; i < items.length; i++) { + try { + const resource = this.getNodeParameter('resource', i) as string; + const operation = this.getNodeParameter('operation', i) as string; + let responseData: any; + + // RESERVATION + if (resource === 'reservation') { + if (operation === 'getAll') { + const filters = this.getNodeParameter('filters', i, {}) as any; + const qs: any = {}; + if (filters.userId) qs.userId = filters.userId; + if (filters.resourceId) qs.resourceId = filters.resourceId; + if (filters.scheduleId) qs.scheduleId = filters.scheduleId; + if (filters.startDateTime) qs.startDateTime = filters.startDateTime; + if (filters.endDateTime) qs.endDateTime = filters.endDateTime; + responseData = await makeApiRequest(this, baseUrl, session, 'GET', '/Reservations/', undefined, qs); + } else if (operation === 'get') { + const referenceNumber = this.getNodeParameter('referenceNumber', i) as string; + responseData = await makeApiRequest(this, baseUrl, session, 'GET', `/Reservations/${referenceNumber}`); + } else if (operation === 'create') { + const resourceId = this.getNodeParameter('resourceId', i) as number; + const startDateTime = this.getNodeParameter('startDateTime', i) as string; + const endDateTime = this.getNodeParameter('endDateTime', i) as string; + const title = this.getNodeParameter('title', i, '') as string; + const additionalFields = this.getNodeParameter('additionalFields', i, {}) as any; + const body: any = { resourceId, startDateTime: new Date(startDateTime).toISOString(), endDateTime: new Date(endDateTime).toISOString() }; + if (title) body.title = title; + if (additionalFields.description) body.description = additionalFields.description; + if (additionalFields.userId) body.userId = additionalFields.userId; + if (additionalFields.resources) body.resources = parseIdList(additionalFields.resources); + if (additionalFields.participants) body.participants = parseIdList(additionalFields.participants); + if (additionalFields.invitees) body.invitees = parseIdList(additionalFields.invitees); + if (additionalFields.allowParticipation !== undefined) body.allowParticipation = additionalFields.allowParticipation; + if (additionalFields.termsAccepted !== undefined) body.termsAccepted = additionalFields.termsAccepted; + responseData = await makeApiRequest(this, baseUrl, session, 'POST', '/Reservations/', body); + } else if (operation === 'update') { + const referenceNumber = this.getNodeParameter('referenceNumber', i) as string; + const startDateTime = this.getNodeParameter('startDateTime', i) as string; + const endDateTime = this.getNodeParameter('endDateTime', i) as string; + const title = this.getNodeParameter('title', i, '') as string; + const updateScope = this.getNodeParameter('updateScope', i, 'this') as string; + const additionalFields = this.getNodeParameter('additionalFields', i, {}) as any; + const body: any = { startDateTime: new Date(startDateTime).toISOString(), endDateTime: new Date(endDateTime).toISOString() }; + if (title) body.title = title; + if (additionalFields.description) body.description = additionalFields.description; + if (additionalFields.resourceId) body.resourceId = additionalFields.resourceId; + if (additionalFields.userId) body.userId = additionalFields.userId; + if (additionalFields.resources) body.resources = parseIdList(additionalFields.resources); + if (additionalFields.participants) body.participants = parseIdList(additionalFields.participants); + if (additionalFields.invitees) body.invitees = parseIdList(additionalFields.invitees); + responseData = await makeApiRequest(this, baseUrl, session, 'POST', `/Reservations/${referenceNumber}?updateScope=${updateScope}`, body); + } else if (operation === 'delete') { + const referenceNumber = this.getNodeParameter('referenceNumber', i) as string; + const updateScope = this.getNodeParameter('updateScope', i, 'this') as string; + responseData = await makeApiRequest(this, baseUrl, session, 'DELETE', `/Reservations/${referenceNumber}?updateScope=${updateScope}`); + } else if (operation === 'approve') { + const referenceNumber = this.getNodeParameter('referenceNumber', i) as string; + responseData = await makeApiRequest(this, baseUrl, session, 'POST', `/Reservations/${referenceNumber}/Approval`); + } else if (operation === 'checkIn') { + const referenceNumber = this.getNodeParameter('referenceNumber', i) as string; + responseData = await makeApiRequest(this, baseUrl, session, 'POST', `/Reservations/${referenceNumber}/CheckIn`); + } else if (operation === 'checkOut') { + const referenceNumber = this.getNodeParameter('referenceNumber', i) as string; + responseData = await makeApiRequest(this, baseUrl, session, 'POST', `/Reservations/${referenceNumber}/CheckOut`); + } + } + + // RESOURCE + else if (resource === 'resource') { + if (operation === 'getAll') { + responseData = await makeApiRequest(this, baseUrl, session, 'GET', '/Resources/'); + } else if (operation === 'get') { + const resourceIdParam = this.getNodeParameter('resourceIdParam', i) as number; + responseData = await makeApiRequest(this, baseUrl, session, 'GET', `/Resources/${resourceIdParam}`); + } else if (operation === 'getAvailability') { + const resourceIdOptional = this.getNodeParameter('resourceIdOptional', i, '') as number | ''; + const availabilityDateTime = this.getNodeParameter('availabilityDateTime', i, '') as string; + let endpoint = '/Resources/Availability'; + if (resourceIdOptional) endpoint = `/Resources/${resourceIdOptional}/Availability`; + const qs: any = {}; + if (availabilityDateTime) qs.dateTime = new Date(availabilityDateTime).toISOString(); + responseData = await makeApiRequest(this, baseUrl, session, 'GET', endpoint, undefined, qs); + } else if (operation === 'getGroups') { + responseData = await makeApiRequest(this, baseUrl, session, 'GET', '/Resources/Groups'); + } else if (operation === 'getTypes') { + responseData = await makeApiRequest(this, baseUrl, session, 'GET', '/Resources/Types'); + } else if (operation === 'getStatuses') { + responseData = await makeApiRequest(this, baseUrl, session, 'GET', '/Resources/Status'); + } else if (operation === 'create') { + const resourceName = this.getNodeParameter('resourceName', i) as string; + const scheduleIdForResource = this.getNodeParameter('scheduleIdForResource', i) as number; + const resourceOptions = this.getNodeParameter('resourceOptions', i, {}) as any; + const body: any = { name: resourceName, scheduleId: scheduleIdForResource }; + if (resourceOptions.location) body.location = resourceOptions.location; + if (resourceOptions.contact) body.contact = resourceOptions.contact; + if (resourceOptions.description) body.description = resourceOptions.description; + if (resourceOptions.notes) body.notes = resourceOptions.notes; + if (resourceOptions.maxParticipants) body.maxParticipants = resourceOptions.maxParticipants; + if (resourceOptions.requiresApproval !== undefined) body.requiresApproval = resourceOptions.requiresApproval; + if (resourceOptions.allowMultiday !== undefined) body.allowMultiday = resourceOptions.allowMultiday; + if (resourceOptions.requiresCheckIn !== undefined) body.requiresCheckIn = resourceOptions.requiresCheckIn; + if (resourceOptions.autoReleaseMinutes) body.autoReleaseMinutes = resourceOptions.autoReleaseMinutes; + if (resourceOptions.color) body.color = resourceOptions.color; + if (resourceOptions.statusId !== undefined) body.statusId = resourceOptions.statusId; + responseData = await makeApiRequest(this, baseUrl, session, 'POST', '/Resources/', body); + } else if (operation === 'update') { + const resourceIdParam = this.getNodeParameter('resourceIdParam', i) as number; + const resourceName = this.getNodeParameter('resourceName', i) as string; + const resourceOptions = this.getNodeParameter('resourceOptions', i, {}) as any; + const body: any = { name: resourceName }; + if (resourceOptions.location) body.location = resourceOptions.location; + if (resourceOptions.contact) body.contact = resourceOptions.contact; + if (resourceOptions.description) body.description = resourceOptions.description; + if (resourceOptions.notes) body.notes = resourceOptions.notes; + if (resourceOptions.maxParticipants) body.maxParticipants = resourceOptions.maxParticipants; + if (resourceOptions.requiresApproval !== undefined) body.requiresApproval = resourceOptions.requiresApproval; + if (resourceOptions.allowMultiday !== undefined) body.allowMultiday = resourceOptions.allowMultiday; + if (resourceOptions.requiresCheckIn !== undefined) body.requiresCheckIn = resourceOptions.requiresCheckIn; + if (resourceOptions.autoReleaseMinutes) body.autoReleaseMinutes = resourceOptions.autoReleaseMinutes; + if (resourceOptions.color) body.color = resourceOptions.color; + if (resourceOptions.statusId !== undefined) body.statusId = resourceOptions.statusId; + responseData = await makeApiRequest(this, baseUrl, session, 'POST', `/Resources/${resourceIdParam}`, body); + } else if (operation === 'delete') { + const resourceIdParam = this.getNodeParameter('resourceIdParam', i) as number; + responseData = await makeApiRequest(this, baseUrl, session, 'DELETE', `/Resources/${resourceIdParam}`); + } + } + + // SCHEDULE + else if (resource === 'schedule') { + if (operation === 'getAll') { + responseData = await makeApiRequest(this, baseUrl, session, 'GET', '/Schedules/'); + } else if (operation === 'get') { + const scheduleId = this.getNodeParameter('scheduleId', i) as number; + responseData = await makeApiRequest(this, baseUrl, session, 'GET', `/Schedules/${scheduleId}`); + } else if (operation === 'getSlots') { + const scheduleId = this.getNodeParameter('scheduleId', i) as number; + const slotsFilters = this.getNodeParameter('slotsFilters', i, {}) as any; + const qs: any = {}; + if (slotsFilters.resourceId) qs.resourceId = slotsFilters.resourceId; + if (slotsFilters.startDateTime) qs.startDateTime = new Date(slotsFilters.startDateTime).toISOString(); + if (slotsFilters.endDateTime) qs.endDateTime = new Date(slotsFilters.endDateTime).toISOString(); + responseData = await makeApiRequest(this, baseUrl, session, 'GET', `/Schedules/${scheduleId}/Slots`, undefined, qs); + } + } + + // USER + else if (resource === 'user') { + if (operation === 'getAll') { + const userFilters = this.getNodeParameter('userFilters', i, {}) as any; + const qs: any = {}; + if (userFilters.username) qs.username = userFilters.username; + if (userFilters.email) qs.email = userFilters.email; + if (userFilters.firstName) qs.firstName = userFilters.firstName; + if (userFilters.lastName) qs.lastName = userFilters.lastName; + if (userFilters.organization) qs.organization = userFilters.organization; + responseData = await makeApiRequest(this, baseUrl, session, 'GET', '/Users/', undefined, qs); + } else if (operation === 'get') { + const userId = this.getNodeParameter('userId', i) as number; + responseData = await makeApiRequest(this, baseUrl, session, 'GET', `/Users/${userId}`); + } else if (operation === 'create') { + const emailAddress = this.getNodeParameter('emailAddress', i) as string; + const userName = this.getNodeParameter('userName', i) as string; + const pw = this.getNodeParameter('password', i) as string; + const firstName = this.getNodeParameter('firstName', i) as string; + const lastName = this.getNodeParameter('lastName', i) as string; + const userOptions = this.getNodeParameter('userOptions', i, {}) as any; + const body: any = { emailAddress, userName, password: pw, firstName, lastName }; + if (userOptions.timezone) body.timezone = userOptions.timezone; + if (userOptions.language) body.language = userOptions.language; + if (userOptions.phone) body.phone = userOptions.phone; + if (userOptions.organization) body.organization = userOptions.organization; + if (userOptions.position) body.position = userOptions.position; + if (userOptions.groups) body.groups = parseIdList(userOptions.groups); + responseData = await makeApiRequest(this, baseUrl, session, 'POST', '/Users/', body); + } else if (operation === 'update') { + const userId = this.getNodeParameter('userId', i) as number; + const firstName = this.getNodeParameter('firstName', i) as string; + const lastName = this.getNodeParameter('lastName', i) as string; + const userOptions = this.getNodeParameter('userOptions', i, {}) as any; + const body: any = { firstName, lastName }; + if (userOptions.timezone) body.timezone = userOptions.timezone; + if (userOptions.language) body.language = userOptions.language; + if (userOptions.phone) body.phone = userOptions.phone; + if (userOptions.organization) body.organization = userOptions.organization; + if (userOptions.position) body.position = userOptions.position; + if (userOptions.groups) body.groups = parseIdList(userOptions.groups); + responseData = await makeApiRequest(this, baseUrl, session, 'POST', `/Users/${userId}`, body); + } else if (operation === 'updatePassword') { + const userId = this.getNodeParameter('userId', i) as number; + const pw = this.getNodeParameter('password', i) as string; + responseData = await makeApiRequest(this, baseUrl, session, 'POST', `/Users/${userId}/Password`, { password: pw }); + } else if (operation === 'delete') { + const userId = this.getNodeParameter('userId', i) as number; + responseData = await makeApiRequest(this, baseUrl, session, 'DELETE', `/Users/${userId}`); + } + } + + // ACCOUNT + else if (resource === 'account') { + if (operation === 'get') { + const accountUserId = this.getNodeParameter('accountUserId', i) as number; + responseData = await makeApiRequest(this, baseUrl, session, 'GET', `/Accounts/${accountUserId}`); + } else if (operation === 'create') { + const accountData = this.getNodeParameter('accountData', i, {}) as any; + const body: any = {}; + if (accountData.emailAddress) body.emailAddress = accountData.emailAddress; + if (accountData.userName) body.userName = accountData.userName; + if (accountData.password) body.password = accountData.password; + if (accountData.firstName) body.firstName = accountData.firstName; + if (accountData.lastName) body.lastName = accountData.lastName; + if (accountData.timezone) body.timezone = accountData.timezone; + if (accountData.language) body.language = accountData.language; + if (accountData.phone) body.phone = accountData.phone; + if (accountData.organization) body.organization = accountData.organization; + if (accountData.position) body.position = accountData.position; + if (accountData.acceptTermsOfService !== undefined) body.acceptTermsOfService = accountData.acceptTermsOfService; + responseData = await makeApiRequest(this, baseUrl, session, 'POST', '/Accounts/', body); + } else if (operation === 'update') { + const accountUserId = this.getNodeParameter('accountUserId', i) as number; + const accountData = this.getNodeParameter('accountData', i, {}) as any; + const body: any = {}; + if (accountData.emailAddress) body.emailAddress = accountData.emailAddress; + if (accountData.userName) body.userName = accountData.userName; + if (accountData.firstName) body.firstName = accountData.firstName; + if (accountData.lastName) body.lastName = accountData.lastName; + if (accountData.timezone) body.timezone = accountData.timezone; + if (accountData.language) body.language = accountData.language; + if (accountData.phone) body.phone = accountData.phone; + if (accountData.organization) body.organization = accountData.organization; + if (accountData.position) body.position = accountData.position; + responseData = await makeApiRequest(this, baseUrl, session, 'POST', `/Accounts/${accountUserId}`, body); + } else if (operation === 'updatePassword') { + const accountUserId = this.getNodeParameter('accountUserId', i) as number; + const passwordChange = this.getNodeParameter('passwordChange', i, {}) as any; + const passwords = passwordChange.passwords || {}; + responseData = await makeApiRequest(this, baseUrl, session, 'POST', `/Accounts/${accountUserId}/Password`, { + currentPassword: passwords.currentPassword, + newPassword: passwords.newPassword, + }); + } + } + + // GROUP + else if (resource === 'group') { + if (operation === 'getAll') { + responseData = await makeApiRequest(this, baseUrl, session, 'GET', '/Groups/'); + } else if (operation === 'get') { + const groupId = this.getNodeParameter('groupId', i) as number; + responseData = await makeApiRequest(this, baseUrl, session, 'GET', `/Groups/${groupId}`); + } else if (operation === 'create') { + const groupName = this.getNodeParameter('groupName', i) as string; + const isDefault = this.getNodeParameter('isDefault', i, false) as boolean; + responseData = await makeApiRequest(this, baseUrl, session, 'POST', '/Groups/', { name: groupName, isDefault }); + } else if (operation === 'update') { + const groupId = this.getNodeParameter('groupId', i) as number; + const groupName = this.getNodeParameter('groupName', i) as string; + const isDefault = this.getNodeParameter('isDefault', i, false) as boolean; + responseData = await makeApiRequest(this, baseUrl, session, 'POST', `/Groups/${groupId}`, { name: groupName, isDefault }); + } else if (operation === 'delete') { + const groupId = this.getNodeParameter('groupId', i) as number; + responseData = await makeApiRequest(this, baseUrl, session, 'DELETE', `/Groups/${groupId}`); + } else if (operation === 'changeRoles') { + const groupId = this.getNodeParameter('groupId', i) as number; + const roleIds = this.getNodeParameter('roleIds', i, '') as string; + responseData = await makeApiRequest(this, baseUrl, session, 'POST', `/Groups/${groupId}/Roles`, { roleIds: parseIdList(roleIds) }); + } else if (operation === 'changePermissions') { + const groupId = this.getNodeParameter('groupId', i) as number; + const permissionResourceIds = this.getNodeParameter('permissionResourceIds', i, '') as string; + responseData = await makeApiRequest(this, baseUrl, session, 'POST', `/Groups/${groupId}/Permissions`, { resourceIds: parseIdList(permissionResourceIds) }); + } else if (operation === 'changeUsers') { + const groupId = this.getNodeParameter('groupId', i) as number; + const groupUserIds = this.getNodeParameter('groupUserIds', i, '') as string; + responseData = await makeApiRequest(this, baseUrl, session, 'POST', `/Groups/${groupId}/Users`, { userIds: parseIdList(groupUserIds) }); + } + } + + // ACCESSORY + else if (resource === 'accessory') { + if (operation === 'getAll') { + responseData = await makeApiRequest(this, baseUrl, session, 'GET', '/Accessories/'); + } else if (operation === 'get') { + const accessoryId = this.getNodeParameter('accessoryId', i) as number; + responseData = await makeApiRequest(this, baseUrl, session, 'GET', `/Accessories/${accessoryId}`); + } + } + + // ATTRIBUTE + else if (resource === 'attribute') { + if (operation === 'get') { + const attributeId = this.getNodeParameter('attributeId', i) as number; + responseData = await makeApiRequest(this, baseUrl, session, 'GET', `/Attributes/${attributeId}`); + } else if (operation === 'getByCategory') { + const categoryId = this.getNodeParameter('categoryId', i) as number; + responseData = await makeApiRequest(this, baseUrl, session, 'GET', `/Attributes/Category/${categoryId}`); + } else if (operation === 'create') { + const attributeLabel = this.getNodeParameter('attributeLabel', i) as string; + const attributeType = this.getNodeParameter('attributeType', i) as number; + const categoryId = this.getNodeParameter('categoryId', i) as number; + const attributeOptions = this.getNodeParameter('attributeOptions', i, {}) as any; + const body: any = { label: attributeLabel, type: attributeType, categoryId }; + if (attributeOptions.required !== undefined) body.required = attributeOptions.required; + if (attributeOptions.adminOnly !== undefined) body.adminOnly = attributeOptions.adminOnly; + if (attributeOptions.isPrivate !== undefined) body.isPrivate = attributeOptions.isPrivate; + if (attributeOptions.sortOrder !== undefined) body.sortOrder = attributeOptions.sortOrder; + if (attributeOptions.regex) body.regex = attributeOptions.regex; + if (attributeOptions.possibleValues) body.possibleValues = attributeOptions.possibleValues.split(',').map((v: string) => v.trim()); + responseData = await makeApiRequest(this, baseUrl, session, 'POST', '/Attributes/', body); + } else if (operation === 'update') { + const attributeId = this.getNodeParameter('attributeId', i) as number; + const attributeLabel = this.getNodeParameter('attributeLabel', i) as string; + const attributeType = this.getNodeParameter('attributeType', i) as number; + const attributeOptions = this.getNodeParameter('attributeOptions', i, {}) as any; + const body: any = { label: attributeLabel, type: attributeType }; + if (attributeOptions.required !== undefined) body.required = attributeOptions.required; + if (attributeOptions.adminOnly !== undefined) body.adminOnly = attributeOptions.adminOnly; + if (attributeOptions.isPrivate !== undefined) body.isPrivate = attributeOptions.isPrivate; + if (attributeOptions.sortOrder !== undefined) body.sortOrder = attributeOptions.sortOrder; + if (attributeOptions.regex) body.regex = attributeOptions.regex; + if (attributeOptions.possibleValues) body.possibleValues = attributeOptions.possibleValues.split(',').map((v: string) => v.trim()); + responseData = await makeApiRequest(this, baseUrl, session, 'POST', `/Attributes/${attributeId}`, body); + } else if (operation === 'delete') { + const attributeId = this.getNodeParameter('attributeId', i) as number; + responseData = await makeApiRequest(this, baseUrl, session, 'DELETE', `/Attributes/${attributeId}`); + } + } + + // Process response + if (responseData) { + if (Array.isArray(responseData)) { + returnData.push(...responseData.map(item => ({ json: item }))); + } else if (responseData.reservations) { + returnData.push(...responseData.reservations.map((item: any) => ({ json: item }))); + } else if (responseData.resources) { + returnData.push(...responseData.resources.map((item: any) => ({ json: item }))); + } else if (responseData.schedules) { + returnData.push(...responseData.schedules.map((item: any) => ({ json: item }))); + } else if (responseData.users) { + returnData.push(...responseData.users.map((item: any) => ({ json: item }))); + } else if (responseData.groups) { + returnData.push(...responseData.groups.map((item: any) => ({ json: item }))); + } else if (responseData.accessories) { + returnData.push(...responseData.accessories.map((item: any) => ({ json: item }))); + } else if (responseData.attributes) { + returnData.push(...responseData.attributes.map((item: any) => ({ json: item }))); + } else { + returnData.push({ json: responseData }); + } + } + + } catch (error: any) { + if (this.continueOnFail()) { + returnData.push({ json: { error: error.message } }); + continue; + } + throw error; + } + } + } finally { + await signOut(this, baseUrl, session); + } + + return [returnData]; + } +} diff --git a/nodes/LibreBooking/librebooking.svg b/nodes/LibreBooking/librebooking.svg new file mode 100644 index 0000000..81306a9 --- /dev/null +++ b/nodes/LibreBooking/librebooking.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/nodes/LibreBookingTrigger/LibreBookingTrigger.node.ts b/nodes/LibreBookingTrigger/LibreBookingTrigger.node.ts new file mode 100644 index 0000000..5e2e9ae --- /dev/null +++ b/nodes/LibreBookingTrigger/LibreBookingTrigger.node.ts @@ -0,0 +1,352 @@ +import { + INodeType, + INodeTypeDescription, + IPollFunctions, + INodeExecutionData, + NodeApiError, + NodeOperationError, +} from 'n8n-workflow'; + +interface LibreBookingSession { + sessionToken: string; + userId: number; + sessionExpires: string; +} + +interface ReservationData { + referenceNumber: string; + startDate: string; + endDate: string; + title: string; + resourceId: number; + userId: number; + [key: string]: any; +} + +/** + * Authentifizierung bei LibreBooking + */ +async function authenticateTrigger( + pollFunctions: IPollFunctions, + baseUrl: string, + username: string, + password: string, +): Promise { + try { + const response = await pollFunctions.helpers.httpRequest({ + method: 'POST', + url: `${baseUrl}/Web/Services/index.php/Authentication/Authenticate`, + headers: { 'Content-Type': 'application/json' }, + body: { username, password }, + json: true, + }); + + if (!response.isAuthenticated) { + throw new NodeOperationError( + pollFunctions.getNode(), + 'Authentifizierung fehlgeschlagen', + ); + } + + return { + sessionToken: response.sessionToken, + userId: response.userId, + sessionExpires: response.sessionExpires, + }; + } catch (error: any) { + throw new NodeApiError(pollFunctions.getNode(), error, { + message: 'Authentifizierung fehlgeschlagen', + }); + } +} + +/** + * Abmeldung + */ +async function signOutTrigger( + pollFunctions: IPollFunctions, + baseUrl: string, + session: LibreBookingSession, +): Promise { + try { + await pollFunctions.helpers.httpRequest({ + method: 'POST', + url: `${baseUrl}/Web/Services/index.php/Authentication/SignOut`, + headers: { 'Content-Type': 'application/json' }, + body: { + userId: session.userId, + sessionToken: session.sessionToken, + }, + json: true, + }); + } catch (error) { + // Ignoriere SignOut-Fehler + } +} + +/** + * Reservierungen abrufen + */ +async function getReservations( + pollFunctions: IPollFunctions, + baseUrl: string, + session: LibreBookingSession, + startDateTime: string, + endDateTime: string, + filters: any, +): Promise { + const qs: any = { + startDateTime, + endDateTime, + }; + + if (filters.resourceId) qs.resourceId = filters.resourceId; + if (filters.scheduleId) qs.scheduleId = filters.scheduleId; + if (filters.userId) qs.userId = filters.userId; + + const response = await pollFunctions.helpers.httpRequest({ + method: 'GET', + url: `${baseUrl}/Web/Services/index.php/Reservations/`, + headers: { + 'Content-Type': 'application/json', + 'X-Booked-SessionToken': session.sessionToken, + 'X-Booked-UserId': session.userId.toString(), + }, + qs, + json: true, + }); + + return response.reservations || []; +} + +/** + * Detaillierte Reservierungsdaten abrufen + */ +async function getReservationDetails( + pollFunctions: IPollFunctions, + baseUrl: string, + session: LibreBookingSession, + referenceNumber: string, +): Promise { + const response = await pollFunctions.helpers.httpRequest({ + method: 'GET', + url: `${baseUrl}/Web/Services/index.php/Reservations/${referenceNumber}`, + headers: { + 'Content-Type': 'application/json', + 'X-Booked-SessionToken': session.sessionToken, + 'X-Booked-UserId': session.userId.toString(), + }, + json: true, + }); + + return response; +} + +/** + * Zeitfenster berechnen + */ +function getTimeWindow(timeWindow: string): { start: string; end: string } { + const now = new Date(); + const start = now.toISOString(); + + let endDate = new Date(now); + switch (timeWindow) { + case '7days': + endDate.setDate(endDate.getDate() + 7); + break; + case '14days': + endDate.setDate(endDate.getDate() + 14); + break; + case '30days': + endDate.setDate(endDate.getDate() + 30); + break; + case '90days': + endDate.setDate(endDate.getDate() + 90); + break; + default: + endDate.setDate(endDate.getDate() + 14); + } + + return { + start, + end: endDate.toISOString(), + }; +} + +/** + * Eindeutigen Schlüssel für Reservierung generieren + */ +function getReservationKey(reservation: ReservationData): string { + return `${reservation.referenceNumber}_${reservation.startDate}_${reservation.endDate}_${reservation.title || ''}`; +} + +/** + * LibreBooking Trigger Node + */ +export class LibreBookingTrigger implements INodeType { + description: INodeTypeDescription = { + displayName: 'LibreBooking Trigger', + name: 'libreBookingTrigger', + icon: 'file:librebooking.svg', + group: ['trigger'], + version: 1, + description: 'Wird bei neuen oder geänderten Reservierungen in LibreBooking ausgelöst', + subtitle: '={{$parameter["event"]}}', + defaults: { + name: 'LibreBooking Trigger', + }, + inputs: [], + outputs: ['main'], + credentials: [ + { + name: 'libreBookingApi', + required: true, + }, + ], + polling: true, + properties: [ + { + displayName: 'Event', + name: 'event', + type: 'options', + options: [ + { name: 'Neue Reservierung', value: 'newReservation', description: 'Wird bei neuen Reservierungen ausgelöst' }, + { name: 'Geänderte Reservierung', value: 'updatedReservation', description: 'Wird bei geänderten Reservierungen ausgelöst' }, + { name: 'Alle Reservierungen', value: 'allReservations', description: 'Wird bei neuen und geänderten Reservierungen ausgelöst' }, + ], + default: 'newReservation', + }, + { + displayName: 'Filter', + name: 'filters', + type: 'collection', + placeholder: 'Filter hinzufügen', + default: {}, + options: [ + { displayName: 'Ressourcen-ID', name: 'resourceId', type: 'number', default: '' }, + { displayName: 'Zeitplan-ID', name: 'scheduleId', type: 'number', default: '' }, + { displayName: 'Benutzer-ID', name: 'userId', type: 'number', default: '' }, + ], + }, + { + displayName: 'Zeitfenster', + name: 'timeWindow', + type: 'options', + options: [ + { name: 'Nächste 7 Tage', value: '7days' }, + { name: 'Nächste 14 Tage', value: '14days' }, + { name: 'Nächste 30 Tage', value: '30days' }, + { name: 'Nächste 90 Tage', value: '90days' }, + ], + default: '14days', + }, + { + displayName: 'Optionen', + name: 'options', + type: 'collection', + placeholder: 'Option hinzufügen', + default: {}, + options: [ + { displayName: 'Detaillierte Daten Abrufen', name: 'fetchDetails', type: 'boolean', default: false }, + ], + }, + ], + }; + + async poll(this: IPollFunctions): Promise { + const credentials = await this.getCredentials('libreBookingApi'); + const baseUrl = (credentials.url as string).replace(/\/$/, ''); + const username = credentials.username as string; + const password = credentials.password as string; + + const event = this.getNodeParameter('event') as string; + const filters = this.getNodeParameter('filters', {}) as any; + const timeWindow = this.getNodeParameter('timeWindow', '14days') as string; + const options = this.getNodeParameter('options', {}) as any; + + const workflowStaticData = this.getWorkflowStaticData('node'); + const previousReservations = (workflowStaticData.reservations as Record) || {}; + + let session: LibreBookingSession; + try { + session = await authenticateTrigger(this, baseUrl, username, password); + } catch (error) { + throw error; + } + + try { + const { start, end } = getTimeWindow(timeWindow); + + const reservations = await getReservations( + this, + baseUrl, + session, + start, + end, + filters, + ); + + const returnData: INodeExecutionData[] = []; + const currentReservations: Record = {}; + + for (const reservation of reservations) { + const refNumber = reservation.referenceNumber; + const reservationKey = getReservationKey(reservation); + currentReservations[refNumber] = reservationKey; + + const isNew = !previousReservations[refNumber]; + const isUpdated = previousReservations[refNumber] && previousReservations[refNumber] !== reservationKey; + + let shouldTrigger = false; + let eventType = ''; + + if (event === 'newReservation' && isNew) { + shouldTrigger = true; + eventType = 'new'; + } else if (event === 'updatedReservation' && isUpdated) { + shouldTrigger = true; + eventType = 'updated'; + } else if (event === 'allReservations' && (isNew || isUpdated)) { + shouldTrigger = true; + eventType = isNew ? 'new' : 'updated'; + } + + if (shouldTrigger) { + let reservationData = reservation; + + if (options.fetchDetails) { + try { + reservationData = await getReservationDetails( + this, + baseUrl, + session, + refNumber, + ); + } catch (error) { + reservationData = reservation; + } + } + + returnData.push({ + json: { + ...reservationData, + _eventType: eventType, + _triggeredAt: new Date().toISOString(), + }, + }); + } + } + + workflowStaticData.reservations = currentReservations; + + if (returnData.length === 0) { + return null; + } + + return [returnData]; + + } finally { + await signOutTrigger(this, baseUrl, session); + } + } +} diff --git a/nodes/LibreBookingTrigger/librebooking.svg b/nodes/LibreBookingTrigger/librebooking.svg new file mode 100644 index 0000000..81306a9 --- /dev/null +++ b/nodes/LibreBookingTrigger/librebooking.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..b734aa9 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,3067 @@ +{ + "name": "n8n-nodes-librebooking", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "n8n-nodes-librebooking", + "version": "1.0.0", + "license": "MIT", + "devDependencies": { + "@types/node": "^20.10.0", + "@typescript-eslint/eslint-plugin": "^6.13.0", + "@typescript-eslint/parser": "^6.13.0", + "eslint": "^8.54.0", + "n8n-workflow": "^1.20.0", + "prettier": "^3.1.0", + "ts-node": "^10.9.2", + "typescript": "^5.3.2" + }, + "engines": { + "node": ">=18.17.0" + }, + "peerDependencies": { + "n8n-workflow": "*" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@n8n_io/riot-tmpl": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@n8n_io/riot-tmpl/-/riot-tmpl-4.0.1.tgz", + "integrity": "sha512-/zdRbEfTFjsm1NqnpPQHgZTkTdbp5v3VUxGeMA9098sps8jRCTraQkc3AQstJgHUm7ylBXJcIVhnVeLUMWAfwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-config-riot": "^1.0.0" + } + }, + "node_modules/@n8n/errors": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@n8n/errors/-/errors-0.5.0.tgz", + "integrity": "sha512-0Vk1Eb3Uor+zeF/WVnuhFgJc51wEBTZNBlVQy3mvyr3sGmW86bP1jA7wmRsd0DZbswPwN0vNOl/TmkDTEopOtQ==", + "dev": true, + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "callsites": "3.1.0" + } + }, + "node_modules/@n8n/tournament": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@n8n/tournament/-/tournament-1.0.6.tgz", + "integrity": "sha512-UGSxYXXVuOX0yL6HTLBStKYwLIa0+JmRKiSZSCMcM2s2Wax984KWT6XIA1TR/27i7yYpDk1MY14KsTPnuEp27A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@n8n_io/riot-tmpl": "^4.0.1", + "ast-types": "^0.16.1", + "esprima-next": "^5.8.4", + "recast": "^0.22.0" + }, + "engines": { + "node": ">=20.15", + "pnpm": ">=9.5" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", + "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.30", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.30.tgz", + "integrity": "sha512-WJtwWJu7UdlvzEAUm484QNg5eAoq5QR08KDNx7g45Usrs2NtOPiX8ugDqmKdXkyL03rBqU5dYNYVQetEpBHq2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz", + "integrity": "sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.5.1", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/type-utils": "6.21.0", + "@typescript-eslint/utils": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.2.4", + "natural-compare": "^1.4.0", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz", + "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz", + "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz", + "integrity": "sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/utils": "6.21.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz", + "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz", + "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "9.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.21.0.tgz", + "integrity": "sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.12", + "@types/semver": "^7.5.0", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", + "semver": "^7.5.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz", + "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "dev": true, + "license": "ISC" + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/assert": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/assert/-/assert-2.1.0.tgz", + "integrity": "sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "is-nan": "^1.3.2", + "object-is": "^1.1.5", + "object.assign": "^4.1.4", + "util": "^0.12.5" + } + }, + "node_modules/ast-types": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.16.1.tgz", + "integrity": "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/charenc": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", + "integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": "*" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/crypt": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", + "integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": "*" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/diff": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-riot": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/eslint-config-riot/-/eslint-config-riot-1.0.0.tgz", + "integrity": "sha512-NB/L/1Y30qyJcG5xZxCJKW/+bqyj+llbcCwo9DEz8bESIP0SLTOQ8T1DWCCFc+wJ61AMEstj4511PSScqMMfCw==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esprima-next": { + "version": "5.8.4", + "resolved": "https://registry.npmjs.org/esprima-next/-/esprima-next-5.8.4.tgz", + "integrity": "sha512-8nYVZ4ioIH4Msjb/XmhnBdz5WRRBaYqevKa1cv9nGJdCehMbzZCPNEEnqfLCZVetUVrUPEcb5IYyu1GG4hFqgg==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-arguments": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", + "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-nan": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/is-nan/-/is-nan-1.3.2.tgz", + "integrity": "sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jmespath": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.16.0.tgz", + "integrity": "sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/js-base64": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.2.tgz", + "integrity": "sha512-NnRs6dsyqUXejqk/yv2aiXlAvOs56sLkX6nUdeaNezI5LFFLlsZjOThmwnrcwh5ZZRwZlCMnVAY3CvhIhoVEKQ==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsonrepair": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/jsonrepair/-/jsonrepair-3.13.1.tgz", + "integrity": "sha512-WJeiE0jGfxYmtLwBTEk8+y/mYcaleyLXWaqp5bJu0/ZTSeG0KQq/wWQ8pmnkKenEdN6pdnn6QtcoSUkbqDHWNw==", + "dev": true, + "license": "ISC", + "bin": { + "jsonrepair": "bin/cli.js" + } + }, + "node_modules/jssha": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jssha/-/jssha-3.3.1.tgz", + "integrity": "sha512-VCMZj12FCFMQYcFLPRm/0lOBbLi8uM2BhXPTqw3U4YAfs4AZfiApOoBLoN8cQE60Z50m1MYMTQVCfgF/KaCVhQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": "*" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/luxon": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz", + "integrity": "sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/md5": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", + "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "charenc": "0.0.2", + "crypt": "0.0.2", + "is-buffer": "~1.1.6" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/n8n-workflow": { + "version": "1.120.7", + "resolved": "https://registry.npmjs.org/n8n-workflow/-/n8n-workflow-1.120.7.tgz", + "integrity": "sha512-kTJVxns085po2Tcv9f4bJdKnngxyVnGjA+cQPsNTcZoxM+09R4+lxOWnH5aeAJkRxKEVYZ278/rwF5B6c/mnvg==", + "dev": true, + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "@n8n/errors": "0.5.0", + "@n8n/tournament": "1.0.6", + "ast-types": "0.16.1", + "callsites": "3.1.0", + "esprima-next": "5.8.4", + "form-data": "4.0.0", + "jmespath": "0.16.0", + "js-base64": "3.7.2", + "jsonrepair": "3.13.1", + "jssha": "3.3.1", + "lodash": "4.17.21", + "luxon": "3.4.4", + "md5": "2.3.0", + "recast": "0.22.0", + "title-case": "3.0.3", + "transliteration": "2.3.5", + "xml2js": "0.6.2", + "zod": "3.25.67" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/object-is": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz", + "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", + "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/recast": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/recast/-/recast-0.22.0.tgz", + "integrity": "sha512-5AAx+mujtXijsEavc5lWXBPQqrM4+Dl5qNH96N2aNeuJFUzpiiToKPsxQD/zAIJHspz7zz0maX0PCtCTFVlixQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "assert": "^2.0.0", + "ast-types": "0.15.2", + "esprima": "~4.0.0", + "source-map": "~0.6.1", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/recast/node_modules/ast-types": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.15.2.tgz", + "integrity": "sha512-c27loCv9QkZinsa5ProX751khO9DJl/AcB5c2KNtA6NRvHKS0PgLfcftz72KVq504vB0Gku5s2kUZzDBvQWvHg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/sax": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.4.tgz", + "integrity": "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, + "license": "MIT" + }, + "node_modules/title-case": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/title-case/-/title-case-3.0.3.tgz", + "integrity": "sha512-e1zGYRvbffpcHIrnuqT0Dh+gEJtDaxDSoG4JAIpq4oDFyooziLBIiYQv0GBT4FUAnUop5uZ1hiIAj7oAF6sOCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.3" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/transliteration": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/transliteration/-/transliteration-2.3.5.tgz", + "integrity": "sha512-HAGI4Lq4Q9dZ3Utu2phaWgtm3vB6PkLUFqWAScg/UW+1eZ/Tg6Exo4oC0/3VUol/w4BlefLhUUSVBr/9/ZGQOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "yargs": "^17.5.1" + }, + "bin": { + "slugify": "dist/bin/slugify", + "transliterate": "dist/bin/transliterate" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/ts-api-utils": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", + "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", + "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "is-arguments": "^1.0.4", + "is-generator-function": "^1.0.7", + "is-typed-array": "^1.1.3", + "which-typed-array": "^1.1.2" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.20", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz", + "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/xml2js": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", + "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", + "dev": true, + "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "3.25.67", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.67.tgz", + "integrity": "sha512-idA2YXwpCdqUSKRCACDE6ItZD9TZzy3OZMtpfLoh6oPR47lipysRrJfjzMqFxQ3uJuUPyUeWe1r9vLH33xO/Qw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..67bb1ee --- /dev/null +++ b/package.json @@ -0,0 +1,90 @@ +{ + "name": "n8n-nodes-librebooking", + "version": "1.1.0", + "description": "n8n Node für LibreBooking - Ressourcen- und Reservierungsverwaltung", + "keywords": [ + "n8n-community-node-package", + "n8n", + "n8n-node", + "workflow", + "automation", + "librebooking", + "booking", + "reservation", + "resource-management", + "room-booking", + "raumbuchung", + "terminbuchung", + "open-source" + ], + "license": "MIT", + "homepage": "https://github.com/your-org/n8n-nodes-librebooking#readme", + "author": { + "name": "LibreBooking n8n Integration", + "email": "support@example.com" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/your-org/n8n-nodes-librebooking.git" + }, + "bugs": { + "url": "https://github.com/your-org/n8n-nodes-librebooking/issues" + }, + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "build": "tsc && npm run copy:icons", + "copy:icons": "cp nodes/LibreBooking/librebooking.svg dist/nodes/LibreBooking/ && cp nodes/LibreBookingTrigger/librebooking.svg dist/nodes/LibreBookingTrigger/", + "dev": "tsc --watch", + "clean": "rm -rf dist node_modules", + "rebuild": "npm run clean && npm install && npm run build", + "format": "prettier nodes credentials --write", + "lint": "eslint nodes credentials --ext .ts", + "lint:fix": "eslint nodes credentials --ext .ts --fix", + "prepack": "npm run build", + "prepublishOnly": "npm run lint && npm run build", + "postinstall": "echo 'Installation abgeschlossen. Führe npm run build aus.'", + "test": "ts-node test/test-api.ts", + "link:n8n": "npm link && echo 'Node verlinkt. Starte n8n neu mit: n8n start'", + "unlink": "npm unlink -g n8n-nodes-librebooking", + "install-in-docker": "npm install && npm run build && echo '\\n✓ Installation abgeschlossen. Bitte n8n Container neustarten: docker restart n8n'", + "docker:copy": "docker cp dist n8n:/home/node/.n8n/custom/n8n-nodes-librebooking/ && docker cp package.json n8n:/home/node/.n8n/custom/n8n-nodes-librebooking/ && docker cp node_modules n8n:/home/node/.n8n/custom/n8n-nodes-librebooking/", + "docker:restart": "docker restart n8n", + "docker:deploy": "npm run build && npm run docker:copy && npm run docker:restart" + }, + "files": [ + "dist", + "README.md", + "LICENSE" + ], + "n8n": { + "n8nNodesApiVersion": 1, + "credentials": [ + "dist/credentials/LibreBookingApi.credentials.js" + ], + "nodes": [ + "dist/nodes/LibreBooking/LibreBooking.node.js", + "dist/nodes/LibreBookingTrigger/LibreBookingTrigger.node.js" + ] + }, + "devDependencies": { + "@types/node": "^20.10.0", + "@typescript-eslint/eslint-plugin": "^6.13.0", + "@typescript-eslint/parser": "^6.13.0", + "eslint": "^8.54.0", + "n8n-workflow": "^1.20.0", + "prettier": "^3.1.0", + "ts-node": "^10.9.2", + "typescript": "^5.3.2" + }, + "peerDependencies": { + "n8n-workflow": "*" + }, + "engines": { + "node": ">=18.17.0" + }, + "overrides": { + "form-data": "^4.0.1", + "lodash": "^4.17.21" + } +} diff --git a/quick-install.sh b/quick-install.sh new file mode 100755 index 0000000..da1c634 --- /dev/null +++ b/quick-install.sh @@ -0,0 +1,71 @@ +#!/bin/bash +# ============================================================================ +# quick-install.sh - Schnellste Installation des LibreBooking n8n Nodes +# +# EMPFOHLENE METHODE: Auf dem Host bauen, in Container kopieren +# ============================================================================ + +set -e + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +CONTAINER_NAME="${1:-n8n}" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +echo -e "${GREEN}=== LibreBooking Quick Install ===${NC}\n" + +# Prüfe Voraussetzungen +if ! command -v node &>/dev/null; then + echo -e "${RED}Fehler: Node.js nicht installiert!${NC}" + echo "Installieren: https://nodejs.org/ (v18+)" + exit 1 +fi + +if ! command -v docker &>/dev/null; then + echo -e "${RED}Fehler: Docker nicht installiert!${NC}" + exit 1 +fi + +if ! docker ps | grep -q "$CONTAINER_NAME"; then + echo -e "${YELLOW}Warnung: Container '$CONTAINER_NAME' nicht gefunden oder läuft nicht.${NC}" + echo "Verfügbare Container:" + docker ps --format " {{.Names}}" + exit 1 +fi + +cd "$SCRIPT_DIR" + +# 1. Dependencies installieren +echo "[1/4] Installiere Dependencies..." +npm install --silent + +# 2. Bauen +echo "[2/4] Baue Node..." +npm run build --silent + +# 3. In Container kopieren +echo "[3/4] Kopiere in Container '$CONTAINER_NAME'..." + +# Erstelle Zielverzeichnis falls nötig +docker exec "$CONTAINER_NAME" mkdir -p /home/node/.n8n/custom/n8n-nodes-librebooking 2>/dev/null || true + +# Kopiere Dateien +docker cp dist "$CONTAINER_NAME":/home/node/.n8n/custom/n8n-nodes-librebooking/ +docker cp package.json "$CONTAINER_NAME":/home/node/.n8n/custom/n8n-nodes-librebooking/ +docker cp node_modules "$CONTAINER_NAME":/home/node/.n8n/custom/n8n-nodes-librebooking/ + +# 4. Container neustarten +echo "[4/4] Starte Container neu..." +docker restart "$CONTAINER_NAME" + +echo "" +echo -e "${GREEN}✓ Installation abgeschlossen!${NC}" +echo "" +echo "Nächste Schritte:" +echo " 1. Öffne n8n: http://localhost:5678" +echo " 2. Gehe zu: Einstellungen → Credentials → Add Credential" +echo " 3. Suche: 'LibreBooking API'" +echo "" diff --git a/test/test-api.ts b/test/test-api.ts new file mode 100644 index 0000000..f1b5058 --- /dev/null +++ b/test/test-api.ts @@ -0,0 +1,325 @@ +/** + * LibreBooking API Test-Skript + * + * Testet die Authentifizierung und grundlegende API-Operationen + * mit den bereitgestellten Test-Credentials. + * + * Ausführen mit: npx ts-node test/test-api.ts + */ + +const https = require('https'); +const http = require('http'); + +// Test-Credentials +const TEST_CONFIG = { + url: 'https://librebooking.zell-cloud.de', + username: 'sebastian.zell@zell-aufmass.de', + password: 'wanUQ4uVqU6lfP', +}; + +interface LibreBookingSession { + sessionToken: string; + userId: number; + sessionExpires: string; +} + +/** + * HTTP/HTTPS Request Helper + */ +async function makeRequest( + url: string, + method: string, + headers: Record, + body?: any +): Promise { + return new Promise((resolve, reject) => { + const urlObj = new URL(url); + const isHttps = urlObj.protocol === 'https:'; + const lib = isHttps ? https : http; + + const options = { + hostname: urlObj.hostname, + port: urlObj.port || (isHttps ? 443 : 80), + path: urlObj.pathname + urlObj.search, + method, + headers: { + 'Content-Type': 'application/json', + ...headers, + }, + }; + + const req = lib.request(options, (res: any) => { + let data = ''; + res.on('data', (chunk: string) => (data += chunk)); + res.on('end', () => { + try { + const jsonData = JSON.parse(data); + resolve({ statusCode: res.statusCode, data: jsonData }); + } catch (e) { + resolve({ statusCode: res.statusCode, data }); + } + }); + }); + + req.on('error', reject); + + if (body) { + req.write(JSON.stringify(body)); + } + req.end(); + }); +} + +/** + * Authentifizierung testen + */ +async function testAuthentication(): Promise { + console.log('\n========================================'); + console.log('TEST 1: Authentifizierung'); + console.log('========================================'); + + try { + const response = await makeRequest( + `${TEST_CONFIG.url}/Web/Services/index.php/Authentication/Authenticate`, + 'POST', + {}, + { + username: TEST_CONFIG.username, + password: TEST_CONFIG.password, + } + ); + + if (response.statusCode === 200 && response.data.isAuthenticated) { + console.log('✅ Authentifizierung erfolgreich!'); + console.log(` Session Token: ${response.data.sessionToken.substring(0, 20)}...`); + console.log(` User ID: ${response.data.userId}`); + console.log(` Session läuft ab: ${response.data.sessionExpires}`); + return { + sessionToken: response.data.sessionToken, + userId: response.data.userId, + sessionExpires: response.data.sessionExpires, + }; + } else { + console.log('❌ Authentifizierung fehlgeschlagen!'); + console.log(' Response:', JSON.stringify(response.data, null, 2)); + return null; + } + } catch (error: any) { + console.log('❌ Fehler bei der Authentifizierung:', error.message); + return null; + } +} + +/** + * Alle Reservierungen abrufen + */ +async function testGetReservations(session: LibreBookingSession): Promise { + console.log('\n========================================'); + console.log('TEST 2: Reservierungen abrufen'); + console.log('========================================'); + + try { + const response = await makeRequest( + `${TEST_CONFIG.url}/Web/Services/index.php/Reservations/`, + 'GET', + { + 'X-Booked-SessionToken': session.sessionToken, + 'X-Booked-UserId': session.userId.toString(), + } + ); + + if (response.statusCode === 200) { + const reservations = response.data.reservations || []; + console.log(`✅ ${reservations.length} Reservierung(en) gefunden`); + + if (reservations.length > 0) { + console.log('\n Erste 3 Reservierungen:'); + reservations.slice(0, 3).forEach((res: any, idx: number) => { + console.log(` ${idx + 1}. ${res.title || 'Ohne Titel'}`); + console.log(` Referenz: ${res.referenceNumber}`); + console.log(` Ressource: ${res.resourceName}`); + console.log(` Zeit: ${res.startDate} - ${res.endDate}`); + console.log(''); + }); + } + } else { + console.log(`❌ Fehler beim Abrufen: Status ${response.statusCode}`); + console.log(' Response:', JSON.stringify(response.data, null, 2)); + } + } catch (error: any) { + console.log('❌ Fehler:', error.message); + } +} + +/** + * Alle Ressourcen abrufen + */ +async function testGetResources(session: LibreBookingSession): Promise { + console.log('\n========================================'); + console.log('TEST 3: Ressourcen abrufen'); + console.log('========================================'); + + try { + const response = await makeRequest( + `${TEST_CONFIG.url}/Web/Services/index.php/Resources/`, + 'GET', + { + 'X-Booked-SessionToken': session.sessionToken, + 'X-Booked-UserId': session.userId.toString(), + } + ); + + if (response.statusCode === 200) { + const resources = response.data.resources || []; + console.log(`✅ ${resources.length} Ressource(n) gefunden`); + + resources.forEach((res: any, idx: number) => { + console.log(` ${idx + 1}. ${res.name} (ID: ${res.resourceId})`); + if (res.location) console.log(` Standort: ${res.location}`); + console.log(` Status: ${res.statusId === 1 ? 'Verfügbar' : res.statusId === 0 ? 'Versteckt' : 'Nicht verfügbar'}`); + }); + } else { + console.log(`❌ Fehler beim Abrufen: Status ${response.statusCode}`); + } + } catch (error: any) { + console.log('❌ Fehler:', error.message); + } +} + +/** + * Alle Zeitpläne abrufen + */ +async function testGetSchedules(session: LibreBookingSession): Promise { + console.log('\n========================================'); + console.log('TEST 4: Zeitpläne abrufen'); + console.log('========================================'); + + try { + const response = await makeRequest( + `${TEST_CONFIG.url}/Web/Services/index.php/Schedules/`, + 'GET', + { + 'X-Booked-SessionToken': session.sessionToken, + 'X-Booked-UserId': session.userId.toString(), + } + ); + + if (response.statusCode === 200) { + const schedules = response.data.schedules || []; + console.log(`✅ ${schedules.length} Zeitplan/Zeitpläne gefunden`); + + schedules.forEach((schedule: any, idx: number) => { + console.log(` ${idx + 1}. ${schedule.name} (ID: ${schedule.id})`); + console.log(` Zeitzone: ${schedule.timezone}`); + console.log(` Standard: ${schedule.isDefault ? 'Ja' : 'Nein'}`); + }); + } else { + console.log(`❌ Fehler beim Abrufen: Status ${response.statusCode}`); + } + } catch (error: any) { + console.log('❌ Fehler:', error.message); + } +} + +/** + * Alle Benutzer abrufen + */ +async function testGetUsers(session: LibreBookingSession): Promise { + console.log('\n========================================'); + console.log('TEST 5: Benutzer abrufen'); + console.log('========================================'); + + try { + const response = await makeRequest( + `${TEST_CONFIG.url}/Web/Services/index.php/Users/`, + 'GET', + { + 'X-Booked-SessionToken': session.sessionToken, + 'X-Booked-UserId': session.userId.toString(), + } + ); + + if (response.statusCode === 200) { + const users = response.data.users || []; + console.log(`✅ ${users.length} Benutzer gefunden`); + + users.slice(0, 5).forEach((user: any, idx: number) => { + console.log(` ${idx + 1}. ${user.firstName} ${user.lastName}`); + console.log(` E-Mail: ${user.emailAddress}`); + console.log(` ID: ${user.id}`); + }); + + if (users.length > 5) { + console.log(` ... und ${users.length - 5} weitere`); + } + } else { + console.log(`❌ Fehler beim Abrufen: Status ${response.statusCode}`); + } + } catch (error: any) { + console.log('❌ Fehler:', error.message); + } +} + +/** + * Abmelden + */ +async function testSignOut(session: LibreBookingSession): Promise { + console.log('\n========================================'); + console.log('TEST 6: Abmelden'); + console.log('========================================'); + + try { + const response = await makeRequest( + `${TEST_CONFIG.url}/Web/Services/index.php/Authentication/SignOut`, + 'POST', + {}, + { + userId: session.userId, + sessionToken: session.sessionToken, + } + ); + + if (response.statusCode === 200 || response.statusCode === 204) { + console.log('✅ Erfolgreich abgemeldet'); + } else { + console.log(`⚠️ Abmeldung mit Status ${response.statusCode} abgeschlossen`); + } + } catch (error: any) { + console.log('⚠️ Abmeldung fehlgeschlagen (kann ignoriert werden):', error.message); + } +} + +/** + * Hauptfunktion + */ +async function runTests(): Promise { + console.log('\n📝 LibreBooking API Test'); + console.log('======================================'); + console.log(`URL: ${TEST_CONFIG.url}`); + console.log(`User: ${TEST_CONFIG.username}`); + console.log('======================================'); + + // Test 1: Authentifizierung + const session = await testAuthentication(); + + if (!session) { + console.log('\n❌ Tests abgebrochen - Authentifizierung fehlgeschlagen'); + process.exit(1); + } + + // Test 2-5: API-Endpunkte + await testGetReservations(session); + await testGetResources(session); + await testGetSchedules(session); + await testGetUsers(session); + + // Test 6: Abmelden + await testSignOut(session); + + console.log('\n========================================'); + console.log('✅ Alle Tests abgeschlossen!'); + console.log('========================================\n'); +} + +// Tests ausführen +runTests().catch(console.error); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..88c33d7 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,31 @@ +{ + "compilerOptions": { + "target": "ES2019", + "module": "commonjs", + "lib": ["ES2019", "ES2020.Promise"], + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "outDir": "./dist", + "rootDir": ".", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "moduleResolution": "node", + "noImplicitAny": true, + "noImplicitReturns": true, + "noUnusedLocals": false, + "noUnusedParameters": false + }, + "include": [ + "nodes/**/*.ts", + "credentials/**/*.ts" + ], + "exclude": [ + "node_modules", + "dist", + "test" + ] +} diff --git a/update-dependencies.sh b/update-dependencies.sh new file mode 100755 index 0000000..da4318e --- /dev/null +++ b/update-dependencies.sh @@ -0,0 +1,112 @@ +#!/bin/bash +# ============================================================================ +# update-dependencies.sh - Aktualisiert Dependencies und behebt Vulnerabilities +# ============================================================================ + +set -e + +# Farben +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +echo -e "${BLUE}============================================${NC}" +echo -e "${BLUE} LibreBooking Node - Dependency Update${NC}" +echo -e "${BLUE}============================================${NC}" +echo "" + +log() { + echo -e "${GREEN}[INFO]${NC} $1" +} + +warn() { + echo -e "${YELLOW}[WARN]${NC} $1" +} + +error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +cd "$SCRIPT_DIR" + +# Schritt 1: Aktuelle Vulnerabilities anzeigen +log "Prüfe aktuelle Vulnerabilities..." +echo "" +echo "=== VOR dem Update ===" +npm audit 2>/dev/null || true +echo "" + +# Schritt 2: npm audit fix +log "Führe npm audit fix aus..." +if npm audit fix 2>&1; then + log "npm audit fix erfolgreich ✓" +else + warn "npm audit fix hatte Probleme (evtl. normale Warnungen)" +fi + +# Schritt 3: npm update +log "Führe npm update aus..." +if npm update 2>&1; then + log "npm update erfolgreich ✓" +else + warn "npm update hatte Probleme" +fi + +# Schritt 4: Outdated Packages prüfen +log "Prüfe veraltete Packages..." +echo "" +npm outdated 2>/dev/null || log "Alle Packages sind aktuell ✓" +echo "" + +# Schritt 5: Build testen +log "Teste Build..." +if npm run build 2>&1 | tail -5; then + log "Build erfolgreich ✓" +else + error "Build fehlgeschlagen!" + echo "" + echo "Versuchen Sie:" + echo " 1. rm -rf node_modules package-lock.json" + echo " 2. npm install" + echo " 3. npm run build" + exit 1 +fi + +# Schritt 6: Finale Vulnerability-Prüfung +log "Finale Vulnerability-Prüfung..." +echo "" +echo "=== NACH dem Update ===" +npm audit 2>/dev/null || true +echo "" + +# Schritt 7: Report +echo -e "${GREEN}============================================${NC}" +echo -e "${GREEN} Update abgeschlossen!${NC}" +echo -e "${GREEN}============================================${NC}" +echo "" + +# Verbleibende Vulnerabilities zählen +AUDIT_OUTPUT=$(npm audit 2>/dev/null || true) +if echo "$AUDIT_OUTPUT" | grep -q "found 0 vulnerabilities"; then + log "Keine Vulnerabilities mehr! ✓" +else + VULN_COUNT=$(echo "$AUDIT_OUTPUT" | grep -oP '\d+ vulnerabilities' | head -1 || echo "Einige") + warn "Verbleibende Vulnerabilities: $VULN_COUNT" + echo "" + echo "Diese kommen wahrscheinlich von n8n-workflow Dependencies." + echo "Siehe SECURITY.md für weitere Informationen." +fi + +echo "" +echo "Nächste Schritte:" +echo " 1. Testen Sie die Änderungen lokal" +echo " 2. Committen Sie die Änderungen:" +echo " git add package.json package-lock.json" +echo " git commit -m 'chore: update dependencies'" +echo " 3. Bauen Sie das Docker-Image neu:" +echo " docker compose build --no-cache" +echo "" diff --git a/update-node.sh b/update-node.sh new file mode 100755 index 0000000..44c940b --- /dev/null +++ b/update-node.sh @@ -0,0 +1,61 @@ +#!/bin/bash +# ============================================================================ +# update-node.sh - Aktualisiert den LibreBooking n8n Node +# +# Verwendung nach git pull oder Änderungen am Code +# ============================================================================ + +set -e + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +CONTAINER_NAME="${1:-n8n}" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +echo -e "${GREEN}=== LibreBooking Node Update ===${NC}\n" + +cd "$SCRIPT_DIR" + +# Git Pull (optional) +if [ -d ".git" ]; then + echo "[1/5] Hole neueste Änderungen..." + git pull 2>/dev/null || echo -e "${YELLOW}Git pull übersprungen${NC}" +else + echo "[1/5] Kein Git Repository - übersprungen" +fi + +# Dependencies aktualisieren +echo "[2/5] Aktualisiere Dependencies..." +npm install --silent + +# Bauen +echo "[3/5] Baue Node..." +npm run build --silent + +# Prüfe Container +if ! docker ps | grep -q "$CONTAINER_NAME"; then + echo -e "${YELLOW}Container '$CONTAINER_NAME' nicht gefunden.${NC}" + echo "Verfügbare Container:" + docker ps --format " {{.Names}}" + echo "" + echo "Manuell kopieren:" + echo " docker cp dist :/home/node/.n8n/custom/n8n-nodes-librebooking/" + exit 0 +fi + +# In Container kopieren +echo "[4/5] Kopiere in Container '$CONTAINER_NAME'..." +docker cp dist "$CONTAINER_NAME":/home/node/.n8n/custom/n8n-nodes-librebooking/ +docker cp package.json "$CONTAINER_NAME":/home/node/.n8n/custom/n8n-nodes-librebooking/ +docker cp node_modules "$CONTAINER_NAME":/home/node/.n8n/custom/n8n-nodes-librebooking/ + +# Container neustarten +echo "[5/5] Starte Container neu..." +docker restart "$CONTAINER_NAME" + +echo "" +echo -e "${GREEN}✓ Update abgeschlossen!${NC}" +echo "" diff --git a/workflows/example-workflows.json b/workflows/example-workflows.json new file mode 100644 index 0000000..a815b81 --- /dev/null +++ b/workflows/example-workflows.json @@ -0,0 +1,265 @@ +{ + "name": "LibreBooking Beispiel-Workflows", + "description": "Sammlung von Beispiel-Workflows für den LibreBooking n8n Node", + "workflows": [ + { + "name": "1. Alle Reservierungen abrufen", + "description": "Ruft alle Reservierungen der nächsten 14 Tage ab", + "nodes": [ + { + "parameters": {}, + "name": "Start", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [100, 300] + }, + { + "parameters": { + "resource": "reservation", + "operation": "getAll", + "filters": {} + }, + "name": "Alle Reservierungen", + "type": "n8n-nodes-librebooking.libreBooking", + "typeVersion": 1, + "position": [300, 300], + "credentials": { + "libreBookingApi": "LibreBooking Account" + } + } + ], + "connections": { + "Start": { + "main": [[{"node": "Alle Reservierungen", "type": "main", "index": 0}]] + } + } + }, + { + "name": "2. Neue Reservierung erstellen", + "description": "Erstellt eine neue Reservierung für morgen 10:00-11:00 Uhr", + "nodes": [ + { + "parameters": {}, + "name": "Start", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [100, 300] + }, + { + "parameters": { + "resource": "reservation", + "operation": "create", + "resourceId": 1, + "startDateTime": "={{ $now.plus({days: 1}).set({hour: 10, minute: 0, second: 0}).toISO() }}", + "endDateTime": "={{ $now.plus({days: 1}).set({hour: 11, minute: 0, second: 0}).toISO() }}", + "title": "Automatisch erstellte Reservierung", + "additionalFields": { + "description": "Diese Reservierung wurde automatisch über n8n erstellt" + } + }, + "name": "Reservierung erstellen", + "type": "n8n-nodes-librebooking.libreBooking", + "typeVersion": 1, + "position": [300, 300], + "credentials": { + "libreBookingApi": "LibreBooking Account" + } + } + ], + "connections": { + "Start": { + "main": [[{"node": "Reservierung erstellen", "type": "main", "index": 0}]] + } + } + }, + { + "name": "3. Ressourcen-Verfügbarkeit prüfen", + "description": "Prüft die Verfügbarkeit aller Ressourcen", + "nodes": [ + { + "parameters": {}, + "name": "Start", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [100, 300] + }, + { + "parameters": { + "resource": "resource", + "operation": "getAvailability" + }, + "name": "Verfügbarkeit prüfen", + "type": "n8n-nodes-librebooking.libreBooking", + "typeVersion": 1, + "position": [300, 300], + "credentials": { + "libreBookingApi": "LibreBooking Account" + } + }, + { + "parameters": { + "conditions": { + "boolean": [ + { + "value1": "={{ $json.available }}", + "value2": true + } + ] + } + }, + "name": "Nur Verfügbare", + "type": "n8n-nodes-base.if", + "typeVersion": 1, + "position": [500, 300] + } + ], + "connections": { + "Start": { + "main": [[{"node": "Verfügbarkeit prüfen", "type": "main", "index": 0}]] + }, + "Verfügbarkeit prüfen": { + "main": [[{"node": "Nur Verfügbare", "type": "main", "index": 0}]] + } + } + }, + { + "name": "4. Benutzer-Übersicht", + "description": "Ruft alle Benutzer ab und formatiert sie", + "nodes": [ + { + "parameters": {}, + "name": "Start", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [100, 300] + }, + { + "parameters": { + "resource": "user", + "operation": "getAll", + "userFilters": {} + }, + "name": "Alle Benutzer", + "type": "n8n-nodes-librebooking.libreBooking", + "typeVersion": 1, + "position": [300, 300], + "credentials": { + "libreBookingApi": "LibreBooking Account" + } + }, + { + "parameters": { + "mode": "runOnceForEachItem", + "jsCode": "return {\n json: {\n name: `${$json.firstName} ${$json.lastName}`,\n email: $json.emailAddress,\n organization: $json.organization || 'Keine',\n lastLogin: $json.lastLogin\n }\n};" + }, + "name": "Formatieren", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [500, 300] + } + ], + "connections": { + "Start": { + "main": [[{"node": "Alle Benutzer", "type": "main", "index": 0}]] + }, + "Alle Benutzer": { + "main": [[{"node": "Formatieren", "type": "main", "index": 0}]] + } + } + }, + { + "name": "5. Trigger: Neue Reservierungen überwachen", + "description": "Trigger-Workflow der bei neuen Reservierungen auslöst", + "nodes": [ + { + "parameters": { + "event": "newReservation", + "filters": {}, + "timeWindow": "14days", + "options": { + "fetchDetails": true + } + }, + "name": "LibreBooking Trigger", + "type": "n8n-nodes-librebooking.libreBookingTrigger", + "typeVersion": 1, + "position": [100, 300], + "credentials": { + "libreBookingApi": "LibreBooking Account" + } + }, + { + "parameters": { + "mode": "runOnceForEachItem", + "jsCode": "const reservation = $json;\nreturn {\n json: {\n message: `Neue Reservierung: ${reservation.title || 'Ohne Titel'}`,\n resource: reservation.resourceName,\n start: reservation.startDate,\n end: reservation.endDate,\n user: `${reservation.firstName} ${reservation.lastName}`\n }\n};" + }, + "name": "Nachricht formatieren", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [300, 300] + } + ], + "connections": { + "LibreBooking Trigger": { + "main": [[{"node": "Nachricht formatieren", "type": "main", "index": 0}]] + } + } + }, + { + "name": "6. Täglicher Reservierungsbericht", + "description": "Sendet täglich eine Übersicht der heutigen Reservierungen", + "nodes": [ + { + "parameters": { + "rule": { + "interval": [ + { + "field": "cronExpression", + "expression": "0 8 * * *" + } + ] + } + }, + "name": "Täglich 8:00", + "type": "n8n-nodes-base.scheduleTrigger", + "typeVersion": 1, + "position": [100, 300] + }, + { + "parameters": { + "resource": "reservation", + "operation": "getAll", + "filters": { + "startDateTime": "={{ $now.startOf('day').toISO() }}", + "endDateTime": "={{ $now.endOf('day').toISO() }}" + } + }, + "name": "Heutige Reservierungen", + "type": "n8n-nodes-librebooking.libreBooking", + "typeVersion": 1, + "position": [300, 300], + "credentials": { + "libreBookingApi": "LibreBooking Account" + } + }, + { + "parameters": { + "aggregate": "aggregateAllItemData" + }, + "name": "Zusammenfassen", + "type": "n8n-nodes-base.aggregate", + "typeVersion": 1, + "position": [500, 300] + } + ], + "connections": { + "Täglich 8:00": { + "main": [[{"node": "Heutige Reservierungen", "type": "main", "index": 0}]] + }, + "Heutige Reservierungen": { + "main": [[{"node": "Zusammenfassen", "type": "main", "index": 0}]] + } + } + } + ] +} \ No newline at end of file