diff --git a/backups/backup_v2docker_20250609_050507_encrypted.sql.gz.enc b/backups/backup_v2docker_20250609_050507_encrypted.sql.gz.enc deleted file mode 100644 index e30e264..0000000 --- a/backups/backup_v2docker_20250609_050507_encrypted.sql.gz.enc +++ /dev/null @@ -1 +0,0 @@ -gAAAAABoRk9kkVVOa1-y0m5PikSvdtCeQihW0DGmwQrNC5LE4Wmeu8eN_qWLkT9tdnrBnYfocUZ6YTVLOmWDHfQar28xll5UbATA3t23VAHOmTEQyDCa5JFAaX8X1EE-dDQyONOlbriNEx78BxBr78LjMcSYKu_dHSXqe4jk9EkC6hktkkAz6EwnRGkDGRtfAdG2Fc4oIyErD1SeRuM8R6GBl6dxjzb4bV4KCKiRv0RLUGby3VyZkKgUwMtI6edvgXtXRS8PvxoJ1rluaN_cAA6L80MtphoGnQfUsD_EqIDDyOKFNcKmDVIQIy3jyuqC_4Apk6BAwwlj-OUWAD-dGZkZaGMURfI_SYOklLXuKgbU2vmkSqNd8xfcYTLijTg9K4Q7y3U-Dg06Q9OYLe-Wg5wF86YUmxSnvs7yoAObkOi8fdITcYf21UEU-OSeWUYtqiMVaVG1JhNZhoqZ_LXOPfKfaf_esQZNCiigXQV1rkt0AMt8-hgtxHGZHeu2yaN3ObEYeQnmEVFZrrA17ui-4SHTfmzShCrMoqCIpQOsQkhgCoA74BhcucsSoStVwna8tk6ZKpgdQfT6Dh2qR2PDIbqz07a0PE_FSmpDO7TSCzrww43vfweYq9iYiZUjbmOeahHiBJpgo2dD2h3oZucBsN2_tZk6cw_bPSA5l8_HEXlxw0lkN9gtWlYWmHGOS_N6L1sYMgHztLGC8zn3I7bnfeNz79hCeJX56IRJ3nuUclZubkOs97Gy-JuLx1t1qZdxybmkLjSidDPDV02aldFcTGeWQTUriCeQlXCOVWLFEi9AnpVyRIOiK19FLCTSm5vRENAQh_I5B5oUsPPZxghIiAYSgq7S0Dide3DgfQkq0N_iQe2r05VIM-PJn6_9tmNdjTxtOe2Z-Nby42SoRtGaL9Y1ZyPreCWWjHOVllm7kFBVbIjC9JgHk4ToTB5RrjI7k1YqAFAS-MJhJHRaZVqwazwJAQ7UQBmoufJktHQKINkninxBDcfrV9cC9lXL9NB-qpQadQWUU21Qwz1zLsWf4-GMGkvvX70cFz3rpJT7WKuyOB2U0khnTUrRS4yfAW2weQeyhRrbJw2dvhL8OKBVofWvoaY3fMeXo886QFftIm-hIcXY_6zmfVSmEP8KPNDkZ1GaaeZ8NiVMnY8LNT8_R5ISOPZGm_BWcHVBWLagpu51Ys_3Fn49mmdSdbKN-0TY3yxBA0e8NRYb87tc2trxoSDy5ItQrUFljm-Sdv9_swKam6sx543jiBWaX9NfH8j12QgQamScAUxBNXTIMvNDogYpWTy_KG3J7hsIiSG-XnVRFkJMoD30vepgvQs8Yz7c_6xECV1lCqFI1POwESoo0cGy-IM8sSG_WEvpbmPeIjjDpTuosz-B7FmhotZt32qMc3hGU4sdhpzoGwK_6A5DtvkTSvkEt7V8uQLb0ciJwAGXMTGKJoH2d2G9XZlVCZABFtkom9PRkyjeq19R5IHnTX6d4votsD3hurtlJapkB_STAy3ovFfo-yaiI2L8wluvw8MvU-UkatdhU9lCBwq4eGwF6DyD9U_B6FAVRqYYd_avjbQy4-x7C_kUpbsdlgEP9S8EwnGhJy1G_Nn3AbzY-ffd8MNIP30uqluIFHeX1zdNeFVOf9u_JCMaqNyvdc1bghhnM_tUskbJurwWaUOYNZaZ6eUCkVzohcRadcaFEI3jWs_H6IGdkSYYNiNJOG5eOG0rLFSkjbHJDQFPZjkFT-rKQmd-dBCNBk645TJiL6FLM5MpK31yN4Wpi_sKy1RmgqyteF4MVc4kk6Sa-04HJkgk_RPStYXhO7aKyJk3fzF9iQ__kQ3GROjGcxgAZnvKqGfU0zKKLnjrXKewnIRgVaz3nVt26aNuCyvCsfRmXq_OT3wn0HXQ6czJcHLFP5z1O_UzaFWN4ec8es_ZqQqdf-aKq_zyreXlgElD1h7iGOcuB1DTF3tzBUvB1mOND9iOz7Qgyu2NX1jm_8VQpuvZLV1m7Cz_Fj4RZWevvrCz9-PpiytuBYW_Y1hNSHNGFj3LYcu-Zot5qmRYQKabA5VwM9mQ7ZuMGgpLlwSnxZx_exRf5hS3sMZWeWb9Q0rtlBTd64r5JEHb01if8AUt_Fc_3fkzCLdAHiqw26FyjzfYjP7KHtljd0csIKS5igtKc7Je3WwL4l53KiFgP17jzccaHBIG9Uybk0o3NRBOkhx-RtGUQHg-0BcCCQjpMqngsRFh0u8DLuFASvRe93j56bqa5fW7UK3cdnECAndQc-22ZhNOk1GVTpKkr_xAJN7pHFJbnGOthp8cvN-ruxq1SUzPt0yMI9BtOfp1htB5LGzzBMWlwy6ob9whBD0voQuS99hT1D3QjdOEuAg1bWYsaNgdmExmF5zsyb3fX-FlYm5Fbq8sAgsnU4awhGoGofCbdxKzS7pck150C-jjhJxYgbIniEmLfBnbz174Wjt3lG-J8HJBOuekjT3i8sQai1gdwu_tZK9qqgbbCY2laYYQPMyw32A3xxKc7-gnLgXqlx7aHvlV-Z2Ahk3o8Ohuxpic4e2wW3OpKWXD9QA_09WCg5hXW6HHGKfpmRrCwLM3OBPH5lyAN2dLP0ZJyZfiN9aHPZSjAYI_vc6SnQzvM4fj6X9gJQtCDwSg8sqXhYVv4Op6VvilaiCWZu9Iuxhty1vq_NzS4miuiXwOeVaMOSHOT9t2lCAlEhv9UdWc1Po6jazWEBYfmA3_BNjxvUzELR8X44qi0-6OdBs3fFKrepvSXp1zn3C-yBQvm1iYbAnmXoq8cW_wrk4j78bn6T1Az1JrmrpVSTRf1r9Ma5rdJeoLJUFZQZtHLxztT3WeCNcDHHbRHFFL13yoDx5-2he_R1zOOgNZBOfbtra_lNgcXGRX0F5K1cVx-hEoH7fa-TCg5naQpd7218oeTirqLPhI68Gtm8vd2RkAVPdo4pV2K6x0o35ILOduQPIULMIwCEwQaFteouym8TeVcQdY6SVHrMWLKm906IqQyksiG6qEne8rgX_72SZerA0nWkru9dvqWhksYKPmhygQShnKMHmnI24m7RTbT9JC3wXYdhl10V25aNRGvvnZmICwh3dqD6GnRhDLuM7nyt5jyur22GBXw1vk8ocyfB853f4USA-BUL93VPMYrTdgSfOWpXlVSSFlb9SuNPHXw3kPVFbEFlLV46DOhVAX6izir4ExqtAWpUQXODn989SpG8rbxf55UrczDAkhnkRx5-Uq9L6vcuw5WFJEBLt2BcSAPfGVr0N_P0aihMl_SDjs-QW7_eFzg--zeY7XwULlWXIpx27I5Pi7WKF68czEqDtVZtBxc4_fzBD57xBiqQ2Wynnpg3ZJJNWLBapHn5uRrtWPLjOqFySbF4P7aIN-LJ2-LSeEQJsW2rSFoNQpQgf34kh6JTdGw0xsmU9MaAzlBHWCr3f_8nt6fd58kwIgLQbu0eAu24WFaqX61TfVQrJ5UbzjZ58x69CCydhDP2bMK2HYHgMiahrlTBdTxJuekpucehbxlnSnlqCDqDwOPixJwYrtqr4u01yASZRMpyEB9bpdxTDzeb9yMMI11YseHIn8qFzOzG5xaB3TcLMsu9K_c9qzw9YlCFJVagsQMXptE5PPPa6LebC37eQ9a8PInCt5_FTu3vlXgEI0bW77iU9Am2QESrWG3pHNBOqpwVsOI_fzCwjdhr-tBVn-tbOE7s3vNWHJmoZkeBwUglR_fFBi9Gsxw2mTBEU8jnrbV9vb6DPyFBJGm2GvkBsxmHuFaGBwjqOv6WQTZYtw1usAqCM9QHEaSR9g_m3_u8xLG6pH09OVSaE19BHRQ9apqcl13LeQUbciqpg5RBOPh8FODx2FEMF0-THTXI7JeW6N5K6KadR7LY7oJ2t7wNlodAbUM2lAaHouhkU52dAWbsAr607cbeR4U5yEAX7wf_yaWdkPzZ4JUUL5aken1e8KwEJ0cSnXvc9CAwk7ABnaJ3KyuHxrMabRSre234xT1GN8mT1KQK7Z1almHZmWF-s2lE2xvYHsTCrcxIP_5Vyvah9PD5L_0Pdf617wOfHscyGXuH4x3slN8tHfes1AWY7gUivwFAVW_MvQzYd9bTrDW0AtZYOo-KB6zAgoi2iqB7M9nOwmUeqyACoCLB9rhSd7VayNMuBI9BiEIuTZyt-y_Tt9QztuP62xlvT5_rx1vvJ1OKLiWDFr8Zy75xVP47BNt6Fp163M0I0dL5eeeDS3PbGA54py7GFbM3VDEPJW6lh4VI4r4fZDDorEB5ktn0hbDSlwum_vcjXqEwepzOy-L3YSkRgzevRJxdsmhp3Ygz5PiiPttQg0iRw-F-7uET2fR1vLlVTCY0pcbSkncxR4yOVOKAqzHdUmJs1rbo4CIk1WHhIJW3e3saTCDyEe5urCpg-tpGY-SWnOFWVPD_stOn63U_IM0QrsRdZXLwcnR01q-39qAeoTMORn4B7lAhZ9OWXI-14kDy2CdkSYVURFX0uXpwAsEvJGEm4lOBa8fmmGKIqeD5XMjRtGoWkiaYKC6FHfXscBSvb8r-6fjHU_fKLkoaU58-vzCAlU0yU3nBTPxg7dv_czz1tcRVy51Z0qLKyyBhikh2edoTdxLPXp-UmHEgA4CFPZZ4mj9Q-hiy-wVwsmodJcTvUhriUVmLs1E6JPjTXgkkF9vI0A4PuySDaW6UeajMlWchDJNU2UnM50bfn7c8j0dk_QdUWIe08KoOWsoANNOwpelp800-uXoFGnQzmEW5UM8ORIw4tLhG6BmS6qP_CMu58Yy9cp5ICqanmB81dPrsLMDNP1ByRO-LbgokLoG_NOmazetF2qYiIaT9KtS53ADPeGzHmC2CFi5ngCxuTP71BEWgpXNE0Dnoxr5SpqZtsTJIjDB2pT0QLoE_DGvTU_2PP0Myltgyb5_h1H2roK-Cz-BxnduKd2cf9P2B1o2YG3WEA_Qo7tzq7lBN1LO1xBD3PEwDI7iiAUIZNLjxzWH5Hzu3OHYBPLpua61uM1e7ZrvJBiduEDnFseEo803qSrbH6Fgj5KZvWAZH6CwmnDXW17fDb7KEzBdQOCZTNK-i-kF3lT8RDLJF4_pF0D1WlQlVrXkHMJ0IHgAkZ3_KTw1HiPBaacBtk6EvqDiuXRn82768_eoZ8RKWd4X8B_acXtuyDZS4SerHuYLIgHK3w04jRsTVK96ADJP8j7yH-32S4lR35Q1yoaWb3xVI6GFCPYtjZMlUbpamVX-MTCZSXnwNj8BNsHhZjKI8Qk0EW0ZSbgWOOyy9q7QtywY6LuzpQ2cZuXkJX0h5zVWPBdmbwEtYFKRdhEw0yDA-XA_4vr4ZINCLVsCV_PuyuGLYviei8DPCHGVoTL8p7AvHhW1LaofbE-uzgjn1uubqnsBB1e1s4NrShUe1XMNDyQu2WE5VcRTXF3v9GtmZ_SyKKb93wZ92oThhus1ve_qMya2P0YtMroKS8CFaizdQYCYh7DD4AUoJt-OurH_BykwafHpJRS7LUhU0UozUVeNQPMBks6ZPE4UvriyaRqLbnoP_t54bekLTYmrna4IrnRViYhzIc0UP3i4GxHq-bzMUNI9Fe9fQykP5UJLSFMGi_IMae7dghucDdHieWeBmsrTgu4uUfPYkcOoZYoLigpEan_hnWovpmJ7tAyfUcD5WcOkOtPX5L0Ym_tZp8LR5URWerQMg2YEbwbmE6sSEwh_TfI0TPWRrqg9LC82OJKO2xOG6JZqeaedEy8PnOl4I7mknd2hSmBInX-T-6q1E0CdugEU2D_xzXvTcFoWSCo7fvd90WgO-zfX4bgGIMZQskiYJbLyGAhO18xomnKyPhiLRpv4nQGCNqFFKgY5wEENYn41dlMa34XXRd4tknouZJRxipAs57nlSq7yGUz_cTl4kLxhi_U314MrXwWK6PveEgn1mod0xsZPzpgkQaIE27oeVVTJeCWdk0OADT2Uwqk_RWwzRJ_Fl5_p0X_PDa6Je8tXipKHRsFMldxnujuxrVDLVQFHMSmh4ihjQzSptLidmlRW0oqZdZCS4GERt-llSD3w0nS5c2LCNTd_K7zCxgYyLtMwA0wZ9-R-7mRAxKuvMKhKvYkHf5yD9ncEbyXkdTfJxfF4zOUSZgAbhKyELhztK367TH69Y2m_jwYzxVOEUODcqivgSloIabbqnBTV725YIzxqCCOsyvnYNkyipk0KVNr6Yob4H1iZiEQhuBygdK4Ghs-rE8I6Q8cD6MjLzZJ1htF0_Rts5cRt_9fUmd_EX1DyS8c3gEyWSD4CyOLZwyvG-QuaU06mZSkqwQDo7tCk4eiDKvaOb3AWqXUG4ZyqgfuEljoo6cKzvhvE6OGVAsh58_p3DYZ2HcvNynEVRd3j7GLgIXFrgCLfgMg6K1lBMiUMCTVzvWkjGRMjeEkwYYwwHkClhOVFgHf6keS1QgdS6MYxHdpqBFzdxIT4JovcdUTjDqmBs1wWAtpCXpuHpj4y9xY8uNOL41FYaJGd9Tv_xxgJwFNCN7tt4PGqVWdMjK36oxjV8o2yNfUYc6CqzsfGSJOJq275cd4Rku7eqrlw7ZSqfsT6uTx5AIu_Kj3leSY7YjV16GEXUTKjmKmNugtOj6bO81tU2Fz4mlCY6Y3Sdp-JoG-X6I_TNDl4mKrIjq-t9UqP9W3cUdyXY-e07hNwcaKDRb0WaXwAELTnwYmxPFXDfAiP_SMZ_Ol9z0mXoos3WxGlx9aaE96s7WzOZtsvtr23tMBjGxsgNull_uXsf2Wwb61xk0U9cQbta09FhjqF2b6X6iW6VY0Wjd9wasUBb2josUTXKyYSfTDI0q2RH9LMhiVFaX4gE3_K0U8c9na3OFNLLGDjwXG6jCPj2JzauFZWnBxPSC7ZdGPEA5umFanhqsNCQy0rLtvCXqfFWe_1InLfUdfCiWwNT7cNNAIFI-zq5e_kbJCRd1fip5BeuPVT62mAUB02V_TyIQhYTN9dUswY6Cg5FHSUKrfUFeBhi2Dmqqu_--FTNqxew99TigTS_gN0cxDxsLStqev8cwddBALWTCP3C6EBfrhP6i5I9bLtxlnPnGV5whojYDQ07xRdWx9x2820ltO5GZj4iPNlX8LggywDz3qClzG0eToWT0WUoS7yN60E-pU3_L5psCTvqh8gIEUFN7MUw6T_DRxjsjaxnPxTQtt31hHNaDbJ99QXJfrnU4KPOsZGFqzwQC_IdNvX0YWRGLeaFQVoh4WfCXLskTBaScekE6zjlmj7fvh99RQuod1aaHCL1c22RA515m9nhauKJPSfw3fq8_t1YlBle1mVkj7-Pn0uo-3icbZXq4-aFFiDWTQz8_IGUFxMCmQRkh_5npUMChTN3ft064Vg8ZFqonCunOe8Id0pN4_JBKkwH6dkuq3JOITnhQXNvY7nMymz8fm8hGupjCogqL2D-x4_Fh18vUHnIn8zTz2HmZd_mQatNEH1C3ppQAWY3blBUGn8zX9EdU60aDnJZbv84xgyrjHF6TUsbt0V4OhhK0frcqP8d8gax4AtNA0kP1TLC_aXAqXjsuh5TAGCCMjYVHr6GrmlYPehsiCN_yhZWtuCVmEqFQBZn-ujDELGw1pxM8DXswp15j42gvIg-E3MUZL7g0Ik349goOzRWuI5Uz8JDNygEtw1f1FD97Yg8tSvEnigLZ0fhIH8MmrD6sA7GXgtgsw-Y22w7NSlTFTA-x1AxmW1A0wP6yRbcPWiN4vyreOyetLWp0R5OJKmcOFFKimcwwdSxjdu1EBZfbSLQZTmmEh5HFMjV2ZvJMkfLOlJEasPWqxgglqF209u127yERZBtHbZmNjBo2tB9x9l4xYSrncu8f2GHOvj3sr3N00ESTs43Ra1_QFM9stZFb45D_Mpd3lVtAZzAitWGx1dUTBTrw5hrl4IUKBrXaCWirSyPheccwKbaDNVTt1mDcWnm1P2QkKDcwECx9P60d5eLUejaq6w6V_EvgA7Pr-S9xodFIF3M4uxCjh0oT6qSpL6TmAQ7jGD2qHRkIbt_ai3o5NM2ZkHL_MB_e73UWgfoMwMF4WPLEizryF-AlPD63rPdcKMy8hvh7aeNTS50467b0g7xrjJsTyiyaQiXByYTusiWv6Mrne83O8opL2sFW_iu3O97lkTCmpcw2quJmVUeDs4351zf3YvlQBo2Q3ihVK5XrpitgiJ3uRPmLSp5-krQ6R9qn9ETd747cVMq-XesWhgnfhSS_vxl89eeRSIceEVryKqcP5GkOujKNw4BaUMwkEwm56drM_RndM4ARELtcTDbqbAR2XvToqCdToyii9LrQFUZfHyeZ-zWMVyJMEwfInTQ8Tw9NMOQ-2k3ML_c546io4gbxIO-Wnt3d-dUcMiH8xpg-5Yg8Vr-HIhvmphhYXG4JNI2tb8J3jXxkPc1tAznyOkfSMAHHKhYcS-CZiq62Yq2X9fjhPFnlUzQwDj03fjL9vHfSXjzQ1LG4jGWKoWFTOUy3xt_8nValuPBlvz2AMMd_uAT_FQ2lUxywIU86Xd3xmWqgr5jen2MbC6LPWPqG8dcuA2MBpNLwD701Aa6P7c9RshOOrg496Waz_vj4SgEoiHFUte5RpbPKijV5Auqh4kJw3_buZWjVOSWzxQvAWPBk5DxProDUitPIdm3Zj66CTFJ9mTfQ4Lo0irg_gofj4qFAVxSkx_rGXdCq3fzxqnzGsSVZnMUgaUJI5Aeygqwg8w3yCmKJGawMbKFTxOcF9ANFAKV5Pgt93SLmNNTJ1D6FUPYdQtPXJwBqpNexwNyewND9uyFUDOVnliK-jqfy5N21ypOfxfwtoBHpQxfaeNO86GCbrNt1FvzNego7-ixE0TvEabRr03o6XCDKXMxHhq5gcOPaSeAJnT3zbxQ-FmoZ_9jO_9OK5CPgeWLON-M9ALKNj4B7j35zdb1g4fH8eYrM5oS6D_QPwngNCxg8Q0p0vxBJXfQfsU6Y7gE2d0OvHN3DQqtj6Q2fKcDOJGhDP7clwf5AL1AWgtjJg9gm_rfBnSbVBZhutdRkHEOCS_Mwp-L0HPDtYqK-_t97y401KE8HxDI__y8833i8g8tDRVAprdr4iQdsWYo_23FHG4vy1nAbzS1pdx3LFWYaI9yecAoj8PIIBwZHHR4ck9SKc1TC-9sML7rw7oks0vUyZOv8e2BOoyiH1AKZYZySV-dQrAHHkBVo5fOli-YFKl-6BlYoJGQk-iD5diykRJ-1Rcxv2umQW8cHGYozuNr-tdz1R_Y4BJ-oVAJVWnlD1zIZfb_iiyU7jbCLbbCAV_R4f-LVSefjGIVrArfBHSzlHeKNkaDfWau4i19SnEjT5knnKo4ShiWmbgqAEMdAQ7EK-ESyO5nz6kDulTz-hRdCcDPBVx_mPLPrPC5uYJJPQH1JF25pcmPpdTsO-HFusNJpuBXsO1j4wy5RVVGc-Ci0ujCyDimUqfvJF1ISn4LeQB4umpV1aWg9k0qgHvV9ajxUBBqWMHYygXYRSer1vqKKhiUFrjXI6NNJH2oHHbb504exE9s3aWvGuywo3QLsEU1Iwho9_GLTJK7GbYDot7iZFS7fQGvtQtonqjeJyGCZQSjTGS-YXg9mf7WhYg1yXD_tAuxWkP9MGvdEUzD2aEBPCBcyjOQfLAKphbzTNUhKUDD6Gq3aB1dOBVQVNe-lH8CsQ2qgg9wjL-bmRXZt9LAw3_x0KrprKn8yRbgr06gyRd5__e5hNR2waAvIyKiRe32e__nC9_ZpptOqxfvEofOxW7n0B5NF6vWK04JKssItxeM5m4sWQMed60ytmS0UeVzBslOamOkAn7-hzEh6Hxgi_0QFNbk6vjmx7SYbV1ttJRV8OiQVvQHoxpSoNZVIZjHRbl0VoOvwsam-VGoNvfsT0Sl1jjKVX9hiEDCXrskIaEl4Stk2qwkr122YfTXiNhypjsAvyxklLfT5enkwzz7Dkz2VfQiTmd31u3kgNPoKJ7f2vI_gpIpqMiaYTdBMBsUu_UXVSawP0zRkvjhcB9Vm3lCTbN8sru2uJ-N73bYIP7bYhcoGjUMmyXyZPdDU1vpcjrAqSsYm_89ry3VnEsjgVB2BuFmT44hRpWLydTQHU4byi57cz8eZXAdWhUz14850BQgTFe0oaWWfFgnYCa3_1Va1-lVFfhK3A3lGa5GOYA_zbprO-AgaoQqelTYxR_LjhfoZbImO1wuUdfDsU494qBCYM0UlI39nP1jP9Ln-3ZXm-VyPv8Q3yCsW_Uq7HL_-BY5yy8LVcoKeqB6PBGQvCr9KZA_IbCyaYsG9AS-EFCZI1cfvNeNN71zzwf07-eyuW9pVc3v0piBOZme2O2d3C2M4Y7styruauGZ3E4LePpphQ4TXFqtJMCTxzRgT63eBRSrBjWHeBayCbr2wRWobJak-lCtKtBaPnHHlsJw0EX7STF2-4FU-z8lOF1JQ8XqgITjO89DHFLKP-dUax-KSYN-XsDmXBpLWd9JR9lpaZrq9CA9DDTdIAJTIx1xt5YTIVCW3RBKnVk3q96mK0zbsOCcNXRk-bhbrCX-6Uxc_ESsaiBhwaqlhhK-Vvs15BgDmiRI8QYO71wtOI1CObu4vi-A5fOakP8QiiIFkpE8Kgis9OqrpNyjve9ORRIUHIlPYQF-lHecNhz3ywNGW-CLxjh_KFAc2jMJ6pmXmJEGMWDY7FPwiT1RnoFUQ_JsNp8L8gnXiTiOHoVpSFywWmcISwhdyVI4Mjwdgp2yKafZz8kktEgKSbD5IGKcG8XmGLadzMjXgij1lD5Lul9MS2lmjMTC8Qm3G-Il0Xc6zzDwDc5PVWHVbNmIIKLHS_YvNFyKgPhbsnqohvQDg3sHjZ6z6-QZVoI9_d4g5t1vHTR-RySAf37q274TGbOcc3Iatr6kh_eJlfDvd4v6uFRyLUCkm-R5DAfDuogriG8hITXHG1b52Tm751xD0lVajn2GDmUKG-tJhoMnBvakRRpJ4rxFiGuPZltpSuX57UCr0Y3dlcJtae2QLAyRnW1B_HEw8AhasR7json3mkkYfqTSiegWzbkQXXaGBkDDx_kBiu6DhVmfG9iku87yVGQVya4yJdQV28d0KkCvsVbtWiY6obqzmvY4RgAIZ7GysXLZGBqbVZ2AvszgC8O3BNnvs0Gjq6nhsEa30XWa39n0yCznd9uiZ57vyAJMhu5S1VFxWBuycrPkxQOfFDcYNFB5aIjLUBMTsMs1n9ias3SdpNAWRpgvNQtDjxwgnL-GoHr980SSmlrRiV5vqC4RDiaH9T6L3TLdr2fJhO39Cekj-0QIhZTwxT4BgcyCMfA0x8PTnfjhJyS31W4BeVaecKPLf1Q6WesuUSFWO-2euCSDMSFu-IWbusia_X0UggmuQixvd9gy-Nxvk5MMxJd9nN1AglwkKp6UdmYXBpqK6cdRB1nJdyM7vIGuuqo3d_PH0d1PN1DZaPSrMk1kUAbI9c66pKevhzboAvIbCo7Q7-DeKm2XLIURixmWDESH1l6U95L_dDYT_30qBH9-E98lyOltuBCSJTQLeWS1NyeFKXUhohJNfZn1tDEKJlf8bUoT-YJ9TpsuW6YYiqLzpmAmD4lm5lPXd4I-8wHQqXNu3Nyg_SsnpCJMym9REb-x2SNz9DzRd8Dhbkzb7ApIiG1MAZ4y8CiNUZizoieAqBStXTtAlZlUVk-DmwxptL8A1Nb5KLlu4smoZVuTVcNCXk01y6F1F3VsMJohTlBmIb9Nhhl155f4TvhaNoeBAZ7bsiAra8d2EdPuDnvLsAUgySmu7uB7Ffw3_4h0F4Xzym1XmNrV1lU4v8KfAmSQLB4FfCh017G3yrhH4a15d5uBUFplvahAZQ_iRvRZm0cnz8Ag2H2SP7Xw1u7uRhH11BQNtYcraFg_uM3Wxi3HiZmjRLrtGVa5Q8PP4avFaRT5BWhGUljK_mMKbZgH9QppmNmAFjCihlPHfQkkEfwC1GTJpmCXEhLAMeO5kOv1ZCOnolZPNlxNUJyQ8dYradf3MbpCpClwF_Ob1gfUFlyChdsAk= \ No newline at end of file diff --git a/backups/backup_v2docker_20250615_232911_encrypted.sql.gz.enc b/backups/backup_v2docker_20250615_232911_encrypted.sql.gz.enc new file mode 100644 index 0000000..d774c98 --- /dev/null +++ b/backups/backup_v2docker_20250615_232911_encrypted.sql.gz.enc @@ -0,0 +1 @@ +gAAAAABoTzsnXiH4bAFbjlzWuE5kBjuMiRQLJH21xiDQnaL5_A69b93-40ZnUZNk6EyMBWXUvqB5Smzn3EJPxF4Xs3Er6_z6-VrzXu3QeqC71--P0mQRu-9iPcoJPBcShFyjUG_R1qEJ93Fm7B1i-8V3qvX7fY-mDatIB6odhr1qdD9uMIzJ0Um1CdJTeA-rkjU1S2VeAkDBEp2tv2xk1JAN1rEDl4zh9djE5tfx61VqT9iMQO8ng3K2zrLz2KFl2TMrRkh0KCUiPLxhulpldB8y__KAGP9ASApdWJO2BHnUXgSBOGIL9KwYGZVb8x_0h8a0lpp_dXVMP_876qJ1wYzkUkceU9_iDY8EbmcwYwOf1YSkZjdoDsKm75gIfQe5pd4-k2TI7RG70wO-laE4d4fyf1U_jW1dGoezWdTYfI98LDyecOZcmCjmDlTRMYQcpruu6E3kYM4on-G2_qXKeLnDpdPGy1Prj1hEM4BmE2K_N8GWRJpN7qFbnfNhSRVQJkVFz-3cyU63B9f5P05GrTbcu9CtVEHO71puB-SHKmhwxMf_Api1m_K_k4N245V4glylJNCHoq6jus-R_87tlfhHIgoRMOZzHttnIFh46wYX3muLHuitEOWJZ4v3mUNrz_dFXsUyt0JsOn_qWZ7T1Z0ntyg6D4Kf-Fwau3jcA314CBad8SfjNykWQm-wOeJ2Sbhl5iUc5hBUc3spfB4sbfKcUmKSNfIcO8J3acz6cb9H-v_NYQDWNec1Vp1C5nDHRCr2uhAxbwY3EYQOFyEHrPfAYZZvXVF6laFSiBGrfZq2hRVX7vDptLe2xPUNJh333m-gGdbDIfYlXrl1DLVeS46tazukMro-A5wM8L9Jz8FkgwEL3_xLsU95ygzOOx_yG84lQQNs5_FkjnUKIZUhb3owBxy335CLAE0n1cUNQDCKHrSWPpy2gXxD_6GLea63zUMG1JgpH7-VrexoyWHq6t7TXBx7ZC63ZfYBxs30F2TEA6VURUcd0TBhppNkUEgdLv5Cy-3rrXya_72VJgkopa9wcFnPgRSNVF_uiFOG1xd-dBCBOEmhch1TwFM644-DR2GmlTvlB_NKJgwAFMUJXt9D0OGcwvgTQIHHYcxOwoAgdQ_n7ZDF-aJ5UkA7bQJTR6sZ_caLHZiMetZuOuxraXtCGdG0uxZ-ja2bQHP9KtDffQtet4GLrHtqb9tt_DEyZbf9aJxIYGeAqzjV-7BJU9LK3oWFmeMr8SSs8XPrmJWiDuOpnHd-gn63GBKD6jXuZ3KiZ4bpEgJNeM6iLe-oZD6_d5KSZNfLqgqIH1vJd7N16uqhFS0B8w49BQRNvtV5VeHcHd_TpAezmboqBffs9dsI6bcqVlFCF1ZkclIYt-R2j-uL1ZrsH5W-9o2MAb8wCk92nGJXQSL0a27jYR3nrPVUwchByTjnag224PO9g9Lomga-CL7YOmw9zp9C2FHeiRlfhTezWNJWZlpzRl_P6owYu60SBg4BkPj2CSPmoqZ3mRWMY8nxXTcI2U1yqRNmQQxLvQw16Wta8ODpmFpKhPR3Zkg5u8XVnN2NX2fMrJn26kU3Dkd5JXibYeXSDNmtShh_vVCkeSkVl1IAbTvIpUO4oJLQPW33SnBvdhJ8mRofve6_VlXAtFn2l6dXK4H0hzojVeyJocwjTQCmsQkR1uoy0xuhY_f3IuMpaTeYAKPC9lTmUuX3nbSG7nu45lwZpxNde0kt4ocsusOHdOmTjf7zNb2xOHoQRW8ikNOvbKefTmk7m0B9k4kwJ43SIeo5JFCrJ9Nrgab_w5HULus8-8_MYGN1eOookqCVoBQYBClJkXClD1QCGueY45zCdt7yTyG-hptowudTRxg9Tapx3mxU6gq-_56SmysmF_3J9amfZ4fo23rCHC4V3XojWBHF5U71r0obciDLv2XPn23r1N2C5GG5RxZSEJQbyttdo49RHK-vygeTagNdPXix9mX9sDbfFgl8smGD0xpUUgf4fCY5WivBP4pfeBLwApxdLhbQP1RQ8YAf6HdeTRzKnGlr6HcAZcKwakFcHMT4mix7Pam_WS60W6p1hhmFQpb7yuzKcjrFzkLEr0owKlusA0jAVW9L8iDMU8MIHiqCUYPS1hNL6YpbRFpVh-za7n0PXwAno1GTOKBQD7Vg5S9ZeT3BqFxjxuGWifnWaFkvZjVKznsiPFI6n0BdzJtELlI1b9mDs6kIsSsiasPfBtOtxYaIc0QZjLH9LtXQs6_kePR31MysfcjqA6cYrinJ1YmGqTYz74pgK7-dRoxN4loEpUoK4weL92j1WiCJrB2udc2bIz2_qlHA_qNSDfZT2227DnzLpES5vx6yovpn_nBNJ45DFiFr9oQ_2yDYFpzvRCLJPEt716RdsRRDUSvXTFIG3fT90GT89GYWZthmTi9i6I1FcpFJUPpUPGdULwGUyeXJqSNUqRf4WZAZMA7bjuEGStPOTYD81JkLpLoiyxmeLbbPLNb3EDczsUSiwBj5mYhak6qg0KxticT2TolL5SxYQxcJ32ihWg-x7b-zAHmjWYs-NfzsOHzuuETVot98TnCsIWwK6UvfD2Z0oqFlEnlAKv-fDFBshV0tmvZmiOf6GsP0o09XghHKV_es09SBYBzKeWC2mzanfL1H0UmV5TUlvNYGtJEmVUJsH_uYwyuO_x0Xa8yA4QFEMA6ZBxrewL3N0lKnhctMkE_igOoUsD1O4xt4eDATql0Y5Oa5djNl6-rP6lgEy7VIEgouH8XVwmMDC1iMCEMh6syBzgCnqJraChAROUc294rciidWWkj0kyPGpClgufM2kcF1h1qt12v4uQsperQbczLipsDRY40qBhtsx4022oSuUR14NKNFVY6agrV2ygZOdj8NyHMU5BjkNHHXzR3OWHtMQRlj0Lx8TnSs7CNbWDZM2U4uEyIeNpPnr0SHbSAsEQC6c02M8TIRFoGmuYC5biW1aDgL7biSCA_fzG74oTQ_bnkQ-MhKQrKPK1HPuGRunTPcqOfGC77FHwJ-FHhSKjDdC3jGsLN-_yqQSbjPmFIBwXq7CHfL340qYlnXJWkOc2jK46Qevkez4GnZfSkRlBUlyVlV-Da_nlnHIikEz6ZT3KtmBMn6oi8sYl_cXWmUk3dMRZ1EXqurL-TcEtRjKOmbOs9rjA11AIBx7OUUJVgeCKTxEKrDA_k6syz2RdxyZgWFhH8prEeZ_S3AU7Ya9A0G0urzEHDnjlbNUJEuhpleakH6iK3KnWY9TG9DW4SCGebvPg6l_AkGsCxQ498Igs6WeCWPNeCvZJnwzDc9lbnZ9K-IdHOH-vZM1MYAhSGo1H6wAS2_5auLIa_f-BXlZQej3Z9jUowthKY8TLARqzWWN49cVpurdWvzt9DYtAFb_6iLCi3uFA-NdxYu8xclRPzbiH79ZEwGsBmtnlXq3vyor4vEOOYrewtx9q0oNgNtPLrz073N8pnJnLYRi0ugi3ql6UzH7PRyuTgROG6q75ihnyCPUQMvu3kbiI0e5-piza4oIxaLsVSz5luTO9T6KkXjMvgdLX2TU_zPAZ0EKKmSdETLpgufWXAIt-Kslq_Gko22S2NVKhEwmWIE6CITjXuQIbh3O_FIOn25pT9A8hKAnOurm0jIZQkMDMvQyun-o761TmbCzz-Npm7r1a0SH7h_lKd3BUnHDhd2AAUzp61hV7_yuNkxrm0w0I-UZKm_N1L3wV3XOEh4u1AvIdmCTiPGvP1nd_UcftnqlV1AA9qKwN2sWhyhDtkqeBrnENifr8YWXZD9O3LPxy9wUnemoGSldWsSpP44NhlstDI1OSxdC6OsurTDXiep3Z9rAtBLjszvIQSyVoOhRDx0qp-dHSp3L2Vz1Abb2YSy5vdp0rc1xo5Gn0q3Rucu1eFMdhszAvNwQaLtjH6ns0urUfhppupeVZxPVBbDhrrBHTsPe4ogL0WBp4UEeSAXGUcZZEfAh7E8mLpeLe0umLBNbYybokU-w5Y03z1NFZc7AXv5D50XJlvf42uauafWYbUeidFGQYRRbY6u9JXu8oa36P2b4MERNfLJdBPr-jrHFYjjQ7CsQZivDx6tAnHuB2OUreXa1w10O16ksp0L8M_hYJq92ZdYrmhpHbXbz8lCQIi_4vg0JuzGZLuhmvT72pHMDmzg5DofDm1pqhJb8JbRS9PxRjhMMohbYdfssWFEY757zQ9wiIcC3hX5Ec5zUVaFLJsGsxZLEicoLo47p-V3yg6Zq_WyVIwm_grwRk5IxBctsHT50-yQD0j7K7KSkzQm6LrMN69WNfeDSiKT7ZAHz1KMol34UzGFbJ01urNd0aL9RMyCVLipAO3q2ArzmBxCjt-fB3zBarKo2RY6u0ye_WSxS8RyFtvJJcyGXLAWxDtq3QmZpeWCnYGgIznO7erKIoyjcs-n1gHr7h7LaY0HBASZ3qKSQZ-j__sxrZYhHuFuyWfQ3wIHDUpRaOXObp0bEXdX-MaESPkAcMIFluR9idg-PmDESuAQrZfjyrMJleYGUn-QFBhUhASH6Tdav2gFjG10RT-roYuE5t2Fvfebx53AgxEIL4KaA_QzDRwkjhLl-I-hT6Oz53EuYbvtlov0-U73cQRIC4zWsdaHFvTzvnZw78AmtQEsvwXdfyclqD1YVTZPs0ceL5VC1zfCT7YsWuMER3l-lZvWnrJF5ac8AiXXzMh4TY_zogskL4bK-x7ac42hTATOBujM-V8P07t45k509iJt9j0Er7q1-tn9_Zlesq91biumVKiUkR84r2a7jyuFfuGRWKWNdLT6-pQ5Drs1jTOBnPUCWpwvASyQwUndaZGk8qSdBNDrKI6K6tf7J5y9uP1k5qSZNhwrE4ocNwqonrEGFHreuFAjBEsmhaf4_mhVdrMWLhYFZj10SdOrCDhnAbGExKe8p1NZIimTGPNrbBlpz1eT0nVGFAkxbnd7G6VNY9rFmU8zVglWcIw5EuQqmi3fV9CFqfGp1swQjhaf9hLTdUXa4rJ7OUwNxfXaalclVg5_5nyX2WIK_LMlM5KvPf34IZjcwmkqsPQzZ6mQCN7YDrLRTVCc7D4lmxv64SPdGBgVlpLbbtNFbwx_lrISn6BMLTLhGeC5VOx9eaQE_d7SEFNaVAl2V1-cJL8mdY3nRfm6lahVD8zOS_UVAL64bmmJlHi5Uoc0_crg9i2EK85JM4_6KG1pntxVi68HurKHnOckUpH6qYqrk2yBsXA6XVSr4OOskUijeqELn1Qd1jOoO_-lPC5qlCD8zRGnov8ZciB8Ve-YO6XoCdNiS8OOBQydYpf4AjrOrYV_uSJmOKOoVdu4ynZpmEWCtlif0i44Pg0tmNrdFkcTaYlYTlpBqrcLOLLR3B6vLOrFcesQ8qxqgpIk3XuvBFHQtSdUgAJKQS0K38irWDk0NYvMnMmFWUNrQ6Z2tiI04evfrjbTrGJxpyN5Id0zL-b4GClwzaMCP-6W16tkataXFjWh27qgyqXMga1J-kepdjI8Bit9_l4WdyDfdVqk23Dx5qVueFyhv1g-_033kKCzauKUyLmuC0i4IWcQfd-iFE8L0Q5X_dfByKmkDCqcbHoW6GOLYd7wO44HNWCBFTD3bITT00SontpAKup_-x_4XWxWutugWLG4K_p74LLn_4ZwNBBPYKieEwap4WBpHsrIL-_n4QsiI_gcL3ku8_8bqeWv6i2E3vprm_k5GCxVMQnmLrPrXAqqtKlhg3_lRudnwVUAHiXtJol93mfJwt2epRjydNya4iI1pE4D8nQ5EES_Tw9qKzLQ7UCJqrPdKDiIweWLCNR8lTLCrAFhM6d7rKk4lsHrtM-QXBAo460nmYcXYrjCjzRF_P1QbLCzjHrz8kZbMzy6dYlJZJ8MHFAJSMntiOhP4t8Vg2jl_s84lcVo7I6h2IZC97NO_FokNL0Y7EutEVBJ2ALuSNB8MoU3eRQ2VzZw0iN1tiRvB5MABqoyD6lt_tSMMbghv9BXBs2c4f3YK0k0iexBQkvsbuz2CXQTRfpmkezfarVsFZSTFx9uOOcWaGEsxXOiwidfQy5VOykKoqcZA5_HzUyofjCeabLsKTl6GT5yrVbFnIhkCw-WvVU8uBkGWlhxSEZ-XxbJnlo7KIygTHStiBEQrkqyIQRIiYhspvqJ-TC-pMMpppPSP6LvZq7VVqTd01K4xwcebsBwNWsXGfvixfDZ2a56m7Hy50rjCrGzL-p4swE_GT4eTz_NgZhQcA4iA1UDuF-F6-ZlUdEZYsAkbhD2PIIJQl3gPmiZWNgomF9UbztdajvBixkFOUef4mrGitUw9mpBG5GCq-Z-mKMsCzBNIwNqiMN8GWqcdz7MhH8H12_N0l7c2RGpYDLYkM6ZdIkQn2xb2aIwK5_38AYb-3qJZkBQCE3ElzvlTjgy8yRaBR_LOVGBZpE-SrSkzdjcLMmDkC_lK-34Zln5pTZI41M4jRFpIJmQfc5BpGt8DTNZoysnRMsTP_Jlnem-zO67LJduntofcxrFEz5dQ-7I7PI79bssd-GYJ0UEYfuZFezPdauqpM5rcASzoHaIfsZsMeaO6Pjg397aRnI_bAyyZpjGx5l0UhoGNWIHgJYw7hLv7mjK56cX1qPAoTKC5j4OzlLNVAdblrRWWPlHCxHSyAOrzx-59i2STBQ7H0Qr4n6RlkRpAoc7bUgtLTniSfTP_s6YDEnVwXw41AGk4hQ0lPGVQB-E6UTHoa-N9UE-yAuD5M0THOvXlPIhkQ9wIDuI-bQpXeQ3327MW6bw0p-n3LGb6bDsGMZi84GiJsz4ISuuUem2g0OdYbXvtiEh9hL6PbLm_360_mBOvcTzXSj_W8PdNA_P33dXz4aZRpQfKK5_gRLcW-YjXgCWWcPFIAfGKuzJA0pDJaqs_jqOkmT_RVeRTaFfiDwPL-VFEvzCyNzlbOROTNSAa4L_1H174a2ILMCOdD6XVcLiM2VjPYvNOGYV-H2d9xbtlptmU7X4ywWLiNxc56qKtFEVlxQ4Ix3pFreqSjdOIV0xbK6odNgZ_BrNQiGHlAjgTA4twQPq-xmZcNVDHeu7mkdRPfIDxzkH5gwLcJfP18QoPMyterBc4zyGyoLzEvYUMLaOsmbVS1dcPlB3ydShugd1S5Dzo30I8rrNSZLDcifMoMAT6C0dnUYXhdCOtun3pKawCDACkoLfulIX7tJlZIoZ4aU4kKZuyG5_azgXvCUAL2vTSCPPTUyyJ0lf5GX-M4VcXxYqkekhXY_FwbLfx9mfJaMdqmNmP4n8cw1TlAvtaWt-QPeglkekxcurJa7OKKYIgFppy_a9mT1Q5zjSG9PPBdsnG6w4nxQrWGCr7u-u1oJg2vbLMiE9GN9eIlr7lZNjoOCTe00zGAc56VztKmyNqw1oiRMNAGK56gYwvMWC9Vdl_PUtItVOUFxCYo8-tM41vmNebbIBvpNeDav-mLYhYyniA900MhfQzx7n2UjBXykYxvV9If7QJ6qTaaHzmsnrbapaJ8_ygyO7MT1NAW-T8udIJe7pEkh8GPTpuSIQuRY0oUefb9jFOzeaI7nVjB9ZP0JSk2mny3VKnLUotM_SrBRQZ4CAaVjSEwvI2Ex6nYkjdlXqkt77ZI8B7M1xr_gQ3ZFHA7eTyUaJDEl8w9kzdLzyCd6-qfZE7nOO4FjnMeRQq0qryiq-INw9dj2wTfZuKQims_CP7CPZckXLGuTDVLZx5MwkWMtn9-SIVQ3l-eYaMftTS3x1w3bOC6_DinQE6TZBJvItOIcAoppfdi1d0khJVynMBVlC8vauKtaHHN8C8bwEBoo9Iqw98zYlTgTw1tW8rFJd95U4YVIjN7i2fXXkXJxA4eDXm_z1ZufArX-ukLQOUfj4TdMYGa8nT1aZKmsammgTt6gQVjm6w_PdC1GNDTB4nbi5A4Cyzu9LaZXobkZGVYkLp4bR9r9Fqw-Ax9vURVCcZz9aoCo8iccrki_pBGB60DBJhnAptas7vqpbZVB6ne8Zd1UzdRS1qWsx83fIZAhgWaFXECJ3J3eUF1HhtfIkW2MUPnL5ZCadPJu_hHDzkhqOQeLYjaUQfbyN0ZCS8cgQskG4QZyB77gJWRG1-ahSVoxWGLhU3aPB0Y13ruBdUFt0Pp5T1H2lDSTE9mNl0uDuMsei4doWRKkT8N34DIVZS3Q9oetCKMDG1eAMj5DYlqyxuPTdumabs5QQYcOj_rBB7qZerSjSCN88b19pv-m9OyUEdZVY3Kih6hBDVrEsdXinNg0PIb4Sgh8gtr7kxOQVNNseKdNkVrlE3ptl_CFlHTfM-xzqm5C3iRII2UmcE8DYEyfYCffwThQjYVEAveSPUIRuB4bbjkViksGcUcvhxFVdvxBu_xZpIFyA138Imt1N0LZZi6_w0elY5V9ilrCm0MZA-VRKMm79_APjz-WVkw2YmsG1wHkJ8ohiHMixrCTye98S-pgHN-AyI_y-X_lLr5cgSNhs3Q9jVzhU0j3NoVsYcb3GdAL98IhGYsjjPHxUev8OY9aUfL5OaQfMTMxAi8KfPnKYgvacPmpKEJYjh77wGzqM57wh3GVkoUuSLBQbVBmF1fH7IkeNojCFvtKBP5-WPEIltKZzfX4XIELIsSvakRRQgzVKnp6AhE-lTAXm5pDSIaOUcKt_i0AnZQMtLsZhEzuECvrX-SoPR_hEOCp1coSRyUPIMK4B87SpR_t3O6pfS-_td7W6gZMHArGzNPssWO0f3lMmpiWxLOjAU3Xz9cm8G7cIJQrNrJv7DSxuYqCbTNa4d2LWCtuBJzFUh1zy_jIAhsSobIaB2z-UUz1InPy2lchnSjm91EWk0silZO93ImVsnlVoB-0Uyn9pcmha0Y-cxT4KnXp5xJXFEc7S3dYaKG_wrn4EESffe6XO-YAIK4ShoNuvPvahO2BfSZ4nKJ-kpZpIfLXF-esZ11WxzUkhM92VzZ95uEfiOrvQKdWQEuY7q0wR-jiyX6d_G0o8Jbq0lvuIDRda57VLI67e0zWs_GggaxVktcjLuRMyjJuuZ9Ev6nBaXt6NZxYHvtOanR5J8U9HpsW2uqWRP_wCr3wFWFJ_sIszws_kxQnU5kOs3ngKbXpEFFUyPypeYYXBfcoloFn754w0N3g1oaypd9OpnHoIJSw2ED39A7pAoMR0sjRivtOCAgYbJfPNA3NkKkKSLfOBHPjdsUD8AJSjCaAG8gpYkAUCtkvWHAPkV_W5WUgCgQbyLDEKEFzmo07VOvEH7-aeiLLeWXfDn97Rz4v_LkuZDD9-_3zea2X0al_T9iUMOPETdGBGG7-0nhyBw2CYfWwFOhIj1GNMycnT7mWXSZkcKScZjndiuL20nY6af1EH0Mqx5ThUFOkI0N_mpCFf1pCY3opEyLzCpPZESGQvj-6VY0Oa6iuG2frotUo-nVJ1GL3n0q3Y6DSUWCRRyq7GKyevG437qwizQzjg9cVEepXJfwALljlcVLySD6oxQUCD9vIjsx7fS7yHkBmiaExsAg0LIjI9vz56n1eCqPxRL5u5FWj_npG7KUlybdG0eDgCvt96F1k82HBq86gOmq_b5r4inALbR4-jAB0ppNvoiYuoDlEEWIR6G5A5wkEsHtb6dX3QlnAVzdRvggr1J3FhaZCNC9NVGxzZVx4p-LAqZo28IN8DFB9iIoZDfgigrIowajFQ-3HjZUU65QKLRsrTD9y7anVIQ-5WpBmgyu4k0J1K_bCWXkyPHA4hKngpczc6HNc9e2iZp9vIWJcNLKHV-nbSz1YwMWj771xSrXXOqACiD5B9ChvVp_t4nc4YfITcL35aJjyoArXtScY5e7sc_mCL4DK9zxZHV0STfPmVnSEJTvjalnrm2S9ggbHnvh_WR4hU6DlpGY83bkXaO2p5aTg1FSpRG9vg-uIKBWHvuyyYJahDSDLYumIM2EFjf1pasAfMuHSRmD0GvgUCNGTb4EzZFegFhHChLgzLHgRVCLO4NQePPI1VJGqo8xfcAYiUSL-RnUspc7QEqjAbrR814tri701Ndoxa0Oc5CvdjViEU6CYlK77T2oaQOMXogGAxVf4DfdS_QdZus3oq6DoQ1Vdru8qDjeKz-lu6_fBc1JxX4-n1B8TABmqrSOadNlR1LofcrXgCtX_Yjja-QqUaUQ4IcwPEyCAttm57paZDcdO4jq_yD6ctuZgOQxT0v_-ZJBNuh_UKDhCbqL0THcHniSE40rhEvZE0k2P3jkgEGakdagJem2cIgivBGAjnG6RIjUQlWzSc5zGZyAPkzwQmd7NpyQ4ef1XwdW31w2L7gQEsELPv4BDvPG3NjRL630bQtsE1mmEB5FwKNHN6xElj8EXLEo2ogUbvaXjvqGHzogahddxgFpL2P65WZLtluE9JSGV_zvGpOwaWVQP3jkzetW-3AZ4viQMQxNqSribjME2EpbkrsIHY1Q7SLXR1TBPqMXjxB4dnr4FtSwZNGEHu3NL2kiVmP_wxpOquSef-EbbRlU9fnuYbMl8bDQD1v9Vt7_25xxW8qT-QusN-GuAKrr9-fGz2l3eAzXosrbXrJg7wiRBXf5b83X9i1hlFK5Owv1x5FqQljYEgrxCzOpgm30beUGWuGn32oQbh4NLH53hncmk2ciJaskO3ltLZfoIbZR1Ec-vzC30SZJJr-VAyCXaGg_1Snx3_Ne7wMvpYpHxrR9FY83u5UUHhUaDXblhNkNzumD2SwJexecOCnNs9tfPp9Z2l27o-5HMIkDwCzVePwCwS11PFL0jwxWx-1sPcGqgXseIcEcdc5qJT6TJ0Hf6cLm2Z7NUPvn4nwltgjGO2JB3CIYwuTauaV21gg5H0muL8pdv6AARGhnWuvZQuL0oHc4FNWWhC18pmlH9z9WYRxxN2se7_RSFLYTYRQAtzRDB7GcsVDn5_QOBrvLx1D8UbXNfMXX8-F8o1pUGmMATOwcJx0YjbYFMSy8WwdOxvMtHV4FnqSd7Mtxnyie-nq4wjucYOUOGlGcXgTdaEBMgvfNxizNVBzsDkWqGBfOQg4rxdOTriEiFZQxx294zjhjYqs4XANrzH0JlBq2LkVwFMNDDV2y2u8o4PzFLJxuW6HpMYLPfM5rASuMT3jka32p4r58dwEM729Zep_WNhjJAVdlhJulczCEfWt4fin6noLisZjJSJnNhZjRdn_ILQ2e8gkpwPz4T3MHkI5p5xhdfIiIjiyP3YhzwU2WrtSdI2PvgGk5c63XnOcrv1O0njpNjw8YN5eKW_4Vfh4lcU4AFOMzY8ZM8mCpW56M1uKTcNmKkCplMlYTncM2aOg5DQBXLWyFbgd-ed8SLYmOzIzx6EhcUzX_rVffIqpkIxOqcJggWXSV--C61wM8En0X2NwNpzQxJDGr1cxLiVYAyKndxxS44aqfpqQAJ_-zSbaWA7OPmRoIw8qHewoxdsjHXH6brR_U3j3mYAwNqQecf37LL0xzNMNh9gp0EGzDAG6-sxfispoF3bNXhwL_TGWjNVuhqg49gkVXmWoaopLhUdRLtIY-FZkirms4-3xhXOCyYPxiTBgsdSwLiw7zMznSqAsP54mjZoDKhegW3Vo6uDoApbwHUvJjnuBAUohcDs8Fa5UmToyzXKoGL_0oDwsFQSg0Y6V958A19HBe1eEcilkndR2lbyI4KpW9PGlRP-i34BgDIM4Jzn1tOaVhhFZYBnmbxqsKl5sP3khSQP42hGca08Zuk8jOdWpOF8otR8ciOXZMe7y8NwlMl9IWpwVZj0YQeaR_2r99hwyOwkXUIlvovV6oZjomH7XnkLIdJHpAwA29tvrbLM3Gm0YyAZgcUgvl8A6gZVWmFRDo9RyGqIEjpEwWhVVqzb4XWq9Xo8rtsTRmPIrDfxTZ2n-ZwKGBJaO6nuJa4XU7YYqwRgd6QwKMvhNvxiy65AFb8PsKfaKnMtc30Vc1rQEa4tLUft6ubfl3uhyiu1kFn1UXscQSoGAgG0aicJerUWM7UTnr_Yqh82uyeCYtnXGD4k2hRLdXa7K3cq_rg7oNG5Zv4dk9bPhzgUe9WqVk39mMrNX31IKShmlvCeMfLiEmKBfO997cqIQzXqwyu3Iia35semlHF6FM2PFxs5paZO5fVXom06gdPwbuyMqODdIVDZYNiC8IBSbiylDbDIsomMRnJqpA5wGlv0uViKbB5h7zsc0KafkC2fDrltxq8jFTFkmHWRlyi47Z8MkgY7i1y4eWukcegL54jT4rFpASGhFMn-P-8ZFdc_G3Jn2PzYB1_MhpGTXpzWhDjQXB1OTgYxJVaqvw52L1IUbdliGPlHadB2JPAcdof6D5y6MtKanr9QfsR2lifpItF4e0zQ4tnLlu5CD0pc_ANrDWU7mCA4xuUBhwAZ-o0K65Nj_UsHO4CBYC6pWAb9Zn27R5-l8hIPJNkY-yJZI4pDj9rkZaVOWoZT3YQ6VyeR4wZmTciHhHj4sZfflpMx9lZdI6ZzAlczV1uTZ3q1NqKpqyc0hk-_fprx7Leu7yXiVw2etgh1UAlkU8h4pIC8mLbTrEwyo835m5drUkOKIcArkRq-aKeOs4MAfxHwmqx5Q1AaJw6J2hnI3_12_7oeqJ45UedNUujKIKZHv-F1NoGh6cWGQHgEgxCHeRS1ND5scE9Z6EO43Jn5-VS5bnmgwGftQNiVxL2OOirXycQlcw8CSK2gznUzeFsIw8jKKhkEEgSgMVRZeZAkHLRGjGlipUSFjaNF1gDCW8klNk9jW1DHlygMfhUI0NxDJVt_swBE4k5ocpssGzgLVxDr-HDk6NcOB-fvBN9yLTHiCtGJ8eOe9A0Oc2jdkHDq0Zr-XEx3W3JwPXc1OeXIca50ex1xEPHCB3UK2WbwJp6Ml-kXz74Rkj5sEpmFmJCNNvgBP8sUJhI2LOmvwOddlqbU6VygmA7CKvzbSbO7c3I3qqtGKTTBV4ZImoH9uuhc_CuvisC8_743FMbrQMbuLTzWdZO6YuWG7WkmKvywl9hL0xSZWtA1u10hHGSU3sz6920-m5ditgBJEuwrNboBYHTlmM5rjl8RmItcHgCMiF3msn6TNQ8SJdzno1yz2zVT9UhfoKoVBwH6s0Iu_ZTCwswVdn_vitNwd6lDJGr6tQDYlv9ISVG4KcJeJBHgAZYJqM9eWFY1y91AB60NX6tKprs3LB7DSQJEWC8wnGIKwnt52p3NCkYmFXadwbitAbhdQxX8Stsub-UaHA5gQntDB_AzeV357pi7ujyDC3CLBXBsarJ4Xd2bAdgglk2R3McfOM5srWTXyf450vRcuhsjgEKOps6hTmCafc3tDZiJVXwikgFbdtdhMCu36QipbvopwXNc4OfTKLG6UX98VHXoWRwG5Umj7mNU-9g7aoAmCN1PkifFesY9S3JQ6-Xk_sTh97PjpkZNLURfw8GgG3hywXc5JW4yyNQtNdy8GcEXd7u-ohjYXiy2uJOqV8Y-_0zBowl5lt6YeIIXnXc8MeZyYLtQkBGrGjAve6B-TZiv8XWMPUYliLxL3qazRjpi_mrtusSQL9WCR0QqrLxGOmBySt8fho1o2auMdPjNQujQYdhRk_OqHcldQv-ljz_TOovfwABlQjlAdC1ZDwdyU5jKP2EOn-T8VEIBq1s6RU99FRUT2LQK0zKpaEBbphM5nnZoy7GAwuGBVTElRC0aQNs4wKkJk7K2Bxb-Q5R-yHzpS6l1JKqWb3HHs3a-zTYoF-oKUe6CRhLiAdi7TZeqfnHv1eW4Cy_-8syYXB2VDa7-VwAyn04pLMyA_r7RznDr5yd71-YzuS-8l7wqGlDa0m83K3WDGcz9nF74J9-39jNDostK9JJGJdHsxoIDlQGZdEF3pMCwu5VF-eyz5aaQ4N0KKZ0u4v8_2E1LdmYTC7Zh5EiKB0_zIMcpWuBqeIaromIZPrg_nZHhKuB4Kaavi0TYdldM53dh67MSI0f-CYLTzJ4WL2JUWHkNRWD4D7w5UNbGG2qzCa6EuR_fjJUEacvv2Ad9ooDoBQcVAXGZiCopoKgdrxgK5E0IfewMrgfmwEkz4ewl6GW58Q86JHo8eLsqX_VrIUtPYOryyiiz_60rjdPti5nEKppL1l5H9s8iF5ncyhqHMjS6ZHBEVUx1SOIEooMUJHSAexKHF4Lfv-Y5AhjvagrLFdlE7RQlNZX54BPzxZqPGLGFWP17W2CF1mQOwkmacUwZQNQSjrF8PZuiTRQQY-BVW_nHHwe8dszDexcNZe_tbmz_rdwn3B-TLRQTbTuG1RyOb8sqi_xbLOfS0ArNfTz54wc4NgiWxVRDpzlfV4Y_AkD0NvuMQcf35bp4LwPRFTTVIXKglIwe-ebVSaydhOhcCWIUrOYQOtVmx_-UxTsygI4ylKEFo6GptKuSDodSoN9LceCQuJVQrGLsrPw2X5aDUnP68p-v28MNKfrvWbLn4_6wExmy8ZfkEjwR2ThPjFezMrLjTUaS11ThUBCo3Bc4mkSm6ROIlsIcdZEDHslggFOEm3zVZ9I1dWOJKyStnDxvsHmGSOg7iklg-3bGeZF3Q0kdmSBjsubAgDQ2doqyq3sTQLXQgTpS4TiS26BPCc6ZOqo4NVxNAaDXG4ZHSlhOUarL3zDMHTEDVoDoAw8UmUrfQtyPa1HfVnH_6Xa9gk3BNyj24W1G-33DQyOrZVv4IG3ndfSJGO6qJ_KIWhG_QUwQ--mGxfXqEt84SD_Qz8Amk0fMd4ZNR8GgKVsO5YmDyXSUAnkReXIk_P0N5iK9BwwUrjyIZeU98y0oPTKlQW9gL-7oyIAPMBx_20keWy2dnepTesjsxJkBhgKHVadNtN-lSqTbBiao6RxgB7A60aSJbpuUgs3m5YCmG8xocDi8XCvIDwaPRUhMbY8PugDnJeUcc78FY3cxhkvOorkeealX8lsVDzhNVp8TIL7UN5NMl2A1jVIzMDx__-kIwODzJc1TOyjfrbOhBzDK8cSUly1iI43W9DYALbMmctgG_y92ry4hfh1Tfd-d2jN3_3RTLQ8mk8FOsvsZIh2tNnkMgKeB6v7OP9QR9VjRUAx-zIBMVrMwrRnwXXlgiQOEx7u996Oxu0ksvMilUxsHVJtmpIke2Utf2BUKtoJu46pwsox2bGMpnD_xqKGS8YXFGpbOls4hbq7L7oGmjjO0SQvbKeDNq46Y7mmzgfhRsXMq5SbMKuYET-vBPFINWIs2_PEpFtHZUf0UF83cyHXxw1zIkBnTIhdwCHh7XCLPesUb9FLWo56s1n2teQA7vZheAxnjdMj4k1PfKm5JZn6e3S-MJf7-4lQGhlBa2Qnok6AZ61wnIQCAw6cOQ1brFjEoWTGIEwUlAspp2nJ6LPJ6QQQyEVvdSiUZ1lvZUlEK0_IgfVzd_cwCtFCy0hsLuMgAYlbT3CdV9OFDcxFReDUCRfopUpx31rkfoveW32a9RIz-M4pkjf6BcAY3P6eSaEbbEAL_yn7Ynt8T35C287zhEiFITctN6AQBvdaZDf3hFsokd0qS_uiaAswZLyWepGrmDtbBMF3VMn3hXFqNOz8EugspHGPxu_CcSgKxetWtWOYeAYwrYua19G5DTZ20d5s7zoMKLhjmMQ6nKny5O3W4WQAo3_TF2brzopEf9KJp6IjcG0lQ6j7mLaxHmxTwVU8tv6YQYZNfZUVVV74gz1hWshOLCvK8ZuNX1ZHtDAmxmdRJtGSbb7C5TNknVXIT__paHvebP9zHiFsj-02X4E1XNVvcmQBNNwt1biBNvjBv16NHmvmDccrODsYRnRgXVqw083obTT3r3WQ2bg2uMWiXVMgn-WAZHED75VH1KfeBTu5bj8usHhznVgSKYNQLrJGp12Y6i35gD91VYQ_4ZUBhuQUm60JgyS02l7GeSQ2lm1FbucZa-yJH9IutIoj5iZ9DQrGRcr7R_RTwrkmhfiwD_1BQtWxRv5H-ZJ0e_3YFtswqU8k_0ywG9tZ_I7V-Tbhv054Lu5YT67wRN7GwstpBfRqWZ7B7kZ0BofAjMRTa5eK3uVekYz4eSj9qJyURGf5WCKdYt3_BS_LcLEH_s1fh4XKHkMXgnC2wLvlfmRKIdb4pN1GrZDxrTDGbL31YDSPtJXdoyHK7UQVBXowkvjy-2nQL2yGDZUHc2rBz3hm_GqewhdhZmqXYfTQmpPmPaBs_xYskOjWj94Hgk4bwuSl-7IWyF2aio6MreZsCQ-sEly3PfPgmVJvltc7uBSzhR-s1dXFA0lUv2NXc22ZSDRR30r_wXCIrUUkAWQ13_xhWP5caOYad7xJ6sswxWKN75S5HlWq8Qw6-a9fA5KH1wzPYnWty3hlmULrohSl93BozWEmXgJTJBgGCrW2cf24La2Lxfrrik9JzXuuu9E5wW9RMQIJnd3S1_MlHlmcZM8yuSwnLoENjaDtnl8y1znHLoo30ST2uSiXWIAnuSfBH723S0AnNNisJ-2uDJRYnHXDGpnLmrZjvoIL9Xlfog85w7QLmQ0Wr8Pb-qA4brnWQpvQdSFcyNgbD_O6TnS6pisRv-LUbJPIAGUtjIxwoc_RTrsSssgGGEUvIg6BzqG3-EWxlAmH7XfUnGZBtrkOaW8wGJLMfGngaU-RK6n58UNmmMvkvi2vHEu79ksHdIn47k5-392lEajdI1k1QPSQ-VwEtIOL6eNGUPrV1WunOl-M9dSNaqaaMwn5m9U0dkWiJV1hOvynIQW9GKVkZAARkMov45XX_AylON7fwvN3Tlk-mN98MfCSjy_WAHZaY9ouaAZqB9yDn3tnNtQwXLEPC5i0Uew8hHxUgS_8rXB9tS7K6COedkypwn5kw9GUjuYKYV3TXZ2kekLlPmghzg1BKLdNXqqRTd9D2N4HdA9foMPV1tFGy1Y-XawW-dqRlr57UQStMb-8WVdubLvOf64kGPnwOfFj2X1ONP-8_njbrru5GXE6SsrhyLCjkyBqaUm87XxkbD6eSatmlOYPMqfpMAWh909fb2u1npafsZ6HBblZjcGfklld87NjDfhhKTsrfiJCrg1Y28MqRBl_42foLKNytsBdVwV5hFnS4RDAQtYyDGlZrahRRra4YAa-ryn-OR9l2c1IvYu4InpSQGxyUWXYQ8MmLKh1zkASmnIhctG7-Fp4Jihvu8cs3vZZkdBX0I3V15DjCG85x3gC3-ywYdBM6TlQ51MfzMUv6xV-7u9sfZ2_6oQ9AfiPYULd4-QeR9uHrzgHXBCqPjGDT74Fm3_qdC0Bdei8J_5kzZuLTsEqxhJVeI3JQttd71bm7Ig0n9btB9pIWQjO3ZdEjT8skXj39TYbElC4N4dNRO24JnseI-I28m753yLhD7D6nMvbwDUwwhnvhdIilGKKu8yYMdyMsIic69GsbAVk7ISAhfb4b8lyorMaM3rlfl_OVP_NXkc0YYQLbl1UtD2kKyv78wBFHQUWwj1hZZVOrPwL1_cK3ct1sr4d3wQJOpp-iroJDgB6Bp20bNVDyob7VB4LNcIMrRCNuHGXov7FtnzG4VNL8BziIlUCN9tNsZMWEiDK6x3eqt3ri8kaCM8K8Gk_eNM5R-3EZuW9oczS39G-EhZlbNajaOhNug_hJm3N_kvNkhbCxJ-GXNEWfOK-Nr-szt9t-1A88_JQCEnYLqCjswF3vJ8XMDNaf9_43x5v49_Gc6XnnZ-6TkkarBxx-AdB4Poi-M2uhDTBv5-okua9v2VSeAng5JAKIyqvsdESTxMwDDQ1x3RrZH-j3Bdj42_1wY-IxqarzKoO1E1c4aB8SFuexG1pmQldAfACOmBRaDraEZBvvVUC7ywog7iLgIVRrzVxDH_Si-ZxXBIprVx_QQg9aNpxGsh_Y_yBY6enoQJr9cGBHr6-wg1XkuWL5Toc7LMP1FGr7zWhdPMFpEsEWIcjmaOVlVc3Mn9NslLTB9-ahmcqKjOZOBwkTFZas-qg3xkIGc4M8NYqqY9Ac7QFLhjQWpEncjtMvpUPgl4Vsw8zIgyhndP3xF-CaWDWenTQEr6sONkNVDEknc1_9M8U5vMFYYWqa2UHWzolN8PKMcMRpYJfV1D7BpWqKeRQXB8j7ejuCJX29NY7zmPZb9zCk6sy3htH8cMV5lVu1GUG5mL0dKE97uZ7Ug--qMFFMX06naEMm3lfcnGmKapVuN3YEc_fn4rPl-1aHQszhsiLJ3L4ie1gN0m7tILk9z3eGaBLrSJBrrMlRpXFVJ_KgIQww3vIApSoqv9lB84jS5bx4T0s0FlxgXhyDii7Z3IbIHJF4yYzWNu_IZ2Zxm0FFLKk3l2Z0whDzbbvJh_cz4UUZlsOrhjxmhOw9nFGgHoUo-VvvvCUXuVPQJ-ipXr5TWoIuGMAqP93Eg8x3q02rI2BYdZOsvUdqVTA8uk3cmrTmMc8aJ-YKOdXoXKoderlPNb1_T4YBCjx6iKmfgbOdTImTIg52jjfVwwg8Wm3YO9k4OmI5P5wtmX_Kp9vi_PTIWCsEPFFOxDPsU8VVspwKOldjRSBnXkIei9s5FSjvweRJQEN9PTUyn4UOmZH85Wdh5RxiO45LNBmuxI-GfmO_ZjwNYTufIhGyJa52GFUejYFhwyn__80fgLy1NCsa6cXsTuMXVlVN9Mcn1duCJtbE8dUCeyRLveGfxX6--CiNIxl7xF7t5jEh1WycNnK92ZiE3v48F9H3xQp1HfIrS-hHmpLihrPNVcVaXh39r49y_Ot9Z8M7IosL9ZhN1scm9tw2ENM7aRpq7BDGENDTEMxByfT_GqougtXbvpnO0cDY0KD4FdoxL5373H42jhk6XHQoZp-8o4hWQ6YFKtDwahg55KFVtedX_56RBMXX1DdBkllVAnVdLl7Zd0fyL4F6ZXPQhTyAWkjF0w26oYfnglAncFo-ip75FxC-PrMKK36caOsyoa1NVykQjF_-Y342doRIQjohNn3rgyZz8kfg5DYU8bayvMPlmzwdQV_LpYCFVprOn35tIR7pCDmj7WA3PdAr5AF7ffAWGAQtIcqsWxZ5vG9wLMHrsZtIj4qhW0MMzTFjKqDkmd-dPcRd8rlbtG9sWsJaoaOZ1maQVPkb4-GnkAKgpHRhnsEpZ0ttehhiWuDOCuyfilBB8NgYKWL-stMd2dSTxI2nveC6JvyUGp5MQ6yTJ0BHvuWjxuGbyvorRNV9C0mTlohavgSQMOldNKI7GUQBw2L6sRWDuJbhEjTZxIboe2F4Q8XoFzhLf7n7xNKf32da6U4pLPluZtwXMFFbVeK8CfwGvGI7F9pRtv0BC6Z2tHH8_qPe4gd1KHouGLMf1xNW8HT_dLxpI3b2W_KzmuQiisoh3ZufLI3jaMbjhCAog5mAtyjcBm58gvqWjDCJJJuqQK7Dup6havOCY89xeM_v_k7weVaKJHltUPKd4iBZyUh3sixWrSFk-r8a-DB381-0dohK_3kT7BXIgd0O3ToxQ7KvpaoJfwhwtKewfVFAtfEkvEq0pmjyM6TbM7DC0mTeYdp2bo2DnpRTo411dyQ35A2jRvVs8mJFZUvtLcwQVPIGWWqYlVfex4r6jwHb9dVgAppfgAdN14aMq8XHL3oSZZrElbOpw69Si7OFOsHVhp3mSDPpAi5tQLq-zNLLqJHf3TejtUPXzX2UFjCKo8qGx2xoEsbHW_lt0_URQQ0kSRZZeAHrLf_LXF1LrS5Ez1cFrsWxC-Y6Q9ilait77lyZ1z2iaJnxcmCIt8cLbv_a7LtCToHQOXve_Xh-RzBPACnk6Kinp3fUvZqYqRnxb9fxsSlhIYTrEmfJUEr3iAQ52TR8DXg4g_N00HSURL6TlpMf_Se9c63b5v-UnFUI0p4qr-J7cF5cp4sJveKfqUserNGtCP8jBBCzh6I1-I4M5Rw2QF3V7W5pOQjVPG7-gqcb0TtYzrxMnEMtmJlzHM3CLq3FzuLkntA66iUrjgdxU2TEDKCWAtkR_xBLOjttPGoxF2w-3Qt5ytjWH-HqP1DjHKnICj9-hoqjtPrCPNDZfRSEkl0SYUFsh-LbcUSWTQCch7avbHpSd5nrubz_uqY-mpMk6iwEBtiXz7RvaqOkBV5L7kfdH1t79tdkEw9z7UwkbEaJtM3TWAQXLPCEBM33yk_7-k3JZzaF0vr3w8_rP0te3bMcBRCmGf5VlM9CJBaUHoaYIIQKYGRY0FKtfvMj92g3_ndpWTUjddYw4BeMl-tNo08PsZhKHR3iz_ZYNGUk8UPhcIT7mmDTjpJlVKNUeDNHXvvD2CDV4j3K4V9vQUWwr5PHwGfc2utCBOZdKxeDFX9zL916MMSxdFYGZ4Txf5V24V9gU_XyDeowuCETR3wdAr0PE0GmVXOmTuzdhlmGKKN43Z1rd4yd3gGXeOhFDy-8BpHPBdD5DXklM-YkKFF9_e9L8JhHybNCCghTJ2pnaeW4YOW7709f8SpihDXrJm7gs0vt8M0q9Nb3lR-1lLy4UhHniR5XUQEW-x7lj6IMvlhS89VlOTi4_BiQM7zUkvsx4VL-UUa0wQqZe8pCddIhUKB8q51SLW6ocTduyfPPCG9jkz2Vt7HNOrjrSwiYw0OHOpNMu19OLQsWvcCGtQtXfifUF6PxcmFoM_UkeFwQYyZW9KtekLZWNo-U5wWqr72a-OJ1klexckgBHYzG51S6ohvNWEWcKtAxa_lsKH3eNSU4Y631hotHKaOoCwrBqO5Sj-AcLHnHroDDE07vM3AsJbtXybPysK0EhctFhlCCS0L6mzPFg2RDJdq6iNHP0OZ6-O4ryHZjaLvNEg01BVzx-Fr_tl7W1Dr9Z7eVdyR3mX_2Y9kdE91Qdz-Qz44OumzGMMsupO8lpAJ_cyQa1dL8u7NfycazqyK_ZszzPCk0utfqMEC5398BnHZwc7MPjVkv8yuBpLw758iu55Bvw-HqdzFxkKUT2GlWb-I_E40vFZdFwR_jbyDrbPZyPJNqnY7BqCfVBCQgrkqTQuZAMbZdDOu274VZ9c9t4F19uWIeL1_BO9x48JHPXgGdeYm66H74kwCTX3Cs6lNFV9IdxINGaY6QLnRQxbCsB-87F0wto0-LuEYFQNdDpFqu1bCI3wDxpu9zQRNBZdb5X1wnSrkZ1zvVBLww5aX5YuRhkqGX3RxUquCdepiUJU0G8o7DPMdbwdHS1XleWMUWmHlbXdV576MpZnrRyp_BE0QYYZNx_E1peM55PBYA0fPE2yIrddNVbtb9yvDfvomoxhqBwT6brFQrBPY2kBN0y_9P5pizBnk= \ No newline at end of file diff --git a/v2_adminpanel/app.py b/v2_adminpanel/app.py index b2d5804..96c85f1 100644 --- a/v2_adminpanel/app.py +++ b/v2_adminpanel/app.py @@ -2743,6 +2743,7 @@ def export_licenses(): # Alle Lizenzen mit Kundeninformationen abrufen (ohne Testdaten, außer explizit gewünscht) include_test = request.args.get('include_test', 'false').lower() == 'true' + customer_id = request.args.get('customer_id', type=int) query = """ SELECT l.id, l.license_key, c.name as customer_name, c.email as customer_email, @@ -2757,12 +2758,23 @@ def export_licenses(): JOIN customers c ON l.customer_id = c.id """ + # Build WHERE clause + where_conditions = [] + params = [] + if not include_test: - query += " WHERE l.is_test = FALSE" + where_conditions.append("l.is_test = FALSE") + + if customer_id: + where_conditions.append("l.customer_id = %s") + params.append(customer_id) + + if where_conditions: + query += " WHERE " + " AND ".join(where_conditions) query += " ORDER BY l.id" - cur.execute(query) + cur.execute(query, params) # Spaltennamen columns = ['ID', 'Lizenzschlüssel', 'Kunde', 'E-Mail', 'Typ', @@ -2963,20 +2975,40 @@ def export_customers(): conn = get_connection() cur = conn.cursor() - # Alle Kunden mit Lizenzstatistiken (ohne Testdaten) - cur.execute(""" - SELECT c.id, c.name, c.email, c.created_at, - COUNT(CASE WHEN l.is_test = FALSE THEN 1 END) as total_licenses, - COUNT(CASE WHEN l.is_active = TRUE AND l.valid_until >= CURRENT_DATE AND l.is_test = FALSE THEN 1 END) as active_licenses, - COUNT(CASE WHEN l.valid_until < CURRENT_DATE AND l.is_test = FALSE THEN 1 END) as expired_licenses - FROM customers c - LEFT JOIN licenses l ON c.id = l.customer_id - GROUP BY c.id, c.name, c.email, c.created_at - ORDER BY c.id - """) + # Check if test data should be included + include_test = request.args.get('include_test', 'false').lower() == 'true' + + # Build query based on test data filter + if include_test: + # Include all customers + query = """ + SELECT c.id, c.name, c.email, c.created_at, c.is_test, + COUNT(l.id) as total_licenses, + COUNT(CASE WHEN l.is_active = TRUE AND l.valid_until >= CURRENT_DATE THEN 1 END) as active_licenses, + COUNT(CASE WHEN l.valid_until < CURRENT_DATE THEN 1 END) as expired_licenses + FROM customers c + LEFT JOIN licenses l ON c.id = l.customer_id + GROUP BY c.id, c.name, c.email, c.created_at, c.is_test + ORDER BY c.id + """ + else: + # Exclude test customers and test licenses + query = """ + SELECT c.id, c.name, c.email, c.created_at, c.is_test, + COUNT(CASE WHEN l.is_test = FALSE THEN 1 END) as total_licenses, + COUNT(CASE WHEN l.is_active = TRUE AND l.valid_until >= CURRENT_DATE AND l.is_test = FALSE THEN 1 END) as active_licenses, + COUNT(CASE WHEN l.valid_until < CURRENT_DATE AND l.is_test = FALSE THEN 1 END) as expired_licenses + FROM customers c + LEFT JOIN licenses l ON c.id = l.customer_id + WHERE c.is_test = FALSE + GROUP BY c.id, c.name, c.email, c.created_at, c.is_test + ORDER BY c.id + """ + + cur.execute(query) # Spaltennamen - columns = ['ID', 'Name', 'E-Mail', 'Erstellt am', + columns = ['ID', 'Name', 'E-Mail', 'Erstellt am', 'Testdaten', 'Lizenzen gesamt', 'Aktive Lizenzen', 'Abgelaufene Lizenzen'] # Daten in DataFrame @@ -2986,6 +3018,9 @@ def export_customers(): # Datumsformatierung df['Erstellt am'] = pd.to_datetime(df['Erstellt am']).dt.strftime('%d.%m.%Y %H:%M') + # Testdaten formatting + df['Testdaten'] = df['Testdaten'].replace({True: 'Ja', False: 'Nein'}) + cur.close() conn.close() diff --git a/v2_lizenzserver/.env b/v2_lizenzserver/.env new file mode 100644 index 0000000..7bd505a --- /dev/null +++ b/v2_lizenzserver/.env @@ -0,0 +1,4 @@ +SECRET_KEY=your-super-secret-key-change-this-in-production-12345 +DATABASE_URL=postgresql://adminuser:supergeheimespasswort@db:5432/meinedatenbank +REDIS_URL=redis://redis:6379 +DEBUG=False \ No newline at end of file diff --git a/v2_lizenzserver/Dockerfile b/v2_lizenzserver/Dockerfile index 69b8d95..6778a91 100644 --- a/v2_lizenzserver/Dockerfile +++ b/v2_lizenzserver/Dockerfile @@ -1,18 +1,21 @@ FROM python:3.11-slim -# Zeitzone setzen -ENV TZ=Europe/Berlin -ENV DEBIAN_FRONTEND=noninteractive - -RUN apt-get update && apt-get install -y tzdata \ - && ln -sf /usr/share/zoneinfo/Europe/Berlin /etc/localtime \ - && echo "Europe/Berlin" > /etc/timezone \ - && apt-get clean \ - && rm -rf /var/lib/apt/lists/* - WORKDIR /app -# Placeholder für Lizenzserver -RUN echo "Lizenzserver noch nicht implementiert" > info.txt +RUN apt-get update && apt-get install -y \ + gcc \ + postgresql-client \ + && rm -rf /var/lib/apt/lists/* -CMD ["python", "-c", "print('Lizenzserver Container läuft, aber noch keine Implementierung vorhanden'); import time; time.sleep(86400)"] \ No newline at end of file +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY app app/ +COPY init_db.py . +COPY .env* ./ + +ENV PYTHONPATH=/app + +EXPOSE 8443 + +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8443"] \ No newline at end of file diff --git a/v2_lizenzserver/LIZENZSERVER_ANLEITUNG.md b/v2_lizenzserver/LIZENZSERVER_ANLEITUNG.md new file mode 100644 index 0000000..517b526 --- /dev/null +++ b/v2_lizenzserver/LIZENZSERVER_ANLEITUNG.md @@ -0,0 +1,335 @@ +# Lizenzserver Nutzungsanleitung + +## Übersicht +Der Lizenzserver läuft unter: `https://api-software-undso.z5m7q9dk3ah2v1plx6ju.com` + +## API Endpoints + +### 1. Lizenz aktivieren +**POST** `/api/license/activate` + +Aktiviert eine Lizenz für eine bestimmte Maschine. + +```json +{ + "license_key": "XXXX-XXXX-XXXX-XXXX", + "machine_id": "UNIQUE-MACHINE-ID", + "hardware_hash": "SHA256-HARDWARE-FINGERPRINT", + "os_info": { + "os": "Windows", + "version": "10.0.19041" + }, + "app_version": "1.0.0" +} +``` + +**Antwort bei Erfolg:** +```json +{ + "success": true, + "message": "License activated successfully", + "activation_id": 123, + "expires_at": "2025-12-31T23:59:59", + "features": { + "all_features": true + } +} +``` + +### 2. Lizenz verifizieren (Heartbeat) +**POST** `/api/license/verify` + +Sollte alle 15 Minuten aufgerufen werden. + +```json +{ + "license_key": "XXXX-XXXX-XXXX-XXXX", + "machine_id": "UNIQUE-MACHINE-ID", + "hardware_hash": "SHA256-HARDWARE-FINGERPRINT", + "activation_id": 123 +} +``` + +**Antwort:** +```json +{ + "valid": true, + "message": "License is valid", + "expires_at": "2025-12-31T23:59:59", + "features": { + "all_features": true + }, + "requires_update": false, + "update_url": null +} +``` + +### 3. Version prüfen +**POST** `/api/version/check` + +```json +{ + "current_version": "1.0.0", + "license_key": "XXXX-XXXX-XXXX-XXXX" +} +``` + +**Antwort:** +```json +{ + "latest_version": "1.1.0", + "current_version": "1.0.0", + "update_available": true, + "is_mandatory": false, + "download_url": "https://download.example.com/v1.1.0", + "release_notes": "Bug fixes and improvements" +} +``` + +### 4. Lizenzinfo abrufen +**GET** `/api/license/info/{license_key}` + +Liefert detaillierte Informationen über eine Lizenz. + +## Authentifizierung + +Alle API-Anfragen benötigen einen API-Key im Authorization Header: + +``` +Authorization: Bearer YOUR-API-KEY-HERE +``` + +## Client-Integration Beispiel (Python) + +```python +import requests +import hashlib +import platform +import uuid +import json +from datetime import datetime, timedelta + +class LicenseClient: + def __init__(self, api_key, server_url="https://api-software-undso.z5m7q9dk3ah2v1plx6ju.com"): + self.api_key = api_key + self.server_url = server_url + self.headers = {"Authorization": f"Bearer {api_key}"} + + def get_machine_id(self): + """Eindeutige Maschinen-ID generieren""" + return str(uuid.getnode()) # MAC-Adresse + + def get_hardware_hash(self): + """Hardware-Fingerprint erstellen""" + machine_id = self.get_machine_id() + cpu_info = platform.processor() + system_info = f"{platform.system()}-{platform.machine()}" + + combined = f"{machine_id}-{cpu_info}-{system_info}" + return hashlib.sha256(combined.encode()).hexdigest() + + def activate_license(self, license_key): + """Lizenz aktivieren""" + data = { + "license_key": license_key, + "machine_id": self.get_machine_id(), + "hardware_hash": self.get_hardware_hash(), + "os_info": { + "os": platform.system(), + "version": platform.version(), + "machine": platform.machine() + }, + "app_version": "1.0.0" + } + + response = requests.post( + f"{self.server_url}/api/license/activate", + headers=self.headers, + json=data, + verify=True # SSL-Zertifikat prüfen + ) + + return response.json() + + def verify_license(self, license_key, activation_id): + """Lizenz verifizieren (Heartbeat)""" + data = { + "license_key": license_key, + "machine_id": self.get_machine_id(), + "hardware_hash": self.get_hardware_hash(), + "activation_id": activation_id + } + + response = requests.post( + f"{self.server_url}/api/license/verify", + headers=self.headers, + json=data, + verify=True + ) + + return response.json() + + def check_version(self, license_key, current_version): + """Version prüfen""" + data = { + "current_version": current_version, + "license_key": license_key + } + + response = requests.post( + f"{self.server_url}/api/version/check", + headers=self.headers, + json=data, + verify=True + ) + + return response.json() + +# Verwendungsbeispiel +if __name__ == "__main__": + client = LicenseClient("YOUR-API-KEY") + + # Lizenz aktivieren + result = client.activate_license("USER-LICENSE-KEY") + if result["success"]: + activation_id = result["activation_id"] + print(f"Lizenz aktiviert! Activation ID: {activation_id}") + + # Lizenz verifizieren + verify_result = client.verify_license("USER-LICENSE-KEY", activation_id) + if verify_result["valid"]: + print("Lizenz ist gültig!") +``` + +## Client-Integration Beispiel (C#) + +```csharp +using System; +using System.Net.Http; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; +using System.Security.Cryptography; +using System.Management; + +public class LicenseClient +{ + private readonly HttpClient httpClient; + private readonly string apiKey; + private readonly string serverUrl; + + public LicenseClient(string apiKey, string serverUrl = "https://api-software-undso.z5m7q9dk3ah2v1plx6ju.com") + { + this.apiKey = apiKey; + this.serverUrl = serverUrl; + + httpClient = new HttpClient(); + httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {apiKey}"); + } + + private string GetMachineId() + { + // CPU ID abrufen + string cpuId = ""; + ManagementObjectSearcher searcher = new ManagementObjectSearcher("SELECT ProcessorId FROM Win32_Processor"); + foreach (ManagementObject obj in searcher.Get()) + { + cpuId = obj["ProcessorId"].ToString(); + break; + } + return cpuId; + } + + private string GetHardwareHash() + { + string machineId = GetMachineId(); + string systemInfo = Environment.MachineName + Environment.OSVersion; + + using (SHA256 sha256 = SHA256.Create()) + { + byte[] hashBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(machineId + systemInfo)); + return BitConverter.ToString(hashBytes).Replace("-", "").ToLowerInvariant(); + } + } + + public async Task ActivateLicenseAsync(string licenseKey) + { + var requestData = new + { + license_key = licenseKey, + machine_id = GetMachineId(), + hardware_hash = GetHardwareHash(), + os_info = new + { + os = "Windows", + version = Environment.OSVersion.Version.ToString() + }, + app_version = "1.0.0" + }; + + var json = JsonSerializer.Serialize(requestData); + var content = new StringContent(json, Encoding.UTF8, "application/json"); + + var response = await httpClient.PostAsync($"{serverUrl}/api/license/activate", content); + var responseContent = await response.Content.ReadAsStringAsync(); + + return JsonSerializer.Deserialize(responseContent); + } +} + +public class LicenseActivationResponse +{ + public bool success { get; set; } + public string message { get; set; } + public int? activation_id { get; set; } + public DateTime? expires_at { get; set; } +} +``` + +## Wichtige Hinweise + +### Sicherheit +- API-Keys niemals im Quellcode hartcodieren +- SSL-Zertifikat immer prüfen (verify=True) +- Hardware-Hash lokal zwischenspeichern + +### Rate Limiting +- Max. 100 Anfragen pro Minute pro API-Key +- Heartbeat alle 15 Minuten (nicht öfter) + +### Offline-Betrieb +- 7 Tage Grace Period bei Verbindungsproblemen +- Letzte gültige Lizenz lokal cachen +- Bei Hardware-Änderung: Grace Period nutzen + +### Fehlerbehandlung +```python +try: + result = client.verify_license(license_key, activation_id) +except requests.exceptions.ConnectionError: + # Offline-Modus: Gecachte Lizenz prüfen + if cached_license_valid(): + continue_execution() + else: + show_error("Keine Internetverbindung und Lizenz abgelaufen") +except requests.exceptions.HTTPError as e: + if e.response.status_code == 401: + show_error("Ungültiger API-Key") + elif e.response.status_code == 403: + show_error("Lizenz ungültig oder abgelaufen") +``` + +## Test-Zugangsdaten + +Für Entwicklungszwecke: +- Test API-Key: `test-api-key-12345` +- Test Lizenz: `TEST-LICENSE-KEY-12345` + +**WICHTIG:** Für Produktion eigene API-Keys und Lizenzen erstellen! + +## Support + +Bei Problemen: +1. Logs prüfen: `docker logs license-server` +2. API-Status: https://api-software-undso.z5m7q9dk3ah2v1plx6ju.com/health +3. Datenbank prüfen: Lizenzen und Aktivierungen \ No newline at end of file diff --git a/v2_lizenzserver/README.md b/v2_lizenzserver/README.md new file mode 100644 index 0000000..4a942a7 --- /dev/null +++ b/v2_lizenzserver/README.md @@ -0,0 +1,29 @@ +# License Server für v2-Docker + +Dieser License Server ersetzt den Dummy-Container und bietet vollständige Lizenzmanagement-Funktionalität. + +## Features +- Lizenzaktivierung mit Hardware-Binding +- Heartbeat/Verifizierung alle 15 Minuten +- Versionskontrolle mit Update-Erzwingung +- Offline Grace Period (7 Tage) +- Multi-Aktivierung Support + +## API Endpoints +- POST `/api/license/activate` - Lizenz aktivieren +- POST `/api/license/verify` - Lizenz verifizieren (Heartbeat) +- POST `/api/version/check` - Version prüfen +- GET `/api/license/info/{license_key}` - Lizenzinfo abrufen + +## Deployment +Der Server läuft im v2-Docker Stack und ist über nginx erreichbar: +- Extern: https://api-software-undso.z5m7q9dk3ah2v1plx6ju.com +- Intern: http://license-server:8443 + +## Konfiguration +Die Konfiguration erfolgt über die `.env` Datei: +- `DATABASE_URL` - Verbindung zur PostgreSQL Datenbank +- `SECRET_KEY` - JWT Secret Key für Token-Signierung + +## Integration +Siehe `LIZENZSERVER_ANLEITUNG.md` und die Beispiele in `client_examples/` für die Client-Integration. \ No newline at end of file diff --git a/v2_lizenzserver/TODO.md b/v2_lizenzserver/TODO.md new file mode 100644 index 0000000..556a25c --- /dev/null +++ b/v2_lizenzserver/TODO.md @@ -0,0 +1,90 @@ +# TODO - License Server & Admin Panel Erweiterungen + +## 🎯 Phase 1: Admin Panel Integration (Priorität: Hoch) + +### Neue Lizenz-Verwaltung im Admin Panel +- [ ] Neuer Menüpunkt "Lizenzen" im Admin Panel +- [ ] Dashboard mit Lizenz-Statistiken + - [ ] Anzahl aktive/abgelaufene Lizenzen + - [ ] Aktivierungen pro Lizenz + - [ ] Grafische Auswertungen +- [ ] Lizenz-Erstellung Formular + - [ ] Lizenzschlüssel generieren + - [ ] Ablaufdatum festlegen + - [ ] Max. Aktivierungen definieren + - [ ] Kundeninformationen speichern +- [ ] Lizenz-Übersicht Tabelle + - [ ] Suche/Filter-Funktionen + - [ ] Status anzeigen (aktiv/abgelaufen/gesperrt) + - [ ] Aktivierungen einsehen + - [ ] Lizenzen deaktivieren/reaktivieren +- [ ] API-Key Verwaltung + - [ ] Neue API-Keys generieren + - [ ] Bestehende Keys anzeigen/deaktivieren + - [ ] Berechtigungen festlegen + +## 🔒 Phase 2: Sicherheits-Optimierungen (Priorität: Mittel) + +### Rate Limiting in Nginx +- [ ] API Rate Limiting implementieren (10 Requests/Minute) +- [ ] Burst-Handling konfigurieren +- [ ] IP-basierte Limits für API-Endpoints + +### Datenbank-Sicherheit +- [ ] Separaten Read-Only DB-User für API erstellen +- [ ] Admin-User mit eingeschränkten Rechten +- [ ] Connection Pooling optimieren + +### GeoIP-Blocking +- [ ] GeoIP-Modul in Nginx installieren +- [ ] Länder-Blacklist konfigurieren +- [ ] VPN-Erkennung implementieren +- [ ] Logging verdächtiger Zugriffe + +## 📊 Phase 3: Monitoring & Backup (Priorität: Mittel) + +### Monitoring-System +- [ ] Prometheus einrichten +- [ ] Grafana Dashboards erstellen + - [ ] API-Zugriffe + - [ ] Lizenz-Aktivierungen + - [ ] System-Ressourcen +- [ ] Alerting bei Anomalien + - [ ] Zu viele fehlgeschlagene Aktivierungen + - [ ] Verdächtige Zugriffsmuster + - [ ] System-Ausfälle + +### Backup-Strategie +- [ ] Automatische tägliche DB-Backups +- [ ] Backup-Rotation (30 Tage aufbewahren) +- [ ] Verschlüsselung der Backups +- [ ] Offsite-Backup zu Cloud-Storage +- [ ] Restore-Tests durchführen + +## 🚀 Phase 4: Performance & Features (Priorität: Niedrig) + +### Performance-Optimierungen +- [ ] Redis Cache für häufige Queries +- [ ] Database Indexing optimieren +- [ ] API Response Caching + +### Erweiterte Features +- [ ] Export-Funktionen (CSV/Excel) +- [ ] Email-Benachrichtigungen + - [ ] Bei Lizenz-Ablauf + - [ ] Bei verdächtigen Aktivitäten +- [ ] Lizenz-Templates für häufige Konfigurationen +- [ ] Bulk-Operationen (mehrere Lizenzen gleichzeitig) + +## 📝 Dokumentation + +- [ ] API-Dokumentation erweitern +- [ ] Admin Panel Benutzerhandbuch +- [ ] Deployment-Guide aktualisieren +- [ ] Troubleshooting-Guide erstellen + +## Prioritäten: +1. **Sofort**: Phase 1 - Admin Panel Integration +2. **Nächste Woche**: Phase 2 - Sicherheits-Optimierungen +3. **Nächster Monat**: Phase 3 - Monitoring & Backup +4. **Bei Bedarf**: Phase 4 - Performance & Features \ No newline at end of file diff --git a/v2_lizenzserver/app/api/__init__.py b/v2_lizenzserver/app/api/__init__.py new file mode 100644 index 0000000..6bdf4c9 --- /dev/null +++ b/v2_lizenzserver/app/api/__init__.py @@ -0,0 +1 @@ +from . import license, version \ No newline at end of file diff --git a/v2_lizenzserver/app/api/license.py b/v2_lizenzserver/app/api/license.py new file mode 100644 index 0000000..0d72e68 --- /dev/null +++ b/v2_lizenzserver/app/api/license.py @@ -0,0 +1,209 @@ +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.orm import Session +from datetime import datetime, timedelta +from typing import Dict, Any + +from app.db.database import get_db +from app.models.models import License, Activation, Version +from app.schemas.license import ( + LicenseActivationRequest, + LicenseActivationResponse, + LicenseVerificationRequest, + LicenseVerificationResponse +) +from app.core.security import get_api_key +from app.core.config import settings + +router = APIRouter() + +@router.post("/activate", response_model=LicenseActivationResponse) +async def activate_license( + request: LicenseActivationRequest, + db: Session = Depends(get_db), + api_key = Depends(get_api_key) +): + license = db.query(License).filter( + License.license_key == request.license_key, + License.is_active == True + ).first() + + if not license: + return LicenseActivationResponse( + success=False, + message="Invalid license key" + ) + + if license.expires_at and license.expires_at < datetime.utcnow(): + return LicenseActivationResponse( + success=False, + message="License has expired" + ) + + existing_activations = db.query(Activation).filter( + Activation.license_id == license.id, + Activation.is_active == True + ).all() + + existing_machine = next( + (a for a in existing_activations if a.machine_id == request.machine_id), + None + ) + + if existing_machine: + if existing_machine.hardware_hash != request.hardware_hash: + return LicenseActivationResponse( + success=False, + message="Hardware mismatch for this machine" + ) + + existing_machine.last_heartbeat = datetime.utcnow() + existing_machine.app_version = request.app_version + existing_machine.os_info = request.os_info + db.commit() + + return LicenseActivationResponse( + success=True, + message="License reactivated successfully", + activation_id=existing_machine.id, + expires_at=license.expires_at, + features={"all_features": True} + ) + + if len(existing_activations) >= license.max_activations: + return LicenseActivationResponse( + success=False, + message=f"Maximum activations ({license.max_activations}) reached" + ) + + new_activation = Activation( + license_id=license.id, + machine_id=request.machine_id, + hardware_hash=request.hardware_hash, + os_info=request.os_info, + app_version=request.app_version + ) + + db.add(new_activation) + db.commit() + db.refresh(new_activation) + + return LicenseActivationResponse( + success=True, + message="License activated successfully", + activation_id=new_activation.id, + expires_at=license.expires_at, + features={"all_features": True} + ) + +@router.post("/verify", response_model=LicenseVerificationResponse) +async def verify_license( + request: LicenseVerificationRequest, + db: Session = Depends(get_db), + api_key = Depends(get_api_key) +): + activation = db.query(Activation).filter( + Activation.id == request.activation_id, + Activation.machine_id == request.machine_id, + Activation.is_active == True + ).first() + + if not activation: + return LicenseVerificationResponse( + valid=False, + message="Invalid activation" + ) + + license = activation.license + + if not license.is_active: + return LicenseVerificationResponse( + valid=False, + message="License is no longer active" + ) + + if license.license_key != request.license_key: + return LicenseVerificationResponse( + valid=False, + message="License key mismatch" + ) + + if activation.hardware_hash != request.hardware_hash: + grace_period = datetime.utcnow() - timedelta(days=settings.OFFLINE_GRACE_PERIOD_DAYS) + if activation.last_heartbeat < grace_period: + return LicenseVerificationResponse( + valid=False, + message="Hardware mismatch and grace period expired" + ) + else: + return LicenseVerificationResponse( + valid=True, + message="Hardware mismatch but within grace period", + expires_at=license.expires_at, + features={"all_features": True} + ) + + if license.expires_at and license.expires_at < datetime.utcnow(): + return LicenseVerificationResponse( + valid=False, + message="License has expired" + ) + + activation.last_heartbeat = datetime.utcnow() + db.commit() + + latest_version = db.query(Version).order_by(Version.release_date.desc()).first() + requires_update = False + update_url = None + + if latest_version and activation.app_version: + if latest_version.version_number > activation.app_version: + requires_update = True + update_url = latest_version.download_url + + return LicenseVerificationResponse( + valid=True, + message="License is valid", + expires_at=license.expires_at, + features={"all_features": True}, + requires_update=requires_update, + update_url=update_url + ) + +@router.get("/info/{license_key}") +async def get_license_info( + license_key: str, + db: Session = Depends(get_db), + api_key = Depends(get_api_key) +): + license = db.query(License).filter( + License.license_key == license_key + ).first() + + if not license: + raise HTTPException(status_code=404, detail="License not found") + + activations = db.query(Activation).filter( + Activation.license_id == license.id, + Activation.is_active == True + ).all() + + return { + "license_key": license.license_key, + "product_id": license.product_id, + "customer_email": license.customer_email, + "customer_name": license.customer_name, + "is_active": license.is_active, + "expires_at": license.expires_at, + "max_activations": license.max_activations, + "current_activations": len(activations), + "activations": [ + { + "id": a.id, + "machine_id": a.machine_id, + "activation_date": a.activation_date, + "last_heartbeat": a.last_heartbeat, + "app_version": a.app_version + } + for a in activations + ] + } \ No newline at end of file diff --git a/v2_lizenzserver/app/api/version.py b/v2_lizenzserver/app/api/version.py new file mode 100644 index 0000000..56849e2 --- /dev/null +++ b/v2_lizenzserver/app/api/version.py @@ -0,0 +1,84 @@ +from fastapi import APIRouter, Depends +from sqlalchemy.orm import Session +from packaging import version + +from app.db.database import get_db +from app.models.models import Version, License +from app.schemas.license import VersionCheckRequest, VersionCheckResponse +from app.core.security import get_api_key + +router = APIRouter() + +@router.post("/check", response_model=VersionCheckResponse) +async def check_version( + request: VersionCheckRequest, + db: Session = Depends(get_db), + api_key = Depends(get_api_key) +): + license = db.query(License).filter( + License.license_key == request.license_key, + License.is_active == True + ).first() + + if not license: + return VersionCheckResponse( + latest_version=request.current_version, + current_version=request.current_version, + update_available=False, + is_mandatory=False + ) + + latest_version = db.query(Version).order_by(Version.release_date.desc()).first() + + if not latest_version: + return VersionCheckResponse( + latest_version=request.current_version, + current_version=request.current_version, + update_available=False, + is_mandatory=False + ) + + current_ver = version.parse(request.current_version) + latest_ver = version.parse(latest_version.version_number) + + update_available = latest_ver > current_ver + is_mandatory = False + + if update_available and latest_version.is_mandatory: + if latest_version.min_version: + min_ver = version.parse(latest_version.min_version) + is_mandatory = current_ver < min_ver + else: + is_mandatory = True + + return VersionCheckResponse( + latest_version=latest_version.version_number, + current_version=request.current_version, + update_available=update_available, + is_mandatory=is_mandatory, + download_url=latest_version.download_url if update_available else None, + release_notes=latest_version.release_notes if update_available else None + ) + +@router.get("/latest") +async def get_latest_version( + db: Session = Depends(get_db), + api_key = Depends(get_api_key) +): + latest_version = db.query(Version).order_by(Version.release_date.desc()).first() + + if not latest_version: + return { + "version": "1.0.0", + "release_date": None, + "release_notes": "Initial release" + } + + return { + "version": latest_version.version_number, + "release_date": latest_version.release_date, + "is_mandatory": latest_version.is_mandatory, + "min_version": latest_version.min_version, + "download_url": latest_version.download_url, + "release_notes": latest_version.release_notes + } \ No newline at end of file diff --git a/v2_lizenzserver/app/core/config.py b/v2_lizenzserver/app/core/config.py new file mode 100644 index 0000000..e0beb22 --- /dev/null +++ b/v2_lizenzserver/app/core/config.py @@ -0,0 +1,32 @@ +from pydantic_settings import BaseSettings +from typing import List + +class Settings(BaseSettings): + PROJECT_NAME: str = "License Server" + VERSION: str = "1.0.0" + API_PREFIX: str = "/api" + + SECRET_KEY: str = "your-secret-key-change-this-in-production" + ALGORITHM: str = "HS256" + ACCESS_TOKEN_EXPIRE_MINUTES: int = 30 + + DATABASE_URL: str = "postgresql://license_user:license_password@db:5432/license_db" + + REDIS_URL: str = "redis://redis:6379" + + ALLOWED_ORIGINS: List[str] = [ + "https://api-software-undso.z5m7q9dk3ah2v1plx6ju.com", + "https://admin-panel-undso.z5m7q9dk3ah2v1plx6ju.com" + ] + + DEBUG: bool = False + + MAX_ACTIVATIONS_PER_LICENSE: int = 5 + HEARTBEAT_INTERVAL_MINUTES: int = 15 + OFFLINE_GRACE_PERIOD_DAYS: int = 7 + + class Config: + env_file = ".env" + case_sensitive = True + +settings = Settings() \ No newline at end of file diff --git a/v2_lizenzserver/app/core/security.py b/v2_lizenzserver/app/core/security.py new file mode 100644 index 0000000..608e613 --- /dev/null +++ b/v2_lizenzserver/app/core/security.py @@ -0,0 +1,52 @@ +from datetime import datetime, timedelta +from typing import Optional +from jose import JWTError, jwt +from passlib.context import CryptContext +from fastapi import HTTPException, Security, Depends +from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials +from sqlalchemy.orm import Session +from app.db.database import get_db +from app.models.models import ApiKey +from app.core.config import settings + +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") +security = HTTPBearer() + +def create_access_token(data: dict, expires_delta: Optional[timedelta] = None): + to_encode = data.copy() + if expires_delta: + expire = datetime.utcnow() + expires_delta + else: + expire = datetime.utcnow() + timedelta(minutes=15) + to_encode.update({"exp": expire}) + encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM) + return encoded_jwt + +def verify_token(credentials: HTTPAuthorizationCredentials = Security(security)): + token = credentials.credentials + try: + payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM]) + return payload + except JWTError: + raise HTTPException(status_code=403, detail="Invalid token") + +def verify_api_key(api_key: str, db: Session): + key = db.query(ApiKey).filter( + ApiKey.key == api_key, + ApiKey.is_active == True + ).first() + + if not key: + raise HTTPException(status_code=401, detail="Invalid API key") + + key.last_used = datetime.utcnow() + db.commit() + + return key + +def get_api_key( + credentials: HTTPAuthorizationCredentials = Security(security), + db: Session = Depends(get_db) +): + api_key = credentials.credentials + return verify_api_key(api_key, db) \ No newline at end of file diff --git a/v2_lizenzserver/app/db/database.py b/v2_lizenzserver/app/db/database.py new file mode 100644 index 0000000..93283a4 --- /dev/null +++ b/v2_lizenzserver/app/db/database.py @@ -0,0 +1,16 @@ +from sqlalchemy import create_engine +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import sessionmaker +from app.core.config import settings + +engine = create_engine(settings.DATABASE_URL) +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + +Base = declarative_base() + +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() \ No newline at end of file diff --git a/v2_lizenzserver/app/main.py b/v2_lizenzserver/app/main.py new file mode 100644 index 0000000..b1a56c2 --- /dev/null +++ b/v2_lizenzserver/app/main.py @@ -0,0 +1,65 @@ +from fastapi import FastAPI, Request +from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import JSONResponse +import uvicorn +import logging +from datetime import datetime + +from app.api import license, version +from app.core.config import settings +from app.db.database import engine, Base + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +Base.metadata.create_all(bind=engine) + +app = FastAPI( + title="License Server API", + description="API for software license management", + version="1.0.0", + docs_url="/docs" if settings.DEBUG else None, + redoc_url="/redoc" if settings.DEBUG else None, +) + +app.add_middleware( + CORSMiddleware, + allow_origins=settings.ALLOWED_ORIGINS, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +@app.exception_handler(Exception) +async def global_exception_handler(request: Request, exc: Exception): + logger.error(f"Global exception: {str(exc)}", exc_info=True) + return JSONResponse( + status_code=500, + content={"detail": "Internal server error"} + ) + +@app.get("/") +async def root(): + return { + "status": "online", + "service": "License Server", + "timestamp": datetime.utcnow().isoformat() + } + +@app.get("/health") +async def health_check(): + return { + "status": "healthy", + "timestamp": datetime.utcnow().isoformat() + } + +app.include_router(license.router, prefix="/api/license", tags=["license"]) +app.include_router(version.router, prefix="/api/version", tags=["version"]) + +if __name__ == "__main__": + uvicorn.run( + "app.main:app", + host="0.0.0.0", + port=8443, + reload=settings.DEBUG + ) \ No newline at end of file diff --git a/v2_lizenzserver/app/models/__init__.py b/v2_lizenzserver/app/models/__init__.py new file mode 100644 index 0000000..efdec05 --- /dev/null +++ b/v2_lizenzserver/app/models/__init__.py @@ -0,0 +1 @@ +from .models import License, Activation, Version, ApiKey \ No newline at end of file diff --git a/v2_lizenzserver/app/models/models.py b/v2_lizenzserver/app/models/models.py new file mode 100644 index 0000000..918ba71 --- /dev/null +++ b/v2_lizenzserver/app/models/models.py @@ -0,0 +1,65 @@ +from sqlalchemy import Column, String, Integer, Boolean, DateTime, ForeignKey, Text, JSON +from sqlalchemy.orm import relationship +from sqlalchemy.sql import func +from app.db.database import Base +import uuid + +class License(Base): + __tablename__ = "licenses" + + id = Column(Integer, primary_key=True, index=True) + license_key = Column(String, unique=True, index=True, default=lambda: str(uuid.uuid4())) + product_id = Column(String, nullable=False) + customer_email = Column(String, nullable=False) + customer_name = Column(String) + + max_activations = Column(Integer, default=1) + is_active = Column(Boolean, default=True) + expires_at = Column(DateTime, nullable=True) + + created_at = Column(DateTime, server_default=func.now()) + updated_at = Column(DateTime, onupdate=func.now()) + + activations = relationship("Activation", back_populates="license") + +class Activation(Base): + __tablename__ = "activations" + + id = Column(Integer, primary_key=True, index=True) + license_id = Column(Integer, ForeignKey("licenses.id")) + machine_id = Column(String, nullable=False) + hardware_hash = Column(String, nullable=False) + + activation_date = Column(DateTime, server_default=func.now()) + last_heartbeat = Column(DateTime, server_default=func.now()) + is_active = Column(Boolean, default=True) + + os_info = Column(JSON) + app_version = Column(String) + + license = relationship("License", back_populates="activations") + +class Version(Base): + __tablename__ = "versions" + + id = Column(Integer, primary_key=True, index=True) + version_number = Column(String, unique=True, nullable=False) + release_date = Column(DateTime, server_default=func.now()) + is_mandatory = Column(Boolean, default=False) + min_version = Column(String) + + download_url = Column(String) + release_notes = Column(Text) + + created_at = Column(DateTime, server_default=func.now()) + +class ApiKey(Base): + __tablename__ = "api_keys" + + id = Column(Integer, primary_key=True, index=True) + key = Column(String, unique=True, index=True, default=lambda: str(uuid.uuid4())) + name = Column(String, nullable=False) + is_active = Column(Boolean, default=True) + + created_at = Column(DateTime, server_default=func.now()) + last_used = Column(DateTime) \ No newline at end of file diff --git a/v2_lizenzserver/app/schemas/__init__.py b/v2_lizenzserver/app/schemas/__init__.py new file mode 100644 index 0000000..9ddbde8 --- /dev/null +++ b/v2_lizenzserver/app/schemas/__init__.py @@ -0,0 +1,8 @@ +from .license import ( + LicenseActivationRequest, + LicenseActivationResponse, + LicenseVerificationRequest, + LicenseVerificationResponse, + VersionCheckRequest, + VersionCheckResponse +) \ No newline at end of file diff --git a/v2_lizenzserver/app/schemas/license.py b/v2_lizenzserver/app/schemas/license.py new file mode 100644 index 0000000..3c6127d --- /dev/null +++ b/v2_lizenzserver/app/schemas/license.py @@ -0,0 +1,43 @@ +from pydantic import BaseModel, EmailStr +from datetime import datetime +from typing import Optional, Dict, Any + +class LicenseActivationRequest(BaseModel): + license_key: str + machine_id: str + hardware_hash: str + os_info: Optional[Dict[str, Any]] = None + app_version: Optional[str] = None + +class LicenseActivationResponse(BaseModel): + success: bool + message: str + activation_id: Optional[int] = None + expires_at: Optional[datetime] = None + features: Optional[Dict[str, Any]] = None + +class LicenseVerificationRequest(BaseModel): + license_key: str + machine_id: str + hardware_hash: str + activation_id: int + +class LicenseVerificationResponse(BaseModel): + valid: bool + message: str + expires_at: Optional[datetime] = None + features: Optional[Dict[str, Any]] = None + requires_update: bool = False + update_url: Optional[str] = None + +class VersionCheckRequest(BaseModel): + current_version: str + license_key: str + +class VersionCheckResponse(BaseModel): + latest_version: str + current_version: str + update_available: bool + is_mandatory: bool + download_url: Optional[str] = None + release_notes: Optional[str] = None \ No newline at end of file diff --git a/v2_lizenzserver/client_examples/csharp_client.cs b/v2_lizenzserver/client_examples/csharp_client.cs new file mode 100644 index 0000000..dbb6b91 --- /dev/null +++ b/v2_lizenzserver/client_examples/csharp_client.cs @@ -0,0 +1,534 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Management; +using System.Net.Http; +using System.Security.Cryptography; +using System.Text; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; + +namespace LicenseClient +{ + /// + /// Vollständige Lizenzserver-Integration für .NET-Anwendungen + /// + public class LicenseManager : IDisposable + { + private readonly HttpClient httpClient; + private readonly string apiKey; + private readonly string appVersion; + private readonly string serverUrl = "https://api-software-undso.z5m7q9dk3ah2v1plx6ju.com"; + private readonly string cacheFilePath; + + private string licenseKey; + private int? activationId; + private DateTime? expiresAt; + private bool isValid; + + private Timer heartbeatTimer; + private readonly SemaphoreSlim semaphore = new SemaphoreSlim(1, 1); + + public LicenseManager(string apiKey, string appVersion = "1.0.0") + { + this.apiKey = apiKey; + this.appVersion = appVersion; + + // HttpClient Setup + var handler = new HttpClientHandler + { + ServerCertificateCustomValidationCallback = (message, cert, chain, errors) => true // Für Entwicklung + }; + + httpClient = new HttpClient(handler); + httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {apiKey}"); + httpClient.Timeout = TimeSpan.FromSeconds(30); + + // Cache-Verzeichnis + string appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); + string appFolder = Path.Combine(appDataPath, "MyApp", "License"); + Directory.CreateDirectory(appFolder); + cacheFilePath = Path.Combine(appFolder, "license.json"); + } + + /// + /// Eindeutige Maschinen-ID generieren + /// + private string GetMachineId() + { + try + { + // CPU-ID abrufen + string cpuId = ""; + using (ManagementObjectSearcher searcher = new ManagementObjectSearcher("SELECT ProcessorId FROM Win32_Processor")) + { + foreach (ManagementObject obj in searcher.Get()) + { + cpuId = obj["ProcessorId"]?.ToString() ?? ""; + break; + } + } + + // Motherboard Serial Number + string motherboardId = ""; + using (ManagementObjectSearcher searcher = new ManagementObjectSearcher("SELECT SerialNumber FROM Win32_BaseBoard")) + { + foreach (ManagementObject obj in searcher.Get()) + { + motherboardId = obj["SerialNumber"]?.ToString() ?? ""; + break; + } + } + + return $"{cpuId}-{motherboardId}"; + } + catch + { + // Fallback: Machine Name + User + return $"{Environment.MachineName}-{Environment.UserName}"; + } + } + + /// + /// Hardware-Fingerprint erstellen + /// + private string GetHardwareHash() + { + var components = new List + { + GetMachineId(), + Environment.MachineName, + Environment.OSVersion.ToString(), + Environment.ProcessorCount.ToString() + }; + + // MAC-Adressen hinzufügen + try + { + using (var searcher = new ManagementObjectSearcher("SELECT MACAddress FROM Win32_NetworkAdapter WHERE MACAddress IS NOT NULL")) + { + foreach (ManagementObject obj in searcher.Get()) + { + string mac = obj["MACAddress"]?.ToString(); + if (!string.IsNullOrEmpty(mac)) + components.Add(mac); + } + } + } + catch { } + + string combined = string.Join("-", components); + using (SHA256 sha256 = SHA256.Create()) + { + byte[] hashBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(combined)); + return BitConverter.ToString(hashBytes).Replace("-", "").ToLowerInvariant(); + } + } + + /// + /// Lizenz-Cache speichern + /// + private async Task SaveLicenseCacheAsync() + { + var cacheData = new + { + license_key = licenseKey, + activation_id = activationId, + expires_at = expiresAt?.ToString("O"), + hardware_hash = GetHardwareHash(), + last_verified = DateTime.UtcNow.ToString("O") + }; + + string json = JsonSerializer.Serialize(cacheData, new JsonSerializerOptions { WriteIndented = true }); + await File.WriteAllTextAsync(cacheFilePath, json); + } + + /// + /// Lizenz-Cache laden + /// + private async Task LoadLicenseCacheAsync() + { + if (!File.Exists(cacheFilePath)) + return null; + + try + { + string json = await File.ReadAllTextAsync(cacheFilePath); + return JsonSerializer.Deserialize(json); + } + catch + { + return null; + } + } + + /// + /// Lizenz aktivieren + /// + public async Task<(bool Success, string Message)> ActivateLicenseAsync(string licenseKey) + { + await semaphore.WaitAsync(); + try + { + var requestData = new + { + license_key = licenseKey, + machine_id = GetMachineId(), + hardware_hash = GetHardwareHash(), + os_info = new + { + os = "Windows", + version = Environment.OSVersion.Version.ToString(), + platform = Environment.OSVersion.Platform.ToString(), + service_pack = Environment.OSVersion.ServicePack + }, + app_version = appVersion + }; + + var json = JsonSerializer.Serialize(requestData); + var content = new StringContent(json, Encoding.UTF8, "application/json"); + + var response = await httpClient.PostAsync($"{serverUrl}/api/license/activate", content); + var responseContent = await response.Content.ReadAsStringAsync(); + + if (response.IsSuccessStatusCode) + { + var result = JsonSerializer.Deserialize(responseContent); + + if (result.success) + { + this.licenseKey = licenseKey; + this.activationId = result.activation_id; + this.isValid = true; + + if (!string.IsNullOrEmpty(result.expires_at)) + this.expiresAt = DateTime.Parse(result.expires_at); + + await SaveLicenseCacheAsync(); + StartHeartbeat(); + + return (true, result.message ?? "Lizenz erfolgreich aktiviert"); + } + else + { + return (false, result.message ?? "Aktivierung fehlgeschlagen"); + } + } + else + { + return (false, $"Server-Fehler: {response.StatusCode}"); + } + } + catch (HttpRequestException ex) + { + return (false, $"Verbindungsfehler: {ex.Message}"); + } + catch (Exception ex) + { + return (false, $"Fehler: {ex.Message}"); + } + finally + { + semaphore.Release(); + } + } + + /// + /// Lizenz verifizieren (Heartbeat) + /// + public async Task<(bool Valid, string Message)> VerifyLicenseAsync() + { + if (string.IsNullOrEmpty(licenseKey) || !activationId.HasValue) + return (false, "Keine aktive Lizenz"); + + await semaphore.WaitAsync(); + try + { + var requestData = new + { + license_key = licenseKey, + machine_id = GetMachineId(), + hardware_hash = GetHardwareHash(), + activation_id = activationId.Value + }; + + var json = JsonSerializer.Serialize(requestData); + var content = new StringContent(json, Encoding.UTF8, "application/json"); + + var response = await httpClient.PostAsync($"{serverUrl}/api/license/verify", content); + var responseContent = await response.Content.ReadAsStringAsync(); + + if (response.IsSuccessStatusCode) + { + var result = JsonSerializer.Deserialize(responseContent); + isValid = result.valid; + + if (isValid) + { + await SaveLicenseCacheAsync(); + + if (result.requires_update) + { + OnUpdateAvailable?.Invoke(result.update_url); + } + } + + return (isValid, result.message ?? ""); + } + else + { + return (false, $"Server-Fehler: {response.StatusCode}"); + } + } + catch (HttpRequestException) + { + // Offline-Verifizierung + return await VerifyOfflineAsync(); + } + catch (Exception ex) + { + return (false, $"Fehler: {ex.Message}"); + } + finally + { + semaphore.Release(); + } + } + + /// + /// Offline-Verifizierung mit Grace Period + /// + private async Task<(bool Valid, string Message)> VerifyOfflineAsync() + { + var cache = await LoadLicenseCacheAsync(); + if (cache == null) + return (false, "Keine gecachte Lizenz vorhanden"); + + // Hardware-Hash prüfen + if (cache.hardware_hash != GetHardwareHash()) + { + // Grace Period bei Hardware-Änderung + var lastVerified = DateTime.Parse(cache.last_verified); + var gracePeriod = TimeSpan.FromDays(7); + + if (DateTime.UtcNow - lastVerified > gracePeriod) + return (false, "Hardware geändert - Grace Period abgelaufen"); + } + + // Ablaufdatum prüfen + if (!string.IsNullOrEmpty(cache.expires_at)) + { + var expiresAt = DateTime.Parse(cache.expires_at); + if (DateTime.UtcNow > expiresAt) + return (false, "Lizenz abgelaufen"); + } + + licenseKey = cache.license_key; + activationId = cache.activation_id; + isValid = true; + + return (true, "Offline-Modus (gecachte Lizenz)"); + } + + /// + /// Nach Updates suchen + /// + public async Task CheckForUpdatesAsync() + { + if (string.IsNullOrEmpty(licenseKey)) + return null; + + try + { + var requestData = new + { + current_version = appVersion, + license_key = licenseKey + }; + + var json = JsonSerializer.Serialize(requestData); + var content = new StringContent(json, Encoding.UTF8, "application/json"); + + var response = await httpClient.PostAsync($"{serverUrl}/api/version/check", content); + + if (response.IsSuccessStatusCode) + { + var responseContent = await response.Content.ReadAsStringAsync(); + return JsonSerializer.Deserialize(responseContent); + } + } + catch { } + + return null; + } + + /// + /// Heartbeat starten + /// + private void StartHeartbeat() + { + heartbeatTimer?.Dispose(); + + // Alle 15 Minuten + heartbeatTimer = new Timer(async _ => + { + var (valid, message) = await VerifyLicenseAsync(); + if (!valid) + { + OnLicenseInvalid?.Invoke(message); + } + }, null, TimeSpan.FromMinutes(15), TimeSpan.FromMinutes(15)); + } + + /// + /// Lizenz-Informationen abrufen + /// + public LicenseInfo GetLicenseInfo() + { + return new LicenseInfo + { + IsValid = isValid, + LicenseKey = string.IsNullOrEmpty(licenseKey) ? null : licenseKey.Substring(0, 4) + "****", + ExpiresAt = expiresAt, + MachineId = GetMachineId() + }; + } + + // Events + public event Action OnUpdateAvailable; + public event Action OnLicenseInvalid; + + public void Dispose() + { + heartbeatTimer?.Dispose(); + httpClient?.Dispose(); + semaphore?.Dispose(); + } + } + + // Hilfsklassen + public class LicenseCache + { + public string license_key { get; set; } + public int? activation_id { get; set; } + public string expires_at { get; set; } + public string hardware_hash { get; set; } + public string last_verified { get; set; } + } + + public class ActivationResponse + { + public bool success { get; set; } + public string message { get; set; } + public int? activation_id { get; set; } + public string expires_at { get; set; } + } + + public class VerificationResponse + { + public bool valid { get; set; } + public string message { get; set; } + public string expires_at { get; set; } + public bool requires_update { get; set; } + public string update_url { get; set; } + } + + public class UpdateInfo + { + public string latest_version { get; set; } + public string current_version { get; set; } + public bool update_available { get; set; } + public bool is_mandatory { get; set; } + public string download_url { get; set; } + public string release_notes { get; set; } + } + + public class LicenseInfo + { + public bool IsValid { get; set; } + public string LicenseKey { get; set; } + public DateTime? ExpiresAt { get; set; } + public string MachineId { get; set; } + } + + // Beispiel-Anwendung + class Program + { + static async Task Main(string[] args) + { + // API-Key aus Umgebungsvariable oder Konfiguration + string apiKey = Environment.GetEnvironmentVariable("LICENSE_API_KEY") ?? "your-api-key-here"; + + using (var licenseManager = new LicenseManager(apiKey, "1.0.0")) + { + // Event-Handler registrieren + licenseManager.OnUpdateAvailable += url => Console.WriteLine($"Update verfügbar: {url}"); + licenseManager.OnLicenseInvalid += msg => Console.WriteLine($"Lizenz ungültig: {msg}"); + + // Lizenz prüfen/aktivieren + var cache = await licenseManager.LoadLicenseCacheAsync(); + + if (cache != null) + { + Console.WriteLine("Gecachte Lizenz gefunden, verifiziere..."); + var (valid, message) = await licenseManager.VerifyLicenseAsync(); + + if (!valid) + { + Console.WriteLine($"Lizenz ungültig: {message}"); + await ActivateNewLicense(licenseManager); + } + else + { + Console.WriteLine($"✓ Lizenz gültig: {message}"); + } + } + else + { + await ActivateNewLicense(licenseManager); + } + + // Update-Check + var updateInfo = await licenseManager.CheckForUpdatesAsync(); + if (updateInfo?.update_available == true) + { + Console.WriteLine($"Update verfügbar: {updateInfo.latest_version}"); + if (updateInfo.is_mandatory) + Console.WriteLine("⚠️ Dies ist ein Pflicht-Update!"); + } + + // Lizenz-Info anzeigen + var info = licenseManager.GetLicenseInfo(); + Console.WriteLine($"\nLizenz-Status:"); + Console.WriteLine($"- Gültig: {info.IsValid}"); + Console.WriteLine($"- Ablauf: {info.ExpiresAt}"); + Console.WriteLine($"- Maschine: {info.MachineId}"); + + // App läuft... + Console.WriteLine("\n✓ Anwendung gestartet"); + Console.WriteLine("Drücken Sie eine Taste zum Beenden..."); + Console.ReadKey(); + } + } + + static async Task ActivateNewLicense(LicenseManager licenseManager) + { + Console.Write("Bitte Lizenzschlüssel eingeben: "); + string licenseKey = Console.ReadLine(); + + var (success, message) = await licenseManager.ActivateLicenseAsync(licenseKey); + + if (success) + { + Console.WriteLine($"✓ {message}"); + } + else + { + Console.WriteLine($"✗ {message}"); + Environment.Exit(1); + } + } + } +} \ No newline at end of file diff --git a/v2_lizenzserver/client_examples/python_client.py b/v2_lizenzserver/client_examples/python_client.py new file mode 100644 index 0000000..81c3138 --- /dev/null +++ b/v2_lizenzserver/client_examples/python_client.py @@ -0,0 +1,331 @@ +#!/usr/bin/env python3 +""" +Vollständiges Beispiel für die Integration des Lizenzservers in eine Python-Anwendung +""" + +import requests +import hashlib +import platform +import uuid +import json +import os +import time +import threading +from datetime import datetime, timedelta +from pathlib import Path + +class LicenseManager: + def __init__(self, api_key, app_version="1.0.0"): + self.api_key = api_key + self.app_version = app_version + self.server_url = "https://api-software-undso.z5m7q9dk3ah2v1plx6ju.com" + self.headers = {"Authorization": f"Bearer {api_key}"} + + # Cache-Verzeichnis + self.cache_dir = Path.home() / ".myapp" / "license" + self.cache_dir.mkdir(parents=True, exist_ok=True) + self.cache_file = self.cache_dir / "license.json" + + # Lizenz-Status + self.license_key = None + self.activation_id = None + self.is_valid = False + self.expires_at = None + + # Heartbeat Thread + self.heartbeat_thread = None + self.stop_heartbeat = False + + def get_machine_id(self): + """Eindeutige Maschinen-ID basierend auf MAC-Adresse""" + mac = uuid.getnode() + return f"MAC-{mac:012X}" + + def get_hardware_hash(self): + """Hardware-Fingerprint aus verschiedenen Systeminfos""" + components = [ + self.get_machine_id(), + platform.processor(), + platform.system(), + platform.machine(), + platform.node() + ] + + combined = "-".join(components) + return hashlib.sha256(combined.encode()).hexdigest() + + def save_license_cache(self): + """Lizenzinfo lokal speichern für Offline-Betrieb""" + cache_data = { + "license_key": self.license_key, + "activation_id": self.activation_id, + "expires_at": self.expires_at.isoformat() if self.expires_at else None, + "hardware_hash": self.get_hardware_hash(), + "last_verified": datetime.now().isoformat() + } + + with open(self.cache_file, 'w') as f: + json.dump(cache_data, f) + + def load_license_cache(self): + """Gespeicherte Lizenz laden""" + if not self.cache_file.exists(): + return None + + try: + with open(self.cache_file, 'r') as f: + return json.load(f) + except: + return None + + def activate_license(self, license_key): + """Neue Lizenz aktivieren""" + data = { + "license_key": license_key, + "machine_id": self.get_machine_id(), + "hardware_hash": self.get_hardware_hash(), + "os_info": { + "os": platform.system(), + "version": platform.version(), + "release": platform.release(), + "machine": platform.machine() + }, + "app_version": self.app_version + } + + try: + response = requests.post( + f"{self.server_url}/api/license/activate", + headers=self.headers, + json=data, + timeout=10, + verify=True + ) + + if response.status_code == 200: + result = response.json() + if result.get("success"): + self.license_key = license_key + self.activation_id = result.get("activation_id") + self.is_valid = True + + if result.get("expires_at"): + self.expires_at = datetime.fromisoformat( + result["expires_at"].replace("Z", "+00:00") + ) + + self.save_license_cache() + self.start_heartbeat() + + return True, result.get("message", "Lizenz aktiviert") + else: + return False, result.get("message", "Aktivierung fehlgeschlagen") + else: + return False, f"Server-Fehler: {response.status_code}" + + except requests.exceptions.RequestException as e: + return False, f"Verbindungsfehler: {str(e)}" + + def verify_license(self): + """Lizenz verifizieren (Heartbeat)""" + if not self.license_key or not self.activation_id: + return False, "Keine aktive Lizenz" + + data = { + "license_key": self.license_key, + "machine_id": self.get_machine_id(), + "hardware_hash": self.get_hardware_hash(), + "activation_id": self.activation_id + } + + try: + response = requests.post( + f"{self.server_url}/api/license/verify", + headers=self.headers, + json=data, + timeout=10, + verify=True + ) + + if response.status_code == 200: + result = response.json() + self.is_valid = result.get("valid", False) + + if self.is_valid: + self.save_license_cache() + + # Update-Check + if result.get("requires_update"): + print(f"Update verfügbar: {result.get('update_url')}") + + return self.is_valid, result.get("message", "") + else: + return False, f"Server-Fehler: {response.status_code}" + + except requests.exceptions.RequestException: + # Offline-Modus: Cache prüfen + return self.verify_offline() + + def verify_offline(self): + """Offline-Verifizierung mit Grace Period""" + cache = self.load_license_cache() + if not cache: + return False, "Keine gecachte Lizenz vorhanden" + + # Hardware-Hash prüfen + if cache.get("hardware_hash") != self.get_hardware_hash(): + # Grace Period bei Hardware-Änderung + last_verified = datetime.fromisoformat(cache.get("last_verified")) + grace_period = timedelta(days=7) + + if datetime.now() - last_verified > grace_period: + return False, "Hardware geändert - Grace Period abgelaufen" + + # Ablaufdatum prüfen + if cache.get("expires_at"): + expires_at = datetime.fromisoformat(cache.get("expires_at")) + if datetime.now() > expires_at: + return False, "Lizenz abgelaufen" + + self.license_key = cache.get("license_key") + self.activation_id = cache.get("activation_id") + self.is_valid = True + + return True, "Offline-Modus (gecachte Lizenz)" + + def check_for_updates(self): + """Nach Updates suchen""" + if not self.license_key: + return None + + data = { + "current_version": self.app_version, + "license_key": self.license_key + } + + try: + response = requests.post( + f"{self.server_url}/api/version/check", + headers=self.headers, + json=data, + timeout=10, + verify=True + ) + + if response.status_code == 200: + return response.json() + + except: + pass + + return None + + def heartbeat_worker(self): + """Background-Thread für regelmäßige Lizenzprüfung""" + while not self.stop_heartbeat: + time.sleep(900) # 15 Minuten + + if self.stop_heartbeat: + break + + valid, message = self.verify_license() + if not valid: + print(f"Lizenz-Warnung: {message}") + # Hier könnte die App reagieren (z.B. Features deaktivieren) + + def start_heartbeat(self): + """Heartbeat-Thread starten""" + if self.heartbeat_thread and self.heartbeat_thread.is_alive(): + return + + self.stop_heartbeat = False + self.heartbeat_thread = threading.Thread( + target=self.heartbeat_worker, + daemon=True + ) + self.heartbeat_thread.start() + + def stop_heartbeat_thread(self): + """Heartbeat-Thread beenden""" + self.stop_heartbeat = True + if self.heartbeat_thread: + self.heartbeat_thread.join(timeout=1) + + def get_license_info(self): + """Aktuelle Lizenzinformationen""" + return { + "valid": self.is_valid, + "license_key": self.license_key[:4] + "****" if self.license_key else None, + "expires_at": self.expires_at.isoformat() if self.expires_at else None, + "machine_id": self.get_machine_id() + } + + +# Beispiel-Anwendung +def main(): + # API-Key sollte sicher gespeichert werden (z.B. verschlüsselt) + API_KEY = os.environ.get("LICENSE_API_KEY", "your-api-key-here") + + # License Manager initialisieren + license_mgr = LicenseManager(API_KEY, app_version="1.0.0") + + # Versuche gecachte Lizenz zu laden + cache = license_mgr.load_license_cache() + if cache: + print("Gecachte Lizenz gefunden, verifiziere...") + valid, message = license_mgr.verify_license() + + if valid: + print(f"✓ Lizenz gültig: {message}") + else: + print(f"✗ Lizenz ungültig: {message}") + # Neue Lizenz erforderlich + license_key = input("Bitte Lizenzschlüssel eingeben: ") + success, message = license_mgr.activate_license(license_key) + + if success: + print(f"✓ {message}") + else: + print(f"✗ {message}") + return + else: + # Erste Aktivierung + print("Keine Lizenz gefunden.") + license_key = input("Bitte Lizenzschlüssel eingeben: ") + success, message = license_mgr.activate_license(license_key) + + if success: + print(f"✓ {message}") + else: + print(f"✗ {message}") + return + + # Update-Check + print("\nPrüfe auf Updates...") + update_info = license_mgr.check_for_updates() + if update_info and update_info.get("update_available"): + print(f"Update verfügbar: {update_info.get('latest_version')}") + if update_info.get("is_mandatory"): + print("⚠️ Dies ist ein Pflicht-Update!") + + # Lizenzinfo anzeigen + info = license_mgr.get_license_info() + print(f"\nLizenz-Status:") + print(f"- Gültig: {info['valid']}") + print(f"- Ablauf: {info['expires_at']}") + print(f"- Maschine: {info['machine_id']}") + + # App läuft... + print("\n✓ Anwendung gestartet mit gültiger Lizenz") + + try: + # Simuliere App-Laufzeit + while True: + time.sleep(1) + except KeyboardInterrupt: + print("\nBeende Anwendung...") + license_mgr.stop_heartbeat_thread() + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/v2_lizenzserver/docker-compose.yml b/v2_lizenzserver/docker-compose.yml new file mode 100644 index 0000000..71a6c86 --- /dev/null +++ b/v2_lizenzserver/docker-compose.yml @@ -0,0 +1,48 @@ +version: '3.8' + +services: + license-server: + build: . + container_name: license-server + restart: unless-stopped + ports: + - "8443:8443" + environment: + - DATABASE_URL=postgresql://license_user:license_password@db:5432/license_db + - REDIS_URL=redis://redis:6379 + depends_on: + - db + - redis + networks: + - license-network + + db: + image: postgres:15-alpine + container_name: license-db + restart: unless-stopped + environment: + POSTGRES_USER: license_user + POSTGRES_PASSWORD: license_password + POSTGRES_DB: license_db + volumes: + - postgres_data:/var/lib/postgresql/data + networks: + - license-network + + redis: + image: redis:7-alpine + container_name: license-redis + restart: unless-stopped + command: redis-server --appendonly yes + volumes: + - redis_data:/data + networks: + - license-network + +volumes: + postgres_data: + redis_data: + +networks: + license-network: + driver: bridge \ No newline at end of file diff --git a/v2_lizenzserver/init_db.py b/v2_lizenzserver/init_db.py new file mode 100644 index 0000000..9f65279 --- /dev/null +++ b/v2_lizenzserver/init_db.py @@ -0,0 +1,44 @@ +import sys +sys.path.append('/app') + +from app.db.database import engine, Base +from app.models.models import License, Activation, Version, ApiKey +from sqlalchemy.orm import Session +import uuid +from datetime import datetime, timedelta + +print("Creating database tables...") +Base.metadata.create_all(bind=engine) + +with Session(engine) as db: + # Create a test API key + api_key = ApiKey( + key="test-api-key-12345", + name="Test API Key", + is_active=True + ) + db.add(api_key) + + # Create a test license + test_license = License( + license_key="TEST-LICENSE-KEY-12345", + product_id="software-v1", + customer_email="test@example.com", + customer_name="Test Customer", + max_activations=5, + expires_at=datetime.utcnow() + timedelta(days=365) + ) + db.add(test_license) + + # Create initial version + initial_version = Version( + version_number="1.0.0", + release_notes="Initial release", + is_mandatory=False + ) + db.add(initial_version) + + db.commit() + print("Database initialized successfully!") + print(f"Test API Key: test-api-key-12345") + print(f"Test License Key: TEST-LICENSE-KEY-12345") \ No newline at end of file diff --git a/v2_lizenzserver/requirements.txt b/v2_lizenzserver/requirements.txt new file mode 100644 index 0000000..f8a3865 --- /dev/null +++ b/v2_lizenzserver/requirements.txt @@ -0,0 +1,14 @@ +fastapi==0.104.1 +uvicorn[standard]==0.24.0 +sqlalchemy==2.0.23 +psycopg2-binary==2.9.9 +python-jose[cryptography]==3.3.0 +passlib[bcrypt]==1.7.4 +python-multipart==0.0.6 +pydantic==2.5.0 +pydantic-settings==2.1.0 +alembic==1.12.1 +python-dotenv==1.0.0 +httpx==0.25.2 +redis==5.0.1 +packaging==23.2 \ No newline at end of file diff --git a/v2_lizenzserver/test_api.py b/v2_lizenzserver/test_api.py new file mode 100644 index 0000000..ed877d4 --- /dev/null +++ b/v2_lizenzserver/test_api.py @@ -0,0 +1,39 @@ +import httpx +import asyncio + +API_URL = "https://api-software-undso.z5m7q9dk3ah2v1plx6ju.com" +API_KEY = "test-api-key-12345" + +async def test_license_server(): + async with httpx.AsyncClient(verify=False) as client: + # Test root endpoint + print("Testing root endpoint...") + response = await client.get(f"{API_URL}/") + print(f"Status: {response.status_code}") + print(f"Response: {response.json()}\n") + + # Test health endpoint + print("Testing health endpoint...") + response = await client.get(f"{API_URL}/health") + print(f"Status: {response.status_code}") + print(f"Response: {response.json()}\n") + + # Test license activation (will fail without valid API key) + print("Testing license activation...") + headers = {"Authorization": f"Bearer {API_KEY}"} + data = { + "license_key": "TEST-LICENSE-KEY-12345", + "machine_id": "MACHINE001", + "hardware_hash": "abc123def456", + "app_version": "1.0.0" + } + response = await client.post( + f"{API_URL}/api/license/activate", + headers=headers, + json=data + ) + print(f"Status: {response.status_code}") + print(f"Response: {response.json()}\n") + +if __name__ == "__main__": + asyncio.run(test_license_server()) \ No newline at end of file diff --git a/v2_lizenzserver_backup/Dockerfile b/v2_lizenzserver_backup/Dockerfile new file mode 100644 index 0000000..69b8d95 --- /dev/null +++ b/v2_lizenzserver_backup/Dockerfile @@ -0,0 +1,18 @@ +FROM python:3.11-slim + +# Zeitzone setzen +ENV TZ=Europe/Berlin +ENV DEBIAN_FRONTEND=noninteractive + +RUN apt-get update && apt-get install -y tzdata \ + && ln -sf /usr/share/zoneinfo/Europe/Berlin /etc/localtime \ + && echo "Europe/Berlin" > /etc/timezone \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +# Placeholder für Lizenzserver +RUN echo "Lizenzserver noch nicht implementiert" > info.txt + +CMD ["python", "-c", "print('Lizenzserver Container läuft, aber noch keine Implementierung vorhanden'); import time; time.sleep(86400)"] \ No newline at end of file