dmart 1.4.40.post8__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dmart/__init__.py +7 -0
- dmart/alembic/README +1 -0
- dmart/alembic/__init__.py +0 -0
- dmart/alembic/env.py +91 -0
- dmart/alembic/notes.txt +11 -0
- dmart/alembic/script.py.mako +28 -0
- dmart/alembic/scripts/__init__.py +0 -0
- dmart/alembic/scripts/calculate_checksums.py +77 -0
- dmart/alembic/scripts/migration_f7a4949eed19.py +28 -0
- dmart/alembic/versions/0f3d2b1a7c21_add_authz_materialized_views.py +87 -0
- dmart/alembic/versions/10d2041b94d4_last_checksum_history.py +62 -0
- dmart/alembic/versions/1cf4e1ee3cb8_ext_permission_with_filter_fields_values.py +33 -0
- dmart/alembic/versions/26bfe19b49d4_rm_failedloginattempts.py +42 -0
- dmart/alembic/versions/3c8bca2219cc_add_otp_table.py +38 -0
- dmart/alembic/versions/6675fd9dfe42_remove_unique_from_sessions_table.py +36 -0
- dmart/alembic/versions/71bc1df82e6a_adding_user_last_login_at.py +43 -0
- dmart/alembic/versions/74288ccbd3b5_initial.py +264 -0
- dmart/alembic/versions/7520a89a8467_rm_activesession_table.py +39 -0
- dmart/alembic/versions/848b623755a4_make_created_nd_updated_at_required.py +138 -0
- dmart/alembic/versions/8640dcbebf85_add_notes_to_users.py +32 -0
- dmart/alembic/versions/91c94250232a_adding_fk_on_owner_shortname.py +104 -0
- dmart/alembic/versions/98ecd6f56f9a_ext_meta_with_owner_group_shortname.py +66 -0
- dmart/alembic/versions/9aae9138c4ef_indexing_created_at_updated_at.py +80 -0
- dmart/alembic/versions/__init__.py +0 -0
- dmart/alembic/versions/b53f916b3f6d_json_to_jsonb.py +492 -0
- dmart/alembic/versions/eb5f1ec65156_adding_user_locked_to_device.py +36 -0
- dmart/alembic/versions/f7a4949eed19_adding_query_policies_to_meta.py +60 -0
- dmart/alembic.ini +117 -0
- dmart/api/__init__.py +0 -0
- dmart/api/info/__init__.py +0 -0
- dmart/api/info/router.py +109 -0
- dmart/api/managed/__init__.py +0 -0
- dmart/api/managed/router.py +1541 -0
- dmart/api/managed/utils.py +1879 -0
- dmart/api/public/__init__.py +0 -0
- dmart/api/public/router.py +758 -0
- dmart/api/qr/__init__.py +0 -0
- dmart/api/qr/router.py +108 -0
- dmart/api/user/__init__.py +0 -0
- dmart/api/user/model/__init__.py +0 -0
- dmart/api/user/model/errors.py +14 -0
- dmart/api/user/model/requests.py +165 -0
- dmart/api/user/model/responses.py +11 -0
- dmart/api/user/router.py +1413 -0
- dmart/api/user/service.py +270 -0
- dmart/bundler.py +52 -0
- dmart/cli.py +1133 -0
- dmart/config/__init__.py +0 -0
- dmart/config/channels.json +11 -0
- dmart/config/notification.json +17 -0
- dmart/config.env.sample +27 -0
- dmart/config.ini.sample +7 -0
- dmart/conftest.py +13 -0
- dmart/curl.sh +196 -0
- dmart/cxb/__init__.py +0 -0
- dmart/cxb/assets/@codemirror-Rn7_6DkE.js +10 -0
- dmart/cxb/assets/@edraj-CS4NwVbD.js +1 -0
- dmart/cxb/assets/@floating-ui-BwwcF-xh.js +1 -0
- dmart/cxb/assets/@formatjs-yKEsAtjs.js +1 -0
- dmart/cxb/assets/@fortawesome-DRW1UCdr.js +9 -0
- dmart/cxb/assets/@jsonquerylang-laKNoFFq.js +12 -0
- dmart/cxb/assets/@lezer-za4Q-8Ew.js +1 -0
- dmart/cxb/assets/@marijn-DXwl3gUT.js +1 -0
- dmart/cxb/assets/@popperjs-l0sNRNKZ.js +1 -0
- dmart/cxb/assets/@replit--ERk53eB.js +1 -0
- dmart/cxb/assets/@roxi-CGMFK4i8.js +6 -0
- dmart/cxb/assets/@typewriter-cCzskkIv.js +17 -0
- dmart/cxb/assets/@zerodevx-BlBZjKxu.js +1 -0
- dmart/cxb/assets/@zerodevx-CVEpe6WZ.css +1 -0
- dmart/cxb/assets/BreadCrumbLite-DAhOx38v.js +1 -0
- dmart/cxb/assets/EntryRenderer-CCqV8Rkg.js +32 -0
- dmart/cxb/assets/EntryRenderer-DXytdFp9.css +1 -0
- dmart/cxb/assets/ListView-BQelo7vZ.js +16 -0
- dmart/cxb/assets/ListView-U8of-_c-.css +1 -0
- dmart/cxb/assets/Prism--hMplq-p.js +3 -0
- dmart/cxb/assets/Prism-Uh6uStUw.css +1 -0
- dmart/cxb/assets/Table2Cols-BsbwicQm.js +1 -0
- dmart/cxb/assets/_..-BvT6vdHa.css +1 -0
- dmart/cxb/assets/_...404_-fuLH_rX9.js +2 -0
- dmart/cxb/assets/_...fallback_-Ba_NLmAE.js +1 -0
- dmart/cxb/assets/_module-3HrtKAWo.js +3 -0
- dmart/cxb/assets/_module-DFKFq0AM.js +4 -0
- dmart/cxb/assets/_module-Dgq0ZVtz.js +1 -0
- dmart/cxb/assets/ajv-Cpj98o6Y.js +1 -0
- dmart/cxb/assets/axios-CG2WSiiR.js +6 -0
- dmart/cxb/assets/clsx-B-dksMZM.js +1 -0
- dmart/cxb/assets/codemirror-wrapped-line-indent-DPhKvljI.js +1 -0
- dmart/cxb/assets/compare-C3AjiGFR.js +1 -0
- dmart/cxb/assets/compute-scroll-into-view-Bl8rNFhg.js +1 -0
- dmart/cxb/assets/consolite-DlCuI0F9.js +1 -0
- dmart/cxb/assets/crelt-C8TCjufn.js +1 -0
- dmart/cxb/assets/date-fns-l0sNRNKZ.js +1 -0
- dmart/cxb/assets/deepmerge-rn4rBaHU.js +1 -0
- dmart/cxb/assets/dmart_services-AL6-IdDE.js +1 -0
- dmart/cxb/assets/downloadFile-D08i0YDh.js +1 -0
- dmart/cxb/assets/easy-signal-BiPFIK3O.js +1 -0
- dmart/cxb/assets/esm-env-rsSWfq8L.js +1 -0
- dmart/cxb/assets/export-OF_rTiXu.js +1 -0
- dmart/cxb/assets/fast-deep-equal-l0sNRNKZ.js +1 -0
- dmart/cxb/assets/fast-diff-C-IidNf4.js +1 -0
- dmart/cxb/assets/fast-uri-l0sNRNKZ.js +1 -0
- dmart/cxb/assets/flowbite-svelte-BLvjb-sa.js +1 -0
- dmart/cxb/assets/flowbite-svelte-CD54FDqW.css +1 -0
- dmart/cxb/assets/flowbite-svelte-icons-BI8GVhw_.js +1 -0
- dmart/cxb/assets/github-slugger-CQ4oX9Ud.js +1 -0
- dmart/cxb/assets/global-igKv-1g9.js +1 -0
- dmart/cxb/assets/hookar-BMRD9G9H.js +1 -0
- dmart/cxb/assets/immutable-json-patch-DtRO2E_S.js +1 -0
- dmart/cxb/assets/import-1vE3gBat.js +1 -0
- dmart/cxb/assets/index-B-eTh-ZX.js +1 -0
- dmart/cxb/assets/index-BSsK-X71.js +1 -0
- dmart/cxb/assets/index-BVyxzKtH.js +1 -0
- dmart/cxb/assets/index-BdeNM69f.js +1 -0
- dmart/cxb/assets/index-CC-A1ipE.js +1 -0
- dmart/cxb/assets/index-CQohGiYB.js +1 -0
- dmart/cxb/assets/index-ChjnkpdZ.js +4 -0
- dmart/cxb/assets/index-DLP7csA4.js +1 -0
- dmart/cxb/assets/index-DTfhnhwd.js +1 -0
- dmart/cxb/assets/index-DdXRK7n9.js +2 -0
- dmart/cxb/assets/index-DtiCmB4o.js +1 -0
- dmart/cxb/assets/index-NBrXBlLA.css +2 -0
- dmart/cxb/assets/index-X1uNehO7.js +1 -0
- dmart/cxb/assets/index-nrQW6Nrr.js +1 -0
- dmart/cxb/assets/info-B986lRiM.js +1 -0
- dmart/cxb/assets/intl-messageformat-Dc5UU-HB.js +3 -0
- dmart/cxb/assets/jmespath-l0sNRNKZ.js +1 -0
- dmart/cxb/assets/json-schema-traverse-l0sNRNKZ.js +1 -0
- dmart/cxb/assets/json-source-map-DRgZidqy.js +5 -0
- dmart/cxb/assets/jsonpath-plus-l0sNRNKZ.js +1 -0
- dmart/cxb/assets/jsonrepair-B30Dx381.js +8 -0
- dmart/cxb/assets/lodash-es-DZVAA2ox.js +1 -0
- dmart/cxb/assets/marked-DKjyhwJX.js +56 -0
- dmart/cxb/assets/marked-gfm-heading-id-U5zO829x.js +2 -0
- dmart/cxb/assets/marked-mangle-CDMeiHC6.js +1 -0
- dmart/cxb/assets/memoize-one-BdPwpGay.js +1 -0
- dmart/cxb/assets/natural-compare-lite-Bg2Xcf-o.js +7 -0
- dmart/cxb/assets/pagination-svelte-D5CyoiE_.js +13 -0
- dmart/cxb/assets/pagination-svelte-v10nAbbM.css +1 -0
- dmart/cxb/assets/plantuml-encoder-C47mzt9T.js +1 -0
- dmart/cxb/assets/prismjs-DTUiLGJu.js +9 -0
- dmart/cxb/assets/profile-BUf-tKMe.js +1 -0
- dmart/cxb/assets/query-CNmXTsgf.js +1 -0
- dmart/cxb/assets/queryHelpers-C9iBWwqe.js +1 -0
- dmart/cxb/assets/scroll-into-view-if-needed-KR58zyjF.js +1 -0
- dmart/cxb/assets/spaces-0oyGvpii.js +1 -0
- dmart/cxb/assets/style-mod-Bs6eFhZE.js +3 -0
- dmart/cxb/assets/svelte-B2XmcTi_.js +4 -0
- dmart/cxb/assets/svelte-awesome-COLlx0DN.css +1 -0
- dmart/cxb/assets/svelte-awesome-DhnMA6Q_.js +1 -0
- dmart/cxb/assets/svelte-datatables-net-CY7LBj6I.js +1 -0
- dmart/cxb/assets/svelte-floating-ui-BlS3sOAQ.js +1 -0
- dmart/cxb/assets/svelte-i18n-CT2KkQaN.js +3 -0
- dmart/cxb/assets/svelte-jsoneditor-BzfX6Usi.css +1 -0
- dmart/cxb/assets/svelte-jsoneditor-CUGSvWId.js +25 -0
- dmart/cxb/assets/svelte-select-CegQKzqH.css +1 -0
- dmart/cxb/assets/svelte-select-CjHAt_85.js +6 -0
- dmart/cxb/assets/tailwind-merge-CJvxXMcu.js +1 -0
- dmart/cxb/assets/tailwind-variants-Cj20BoQ3.js +1 -0
- dmart/cxb/assets/toast-B9WDyfyI.js +1 -0
- dmart/cxb/assets/tslib-pJfR_DrR.js +1 -0
- dmart/cxb/assets/typewriter-editor-DkTVIJdm.js +25 -0
- dmart/cxb/assets/user-DeK_NB5v.js +1 -0
- dmart/cxb/assets/vanilla-picker-l5rcX3cq.js +8 -0
- dmart/cxb/assets/w3c-keyname-Vcq4gwWv.js +1 -0
- dmart/cxb/config.json +11 -0
- dmart/cxb/config.sample.json +11 -0
- dmart/cxb/favicon.ico +0 -0
- dmart/cxb/favicon.png +0 -0
- dmart/cxb/index.html +28 -0
- dmart/data_adapters/__init__.py +0 -0
- dmart/data_adapters/adapter.py +16 -0
- dmart/data_adapters/base_data_adapter.py +467 -0
- dmart/data_adapters/file/__init__.py +0 -0
- dmart/data_adapters/file/adapter.py +2043 -0
- dmart/data_adapters/file/adapter_helpers.py +1013 -0
- dmart/data_adapters/file/archive.py +150 -0
- dmart/data_adapters/file/create_index.py +331 -0
- dmart/data_adapters/file/create_users_folders.py +52 -0
- dmart/data_adapters/file/custom_validations.py +68 -0
- dmart/data_adapters/file/drop_index.py +40 -0
- dmart/data_adapters/file/health_check.py +560 -0
- dmart/data_adapters/file/redis_services.py +1110 -0
- dmart/data_adapters/helpers.py +27 -0
- dmart/data_adapters/sql/__init__.py +0 -0
- dmart/data_adapters/sql/adapter.py +3218 -0
- dmart/data_adapters/sql/adapter_helpers.py +491 -0
- dmart/data_adapters/sql/create_tables.py +451 -0
- dmart/data_adapters/sql/create_users_folders.py +53 -0
- dmart/data_adapters/sql/db_to_json_migration.py +485 -0
- dmart/data_adapters/sql/health_check_sql.py +232 -0
- dmart/data_adapters/sql/json_to_db_migration.py +454 -0
- dmart/data_adapters/sql/update_query_policies.py +101 -0
- dmart/data_generator.py +81 -0
- dmart/dmart.py +761 -0
- dmart/get_settings.py +7 -0
- dmart/hypercorn_config.toml +3 -0
- dmart/info.json +1 -0
- dmart/languages/__init__.py +0 -0
- dmart/languages/arabic.json +15 -0
- dmart/languages/english.json +16 -0
- dmart/languages/kurdish.json +14 -0
- dmart/languages/loader.py +12 -0
- dmart/login_creds.sh +7 -0
- dmart/login_creds.sh.sample +7 -0
- dmart/main.py +563 -0
- dmart/manifest.sh +12 -0
- dmart/migrate.py +24 -0
- dmart/models/__init__.py +0 -0
- dmart/models/api.py +203 -0
- dmart/models/core.py +597 -0
- dmart/models/enums.py +255 -0
- dmart/password_gen.py +8 -0
- dmart/plugins/__init__.py +0 -0
- dmart/plugins/action_log/__init__.py +0 -0
- dmart/plugins/action_log/config.json +13 -0
- dmart/plugins/action_log/plugin.py +121 -0
- dmart/plugins/admin_notification_sender/__init__.py +0 -0
- dmart/plugins/admin_notification_sender/config.json +13 -0
- dmart/plugins/admin_notification_sender/plugin.py +124 -0
- dmart/plugins/ldap_manager/__init__.py +0 -0
- dmart/plugins/ldap_manager/config.json +12 -0
- dmart/plugins/ldap_manager/dmart.schema +146 -0
- dmart/plugins/ldap_manager/plugin.py +100 -0
- dmart/plugins/ldap_manager/slapd.conf +53 -0
- dmart/plugins/local_notification/__init__.py +0 -0
- dmart/plugins/local_notification/config.json +13 -0
- dmart/plugins/local_notification/plugin.py +123 -0
- dmart/plugins/realtime_updates_notifier/__init__.py +0 -0
- dmart/plugins/realtime_updates_notifier/config.json +12 -0
- dmart/plugins/realtime_updates_notifier/plugin.py +58 -0
- dmart/plugins/redis_db_update/__init__.py +0 -0
- dmart/plugins/redis_db_update/config.json +13 -0
- dmart/plugins/redis_db_update/plugin.py +188 -0
- dmart/plugins/resource_folders_creation/__init__.py +0 -0
- dmart/plugins/resource_folders_creation/config.json +12 -0
- dmart/plugins/resource_folders_creation/plugin.py +81 -0
- dmart/plugins/system_notification_sender/__init__.py +0 -0
- dmart/plugins/system_notification_sender/config.json +13 -0
- dmart/plugins/system_notification_sender/plugin.py +188 -0
- dmart/plugins/update_access_controls/__init__.py +0 -0
- dmart/plugins/update_access_controls/config.json +12 -0
- dmart/plugins/update_access_controls/plugin.py +9 -0
- dmart/publish.sh +57 -0
- dmart/pylint.sh +16 -0
- dmart/pyrightconfig.json +7 -0
- dmart/redis_connections.sh +13 -0
- dmart/reload.sh +56 -0
- dmart/run.sh +3 -0
- dmart/run_notification_campaign.py +85 -0
- dmart/sample/spaces/applications/.dm/meta.space.json +30 -0
- dmart/sample/spaces/applications/api/.dm/meta.folder.json +1 -0
- dmart/sample/spaces/applications/api/.dm/query_all_applications/meta.content.json +1 -0
- dmart/sample/spaces/applications/api/.dm/test_by_saad/attachments.media/meta.warframe.json +1 -0
- dmart/sample/spaces/applications/api/.dm/test_by_saad/attachments.media/warframe.png +0 -0
- dmart/sample/spaces/applications/api/.dm/test_by_saad/meta.content.json +1 -0
- dmart/sample/spaces/applications/api/.dm/user_profile/meta.content.json +1 -0
- dmart/sample/spaces/applications/api/applications/.dm/create_log/meta.content.json +1 -0
- dmart/sample/spaces/applications/api/applications/.dm/create_public_logs/meta.content.json +1 -0
- dmart/sample/spaces/applications/api/applications/.dm/meta.folder.json +1 -0
- dmart/sample/spaces/applications/api/applications/.dm/query_all_translated_data/meta.content.json +1 -0
- dmart/sample/spaces/applications/api/applications/.dm/query_logs/meta.content.json +1 -0
- dmart/sample/spaces/applications/api/applications/.dm/query_translated_enums/meta.content.json +1 -0
- dmart/sample/spaces/applications/api/applications/.dm/query_translated_others/meta.content.json +1 -0
- dmart/sample/spaces/applications/api/applications/.dm/query_translated_resolution/meta.content.json +1 -0
- dmart/sample/spaces/applications/api/applications/create_log.json +1 -0
- dmart/sample/spaces/applications/api/applications/create_public_logs.json +1 -0
- dmart/sample/spaces/applications/api/applications/query_all_translated_data.json +1 -0
- dmart/sample/spaces/applications/api/applications/query_logs.json +1 -0
- dmart/sample/spaces/applications/api/applications/query_translated_enums.json +1 -0
- dmart/sample/spaces/applications/api/applications/query_translated_others.json +1 -0
- dmart/sample/spaces/applications/api/applications/query_translated_resolution.json +1 -0
- dmart/sample/spaces/applications/api/applications.json +1 -0
- dmart/sample/spaces/applications/api/management/.dm/create_subaccount/meta.content.json +1 -0
- dmart/sample/spaces/applications/api/management/.dm/meta.folder.json +1 -0
- dmart/sample/spaces/applications/api/management/.dm/update_password/meta.content.json +1 -0
- dmart/sample/spaces/applications/api/management/create_subaccount.json +53 -0
- dmart/sample/spaces/applications/api/management/update_password.json +1 -0
- dmart/sample/spaces/applications/api/management.json +1 -0
- dmart/sample/spaces/applications/api/query_all_applications.json +15 -0
- dmart/sample/spaces/applications/api/test_by_saad.json +1 -0
- dmart/sample/spaces/applications/api/user/.dm/meta.folder.json +1 -0
- dmart/sample/spaces/applications/api/user/.dm/test_by_saad/meta.content.json +1 -0
- dmart/sample/spaces/applications/api/user/.dm/user_profile/meta.content.json +1 -0
- dmart/sample/spaces/applications/api/user/test_by_saad.json +1 -0
- dmart/sample/spaces/applications/api/user/user_profile.json +1 -0
- dmart/sample/spaces/applications/api/user_profile.json +1 -0
- dmart/sample/spaces/applications/api.json +1 -0
- dmart/sample/spaces/applications/collections/.dm/meta.folder.json +19 -0
- dmart/sample/spaces/applications/collections.json +1 -0
- dmart/sample/spaces/applications/configurations/.dm/meta.folder.json +1 -0
- dmart/sample/spaces/applications/configurations/time_out.json +1 -0
- dmart/sample/spaces/applications/configurations.json +19 -0
- dmart/sample/spaces/applications/errors.json +1 -0
- dmart/sample/spaces/applications/logs/.dm/meta.folder.json +1 -0
- dmart/sample/spaces/applications/logs.json +1 -0
- dmart/sample/spaces/applications/queries/.dm/meta.folder.json +1 -0
- dmart/sample/spaces/applications/queries/.dm/order/meta.content.json +1 -0
- dmart/sample/spaces/applications/queries/order.json +1 -0
- dmart/sample/spaces/applications/queries.json +1 -0
- dmart/sample/spaces/applications/schema/.dm/api/meta.schema.json +1 -0
- dmart/sample/spaces/applications/schema/.dm/configuration/meta.schema.json +1 -0
- dmart/sample/spaces/applications/schema/.dm/error/meta.schema.json +1 -0
- dmart/sample/spaces/applications/schema/.dm/log/meta.schema.json +1 -0
- dmart/sample/spaces/applications/schema/.dm/meta.folder.json +1 -0
- dmart/sample/spaces/applications/schema/.dm/query/meta.schema.json +16 -0
- dmart/sample/spaces/applications/schema/.dm/translation/meta.schema.json +1 -0
- dmart/sample/spaces/applications/schema/api.json +28 -0
- dmart/sample/spaces/applications/schema/configuration.json +1 -0
- dmart/sample/spaces/applications/schema/error.json +43 -0
- dmart/sample/spaces/applications/schema/log.json +1 -0
- dmart/sample/spaces/applications/schema/query.json +118 -0
- dmart/sample/spaces/applications/schema/translation.json +26 -0
- dmart/sample/spaces/applications/schema.json +1 -0
- dmart/sample/spaces/applications/translations/.dm/meta.folder.json +1 -0
- dmart/sample/spaces/applications/translations.json +1 -0
- dmart/sample/spaces/archive/.dm/meta.space.json +27 -0
- dmart/sample/spaces/custom_plugins/dummy/__pycache__/plugin.cpython-314.pyc +0 -0
- dmart/sample/spaces/custom_plugins/dummy/config.json +28 -0
- dmart/sample/spaces/custom_plugins/dummy/plugin.py +6 -0
- dmart/sample/spaces/custom_plugins/missed_entry/config.json +12 -0
- dmart/sample/spaces/custom_plugins/missed_entry/plugin.py +119 -0
- dmart/sample/spaces/custom_plugins/own_changed_notification/__pycache__/plugin.cpython-314.pyc +0 -0
- dmart/sample/spaces/custom_plugins/own_changed_notification/config.json +12 -0
- dmart/sample/spaces/custom_plugins/own_changed_notification/plugin.py +65 -0
- dmart/sample/spaces/custom_plugins/reports_stats/config.json +14 -0
- dmart/sample/spaces/custom_plugins/reports_stats/plugin.py +82 -0
- dmart/sample/spaces/custom_plugins/system_notification_sender/config.json +22 -0
- dmart/sample/spaces/custom_plugins/system_notification_sender/notification.py +268 -0
- dmart/sample/spaces/custom_plugins/system_notification_sender/plugin.py +98 -0
- dmart/sample/spaces/management/.dm/events.jsonl +32 -0
- dmart/sample/spaces/management/.dm/meta.space.json +48 -0
- dmart/sample/spaces/management/.dm/notifications/attachments.view.json/admin.json +36 -0
- dmart/sample/spaces/management/.dm/notifications/attachments.view.json/meta.admin.json +1 -0
- dmart/sample/spaces/management/.dm/notifications/attachments.view.json/meta.system.json +1 -0
- dmart/sample/spaces/management/.dm/notifications/attachments.view.json/system.json +32 -0
- dmart/sample/spaces/management/collections/.dm/meta.folder.json +1 -0
- dmart/sample/spaces/management/collections.json +1 -0
- dmart/sample/spaces/management/groups/.dm/meta.folder.json +1 -0
- dmart/sample/spaces/management/groups.json +1 -0
- dmart/sample/spaces/management/health_check/.dm/meta.folder.json +1 -0
- dmart/sample/spaces/management/health_check.json +1 -0
- dmart/sample/spaces/management/notifications/.dm/meta.folder.json +1 -0
- dmart/sample/spaces/management/notifications/admin/.dm/meta.folder.json +9 -0
- dmart/sample/spaces/management/notifications/system/.dm/meta.folder.json +9 -0
- dmart/sample/spaces/management/notifications.json +1 -0
- dmart/sample/spaces/management/permissions/.dm/access_applications/meta.permission.json +31 -0
- dmart/sample/spaces/management/permissions/.dm/access_applications_world/meta.permission.json +31 -0
- dmart/sample/spaces/management/permissions/.dm/access_messages/meta.permission.json +23 -0
- dmart/sample/spaces/management/permissions/.dm/access_personal/meta.permission.json +40 -0
- dmart/sample/spaces/management/permissions/.dm/access_protected/meta.permission.json +33 -0
- dmart/sample/spaces/management/permissions/.dm/access_public/meta.permission.json +24 -0
- dmart/sample/spaces/management/permissions/.dm/browse_all_folders/meta.permission.json +23 -0
- dmart/sample/spaces/management/permissions/.dm/create_log/meta.permission.json +24 -0
- dmart/sample/spaces/management/permissions/.dm/interviewer/meta.permission.json +1 -0
- dmart/sample/spaces/management/permissions/.dm/manage_applications/meta.permission.json +1 -0
- dmart/sample/spaces/management/permissions/.dm/manage_debug/meta.permission.json +25 -0
- dmart/sample/spaces/management/permissions/.dm/manage_spaces/meta.permission.json +24 -0
- dmart/sample/spaces/management/permissions/.dm/meta.folder.json +1 -0
- dmart/sample/spaces/management/permissions/.dm/rules_management_default/meta.permission.json +32 -0
- dmart/sample/spaces/management/permissions/.dm/super_manager/meta.permission.json +52 -0
- dmart/sample/spaces/management/permissions/.dm/view_activity_log/meta.permission.json +26 -0
- dmart/sample/spaces/management/permissions/.dm/view_collections/meta.permission.json +29 -0
- dmart/sample/spaces/management/permissions/.dm/view_logs/meta.permission.json +30 -0
- dmart/sample/spaces/management/permissions/.dm/view_roles/meta.permission.json +29 -0
- dmart/sample/spaces/management/permissions/.dm/view_users/meta.permission.json +25 -0
- dmart/sample/spaces/management/permissions/.dm/view_world/meta.permission.json +31 -0
- dmart/sample/spaces/management/permissions/.dm/world/meta.permission.json +35 -0
- dmart/sample/spaces/management/permissions.json +1 -0
- dmart/sample/spaces/management/requests.json +1 -0
- dmart/sample/spaces/management/roles/.dm/dummy/meta.role.json +12 -0
- dmart/sample/spaces/management/roles/.dm/logged_in/meta.role.json +18 -0
- dmart/sample/spaces/management/roles/.dm/manager/meta.role.json +13 -0
- dmart/sample/spaces/management/roles/.dm/meta.folder.json +1 -0
- dmart/sample/spaces/management/roles/.dm/moderator/meta.role.json +13 -0
- dmart/sample/spaces/management/roles/.dm/super_admin/meta.role.json +14 -0
- dmart/sample/spaces/management/roles/.dm/test_role/meta.role.json +13 -0
- dmart/sample/spaces/management/roles/.dm/world/meta.role.json +15 -0
- dmart/sample/spaces/management/roles.json +1 -0
- dmart/sample/spaces/management/schema/.dm/admin_notification_request/attachments.media/meta.ui_schema.json +10 -0
- dmart/sample/spaces/management/schema/.dm/admin_notification_request/attachments.media/ui_schema.json +32 -0
- dmart/sample/spaces/management/schema/.dm/admin_notification_request/meta.schema.json +1 -0
- dmart/sample/spaces/management/schema/.dm/api/meta.schema.json +1 -0
- dmart/sample/spaces/management/schema/.dm/folder_rendering/meta.schema.json +1 -0
- dmart/sample/spaces/management/schema/.dm/health_check/meta.schema.json +17 -0
- dmart/sample/spaces/management/schema/.dm/meta.folder.json +1 -0
- dmart/sample/spaces/management/schema/.dm/meta_schema/meta.schema.json +1 -0
- dmart/sample/spaces/management/schema/.dm/metafile/meta.schema.json +14 -0
- dmart/sample/spaces/management/schema/.dm/notification/meta.schema.json +1 -0
- dmart/sample/spaces/management/schema/.dm/system_notification_request/attachments.media/meta.ui_schema.json +10 -0
- dmart/sample/spaces/management/schema/.dm/system_notification_request/attachments.media/ui_schema.json +32 -0
- dmart/sample/spaces/management/schema/.dm/system_notification_request/meta.schema.json +1 -0
- dmart/sample/spaces/management/schema/.dm/view/meta.schema.json +1 -0
- dmart/sample/spaces/management/schema/.dm/workflow/meta.schema.json +1 -0
- dmart/sample/spaces/management/schema/admin_notification_request.json +89 -0
- dmart/sample/spaces/management/schema/api.json +1 -0
- dmart/sample/spaces/management/schema/folder_rendering.json +238 -0
- dmart/sample/spaces/management/schema/health_check.json +8 -0
- dmart/sample/spaces/management/schema/meta_schema.json +74 -0
- dmart/sample/spaces/management/schema/metafile.json +153 -0
- dmart/sample/spaces/management/schema/notification.json +28 -0
- dmart/sample/spaces/management/schema/system_notification_request.json +57 -0
- dmart/sample/spaces/management/schema/view.json +23 -0
- dmart/sample/spaces/management/schema/workflow.json +87 -0
- dmart/sample/spaces/management/schema.json +1 -0
- dmart/sample/spaces/management/users/.dm/alibaba/meta.user.json +23 -0
- dmart/sample/spaces/management/users/.dm/anonymous/meta.user.json +18 -0
- dmart/sample/spaces/management/users/.dm/dmart/meta.user.json +26 -0
- dmart/sample/spaces/management/users/.dm/meta.folder.json +14 -0
- dmart/sample/spaces/management/workflows/.dm/channel/meta.content.json +1 -0
- dmart/sample/spaces/management/workflows/.dm/meta.folder.json +1 -0
- dmart/sample/spaces/management/workflows/channel.json +148 -0
- dmart/sample/spaces/management/workflows.json +1 -0
- dmart/sample/spaces/maqola/.dm/meta.space.json +33 -0
- dmart/sample/spaces/personal/.dm/meta.space.json +24 -0
- dmart/sample/spaces/personal/people/.dm/meta.folder.json +1 -0
- dmart/sample/spaces/personal/people/dmart/.dm/meta.folder.json +1 -0
- dmart/sample/spaces/personal/people/dmart/messages/.dm/0b5f7e7f/meta.content.json +1 -0
- dmart/sample/spaces/personal/people/dmart/messages/.dm/meta.folder.json +1 -0
- dmart/sample/spaces/personal/people/dmart/messages/.dm/mytest/meta.content.json +1 -0
- dmart/sample/spaces/personal/people/dmart/messages/0b5f7e7f.json +1 -0
- dmart/sample/spaces/personal/people/dmart/messages/mytest.json +1 -0
- dmart/sample/spaces/personal/people/dmart/notifications/.dm/meta.folder.json +1 -0
- dmart/sample/spaces/personal/people/dmart/private/.dm/inner/meta.content.json +1 -0
- dmart/sample/spaces/personal/people/dmart/private/.dm/meta.folder.json +1 -0
- dmart/sample/spaces/personal/people/dmart/private/inner.json +1 -0
- dmart/sample/spaces/personal/people/dmart/protected/.dm/avatar/meta.content.json +1 -0
- dmart/sample/spaces/personal/people/dmart/protected/.dm/meta.folder.json +1 -0
- dmart/sample/spaces/personal/people/dmart/protected/avatar.png +0 -0
- dmart/sample/spaces/personal/people/dmart/public/.dm/meta.folder.json +1 -0
- dmart/sample/test/.gitignore +2 -0
- dmart/sample/test/createcontent.json +9 -0
- dmart/sample/test/createmedia.json +9 -0
- dmart/sample/test/createmedia_entry.json +6 -0
- dmart/sample/test/createschema.json +8 -0
- dmart/sample/test/createschemawork.json +11 -0
- dmart/sample/test/createticket.json +13 -0
- dmart/sample/test/data.json +4 -0
- dmart/sample/test/deletecontent.json +12 -0
- dmart/sample/test/logo.jpeg +0 -0
- dmart/sample/test/my.jpg +0 -0
- dmart/sample/test/myticket.json +23 -0
- dmart/sample/test/resources.csv +12 -0
- dmart/sample/test/schema.json +16 -0
- dmart/sample/test/temp.json +1 -0
- dmart/sample/test/test.dmart +45 -0
- dmart/sample/test/ticket_schema.json +23 -0
- dmart/sample/test/ticket_workflow.json +85 -0
- dmart/sample/test/ticketbody.json +4 -0
- dmart/sample/test/ticketcontent.json +14 -0
- dmart/sample/test/updatecontent.json +20 -0
- dmart/sample/test/workflow_schema.json +68 -0
- dmart/scheduled_notification_handler.py +121 -0
- dmart/schema_migration.py +208 -0
- dmart/schema_modulate.py +192 -0
- dmart/set_admin_passwd.py +75 -0
- dmart/sync.py +202 -0
- dmart/test_utils.py +34 -0
- dmart/utils/__init__.py +0 -0
- dmart/utils/access_control.py +306 -0
- dmart/utils/async_request.py +8 -0
- dmart/utils/exporter.py +309 -0
- dmart/utils/firebase_notifier.py +57 -0
- dmart/utils/generate_email.py +37 -0
- dmart/utils/helpers.py +352 -0
- dmart/utils/hypercorn_config.py +12 -0
- dmart/utils/internal_error_code.py +60 -0
- dmart/utils/jwt.py +124 -0
- dmart/utils/logger.py +167 -0
- dmart/utils/middleware.py +99 -0
- dmart/utils/notification.py +75 -0
- dmart/utils/password_hashing.py +16 -0
- dmart/utils/plugin_manager.py +202 -0
- dmart/utils/query_policies_helper.py +128 -0
- dmart/utils/regex.py +44 -0
- dmart/utils/repository.py +529 -0
- dmart/utils/router_helper.py +19 -0
- dmart/utils/settings.py +212 -0
- dmart/utils/sms_notifier.py +21 -0
- dmart/utils/social_sso.py +67 -0
- dmart/utils/templates/activation.html.j2 +26 -0
- dmart/utils/templates/reminder.html.j2 +17 -0
- dmart/utils/ticket_sys_utils.py +203 -0
- dmart/utils/web_notifier.py +29 -0
- dmart/websocket.py +231 -0
- dmart-1.4.40.post8.dist-info/METADATA +75 -0
- dmart-1.4.40.post8.dist-info/RECORD +489 -0
- dmart-1.4.40.post8.dist-info/WHEEL +5 -0
- dmart-1.4.40.post8.dist-info/entry_points.txt +2 -0
- dmart-1.4.40.post8.dist-info/top_level.txt +1 -0
dmart/api/user/router.py
ADDED
|
@@ -0,0 +1,1413 @@
|
|
|
1
|
+
""" Session Apis """
|
|
2
|
+
import json
|
|
3
|
+
import re
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
import aiofiles
|
|
6
|
+
from utils.async_request import AsyncRequest
|
|
7
|
+
from utils.generate_email import generate_subject
|
|
8
|
+
from utils.generate_email import generate_email_from_template
|
|
9
|
+
from fastapi import APIRouter, Body, Query, Request, status, Depends, Response, Header
|
|
10
|
+
import models.api as api
|
|
11
|
+
import models.core as core
|
|
12
|
+
from models.enums import ActionType, RequestType, ResourceType, ContentType, UserType
|
|
13
|
+
from data_adapters.adapter import data_adapter as db
|
|
14
|
+
from utils.access_control import access_control
|
|
15
|
+
from utils.helpers import flatten_dict
|
|
16
|
+
from utils.internal_error_code import InternalErrorCode
|
|
17
|
+
from utils.jwt import JWTBearer, sign_jwt, decode_jwt
|
|
18
|
+
from typing import Any
|
|
19
|
+
from utils.settings import settings
|
|
20
|
+
import utils.repository as repository
|
|
21
|
+
from utils.plugin_manager import plugin_manager
|
|
22
|
+
import utils.password_hashing as password_hashing
|
|
23
|
+
from models.api import Error, Exception, Status
|
|
24
|
+
from utils.social_sso import get_apple_sso, get_facebook_sso, get_google_sso
|
|
25
|
+
from .service import (
|
|
26
|
+
gen_alphanumeric,
|
|
27
|
+
get_otp_key,
|
|
28
|
+
send_email,
|
|
29
|
+
send_sms,
|
|
30
|
+
send_otp,
|
|
31
|
+
email_send_otp,
|
|
32
|
+
get_shortname_from_identifier,
|
|
33
|
+
check_user_validation,
|
|
34
|
+
set_user_profile,
|
|
35
|
+
update_user_payload,
|
|
36
|
+
get_otp_confirmation_email_or_msisdn,
|
|
37
|
+
)
|
|
38
|
+
from .model.requests import (
|
|
39
|
+
ConfirmOTPRequest,
|
|
40
|
+
PasswordResetRequest,
|
|
41
|
+
SendOTPRequest,
|
|
42
|
+
UserLoginRequest,
|
|
43
|
+
)
|
|
44
|
+
import utils.regex as rgx
|
|
45
|
+
from languages.loader import languages
|
|
46
|
+
from fastapi_sso.sso.google import GoogleSSO
|
|
47
|
+
from fastapi_sso.sso.facebook import FacebookSSO
|
|
48
|
+
from fastapi_sso.sso.base import OpenID, SSOBase
|
|
49
|
+
from fastapi.logger import logger
|
|
50
|
+
from fastapi.responses import ORJSONResponse
|
|
51
|
+
from datetime import datetime
|
|
52
|
+
|
|
53
|
+
router = APIRouter(default_response_class=ORJSONResponse)
|
|
54
|
+
|
|
55
|
+
MANAGEMENT_SPACE: str = settings.management_space
|
|
56
|
+
USERS_SUBPATH: str = "users"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@router.get(
|
|
60
|
+
"/check-existing", response_model=api.Response, response_model_exclude_none=True
|
|
61
|
+
)
|
|
62
|
+
async def check_existing_user_fields(
|
|
63
|
+
shortname: str | None = Query(
|
|
64
|
+
default=None, pattern=rgx.SHORTNAME, examples=["john_doo"]),
|
|
65
|
+
msisdn: str | None = Query(
|
|
66
|
+
default=None, pattern=rgx.MSISDN, examples=["7777778110"]),
|
|
67
|
+
email: str | None = Query(default=None, pattern=rgx.EMAIL, examples=[
|
|
68
|
+
"john_doo@mail.com"]),
|
|
69
|
+
):
|
|
70
|
+
unique_fields = {"shortname": shortname,
|
|
71
|
+
"msisdn": msisdn, "email_unescaped": email}
|
|
72
|
+
|
|
73
|
+
search_str = f"@subpath:{USERS_SUBPATH}"
|
|
74
|
+
redis_escape_chars = str.maketrans(
|
|
75
|
+
{".": r"\.", "@": r"\@", ":": r"\:", "/": r"\/", "-": r"\-", " ": r"\ "}
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
attributes = await db.check_uniqueness(unique_fields, search_str, redis_escape_chars)
|
|
79
|
+
return api.Response(status=api.Status.success, attributes=attributes)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@router.post("/create", response_model=api.Response, response_model_exclude_none=True)
|
|
83
|
+
async def create_user(response: Response, record: core.Record, http_request: Request) -> api.Response:
|
|
84
|
+
"""Register a new user by invitation"""
|
|
85
|
+
if not settings.is_registrable:
|
|
86
|
+
raise api.Exception(
|
|
87
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
|
88
|
+
error=api.Error(type="create", code=50,
|
|
89
|
+
message="Register API is disabled"),
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
validation_message: str | None = None
|
|
93
|
+
if "email" not in record.attributes and "msisdn" not in record.attributes:
|
|
94
|
+
validation_message = "Email or MSISDN is required"
|
|
95
|
+
|
|
96
|
+
if record.attributes.get("email") and (settings.is_otp_for_create_required and not record.attributes.get("email_otp")):
|
|
97
|
+
validation_message = "Email OTP is required"
|
|
98
|
+
|
|
99
|
+
if record.attributes.get("msisdn") and (settings.is_otp_for_create_required and not record.attributes.get("msisdn_otp")):
|
|
100
|
+
validation_message = "MSISDN OTP is required"
|
|
101
|
+
|
|
102
|
+
if record.attributes.get("password") and not re.match(rgx.PASSWORD, record.attributes["password"]):
|
|
103
|
+
validation_message = "password dose not match required rules"
|
|
104
|
+
|
|
105
|
+
if validation_message:
|
|
106
|
+
raise api.Exception(
|
|
107
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
|
108
|
+
error=api.Error(type="create", code=50,
|
|
109
|
+
message=validation_message),
|
|
110
|
+
)
|
|
111
|
+
await db.validate_uniqueness(MANAGEMENT_SPACE, record, RequestType.create, "dmart")
|
|
112
|
+
|
|
113
|
+
await plugin_manager.before_action(
|
|
114
|
+
core.Event(
|
|
115
|
+
space_name=MANAGEMENT_SPACE,
|
|
116
|
+
subpath=USERS_SUBPATH,
|
|
117
|
+
shortname=record.shortname,
|
|
118
|
+
action_type=core.ActionType.create,
|
|
119
|
+
resource_type=ResourceType.user,
|
|
120
|
+
user_shortname=record.shortname,
|
|
121
|
+
)
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
record.resource_type = ResourceType.user
|
|
125
|
+
user = core.User.from_record(
|
|
126
|
+
record=record,
|
|
127
|
+
owner_shortname="dmart"
|
|
128
|
+
)
|
|
129
|
+
separate_payload_data: str | dict[str, Any] | None = {}
|
|
130
|
+
if record.attributes.get("payload", {}).get("body"):
|
|
131
|
+
schema_shortname = getattr(user.payload, "schema_shortname", None)
|
|
132
|
+
user.payload = core.Payload(
|
|
133
|
+
content_type=ContentType.json,
|
|
134
|
+
schema_shortname=schema_shortname,
|
|
135
|
+
body=record.attributes["payload"].get("body", ""),
|
|
136
|
+
)
|
|
137
|
+
if user.payload:
|
|
138
|
+
separate_payload_data = user.payload.body
|
|
139
|
+
user.payload.body = record.shortname + ".json"
|
|
140
|
+
|
|
141
|
+
if user.payload and separate_payload_data:
|
|
142
|
+
if not isinstance(separate_payload_data, str) and not isinstance(
|
|
143
|
+
separate_payload_data, Path
|
|
144
|
+
):
|
|
145
|
+
if user.payload.schema_shortname:
|
|
146
|
+
await db.validate_payload_with_schema(
|
|
147
|
+
payload_data=separate_payload_data,
|
|
148
|
+
space_name=MANAGEMENT_SPACE,
|
|
149
|
+
schema_shortname=user.payload.schema_shortname,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
if user.msisdn:
|
|
153
|
+
is_valid_otp = True if not settings.is_otp_for_create_required else await verify_user(ConfirmOTPRequest(
|
|
154
|
+
msisdn=record.attributes.get("msisdn"),
|
|
155
|
+
email=None,
|
|
156
|
+
shortname=None,
|
|
157
|
+
code=record.attributes.get("msisdn_otp", "")
|
|
158
|
+
))
|
|
159
|
+
if not is_valid_otp:
|
|
160
|
+
raise api.Exception(
|
|
161
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
|
162
|
+
error=api.Error(type="create", code=50,
|
|
163
|
+
message="Invalid MSISDN OTP"),
|
|
164
|
+
)
|
|
165
|
+
user.is_msisdn_verified = True
|
|
166
|
+
if user.email:
|
|
167
|
+
is_valid_otp = True if not settings.is_otp_for_create_required else await verify_user(ConfirmOTPRequest(
|
|
168
|
+
email=record.attributes.get("email"),
|
|
169
|
+
msisdn=None,
|
|
170
|
+
shortname=None,
|
|
171
|
+
code=record.attributes.get("email_otp", "")
|
|
172
|
+
))
|
|
173
|
+
if not is_valid_otp:
|
|
174
|
+
raise api.Exception(
|
|
175
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
|
176
|
+
error=api.Error(type="create", code=50,
|
|
177
|
+
message="Invalid Email OTP"),
|
|
178
|
+
)
|
|
179
|
+
user.is_email_verified = True
|
|
180
|
+
|
|
181
|
+
user.is_active = True
|
|
182
|
+
|
|
183
|
+
await db.create(MANAGEMENT_SPACE, USERS_SUBPATH, user)
|
|
184
|
+
if isinstance(separate_payload_data, dict) and separate_payload_data:
|
|
185
|
+
await db.update_payload(
|
|
186
|
+
MANAGEMENT_SPACE, USERS_SUBPATH, user, separate_payload_data, user.owner_shortname
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
response_record = await process_user_login(user, response, {}, None, http_request.headers)
|
|
190
|
+
|
|
191
|
+
await plugin_manager.after_action(
|
|
192
|
+
core.Event(
|
|
193
|
+
space_name=MANAGEMENT_SPACE,
|
|
194
|
+
subpath=USERS_SUBPATH,
|
|
195
|
+
shortname=record.shortname,
|
|
196
|
+
action_type=core.ActionType.create,
|
|
197
|
+
resource_type=ResourceType.user,
|
|
198
|
+
user_shortname=record.shortname,
|
|
199
|
+
)
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
return api.Response(
|
|
203
|
+
status=api.Status.success,
|
|
204
|
+
records=[
|
|
205
|
+
core.Record(
|
|
206
|
+
shortname=user.shortname,
|
|
207
|
+
subpath=USERS_SUBPATH,
|
|
208
|
+
resource_type=ResourceType.user,
|
|
209
|
+
attributes=response_record.attributes
|
|
210
|
+
)
|
|
211
|
+
]
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
async def verify_user(user_request: ConfirmOTPRequest):
|
|
215
|
+
user_identifier = user_request.check_fields()
|
|
216
|
+
key = get_otp_key(user_identifier)
|
|
217
|
+
code = await db.get_otp(key)
|
|
218
|
+
if not code or code != user_request.code:
|
|
219
|
+
return False
|
|
220
|
+
|
|
221
|
+
return True
|
|
222
|
+
|
|
223
|
+
@router.post(
|
|
224
|
+
"/login",
|
|
225
|
+
response_model=api.Response,
|
|
226
|
+
response_model_exclude_none=True,
|
|
227
|
+
)
|
|
228
|
+
async def login(response: Response, request: UserLoginRequest, http_request: Request) -> api.Response:
|
|
229
|
+
"""Login and generate refresh token"""
|
|
230
|
+
shortname: str | None = None
|
|
231
|
+
user = None
|
|
232
|
+
user_updates: dict[str, Any] = {}
|
|
233
|
+
identifier: dict[str, str] | None = request.check_fields()
|
|
234
|
+
try:
|
|
235
|
+
if request.invitation:
|
|
236
|
+
invitation_token = await db.get_invitation(request.invitation)
|
|
237
|
+
if invitation_token is None:
|
|
238
|
+
raise api.Exception(
|
|
239
|
+
status.HTTP_401_UNAUTHORIZED,
|
|
240
|
+
api.Error(
|
|
241
|
+
type="jwtauth",
|
|
242
|
+
code=InternalErrorCode.INVALID_INVITATION,
|
|
243
|
+
message="Expired or invalid invitation",
|
|
244
|
+
),
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
data = decode_jwt(request.invitation)
|
|
248
|
+
shortname = data.get("shortname", None)
|
|
249
|
+
if (shortname is None) or (request.shortname is not None and request.shortname != shortname):
|
|
250
|
+
raise api.Exception(
|
|
251
|
+
status.HTTP_401_UNAUTHORIZED,
|
|
252
|
+
api.Error(
|
|
253
|
+
type="jwtauth",
|
|
254
|
+
code=InternalErrorCode.INVALID_INVITATION,
|
|
255
|
+
message="Invalid invitation or data provided",
|
|
256
|
+
),
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
user = await db.load(
|
|
260
|
+
space_name=MANAGEMENT_SPACE,
|
|
261
|
+
subpath=USERS_SUBPATH,
|
|
262
|
+
shortname=shortname,
|
|
263
|
+
class_type=core.User,
|
|
264
|
+
user_shortname=shortname,
|
|
265
|
+
)
|
|
266
|
+
if (
|
|
267
|
+
request.shortname != user.shortname
|
|
268
|
+
and request.msisdn != user.msisdn
|
|
269
|
+
and request.email != user.email
|
|
270
|
+
):
|
|
271
|
+
raise api.Exception(
|
|
272
|
+
status.HTTP_401_UNAUTHORIZED,
|
|
273
|
+
api.Error(
|
|
274
|
+
type="jwtauth",
|
|
275
|
+
code=InternalErrorCode.INVALID_INVITATION,
|
|
276
|
+
message="Invalid invitation or data provided",
|
|
277
|
+
),
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
await db.delete_invitation(request.invitation)
|
|
281
|
+
await db.delete_url_shortner_by_token(request.invitation)
|
|
282
|
+
user_updates["force_password_change"] = True
|
|
283
|
+
|
|
284
|
+
user_updates = check_user_validation(user, data, user_updates, invitation_token)
|
|
285
|
+
|
|
286
|
+
elif request.otp is not None:
|
|
287
|
+
otp_code = request.otp
|
|
288
|
+
|
|
289
|
+
if bool(request.email) and bool(request.msisdn) and bool(request.shortname):
|
|
290
|
+
raise api.Exception(
|
|
291
|
+
status.HTTP_400_BAD_REQUEST,
|
|
292
|
+
api.Error(
|
|
293
|
+
type="auth",
|
|
294
|
+
code=InternalErrorCode.OTP_ISSUE,
|
|
295
|
+
message="Provide either msisdn, email or shortname, not both."
|
|
296
|
+
)
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
if not request.email and not request.msisdn and not request.shortname:
|
|
300
|
+
raise api.Exception(
|
|
301
|
+
status.HTTP_400_BAD_REQUEST,
|
|
302
|
+
api.Error(
|
|
303
|
+
type="auth",
|
|
304
|
+
code=InternalErrorCode.OTP_ISSUE,
|
|
305
|
+
message="Either msisdn, email or shortname must be provided."
|
|
306
|
+
)
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
key: str | None = None
|
|
310
|
+
if not shortname and identifier:
|
|
311
|
+
if isinstance(identifier, dict):
|
|
312
|
+
key, value = list(identifier.items())[0]
|
|
313
|
+
shortname = identifier.get("shortname") or await get_shortname_from_identifier(value=value, key=key)
|
|
314
|
+
else:
|
|
315
|
+
shortname = await get_shortname_from_identifier(value=identifier, key=key)
|
|
316
|
+
if not shortname:
|
|
317
|
+
raise api.Exception(
|
|
318
|
+
status.HTTP_401_UNAUTHORIZED,
|
|
319
|
+
api.Error(
|
|
320
|
+
type="auth",
|
|
321
|
+
code=InternalErrorCode.INVALID_USERNAME_AND_PASS,
|
|
322
|
+
message="Invalid username or password"
|
|
323
|
+
)
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
user = await db.load_or_none('management', '/users', shortname, core.User)
|
|
327
|
+
if user is None:
|
|
328
|
+
raise api.Exception(
|
|
329
|
+
status.HTTP_401_UNAUTHORIZED,
|
|
330
|
+
api.Error(
|
|
331
|
+
type="auth",
|
|
332
|
+
code=InternalErrorCode.SHORTNAME_DOES_NOT_EXIST,
|
|
333
|
+
message="User does not exist"
|
|
334
|
+
)
|
|
335
|
+
)
|
|
336
|
+
if user.type == UserType.mobile and user.locked_to_device and (not request.firebase_token or not user.firebase_token or request.firebase_token != user.firebase_token):
|
|
337
|
+
raise api.Exception(
|
|
338
|
+
status.HTTP_401_UNAUTHORIZED,
|
|
339
|
+
api.Error(type="auth", code=InternalErrorCode.USER_ACCOUNT_LOCKED, message="This account is locked to a unique device !"),
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
if user is None:
|
|
343
|
+
raise api.Exception(
|
|
344
|
+
status.HTTP_401_UNAUTHORIZED,
|
|
345
|
+
api.Error(
|
|
346
|
+
type="auth",
|
|
347
|
+
code=InternalErrorCode.INVALID_USERNAME_AND_PASS,
|
|
348
|
+
message="Invalid username or password"
|
|
349
|
+
)
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
key = ""
|
|
353
|
+
if request.shortname:
|
|
354
|
+
if user.msisdn:
|
|
355
|
+
key = f"users:otp:otps/{user.msisdn}"
|
|
356
|
+
else:
|
|
357
|
+
key = f"users:otp:otps/{request.msisdn or request.email or request.shortname}"
|
|
358
|
+
stored_otp = await db.get_otp(key)
|
|
359
|
+
|
|
360
|
+
if stored_otp is None or stored_otp != otp_code:
|
|
361
|
+
# await handle_failed_login_attempt(user)
|
|
362
|
+
raise api.Exception(
|
|
363
|
+
status.HTTP_401_UNAUTHORIZED,
|
|
364
|
+
api.Error(
|
|
365
|
+
type="auth",
|
|
366
|
+
code=InternalErrorCode.OTP_INVALID,
|
|
367
|
+
message="Wrong OTP"
|
|
368
|
+
),
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
user = await db.load(
|
|
372
|
+
space_name=MANAGEMENT_SPACE,
|
|
373
|
+
subpath=USERS_SUBPATH,
|
|
374
|
+
shortname=shortname,
|
|
375
|
+
class_type=core.User,
|
|
376
|
+
user_shortname=shortname,
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
is_password_valid = None
|
|
380
|
+
if request.password:
|
|
381
|
+
is_password_valid = password_hashing.verify_password(
|
|
382
|
+
request.password or "", user.password or ""
|
|
383
|
+
)
|
|
384
|
+
if (
|
|
385
|
+
user
|
|
386
|
+
and user.is_active
|
|
387
|
+
and (is_password_valid is None or is_password_valid)
|
|
388
|
+
):
|
|
389
|
+
await db.clear_failed_password_attempts(shortname)
|
|
390
|
+
await reset_failed_login_attempt(user)
|
|
391
|
+
|
|
392
|
+
if request.otp:
|
|
393
|
+
await db.delete_otp(key)
|
|
394
|
+
|
|
395
|
+
record = await process_user_login(user, response, {}, request.firebase_token, http_request.headers)
|
|
396
|
+
|
|
397
|
+
await plugin_manager.after_action(
|
|
398
|
+
core.Event(
|
|
399
|
+
space_name=MANAGEMENT_SPACE,
|
|
400
|
+
subpath=USERS_SUBPATH,
|
|
401
|
+
shortname=shortname,
|
|
402
|
+
action_type=core.ActionType.update,
|
|
403
|
+
resource_type=ResourceType.user,
|
|
404
|
+
user_shortname=shortname,
|
|
405
|
+
)
|
|
406
|
+
)
|
|
407
|
+
return api.Response(status=api.Status.success, records=[record])
|
|
408
|
+
else:
|
|
409
|
+
if is_password_valid is not None and not is_password_valid:
|
|
410
|
+
await handle_failed_login_attempt(user)
|
|
411
|
+
elif not user.is_active:
|
|
412
|
+
raise api.Exception(
|
|
413
|
+
status.HTTP_401_UNAUTHORIZED,
|
|
414
|
+
api.Error(type="auth", code=InternalErrorCode.USER_ACCOUNT_LOCKED,
|
|
415
|
+
message="Account has been locked."),
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
raise api.Exception(
|
|
419
|
+
status.HTTP_401_UNAUTHORIZED,
|
|
420
|
+
api.Error(
|
|
421
|
+
type="auth",
|
|
422
|
+
code=InternalErrorCode.INVALID_USERNAME_AND_PASS,
|
|
423
|
+
message="Invalid username or password"
|
|
424
|
+
),
|
|
425
|
+
)
|
|
426
|
+
else:
|
|
427
|
+
if identifier is None:
|
|
428
|
+
raise api.Exception(
|
|
429
|
+
status.HTTP_422_UNPROCESSABLE_ENTITY,
|
|
430
|
+
api.Error(
|
|
431
|
+
type="request", code=InternalErrorCode.INVALID_IDENTIFIER, message="Invalid identifier [2]"
|
|
432
|
+
),
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
if "shortname" in identifier:
|
|
436
|
+
shortname = identifier["shortname"]
|
|
437
|
+
else:
|
|
438
|
+
_list = list(identifier.items())
|
|
439
|
+
if len(_list) != 1:
|
|
440
|
+
raise api.Exception(
|
|
441
|
+
status.HTTP_422_UNPROCESSABLE_ENTITY,
|
|
442
|
+
api.Error(
|
|
443
|
+
type="request",
|
|
444
|
+
code=InternalErrorCode.INVALID_IDENTIFIER,
|
|
445
|
+
message="Only one valid identifier is allowed!",
|
|
446
|
+
),
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
key, value = _list[0]
|
|
450
|
+
shortname = await get_shortname_from_identifier(key, value)
|
|
451
|
+
if shortname is None:
|
|
452
|
+
raise api.Exception(
|
|
453
|
+
status.HTTP_401_UNAUTHORIZED,
|
|
454
|
+
api.Error(
|
|
455
|
+
type="auth",
|
|
456
|
+
code=InternalErrorCode.INVALID_USERNAME_AND_PASS,
|
|
457
|
+
message="Invalid username or password [1]",
|
|
458
|
+
),
|
|
459
|
+
)
|
|
460
|
+
user = await db.load(
|
|
461
|
+
space_name=MANAGEMENT_SPACE,
|
|
462
|
+
subpath=USERS_SUBPATH,
|
|
463
|
+
shortname=shortname,
|
|
464
|
+
class_type=core.User,
|
|
465
|
+
user_shortname=shortname,
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
is_password_valid = password_hashing.verify_password(
|
|
469
|
+
request.password or "", user.password or ""
|
|
470
|
+
)
|
|
471
|
+
if (
|
|
472
|
+
user
|
|
473
|
+
and user.is_active
|
|
474
|
+
and (
|
|
475
|
+
request.invitation
|
|
476
|
+
or is_password_valid
|
|
477
|
+
)
|
|
478
|
+
):
|
|
479
|
+
if request.invitation is None and user.type == UserType.mobile and (not request.firebase_token or not user.firebase_token or request.firebase_token != user.firebase_token):
|
|
480
|
+
if user.locked_to_device:
|
|
481
|
+
raise api.Exception(
|
|
482
|
+
status.HTTP_401_UNAUTHORIZED,
|
|
483
|
+
api.Error(type="auth", code=InternalErrorCode.USER_ACCOUNT_LOCKED,
|
|
484
|
+
message="This account is locked to a unique device !"),
|
|
485
|
+
)
|
|
486
|
+
else:
|
|
487
|
+
raise api.Exception(
|
|
488
|
+
status.HTTP_401_UNAUTHORIZED,
|
|
489
|
+
api.Error(type="auth", code=InternalErrorCode.OTP_NEEDED, message="New device detected, login with otp"),
|
|
490
|
+
)
|
|
491
|
+
|
|
492
|
+
await db.clear_failed_password_attempts(shortname)
|
|
493
|
+
record = await process_user_login(user, response, user_updates, request.firebase_token, http_request.headers)
|
|
494
|
+
await reset_failed_login_attempt(user)
|
|
495
|
+
|
|
496
|
+
await plugin_manager.after_action(
|
|
497
|
+
core.Event(
|
|
498
|
+
space_name=MANAGEMENT_SPACE,
|
|
499
|
+
subpath=USERS_SUBPATH,
|
|
500
|
+
shortname=shortname,
|
|
501
|
+
action_type=core.ActionType.update,
|
|
502
|
+
resource_type=ResourceType.user,
|
|
503
|
+
user_shortname=shortname,
|
|
504
|
+
)
|
|
505
|
+
)
|
|
506
|
+
return api.Response(status=api.Status.success, records=[record])
|
|
507
|
+
# Check if user entered a wrong password
|
|
508
|
+
if not is_password_valid:
|
|
509
|
+
await handle_failed_login_attempt(user)
|
|
510
|
+
elif not user.is_active:
|
|
511
|
+
raise api.Exception(
|
|
512
|
+
status.HTTP_401_UNAUTHORIZED,
|
|
513
|
+
api.Error(type="auth", code=InternalErrorCode.USER_ACCOUNT_LOCKED,
|
|
514
|
+
message="Account has been locked."),
|
|
515
|
+
)
|
|
516
|
+
|
|
517
|
+
raise api.Exception(
|
|
518
|
+
status.HTTP_401_UNAUTHORIZED,
|
|
519
|
+
api.Error(
|
|
520
|
+
type="auth",
|
|
521
|
+
code=InternalErrorCode.INVALID_USERNAME_AND_PASS,
|
|
522
|
+
message="Invalid username or password"
|
|
523
|
+
),
|
|
524
|
+
)
|
|
525
|
+
except api.Exception as e:
|
|
526
|
+
if e.error.type == "db":
|
|
527
|
+
raise api.Exception(
|
|
528
|
+
status.HTTP_401_UNAUTHORIZED,
|
|
529
|
+
api.Error(
|
|
530
|
+
type="auth",
|
|
531
|
+
code=InternalErrorCode.INVALID_USERNAME_AND_PASS,
|
|
532
|
+
message="Invalid username or password"
|
|
533
|
+
),
|
|
534
|
+
)
|
|
535
|
+
else:
|
|
536
|
+
raise e
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
@router.get("/profile", response_model=api.Response, response_model_exclude_none=True)
|
|
541
|
+
async def get_profile(shortname=Depends(JWTBearer())) -> api.Response:
|
|
542
|
+
|
|
543
|
+
await plugin_manager.before_action(
|
|
544
|
+
core.Event(
|
|
545
|
+
space_name=MANAGEMENT_SPACE,
|
|
546
|
+
subpath=USERS_SUBPATH,
|
|
547
|
+
shortname=shortname,
|
|
548
|
+
action_type=core.ActionType.view,
|
|
549
|
+
resource_type=ResourceType.user,
|
|
550
|
+
user_shortname=shortname,
|
|
551
|
+
)
|
|
552
|
+
)
|
|
553
|
+
|
|
554
|
+
user = await db.load(
|
|
555
|
+
space_name=MANAGEMENT_SPACE,
|
|
556
|
+
subpath=USERS_SUBPATH,
|
|
557
|
+
shortname=shortname,
|
|
558
|
+
class_type=core.User,
|
|
559
|
+
user_shortname=shortname,
|
|
560
|
+
)
|
|
561
|
+
attributes: dict[str, Any] = {}
|
|
562
|
+
if user.email:
|
|
563
|
+
attributes["email"] = user.email
|
|
564
|
+
|
|
565
|
+
if user.displayname:
|
|
566
|
+
attributes["displayname"] = user.displayname
|
|
567
|
+
|
|
568
|
+
if user.description:
|
|
569
|
+
attributes["description"] = user.description
|
|
570
|
+
|
|
571
|
+
if user.msisdn:
|
|
572
|
+
attributes["msisdn"] = user.msisdn
|
|
573
|
+
|
|
574
|
+
if user.payload:
|
|
575
|
+
attributes["payload"] = user.payload
|
|
576
|
+
if settings.active_data_db == 'file':
|
|
577
|
+
path = settings.spaces_folder / MANAGEMENT_SPACE / USERS_SUBPATH
|
|
578
|
+
if (
|
|
579
|
+
user.payload
|
|
580
|
+
and user.payload.content_type
|
|
581
|
+
and user.payload.content_type == ContentType.json
|
|
582
|
+
and (path / str(user.payload.body)).is_file()
|
|
583
|
+
):
|
|
584
|
+
async with aiofiles.open(
|
|
585
|
+
path / str(user.payload.body), "r"
|
|
586
|
+
) as payload_file_content:
|
|
587
|
+
attributes["payload"].body = json.loads(
|
|
588
|
+
await payload_file_content.read()
|
|
589
|
+
)
|
|
590
|
+
|
|
591
|
+
attributes["type"] = user.type
|
|
592
|
+
attributes["language"] = user.language
|
|
593
|
+
attributes["is_email_verified"] = user.is_email_verified
|
|
594
|
+
attributes["is_msisdn_verified"] = user.is_msisdn_verified
|
|
595
|
+
attributes["force_password_change"] = user.force_password_change
|
|
596
|
+
|
|
597
|
+
attributes["permissions"] = await db.get_user_permissions(shortname)
|
|
598
|
+
attributes["roles"] = user.roles
|
|
599
|
+
attributes["groups"] = user.groups
|
|
600
|
+
|
|
601
|
+
attachments_path = (
|
|
602
|
+
settings.spaces_folder
|
|
603
|
+
/ MANAGEMENT_SPACE
|
|
604
|
+
/ USERS_SUBPATH
|
|
605
|
+
/ ".dm"
|
|
606
|
+
/ user.shortname
|
|
607
|
+
)
|
|
608
|
+
user_avatar = await db.get_entry_attachments(
|
|
609
|
+
subpath=f"{USERS_SUBPATH}/{user.shortname}",
|
|
610
|
+
attachments_path=attachments_path,
|
|
611
|
+
filter_shortnames=["avatar"],
|
|
612
|
+
)
|
|
613
|
+
|
|
614
|
+
record = core.Record(
|
|
615
|
+
subpath=USERS_SUBPATH,
|
|
616
|
+
shortname=user.shortname,
|
|
617
|
+
resource_type=core.ResourceType.user,
|
|
618
|
+
attributes=attributes,
|
|
619
|
+
attachments=user_avatar,
|
|
620
|
+
)
|
|
621
|
+
|
|
622
|
+
await plugin_manager.after_action(
|
|
623
|
+
core.Event(
|
|
624
|
+
space_name=MANAGEMENT_SPACE,
|
|
625
|
+
subpath=USERS_SUBPATH,
|
|
626
|
+
shortname=shortname,
|
|
627
|
+
action_type=core.ActionType.view,
|
|
628
|
+
resource_type=ResourceType.user,
|
|
629
|
+
user_shortname=shortname,
|
|
630
|
+
)
|
|
631
|
+
)
|
|
632
|
+
|
|
633
|
+
return api.Response(status=api.Status.success, records=[record])
|
|
634
|
+
|
|
635
|
+
|
|
636
|
+
@router.post("/profile", response_model=api.Response, response_model_exclude_none=True)
|
|
637
|
+
async def update_profile(
|
|
638
|
+
profile: core.Record, shortname=Depends(JWTBearer())
|
|
639
|
+
) -> api.Response:
|
|
640
|
+
"""Update user profile"""
|
|
641
|
+
profile_user = core.Meta.check_record(
|
|
642
|
+
record=profile, owner_shortname=profile.shortname
|
|
643
|
+
)
|
|
644
|
+
|
|
645
|
+
if profile_user.password and not re.match(rgx.PASSWORD, profile_user.password):
|
|
646
|
+
raise api.Exception(
|
|
647
|
+
status.HTTP_401_UNAUTHORIZED,
|
|
648
|
+
api.Error(
|
|
649
|
+
type="jwtauth",
|
|
650
|
+
code=InternalErrorCode.INVALID_USERNAME_AND_PASS,
|
|
651
|
+
message="Invalid username or password",
|
|
652
|
+
),
|
|
653
|
+
)
|
|
654
|
+
|
|
655
|
+
await plugin_manager.before_action(
|
|
656
|
+
core.Event(
|
|
657
|
+
space_name=MANAGEMENT_SPACE,
|
|
658
|
+
subpath=USERS_SUBPATH,
|
|
659
|
+
shortname=shortname,
|
|
660
|
+
action_type=core.ActionType.update,
|
|
661
|
+
resource_type=ResourceType.user,
|
|
662
|
+
user_shortname=shortname,
|
|
663
|
+
)
|
|
664
|
+
)
|
|
665
|
+
|
|
666
|
+
user = await db.load(
|
|
667
|
+
space_name=MANAGEMENT_SPACE,
|
|
668
|
+
subpath=USERS_SUBPATH,
|
|
669
|
+
shortname=shortname,
|
|
670
|
+
class_type=core.User,
|
|
671
|
+
user_shortname=shortname,
|
|
672
|
+
)
|
|
673
|
+
|
|
674
|
+
old_version_flattened = flatten_dict(user.model_dump())
|
|
675
|
+
|
|
676
|
+
if profile_user.password:
|
|
677
|
+
if "old_password" not in profile.attributes:
|
|
678
|
+
raise Exception(
|
|
679
|
+
status.HTTP_403_FORBIDDEN,
|
|
680
|
+
Error(
|
|
681
|
+
type="auth",
|
|
682
|
+
code=InternalErrorCode.PASSWORD_RESET_ERROR,
|
|
683
|
+
message="Wrong password have been provided!",
|
|
684
|
+
),
|
|
685
|
+
)
|
|
686
|
+
if not password_hashing.verify_password(
|
|
687
|
+
profile.attributes["old_password"], user.password or ""
|
|
688
|
+
):
|
|
689
|
+
raise api.Exception(
|
|
690
|
+
status.HTTP_401_UNAUTHORIZED,
|
|
691
|
+
api.Error(type="request", code=InternalErrorCode.UNMATCHED_DATA,
|
|
692
|
+
message="mismatch with the information provided"),
|
|
693
|
+
)
|
|
694
|
+
|
|
695
|
+
# if "force_password_change" in profile.attributes:
|
|
696
|
+
# user.force_password_change = profile.attributes["force_password_change"]
|
|
697
|
+
|
|
698
|
+
user = await set_user_profile(profile, profile_user, user)
|
|
699
|
+
|
|
700
|
+
if profile.attributes.get("email") and user.email != profile.attributes.get("email") and not profile.attributes.get("email_otp"):
|
|
701
|
+
raise api.Exception(
|
|
702
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
|
703
|
+
error=api.Error(type="create", code=50, message="Email OTP is required to update your email"),
|
|
704
|
+
)
|
|
705
|
+
|
|
706
|
+
if profile.attributes.get("msisdn") and user.msisdn != profile.attributes.get("msisdn") and not profile.attributes.get("msisdn_otp"):
|
|
707
|
+
raise api.Exception(
|
|
708
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
|
709
|
+
error=api.Error(type="create", code=50, message="msisdn OTP is required to update your msisdn"),
|
|
710
|
+
)
|
|
711
|
+
|
|
712
|
+
if "confirmation" in profile.attributes:
|
|
713
|
+
result = await get_otp_confirmation_email_or_msisdn(profile_user)
|
|
714
|
+
|
|
715
|
+
if result is None or result != profile.attributes["confirmation"]:
|
|
716
|
+
raise api.Exception(
|
|
717
|
+
status.HTTP_422_UNPROCESSABLE_ENTITY,
|
|
718
|
+
api.Error(type="request", code=InternalErrorCode.INVALID_CONFIRMATION,
|
|
719
|
+
message="Invalid confirmation code [1]"),
|
|
720
|
+
)
|
|
721
|
+
|
|
722
|
+
if profile_user.email:
|
|
723
|
+
user.is_email_verified = True
|
|
724
|
+
elif profile_user.msisdn:
|
|
725
|
+
user.is_msisdn_verified = True
|
|
726
|
+
else:
|
|
727
|
+
await db.validate_uniqueness(MANAGEMENT_SPACE, profile, RequestType.update, shortname)
|
|
728
|
+
if "email" in profile.attributes and user.email != profile_user.email:
|
|
729
|
+
is_valid_otp = await verify_user(ConfirmOTPRequest(
|
|
730
|
+
email=profile.attributes.get("email"),
|
|
731
|
+
msisdn=None,
|
|
732
|
+
shortname=None,
|
|
733
|
+
code=profile.attributes.get("email_otp", "")
|
|
734
|
+
))
|
|
735
|
+
if not is_valid_otp:
|
|
736
|
+
raise api.Exception(
|
|
737
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
|
738
|
+
error=api.Error(type="create", code=50,
|
|
739
|
+
message="Invalid Email OTP"),
|
|
740
|
+
)
|
|
741
|
+
user.email = profile_user.email
|
|
742
|
+
user.is_email_verified = True
|
|
743
|
+
|
|
744
|
+
if "msisdn" in profile.attributes and user.msisdn != profile_user.msisdn:
|
|
745
|
+
is_valid_otp = await verify_user(ConfirmOTPRequest(
|
|
746
|
+
msisdn=profile.attributes.get("msisdn"),
|
|
747
|
+
email=None,
|
|
748
|
+
shortname=None,
|
|
749
|
+
code=profile.attributes.get("msisdn_otp", "")
|
|
750
|
+
))
|
|
751
|
+
if not is_valid_otp:
|
|
752
|
+
raise api.Exception(
|
|
753
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
|
754
|
+
error=api.Error(type="create", code=50,
|
|
755
|
+
message="Invalid MSISDN OTP"),
|
|
756
|
+
)
|
|
757
|
+
user.msisdn = profile_user.msisdn
|
|
758
|
+
user.is_msisdn_verified = True
|
|
759
|
+
|
|
760
|
+
if "payload" in profile.attributes and "body" in profile.attributes["payload"]:
|
|
761
|
+
await update_user_payload(profile, profile_user, user, shortname)
|
|
762
|
+
|
|
763
|
+
if user.is_active and profile.attributes.get("is_active", None) is not None:
|
|
764
|
+
if not profile.attributes.get("is_active"):
|
|
765
|
+
await db.remove_user_session(user.shortname)
|
|
766
|
+
|
|
767
|
+
history_diff = await db.update(
|
|
768
|
+
MANAGEMENT_SPACE,
|
|
769
|
+
USERS_SUBPATH,
|
|
770
|
+
user,
|
|
771
|
+
old_version_flattened,
|
|
772
|
+
flatten_dict(user.model_dump()),
|
|
773
|
+
list(profile.attributes.keys()),
|
|
774
|
+
shortname,
|
|
775
|
+
retrieve_lock_status=profile.retrieve_lock_status,
|
|
776
|
+
)
|
|
777
|
+
|
|
778
|
+
if settings.logout_on_pwd_change and profile_user.password:
|
|
779
|
+
await db.remove_user_session(shortname)
|
|
780
|
+
|
|
781
|
+
await plugin_manager.after_action(
|
|
782
|
+
core.Event(
|
|
783
|
+
space_name=MANAGEMENT_SPACE,
|
|
784
|
+
subpath=USERS_SUBPATH,
|
|
785
|
+
shortname=shortname,
|
|
786
|
+
action_type=core.ActionType.update,
|
|
787
|
+
resource_type=ResourceType.user,
|
|
788
|
+
user_shortname=shortname,
|
|
789
|
+
attributes={"history_diff": history_diff},
|
|
790
|
+
)
|
|
791
|
+
)
|
|
792
|
+
return api.Response(status=api.Status.success)
|
|
793
|
+
|
|
794
|
+
|
|
795
|
+
# cookie_options = {
|
|
796
|
+
# "key": "auth_token",
|
|
797
|
+
# "httponly": True,
|
|
798
|
+
# "secure": True,
|
|
799
|
+
# "samesite": "none",
|
|
800
|
+
# }
|
|
801
|
+
# "samesite": "lax" }
|
|
802
|
+
# samesite="none",
|
|
803
|
+
# secure=True,
|
|
804
|
+
|
|
805
|
+
|
|
806
|
+
@router.post(
|
|
807
|
+
"/logout",
|
|
808
|
+
response_model=api.Response,
|
|
809
|
+
response_model_exclude_none=True,
|
|
810
|
+
)
|
|
811
|
+
async def logout(
|
|
812
|
+
response: Response,
|
|
813
|
+
shortname=Depends(JWTBearer()),
|
|
814
|
+
) -> api.Response:
|
|
815
|
+
response.set_cookie(value="", max_age=0, key="auth_token",
|
|
816
|
+
httponly=True, secure=True, samesite="none")
|
|
817
|
+
|
|
818
|
+
await db.remove_user_session(shortname)
|
|
819
|
+
|
|
820
|
+
return api.Response(status=api.Status.success, records=[])
|
|
821
|
+
|
|
822
|
+
|
|
823
|
+
@router.post("/delete", response_model=api.Response, response_model_exclude_none=True)
|
|
824
|
+
async def delete_account(shortname=Depends(JWTBearer())) -> api.Response:
|
|
825
|
+
"""Delete own user"""
|
|
826
|
+
await plugin_manager.before_action(
|
|
827
|
+
core.Event(
|
|
828
|
+
space_name=MANAGEMENT_SPACE,
|
|
829
|
+
subpath=USERS_SUBPATH,
|
|
830
|
+
shortname=shortname,
|
|
831
|
+
action_type=core.ActionType.delete,
|
|
832
|
+
resource_type=ResourceType.user,
|
|
833
|
+
user_shortname=shortname,
|
|
834
|
+
)
|
|
835
|
+
)
|
|
836
|
+
user = await db.load(
|
|
837
|
+
space_name=MANAGEMENT_SPACE,
|
|
838
|
+
subpath=USERS_SUBPATH,
|
|
839
|
+
shortname=shortname,
|
|
840
|
+
class_type=core.User,
|
|
841
|
+
user_shortname=shortname,
|
|
842
|
+
)
|
|
843
|
+
await db.delete(MANAGEMENT_SPACE, USERS_SUBPATH, user, shortname)
|
|
844
|
+
|
|
845
|
+
await db.remove_user_session(shortname)
|
|
846
|
+
|
|
847
|
+
await plugin_manager.after_action(
|
|
848
|
+
core.Event(
|
|
849
|
+
space_name=MANAGEMENT_SPACE,
|
|
850
|
+
subpath=USERS_SUBPATH,
|
|
851
|
+
shortname=shortname,
|
|
852
|
+
action_type=core.ActionType.delete,
|
|
853
|
+
resource_type=ResourceType.user,
|
|
854
|
+
user_shortname=shortname,
|
|
855
|
+
attributes={"entry":user}
|
|
856
|
+
)
|
|
857
|
+
)
|
|
858
|
+
|
|
859
|
+
return api.Response(status=api.Status.success)
|
|
860
|
+
|
|
861
|
+
|
|
862
|
+
@router.post(
|
|
863
|
+
"/otp-request",
|
|
864
|
+
response_model=api.Response,
|
|
865
|
+
response_model_exclude_none=True,
|
|
866
|
+
)
|
|
867
|
+
async def otp_request(
|
|
868
|
+
user_request: SendOTPRequest,
|
|
869
|
+
skel_accept_language=Header(default=None)
|
|
870
|
+
) -> api.Response:
|
|
871
|
+
"""Request new OTP"""
|
|
872
|
+
|
|
873
|
+
user_identifier = user_request.check_fields()
|
|
874
|
+
key, value = list(user_identifier.items())[0]
|
|
875
|
+
user = await db.get_user_by_criteria(key, value)
|
|
876
|
+
if not user and not settings.is_registrable:
|
|
877
|
+
raise api.Exception(
|
|
878
|
+
status.HTTP_404_NOT_FOUND,
|
|
879
|
+
api.Error(
|
|
880
|
+
type="request",
|
|
881
|
+
code=InternalErrorCode.USERNAME_NOT_EXIST,
|
|
882
|
+
message="No user found with the provided information",
|
|
883
|
+
),
|
|
884
|
+
)
|
|
885
|
+
otp_key = get_otp_key(user_identifier)
|
|
886
|
+
last_otp_since = await db.otp_created_since(otp_key)
|
|
887
|
+
|
|
888
|
+
if (last_otp_since and last_otp_since < settings.allow_otp_resend_after):
|
|
889
|
+
raise api.Exception(
|
|
890
|
+
status.HTTP_403_FORBIDDEN,
|
|
891
|
+
api.Error(
|
|
892
|
+
type="request",
|
|
893
|
+
code=InternalErrorCode.OTP_RESEND_BLOCKED,
|
|
894
|
+
message=f"Resend OTP is allowed after {int(settings.allow_otp_resend_after - last_otp_since)} seconds",
|
|
895
|
+
),
|
|
896
|
+
)
|
|
897
|
+
|
|
898
|
+
if "msisdn" in user_identifier:
|
|
899
|
+
await send_otp(user_identifier["msisdn"], skel_accept_language or "")
|
|
900
|
+
elif "email" in user_identifier:
|
|
901
|
+
await email_send_otp(user_identifier["email"], skel_accept_language or "")
|
|
902
|
+
|
|
903
|
+
return api.Response(status=api.Status.success)
|
|
904
|
+
|
|
905
|
+
@router.post(
|
|
906
|
+
"/otp-request-login",
|
|
907
|
+
response_model=api.Response,
|
|
908
|
+
response_model_exclude_none=True,
|
|
909
|
+
)
|
|
910
|
+
async def otp_request_login(
|
|
911
|
+
user_request: SendOTPRequest,
|
|
912
|
+
skel_accept_language=Header(default=None),
|
|
913
|
+
) -> api.Response:
|
|
914
|
+
"""Request new OTP"""
|
|
915
|
+
|
|
916
|
+
result = user_request.check_fields()
|
|
917
|
+
shortname = result.get("shortname")
|
|
918
|
+
msisdn = result.get("msisdn")
|
|
919
|
+
email = result.get("email")
|
|
920
|
+
|
|
921
|
+
if bool(msisdn) ^ bool(email) ^ bool(shortname):
|
|
922
|
+
value = msisdn or email or shortname
|
|
923
|
+
if value is None:
|
|
924
|
+
raise api.Exception(
|
|
925
|
+
status.HTTP_400_BAD_REQUEST,
|
|
926
|
+
api.Error(
|
|
927
|
+
type="request",
|
|
928
|
+
code=InternalErrorCode.INVALID_IDENTIFIER,
|
|
929
|
+
message="Expected msisdn, email or shortname to be present."
|
|
930
|
+
)
|
|
931
|
+
)
|
|
932
|
+
else:
|
|
933
|
+
raise api.Exception(
|
|
934
|
+
status.HTTP_400_BAD_REQUEST,
|
|
935
|
+
api.Error(
|
|
936
|
+
type="auth",
|
|
937
|
+
code=InternalErrorCode.OTP_ISSUE,
|
|
938
|
+
message="one of msisdn, email or shortname must be provided"
|
|
939
|
+
)
|
|
940
|
+
)
|
|
941
|
+
|
|
942
|
+
user: str | core.User | None = None
|
|
943
|
+
if shortname:
|
|
944
|
+
user = await db.load_or_none('management', '/users', shortname, core.User)
|
|
945
|
+
else:
|
|
946
|
+
user = await db.get_user_by_criteria(
|
|
947
|
+
"msisdn" if msisdn else "email",
|
|
948
|
+
value,
|
|
949
|
+
)
|
|
950
|
+
|
|
951
|
+
|
|
952
|
+
if user is None:
|
|
953
|
+
logger.warning("user not found!")
|
|
954
|
+
return api.Response(status=api.Status.success)
|
|
955
|
+
|
|
956
|
+
if msisdn:
|
|
957
|
+
await send_otp(msisdn, skel_accept_language or "")
|
|
958
|
+
elif email:
|
|
959
|
+
await email_send_otp(email, skel_accept_language or "")
|
|
960
|
+
elif shortname and type(user) is core.User:
|
|
961
|
+
if user.msisdn and user.is_active:
|
|
962
|
+
await send_otp(user.msisdn, skel_accept_language or "")
|
|
963
|
+
else:
|
|
964
|
+
logger.warning(f"bad value for either {user.msisdn if hasattr(user, 'msisdn') else 'msisdn:N/A'} or {user.is_active}")
|
|
965
|
+
else:
|
|
966
|
+
logger.warning(f"Bad user object value {user} type {type(user)}")
|
|
967
|
+
|
|
968
|
+
|
|
969
|
+
return api.Response(status=api.Status.success)
|
|
970
|
+
|
|
971
|
+
|
|
972
|
+
@router.post(
|
|
973
|
+
"/password-reset-request",
|
|
974
|
+
response_model=api.Response,
|
|
975
|
+
response_model_exclude_none=True,
|
|
976
|
+
)
|
|
977
|
+
async def reset_password(user_request: PasswordResetRequest) -> api.Response:
|
|
978
|
+
result = user_request.check_fields()
|
|
979
|
+
key, value = list(result.items())[0]
|
|
980
|
+
shortname = await db.get_user_by_criteria(key, value)
|
|
981
|
+
|
|
982
|
+
|
|
983
|
+
if shortname is not None:
|
|
984
|
+
try:
|
|
985
|
+
user = await db.load(
|
|
986
|
+
space_name=MANAGEMENT_SPACE,
|
|
987
|
+
subpath=USERS_SUBPATH,
|
|
988
|
+
shortname=shortname,
|
|
989
|
+
class_type=core.User,
|
|
990
|
+
user_shortname=shortname,
|
|
991
|
+
)
|
|
992
|
+
|
|
993
|
+
reset_password_message = "Reset password via this link: {link}, This link can be used once and within the next 48 hours."
|
|
994
|
+
|
|
995
|
+
if "msisdn" in result or "shortname" in result:
|
|
996
|
+
if user.msisdn and ("msisdn" not in result or user.msisdn == result["msisdn"]):
|
|
997
|
+
token = await repository.store_user_invitation_token(
|
|
998
|
+
user, "SMS"
|
|
999
|
+
)
|
|
1000
|
+
if token:
|
|
1001
|
+
shortened_link = await repository.url_shortner(token)
|
|
1002
|
+
await send_sms(
|
|
1003
|
+
msisdn=user.msisdn,
|
|
1004
|
+
message=reset_password_message.replace("{link}", shortened_link),
|
|
1005
|
+
)
|
|
1006
|
+
else:
|
|
1007
|
+
logger.warning("token could not be generated")
|
|
1008
|
+
else:
|
|
1009
|
+
logger.warning("value mismatch")
|
|
1010
|
+
else:
|
|
1011
|
+
if user.email and user.email == result["email"]:
|
|
1012
|
+
token = await repository.store_user_invitation_token(
|
|
1013
|
+
user, "EMAIL"
|
|
1014
|
+
)
|
|
1015
|
+
if token:
|
|
1016
|
+
shortened_link = await repository.url_shortner(token)
|
|
1017
|
+
await send_email(
|
|
1018
|
+
from_address=settings.email_sender,
|
|
1019
|
+
to_address=user.email,
|
|
1020
|
+
message=reset_password_message.replace("{link}", shortened_link),
|
|
1021
|
+
subject="Reset password",
|
|
1022
|
+
)
|
|
1023
|
+
else:
|
|
1024
|
+
logger.warning("token could not be generated")
|
|
1025
|
+
else:
|
|
1026
|
+
logger.warning(f"email mismatch {user.email} {result['email']}")
|
|
1027
|
+
except Exception as e:
|
|
1028
|
+
logger.error(f"reset_password failed: {e}")
|
|
1029
|
+
else:
|
|
1030
|
+
logger.warning("user requested not found.")
|
|
1031
|
+
|
|
1032
|
+
return api.Response(
|
|
1033
|
+
status=api.Status.success ,
|
|
1034
|
+
attributes={"message": "If the provided email or phone number exists, a password reset link has been sent."},
|
|
1035
|
+
)
|
|
1036
|
+
|
|
1037
|
+
|
|
1038
|
+
@router.post(
|
|
1039
|
+
"/otp-confirm",
|
|
1040
|
+
response_model=api.Response,
|
|
1041
|
+
response_model_exclude_none=True,
|
|
1042
|
+
)
|
|
1043
|
+
async def confirm_otp(
|
|
1044
|
+
user_request: ConfirmOTPRequest, user=Depends(JWTBearer())
|
|
1045
|
+
) -> api.Response:
|
|
1046
|
+
"""Confirm OTP"""
|
|
1047
|
+
|
|
1048
|
+
result = user_request.check_fields()
|
|
1049
|
+
key = get_otp_key(result)
|
|
1050
|
+
|
|
1051
|
+
code = await db.get_otp(key)
|
|
1052
|
+
if not code or code != user_request.code:
|
|
1053
|
+
raise Exception(
|
|
1054
|
+
status.HTTP_400_BAD_REQUEST,
|
|
1055
|
+
Error(
|
|
1056
|
+
type="OTP",
|
|
1057
|
+
code=InternalErrorCode.OTP_EXPIRED,
|
|
1058
|
+
message="Invalid OTP",
|
|
1059
|
+
),
|
|
1060
|
+
)
|
|
1061
|
+
|
|
1062
|
+
confirmation = gen_alphanumeric()
|
|
1063
|
+
data: core.Record = core.Record(
|
|
1064
|
+
resource_type=ResourceType.user,
|
|
1065
|
+
subpath="users",
|
|
1066
|
+
attributes={"confirmation": confirmation},
|
|
1067
|
+
shortname=user,
|
|
1068
|
+
)
|
|
1069
|
+
|
|
1070
|
+
if "msisdn" in result:
|
|
1071
|
+
key = f"users:otp:confirmation/msisdn/{user_request.msisdn}"
|
|
1072
|
+
data.attributes["msisdn"] = user_request.msisdn
|
|
1073
|
+
else:
|
|
1074
|
+
key = f"users:otp:confirmation/email/{user_request.email}"
|
|
1075
|
+
data.attributes["email"] = user_request.email
|
|
1076
|
+
|
|
1077
|
+
await db.save_otp(key, confirmation)
|
|
1078
|
+
|
|
1079
|
+
response = await update_profile(data, shortname=user)
|
|
1080
|
+
|
|
1081
|
+
if response.status == Status.success:
|
|
1082
|
+
return api.Response(status=api.Status.success, records=[])
|
|
1083
|
+
else:
|
|
1084
|
+
raise Exception(
|
|
1085
|
+
status.HTTP_400_BAD_REQUEST,
|
|
1086
|
+
Error(
|
|
1087
|
+
type="OTP",
|
|
1088
|
+
code=InternalErrorCode.OTP_FAILED,
|
|
1089
|
+
message=response.error.message
|
|
1090
|
+
if response.error
|
|
1091
|
+
else "Internal error",
|
|
1092
|
+
),
|
|
1093
|
+
)
|
|
1094
|
+
|
|
1095
|
+
|
|
1096
|
+
@router.post("/reset", response_model=api.Response, response_model_exclude_none=True)
|
|
1097
|
+
async def user_reset(
|
|
1098
|
+
shortname: str = Body(..., pattern=rgx.SHORTNAME,
|
|
1099
|
+
embed=True, examples=["john_doo"]),
|
|
1100
|
+
logged_user=Depends(JWTBearer()),
|
|
1101
|
+
) -> api.Response:
|
|
1102
|
+
|
|
1103
|
+
user = await db.load(
|
|
1104
|
+
space_name=MANAGEMENT_SPACE,
|
|
1105
|
+
subpath=USERS_SUBPATH,
|
|
1106
|
+
shortname=shortname,
|
|
1107
|
+
class_type=core.User,
|
|
1108
|
+
user_shortname=shortname,
|
|
1109
|
+
)
|
|
1110
|
+
if not await access_control.check_access(
|
|
1111
|
+
user_shortname=logged_user,
|
|
1112
|
+
space_name=MANAGEMENT_SPACE,
|
|
1113
|
+
subpath=USERS_SUBPATH,
|
|
1114
|
+
resource_type=ResourceType.user,
|
|
1115
|
+
action_type=ActionType.update,
|
|
1116
|
+
resource_is_active=user.is_active,
|
|
1117
|
+
resource_owner_shortname=user.owner_shortname,
|
|
1118
|
+
resource_owner_group=user.owner_group_shortname,
|
|
1119
|
+
entry_shortname=user.shortname,
|
|
1120
|
+
):
|
|
1121
|
+
raise api.Exception(
|
|
1122
|
+
status.HTTP_401_UNAUTHORIZED,
|
|
1123
|
+
api.Error(
|
|
1124
|
+
type="request",
|
|
1125
|
+
code=InternalErrorCode.NOT_ALLOWED,
|
|
1126
|
+
message="You don't have permission to this action [20]",
|
|
1127
|
+
),
|
|
1128
|
+
)
|
|
1129
|
+
|
|
1130
|
+
if not user.force_password_change:
|
|
1131
|
+
await db.internal_sys_update_model(
|
|
1132
|
+
space_name=MANAGEMENT_SPACE,
|
|
1133
|
+
subpath=USERS_SUBPATH,
|
|
1134
|
+
meta=user,
|
|
1135
|
+
updates={"force_password_change": True}
|
|
1136
|
+
)
|
|
1137
|
+
|
|
1138
|
+
sms_link = None
|
|
1139
|
+
email_link = None
|
|
1140
|
+
|
|
1141
|
+
if user.msisdn and not user.is_msisdn_verified:
|
|
1142
|
+
token = await repository.store_user_invitation_token(
|
|
1143
|
+
user, "SMS"
|
|
1144
|
+
)
|
|
1145
|
+
if token:
|
|
1146
|
+
sms_link = await repository.url_shortner(token)
|
|
1147
|
+
await send_sms(
|
|
1148
|
+
msisdn=user.msisdn,
|
|
1149
|
+
message=languages[
|
|
1150
|
+
user.language
|
|
1151
|
+
]["reset_message"].replace(
|
|
1152
|
+
"{link}",
|
|
1153
|
+
sms_link
|
|
1154
|
+
),
|
|
1155
|
+
)
|
|
1156
|
+
if user.email and not user.is_email_verified:
|
|
1157
|
+
token = await repository.store_user_invitation_token(
|
|
1158
|
+
user, "SMS"
|
|
1159
|
+
)
|
|
1160
|
+
if token:
|
|
1161
|
+
email_link = await repository.url_shortner(token)
|
|
1162
|
+
await send_email(
|
|
1163
|
+
from_address=settings.email_sender,
|
|
1164
|
+
to_address=user.email,
|
|
1165
|
+
message=generate_email_from_template(
|
|
1166
|
+
"activation",
|
|
1167
|
+
{
|
|
1168
|
+
"link": email_link,
|
|
1169
|
+
"name": user.displayname.en if user.displayname else "",
|
|
1170
|
+
"shortname": user.shortname,
|
|
1171
|
+
"msisdn": user.msisdn,
|
|
1172
|
+
},
|
|
1173
|
+
),
|
|
1174
|
+
subject=generate_subject("activation"),
|
|
1175
|
+
)
|
|
1176
|
+
|
|
1177
|
+
return api.Response(
|
|
1178
|
+
status=api.Status.success,
|
|
1179
|
+
attributes={"sms_sent": bool(sms_link), "email_sent": bool(email_link)}
|
|
1180
|
+
)
|
|
1181
|
+
|
|
1182
|
+
|
|
1183
|
+
@router.post(
|
|
1184
|
+
"/validate_password",
|
|
1185
|
+
response_model=api.Response,
|
|
1186
|
+
response_model_exclude_none=True,
|
|
1187
|
+
)
|
|
1188
|
+
async def validate_password(
|
|
1189
|
+
password: str, shortname=Depends(JWTBearer())
|
|
1190
|
+
) -> api.Response:
|
|
1191
|
+
"""Validate Password"""
|
|
1192
|
+
user = await db.load(
|
|
1193
|
+
MANAGEMENT_SPACE, USERS_SUBPATH, shortname, core.User, shortname
|
|
1194
|
+
)
|
|
1195
|
+
if user and password_hashing.verify_password(password, user.password or ""):
|
|
1196
|
+
return api.Response(status=api.Status.success)
|
|
1197
|
+
else:
|
|
1198
|
+
raise api.Exception(
|
|
1199
|
+
status.HTTP_401_UNAUTHORIZED,
|
|
1200
|
+
api.Error(type="jwtauth", code=InternalErrorCode.PASSWORD_NOT_VALIDATED,
|
|
1201
|
+
message="Password dose not match"),
|
|
1202
|
+
)
|
|
1203
|
+
|
|
1204
|
+
|
|
1205
|
+
|
|
1206
|
+
async def process_user_login(
|
|
1207
|
+
user: core.User,
|
|
1208
|
+
response: Response,
|
|
1209
|
+
user_updates: dict = {},
|
|
1210
|
+
firebase_token: str | None = None,
|
|
1211
|
+
request_headers = None
|
|
1212
|
+
) -> core.Record:
|
|
1213
|
+
access_token = await sign_jwt(
|
|
1214
|
+
{"shortname": user.shortname, "type": user.type}, settings.jwt_access_expires
|
|
1215
|
+
)
|
|
1216
|
+
|
|
1217
|
+
response.set_cookie(
|
|
1218
|
+
value=access_token,
|
|
1219
|
+
max_age=settings.jwt_access_expires,
|
|
1220
|
+
key="auth_token",
|
|
1221
|
+
httponly=True,
|
|
1222
|
+
secure=True,
|
|
1223
|
+
samesite="lax",
|
|
1224
|
+
)
|
|
1225
|
+
record = core.Record(
|
|
1226
|
+
resource_type=core.ResourceType.user,
|
|
1227
|
+
subpath="users",
|
|
1228
|
+
shortname=user.shortname,
|
|
1229
|
+
attributes={
|
|
1230
|
+
"access_token": access_token,
|
|
1231
|
+
"type": user.type,
|
|
1232
|
+
},
|
|
1233
|
+
)
|
|
1234
|
+
if user.displayname:
|
|
1235
|
+
record.attributes["displayname"] = user.displayname
|
|
1236
|
+
|
|
1237
|
+
if firebase_token:
|
|
1238
|
+
user_updates["firebase_token"] = firebase_token
|
|
1239
|
+
|
|
1240
|
+
if request_headers:
|
|
1241
|
+
headers_dict = dict(request_headers)
|
|
1242
|
+
headers_dict.pop("authorization", None)
|
|
1243
|
+
headers_dict.pop("cookie", None)
|
|
1244
|
+
user_updates["last_login"] = {
|
|
1245
|
+
"timestamp": int(datetime.now().timestamp()),
|
|
1246
|
+
"headers": headers_dict
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
if user_updates:
|
|
1250
|
+
await db.internal_sys_update_model(
|
|
1251
|
+
space_name=MANAGEMENT_SPACE,
|
|
1252
|
+
subpath=USERS_SUBPATH,
|
|
1253
|
+
meta=user,
|
|
1254
|
+
updates=user_updates,
|
|
1255
|
+
sync_redis=False,
|
|
1256
|
+
)
|
|
1257
|
+
|
|
1258
|
+
return record
|
|
1259
|
+
|
|
1260
|
+
|
|
1261
|
+
async def reset_failed_login_attempt(user: core.User):
|
|
1262
|
+
await db.set_failed_password_attempt_count(user.shortname, 0)
|
|
1263
|
+
|
|
1264
|
+
async def handle_failed_login_attempt(user: core.User):
|
|
1265
|
+
failed_login_attempts_count: int = await db.get_failed_password_attempt_count(user.shortname)
|
|
1266
|
+
|
|
1267
|
+
# Increment the failed login attempts counter
|
|
1268
|
+
failed_login_attempts_count += 1
|
|
1269
|
+
|
|
1270
|
+
if failed_login_attempts_count >= settings.max_failed_login_attempts:
|
|
1271
|
+
# If the user reach the configured limit, lock the user by setting the is_active to false
|
|
1272
|
+
if user.is_active:
|
|
1273
|
+
await db.set_failed_password_attempt_count(user.shortname, failed_login_attempts_count)
|
|
1274
|
+
|
|
1275
|
+
logger.info(f"User {user.shortname} reached the maximum failed login attempts ({settings.max_failed_login_attempts}) disabling the user")
|
|
1276
|
+
|
|
1277
|
+
old_version_flattend = flatten_dict(user.model_dump())
|
|
1278
|
+
user.is_active = False
|
|
1279
|
+
|
|
1280
|
+
await db.remove_user_session(user.shortname)
|
|
1281
|
+
|
|
1282
|
+
await db.update(
|
|
1283
|
+
MANAGEMENT_SPACE,
|
|
1284
|
+
USERS_SUBPATH,
|
|
1285
|
+
user,
|
|
1286
|
+
old_version_flattend,
|
|
1287
|
+
flatten_dict(user.model_dump()),
|
|
1288
|
+
["is_active"],
|
|
1289
|
+
user.shortname,
|
|
1290
|
+
)
|
|
1291
|
+
|
|
1292
|
+
await plugin_manager.after_action(
|
|
1293
|
+
core.Event(
|
|
1294
|
+
space_name=MANAGEMENT_SPACE,
|
|
1295
|
+
subpath=USERS_SUBPATH,
|
|
1296
|
+
shortname=user.shortname,
|
|
1297
|
+
action_type=core.ActionType.update,
|
|
1298
|
+
resource_type=ResourceType.user,
|
|
1299
|
+
user_shortname=user.shortname,
|
|
1300
|
+
)
|
|
1301
|
+
)
|
|
1302
|
+
|
|
1303
|
+
raise api.Exception(
|
|
1304
|
+
status.HTTP_401_UNAUTHORIZED,
|
|
1305
|
+
api.Error(type="auth", code=InternalErrorCode.USER_ACCOUNT_LOCKED,
|
|
1306
|
+
message="Account has been locked due to too many failed login attempts."),
|
|
1307
|
+
)
|
|
1308
|
+
else:
|
|
1309
|
+
# Count until the failed attempts reach the limit
|
|
1310
|
+
await db.set_failed_password_attempt_count(user.shortname, failed_login_attempts_count)
|
|
1311
|
+
|
|
1312
|
+
|
|
1313
|
+
if settings.social_login_allowed:
|
|
1314
|
+
|
|
1315
|
+
@router.get("/google/callback")
|
|
1316
|
+
async def google_profile(
|
|
1317
|
+
request: Request,
|
|
1318
|
+
response: Response,
|
|
1319
|
+
google_sso: GoogleSSO = Depends(get_google_sso),
|
|
1320
|
+
):
|
|
1321
|
+
async with google_sso:
|
|
1322
|
+
user_model = await social_login(request, google_sso, "google")
|
|
1323
|
+
|
|
1324
|
+
record = await process_user_login(
|
|
1325
|
+
user=user_model,
|
|
1326
|
+
response=response,
|
|
1327
|
+
request_headers=request.headers
|
|
1328
|
+
)
|
|
1329
|
+
return api.Response(status=api.Status.success, records=[record])
|
|
1330
|
+
|
|
1331
|
+
@router.get("/facebook/callback")
|
|
1332
|
+
async def facebook_login(
|
|
1333
|
+
request: Request,
|
|
1334
|
+
response: Response,
|
|
1335
|
+
facebook_sso: FacebookSSO = Depends(get_facebook_sso),
|
|
1336
|
+
):
|
|
1337
|
+
async with facebook_sso:
|
|
1338
|
+
user_model = await social_login(request, facebook_sso, "facebook")
|
|
1339
|
+
|
|
1340
|
+
record = await process_user_login(
|
|
1341
|
+
user=user_model,
|
|
1342
|
+
response=response,
|
|
1343
|
+
request_headers=request.headers
|
|
1344
|
+
)
|
|
1345
|
+
return api.Response(status=api.Status.success, records=[record])
|
|
1346
|
+
|
|
1347
|
+
@router.get("/apple/callback")
|
|
1348
|
+
async def apple_login(
|
|
1349
|
+
request: Request,
|
|
1350
|
+
response: Response,
|
|
1351
|
+
apple_sso: SSOBase = Depends(get_apple_sso),
|
|
1352
|
+
):
|
|
1353
|
+
async with apple_sso:
|
|
1354
|
+
user_model = await social_login(request, apple_sso, "apple")
|
|
1355
|
+
|
|
1356
|
+
record = await process_user_login(
|
|
1357
|
+
user=user_model,
|
|
1358
|
+
response=response,
|
|
1359
|
+
request_headers=request.headers
|
|
1360
|
+
)
|
|
1361
|
+
return api.Response(status=api.Status.success, records=[record])
|
|
1362
|
+
|
|
1363
|
+
async def social_login(request: Request, sso: SSOBase, provider: str) -> core.User:
|
|
1364
|
+
provider_user: OpenID | None = await sso.verify_and_process(request)
|
|
1365
|
+
if not provider_user or not provider_user.id:
|
|
1366
|
+
raise api.Exception(
|
|
1367
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
|
1368
|
+
error=api.Error(type="auth", code=InternalErrorCode.INVALID_DATA, message="Misconfigured provider"),
|
|
1369
|
+
)
|
|
1370
|
+
|
|
1371
|
+
user: core.User | None = await db.load_or_none(
|
|
1372
|
+
space_name=MANAGEMENT_SPACE,
|
|
1373
|
+
subpath=USERS_SUBPATH,
|
|
1374
|
+
shortname=f"{provider}_{provider_user.id}",
|
|
1375
|
+
class_type=core.User
|
|
1376
|
+
)
|
|
1377
|
+
|
|
1378
|
+
if not user:
|
|
1379
|
+
msisdn = await get_provider_phone_number(sso, provider)
|
|
1380
|
+
user = core.User(
|
|
1381
|
+
shortname=f"{provider}_{provider_user.id}",
|
|
1382
|
+
owner_shortname="dmart",
|
|
1383
|
+
displayname=core.Translation(
|
|
1384
|
+
en=f"{provider_user.first_name} {provider_user.last_name}"
|
|
1385
|
+
),
|
|
1386
|
+
email=provider_user.email,
|
|
1387
|
+
is_active=True,
|
|
1388
|
+
is_email_verified=True,
|
|
1389
|
+
social_avatar_url=provider_user.picture,
|
|
1390
|
+
)
|
|
1391
|
+
if msisdn and re.match(rgx.MSISDN, msisdn):
|
|
1392
|
+
user.msisdn = msisdn
|
|
1393
|
+
user.is_msisdn_verified = True
|
|
1394
|
+
setattr(user, f"{provider}_id", provider_user.id)
|
|
1395
|
+
|
|
1396
|
+
await db.create(MANAGEMENT_SPACE, USERS_SUBPATH, user)
|
|
1397
|
+
|
|
1398
|
+
return user
|
|
1399
|
+
|
|
1400
|
+
async def get_provider_phone_number(sso: SSOBase, provider: str,) -> None | str:
|
|
1401
|
+
if provider == "google":
|
|
1402
|
+
async with AsyncRequest() as session:
|
|
1403
|
+
res = await session.get(
|
|
1404
|
+
url="https://people.googleapis.com/v1/people/me",
|
|
1405
|
+
headers={"Authorization": f"Bearer {sso.access_token}"},
|
|
1406
|
+
params={"personFields": "phoneNumbers"}
|
|
1407
|
+
)
|
|
1408
|
+
if res.status != 200:
|
|
1409
|
+
return None
|
|
1410
|
+
data = await res.json()
|
|
1411
|
+
phone_number = data["phoneNumbers"][0].get("value") if len(data.get("phoneNumbers", [])) > 0 else None
|
|
1412
|
+
return str(phone_number).replace(" ", "")
|
|
1413
|
+
return None
|