create-workframe 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -201
- package/NOTICE +12 -12
- package/README.md +8 -92
- package/SECURITY.md +38 -40
- package/bin/workframe.js +329 -329
- package/docs/workspace-instructions/WORKFRAME_ONBOARDING.md +1 -1
- package/docs/workspace-instructions/WORKFRAME_ROUTING.md +8 -8
- package/package.json +3 -6
- package/profiles/architect/AGENTS.md +29 -29
- package/profiles/architect/SOUL.md +2 -2
- package/profiles/architect/skills/devops/kanban-worker/SKILL.md +27 -27
- package/profiles/designer/AGENTS.md +26 -26
- package/profiles/designer/skills/devops/kanban-worker/SKILL.md +27 -27
- package/profiles/dev/AGENTS.md +28 -28
- package/profiles/dev/skills/devops/kanban-worker/SKILL.md +27 -27
- package/profiles/docs/AGENTS.md +27 -27
- package/profiles/docs/skills/devops/kanban-worker/SKILL.md +27 -27
- package/profiles/research/AGENTS.md +26 -26
- package/profiles/research/skills/devops/kanban-worker/SKILL.md +27 -27
- package/profiles/visionary/AGENTS.md +25 -25
- package/profiles/visionary/skills/devops/kanban-worker/SKILL.md +27 -27
- package/profiles/workframe-agent/AGENTS.md +37 -37
- package/profiles/workframe-agent/skills/devops/botfather/SKILL.md +85 -85
- package/profiles/workframe-agent/skills/devops/kanban-handoff-pattern/SKILL.md +58 -58
- package/profiles/workframe-agent/skills/devops/workframe-cohort/SKILL.md +54 -54
- package/rules/workspace-README.md +5 -5
- package/scripts/bundle-workframe-ui.mjs +3 -3
- package/scripts/ensure-compose-host-paths.mjs +51 -51
- package/scripts/lib/install-identity.mjs +212 -212
- package/scripts/set-compose-public-url.mjs +92 -92
- package/scripts/sync-canonical-to-package.mjs +27 -9
- package/shared/WORKFRAME_AGENT_LIBRARY.md +17 -17
- package/shared/WORKFRAME_AGENT_OPERATIONS.md +15 -15
- package/shared/WORKFRAME_AGENT_PACKS.json +18 -18
- package/shared/WORKFRAME_AGENT_PACKS.yaml +8 -8
- package/shared/WORKFRAME_SKILL_CURATION.md +4 -4
- package/workframe-api/README.md +26 -28
- package/workframe-api/action_proxy.py +131 -131
- package/workframe-api/auth_rate_limit.py +49 -49
- package/workframe-api/credential_vault.py +445 -445
- package/workframe-api/data/avatar-catalog.json +41 -41
- package/workframe-api/email_sender.py +220 -220
- package/workframe-api/google_auth.py +90 -90
- package/workframe-api/install_api.py +359 -359
- package/workframe-api/internal_proxy_auth.py +150 -150
- package/workframe-api/llm_proxy.py +277 -277
- package/workframe-api/oidc_jwt.py +108 -108
- package/workframe-api/package.json +12 -13
- package/workframe-api/public/assets/index-DPXu_lGn.css +1 -1
- package/workframe-api/public/assets/index-DYnLrCZZ.js +8 -8
- package/workframe-api/requirements.txt +2 -2
- package/workframe-api/site_meta.py +271 -271
- package/workframe-api/stack_config.py +427 -427
- package/workframe-api/time-bind-chat.py +99 -99
- package/workframe-api/turn_credentials.py +226 -226
- package/workframe-api/updates.py +417 -417
- package/workframe-api/vault_kek.py +159 -159
- package/workframe-api/zk_auth.py +633 -633
- package/workframe-supervisor/Dockerfile +11 -11
- package/workframe-supervisor/server.py +787 -787
- package/workframe-ui/docker/nginx.conf +85 -85
- package/workframe-ui/public/assets/{arc-CBDYvkAF.js → arc-COAT3laO.js} +1 -1
- package/workframe-ui/public/assets/architecture-7EHR7CIX-DUyH3hWG.js +1 -0
- package/workframe-ui/public/assets/{architectureDiagram-3BPJPVTR-XnBRKeW0.js → architectureDiagram-3BPJPVTR-BFjWV24l.js} +1 -1
- package/workframe-ui/public/assets/{blockDiagram-GPEHLZMM-VYHUfVhd.js → blockDiagram-GPEHLZMM-DSQLPfrj.js} +1 -1
- package/workframe-ui/public/assets/{c4Diagram-AAUBKEIU-BTjUcJpm.js → c4Diagram-AAUBKEIU-DKEHv1t2.js} +1 -1
- package/workframe-ui/public/assets/channel-g7r_RGaY.js +1 -0
- package/workframe-ui/public/assets/{chunk-2J33WTMH-w7uu7R-b.js → chunk-2J33WTMH-DHZg-DUi.js} +1 -1
- package/workframe-ui/public/assets/{chunk-3OPIFGDE-Cb9LtnDX.js → chunk-3OPIFGDE-BB-OYTfp.js} +1 -1
- package/workframe-ui/public/assets/{chunk-4BX2VUAB-DiQ-qCwH.js → chunk-4BX2VUAB-C93q0YIm.js} +1 -1
- package/workframe-ui/public/assets/{chunk-55IACEB6-C-mLFr7z.js → chunk-55IACEB6-MAYniqik.js} +1 -1
- package/workframe-ui/public/assets/{chunk-5ZQYHXKU-DOesfiCI.js → chunk-5ZQYHXKU-ChgN6YJs.js} +1 -1
- package/workframe-ui/public/assets/{chunk-727SXJPM-BJ3oBZuz.js → chunk-727SXJPM-B_FYwdAv.js} +1 -1
- package/workframe-ui/public/assets/{chunk-AQP2D5EJ-CCA6xpGs.js → chunk-AQP2D5EJ-1_Hw_h1A.js} +1 -1
- package/workframe-ui/public/assets/{chunk-BSJP7CBP-a0cMNFb2.js → chunk-BSJP7CBP-CFiDQ1Rv.js} +1 -1
- package/workframe-ui/public/assets/{chunk-CSCIHK7Q-kuqN8EIY.js → chunk-CSCIHK7Q-DZ9UMTlB.js} +1 -1
- package/workframe-ui/public/assets/{chunk-FMBD7UC4-DyPgYHCg.js → chunk-FMBD7UC4-DlMlyFgw.js} +1 -1
- package/workframe-ui/public/assets/{chunk-KSCS5N6A-CdUuvR0V.js → chunk-KSCS5N6A-DHXtQ_Hf.js} +1 -1
- package/workframe-ui/public/assets/{chunk-L5ZTLDWV-Dq9NoWmK.js → chunk-L5ZTLDWV-CuQzg-QG.js} +1 -1
- package/workframe-ui/public/assets/{chunk-LZXEDZCA-p74rddlO.js → chunk-LZXEDZCA-BHzjzCGg.js} +2 -2
- package/workframe-ui/public/assets/{chunk-ND2GUHAM-DBD2u1Gz.js → chunk-ND2GUHAM-DHXx05n2.js} +1 -1
- package/workframe-ui/public/assets/{chunk-NZK2D7GU-BeIeYFnd.js → chunk-NZK2D7GU-CV5pmDM_.js} +1 -1
- package/workframe-ui/public/assets/{chunk-O5CBEL6O-ClHc56ib.js → chunk-O5CBEL6O-6tkCHxsV.js} +1 -1
- package/workframe-ui/public/assets/chunk-QZHKN3VN-C5UQehWY.js +1 -0
- package/workframe-ui/public/assets/chunk-WU5MYG2G-DhWllrI8.js +1 -0
- package/workframe-ui/public/assets/{chunk-XPW4576I-EFr8R_1p.js → chunk-XPW4576I-BClwIiCp.js} +1 -1
- package/workframe-ui/public/assets/classDiagram-4FO5ZUOK-BBM_8T8E.js +1 -0
- package/workframe-ui/public/assets/classDiagram-v2-Q7XG4LA2-BBM_8T8E.js +1 -0
- package/workframe-ui/public/assets/{cose-bilkent-S5V4N54A-C7aPBODd.js → cose-bilkent-S5V4N54A-DOrGV6DQ.js} +1 -1
- package/workframe-ui/public/assets/{dagre-BM42HDAG-BdU1Rv-H.js → dagre-BM42HDAG-DXTPvJkX.js} +1 -1
- package/workframe-ui/public/assets/{diagram-2AECGRRQ-DWowSo85.js → diagram-2AECGRRQ-xX_v-pbf.js} +1 -1
- package/workframe-ui/public/assets/{diagram-5GNKFQAL-MnxBbceO.js → diagram-5GNKFQAL-Cd2pXbBe.js} +1 -1
- package/workframe-ui/public/assets/{diagram-KO2AKTUF-DQaLRXFf.js → diagram-KO2AKTUF-Df3XvUtk.js} +1 -1
- package/workframe-ui/public/assets/{diagram-LMA3HP47-CQaBud9k.js → diagram-LMA3HP47-CsijIPaD.js} +1 -1
- package/workframe-ui/public/assets/{diagram-OG6HWLK6-D8bAXbY9.js → diagram-OG6HWLK6-aq5fmfHd.js} +1 -1
- package/workframe-ui/public/assets/{dist-DGpTLHr_.js → dist-D1c0mkbB.js} +1 -1
- package/workframe-ui/public/assets/{erDiagram-TEJ5UH35-1E-xSvBK.js → erDiagram-TEJ5UH35-DnFysVRY.js} +1 -1
- package/workframe-ui/public/assets/eventmodeling-FCH6USID-Ci8mdb44.js +1 -0
- package/workframe-ui/public/assets/{flowDiagram-I6XJVG4X-CgOVD5hu.js → flowDiagram-I6XJVG4X-C6Ebi3su.js} +1 -1
- package/workframe-ui/public/assets/{ganttDiagram-6RSMTGT7-JFYAIauo.js → ganttDiagram-6RSMTGT7-BQXQtUpa.js} +1 -1
- package/workframe-ui/public/assets/{gitGraph-WXDBUCRP-B9REenIl.js → gitGraph-WXDBUCRP-Dt0zIs_M.js} +1 -1
- package/workframe-ui/public/assets/{gitGraphDiagram-PVQCEYII-BQ7NcMSn.js → gitGraphDiagram-PVQCEYII-BF8gHzRn.js} +1 -1
- package/workframe-ui/public/assets/index-DpoUZAxh.css +1 -0
- package/workframe-ui/public/assets/{index-Dnw6vjqb.js → index-lRpzpNPT.js} +2 -2
- package/workframe-ui/public/assets/{info-J43DQDTF-CL6-eTjH.js → info-J43DQDTF-CSmszQJT.js} +1 -1
- package/workframe-ui/public/assets/{infoDiagram-5YYISTIA-LJTODW4W.js → infoDiagram-5YYISTIA-CVTKGW6p.js} +1 -1
- package/workframe-ui/public/assets/{ishikawaDiagram-YF4QCWOH-bchrQVuo.js → ishikawaDiagram-YF4QCWOH-Z8pT09Lv.js} +1 -1
- package/workframe-ui/public/assets/{journeyDiagram-JHISSGLW-DkrvYuxP.js → journeyDiagram-JHISSGLW-r3wD68_T.js} +1 -1
- package/workframe-ui/public/assets/{kanban-definition-UN3LZRKU-DFRbj0IG.js → kanban-definition-UN3LZRKU-Il8VglqN.js} +1 -1
- package/workframe-ui/public/assets/{line-Vd48P7-O.js → line-oyjpfz2A.js} +1 -1
- package/workframe-ui/public/assets/{linear-Ckizh2G7.js → linear-Cf7p5tVp.js} +1 -1
- package/workframe-ui/public/assets/{mermaid-parser.core-Bkimsnqj.js → mermaid-parser.core-YmbZ-AfY.js} +2 -2
- package/workframe-ui/public/assets/{mermaid.core-x0TvVuPo.js → mermaid.core-BFdCAqCo.js} +3 -3
- package/workframe-ui/public/assets/{mindmap-definition-RKZ34NQL-6ykAFPEz.js → mindmap-definition-RKZ34NQL-Cy2iCtEl.js} +1 -1
- package/workframe-ui/public/assets/{packet-YPE3B663-Dw3xgMDt.js → packet-YPE3B663-DwOBZL6K.js} +1 -1
- package/workframe-ui/public/assets/{pie-LRSECV5Y-DATysawG.js → pie-LRSECV5Y-04PPhnKK.js} +1 -1
- package/workframe-ui/public/assets/{pieDiagram-4H26LBE5-SJKD1S0S.js → pieDiagram-4H26LBE5-LxIpgHqi.js} +1 -1
- package/workframe-ui/public/assets/{quadrantDiagram-W4KKPZXB-BrYDZX8q.js → quadrantDiagram-W4KKPZXB-0nBYfYm4.js} +1 -1
- package/workframe-ui/public/assets/{radar-GUYGQ44K-BmWYPCds.js → radar-GUYGQ44K-D2-vBqps.js} +1 -1
- package/workframe-ui/public/assets/{requirementDiagram-4Y6WPE33-DwL9Mc8e.js → requirementDiagram-4Y6WPE33-DbuU0nlu.js} +1 -1
- package/workframe-ui/public/assets/{sankeyDiagram-5OEKKPKP-DYIFsL8h.js → sankeyDiagram-5OEKKPKP-B2hQ6B2x.js} +1 -1
- package/workframe-ui/public/assets/{sequenceDiagram-3UESZ5HK-0-FPkFk8.js → sequenceDiagram-3UESZ5HK-BBrU30e1.js} +1 -1
- package/workframe-ui/public/assets/{src-B_od6b6h.js → src-BJEDmV70.js} +1 -1
- package/workframe-ui/public/assets/{stateDiagram-AJRCARHV-BQCiBk6u.js → stateDiagram-AJRCARHV-7FGO4kkH.js} +1 -1
- package/workframe-ui/public/assets/stateDiagram-v2-BHNVJYJU-DLTSizMg.js +1 -0
- package/workframe-ui/public/assets/{timeline-definition-PNZ67QCA-DS3tFcXj.js → timeline-definition-PNZ67QCA-ptDm4rCN.js} +1 -1
- package/workframe-ui/public/assets/{treeView-BLDUP644-DSyUCKLY.js → treeView-BLDUP644-CS6Z-0q8.js} +1 -1
- package/workframe-ui/public/assets/{treemap-LRROVOQU-CEZaNh5Y.js → treemap-LRROVOQU-DqV4Y2VA.js} +1 -1
- package/workframe-ui/public/assets/{vennDiagram-CIIHVFJN-CD-Vc9NF.js → vennDiagram-CIIHVFJN-C0UrZJYt.js} +1 -1
- package/workframe-ui/public/assets/{wardley-L42UT6IY-Drq5w1Mc.js → wardley-L42UT6IY-bNDN3_Sa.js} +1 -1
- package/workframe-ui/public/assets/{wardleyDiagram-YWT4CUSO-DouXDJoF.js → wardleyDiagram-YWT4CUSO-jWiJsefM.js} +1 -1
- package/workframe-ui/public/assets/{xychartDiagram-2RQKCTM6-DDf_Lol5.js → xychartDiagram-2RQKCTM6-Dsh_fLCy.js} +1 -1
- package/workframe-ui/public/favicon.svg +7 -7
- package/workframe-ui/public/index.html +50 -50
- package/workframe-ui/public/workframe-config.json +3 -3
- package/scripts/security_audit.py +0 -156
- package/scripts/test-scaffold.mjs +0 -390
- package/workframe-api/tests/__init__.py +0 -0
- package/workframe-api/tests/db_setup.py +0 -13
- package/workframe-api/tests/test_admin_updates_gated.py +0 -30
- package/workframe-api/tests/test_agent_dm_bootstrap.py +0 -196
- package/workframe-api/tests/test_agent_profile_sync.py +0 -76
- package/workframe-api/tests/test_auth_email.py +0 -222
- package/workframe-api/tests/test_auth_hole_fix_selfcheck.py +0 -99
- package/workframe-api/tests/test_auth_rate_limit.py +0 -19
- package/workframe-api/tests/test_avatar_resolve.py +0 -77
- package/workframe-api/tests/test_child_soul_template.py +0 -71
- package/workframe-api/tests/test_credential_canary.py +0 -135
- package/workframe-api/tests/test_credential_isolation.py +0 -448
- package/workframe-api/tests/test_credential_resolution.py +0 -206
- package/workframe-api/tests/test_device_oauth.py +0 -108
- package/workframe-api/tests/test_doctor_repair.py +0 -103
- package/workframe-api/tests/test_ensure_profile_api.py +0 -77
- package/workframe-api/tests/test_gateway_compose_security.py +0 -136
- package/workframe-api/tests/test_install_secure_host.py +0 -39
- package/workframe-api/tests/test_internal_proxy_auth.py +0 -125
- package/workframe-api/tests/test_invite_runtime_bootstrap.py +0 -72
- package/workframe-api/tests/test_kanban_delegation.py +0 -185
- package/workframe-api/tests/test_llm_proxy.py +0 -155
- package/workframe-api/tests/test_login_access_policy.py +0 -183
- package/workframe-api/tests/test_mvp_model_bootstrap.py +0 -75
- package/workframe-api/tests/test_onboarding_bootstrap.py +0 -248
- package/workframe-api/tests/test_platform_auth.py +0 -47
- package/workframe-api/tests/test_profile_config_path.py +0 -56
- package/workframe-api/tests/test_profile_config_yaml_repair.py +0 -63
- package/workframe-api/tests/test_profile_create.py +0 -72
- package/workframe-api/tests/test_profile_identity_overlay.py +0 -61
- package/workframe-api/tests/test_profile_install_health.py +0 -45
- package/workframe-api/tests/test_profile_secret_policy.py +0 -57
- package/workframe-api/tests/test_profile_workspace_cwd.py +0 -34
- package/workframe-api/tests/test_provider_bootstrap.py +0 -75
- package/workframe-api/tests/test_provider_connect.py +0 -54
- package/workframe-api/tests/test_room_crud.py +0 -192
- package/workframe-api/tests/test_room_tenancy.py +0 -701
- package/workframe-api/tests/test_runtime_identity_backfill.py +0 -34
- package/workframe-api/tests/test_site_meta.py +0 -81
- package/workframe-api/tests/test_soul_stub.py +0 -42
- package/workframe-api/tests/test_space_member_sync.py +0 -99
- package/workframe-api/tests/test_stripe_stack_config.py +0 -37
- package/workframe-api/tests/test_supervisor_lifecycle.py +0 -52
- package/workframe-api/tests/test_turn_credential_vault.py +0 -125
- package/workframe-api/tests/test_updates.py +0 -176
- package/workframe-api/tests/test_user_cohort.py +0 -113
- package/workframe-api/tests/test_vault_envelope.py +0 -110
- package/workframe-api/tests/test_workspace_members.py +0 -183
- package/workframe-api/tests/test_workspace_messaging_sync.py +0 -125
- package/workframe-api/tests/test_workspace_provider_list.py +0 -57
- package/workframe-supervisor/tests/test_exec_guard.py +0 -42
- package/workframe-supervisor/tests/test_server_import.py +0 -21
- package/workframe-ui/public/assets/architecture-7EHR7CIX-CtbQKTuT.js +0 -1
- package/workframe-ui/public/assets/channel-Dy4Z4-jn.js +0 -1
- package/workframe-ui/public/assets/chunk-QZHKN3VN-CtBEchFK.js +0 -1
- package/workframe-ui/public/assets/chunk-WU5MYG2G-B9pBtriN.js +0 -1
- package/workframe-ui/public/assets/classDiagram-4FO5ZUOK-BMAEA8jI.js +0 -1
- package/workframe-ui/public/assets/classDiagram-v2-Q7XG4LA2-BMAEA8jI.js +0 -1
- package/workframe-ui/public/assets/eventmodeling-FCH6USID-D75cstNT.js +0 -1
- package/workframe-ui/public/assets/index-DpAGxump.css +0 -1
- package/workframe-ui/public/assets/stateDiagram-v2-BHNVJYJU-B89jAMFF.js +0 -1
|
@@ -1,41 +1,41 @@
|
|
|
1
|
-
{
|
|
2
|
-
"version": 2,
|
|
3
|
-
"public_base": "/assets/agents",
|
|
4
|
-
"avatars": [
|
|
5
|
-
{ "id": "ada", "file": "ada.png", "label": "Ada" },
|
|
6
|
-
{ "id": "aibert", "file": "aibert.png", "label": "Aibert" },
|
|
7
|
-
{ "id": "amelia", "file": "amelia.png", "label": "Amelia" },
|
|
8
|
-
{ "id": "andy", "file": "andy.png", "label": "Andy" },
|
|
9
|
-
{ "id": "bob", "file": "bob.png", "label": "Bob" },
|
|
10
|
-
{ "id": "buzz", "file": "buzz.png", "label": "Buzz" },
|
|
11
|
-
{ "id": "carl", "file": "carl.png", "label": "Carl" },
|
|
12
|
-
{ "id": "corbu", "file": "corbu.png", "label": "Corbu" },
|
|
13
|
-
{ "id": "diana", "file": "diana.png", "label": "Diana" },
|
|
14
|
-
{ "id": "ella", "file": "ella.png", "label": "Ella" },
|
|
15
|
-
{ "id": "elvis", "file": "elvis.png", "label": "Elvis" },
|
|
16
|
-
{ "id": "frida", "file": "frida.png", "label": "Frida" },
|
|
17
|
-
{ "id": "george", "file": "george.png", "label": "George" },
|
|
18
|
-
{ "id": "grace", "file": "grace.png", "label": "Grace" },
|
|
19
|
-
{ "id": "hedy", "file": "hedy.png", "label": "Hedy" },
|
|
20
|
-
{ "id": "isaac", "file": "isaac.png", "label": "Isaac" },
|
|
21
|
-
{ "id": "john", "file": "john.png", "label": "John" },
|
|
22
|
-
{ "id": "joni", "file": "joni.png", "label": "Joni" },
|
|
23
|
-
{ "id": "leo", "file": "leo.png", "label": "Leo" },
|
|
24
|
-
{ "id": "louis", "file": "louis.png", "label": "Louis" },
|
|
25
|
-
{ "id": "ludwig", "file": "ludwig.png", "label": "Ludwig" },
|
|
26
|
-
{ "id": "marie", "file": "marie.png", "label": "Marie" },
|
|
27
|
-
{ "id": "marilyn", "file": "marilyn.png", "label": "Marilyn" },
|
|
28
|
-
{ "id": "neil", "file": "neil.png", "label": "Neil" },
|
|
29
|
-
{ "id": "nikola", "file": "nikola.png", "label": "Nikola" },
|
|
30
|
-
{ "id": "nina", "file": "nina.png", "label": "Nina" },
|
|
31
|
-
{ "id": "paul", "file": "paul.png", "label": "Paul" },
|
|
32
|
-
{ "id": "ringo", "file": "ringo.png", "label": "Ringo" },
|
|
33
|
-
{ "id": "rosie", "file": "rosie.png", "label": "Rosie" },
|
|
34
|
-
{ "id": "steve", "file": "steve.png", "label": "Steve" },
|
|
35
|
-
{ "id": "sun", "file": "sun.png", "label": "Sun" },
|
|
36
|
-
{ "id": "tom", "file": "tom.png", "label": "Tom" },
|
|
37
|
-
{ "id": "warren", "file": "warren.png", "label": "Warren" },
|
|
38
|
-
{ "id": "woz", "file": "woz.png", "label": "Woz" },
|
|
39
|
-
{ "id": "zaha", "file": "zaha.png", "label": "Zaha" }
|
|
40
|
-
]
|
|
41
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"version": 2,
|
|
3
|
+
"public_base": "/assets/agents",
|
|
4
|
+
"avatars": [
|
|
5
|
+
{ "id": "ada", "file": "ada.png", "label": "Ada" },
|
|
6
|
+
{ "id": "aibert", "file": "aibert.png", "label": "Aibert" },
|
|
7
|
+
{ "id": "amelia", "file": "amelia.png", "label": "Amelia" },
|
|
8
|
+
{ "id": "andy", "file": "andy.png", "label": "Andy" },
|
|
9
|
+
{ "id": "bob", "file": "bob.png", "label": "Bob" },
|
|
10
|
+
{ "id": "buzz", "file": "buzz.png", "label": "Buzz" },
|
|
11
|
+
{ "id": "carl", "file": "carl.png", "label": "Carl" },
|
|
12
|
+
{ "id": "corbu", "file": "corbu.png", "label": "Corbu" },
|
|
13
|
+
{ "id": "diana", "file": "diana.png", "label": "Diana" },
|
|
14
|
+
{ "id": "ella", "file": "ella.png", "label": "Ella" },
|
|
15
|
+
{ "id": "elvis", "file": "elvis.png", "label": "Elvis" },
|
|
16
|
+
{ "id": "frida", "file": "frida.png", "label": "Frida" },
|
|
17
|
+
{ "id": "george", "file": "george.png", "label": "George" },
|
|
18
|
+
{ "id": "grace", "file": "grace.png", "label": "Grace" },
|
|
19
|
+
{ "id": "hedy", "file": "hedy.png", "label": "Hedy" },
|
|
20
|
+
{ "id": "isaac", "file": "isaac.png", "label": "Isaac" },
|
|
21
|
+
{ "id": "john", "file": "john.png", "label": "John" },
|
|
22
|
+
{ "id": "joni", "file": "joni.png", "label": "Joni" },
|
|
23
|
+
{ "id": "leo", "file": "leo.png", "label": "Leo" },
|
|
24
|
+
{ "id": "louis", "file": "louis.png", "label": "Louis" },
|
|
25
|
+
{ "id": "ludwig", "file": "ludwig.png", "label": "Ludwig" },
|
|
26
|
+
{ "id": "marie", "file": "marie.png", "label": "Marie" },
|
|
27
|
+
{ "id": "marilyn", "file": "marilyn.png", "label": "Marilyn" },
|
|
28
|
+
{ "id": "neil", "file": "neil.png", "label": "Neil" },
|
|
29
|
+
{ "id": "nikola", "file": "nikola.png", "label": "Nikola" },
|
|
30
|
+
{ "id": "nina", "file": "nina.png", "label": "Nina" },
|
|
31
|
+
{ "id": "paul", "file": "paul.png", "label": "Paul" },
|
|
32
|
+
{ "id": "ringo", "file": "ringo.png", "label": "Ringo" },
|
|
33
|
+
{ "id": "rosie", "file": "rosie.png", "label": "Rosie" },
|
|
34
|
+
{ "id": "steve", "file": "steve.png", "label": "Steve" },
|
|
35
|
+
{ "id": "sun", "file": "sun.png", "label": "Sun" },
|
|
36
|
+
{ "id": "tom", "file": "tom.png", "label": "Tom" },
|
|
37
|
+
{ "id": "warren", "file": "warren.png", "label": "Warren" },
|
|
38
|
+
{ "id": "woz", "file": "woz.png", "label": "Woz" },
|
|
39
|
+
{ "id": "zaha", "file": "zaha.png", "label": "Zaha" }
|
|
40
|
+
]
|
|
41
|
+
}
|
|
@@ -1,220 +1,220 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Email sender module for Workframe API.
|
|
3
|
-
Sends OTP verification emails via SMTP (env or stack_config).
|
|
4
|
-
"""
|
|
5
|
-
|
|
6
|
-
from __future__ import annotations
|
|
7
|
-
|
|
8
|
-
import os
|
|
9
|
-
import smtplib
|
|
10
|
-
from email.mime.multipart import MIMEMultipart
|
|
11
|
-
from email.mime.text import MIMEText
|
|
12
|
-
from typing import Any
|
|
13
|
-
|
|
14
|
-
import stack_config
|
|
15
|
-
|
|
16
|
-
APP_BASE_URL = os.environ.get("APP_BASE_URL", "http://127.0.0.1:18644").rstrip("/")
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
def _tls_flags(cfg: dict[str, Any]) -> tuple[bool, bool]:
|
|
20
|
-
port = int(cfg.get("port") or 587)
|
|
21
|
-
secure = stack_config.normalize_smtp_secure(port, str(cfg.get("secure") or ""))
|
|
22
|
-
use_ssl = secure == "ssl"
|
|
23
|
-
use_tls = secure == "starttls"
|
|
24
|
-
return use_ssl, use_tls
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
def _smtp_send(msg: MIMEMultipart, to_email: str, cfg: dict[str, Any]) -> None:
|
|
28
|
-
host = str(cfg.get("host") or "").strip()
|
|
29
|
-
if not host:
|
|
30
|
-
raise RuntimeError("SMTP_HOST not configured")
|
|
31
|
-
port = int(cfg.get("port") or 587)
|
|
32
|
-
user = str(cfg.get("user") or "").strip()
|
|
33
|
-
password = str(cfg.get("password") or "").strip().replace(" ", "")
|
|
34
|
-
from_addr = str(cfg.get("from") or user or "").strip()
|
|
35
|
-
if not from_addr:
|
|
36
|
-
raise RuntimeError("SMTP from address not configured")
|
|
37
|
-
if user and not password:
|
|
38
|
-
raise RuntimeError("SMTP password is required when SMTP user is set")
|
|
39
|
-
msg["From"] = from_addr
|
|
40
|
-
use_ssl, use_tls = _tls_flags(cfg)
|
|
41
|
-
try:
|
|
42
|
-
if use_ssl:
|
|
43
|
-
server = smtplib.SMTP_SSL(host, port, timeout=30)
|
|
44
|
-
else:
|
|
45
|
-
server = smtplib.SMTP(host, port, timeout=30)
|
|
46
|
-
with server:
|
|
47
|
-
server.ehlo()
|
|
48
|
-
if use_tls and not use_ssl:
|
|
49
|
-
server.starttls()
|
|
50
|
-
server.ehlo()
|
|
51
|
-
if user:
|
|
52
|
-
server.login(user, password)
|
|
53
|
-
server.sendmail(from_addr, [to_email], msg.as_string())
|
|
54
|
-
except smtplib.SMTPAuthenticationError as exc:
|
|
55
|
-
raise RuntimeError(f"SMTP login failed: {exc}") from exc
|
|
56
|
-
except smtplib.SMTPSenderRefused as exc:
|
|
57
|
-
sender = str(getattr(exc, "sender", "") or from_addr)
|
|
58
|
-
if user and sender.lower() != user.lower():
|
|
59
|
-
raise RuntimeError(
|
|
60
|
-
f"SMTP rejected From address {sender!r} for login {user!r}"
|
|
61
|
-
) from exc
|
|
62
|
-
raise RuntimeError(f"SMTP error: {exc}") from exc
|
|
63
|
-
except smtplib.SMTPException as exc:
|
|
64
|
-
raise RuntimeError(f"SMTP error: {exc}") from exc
|
|
65
|
-
except OSError as exc:
|
|
66
|
-
raise RuntimeError(f"Network error sending email: {exc}") from exc
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
def _active_smtp() -> dict[str, Any]:
|
|
70
|
-
cfg = stack_config.resolved_smtp()
|
|
71
|
-
if cfg.get("source") == "none":
|
|
72
|
-
return {}
|
|
73
|
-
return cfg
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
def send_email_with_config(
|
|
77
|
-
to_email: str,
|
|
78
|
-
subject: str,
|
|
79
|
-
body_text: str,
|
|
80
|
-
body_html: str = "",
|
|
81
|
-
cfg: dict[str, Any] | None = None,
|
|
82
|
-
) -> None:
|
|
83
|
-
smtp = cfg or _active_smtp()
|
|
84
|
-
if not smtp.get("host"):
|
|
85
|
-
raise RuntimeError("SMTP_HOST not configured")
|
|
86
|
-
msg = MIMEMultipart("alternative")
|
|
87
|
-
msg["Subject"] = subject
|
|
88
|
-
msg["To"] = to_email
|
|
89
|
-
msg.attach(MIMEText(body_text, "plain"))
|
|
90
|
-
if body_html:
|
|
91
|
-
msg.attach(MIMEText(body_html, "html"))
|
|
92
|
-
_smtp_send(msg, to_email, smtp)
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
def _branded_html(
|
|
96
|
-
*,
|
|
97
|
-
brand: str,
|
|
98
|
-
headline: str,
|
|
99
|
-
intro: str,
|
|
100
|
-
body_html: str,
|
|
101
|
-
logo_url: str = "",
|
|
102
|
-
footer: str = "If you didn't request this, you can safely ignore this email.",
|
|
103
|
-
) -> str:
|
|
104
|
-
logo_block = (
|
|
105
|
-
f'<img src="{logo_url}" alt="" style="height:40px;margin-bottom:12px;" />'
|
|
106
|
-
if logo_url
|
|
107
|
-
else ""
|
|
108
|
-
)
|
|
109
|
-
return f"""\
|
|
110
|
-
<html>
|
|
111
|
-
<body style="margin:0; background:#f6f7fb; font-family:-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; color:#1d2340;">
|
|
112
|
-
<div style="max-width:560px; margin:0 auto; padding:32px 20px;">
|
|
113
|
-
<div style="background:#ffffff; border:1px solid #e4e7f2; border-radius:20px; padding:32px; box-shadow:0 24px 64px rgba(14,20,49,0.08);">
|
|
114
|
-
{logo_block}
|
|
115
|
-
<div style="font-size:12px; font-weight:700; letter-spacing:0.14em; text-transform:uppercase; color:#6c5ce7; margin-bottom:12px;">{brand}</div>
|
|
116
|
-
<h1 style="margin:0 0 12px; font-size:28px; line-height:1.2;">{headline}</h1>
|
|
117
|
-
<p style="margin:0 0 24px; color:#4f5878; font-size:16px; line-height:1.6;">{intro}</p>
|
|
118
|
-
{body_html}
|
|
119
|
-
<p style="margin:24px 0 0; color:#7b849d; font-size:12px; line-height:1.6;">{footer}</p>
|
|
120
|
-
</div>
|
|
121
|
-
</div>
|
|
122
|
-
</body>
|
|
123
|
-
</html>
|
|
124
|
-
"""
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
def _code_block_html(code: str) -> str:
|
|
128
|
-
return f"""\
|
|
129
|
-
<div style="background:linear-gradient(180deg, #f5f7ff 0%, #eef1ff 100%); border:1px solid #d9defa; border-radius:16px; padding:18px 20px; text-align:center; margin-bottom:20px;">
|
|
130
|
-
<div style="font-size:12px; font-weight:700; letter-spacing:0.08em; text-transform:uppercase; color:#6b7280; margin-bottom:10px;">Verification code</div>
|
|
131
|
-
<div style="font-size:34px; font-weight:800; letter-spacing:10px; color:#111827; font-variant-numeric:tabular-nums;">{code}</div>
|
|
132
|
-
</div>
|
|
133
|
-
<p style="margin:0 0 20px; color:#4f5878; font-size:14px; line-height:1.6;">This code expires in 10 minutes.</p>
|
|
134
|
-
"""
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
def _cta_button_html(url: str, label: str) -> str:
|
|
138
|
-
return (
|
|
139
|
-
f'<a href="{url}" style="display:inline-block; padding:12px 20px; background:#6c5ce7; '
|
|
140
|
-
f'color:#ffffff; text-decoration:none; border-radius:10px; font-weight:700;">{label}</a>'
|
|
141
|
-
)
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
def _build_verification_email(
|
|
145
|
-
to_email: str,
|
|
146
|
-
code: str,
|
|
147
|
-
verification_url: str,
|
|
148
|
-
workspace_name: str = "Workframe",
|
|
149
|
-
logo_url: str = "",
|
|
150
|
-
) -> MIMEMultipart:
|
|
151
|
-
msg = MIMEMultipart("alternative")
|
|
152
|
-
brand = workspace_name or "Workframe"
|
|
153
|
-
msg["Subject"] = f"Your {brand} sign-in code"
|
|
154
|
-
msg["To"] = to_email
|
|
155
|
-
text_body = f"""\
|
|
156
|
-
{brand} sign-in code
|
|
157
|
-
|
|
158
|
-
Your verification code is:
|
|
159
|
-
|
|
160
|
-
{code}
|
|
161
|
-
|
|
162
|
-
This code expires in 10 minutes.
|
|
163
|
-
|
|
164
|
-
Or click the link to verify:
|
|
165
|
-
{verification_url}
|
|
166
|
-
|
|
167
|
-
If you didn't request this, ignore this email.
|
|
168
|
-
"""
|
|
169
|
-
body_html = _code_block_html(code) + _cta_button_html(verification_url, "Verify now")
|
|
170
|
-
html_body = _branded_html(
|
|
171
|
-
brand=brand,
|
|
172
|
-
headline="Sign in with your code",
|
|
173
|
-
intro="Use this one-time code to finish signing in to Workframe.",
|
|
174
|
-
body_html=body_html,
|
|
175
|
-
logo_url=logo_url,
|
|
176
|
-
)
|
|
177
|
-
msg.attach(MIMEText(text_body, "plain"))
|
|
178
|
-
msg.attach(MIMEText(html_body, "html"))
|
|
179
|
-
return msg
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
def send_verification_email(
|
|
183
|
-
to_email: str,
|
|
184
|
-
code: str,
|
|
185
|
-
verification_url: str,
|
|
186
|
-
workspace_name: str = "",
|
|
187
|
-
logo_url: str = "",
|
|
188
|
-
) -> None:
|
|
189
|
-
msg = _build_verification_email(to_email, code, verification_url, workspace_name, logo_url)
|
|
190
|
-
_smtp_send(msg, to_email, _active_smtp())
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
def send_email(to_email: str, subject: str, body_text: str, body_html: str = "") -> None:
|
|
194
|
-
send_email_with_config(to_email, subject, body_text, body_html)
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
def send_branded_invite_email(
|
|
198
|
-
to_email: str,
|
|
199
|
-
workspace_name: str,
|
|
200
|
-
invite_url: str,
|
|
201
|
-
logo_url: str = "",
|
|
202
|
-
) -> None:
|
|
203
|
-
brand = workspace_name or "Workframe"
|
|
204
|
-
subject = f"Join {brand} on Workframe"
|
|
205
|
-
text = f"You were invited to {brand}.\n\nAccept: {invite_url}\n"
|
|
206
|
-
body_html = _cta_button_html(invite_url, "Accept invite")
|
|
207
|
-
html = _branded_html(
|
|
208
|
-
brand=brand,
|
|
209
|
-
headline=f"Join {brand}",
|
|
210
|
-
intro="You were invited to collaborate on Workframe.",
|
|
211
|
-
body_html=body_html,
|
|
212
|
-
logo_url=logo_url,
|
|
213
|
-
footer="If you weren't expecting this invite, you can ignore this email.",
|
|
214
|
-
)
|
|
215
|
-
send_email_with_config(to_email, subject, text, html)
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
if __name__ == "__main__":
|
|
219
|
-
html = _build_verification_email("test@example.com", "123456", "http://127.0.0.1:18644/?code=123456").as_string()
|
|
220
|
-
assert "Verification code" in html and "Verify now" in html
|
|
1
|
+
"""
|
|
2
|
+
Email sender module for Workframe API.
|
|
3
|
+
Sends OTP verification emails via SMTP (env or stack_config).
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import smtplib
|
|
10
|
+
from email.mime.multipart import MIMEMultipart
|
|
11
|
+
from email.mime.text import MIMEText
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
import stack_config
|
|
15
|
+
|
|
16
|
+
APP_BASE_URL = os.environ.get("APP_BASE_URL", "http://127.0.0.1:18644").rstrip("/")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _tls_flags(cfg: dict[str, Any]) -> tuple[bool, bool]:
|
|
20
|
+
port = int(cfg.get("port") or 587)
|
|
21
|
+
secure = stack_config.normalize_smtp_secure(port, str(cfg.get("secure") or ""))
|
|
22
|
+
use_ssl = secure == "ssl"
|
|
23
|
+
use_tls = secure == "starttls"
|
|
24
|
+
return use_ssl, use_tls
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _smtp_send(msg: MIMEMultipart, to_email: str, cfg: dict[str, Any]) -> None:
|
|
28
|
+
host = str(cfg.get("host") or "").strip()
|
|
29
|
+
if not host:
|
|
30
|
+
raise RuntimeError("SMTP_HOST not configured")
|
|
31
|
+
port = int(cfg.get("port") or 587)
|
|
32
|
+
user = str(cfg.get("user") or "").strip()
|
|
33
|
+
password = str(cfg.get("password") or "").strip().replace(" ", "")
|
|
34
|
+
from_addr = str(cfg.get("from") or user or "").strip()
|
|
35
|
+
if not from_addr:
|
|
36
|
+
raise RuntimeError("SMTP from address not configured")
|
|
37
|
+
if user and not password:
|
|
38
|
+
raise RuntimeError("SMTP password is required when SMTP user is set")
|
|
39
|
+
msg["From"] = from_addr
|
|
40
|
+
use_ssl, use_tls = _tls_flags(cfg)
|
|
41
|
+
try:
|
|
42
|
+
if use_ssl:
|
|
43
|
+
server = smtplib.SMTP_SSL(host, port, timeout=30)
|
|
44
|
+
else:
|
|
45
|
+
server = smtplib.SMTP(host, port, timeout=30)
|
|
46
|
+
with server:
|
|
47
|
+
server.ehlo()
|
|
48
|
+
if use_tls and not use_ssl:
|
|
49
|
+
server.starttls()
|
|
50
|
+
server.ehlo()
|
|
51
|
+
if user:
|
|
52
|
+
server.login(user, password)
|
|
53
|
+
server.sendmail(from_addr, [to_email], msg.as_string())
|
|
54
|
+
except smtplib.SMTPAuthenticationError as exc:
|
|
55
|
+
raise RuntimeError(f"SMTP login failed: {exc}") from exc
|
|
56
|
+
except smtplib.SMTPSenderRefused as exc:
|
|
57
|
+
sender = str(getattr(exc, "sender", "") or from_addr)
|
|
58
|
+
if user and sender.lower() != user.lower():
|
|
59
|
+
raise RuntimeError(
|
|
60
|
+
f"SMTP rejected From address {sender!r} for login {user!r}"
|
|
61
|
+
) from exc
|
|
62
|
+
raise RuntimeError(f"SMTP error: {exc}") from exc
|
|
63
|
+
except smtplib.SMTPException as exc:
|
|
64
|
+
raise RuntimeError(f"SMTP error: {exc}") from exc
|
|
65
|
+
except OSError as exc:
|
|
66
|
+
raise RuntimeError(f"Network error sending email: {exc}") from exc
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _active_smtp() -> dict[str, Any]:
|
|
70
|
+
cfg = stack_config.resolved_smtp()
|
|
71
|
+
if cfg.get("source") == "none":
|
|
72
|
+
return {}
|
|
73
|
+
return cfg
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def send_email_with_config(
|
|
77
|
+
to_email: str,
|
|
78
|
+
subject: str,
|
|
79
|
+
body_text: str,
|
|
80
|
+
body_html: str = "",
|
|
81
|
+
cfg: dict[str, Any] | None = None,
|
|
82
|
+
) -> None:
|
|
83
|
+
smtp = cfg or _active_smtp()
|
|
84
|
+
if not smtp.get("host"):
|
|
85
|
+
raise RuntimeError("SMTP_HOST not configured")
|
|
86
|
+
msg = MIMEMultipart("alternative")
|
|
87
|
+
msg["Subject"] = subject
|
|
88
|
+
msg["To"] = to_email
|
|
89
|
+
msg.attach(MIMEText(body_text, "plain"))
|
|
90
|
+
if body_html:
|
|
91
|
+
msg.attach(MIMEText(body_html, "html"))
|
|
92
|
+
_smtp_send(msg, to_email, smtp)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _branded_html(
|
|
96
|
+
*,
|
|
97
|
+
brand: str,
|
|
98
|
+
headline: str,
|
|
99
|
+
intro: str,
|
|
100
|
+
body_html: str,
|
|
101
|
+
logo_url: str = "",
|
|
102
|
+
footer: str = "If you didn't request this, you can safely ignore this email.",
|
|
103
|
+
) -> str:
|
|
104
|
+
logo_block = (
|
|
105
|
+
f'<img src="{logo_url}" alt="" style="height:40px;margin-bottom:12px;" />'
|
|
106
|
+
if logo_url
|
|
107
|
+
else ""
|
|
108
|
+
)
|
|
109
|
+
return f"""\
|
|
110
|
+
<html>
|
|
111
|
+
<body style="margin:0; background:#f6f7fb; font-family:-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; color:#1d2340;">
|
|
112
|
+
<div style="max-width:560px; margin:0 auto; padding:32px 20px;">
|
|
113
|
+
<div style="background:#ffffff; border:1px solid #e4e7f2; border-radius:20px; padding:32px; box-shadow:0 24px 64px rgba(14,20,49,0.08);">
|
|
114
|
+
{logo_block}
|
|
115
|
+
<div style="font-size:12px; font-weight:700; letter-spacing:0.14em; text-transform:uppercase; color:#6c5ce7; margin-bottom:12px;">{brand}</div>
|
|
116
|
+
<h1 style="margin:0 0 12px; font-size:28px; line-height:1.2;">{headline}</h1>
|
|
117
|
+
<p style="margin:0 0 24px; color:#4f5878; font-size:16px; line-height:1.6;">{intro}</p>
|
|
118
|
+
{body_html}
|
|
119
|
+
<p style="margin:24px 0 0; color:#7b849d; font-size:12px; line-height:1.6;">{footer}</p>
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
</body>
|
|
123
|
+
</html>
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _code_block_html(code: str) -> str:
|
|
128
|
+
return f"""\
|
|
129
|
+
<div style="background:linear-gradient(180deg, #f5f7ff 0%, #eef1ff 100%); border:1px solid #d9defa; border-radius:16px; padding:18px 20px; text-align:center; margin-bottom:20px;">
|
|
130
|
+
<div style="font-size:12px; font-weight:700; letter-spacing:0.08em; text-transform:uppercase; color:#6b7280; margin-bottom:10px;">Verification code</div>
|
|
131
|
+
<div style="font-size:34px; font-weight:800; letter-spacing:10px; color:#111827; font-variant-numeric:tabular-nums;">{code}</div>
|
|
132
|
+
</div>
|
|
133
|
+
<p style="margin:0 0 20px; color:#4f5878; font-size:14px; line-height:1.6;">This code expires in 10 minutes.</p>
|
|
134
|
+
"""
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _cta_button_html(url: str, label: str) -> str:
|
|
138
|
+
return (
|
|
139
|
+
f'<a href="{url}" style="display:inline-block; padding:12px 20px; background:#6c5ce7; '
|
|
140
|
+
f'color:#ffffff; text-decoration:none; border-radius:10px; font-weight:700;">{label}</a>'
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _build_verification_email(
|
|
145
|
+
to_email: str,
|
|
146
|
+
code: str,
|
|
147
|
+
verification_url: str,
|
|
148
|
+
workspace_name: str = "Workframe",
|
|
149
|
+
logo_url: str = "",
|
|
150
|
+
) -> MIMEMultipart:
|
|
151
|
+
msg = MIMEMultipart("alternative")
|
|
152
|
+
brand = workspace_name or "Workframe"
|
|
153
|
+
msg["Subject"] = f"Your {brand} sign-in code"
|
|
154
|
+
msg["To"] = to_email
|
|
155
|
+
text_body = f"""\
|
|
156
|
+
{brand} sign-in code
|
|
157
|
+
|
|
158
|
+
Your verification code is:
|
|
159
|
+
|
|
160
|
+
{code}
|
|
161
|
+
|
|
162
|
+
This code expires in 10 minutes.
|
|
163
|
+
|
|
164
|
+
Or click the link to verify:
|
|
165
|
+
{verification_url}
|
|
166
|
+
|
|
167
|
+
If you didn't request this, ignore this email.
|
|
168
|
+
"""
|
|
169
|
+
body_html = _code_block_html(code) + _cta_button_html(verification_url, "Verify now")
|
|
170
|
+
html_body = _branded_html(
|
|
171
|
+
brand=brand,
|
|
172
|
+
headline="Sign in with your code",
|
|
173
|
+
intro="Use this one-time code to finish signing in to Workframe.",
|
|
174
|
+
body_html=body_html,
|
|
175
|
+
logo_url=logo_url,
|
|
176
|
+
)
|
|
177
|
+
msg.attach(MIMEText(text_body, "plain"))
|
|
178
|
+
msg.attach(MIMEText(html_body, "html"))
|
|
179
|
+
return msg
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def send_verification_email(
|
|
183
|
+
to_email: str,
|
|
184
|
+
code: str,
|
|
185
|
+
verification_url: str,
|
|
186
|
+
workspace_name: str = "",
|
|
187
|
+
logo_url: str = "",
|
|
188
|
+
) -> None:
|
|
189
|
+
msg = _build_verification_email(to_email, code, verification_url, workspace_name, logo_url)
|
|
190
|
+
_smtp_send(msg, to_email, _active_smtp())
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def send_email(to_email: str, subject: str, body_text: str, body_html: str = "") -> None:
|
|
194
|
+
send_email_with_config(to_email, subject, body_text, body_html)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def send_branded_invite_email(
|
|
198
|
+
to_email: str,
|
|
199
|
+
workspace_name: str,
|
|
200
|
+
invite_url: str,
|
|
201
|
+
logo_url: str = "",
|
|
202
|
+
) -> None:
|
|
203
|
+
brand = workspace_name or "Workframe"
|
|
204
|
+
subject = f"Join {brand} on Workframe"
|
|
205
|
+
text = f"You were invited to {brand}.\n\nAccept: {invite_url}\n"
|
|
206
|
+
body_html = _cta_button_html(invite_url, "Accept invite")
|
|
207
|
+
html = _branded_html(
|
|
208
|
+
brand=brand,
|
|
209
|
+
headline=f"Join {brand}",
|
|
210
|
+
intro="You were invited to collaborate on Workframe.",
|
|
211
|
+
body_html=body_html,
|
|
212
|
+
logo_url=logo_url,
|
|
213
|
+
footer="If you weren't expecting this invite, you can ignore this email.",
|
|
214
|
+
)
|
|
215
|
+
send_email_with_config(to_email, subject, text, html)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
if __name__ == "__main__":
|
|
219
|
+
html = _build_verification_email("test@example.com", "123456", "http://127.0.0.1:18644/?code=123456").as_string()
|
|
220
|
+
assert "Verification code" in html and "Verify now" in html
|