diff --git a/cmd/zitadel/setup.yaml b/cmd/zitadel/setup.yaml index 7a9115c0e5..8eff6e25f4 100644 --- a/cmd/zitadel/setup.yaml +++ b/cmd/zitadel/setup.yaml @@ -94,87 +94,6 @@ SetUp: Step10: DefaultMailTemplate: Template: PCFkb2N0eXBlIGh0bWw+CjxodG1sIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hodG1sIiB4bWxuczp2PSJ1cm46c2NoZW1hcy1taWNyb3NvZnQtY29tOnZtbCIgeG1sbnM6bz0idXJuOnNjaGVtYXMtbWljcm9zb2Z0LWNvbTpvZmZpY2U6b2ZmaWNlIj4KCjxoZWFkPgogICAgPHRpdGxlPiA8L3RpdGxlPgogICAgPCEtLVtpZiAhbXNvXT48IS0tIC0tPgogICAgPG1ldGEgaHR0cC1lcXVpdj0iWC1VQS1Db21wYXRpYmxlIiBjb250ZW50PSJJRT1lZGdlIj4KICAgIDwhLS08IVtlbmRpZl0tLT4KICAgIDxtZXRhIGh0dHAtZXF1aXY9IkNvbnRlbnQtVHlwZSIgY29udGVudD0idGV4dC9odG1sOyBjaGFyc2V0PVVURi04Ij4KICAgIDxtZXRhIG5hbWU9InZpZXdwb3J0IiBjb250ZW50PSJ3aWR0aD1kZXZpY2Utd2lkdGgsIGluaXRpYWwtc2NhbGU9MSI+CiAgICA8c3R5bGUgdHlwZT0idGV4dC9jc3MiPgogICAgICAgICNvdXRsb29rIGEgewogICAgICAgICAgICBwYWRkaW5nOiAwOwogICAgICAgIH0KCiAgICAgICAgYm9keSB7CiAgICAgICAgICAgIG1hcmdpbjogMDsKICAgICAgICAgICAgcGFkZGluZzogMDsKICAgICAgICAgICAgLXdlYmtpdC10ZXh0LXNpemUtYWRqdXN0OiAxMDAlOwogICAgICAgICAgICAtbXMtdGV4dC1zaXplLWFkanVzdDogMTAwJTsKICAgICAgICB9CgogICAgICAgIHRhYmxlLAogICAgICAgIHRkIHsKICAgICAgICAgICAgYm9yZGVyLWNvbGxhcHNlOiBjb2xsYXBzZTsKICAgICAgICAgICAgbXNvLXRhYmxlLWxzcGFjZTogMHB0OwogICAgICAgICAgICBtc28tdGFibGUtcnNwYWNlOiAwcHQ7CiAgICAgICAgfQoKICAgICAgICBpbWcgewogICAgICAgICAgICBib3JkZXI6IDA7CiAgICAgICAgICAgIGhlaWdodDogYXV0bzsKICAgICAgICAgICAgbGluZS1oZWlnaHQ6IDEwMCU7CiAgICAgICAgICAgIG91dGxpbmU6IG5vbmU7CiAgICAgICAgICAgIHRleHQtZGVjb3JhdGlvbjogbm9uZTsKICAgICAgICAgICAgLW1zLWludGVycG9sYXRpb24tbW9kZTogYmljdWJpYzsKICAgICAgICB9CgogICAgICAgIHAgewogICAgICAgICAgICBkaXNwbGF5OiBibG9jazsKICAgICAgICAgICAgbWFyZ2luOiAxM3B4IDA7CiAgICAgICAgfQogICAgPC9zdHlsZT4KICAgIDwhLS1baWYgbXNvXT4KICAgICAgICAgIDx4bWw+CiAgICAgICAgICA8bzpPZmZpY2VEb2N1bWVudFNldHRpbmdzPgogICAgICAgICAgICA8bzpBbGxvd1BORy8+CiAgICAgICAgICAgIDxvOlBpeGVsc1BlckluY2g+OTY8L286UGl4ZWxzUGVySW5jaD4KICAgICAgICAgIDwvbzpPZmZpY2VEb2N1bWVudFNldHRpbmdzPgogICAgICAgICAgPC94bWw+CiAgICAgICAgICA8IVtlbmRpZl0tLT4KICAgIDwhLS1baWYgbHRlIG1zbyAxMV0+CiAgICAgICAgICA8c3R5bGUgdHlwZT0idGV4dC9jc3MiPgogICAgICAgICAgICAubWotb3V0bG9vay1ncm91cC1maXggeyB3aWR0aDoxMDAlICFpbXBvcnRhbnQ7IH0KICAgICAgICAgIDwvc3R5bGU+CiAgICAgICAgICA8IVtlbmRpZl0tLT4KICAgIDwhLS1baWYgIW1zb10+PCEtLT4KICAgIDxsaW5rIGhyZWY9Imh0dHBzOi8vZm9udHMuZ29vZ2xlYXBpcy5jb20vY3NzP2ZhbWlseT1MYXRvOjMwMCw0MDAsNTAwLDcwMCIgcmVsPSJzdHlsZXNoZWV0IiB0eXBlPSJ0ZXh0L2NzcyI+CiAgICA8bGluayBocmVmPSJodHRwczovL2ZvbnRzLmdvb2dsZWFwaXMuY29tL2Nzcz9mYW1pbHk9VWJ1bnR1OjMwMCw0MDAsNTAwLDcwMCIgcmVsPSJzdHlsZXNoZWV0IiB0eXBlPSJ0ZXh0L2NzcyI+CiAgICA8c3R5bGUgdHlwZT0idGV4dC9jc3MiPgogICAgICAgIEBpbXBvcnQgdXJsKGh0dHBzOi8vZm9udHMuZ29vZ2xlYXBpcy5jb20vY3NzP2ZhbWlseT1MYXRvOjMwMCw0MDAsNTAwLDcwMCk7CiAgICAgICAgQGltcG9ydCB1cmwoaHR0cHM6Ly9mb250cy5nb29nbGVhcGlzLmNvbS9jc3M/ZmFtaWx5PVVidW50dTozMDAsNDAwLDUwMCw3MDApOwogICAgPC9zdHlsZT4KICAgIDwhLS08IVtlbmRpZl0tLT4KICAgIDxzdHlsZSB0eXBlPSJ0ZXh0L2NzcyI+CiAgICAgICAgQG1lZGlhIG9ubHkgc2NyZWVuIGFuZCAobWluLXdpZHRoOjQ4MHB4KSB7CiAgICAgICAgICAgIC5tai1jb2x1bW4tcGVyLTEwMCB7CiAgICAgICAgICAgICAgICB3aWR0aDogMTAwJSAhaW1wb3J0YW50OwogICAgICAgICAgICAgICAgbWF4LXdpZHRoOiAxMDAlOwogICAgICAgICAgICB9CiAgICAgICAgICAgIC5tai1jb2x1bW4tcGVyLTIwIHsKICAgICAgICAgICAgICAgIHdpZHRoOiAyMCUgIWltcG9ydGFudDsKICAgICAgICAgICAgICAgIG1heC13aWR0aDogMjAlOwogICAgICAgICAgICB9CiAgICAgICAgICAgIC5tai1jb2x1bW4tcGVyLTYwIHsKICAgICAgICAgICAgICAgIHdpZHRoOiA2MCUgIWltcG9ydGFudDsKICAgICAgICAgICAgICAgIG1heC13aWR0aDogNjAlOwogICAgICAgICAgICB9CiAgICAgICAgfQogICAgPC9zdHlsZT4KICAgIDxzdHlsZSB0eXBlPSJ0ZXh0L2NzcyI+CiAgICAgICAgQG1lZGlhIG9ubHkgc2NyZWVuIGFuZCAobWF4LXdpZHRoOjQ4MHB4KSB7CiAgICAgICAgICAgIHRhYmxlLm1qLWZ1bGwtd2lkdGgtbW9iaWxlIHsKICAgICAgICAgICAgICAgIHdpZHRoOiAxMDAlICFpbXBvcnRhbnQ7CiAgICAgICAgICAgIH0KICAgICAgICAgICAgdGQubWotZnVsbC13aWR0aC1tb2JpbGUgewogICAgICAgICAgICAgICAgd2lkdGg6IGF1dG8gIWltcG9ydGFudDsKICAgICAgICAgICAgfQogICAgICAgIH0KICAgIDwvc3R5bGU+CiAgICA8c3R5bGUgdHlwZT0idGV4dC9jc3MiPgogICAgICAgIEBtZWRpYSAobWF4LXdpZHRoOjQ4MHB4KSB7CiAgICAgICAgICAgIC5tb2JpbGVfaGlkZGVuIHsKICAgICAgICAgICAgICAgIGRpc3BsYXk6IG5vbmUgIWltcG9ydGFudDsKICAgICAgICAgICAgfQogICAgICAgIH0KICAgIDwvc3R5bGU+CjwvaGVhZD4KCjxib2R5IHN0eWxlPSJiYWNrZ3JvdW5kLWNvbG9yOnt7LlByaW1hcnlDb2xvcn19OyI+CjxkaXYgc3R5bGU9ImJhY2tncm91bmQtY29sb3I6e3suUHJpbWFyeUNvbG9yfX07Ij4KICAgIDx0YWJsZSBhbGlnbj0iY2VudGVyIiBiYWNrZ3JvdW5kPSJodHRwczovL3N0YXRpYy56aXRhZGVsLmNoL3ppdGFkZWwtbG9nby1vdXRsaW5lLWxpZ2h0LnBuZyIgYm9yZGVyPSIwIiBjZWxscGFkZGluZz0iMCIgY2VsbHNwYWNpbmc9IjAiIHJvbGU9InByZXNlbnRhdGlvbiIgc3R5bGU9ImJhY2tncm91bmQ6dXJsKGh0dHBzOi8vc3RhdGljLnppdGFkZWwuY2gveml0YWRlbC1sb2dvLW91dGxpbmUtbGlnaHQucG5nKSBjZW50ZXIgdG9wIC8gYXV0byBuby1yZXBlYXQ7YmFja2dyb3VuZC1wb3NpdGlvbjpjZW50ZXIgdG9wO2JhY2tncm91bmQtcmVwZWF0Om5vLXJlcGVhdDtiYWNrZ3JvdW5kLXNpemU6YXV0bzt3aWR0aDoxMDAlOyI+CiAgICAgICAgPHRib2R5PgogICAgICAgIDx0cj4KICAgICAgICAgICAgPHRkPgogICAgICAgICAgICAgICAgPCEtLVtpZiBtc28gfCBJRV0+CiAgICAgICAgICAgIDx2OnJlY3QgIHN0eWxlPSJtc28td2lkdGgtcGVyY2VudDoxMDAwOyIgeG1sbnM6dj0idXJuOnNjaGVtYXMtbWljcm9zb2Z0LWNvbTp2bWwiIGZpbGw9InRydWUiIHN0cm9rZT0iZmFsc2UiPgogICAgICAgICAgICA8djpmaWxsICBvcmlnaW49IjAuNSwgMCIgcG9zaXRpb249IjAuNSwgMCIgc3JjPSJodHRwczovL3N0YXRpYy56aXRhZGVsLmNoL3ppdGFkZWwtbG9nby1vdXRsaW5lLWxpZ2h0LnBuZyIgdHlwZT0idGlsZSIgLz4KICAgICAgICAgICAgPHY6dGV4dGJveCBzdHlsZT0ibXNvLWZpdC1zaGFwZS10by10ZXh0OnRydWUiIGluc2V0PSIwLDAsMCwwIj4KICAgICAgICAgIDx0YWJsZQogICAgICAgICAgICAgYWxpZ249ImNlbnRlciIgYm9yZGVyPSIwIiBjZWxscGFkZGluZz0iMCIgY2VsbHNwYWNpbmc9IjAiIGNsYXNzPSIiIHN0eWxlPSJ3aWR0aDo4MDBweDsiIHdpZHRoPSI4MDAiCiAgICAgICAgICA+CiAgICAgICAgICAgIDx0cj4KICAgICAgICAgICAgICA8dGQgc3R5bGU9ImxpbmUtaGVpZ2h0OjBweDtmb250LXNpemU6MHB4O21zby1saW5lLWhlaWdodC1ydWxlOmV4YWN0bHk7Ij4KICAgICAgICAgIDwhW2VuZGlmXS0tPgogICAgICAgICAgICAgICAgPGRpdiBzdHlsZT0ibWFyZ2luOjBweCBhdXRvO21heC13aWR0aDo4MDBweDsiPgogICAgICAgICAgICAgICAgICAgIDxkaXYgc3R5bGU9ImxpbmUtaGVpZ2h0OjA7Zm9udC1zaXplOjA7Ij4KICAgICAgICAgICAgICAgICAgICAgICAgPHRhYmxlIGFsaWduPSJjZW50ZXIiIGJvcmRlcj0iMCIgY2VsbHBhZGRpbmc9IjAiIGNlbGxzcGFjaW5nPSIwIiByb2xlPSJwcmVzZW50YXRpb24iIHN0eWxlPSJ3aWR0aDoxMDAlOyI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGJvZHk+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dHI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRkIHN0eWxlPSJib3JkZXI6MDtkaXJlY3Rpb246bHRyO2ZvbnQtc2l6ZTowcHg7cGFkZGluZzoyMHB4IDA7cGFkZGluZy1sZWZ0OjA7dGV4dC1hbGlnbjpjZW50ZXI7Ij4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPCEtLVtpZiBtc28gfCBJRV0+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0YWJsZSByb2xlPSJwcmVzZW50YXRpb24iIGJvcmRlcj0iMCIgY2VsbHBhZGRpbmc9IjAiIGNlbGxzcGFjaW5nPSIwIj4KICAgICAgICAgICAgICAgICAgICAgICAgPHRyPgogICAgICAgICAgICAgICAgICAgICAgICAgIDx0ZAogICAgICAgICAgICAgICAgICAgICAgICAgICAgIGNsYXNzPSIiIHdpZHRoPSI4MDBweCIKICAgICAgICAgICAgICAgICAgICAgICAgICA+CiAgICAgICAgICAgICAgICAgICAgICA8IVtlbmRpZl0tLT4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRhYmxlIGFsaWduPSJjZW50ZXIiIGJvcmRlcj0iMCIgY2VsbHBhZGRpbmc9IjAiIGNlbGxzcGFjaW5nPSIwIiByb2xlPSJwcmVzZW50YXRpb24iIHN0eWxlPSJ3aWR0aDoxMDAlOyI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGJvZHk+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dHI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRkPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8IS0tW2lmIG1zbyB8IElFXT4KICAgICAgICAgICAgICAgICAgICAgIDx0YWJsZQogICAgICAgICAgICAgICAgICAgICAgICAgYWxpZ249ImNlbnRlciIgYm9yZGVyPSIwIiBjZWxscGFkZGluZz0iMCIgY2VsbHNwYWNpbmc9IjAiIGNsYXNzPSIiIHN0eWxlPSJ3aWR0aDo4MDBweDsiIHdpZHRoPSI4MDAiCiAgICAgICAgICAgICAgICAgICAgICA+CiAgICAgICAgICAgICAgICAgICAgICAgIDx0cj4KICAgICAgICAgICAgICAgICAgICAgICAgICA8dGQgc3R5bGU9ImxpbmUtaGVpZ2h0OjBweDtmb250LXNpemU6MHB4O21zby1saW5lLWhlaWdodC1ydWxlOmV4YWN0bHk7Ij4KICAgICAgICAgICAgICAgICAgICAgIDwhW2VuZGlmXS0tPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8ZGl2IHN0eWxlPSJtYXJnaW46MHB4IGF1dG87bWF4LXdpZHRoOjgwMHB4OyI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGFibGUgYWxpZ249ImNlbnRlciIgYm9yZGVyPSIwIiBjZWxscGFkZGluZz0iMCIgY2VsbHNwYWNpbmc9IjAiIHJvbGU9InByZXNlbnRhdGlvbiIgc3R5bGU9IndpZHRoOjEwMCU7Ij4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGJvZHk+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRyPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGQgc3R5bGU9ImRpcmVjdGlvbjpsdHI7Zm9udC1zaXplOjBweDtwYWRkaW5nOjA7dGV4dC1hbGlnbjpjZW50ZXI7Ij4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwhLS1baWYgbXNvIHwgSUVdPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRhYmxlIHJvbGU9InByZXNlbnRhdGlvbiIgYm9yZGVyPSIwIiBjZWxscGFkZGluZz0iMCIgY2VsbHNwYWNpbmc9IjAiPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dHI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGQKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGNsYXNzPSIiIHN0eWxlPSJ3aWR0aDo4MDBweDsiCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPCFbZW5kaWZdLS0+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8ZGl2IGNsYXNzPSJtai1jb2x1bW4tcGVyLTEwMCBtai1vdXRsb29rLWdyb3VwLWZpeCIgc3R5bGU9ImZvbnQtc2l6ZTowO2xpbmUtaGVpZ2h0OjA7dGV4dC1hbGlnbjpsZWZ0O2Rpc3BsYXk6aW5saW5lLWJsb2NrO3dpZHRoOjEwMCU7ZGlyZWN0aW9uOmx0cjsiPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwhLS1baWYgbXNvIHwgSUVdPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0YWJsZQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGJvcmRlcj0iMCIgY2VsbHBhZGRpbmc9IjAiIGNlbGxzcGFjaW5nPSIwIiByb2xlPSJwcmVzZW50YXRpb24iCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRyPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0ZAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHN0eWxlPSJ2ZXJ0aWNhbC1hbGlnbjp0b3A7d2lkdGg6ODAwcHg7IgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgID4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8IVtlbmRpZl0tLT4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8ZGl2IGNsYXNzPSJtai1jb2x1bW4tcGVyLTEwMCBtai1vdXRsb29rLWdyb3VwLWZpeCIgc3R5bGU9ImZvbnQtc2l6ZTowcHg7dGV4dC1hbGlnbjpsZWZ0O2RpcmVjdGlvbjpsdHI7ZGlzcGxheTppbmxpbmUtYmxvY2s7dmVydGljYWwtYWxpZ246dG9wO3dpZHRoOjEwMCU7Ij4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRhYmxlIGJvcmRlcj0iMCIgY2VsbHBhZGRpbmc9IjAiIGNlbGxzcGFjaW5nPSIwIiByb2xlPSJwcmVzZW50YXRpb24iIHdpZHRoPSIxMDAlIj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0Ym9keT4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGQgc3R5bGU9InZlcnRpY2FsLWFsaWduOnRvcDtwYWRkaW5nOjA7Ij4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRhYmxlIGJvcmRlcj0iMCIgY2VsbHBhZGRpbmc9IjAiIGNlbGxzcGFjaW5nPSIwIiByb2xlPSJwcmVzZW50YXRpb24iIHN0eWxlPSIiIHdpZHRoPSIxMDAlIj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGQgYWxpZ249ImxlZnQiIHN0eWxlPSJmb250LXNpemU6MHB4O3BhZGRpbmc6MjBweCAwIDUwcHggMjBweDt3b3JkLWJyZWFrOmJyZWFrLXdvcmQ7Ij4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRhYmxlIGJvcmRlcj0iMCIgY2VsbHBhZGRpbmc9IjAiIGNlbGxzcGFjaW5nPSIwIiByb2xlPSJwcmVzZW50YXRpb24iIHN0eWxlPSJib3JkZXItY29sbGFwc2U6Y29sbGFwc2U7Ym9yZGVyLXNwYWNpbmc6MHB4OyI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGJvZHk+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dHI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRkIHN0eWxlPSJ3aWR0aDoxNTBweDsiPiA8aW1nIGhlaWdodD0iYXV0byIgc3JjPSJodHRwczovL3N0YXRpYy56aXRhZGVsLmNoL3ppdGFkZWwtbG9nby1saWdodC5wbmciIHN0eWxlPSJib3JkZXI6MDtkaXNwbGF5OmJsb2NrO291dGxpbmU6bm9uZTt0ZXh0LWRlY29yYXRpb246bm9uZTtoZWlnaHQ6YXV0bzt3aWR0aDoxMDAlO2ZvbnQtc2l6ZToxM3B4OyIgd2lkdGg9IjE1MCIKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgLz4gPC90ZD4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvdHI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L3Rib2R5PgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L3RhYmxlPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvdGQ+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L3RyPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L3RhYmxlPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvdGQ+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L3RyPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC90Ym9keT4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC90YWJsZT4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L2Rpdj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8IS0tW2lmIG1zbyB8IElFXT4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L3RkPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC90cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvdGFibGU+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPCFbZW5kaWZdLS0+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L2Rpdj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwhLS1baWYgbXNvIHwgSUVdPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC90ZD4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC90cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvdGFibGU+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPCFbZW5kaWZdLS0+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvdGQ+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC90cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L3Rib2R5PgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC90YWJsZT4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC9kaXY+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwhLS1baWYgbXNvIHwgSUVdPgogICAgICAgICAgICAgICAgICAgICAgICAgIDwvdGQ+CiAgICAgICAgICAgICAgICAgICAgICAgIDwvdHI+CiAgICAgICAgICAgICAgICAgICAgICA8L3RhYmxlPgogICAgICAgICAgICAgICAgICAgICAgPCFbZW5kaWZdLS0+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC90ZD4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvdHI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L3Rib2R5PgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L3RhYmxlPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8IS0tW2lmIG1zbyB8IElFXT4KICAgICAgICAgICAgICAgICAgICAgICAgICA8L3RkPgogICAgICAgICAgICAgICAgICAgICAgICA8L3RyPgogICAgICAgICAgICAgICAgICAgICAgICA8dHI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgPHRkCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgY2xhc3M9IiIgd2lkdGg9IjgwMHB4IgogICAgICAgICAgICAgICAgICAgICAgICAgID4KICAgICAgICAgICAgICAgICAgICAgIDwhW2VuZGlmXS0tPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGFibGUgYWxpZ249ImNlbnRlciIgYm9yZGVyPSIwIiBjZWxscGFkZGluZz0iMCIgY2VsbHNwYWNpbmc9IjAiIHJvbGU9InByZXNlbnRhdGlvbiIgc3R5bGU9IndpZHRoOjEwMCU7Ij4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0Ym9keT4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGQ+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwhLS1baWYgbXNvIHwgSUVdPgogICAgICAgICAgICAgICAgICAgICAgPHRhYmxlCiAgICAgICAgICAgICAgICAgICAgICAgICBhbGlnbj0iY2VudGVyIiBib3JkZXI9IjAiIGNlbGxwYWRkaW5nPSIwIiBjZWxsc3BhY2luZz0iMCIgY2xhc3M9IiIgc3R5bGU9IndpZHRoOjgwMHB4OyIgd2lkdGg9IjgwMCIKICAgICAgICAgICAgICAgICAgICAgID4KICAgICAgICAgICAgICAgICAgICAgICAgPHRyPgogICAgICAgICAgICAgICAgICAgICAgICAgIDx0ZCBzdHlsZT0ibGluZS1oZWlnaHQ6MHB4O2ZvbnQtc2l6ZTowcHg7bXNvLWxpbmUtaGVpZ2h0LXJ1bGU6ZXhhY3RseTsiPgogICAgICAgICAgICAgICAgICAgICAgPCFbZW5kaWZdLS0+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDxkaXYgc3R5bGU9Im1hcmdpbjowcHggYXV0bzttYXgtd2lkdGg6ODAwcHg7Ij4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0YWJsZSBhbGlnbj0iY2VudGVyIiBib3JkZXI9IjAiIGNlbGxwYWRkaW5nPSIwIiBjZWxsc3BhY2luZz0iMCIgcm9sZT0icHJlc2VudGF0aW9uIiBzdHlsZT0id2lkdGg6MTAwJTsiPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0Ym9keT4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dHI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0ZCBzdHlsZT0iZGlyZWN0aW9uOmx0cjtmb250LXNpemU6MHB4O3BhZGRpbmc6MDt0ZXh0LWFsaWduOmNlbnRlcjsiPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPCEtLVtpZiBtc28gfCBJRV0+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGFibGUgcm9sZT0icHJlc2VudGF0aW9uIiBib3JkZXI9IjAiIGNlbGxwYWRkaW5nPSIwIiBjZWxsc3BhY2luZz0iMCI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0ZAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgY2xhc3M9Im1vYmlsZV9oaWRkZW4tb3V0bG9vayIgc3R5bGU9InZlcnRpY2FsLWFsaWduOnRvcDt3aWR0aDoxNjBweDsiCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPCFbZW5kaWZdLS0+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8ZGl2IGNsYXNzPSJtai1jb2x1bW4tcGVyLTIwIG1qLW91dGxvb2stZ3JvdXAtZml4IG1vYmlsZV9oaWRkZW4iIHN0eWxlPSJmb250LXNpemU6MHB4O3RleHQtYWxpZ246bGVmdDtkaXJlY3Rpb246bHRyO2Rpc3BsYXk6aW5saW5lLWJsb2NrO3ZlcnRpY2FsLWFsaWduOnRvcDt3aWR0aDoxMDAlOyI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRhYmxlIGJvcmRlcj0iMCIgY2VsbHBhZGRpbmc9IjAiIGNlbGxzcGFjaW5nPSIwIiByb2xlPSJwcmVzZW50YXRpb24iIHdpZHRoPSIxMDAlIj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRib2R5PgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dHI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGQgc3R5bGU9InZlcnRpY2FsLWFsaWduOnRvcDtwYWRkaW5nOjA7Ij4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGFibGUgYm9yZGVyPSIwIiBjZWxscGFkZGluZz0iMCIgY2VsbHNwYWNpbmc9IjAiIHJvbGU9InByZXNlbnRhdGlvbiIgc3R5bGU9IiIgd2lkdGg9IjEwMCUiPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dHI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGQgYWxpZ249ImxlZnQiIHN0eWxlPSJmb250LXNpemU6MHB4O3BhZGRpbmc6MDt3b3JkLWJyZWFrOmJyZWFrLXdvcmQ7Ij4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGFibGUgYm9yZGVyPSIwIiBjZWxscGFkZGluZz0iMCIgY2VsbHNwYWNpbmc9IjAiIHJvbGU9InByZXNlbnRhdGlvbiIgc3R5bGU9ImJvcmRlci1jb2xsYXBzZTpjb2xsYXBzZTtib3JkZXItc3BhY2luZzowcHg7Ij4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRib2R5PgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dHI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGQgc3R5bGU9IndpZHRoOjgwcHg7Ij4gPGltZyBoZWlnaHQ9IjEwMCIgc3JjPSJodHRwczovL3N0YXRpYy56aXRhZGVsLmNoL2ZsYXZvci1zcGlrZXMtc21hbGwtb3BhY2l0eTQwLnBuZyIgc3R5bGU9ImJvcmRlcjowO2Rpc3BsYXk6YmxvY2s7b3V0bGluZTpub25lO3RleHQtZGVjb3JhdGlvbjpub25lO2hlaWdodDoxMDAlO3dpZHRoOjEwMCU7Zm9udC1zaXplOjEzcHg7IgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgd2lkdGg9IjgwIiAvPiA8L3RkPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L3RyPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L3Rib2R5PgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvdGFibGU+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L3RkPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L3RyPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvdGFibGU+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L3RkPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L3RyPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L3Rib2R5PgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvdGFibGU+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L2Rpdj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwhLS1baWYgbXNvIHwgSUVdPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC90ZD4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0ZAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgY2xhc3M9IiIgc3R5bGU9InZlcnRpY2FsLWFsaWduOnRvcDt3aWR0aDo0ODBweDsiCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPCFbZW5kaWZdLS0+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8ZGl2IGNsYXNzPSJtai1jb2x1bW4tcGVyLTYwIG1qLW91dGxvb2stZ3JvdXAtZml4IiBzdHlsZT0iZm9udC1zaXplOjBweDt0ZXh0LWFsaWduOmxlZnQ7ZGlyZWN0aW9uOmx0cjtkaXNwbGF5OmlubGluZS1ibG9jazt2ZXJ0aWNhbC1hbGlnbjp0b3A7d2lkdGg6MTAwJTsiPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0YWJsZSBib3JkZXI9IjAiIGNlbGxwYWRkaW5nPSIwIiBjZWxsc3BhY2luZz0iMCIgcm9sZT0icHJlc2VudGF0aW9uIiB3aWR0aD0iMTAwJSI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0Ym9keT4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRyPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRkIHN0eWxlPSJ2ZXJ0aWNhbC1hbGlnbjp0b3A7cGFkZGluZzowOyI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRhYmxlIGJvcmRlcj0iMCIgY2VsbHBhZGRpbmc9IjAiIGNlbGxzcGFjaW5nPSIwIiByb2xlPSJwcmVzZW50YXRpb24iIHN0eWxlPSIiIHdpZHRoPSIxMDAlIj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRyPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRkIGFsaWduPSJjZW50ZXIiIHN0eWxlPSJmb250LXNpemU6MHB4O3BhZGRpbmc6MTBweCAyNXB4O3dvcmQtYnJlYWs6YnJlYWstd29yZDsiPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDxkaXYgc3R5bGU9ImZvbnQtZmFtaWx5OkxhdG8sIEFyaWFsLCBIZWx2ZXRpY2EsIHNhbnMtc2VyaWY7Zm9udC1zaXplOjJyZW07Zm9udC13ZWlnaHQ6MjAwO2xpbmUtaGVpZ2h0OjE7dGV4dC1hbGlnbjpjZW50ZXI7Y29sb3I6e3suU2Vjb25kYXJ5Q29sb3J9fTsiPnt7LkdyZWV0aW5nfX08L2Rpdj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvdGQ+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvdHI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0ZCBhbGlnbj0iY2VudGVyIiBzdHlsZT0iZm9udC1zaXplOjBweDtwYWRkaW5nOjEwcHggMjVweDt3b3JkLWJyZWFrOmJyZWFrLXdvcmQ7Ij4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8ZGl2IHN0eWxlPSJmb250LWZhbWlseTpMYXRvLCBBcmlhbCwgSGVsdmV0aWNhLCBzYW5zLXNlcmlmO2ZvbnQtc2l6ZToxcmVtO2ZvbnQtd2VpZ2h0OmxpZ2h0O2xpbmUtaGVpZ2h0OjEuNTt0ZXh0LWFsaWduOmNlbnRlcjtjb2xvcjp7ey5TZWNvbmRhcnlDb2xvcn19OyI+e3suVGV4dH19PC9kaXY+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L3RkPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L3RyPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dHI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGQgYWxpZ249ImNlbnRlciIgdmVydGljYWwtYWxpZ249Im1pZGRsZSIgc3R5bGU9ImZvbnQtc2l6ZTowcHg7cGFkZGluZzoxMHB4IDI1cHg7d29yZC1icmVhazpicmVhay13b3JkOyI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRhYmxlIGJvcmRlcj0iMCIgY2VsbHBhZGRpbmc9IjAiIGNlbGxzcGFjaW5nPSIwIiByb2xlPSJwcmVzZW50YXRpb24iIHN0eWxlPSJib3JkZXItY29sbGFwc2U6c2VwYXJhdGU7bGluZS1oZWlnaHQ6MTAwJTsiPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dHI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGQgYWxpZ249ImNlbnRlciIgYmdjb2xvcj0iIzUyODJDMSIgcm9sZT0icHJlc2VudGF0aW9uIiBzdHlsZT0iYm9yZGVyOm5vbmU7Ym9yZGVyLXJhZGl1czozcHg7Y3Vyc29yOmF1dG87bXNvLXBhZGRpbmctYWx0OjEwcHggMjVweDtiYWNrZ3JvdW5kOiM1MjgyQzE7IiB2YWxpZ249Im1pZGRsZSI+IDxhIGhyZWY9Int7LlVSTH19IiBzdHlsZT0iZGlzcGxheTppbmxpbmUtYmxvY2s7YmFja2dyb3VuZDojNTI4MkMxO2NvbG9yOiNmZmZmZmY7Zm9udC1mYW1pbHk6VWJ1bnR1LCBIZWx2ZXRpY2EsIEFyaWFsLCBzYW5zLXNlcmlmO2ZvbnQtc2l6ZToxNnB4O2ZvbnQtd2VpZ2h0Om5vcm1hbDtsaW5lLWhlaWdodDoxMjAlO21hcmdpbjowO3RleHQtZGVjb3JhdGlvbjpub25lO3RleHQtdHJhbnNmb3JtOm5vbmU7cGFkZGluZzoxMHB4IDI1cHg7bXNvLXBhZGRpbmctYWx0OjBweDtib3JkZXItcmFkaXVzOjNweDsiCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHRhcmdldD0iX2JsYW5rIj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAge3suQnV0dG9uVGV4dH19CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC9hPiA8L3RkPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L3RyPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvdGFibGU+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L3RkPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L3RyPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dHI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGQgYWxpZ249ImNlbnRlciIgc3R5bGU9ImZvbnQtc2l6ZTowcHg7cGFkZGluZzozMHB4IDA7d29yZC1icmVhazpicmVhay13b3JkOyI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPGRpdiBzdHlsZT0iZm9udC1mYW1pbHk6TGF0bywgQXJpYWwsIEhlbHZldGljYSwgc2Fucy1zZXJpZjtmb250LXNpemU6MTNweDtsaW5lLWhlaWdodDoxO3RleHQtYWxpZ246Y2VudGVyO2NvbG9yOnt7LlNlY29uZGFyeUNvbG9yfX07Ij48YSBocmVmPSJodHRwOi8vd3d3LmNhb3MuY2giIHN0eWxlPSJjb2xvcjojZTkxZTYzOyB0ZXh0LWRlY29yYXRpb246IG5vbmU7IiB0YXJnZXQ9Il9ibGFuayI+IENBT1MgQUcgPC9hPiB8IFRldWZlbmVyIFN0cmFzc2UgMTkgfCBDSC05MDAwIFN0LiBHYWxsZW48L2Rpdj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvdGQ+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvdHI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC90YWJsZT4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvdGQ+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvdHI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvdGJvZHk+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC90YWJsZT4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvZGl2PgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPCEtLVtpZiBtc28gfCBJRV0+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L3RkPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRkCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBjbGFzcz0ibW9iaWxlX2hpZGRlbi1vdXRsb29rIiBzdHlsZT0idmVydGljYWwtYWxpZ246dG9wO3dpZHRoOjE2MHB4OyIKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgID4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8IVtlbmRpZl0tLT4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDxkaXYgY2xhc3M9Im1qLWNvbHVtbi1wZXItMjAgbWotb3V0bG9vay1ncm91cC1maXggbW9iaWxlX2hpZGRlbiIgc3R5bGU9ImZvbnQtc2l6ZTowcHg7dGV4dC1hbGlnbjpsZWZ0O2RpcmVjdGlvbjpsdHI7ZGlzcGxheTppbmxpbmUtYmxvY2s7dmVydGljYWwtYWxpZ246dG9wO3dpZHRoOjEwMCU7Ij4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGFibGUgYm9yZGVyPSIwIiBjZWxscGFkZGluZz0iMCIgY2VsbHNwYWNpbmc9IjAiIHJvbGU9InByZXNlbnRhdGlvbiIgd2lkdGg9IjEwMCUiPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGJvZHk+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0ZCBzdHlsZT0idmVydGljYWwtYWxpZ246dG9wO3BhZGRpbmc6MDsiPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0YWJsZSBib3JkZXI9IjAiIGNlbGxwYWRkaW5nPSIwIiBjZWxsc3BhY2luZz0iMCIgcm9sZT0icHJlc2VudGF0aW9uIiBzdHlsZT0iIiB3aWR0aD0iMTAwJSI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0ZCBhbGlnbj0icmlnaHQiIHN0eWxlPSJmb250LXNpemU6MHB4O3BhZGRpbmc6MDt3b3JkLWJyZWFrOmJyZWFrLXdvcmQ7Ij4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGFibGUgYm9yZGVyPSIwIiBjZWxscGFkZGluZz0iMCIgY2VsbHNwYWNpbmc9IjAiIHJvbGU9InByZXNlbnRhdGlvbiIgc3R5bGU9ImJvcmRlci1jb2xsYXBzZTpjb2xsYXBzZTtib3JkZXItc3BhY2luZzowcHg7Ij4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRib2R5PgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dHI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGQgc3R5bGU9IndpZHRoOjE2MHB4OyI+IDxpbWcgaGVpZ2h0PSJhdXRvIiBzcmM9Imh0dHBzOi8vc3RhdGljLnppdGFkZWwuY2gvZmxhdm9yLXNwaWtlcy1iaWctb3BhY2l0eTQwLnBuZyIgc3R5bGU9ImJvcmRlcjowO2Rpc3BsYXk6YmxvY2s7b3V0bGluZTpub25lO3RleHQtZGVjb3JhdGlvbjpub25lO2hlaWdodDphdXRvO3dpZHRoOjEwMCU7Zm9udC1zaXplOjEzcHg7IgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHdpZHRoPSIxNjAiIC8+IDwvdGQ+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvdHI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvdGJvZHk+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC90YWJsZT4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvdGQ+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvdHI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC90YWJsZT4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvdGQ+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvdHI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvdGJvZHk+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC90YWJsZT4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvZGl2PgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPCEtLVtpZiBtc28gfCBJRV0+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L3RkPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L3RyPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC90YWJsZT4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8IVtlbmRpZl0tLT4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC90ZD4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L3RyPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvdGJvZHk+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L3RhYmxlPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L2Rpdj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPCEtLVtpZiBtc28gfCBJRV0+CiAgICAgICAgICAgICAgICAgICAgICAgICAgPC90ZD4KICAgICAgICAgICAgICAgICAgICAgICAgPC90cj4KICAgICAgICAgICAgICAgICAgICAgIDwvdGFibGU+CiAgICAgICAgICAgICAgICAgICAgICA8IVtlbmRpZl0tLT4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L3RkPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC90cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvdGJvZHk+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvdGFibGU+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwhLS1baWYgbXNvIHwgSUVdPgogICAgICAgICAgICAgICAgICAgICAgICAgIDwvdGQ+CiAgICAgICAgICAgICAgICAgICAgICAgIDwvdHI+CiAgICAgICAgICAgICAgICAgICAgICAgIDx0cj4KICAgICAgICAgICAgICAgICAgICAgICAgICA8dGQKICAgICAgICAgICAgICAgICAgICAgICAgICAgICBjbGFzcz0iIiB3aWR0aD0iODAwcHgiCiAgICAgICAgICAgICAgICAgICAgICAgICAgPgogICAgICAgICAgICAgICAgICAgICAgPCFbZW5kaWZdLS0+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0YWJsZSBhbGlnbj0iY2VudGVyIiBib3JkZXI9IjAiIGNlbGxwYWRkaW5nPSIwIiBjZWxsc3BhY2luZz0iMCIgcm9sZT0icHJlc2VudGF0aW9uIiBzdHlsZT0id2lkdGg6MTAwJTsiPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRib2R5PgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRyPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0ZD4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPCEtLVtpZiBtc28gfCBJRV0+CiAgICAgICAgICAgICAgICAgICAgICA8dGFibGUKICAgICAgICAgICAgICAgICAgICAgICAgIGFsaWduPSJjZW50ZXIiIGJvcmRlcj0iMCIgY2VsbHBhZGRpbmc9IjAiIGNlbGxzcGFjaW5nPSIwIiBjbGFzcz0iIiBzdHlsZT0id2lkdGg6ODAwcHg7IiB3aWR0aD0iODAwIgogICAgICAgICAgICAgICAgICAgICAgPgogICAgICAgICAgICAgICAgICAgICAgICA8dHI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgPHRkIHN0eWxlPSJsaW5lLWhlaWdodDowcHg7Zm9udC1zaXplOjBweDttc28tbGluZS1oZWlnaHQtcnVsZTpleGFjdGx5OyI+CiAgICAgICAgICAgICAgICAgICAgICA8IVtlbmRpZl0tLT4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPGRpdiBzdHlsZT0ibWFyZ2luOjBweCBhdXRvO21heC13aWR0aDo4MDBweDsiPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRhYmxlIGFsaWduPSJjZW50ZXIiIGJvcmRlcj0iMCIgY2VsbHBhZGRpbmc9IjAiIGNlbGxzcGFjaW5nPSIwIiByb2xlPSJwcmVzZW50YXRpb24iIHN0eWxlPSJ3aWR0aDoxMDAlOyI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRib2R5PgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRkIHN0eWxlPSJkaXJlY3Rpb246bHRyO2ZvbnQtc2l6ZTowcHg7cGFkZGluZzowO3RleHQtYWxpZ246Y2VudGVyOyI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8IS0tW2lmIG1zbyB8IElFXT4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0YWJsZSByb2xlPSJwcmVzZW50YXRpb24iIGJvcmRlcj0iMCIgY2VsbHBhZGRpbmc9IjAiIGNlbGxzcGFjaW5nPSIwIj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRyPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRkCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBjbGFzcz0iIiBzdHlsZT0id2lkdGg6ODAwcHg7IgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwhW2VuZGlmXS0tPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPGRpdiBjbGFzcz0ibWotY29sdW1uLXBlci0xMDAgbWotb3V0bG9vay1ncm91cC1maXgiIHN0eWxlPSJmb250LXNpemU6MDtsaW5lLWhlaWdodDowO3RleHQtYWxpZ246bGVmdDtkaXNwbGF5OmlubGluZS1ibG9jazt3aWR0aDoxMDAlO2RpcmVjdGlvbjpsdHI7Ij4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8IS0tW2lmIG1zbyB8IElFXT4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGFibGUKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBib3JkZXI9IjAiIGNlbGxwYWRkaW5nPSIwIiBjZWxsc3BhY2luZz0iMCIgcm9sZT0icHJlc2VudGF0aW9uIgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgID4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGQKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBzdHlsZT0idmVydGljYWwtYWxpZ246dG9wO3dpZHRoOjgwMHB4OyIKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPCFbZW5kaWZdLS0+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPGRpdiBjbGFzcz0ibWotY29sdW1uLXBlci0xMDAgbWotb3V0bG9vay1ncm91cC1maXgiIHN0eWxlPSJmb250LXNpemU6MHB4O3RleHQtYWxpZ246bGVmdDtkaXJlY3Rpb246bHRyO2Rpc3BsYXk6aW5saW5lLWJsb2NrO3ZlcnRpY2FsLWFsaWduOnRvcDt3aWR0aDoxMDAlOyI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0YWJsZSBib3JkZXI9IjAiIGNlbGxwYWRkaW5nPSIwIiBjZWxsc3BhY2luZz0iMCIgcm9sZT0icHJlc2VudGF0aW9uIiB3aWR0aD0iMTAwJSI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGJvZHk+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dHI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRkIHN0eWxlPSJ2ZXJ0aWNhbC1hbGlnbjp0b3A7cGFkZGluZzoyMHB4OyI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0YWJsZSBib3JkZXI9IjAiIGNlbGxwYWRkaW5nPSIwIiBjZWxsc3BhY2luZz0iMCIgcm9sZT0icHJlc2VudGF0aW9uIiBzdHlsZT0iIiB3aWR0aD0iMTAwJSI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dHI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRkIGFsaWduPSJyaWdodCIgc3R5bGU9ImZvbnQtc2l6ZTowcHg7cGFkZGluZzowO3dvcmQtYnJlYWs6YnJlYWstd29yZDsiPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGFibGUgYm9yZGVyPSIwIiBjZWxscGFkZGluZz0iMCIgY2VsbHNwYWNpbmc9IjAiIHJvbGU9InByZXNlbnRhdGlvbiIgc3R5bGU9ImJvcmRlci1jb2xsYXBzZTpjb2xsYXBzZTtib3JkZXItc3BhY2luZzowcHg7Ij4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0Ym9keT4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGQgc3R5bGU9IndpZHRoOjY1cHg7Ij4gPGltZyBoZWlnaHQ9ImF1dG8iIHNyYz0iaHR0cHM6Ly9zdGF0aWMueml0YWRlbC5jaC9sb2dvX3doaXRlZm9udF90cmFuc3BhcmVudGJnLnBuZyIgc3R5bGU9ImJvcmRlcjowO2Rpc3BsYXk6YmxvY2s7b3V0bGluZTpub25lO3RleHQtZGVjb3JhdGlvbjpub25lO2hlaWdodDphdXRvO3dpZHRoOjEwMCU7Zm9udC1zaXplOjEzcHg7IgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHdpZHRoPSI2NSIgLz4gPC90ZD4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvdHI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L3Rib2R5PgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L3RhYmxlPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvdGQ+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L3RyPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L3RhYmxlPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvdGQ+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L3RyPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC90Ym9keT4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC90YWJsZT4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L2Rpdj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8IS0tW2lmIG1zbyB8IElFXT4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L3RkPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC90cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvdGFibGU+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPCFbZW5kaWZdLS0+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L2Rpdj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwhLS1baWYgbXNvIHwgSUVdPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC90ZD4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC90cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvdGFibGU+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPCFbZW5kaWZdLS0+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvdGQ+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC90cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L3Rib2R5PgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC90YWJsZT4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC9kaXY+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwhLS1baWYgbXNvIHwgSUVdPgogICAgICAgICAgICAgICAgICAgICAgICAgIDwvdGQ+CiAgICAgICAgICAgICAgICAgICAgICAgIDwvdHI+CiAgICAgICAgICAgICAgICAgICAgICA8L3RhYmxlPgogICAgICAgICAgICAgICAgICAgICAgPCFbZW5kaWZdLS0+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC90ZD4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvdHI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L3Rib2R5PgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L3RhYmxlPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8IS0tW2lmIG1zbyB8IElFXT4KICAgICAgICAgICAgICAgICAgICAgICAgICA8L3RkPgogICAgICAgICAgICAgICAgICAgICAgICA8L3RyPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L3RhYmxlPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgPCFbZW5kaWZdLS0+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC90ZD4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvdHI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L3Rib2R5PgogICAgICAgICAgICAgICAgICAgICAgICA8L3RhYmxlPgogICAgICAgICAgICAgICAgICAgIDwvZGl2PgogICAgICAgICAgICAgICAgPC9kaXY+CiAgICAgICAgICAgICAgICA8IS0tW2lmIG1zbyB8IElFXT4KICAgICAgICAgICAgICA8L3RkPgogICAgICAgICAgICA8L3RyPgogICAgICAgICAgPC90YWJsZT4KICAgICAgICAgICAgPC92OnRleHRib3g+CiAgICAgICAgICA8L3Y6cmVjdD4KICAgICAgICA8IVtlbmRpZl0tLT4KICAgICAgICAgICAgPC90ZD4KICAgICAgICA8L3RyPgogICAgICAgIDwvdGJvZHk+CiAgICA8L3RhYmxlPgo8L2Rpdj4KPC9ib2R5PgoKPC9odG1sPg== - DefaultMailTexts: - - MailTextType: InitCode - Language: DE - Title: Zitadel - User initialisieren - PreHeader: User initialisieren - Subject: User initialisieren - Greeting: Hallo {{.FirstName}} {{.LastName}}, - Text: Dieser Benutzer wurde soeben im Zitadel erstellt. Mit dem Benutzernamen <br><strong>{{.PreferredLoginName}}</strong><br> kannst du dich anmelden. Nutze den untenstehenden Button, um die Initialisierung abzuschliessen <br>(Code <strong>{{.Code}}</strong>).<br> Falls du dieses Mail nicht angefordert hast, kannst du es einfach ignorieren. - ButtonText: Initialisierung abschliessen - - MailTextType: PasswordReset - Language: DE - Title: Zitadel - Passwort zurücksetzen - PreHeader: Passwort zurücksetzen - Subject: Passwort zurücksetzen - Greeting: Hallo {{.FirstName}} {{.LastName}}, - Text: Wir haben eine Anfrage für das Zurücksetzen deines Passwortes bekommen. Du kannst den untenstehenden Button verwenden, um dein Passwort zurückzusetzen <br>(Code <strong>{{.Code}}</strong>).<br> Falls du dieses Mail nicht angefordert hast, kannst du es ignorieren. - ButtonText: Passwort zurücksetzen - - MailTextType: VerifyEmail - Language: DE - Title: Zitadel - Email verifizieren - PreHeader: Email verifizieren - Subject: Email verifizieren - Greeting: Hallo {{.FirstName}} {{.LastName}}, - Text: Eine neue E-Mail Adresse wurde hinzugefügt. Bitte verwende den untenstehenden Button um diese zu verifizieren <br>(Code <strong>{{.Code}}</strong>).<br> Falls du deine E-Mail Adresse nicht selber hinzugefügt hast, kannst du dieses E-Mail ignorieren. - ButtonText: Email verifizieren - - MailTextType: VerifyPhone - Language: DE - Title: Zitadel - Telefonnummer verifizieren - PreHeader: Telefonnummer verifizieren - Subject: Telefonnummer verifizieren - Greeting: Hallo {{.FirstName}} {{.LastName}}, - Text: Eine Telefonnummer wurde hinzugefügt. Bitte verifiziere diese in dem du folgenden Code eingibst<br>(Code <strong>{{.Code}}</strong>).<br> - ButtonText: Telefon verifizieren - - MailTextType: DomainClaimed - Language: DE - Title: Zitadel - Domain wurde beansprucht - PreHeader: Email / Username ändern - Subject: Domain wurde beansprucht - Greeting: Hallo {{.FirstName}} {{.LastName}}, - Text: Die Domain {{.Domain}} wurde von einer Organisation beansprucht. Dein derzeitiger User {{.Username}} ist nicht Teil dieser Organisation. Daher musst du beim nächsten Login eine neue Email hinterlegen. Für diesen Login haben wir dir einen temporären Usernamen ({{.TempUsername}}) erstellt. - ButtonText: Login - - MailTextType: InitCode - Language: EN - Title: Zitadel - Initialize User - PreHeader: Initialize User - Subject: Initialize User - Greeting: Hello {{.FirstName}} {{.LastName}}, - Text: This user was created in Zitadel. Use the username {{.PreferredLoginName}} to login. Please click the button below to finish the initialization process. (Code {{.Code}}) If you didn't ask for this mail, please ignore it. - ButtonText: Finish initialization - - MailTextType: PasswordReset - Language: EN - Title: Zitadel - Reset password - PreHeader: Reset password - Subject: Reset password - Greeting: Hello {{.FirstName}} {{.LastName}}, - Text: We received a password reset request. Please use the button below to reset your password. (Code {{.Code}}) If you didn't ask for this mail, please ignore it. - ButtonText: Reset password - - MailTextType: VerifyEmail - Language: EN - Title: Zitadel - Verify email - PreHeader: Verify email - Subject: Verify email - Greeting: Hello {{.FirstName}} {{.LastName}}, - Text: A new email has been added. Please use the button below to verify your mail. (Code {{.Code}}) If you din't add a new email, please ignore this email. - ButtonText: Verify email - - MailTextType: VerifyPhone - Language: EN - Title: Zitadel - Verify phone - PreHeader: Verify phone - Subject: Verify phone - Greeting: Hello {{.FirstName}} {{.LastName}}, - Text: A new phonenumber has been added. Please use the following code to verify it {{.Code}}. - ButtonText: Verify phone - - MailTextType: DomainClaimed - Language: EN - Title: Zitadel - Domain has been claimed - PreHeader: Change email / username - Subject: Domain has been claimed - Greeting: Hello {{.FirstName}} {{.LastName}}, - Text: The domain {{.Domain}} has been claimed by an organisation. Your current user {{.Username}} is not part of this organisation. Therefore you'll have to change your email when you login. We have created a temporary username ({{.TempUsername}}) for this login. - ButtonText: Login Step11: MigrateV1EventstoreToV2: $ZITADEL_MIGRATE_ES_V1 Step12: @@ -188,3 +107,85 @@ SetUp: Step15: DefaultMailTemplate: Template: CjwhZG9jdHlwZSBodG1sPgo8aHRtbCB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94aHRtbCIgeG1sbnM6dj0idXJuOnNjaGVtYXMtbWljcm9zb2Z0LWNvbTp2bWwiIHhtbG5zOm89InVybjpzY2hlbWFzLW1pY3Jvc29mdC1jb206b2ZmaWNlOm9mZmljZSI+CjxoZWFkPgogIDx0aXRsZT4KCiAgPC90aXRsZT4KICA8IS0tW2lmICFtc29dPjwhLS0+CiAgPG1ldGEgaHR0cC1lcXVpdj0iWC1VQS1Db21wYXRpYmxlIiBjb250ZW50PSJJRT1lZGdlIj4KICA8IS0tPCFbZW5kaWZdLS0+CiAgPG1ldGEgaHR0cC1lcXVpdj0iQ29udGVudC1UeXBlIiBjb250ZW50PSJ0ZXh0L2h0bWw7IGNoYXJzZXQ9VVRGLTgiPgogIDxtZXRhIG5hbWU9InZpZXdwb3J0IiBjb250ZW50PSJ3aWR0aD1kZXZpY2Utd2lkdGgsIGluaXRpYWwtc2NhbGU9MSI+CiAgPHN0eWxlIHR5cGU9InRleHQvY3NzIj4KICAgICNvdXRsb29rIGEgeyBwYWRkaW5nOjA7IH0KICAgIGJvZHkgeyBtYXJnaW46MDtwYWRkaW5nOjA7LXdlYmtpdC10ZXh0LXNpemUtYWRqdXN0OjEwMCU7LW1zLXRleHQtc2l6ZS1hZGp1c3Q6MTAwJTsgfQogICAgdGFibGUsIHRkIHsgYm9yZGVyLWNvbGxhcHNlOmNvbGxhcHNlO21zby10YWJsZS1sc3BhY2U6MHB0O21zby10YWJsZS1yc3BhY2U6MHB0OyB9CiAgICBpbWcgeyBib3JkZXI6MDtoZWlnaHQ6YXV0bztsaW5lLWhlaWdodDoxMDAlOyBvdXRsaW5lOm5vbmU7dGV4dC1kZWNvcmF0aW9uOm5vbmU7LW1zLWludGVycG9sYXRpb24tbW9kZTpiaWN1YmljOyB9CiAgICBwIHsgZGlzcGxheTpibG9jazttYXJnaW46MTNweCAwOyB9CiAgPC9zdHlsZT4KICA8IS0tW2lmIG1zb10+CiAgPHhtbD4KICAgIDxvOk9mZmljZURvY3VtZW50U2V0dGluZ3M+CiAgICAgIDxvOkFsbG93UE5HLz4KICAgICAgPG86UGl4ZWxzUGVySW5jaD45NjwvbzpQaXhlbHNQZXJJbmNoPgogICAgPC9vOk9mZmljZURvY3VtZW50U2V0dGluZ3M+CiAgPC94bWw+CiAgPCFbZW5kaWZdLS0+CiAgPCEtLVtpZiBsdGUgbXNvIDExXT4KICA8c3R5bGUgdHlwZT0idGV4dC9jc3MiPgogICAgLm1qLW91dGxvb2stZ3JvdXAtZml4IHsgd2lkdGg6MTAwJSAhaW1wb3J0YW50OyB9CiAgPC9zdHlsZT4KICA8IVtlbmRpZl0tLT4KCiAgPCEtLVtpZiAhbXNvXT48IS0tPgogIDxsaW5rIGhyZWY9Imh0dHBzOi8vZm9udHMuZ29vZ2xlYXBpcy5jb20vY3NzP2ZhbWlseT1VYnVudHU6MzAwLDQwMCw1MDAsNzAwIiByZWw9InN0eWxlc2hlZXQiIHR5cGU9InRleHQvY3NzIj4KICA8c3R5bGUgdHlwZT0idGV4dC9jc3MiPgogICAgQGltcG9ydCB1cmwoaHR0cHM6Ly9mb250cy5nb29nbGVhcGlzLmNvbS9jc3M/ZmFtaWx5PVVidW50dTozMDAsNDAwLDUwMCw3MDApOwogIDwvc3R5bGU+CiAgPCEtLTwhW2VuZGlmXS0tPgoKCgogIDxzdHlsZSB0eXBlPSJ0ZXh0L2NzcyI+CiAgICBAbWVkaWEgb25seSBzY3JlZW4gYW5kIChtaW4td2lkdGg6NDgwcHgpIHsKICAgICAgLm1qLWNvbHVtbi1wZXItMTAwIHsgd2lkdGg6MTAwJSAhaW1wb3J0YW50OyBtYXgtd2lkdGg6IDEwMCU7IH0KICAgICAgLm1qLWNvbHVtbi1wZXItNjAgeyB3aWR0aDo2MCUgIWltcG9ydGFudDsgbWF4LXdpZHRoOiA2MCU7IH0KICAgIH0KICA8L3N0eWxlPgoKCiAgPHN0eWxlIHR5cGU9InRleHQvY3NzIj4KCgoKICAgIEBtZWRpYSBvbmx5IHNjcmVlbiBhbmQgKG1heC13aWR0aDo0ODBweCkgewogICAgICB0YWJsZS5tai1mdWxsLXdpZHRoLW1vYmlsZSB7IHdpZHRoOiAxMDAlICFpbXBvcnRhbnQ7IH0KICAgICAgdGQubWotZnVsbC13aWR0aC1tb2JpbGUgeyB3aWR0aDogYXV0byAhaW1wb3J0YW50OyB9CiAgICB9CgogIDwvc3R5bGU+CiAgPHN0eWxlIHR5cGU9InRleHQvY3NzIj4uc2hhZG93IGEgewogICAgYm94LXNoYWRvdzogMHB4IDNweCAxcHggLTJweCByZ2JhKDAsIDAsIDAsIDAuMiksIDBweCAycHggMnB4IDBweCByZ2JhKDAsIDAsIDAsIDAuMTQpLCAwcHggMXB4IDVweCAwcHggcmdiYSgwLCAwLCAwLCAwLjEyKTsKICB9PC9zdHlsZT4KCiAge3tpZiAuRm9udFVSTH19CiAgPHN0eWxlPgogICAgQGZvbnQtZmFjZSB7CiAgICAgIGZvbnQtZmFtaWx5OiAne3suRm9udEZhbWlseX19JzsKICAgICAgZm9udC1zdHlsZTogbm9ybWFsOwogICAgICBmb250LWRpc3BsYXk6IHN3YXA7CiAgICAgIHNyYzogdXJsKHt7LkZvbnRVUkx9fSk7CiAgICB9CiAgPC9zdHlsZT4KICB7e2VuZH19Cgo8L2hlYWQ+Cjxib2R5IHN0eWxlPSJ3b3JkLXNwYWNpbmc6bm9ybWFsOyI+CgoKPGRpdgogICAgICAgIHN0eWxlPSIiCj4KCiAgPHRhYmxlCiAgICAgICAgICBhbGlnbj0iY2VudGVyIiBib3JkZXI9IjAiIGNlbGxwYWRkaW5nPSIwIiBjZWxsc3BhY2luZz0iMCIgcm9sZT0icHJlc2VudGF0aW9uIiBzdHlsZT0iYmFja2dyb3VuZDp7ey5CYWNrZ3JvdW5kQ29sb3J9fTtiYWNrZ3JvdW5kLWNvbG9yOnt7LkJhY2tncm91bmRDb2xvcn19O3dpZHRoOjEwMCU7Ym9yZGVyLXJhZGl1czoxNnB4OyIKICA+CiAgICA8dGJvZHk+CiAgICA8dHI+CiAgICAgIDx0ZD4KCgogICAgICAgIDwhLS1baWYgbXNvIHwgSUVdPjx0YWJsZSBhbGlnbj0iY2VudGVyIiBib3JkZXI9IjAiIGNlbGxwYWRkaW5nPSIwIiBjZWxsc3BhY2luZz0iMCIgY2xhc3M9IiIgc3R5bGU9IndpZHRoOjgwMHB4OyIgd2lkdGg9IjgwMCIgPjx0cj48dGQgc3R5bGU9ImxpbmUtaGVpZ2h0OjBweDtmb250LXNpemU6MHB4O21zby1saW5lLWhlaWdodC1ydWxlOmV4YWN0bHk7Ij48IVtlbmRpZl0tLT4KCgogICAgICAgIDxkaXYgIHN0eWxlPSJtYXJnaW46MHB4IGF1dG87Ym9yZGVyLXJhZGl1czoxNnB4O21heC13aWR0aDo4MDBweDsiPgoKICAgICAgICAgIDx0YWJsZQogICAgICAgICAgICAgICAgICBhbGlnbj0iY2VudGVyIiBib3JkZXI9IjAiIGNlbGxwYWRkaW5nPSIwIiBjZWxsc3BhY2luZz0iMCIgcm9sZT0icHJlc2VudGF0aW9uIiBzdHlsZT0id2lkdGg6MTAwJTtib3JkZXItcmFkaXVzOjE2cHg7IgogICAgICAgICAgPgogICAgICAgICAgICA8dGJvZHk+CiAgICAgICAgICAgIDx0cj4KICAgICAgICAgICAgICA8dGQKICAgICAgICAgICAgICAgICAgICAgIHN0eWxlPSJkaXJlY3Rpb246bHRyO2ZvbnQtc2l6ZTowcHg7cGFkZGluZzoyMHB4IDA7cGFkZGluZy1sZWZ0OjA7dGV4dC1hbGlnbjpjZW50ZXI7IgogICAgICAgICAgICAgID4KICAgICAgICAgICAgICAgIDwhLS1baWYgbXNvIHwgSUVdPjx0YWJsZSByb2xlPSJwcmVzZW50YXRpb24iIGJvcmRlcj0iMCIgY2VsbHBhZGRpbmc9IjAiIGNlbGxzcGFjaW5nPSIwIj48dHI+PHRkIGNsYXNzPSIiIHdpZHRoPSI4MDBweCIgPjwhW2VuZGlmXS0tPgoKICAgICAgICAgICAgICAgIDx0YWJsZQogICAgICAgICAgICAgICAgICAgICAgICBhbGlnbj0iY2VudGVyIiBib3JkZXI9IjAiIGNlbGxwYWRkaW5nPSIwIiBjZWxsc3BhY2luZz0iMCIgcm9sZT0icHJlc2VudGF0aW9uIiBzdHlsZT0id2lkdGg6MTAwJTsiCiAgICAgICAgICAgICAgICA+CiAgICAgICAgICAgICAgICAgIDx0Ym9keT4KICAgICAgICAgICAgICAgICAgPHRyPgogICAgICAgICAgICAgICAgICAgIDx0ZD4KCgogICAgICAgICAgICAgICAgICAgICAgPCEtLVtpZiBtc28gfCBJRV0+PHRhYmxlIGFsaWduPSJjZW50ZXIiIGJvcmRlcj0iMCIgY2VsbHBhZGRpbmc9IjAiIGNlbGxzcGFjaW5nPSIwIiBjbGFzcz0iIiBzdHlsZT0id2lkdGg6ODAwcHg7IiB3aWR0aD0iODAwIiA+PHRyPjx0ZCBzdHlsZT0ibGluZS1oZWlnaHQ6MHB4O2ZvbnQtc2l6ZTowcHg7bXNvLWxpbmUtaGVpZ2h0LXJ1bGU6ZXhhY3RseTsiPjwhW2VuZGlmXS0tPgoKCiAgICAgICAgICAgICAgICAgICAgICA8ZGl2ICBzdHlsZT0ibWFyZ2luOjBweCBhdXRvO21heC13aWR0aDo4MDBweDsiPgoKICAgICAgICAgICAgICAgICAgICAgICAgPHRhYmxlCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgYWxpZ249ImNlbnRlciIgYm9yZGVyPSIwIiBjZWxscGFkZGluZz0iMCIgY2VsbHNwYWNpbmc9IjAiIHJvbGU9InByZXNlbnRhdGlvbiIgc3R5bGU9IndpZHRoOjEwMCU7IgogICAgICAgICAgICAgICAgICAgICAgICA+CiAgICAgICAgICAgICAgICAgICAgICAgICAgPHRib2R5PgogICAgICAgICAgICAgICAgICAgICAgICAgIDx0cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0ZAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBzdHlsZT0iZGlyZWN0aW9uOmx0cjtmb250LXNpemU6MHB4O3BhZGRpbmc6MDt0ZXh0LWFsaWduOmNlbnRlcjsiCiAgICAgICAgICAgICAgICAgICAgICAgICAgICA+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwhLS1baWYgbXNvIHwgSUVdPjx0YWJsZSByb2xlPSJwcmVzZW50YXRpb24iIGJvcmRlcj0iMCIgY2VsbHBhZGRpbmc9IjAiIGNlbGxzcGFjaW5nPSIwIj48dHI+PHRkIGNsYXNzPSIiIHN0eWxlPSJ3aWR0aDo4MDBweDsiID48IVtlbmRpZl0tLT4KCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDxkaXYKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBjbGFzcz0ibWotY29sdW1uLXBlci0xMDAgbWotb3V0bG9vay1ncm91cC1maXgiIHN0eWxlPSJmb250LXNpemU6MDtsaW5lLWhlaWdodDowO3RleHQtYWxpZ246bGVmdDtkaXNwbGF5OmlubGluZS1ibG9jazt3aWR0aDoxMDAlO2RpcmVjdGlvbjpsdHI7IgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICA+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPCEtLVtpZiBtc28gfCBJRV0+PHRhYmxlIGJvcmRlcj0iMCIgY2VsbHBhZGRpbmc9IjAiIGNlbGxzcGFjaW5nPSIwIiByb2xlPSJwcmVzZW50YXRpb24iID48dHI+PHRkIHN0eWxlPSJ2ZXJ0aWNhbC1hbGlnbjp0b3A7d2lkdGg6ODAwcHg7IiA+PCFbZW5kaWZdLS0+CgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDxkaXYKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGNsYXNzPSJtai1jb2x1bW4tcGVyLTEwMCBtai1vdXRsb29rLWdyb3VwLWZpeCIgc3R5bGU9ImZvbnQtc2l6ZTowcHg7dGV4dC1hbGlnbjpsZWZ0O2RpcmVjdGlvbjpsdHI7ZGlzcGxheTppbmxpbmUtYmxvY2s7dmVydGljYWwtYWxpZ246dG9wO3dpZHRoOjEwMCU7IgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgID4KCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGFibGUKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgYm9yZGVyPSIwIiBjZWxscGFkZGluZz0iMCIgY2VsbHNwYWNpbmc9IjAiIHJvbGU9InByZXNlbnRhdGlvbiIgd2lkdGg9IjEwMCUiCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0Ym9keT4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRyPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0ZCAgc3R5bGU9InZlcnRpY2FsLWFsaWduOnRvcDtwYWRkaW5nOjA7Ij4KCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGFibGUKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgYm9yZGVyPSIwIiBjZWxscGFkZGluZz0iMCIgY2VsbHNwYWNpbmc9IjAiIHJvbGU9InByZXNlbnRhdGlvbiIgc3R5bGU9IiIgd2lkdGg9IjEwMCUiCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0Ym9keT4KCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGQKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGFsaWduPSJjZW50ZXIiIHN0eWxlPSJmb250LXNpemU6MHB4O3BhZGRpbmc6NTBweCAwIDMwcHggMDt3b3JkLWJyZWFrOmJyZWFrLXdvcmQ7IgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgID4KCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGFibGUKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgYm9yZGVyPSIwIiBjZWxscGFkZGluZz0iMCIgY2VsbHNwYWNpbmc9IjAiIHJvbGU9InByZXNlbnRhdGlvbiIgc3R5bGU9ImJvcmRlci1jb2xsYXBzZTpjb2xsYXBzZTtib3JkZXItc3BhY2luZzowcHg7IgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGJvZHk+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGQgIHN0eWxlPSJ3aWR0aDoxODBweDsiPgoKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDxpbWcKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgaGVpZ2h0PSJhdXRvIiBzcmM9Int7LkxvZ29VUkx9fSIgc3R5bGU9ImJvcmRlcjowO2JvcmRlci1yYWRpdXM6OHB4O2Rpc3BsYXk6YmxvY2s7b3V0bGluZTpub25lO3RleHQtZGVjb3JhdGlvbjpub25lO2hlaWdodDphdXRvO3dpZHRoOjEwMCU7Zm9udC1zaXplOjEzcHg7IiB3aWR0aD0iMTgwIgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgLz4KCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC90ZD4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC90cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC90Ym9keT4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvdGFibGU+CgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvdGQ+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvdHI+CgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L3Rib2R5PgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC90YWJsZT4KCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC90ZD4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC90cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC90Ym9keT4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvdGFibGU+CgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvZGl2PgoKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8IS0tW2lmIG1zbyB8IElFXT48L3RkPjwvdHI+PC90YWJsZT48IVtlbmRpZl0tLT4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC9kaXY+CgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8IS0tW2lmIG1zbyB8IElFXT48L3RkPjwvdHI+PC90YWJsZT48IVtlbmRpZl0tLT4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvdGQ+CiAgICAgICAgICAgICAgICAgICAgICAgICAgPC90cj4KICAgICAgICAgICAgICAgICAgICAgICAgICA8L3Rib2R5PgogICAgICAgICAgICAgICAgICAgICAgICA8L3RhYmxlPgoKICAgICAgICAgICAgICAgICAgICAgIDwvZGl2PgoKCiAgICAgICAgICAgICAgICAgICAgICA8IS0tW2lmIG1zbyB8IElFXT48L3RkPjwvdHI+PC90YWJsZT48IVtlbmRpZl0tLT4KCgogICAgICAgICAgICAgICAgICAgIDwvdGQ+CiAgICAgICAgICAgICAgICAgIDwvdHI+CiAgICAgICAgICAgICAgICAgIDwvdGJvZHk+CiAgICAgICAgICAgICAgICA8L3RhYmxlPgoKICAgICAgICAgICAgICAgIDwhLS1baWYgbXNvIHwgSUVdPjwvdGQ+PC90cj48dHI+PHRkIGNsYXNzPSIiIHdpZHRoPSI4MDBweCIgPjwhW2VuZGlmXS0tPgoKICAgICAgICAgICAgICAgIDx0YWJsZQogICAgICAgICAgICAgICAgICAgICAgICBhbGlnbj0iY2VudGVyIiBib3JkZXI9IjAiIGNlbGxwYWRkaW5nPSIwIiBjZWxsc3BhY2luZz0iMCIgcm9sZT0icHJlc2VudGF0aW9uIiBzdHlsZT0id2lkdGg6MTAwJTsiCiAgICAgICAgICAgICAgICA+CiAgICAgICAgICAgICAgICAgIDx0Ym9keT4KICAgICAgICAgICAgICAgICAgPHRyPgogICAgICAgICAgICAgICAgICAgIDx0ZD4KCgogICAgICAgICAgICAgICAgICAgICAgPCEtLVtpZiBtc28gfCBJRV0+PHRhYmxlIGFsaWduPSJjZW50ZXIiIGJvcmRlcj0iMCIgY2VsbHBhZGRpbmc9IjAiIGNlbGxzcGFjaW5nPSIwIiBjbGFzcz0iIiBzdHlsZT0id2lkdGg6ODAwcHg7IiB3aWR0aD0iODAwIiA+PHRyPjx0ZCBzdHlsZT0ibGluZS1oZWlnaHQ6MHB4O2ZvbnQtc2l6ZTowcHg7bXNvLWxpbmUtaGVpZ2h0LXJ1bGU6ZXhhY3RseTsiPjwhW2VuZGlmXS0tPgoKCiAgICAgICAgICAgICAgICAgICAgICA8ZGl2ICBzdHlsZT0ibWFyZ2luOjBweCBhdXRvO21heC13aWR0aDo4MDBweDsiPgoKICAgICAgICAgICAgICAgICAgICAgICAgPHRhYmxlCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgYWxpZ249ImNlbnRlciIgYm9yZGVyPSIwIiBjZWxscGFkZGluZz0iMCIgY2VsbHNwYWNpbmc9IjAiIHJvbGU9InByZXNlbnRhdGlvbiIgc3R5bGU9IndpZHRoOjEwMCU7IgogICAgICAgICAgICAgICAgICAgICAgICA+CiAgICAgICAgICAgICAgICAgICAgICAgICAgPHRib2R5PgogICAgICAgICAgICAgICAgICAgICAgICAgIDx0cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0ZAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBzdHlsZT0iZGlyZWN0aW9uOmx0cjtmb250LXNpemU6MHB4O3BhZGRpbmc6MDt0ZXh0LWFsaWduOmNlbnRlcjsiCiAgICAgICAgICAgICAgICAgICAgICAgICAgICA+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwhLS1baWYgbXNvIHwgSUVdPjx0YWJsZSByb2xlPSJwcmVzZW50YXRpb24iIGJvcmRlcj0iMCIgY2VsbHBhZGRpbmc9IjAiIGNlbGxzcGFjaW5nPSIwIj48dHI+PHRkIGNsYXNzPSIiIHN0eWxlPSJ2ZXJ0aWNhbC1hbGlnbjp0b3A7d2lkdGg6NDgwcHg7IiA+PCFbZW5kaWZdLS0+CgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8ZGl2CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgY2xhc3M9Im1qLWNvbHVtbi1wZXItNjAgbWotb3V0bG9vay1ncm91cC1maXgiIHN0eWxlPSJmb250LXNpemU6MHB4O3RleHQtYWxpZ246bGVmdDtkaXJlY3Rpb246bHRyO2Rpc3BsYXk6aW5saW5lLWJsb2NrO3ZlcnRpY2FsLWFsaWduOnRvcDt3aWR0aDoxMDAlOyIKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPgoKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGFibGUKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGJvcmRlcj0iMCIgY2VsbHBhZGRpbmc9IjAiIGNlbGxzcGFjaW5nPSIwIiByb2xlPSJwcmVzZW50YXRpb24iIHdpZHRoPSIxMDAlIgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgID4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0Ym9keT4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRkICBzdHlsZT0idmVydGljYWwtYWxpZ246dG9wO3BhZGRpbmc6MDsiPgoKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGFibGUKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGJvcmRlcj0iMCIgY2VsbHBhZGRpbmc9IjAiIGNlbGxzcGFjaW5nPSIwIiByb2xlPSJwcmVzZW50YXRpb24iIHN0eWxlPSIiIHdpZHRoPSIxMDAlIgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgID4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0Ym9keT4KCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dHI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0ZAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGFsaWduPSJjZW50ZXIiIHN0eWxlPSJmb250LXNpemU6MHB4O3BhZGRpbmc6MTBweCAyNXB4O3dvcmQtYnJlYWs6YnJlYWstd29yZDsiCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgID4KCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPGRpdgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgc3R5bGU9ImZvbnQtZmFtaWx5Ont7LkZvbnRGYW1pbHl9fTtmb250LXNpemU6MjRweDtmb250LXdlaWdodDo1MDA7bGluZS1oZWlnaHQ6MTt0ZXh0LWFsaWduOmNlbnRlcjtjb2xvcjp7ey5Gb250Q29sb3J9fTsiCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPnt7LkdyZWV0aW5nfX08L2Rpdj4KCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvdGQ+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L3RyPgoKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRkCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgYWxpZ249ImNlbnRlciIgc3R5bGU9ImZvbnQtc2l6ZTowcHg7cGFkZGluZzoxMHB4IDI1cHg7d29yZC1icmVhazpicmVhay13b3JkOyIKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPgoKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8ZGl2CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBzdHlsZT0iZm9udC1mYW1pbHk6e3suRm9udEZhbWlseX19O2ZvbnQtc2l6ZToxNnB4O2ZvbnQtd2VpZ2h0OmxpZ2h0O2xpbmUtaGVpZ2h0OjEuNTt0ZXh0LWFsaWduOmNlbnRlcjtjb2xvcjp7ey5Gb250Q29sb3J9fTsiCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPnt7LlRleHR9fTwvZGl2PgoKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC90ZD4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvdHI+CgoKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRkCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgYWxpZ249ImNlbnRlciIgdmVydGljYWwtYWxpZ249Im1pZGRsZSIgY2xhc3M9InNoYWRvdyIgc3R5bGU9ImZvbnQtc2l6ZTowcHg7cGFkZGluZzoxMHB4IDI1cHg7d29yZC1icmVhazpicmVhay13b3JkOyIKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPgoKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGFibGUKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGJvcmRlcj0iMCIgY2VsbHBhZGRpbmc9IjAiIGNlbGxzcGFjaW5nPSIwIiByb2xlPSJwcmVzZW50YXRpb24iIHN0eWxlPSJib3JkZXItY29sbGFwc2U6c2VwYXJhdGU7bGluZS1oZWlnaHQ6MTAwJTsiCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRyPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGQKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBhbGlnbj0iY2VudGVyIiBiZ2NvbG9yPSJ7ey5QcmltYXJ5Q29sb3J9fSIgcm9sZT0icHJlc2VudGF0aW9uIiBzdHlsZT0iYm9yZGVyOm5vbmU7Ym9yZGVyLXJhZGl1czo2cHg7Y3Vyc29yOmF1dG87bXNvLXBhZGRpbmctYWx0OjEwcHggMjVweDtiYWNrZ3JvdW5kOnt7LlByaW1hcnlDb2xvcn19OyIgdmFsaWduPSJtaWRkbGUiCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgID4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8YQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgaHJlZj0ie3suVVJMfX0iIHJlbD0ibm9vcGVuZXIgbm9yZWZlcnJlciIgc3R5bGU9ImRpc3BsYXk6aW5saW5lLWJsb2NrO2JhY2tncm91bmQ6e3suUHJpbWFyeUNvbG9yfX07Y29sb3I6I2ZmZmZmZjtmb250LWZhbWlseTpVYnVudHUsIEhlbHZldGljYSwgQXJpYWwsIHNhbnMtc2VyaWY7Zm9udC1zaXplOjE0cHg7Zm9udC13ZWlnaHQ6NTAwO2xpbmUtaGVpZ2h0OjEyMCU7bWFyZ2luOjA7dGV4dC1kZWNvcmF0aW9uOm5vbmU7dGV4dC10cmFuc2Zvcm06bm9uZTtwYWRkaW5nOjEwcHggMjVweDttc28tcGFkZGluZy1hbHQ6MHB4O2JvcmRlci1yYWRpdXM6NnB4OyIgdGFyZ2V0PSJfYmxhbmsiCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAge3suQnV0dG9uVGV4dH19CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC9hPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L3RkPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC90cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L3RhYmxlPgoKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC90ZD4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvdHI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB7e2lmIC5JbmNsdWRlRm9vdGVyfX0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRkCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgYWxpZ249ImNlbnRlciIgc3R5bGU9ImZvbnQtc2l6ZTowcHg7cGFkZGluZzoxMHB4IDI1cHg7cGFkZGluZy10b3A6MjBweDtwYWRkaW5nLXJpZ2h0OjIwcHg7cGFkZGluZy1ib3R0b206MjBweDtwYWRkaW5nLWxlZnQ6MjBweDt3b3JkLWJyZWFrOmJyZWFrLXdvcmQ7IgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA+CgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDxwCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBzdHlsZT0iYm9yZGVyLXRvcDpzb2xpZCAycHggI2RiZGJkYjtmb250LXNpemU6MXB4O21hcmdpbjowcHggYXV0bzt3aWR0aDoxMDAlOyIKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC9wPgoKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8IS0tW2lmIG1zbyB8IElFXT48dGFibGUgYWxpZ249ImNlbnRlciIgYm9yZGVyPSIwIiBjZWxscGFkZGluZz0iMCIgY2VsbHNwYWNpbmc9IjAiIHN0eWxlPSJib3JkZXItdG9wOnNvbGlkIDJweCAjZGJkYmRiO2ZvbnQtc2l6ZToxcHg7bWFyZ2luOjBweCBhdXRvO3dpZHRoOjQ0MHB4OyIgcm9sZT0icHJlc2VudGF0aW9uIiB3aWR0aD0iNDQwcHgiID48dHI+PHRkIHN0eWxlPSJoZWlnaHQ6MDtsaW5lLWhlaWdodDowOyI+ICZuYnNwOwogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvdGQ+PC90cj48L3RhYmxlPjwhW2VuZGlmXS0tPgoKCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvdGQ+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L3RyPgoKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRkCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgYWxpZ249ImNlbnRlciIgc3R5bGU9ImZvbnQtc2l6ZTowcHg7cGFkZGluZzoxNnB4O3dvcmQtYnJlYWs6YnJlYWstd29yZDsiCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgID4KCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPGRpdgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgc3R5bGU9ImZvbnQtZmFtaWx5Ont7LkZvbnRGYW1pbHl9fTtmb250LXNpemU6MTNweDtsaW5lLWhlaWdodDoxO3RleHQtYWxpZ246Y2VudGVyO2NvbG9yOnt7LkZvbnRDb2xvcn19OyIKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA+e3suRm9vdGVyVGV4dH19PC9kaXY+CgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L3RkPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC90cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHt7ZW5kfX0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvdGJvZHk+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC90YWJsZT4KCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvdGQ+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L3RyPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC90Ym9keT4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L3RhYmxlPgoKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC9kaXY+CgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8IS0tW2lmIG1zbyB8IElFXT48L3RkPjwvdHI+PC90YWJsZT48IVtlbmRpZl0tLT4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvdGQ+CiAgICAgICAgICAgICAgICAgICAgICAgICAgPC90cj4KICAgICAgICAgICAgICAgICAgICAgICAgICA8L3Rib2R5PgogICAgICAgICAgICAgICAgICAgICAgICA8L3RhYmxlPgoKICAgICAgICAgICAgICAgICAgICAgIDwvZGl2PgoKCiAgICAgICAgICAgICAgICAgICAgICA8IS0tW2lmIG1zbyB8IElFXT48L3RkPjwvdHI+PC90YWJsZT48IVtlbmRpZl0tLT4KCgogICAgICAgICAgICAgICAgICAgIDwvdGQ+CiAgICAgICAgICAgICAgICAgIDwvdHI+CiAgICAgICAgICAgICAgICAgIDwvdGJvZHk+CiAgICAgICAgICAgICAgICA8L3RhYmxlPgoKICAgICAgICAgICAgICAgIDwhLS1baWYgbXNvIHwgSUVdPjwvdGQ+PC90cj48L3RhYmxlPjwhW2VuZGlmXS0tPgogICAgICAgICAgICAgIDwvdGQ+CiAgICAgICAgICAgIDwvdHI+CiAgICAgICAgICAgIDwvdGJvZHk+CiAgICAgICAgICA8L3RhYmxlPgoKICAgICAgICA8L2Rpdj4KCgogICAgICAgIDwhLS1baWYgbXNvIHwgSUVdPjwvdGQ+PC90cj48L3RhYmxlPjwhW2VuZGlmXS0tPgoKCiAgICAgIDwvdGQ+CiAgICA8L3RyPgogICAgPC90Ym9keT4KICA8L3RhYmxlPgoKPC9kaXY+Cgo8L2JvZHk+CjwvaHRtbD4KICA= + Step16: + DefaultMessageTexts: + - MessageTextType: InitCode + Language: de + Title: Zitadel - User initialisieren + PreHeader: User initialisieren + Subject: User initialisieren + Greeting: Hallo {{.FirstName}} {{.LastName}}, + Text: Dieser Benutzer wurde soeben im Zitadel erstellt. Mit dem Benutzernamen <br><strong>{{.PreferredLoginName}}</strong><br> kannst du dich anmelden. Nutze den untenstehenden Button, um die Initialisierung abzuschliessen <br>(Code <strong>{{.Code}}</strong>).<br> Falls du dieses Mail nicht angefordert hast, kannst du es einfach ignorieren. + ButtonText: Initialisierung abschliessen + - MessageTextType: PasswordReset + Language: de + Title: Zitadel - Passwort zurücksetzen + PreHeader: Passwort zurücksetzen + Subject: Passwort zurücksetzen + Greeting: Hallo {{.FirstName}} {{.LastName}}, + Text: Wir haben eine Anfrage für das Zurücksetzen deines Passwortes bekommen. Du kannst den untenstehenden Button verwenden, um dein Passwort zurückzusetzen <br>(Code <strong>{{.Code}}</strong>).<br> Falls du dieses Mail nicht angefordert hast, kannst du es ignorieren. + ButtonText: Passwort zurücksetzen + - MessageTextType: VerifyEmail + Language: de + Title: Zitadel - Email verifizieren + PreHeader: Email verifizieren + Subject: Email verifizieren + Greeting: Hallo {{.FirstName}} {{.LastName}}, + Text: Eine neue E-Mail Adresse wurde hinzugefügt. Bitte verwende den untenstehenden Button um diese zu verifizieren <br>(Code <strong>{{.Code}}</strong>).<br> Falls du deine E-Mail Adresse nicht selber hinzugefügt hast, kannst du dieses E-Mail ignorieren. + ButtonText: Email verifizieren + - MessageTextType: VerifyPhone + Language: de + Title: Zitadel - Telefonnummer verifizieren + PreHeader: Telefonnummer verifizieren + Subject: Telefonnummer verifizieren + Greeting: Hallo {{.FirstName}} {{.LastName}}, + Text: Eine Telefonnummer wurde hinzugefügt. Bitte verifiziere diese in dem du folgenden Code eingibst (Code {{.Code}}) + ButtonText: Telefon verifizieren + - MessageTextType: DomainClaimed + Language: de + Title: Zitadel - Domain wurde beansprucht + PreHeader: Email / Username ändern + Subject: Domain wurde beansprucht + Greeting: Hallo {{.FirstName}} {{.LastName}}, + Text: Die Domain {{.Domain}} wurde von einer Organisation beansprucht. Dein derzeitiger User {{.Username}} ist nicht Teil dieser Organisation. Daher musst du beim nächsten Login eine neue Email hinterlegen. Für diesen Login haben wir dir einen temporären Usernamen ({{.TempUsername}}) erstellt. + ButtonText: Login + - MessageTextType: InitCode + Language: en + Title: Zitadel - Initialize User + PreHeader: Initialize User + Subject: Initialize User + Greeting: Hello {{.FirstName}} {{.LastName}}, + Text: This user was created in Zitadel. Use the username {{.PreferredLoginName}} to login. Please click the button below to finish the initialization process. (Code {{.Code}}) If you didn't ask for this mail, please ignore it. + ButtonText: Finish initialization + - MessageTextType: PasswordReset + Language: en + Title: Zitadel - Reset password + PreHeader: Reset password + Subject: Reset password + Greeting: Hello {{.FirstName}} {{.LastName}}, + Text: We received a password reset request. Please use the button below to reset your password. (Code {{.Code}}) If you didn't ask for this mail, please ignore it. + ButtonText: Reset password + - MessageTextType: VerifyEmail + Language: en + Title: Zitadel - Verify email + PreHeader: Verify email + Subject: Verify email + Greeting: Hello {{.FirstName}} {{.LastName}}, + Text: A new email has been added. Please use the button below to verify your mail. (Code {{.Code}}) If you din't add a new email, please ignore this email. + ButtonText: Verify email + - MessageTextType: VerifyPhone + Language: en + Title: Zitadel - Verify phone + PreHeader: Verify phone + Subject: Verify phone + Greeting: Hello {{.FirstName}} {{.LastName}}, + Text: A new phonenumber has been added. Please use the following code to verify it {{.Code}}. + ButtonText: Verify phone + - MessageTextType: DomainClaimed + Language: en + Title: Zitadel - Domain has been claimed + PreHeader: Change email / username + Subject: Domain has been claimed + Greeting: Hello {{.FirstName}} {{.LastName}}, + Text: The domain {{.Domain}} has been claimed by an organisation. Your current user {{.UserName}} is not part of this organisation. Therefore you'll have to change your email when you login. We have created a temporary username ({{.TempUsername}}) for this login. + ButtonText: Login \ No newline at end of file diff --git a/console/src/app/modules/features/features.component.html b/console/src/app/modules/features/features.component.html index 20d24297a3..3fbca69eef 100644 --- a/console/src/app/modules/features/features.component.html +++ b/console/src/app/modules/features/features.component.html @@ -141,7 +141,15 @@ - + + +
+ {{'FEATURES.DATA.CUSTOMTEXT' | translate}} + + + +
diff --git a/console/src/app/modules/features/features.component.ts b/console/src/app/modules/features/features.component.ts index 7f6d03c2e2..7444f99a61 100644 --- a/console/src/app/modules/features/features.component.ts +++ b/console/src/app/modules/features/features.component.ts @@ -161,6 +161,7 @@ export class FeaturesComponent implements OnDestroy { req.setLabelPolicyPrivateLabel(this.features.labelPolicyPrivateLabel); req.setLabelPolicyWatermark(this.features.labelPolicyWatermark); req.setCustomDomain(this.features.customDomain); + req.setCustomText(this.features.customText); this.adminService.setOrgFeatures(req).then(() => { this.toast.showInfo('POLICY.TOAST.SET', true); @@ -181,6 +182,7 @@ export class FeaturesComponent implements OnDestroy { dreq.setLabelPolicyPrivateLabel(this.features.labelPolicyPrivateLabel); dreq.setLabelPolicyWatermark(this.features.labelPolicyWatermark); dreq.setCustomDomain(this.features.customDomain); + dreq.setCustomText(this.features.customText); this.adminService.setDefaultFeatures(dreq).then(() => { this.toast.showInfo('POLICY.TOAST.SET', true); diff --git a/console/src/assets/i18n/de.json b/console/src/assets/i18n/de.json index 39e1e5ba7f..9ac6d424e7 100644 --- a/console/src/assets/i18n/de.json +++ b/console/src/assets/i18n/de.json @@ -611,12 +611,13 @@ "LOGINPOLICYPASSWORDRESET": "Login Richtlinie: Passwort vergessen Link nicht anzeigen - benutzerdefiniert", "LOGINPOLICYREGISTRATION": "Login Richtlinie: Registration erlauben - benutzerdefiniert", "LOGINPOLICYIDP": "Login Richtlinie: Identity Providers - benutzerdefiniert", - "LOGINPOLICYFACTORS": "Login Richtlinie: Mltifaktoren - benutzerdefiniert", + "LOGINPOLICYFACTORS": "Login Richtlinie: Multifaktoren - benutzerdefiniert", "LOGINPOLICYPASSWORDLESS": "Login Richtlinie: Passwortlose Authentifizierung - benutzerdefiniert", "LOGINPOLICYCOMPLEXITYPOLICY": "Passwortkomplexitäts Richtlinie - benutzerdefiniert", "LABELPOLICYPRIVATELABEL": "Label Richtlinie - benutzerdefiniert", "LABELPOLICYWATERMARK": "Label Richtlinie - Wasserzeichen", - "CUSTOMDOMAIN": "Domänen Verifikation - verfügbar" + "CUSTOMDOMAIN": "Domänen Verifikation - verfügbar", + "CUSTOMTEXT": "Benutzerdefinierte Texte" }, "TIERSTATES": { "0": "Aktiv", diff --git a/console/src/assets/i18n/en.json b/console/src/assets/i18n/en.json index d9b9d838c4..19b034e29f 100644 --- a/console/src/assets/i18n/en.json +++ b/console/src/assets/i18n/en.json @@ -616,7 +616,8 @@ "LOGINPOLICYCOMPLEXITYPOLICY": "Password Complexity Policy - custom", "LABELPOLICYPRIVATELABEL": "Label Richtlinie - benutzerdefiniert", "LABELPOLICYWATERMARK": "Label Richtlinie - Wasserzeichen", - "CUSTOMDOMAIN": "Domain Verification - available" + "CUSTOMDOMAIN": "Domain Verification - available", + "CUSTOMTEXT": "Custom texts" }, "TIERSTATES": { "0": "Active", diff --git a/docs/docs/apis/proto/admin.md b/docs/docs/apis/proto/admin.md index a9d252a8a6..a2a8cc8ef2 100644 --- a/docs/docs/apis/proto/admin.md +++ b/docs/docs/apis/proto/admin.md @@ -528,6 +528,121 @@ it impacts all organisations without a customised policy +### GetDefaultInitMessageText + +> **rpc** GetDefaultInitMessageText([GetDefaultInitMessageTextRequest](#getdefaultinitmessagetextrequest)) +[GetDefaultInitMessageTextResponse](#getdefaultinitmessagetextresponse) + +Returns the custom text for initial message + + + + +### SetDefaultInitMessageText + +> **rpc** SetDefaultInitMessageText([SetDefaultInitMessageTextRequest](#setdefaultinitmessagetextrequest)) +[SetDefaultInitMessageTextResponse](#setdefaultinitmessagetextresponse) + +Sets the default custom text for initial message +it impacts all organisations without customized initial message text +The Following Variables can be used: +{{.Code}} {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}} + + + + +### GetDefaultPasswordResetMessageText + +> **rpc** GetDefaultPasswordResetMessageText([GetDefaultPasswordResetMessageTextRequest](#getdefaultpasswordresetmessagetextrequest)) +[GetDefaultPasswordResetMessageTextResponse](#getdefaultpasswordresetmessagetextresponse) + +Returns the custom text for password reset message + + + + +### SetDefaultPasswordResetMessageText + +> **rpc** SetDefaultPasswordResetMessageText([SetDefaultPasswordResetMessageTextRequest](#setdefaultpasswordresetmessagetextrequest)) +[SetDefaultPasswordResetMessageTextResponse](#setdefaultpasswordresetmessagetextresponse) + +Sets the default custom text for password reset message +it impacts all organisations without customized password reset message text +The Following Variables can be used: +{{.Code}} {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}} + + + + +### GetDefaultVerifyEmailMessageText + +> **rpc** GetDefaultVerifyEmailMessageText([GetDefaultVerifyEmailMessageTextRequest](#getdefaultverifyemailmessagetextrequest)) +[GetDefaultVerifyEmailMessageTextResponse](#getdefaultverifyemailmessagetextresponse) + +Returns the custom text for verify email message + + + + +### SetDefaultVerifyEmailMessageText + +> **rpc** SetDefaultVerifyEmailMessageText([SetDefaultVerifyEmailMessageTextRequest](#setdefaultverifyemailmessagetextrequest)) +[SetDefaultVerifyEmailMessageTextResponse](#setdefaultverifyemailmessagetextresponse) + +Sets the default custom text for verify email message +it impacts all organisations without customized verify email message text +The Following Variables can be used: +{{.Code}} {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}} + + + + +### GetDefaultVerifyPhoneMessageText + +> **rpc** GetDefaultVerifyPhoneMessageText([GetDefaultVerifyPhoneMessageTextRequest](#getdefaultverifyphonemessagetextrequest)) +[GetDefaultVerifyPhoneMessageTextResponse](#getdefaultverifyphonemessagetextresponse) + +Returns the custom text for verify phone message + + + + +### SetDefaultVerifyPhoneMessageText + +> **rpc** SetDefaultVerifyPhoneMessageText([SetDefaultVerifyPhoneMessageTextRequest](#setdefaultverifyphonemessagetextrequest)) +[SetDefaultVerifyPhoneMessageTextResponse](#setdefaultverifyphonemessagetextresponse) + +Sets the default custom text for verify phone message +it impacts all organisations without customized verify phone message text +The Following Variables can be used: +{{.Code}} {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}} + + + + +### GetDefaultDomainClaimedMessageText + +> **rpc** GetDefaultDomainClaimedMessageText([GetDefaultDomainClaimedMessageTextRequest](#getdefaultdomainclaimedmessagetextrequest)) +[GetDefaultDomainClaimedMessageTextResponse](#getdefaultdomainclaimedmessagetextresponse) + +Returns the custom text for domain claimed message + + + + +### SetDefaultDomainClaimedMessageText + +> **rpc** SetDefaultDomainClaimedMessageText([SetDefaultDomainClaimedMessageTextRequest](#setdefaultdomainclaimedmessagetextrequest)) +[SetDefaultDomainClaimedMessageTextResponse](#setdefaultdomainclaimedmessagetextresponse) + +Sets the default custom text for domain claimed phone message +it impacts all organisations without customized verify phone message text +The Following Variables can be used: +{{.Domain}} {{.TempUsername}} {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}} + + + + ### ListIAMMemberRoles > **rpc** ListIAMMemberRoles([ListIAMMemberRolesRequest](#listiammemberrolesrequest)) @@ -877,6 +992,28 @@ This is an empty response +### GetDefaultDomainClaimedMessageTextRequest +This is an empty request + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| language | string | - | string.min_len: 1
string.max_len: 200
| + + + + +### GetDefaultDomainClaimedMessageTextResponse + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| custom_text | zitadel.text.v1.MessageCustomText | - | | + + + + ### GetDefaultFeaturesRequest @@ -894,6 +1031,94 @@ This is an empty response +### GetDefaultInitMessageTextRequest +This is an empty request + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| language | string | - | string.min_len: 1
string.max_len: 200
| + + + + +### GetDefaultInitMessageTextResponse + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| custom_text | zitadel.text.v1.MessageCustomText | - | | + + + + +### GetDefaultPasswordResetMessageTextRequest +This is an empty request + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| language | string | - | string.min_len: 1
string.max_len: 200
| + + + + +### GetDefaultPasswordResetMessageTextResponse + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| custom_text | zitadel.text.v1.MessageCustomText | - | | + + + + +### GetDefaultVerifyEmailMessageTextRequest +This is an empty request + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| language | string | - | string.min_len: 1
string.max_len: 200
| + + + + +### GetDefaultVerifyEmailMessageTextResponse + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| custom_text | zitadel.text.v1.MessageCustomText | - | | + + + + +### GetDefaultVerifyPhoneMessageTextRequest +This is an empty request + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| language | string | - | string.min_len: 1
string.max_len: 200
| + + + + +### GetDefaultVerifyPhoneMessageTextResponse + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| custom_text | zitadel.text.v1.MessageCustomText | - | | + + + + ### GetIDPByIDRequest @@ -1593,6 +1818,35 @@ This is an empty request +### SetDefaultDomainClaimedMessageTextRequest + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| language | string | - | string.min_len: 1
string.max_len: 200
| +| title | string | - | string.max_len: 200
| +| pre_header | string | - | string.max_len: 200
| +| subject | string | - | string.max_len: 200
| +| greeting | string | - | string.max_len: 200
| +| text | string | - | string.max_len: 800
| +| button_text | string | - | string.max_len: 200
| +| footer_text | string | - | string.max_len: 200
| + + + + +### SetDefaultDomainClaimedMessageTextResponse + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| details | zitadel.v1.ObjectDetails | - | | + + + + ### SetDefaultFeaturesRequest @@ -1613,6 +1867,7 @@ This is an empty request | login_policy_password_reset | bool | - | | | label_policy_private_label | bool | - | | | label_policy_watermark | bool | - | | +| custom_text | bool | - | | @@ -1628,6 +1883,122 @@ This is an empty request +### SetDefaultInitMessageTextRequest + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| language | string | - | string.min_len: 1
string.max_len: 200
| +| title | string | - | string.max_len: 200
| +| pre_header | string | - | string.max_len: 200
| +| subject | string | - | string.max_len: 200
| +| greeting | string | - | string.max_len: 200
| +| text | string | - | string.max_len: 1000
| +| button_text | string | - | string.max_len: 200
| +| footer_text | string | - | string.max_len: 200
| + + + + +### SetDefaultInitMessageTextResponse + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| details | zitadel.v1.ObjectDetails | - | | + + + + +### SetDefaultPasswordResetMessageTextRequest + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| language | string | - | string.min_len: 1
string.max_len: 200
| +| title | string | - | string.max_len: 200
| +| pre_header | string | - | string.max_len: 200
| +| subject | string | - | string.max_len: 200
| +| greeting | string | - | string.max_len: 200
| +| text | string | - | string.max_len: 800
| +| button_text | string | - | string.max_len: 200
| +| footer_text | string | - | string.max_len: 200
| + + + + +### SetDefaultPasswordResetMessageTextResponse + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| details | zitadel.v1.ObjectDetails | - | | + + + + +### SetDefaultVerifyEmailMessageTextRequest + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| language | string | - | string.min_len: 1
string.max_len: 200
| +| title | string | - | string.max_len: 200
| +| pre_header | string | - | string.max_len: 200
| +| subject | string | - | string.max_len: 200
| +| greeting | string | - | string.max_len: 200
| +| text | string | - | string.max_len: 800
| +| button_text | string | - | string.max_len: 200
| +| footer_text | string | - | string.max_len: 200
| + + + + +### SetDefaultVerifyEmailMessageTextResponse + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| details | zitadel.v1.ObjectDetails | - | | + + + + +### SetDefaultVerifyPhoneMessageTextRequest + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| language | string | - | string.min_len: 1
string.max_len: 200
| +| title | string | - | string.max_len: 200
| +| pre_header | string | - | string.max_len: 200
| +| subject | string | - | string.max_len: 200
| +| greeting | string | - | string.max_len: 200
| +| text | string | - | string.max_len: 800
| +| button_text | string | - | string.max_len: 200
| +| footer_text | string | - | string.max_len: 200
| + + + + +### SetDefaultVerifyPhoneMessageTextResponse + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| details | zitadel.v1.ObjectDetails | - | | + + + + ### SetOrgFeaturesRequest @@ -1651,6 +2022,7 @@ This is an empty request | login_policy_password_reset | bool | - | | | label_policy_private_label | bool | - | | | label_policy_watermark | bool | - | | +| custom_text | bool | - | | diff --git a/docs/docs/apis/proto/management.md b/docs/docs/apis/proto/management.md index 492e4bd0b6..66e7ab5248 100644 --- a/docs/docs/apis/proto/management.md +++ b/docs/docs/apis/proto/management.md @@ -1712,6 +1712,176 @@ The default policy of the IAM will trigger after +### GetCustomInitMessageText + +> **rpc** GetCustomInitMessageText([GetCustomInitMessageTextRequest](#getcustominitmessagetextrequest)) +[GetCustomInitMessageTextResponse](#getcustominitmessagetextresponse) + +Returns the custom text for initial message + + + + +### SetCustomInitMessageText + +> **rpc** SetCustomInitMessageText([SetCustomInitMessageTextRequest](#setcustominitmessagetextrequest)) +[SetCustomInitMessageTextResponse](#setcustominitmessagetextresponse) + +Sets the default custom text for initial message +it impacts all organisations without customized initial message text +The Following Variables can be used: +{{.Code}} {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}} + + + + +### ResetCustomInitMessageTextToDefault + +> **rpc** ResetCustomInitMessageTextToDefault([ResetCustomInitMessageTextToDefaultRequest](#resetcustominitmessagetexttodefaultrequest)) +[ResetCustomInitMessageTextToDefaultResponse](#resetcustominitmessagetexttodefaultresponse) + +Removes the custom init message text of the organisation +The default text of the IAM will trigger after + + + + +### GetCustomPasswordResetMessageText + +> **rpc** GetCustomPasswordResetMessageText([GetCustomPasswordResetMessageTextRequest](#getcustompasswordresetmessagetextrequest)) +[GetCustomPasswordResetMessageTextResponse](#getcustompasswordresetmessagetextresponse) + +Returns the custom text for password reset message + + + + +### SetCustomPasswordResetMessageText + +> **rpc** SetCustomPasswordResetMessageText([SetCustomPasswordResetMessageTextRequest](#setcustompasswordresetmessagetextrequest)) +[SetCustomPasswordResetMessageTextResponse](#setcustompasswordresetmessagetextresponse) + +Sets the default custom text for password reset message +it impacts all organisations without customized password reset message text +The Following Variables can be used: +{{.Code}} {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}} + + + + +### ResetCustomPasswordResetMessageTextToDefault + +> **rpc** ResetCustomPasswordResetMessageTextToDefault([ResetCustomPasswordResetMessageTextToDefaultRequest](#resetcustompasswordresetmessagetexttodefaultrequest)) +[ResetCustomPasswordResetMessageTextToDefaultResponse](#resetcustompasswordresetmessagetexttodefaultresponse) + +Removes the custom init message text of the organisation +The default text of the IAM will trigger after + + + + +### GetCustomVerifyEmailMessageText + +> **rpc** GetCustomVerifyEmailMessageText([GetCustomVerifyEmailMessageTextRequest](#getcustomverifyemailmessagetextrequest)) +[GetCustomVerifyEmailMessageTextResponse](#getcustomverifyemailmessagetextresponse) + +Returns the custom text for verify email message + + + + +### SetCustomVerifyEmailMessageText + +> **rpc** SetCustomVerifyEmailMessageText([SetCustomVerifyEmailMessageTextRequest](#setcustomverifyemailmessagetextrequest)) +[SetCustomVerifyEmailMessageTextResponse](#setcustomverifyemailmessagetextresponse) + +Sets the default custom text for verify email message +it impacts all organisations without customized verify email message text +The Following Variables can be used: +{{.Code}} {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}} + + + + +### ResetCustomVerifyEmailMessageTextToDefault + +> **rpc** ResetCustomVerifyEmailMessageTextToDefault([ResetCustomVerifyEmailMessageTextToDefaultRequest](#resetcustomverifyemailmessagetexttodefaultrequest)) +[ResetCustomVerifyEmailMessageTextToDefaultResponse](#resetcustomverifyemailmessagetexttodefaultresponse) + +Removes the custom init message text of the organisation +The default text of the IAM will trigger after + + + + +### GetCustomVerifyPhoneMessageText + +> **rpc** GetCustomVerifyPhoneMessageText([GetCustomVerifyPhoneMessageTextRequest](#getcustomverifyphonemessagetextrequest)) +[GetCustomVerifyPhoneMessageTextResponse](#getcustomverifyphonemessagetextresponse) + +Returns the custom text for verify email message + + + + +### SetCustomVerifyPhoneMessageText + +> **rpc** SetCustomVerifyPhoneMessageText([SetCustomVerifyPhoneMessageTextRequest](#setcustomverifyphonemessagetextrequest)) +[SetCustomVerifyPhoneMessageTextResponse](#setcustomverifyphonemessagetextresponse) + +Sets the default custom text for verify email message +it impacts all organisations without customized verify email message text +The Following Variables can be used: +{{.Code}} {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}} + + + + +### ResetCustomVerifyPhoneMessageTextToDefault + +> **rpc** ResetCustomVerifyPhoneMessageTextToDefault([ResetCustomVerifyPhoneMessageTextToDefaultRequest](#resetcustomverifyphonemessagetexttodefaultrequest)) +[ResetCustomVerifyPhoneMessageTextToDefaultResponse](#resetcustomverifyphonemessagetexttodefaultresponse) + +Removes the custom init message text of the organisation +The default text of the IAM will trigger after + + + + +### GetCustomDomainClaimedMessageText + +> **rpc** GetCustomDomainClaimedMessageText([GetCustomDomainClaimedMessageTextRequest](#getcustomdomainclaimedmessagetextrequest)) +[GetCustomDomainClaimedMessageTextResponse](#getcustomdomainclaimedmessagetextresponse) + +Returns the custom text for domain claimed message + + + + +### SetCustomDomainClaimedMessageCustomText + +> **rpc** SetCustomDomainClaimedMessageCustomText([SetCustomDomainClaimedMessageTextRequest](#setcustomdomainclaimedmessagetextrequest)) +[SetCustomDomainClaimedMessageTextResponse](#setcustomdomainclaimedmessagetextresponse) + +Sets the default custom text for domain claimed message +it impacts all organisations without customized domain claimed message text +The Following Variables can be used: +{{.Domain}} {{.TempUsername}} {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}} + + + + +### ResetCustomDomainClaimedMessageTextToDefault + +> **rpc** ResetCustomDomainClaimedMessageTextToDefault([ResetCustomDomainClaimedMessageTextToDefaultRequest](#resetcustomdomainclaimedmessagetexttodefaultrequest)) +[ResetCustomDomainClaimedMessageTextToDefaultResponse](#resetcustomdomainclaimedmessagetexttodefaultresponse) + +Removes the custom init message text of the organisation +The default text of the IAM will trigger after + + + + ### GetOrgIDPByID > **rpc** GetOrgIDPByID([GetOrgIDPByIDRequest](#getorgidpbyidrequest)) @@ -2758,6 +2928,116 @@ This is an empty request +### GetCustomDomainClaimedMessageTextRequest +This is an empty request + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| language | string | - | string.min_len: 1
string.max_len: 200
| + + + + +### GetCustomDomainClaimedMessageTextResponse + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| custom_text | zitadel.text.v1.MessageCustomText | - | | + + + + +### GetCustomInitMessageTextRequest +This is an empty request + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| language | string | - | string.min_len: 1
string.max_len: 200
| + + + + +### GetCustomInitMessageTextResponse + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| custom_text | zitadel.text.v1.MessageCustomText | - | | + + + + +### GetCustomPasswordResetMessageTextRequest +This is an empty request + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| language | string | - | string.min_len: 1
string.max_len: 200
| + + + + +### GetCustomPasswordResetMessageTextResponse + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| custom_text | zitadel.text.v1.MessageCustomText | - | | + + + + +### GetCustomVerifyEmailMessageTextRequest +This is an empty request + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| language | string | - | string.min_len: 1
string.max_len: 200
| + + + + +### GetCustomVerifyEmailMessageTextResponse + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| custom_text | zitadel.text.v1.MessageCustomText | - | | + + + + +### GetCustomVerifyPhoneMessageTextRequest +This is an empty request + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| language | string | - | string.min_len: 1
string.max_len: 200
| + + + + +### GetCustomVerifyPhoneMessageTextResponse + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| custom_text | zitadel.text.v1.MessageCustomText | - | | + + + + ### GetDefaultLabelPolicyRequest This is an empty request @@ -4972,6 +5252,116 @@ This is an empty response +### ResetCustomDomainClaimedMessageTextToDefaultRequest +This is an empty request + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| language | string | - | string.min_len: 1
string.max_len: 200
| + + + + +### ResetCustomDomainClaimedMessageTextToDefaultResponse + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| details | zitadel.v1.ObjectDetails | - | | + + + + +### ResetCustomInitMessageTextToDefaultRequest +This is an empty request + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| language | string | - | string.min_len: 1
string.max_len: 200
| + + + + +### ResetCustomInitMessageTextToDefaultResponse + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| details | zitadel.v1.ObjectDetails | - | | + + + + +### ResetCustomPasswordResetMessageTextToDefaultRequest +This is an empty request + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| language | string | - | string.min_len: 1
string.max_len: 200
| + + + + +### ResetCustomPasswordResetMessageTextToDefaultResponse + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| details | zitadel.v1.ObjectDetails | - | | + + + + +### ResetCustomVerifyEmailMessageTextToDefaultRequest +This is an empty request + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| language | string | - | string.min_len: 1
string.max_len: 200
| + + + + +### ResetCustomVerifyEmailMessageTextToDefaultResponse + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| details | zitadel.v1.ObjectDetails | - | | + + + + +### ResetCustomVerifyPhoneMessageTextToDefaultRequest +This is an empty request + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| language | string | - | string.min_len: 1
string.max_len: 200
| + + + + +### ResetCustomVerifyPhoneMessageTextToDefaultResponse + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| details | zitadel.v1.ObjectDetails | - | | + + + + ### ResetLabelPolicyToDefaultRequest This is an empty request @@ -5080,6 +5470,151 @@ This is an empty request +### SetCustomDomainClaimedMessageTextRequest + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| language | string | - | string.min_len: 1
string.max_len: 200
| +| title | string | - | string.max_len: 200
| +| pre_header | string | - | string.max_len: 200
| +| subject | string | - | string.max_len: 200
| +| greeting | string | - | string.max_len: 200
| +| text | string | - | string.max_len: 800
| +| button_text | string | - | string.max_len: 200
| +| footer_text | string | - | string.max_len: 200
| + + + + +### SetCustomDomainClaimedMessageTextResponse + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| details | zitadel.v1.ObjectDetails | - | | + + + + +### SetCustomInitMessageTextRequest + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| language | string | - | string.min_len: 1
string.max_len: 200
| +| title | string | - | string.max_len: 200
| +| pre_header | string | - | string.max_len: 200
| +| subject | string | - | string.max_len: 200
| +| greeting | string | - | string.max_len: 200
| +| text | string | - | string.max_len: 800
| +| button_text | string | - | string.max_len: 200
| +| footer_text | string | - | string.max_len: 200
| + + + + +### SetCustomInitMessageTextResponse + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| details | zitadel.v1.ObjectDetails | - | | + + + + +### SetCustomPasswordResetMessageTextRequest + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| language | string | - | string.min_len: 1
string.max_len: 200
| +| title | string | - | string.max_len: 200
| +| pre_header | string | - | string.max_len: 200
| +| subject | string | - | string.max_len: 200
| +| greeting | string | - | string.max_len: 200
| +| text | string | - | string.max_len: 800
| +| button_text | string | - | string.max_len: 200
| +| footer_text | string | - | string.max_len: 200
| + + + + +### SetCustomPasswordResetMessageTextResponse + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| details | zitadel.v1.ObjectDetails | - | | + + + + +### SetCustomVerifyEmailMessageTextRequest + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| language | string | - | string.min_len: 1
string.max_len: 200
| +| title | string | - | string.max_len: 200
| +| pre_header | string | - | string.max_len: 200
| +| subject | string | - | string.max_len: 200
| +| greeting | string | - | string.max_len: 200
| +| text | string | - | string.max_len: 800
| +| button_text | string | - | string.max_len: 200
| +| footer_text | string | - | string.max_len: 200
| + + + + +### SetCustomVerifyEmailMessageTextResponse + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| details | zitadel.v1.ObjectDetails | - | | + + + + +### SetCustomVerifyPhoneMessageTextRequest + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| language | string | - | string.min_len: 1
string.max_len: 200
| +| title | string | - | string.max_len: 200
| +| pre_header | string | - | string.max_len: 200
| +| subject | string | - | string.max_len: 200
| +| greeting | string | - | string.max_len: 200
| +| text | string | - | string.max_len: 800
| +| button_text | string | - | string.max_len: 200
| +| footer_text | string | - | string.max_len: 200
| + + + + +### SetCustomVerifyPhoneMessageTextResponse + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| details | zitadel.v1.ObjectDetails | - | | + + + + ### SetHumanInitialPasswordRequest diff --git a/internal/admin/repository/eventsourcing/eventstore/iam.go b/internal/admin/repository/eventsourcing/eventstore/iam.go index be2902f790..670809d271 100644 --- a/internal/admin/repository/eventsourcing/eventstore/iam.go +++ b/internal/admin/repository/eventsourcing/eventstore/iam.go @@ -2,16 +2,18 @@ package eventstore import ( "context" + "strings" + "github.com/caos/zitadel/internal/domain" "github.com/caos/zitadel/internal/eventstore/v1" "github.com/caos/zitadel/internal/eventstore/v1/models" iam_view "github.com/caos/zitadel/internal/iam/repository/view" "github.com/caos/zitadel/internal/user/repository/view/model" - "strings" caos_errs "github.com/caos/zitadel/internal/errors" "github.com/caos/logging" + admin_view "github.com/caos/zitadel/internal/admin/repository/eventsourcing/view" "github.com/caos/zitadel/internal/config/systemdefaults" iam_model "github.com/caos/zitadel/internal/iam/model" @@ -347,21 +349,21 @@ func (repo *IAMRepository) SearchIAMMembersx(ctx context.Context, request *iam_m return result, nil } -func (repo *IAMRepository) GetDefaultMailTexts(ctx context.Context) (*iam_model.MailTextsView, error) { - text, err := repo.View.MailTexts(repo.SystemDefaults.IamID) +func (repo *IAMRepository) GetDefaultMessageTexts(ctx context.Context) (*iam_model.MessageTextsView, error) { + text, err := repo.View.MessageTexts(repo.SystemDefaults.IamID) if err != nil { return nil, err } - return iam_es_model.MailTextsViewToModel(text, true), err + return iam_es_model.MessageTextsViewToModel(text, true), err } -func (repo *IAMRepository) GetDefaultMailText(ctx context.Context, textType string, language string) (*iam_model.MailTextView, error) { - text, err := repo.View.MailTextByIDs(repo.SystemDefaults.IamID, textType, language) +func (repo *IAMRepository) GetDefaultMessageText(ctx context.Context, textType, lang string) (*iam_model.MessageTextView, error) { + text, err := repo.View.MessageTextByIDs(repo.SystemDefaults.IamID, textType, lang) if err != nil { return nil, err } text.Default = true - return iam_es_model.MailTextViewToModel(text), err + return iam_es_model.MessageTextViewToModel(text), err } func (repo *IAMRepository) getIAMEvents(ctx context.Context, sequence uint64) ([]*models.Event, error) { diff --git a/internal/admin/repository/eventsourcing/handler/handler.go b/internal/admin/repository/eventsourcing/handler/handler.go index b8db6cd996..9695aeb756 100644 --- a/internal/admin/repository/eventsourcing/handler/handler.go +++ b/internal/admin/repository/eventsourcing/handler/handler.go @@ -62,8 +62,8 @@ func Register(configs Configs, bulkLimit, errorCount uint64, view *view.View, es defaults), newMailTemplate( handler{view, bulkLimit, configs.cycleDuration("MailTemplate"), errorCount, es}), - newMailText( - handler{view, bulkLimit, configs.cycleDuration("MailText"), errorCount, es}), + newMessageText( + handler{view, bulkLimit, configs.cycleDuration("MessageText"), errorCount, es}), newFeatures( handler{view, bulkLimit, configs.cycleDuration("Features"), errorCount, es}), } diff --git a/internal/admin/repository/eventsourcing/handler/mail_text.go b/internal/admin/repository/eventsourcing/handler/mail_text.go deleted file mode 100644 index 7656fbc1b9..0000000000 --- a/internal/admin/repository/eventsourcing/handler/mail_text.go +++ /dev/null @@ -1,108 +0,0 @@ -package handler - -import ( - "github.com/caos/logging" - "github.com/caos/zitadel/internal/eventstore/v1" - - es_models "github.com/caos/zitadel/internal/eventstore/v1/models" - "github.com/caos/zitadel/internal/eventstore/v1/query" - "github.com/caos/zitadel/internal/eventstore/v1/spooler" - "github.com/caos/zitadel/internal/iam/repository/eventsourcing/model" - iam_es_model "github.com/caos/zitadel/internal/iam/repository/eventsourcing/model" - iam_model "github.com/caos/zitadel/internal/iam/repository/view/model" -) - -type MailText struct { - handler - subscription *v1.Subscription -} - -func newMailText(handler handler) *MailText { - h := &MailText{ - handler: handler, - } - - h.subscribe() - - return h -} - -func (m *MailText) subscribe() { - m.subscription = m.es.Subscribe(m.AggregateTypes()...) - go func() { - for event := range m.subscription.Events { - query.ReduceEvent(m, event) - } - }() -} - -const ( - mailTextTable = "adminapi.mail_texts" -) - -func (m *MailText) ViewModel() string { - return mailTextTable -} - -func (_ *MailText) AggregateTypes() []es_models.AggregateType { - return []es_models.AggregateType{iam_es_model.IAMAggregate} -} - -func (p *MailText) CurrentSequence() (uint64, error) { - sequence, err := p.view.GetLatestMailTextSequence() - if err != nil { - return 0, err - } - return sequence.CurrentSequence, nil -} - -func (m *MailText) EventQuery() (*es_models.SearchQuery, error) { - sequence, err := m.view.GetLatestMailTextSequence() - if err != nil { - return nil, err - } - return es_models.NewSearchQuery(). - AggregateTypeFilter(m.AggregateTypes()...). - LatestSequenceFilter(sequence.CurrentSequence), nil -} - -func (m *MailText) Reduce(event *es_models.Event) (err error) { - switch event.AggregateType { - case model.IAMAggregate: - err = m.processMailText(event) - } - return err -} - -func (m *MailText) processMailText(event *es_models.Event) (err error) { - mailText := new(iam_model.MailTextView) - switch event.Type { - case model.MailTextAdded: - err = mailText.AppendEvent(event) - case model.MailTextChanged: - err = mailText.SetData(event) - if err != nil { - return err - } - mailText, err = m.view.MailTextByIDs(event.AggregateID, mailText.MailTextType, mailText.Language) - if err != nil { - return err - } - err = mailText.AppendEvent(event) - default: - return m.view.ProcessedMailTextSequence(event) - } - if err != nil { - return err - } - return m.view.PutMailText(mailText, event) -} - -func (m *MailText) OnError(event *es_models.Event, err error) error { - logging.LogWithFields("HANDL-5jk84", "id", event.AggregateID).WithError(err).Warn("something went wrong in label mailText handler") - return spooler.HandleError(event, err, m.view.GetLatestMailTextFailedEvent, m.view.ProcessedMailTextFailedEvent, m.view.ProcessedMailTextSequence, m.errorCountUntilSkip) -} - -func (o *MailText) OnSuccess() error { - return spooler.HandleSuccess(o.view.UpdateMailTextSpoolerRunTimestamp) -} diff --git a/internal/admin/repository/eventsourcing/handler/message_text.go b/internal/admin/repository/eventsourcing/handler/message_text.go new file mode 100644 index 0000000000..6a6fc5ca2c --- /dev/null +++ b/internal/admin/repository/eventsourcing/handler/message_text.go @@ -0,0 +1,116 @@ +package handler + +import ( + "github.com/caos/logging" + caos_errs "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/eventstore/v1" + + es_models "github.com/caos/zitadel/internal/eventstore/v1/models" + "github.com/caos/zitadel/internal/eventstore/v1/query" + "github.com/caos/zitadel/internal/eventstore/v1/spooler" + "github.com/caos/zitadel/internal/iam/repository/eventsourcing/model" + iam_es_model "github.com/caos/zitadel/internal/iam/repository/eventsourcing/model" + iam_model "github.com/caos/zitadel/internal/iam/repository/view/model" +) + +type MessageText struct { + handler + subscription *v1.Subscription +} + +func newMessageText(handler handler) *MessageText { + h := &MessageText{ + handler: handler, + } + + h.subscribe() + + return h +} + +func (m *MessageText) subscribe() { + m.subscription = m.es.Subscribe(m.AggregateTypes()...) + go func() { + for event := range m.subscription.Events { + query.ReduceEvent(m, event) + } + }() +} + +const ( + mailTextTable = "adminapi.message_texts" +) + +func (m *MessageText) ViewModel() string { + return mailTextTable +} + +func (_ *MessageText) AggregateTypes() []es_models.AggregateType { + return []es_models.AggregateType{iam_es_model.IAMAggregate} +} + +func (p *MessageText) CurrentSequence() (uint64, error) { + sequence, err := p.view.GetLatestMessageTextSequence() + if err != nil { + return 0, err + } + return sequence.CurrentSequence, nil +} + +func (m *MessageText) EventQuery() (*es_models.SearchQuery, error) { + sequence, err := m.view.GetLatestMessageTextSequence() + if err != nil { + return nil, err + } + return es_models.NewSearchQuery(). + AggregateTypeFilter(m.AggregateTypes()...). + LatestSequenceFilter(sequence.CurrentSequence), nil +} + +func (m *MessageText) Reduce(event *es_models.Event) (err error) { + switch event.AggregateType { + case model.IAMAggregate: + err = m.processMessageText(event) + } + return err +} + +func (m *MessageText) processMessageText(event *es_models.Event) (err error) { + message := new(iam_model.MessageTextView) + switch event.Type { + case model.CustomTextSet, + model.CustomTextRemoved: + text := new(iam_model.CustomText) + err = text.SetData(event) + if err != nil { + return err + } + message, err = m.view.MessageTextByIDs(event.AggregateID, text.Template, text.Language.String()) + if err != nil && !caos_errs.IsNotFound(err) { + return err + } + if caos_errs.IsNotFound(err) { + err = nil + message = new(iam_model.MessageTextView) + message.Language = text.Language.String() + message.MessageTextType = text.Template + message.CreationDate = event.CreationDate + } + err = message.AppendEvent(event) + default: + return m.view.ProcessedMessageTextSequence(event) + } + if err != nil { + return err + } + return m.view.PutMessageText(message, event) +} + +func (m *MessageText) OnError(event *es_models.Event, err error) error { + logging.LogWithFields("HANDL-5jk84", "id", event.AggregateID).WithError(err).Warn("something went wrong in label mailText handler") + return spooler.HandleError(event, err, m.view.GetLatestMessageTextFailedEvent, m.view.ProcessedMessageTextFailedEvent, m.view.ProcessedMessageTextSequence, m.errorCountUntilSkip) +} + +func (o *MessageText) OnSuccess() error { + return spooler.HandleSuccess(o.view.UpdateMessageTextSpoolerRunTimestamp) +} diff --git a/internal/admin/repository/eventsourcing/view/mail_texts.go b/internal/admin/repository/eventsourcing/view/mail_texts.go deleted file mode 100644 index 21aa83b90d..0000000000 --- a/internal/admin/repository/eventsourcing/view/mail_texts.go +++ /dev/null @@ -1,48 +0,0 @@ -package view - -import ( - "github.com/caos/zitadel/internal/eventstore/v1/models" - "github.com/caos/zitadel/internal/iam/repository/view" - "github.com/caos/zitadel/internal/iam/repository/view/model" - global_view "github.com/caos/zitadel/internal/view/repository" -) - -const ( - mailTextTable = "adminapi.mail_texts" -) - -func (v *View) MailTexts(aggregateID string) ([]*model.MailTextView, error) { - return view.GetMailTexts(v.Db, mailTextTable, aggregateID) -} - -func (v *View) MailTextByIDs(aggregateID string, textType string, language string) (*model.MailTextView, error) { - return view.GetMailTextByIDs(v.Db, mailTextTable, aggregateID, textType, language) -} - -func (v *View) PutMailText(template *model.MailTextView, event *models.Event) error { - err := view.PutMailText(v.Db, mailTextTable, template) - if err != nil { - return err - } - return v.ProcessedMailTextSequence(event) -} - -func (v *View) GetLatestMailTextSequence() (*global_view.CurrentSequence, error) { - return v.latestSequence(mailTextTable) -} - -func (v *View) ProcessedMailTextSequence(event *models.Event) error { - return v.saveCurrentSequence(mailTextTable, event) -} - -func (v *View) UpdateMailTextSpoolerRunTimestamp() error { - return v.updateSpoolerRunSequence(mailTextTable) -} - -func (v *View) GetLatestMailTextFailedEvent(sequence uint64) (*global_view.FailedEvent, error) { - return v.latestFailedEvent(mailTextTable, sequence) -} - -func (v *View) ProcessedMailTextFailedEvent(failedEvent *global_view.FailedEvent) error { - return v.saveFailedEvent(failedEvent) -} diff --git a/internal/admin/repository/eventsourcing/view/message_texts.go b/internal/admin/repository/eventsourcing/view/message_texts.go new file mode 100644 index 0000000000..17128d5e17 --- /dev/null +++ b/internal/admin/repository/eventsourcing/view/message_texts.go @@ -0,0 +1,48 @@ +package view + +import ( + "github.com/caos/zitadel/internal/eventstore/v1/models" + "github.com/caos/zitadel/internal/iam/repository/view" + "github.com/caos/zitadel/internal/iam/repository/view/model" + global_view "github.com/caos/zitadel/internal/view/repository" +) + +const ( + messageTextTable = "adminapi.message_texts" +) + +func (v *View) MessageTexts(aggregateID string) ([]*model.MessageTextView, error) { + return view.GetMessageTexts(v.Db, messageTextTable, aggregateID) +} + +func (v *View) MessageTextByIDs(aggregateID, textType, lang string) (*model.MessageTextView, error) { + return view.GetMessageTextByIDs(v.Db, messageTextTable, aggregateID, textType, lang) +} + +func (v *View) PutMessageText(template *model.MessageTextView, event *models.Event) error { + err := view.PutMessageText(v.Db, messageTextTable, template) + if err != nil { + return err + } + return v.ProcessedMessageTextSequence(event) +} + +func (v *View) GetLatestMessageTextSequence() (*global_view.CurrentSequence, error) { + return v.latestSequence(messageTextTable) +} + +func (v *View) ProcessedMessageTextSequence(event *models.Event) error { + return v.saveCurrentSequence(messageTextTable, event) +} + +func (v *View) UpdateMessageTextSpoolerRunTimestamp() error { + return v.updateSpoolerRunSequence(messageTextTable) +} + +func (v *View) GetLatestMessageTextFailedEvent(sequence uint64) (*global_view.FailedEvent, error) { + return v.latestFailedEvent(messageTextTable, sequence) +} + +func (v *View) ProcessedMessageTextFailedEvent(failedEvent *global_view.FailedEvent) error { + return v.saveFailedEvent(failedEvent) +} diff --git a/internal/admin/repository/iam.go b/internal/admin/repository/iam.go index a025d472eb..4bf9c0f653 100644 --- a/internal/admin/repository/iam.go +++ b/internal/admin/repository/iam.go @@ -28,8 +28,8 @@ type IAMRepository interface { GetDefaultMailTemplate(ctx context.Context) (*iam_model.MailTemplateView, error) - GetDefaultMailTexts(ctx context.Context) (*iam_model.MailTextsView, error) - GetDefaultMailText(ctx context.Context, textType string, language string) (*iam_model.MailTextView, error) + GetDefaultMessageTexts(ctx context.Context) (*iam_model.MessageTextsView, error) + GetDefaultMessageText(ctx context.Context, textType string, language string) (*iam_model.MessageTextView, error) GetDefaultPasswordComplexityPolicy(ctx context.Context) (*iam_model.PasswordComplexityPolicyView, error) diff --git a/internal/api/grpc/admin/custom_text.go b/internal/api/grpc/admin/custom_text.go new file mode 100644 index 0000000000..7e430bd69a --- /dev/null +++ b/internal/api/grpc/admin/custom_text.go @@ -0,0 +1,130 @@ +package admin + +import ( + "context" + + "github.com/caos/zitadel/internal/api/grpc/object" + text_grpc "github.com/caos/zitadel/internal/api/grpc/text" + "github.com/caos/zitadel/internal/domain" + admin_pb "github.com/caos/zitadel/pkg/grpc/admin" +) + +func (s *Server) GetDefaultInitMessageText(ctx context.Context, req *admin_pb.GetDefaultInitMessageTextRequest) (*admin_pb.GetDefaultInitMessageTextResponse, error) { + msg, err := s.iam.GetDefaultMessageText(ctx, domain.InitCodeMessageType, req.Language) + if err != nil { + return nil, err + } + return &admin_pb.GetDefaultInitMessageTextResponse{ + CustomText: text_grpc.ModelCustomMsgTextToPb(msg), + }, nil +} + +func (s *Server) SetDefaultInitMessageText(ctx context.Context, req *admin_pb.SetDefaultInitMessageTextRequest) (*admin_pb.SetDefaultInitMessageTextResponse, error) { + result, err := s.command.SetDefaultMessageText(ctx, SetInitCustomTextToDomain(req)) + if err != nil { + return nil, err + } + return &admin_pb.SetDefaultInitMessageTextResponse{ + Details: object.ChangeToDetailsPb( + result.Sequence, + result.EventDate, + result.ResourceOwner, + ), + }, nil +} + +func (s *Server) GetDefaultPasswordResetMessageText(ctx context.Context, req *admin_pb.GetDefaultPasswordResetMessageTextRequest) (*admin_pb.GetDefaultPasswordResetMessageTextResponse, error) { + msg, err := s.iam.GetDefaultMessageText(ctx, domain.PasswordResetMessageType, req.Language) + if err != nil { + return nil, err + } + return &admin_pb.GetDefaultPasswordResetMessageTextResponse{ + CustomText: text_grpc.ModelCustomMsgTextToPb(msg), + }, nil +} + +func (s *Server) SetDefaultPasswordResetMessageText(ctx context.Context, req *admin_pb.SetDefaultPasswordResetMessageTextRequest) (*admin_pb.SetDefaultPasswordResetMessageTextResponse, error) { + result, err := s.command.SetDefaultMessageText(ctx, SetPasswordResetCustomTextToDomain(req)) + if err != nil { + return nil, err + } + return &admin_pb.SetDefaultPasswordResetMessageTextResponse{ + Details: object.ChangeToDetailsPb( + result.Sequence, + result.EventDate, + result.ResourceOwner, + ), + }, nil +} + +func (s *Server) GetDefaultVerifyEmailMessageText(ctx context.Context, req *admin_pb.GetDefaultVerifyEmailMessageTextRequest) (*admin_pb.GetDefaultVerifyEmailMessageTextResponse, error) { + msg, err := s.iam.GetDefaultMessageText(ctx, domain.VerifyEmailMessageType, req.Language) + if err != nil { + return nil, err + } + return &admin_pb.GetDefaultVerifyEmailMessageTextResponse{ + CustomText: text_grpc.ModelCustomMsgTextToPb(msg), + }, nil +} + +func (s *Server) SetVerifyEmailMessageCustomText(ctx context.Context, req *admin_pb.SetDefaultVerifyEmailMessageTextRequest) (*admin_pb.SetDefaultVerifyEmailMessageTextResponse, error) { + result, err := s.command.SetDefaultMessageText(ctx, SetVerifyEmailCustomTextToDomain(req)) + if err != nil { + return nil, err + } + return &admin_pb.SetDefaultVerifyEmailMessageTextResponse{ + Details: object.ChangeToDetailsPb( + result.Sequence, + result.EventDate, + result.ResourceOwner, + ), + }, nil +} + +func (s *Server) GetDefaultVerifyPhoneMessageText(ctx context.Context, req *admin_pb.GetDefaultVerifyPhoneMessageTextRequest) (*admin_pb.GetDefaultVerifyPhoneMessageTextResponse, error) { + msg, err := s.iam.GetDefaultMessageText(ctx, domain.VerifyPhoneMessageType, req.Language) + if err != nil { + return nil, err + } + return &admin_pb.GetDefaultVerifyPhoneMessageTextResponse{ + CustomText: text_grpc.ModelCustomMsgTextToPb(msg), + }, nil +} + +func (s *Server) SetDefaultVerifyPhoneMessageText(ctx context.Context, req *admin_pb.SetDefaultVerifyPhoneMessageTextRequest) (*admin_pb.SetDefaultVerifyPhoneMessageTextResponse, error) { + result, err := s.command.SetDefaultMessageText(ctx, SetVerifyPhoneCustomTextToDomain(req)) + if err != nil { + return nil, err + } + return &admin_pb.SetDefaultVerifyPhoneMessageTextResponse{ + Details: object.ChangeToDetailsPb( + result.Sequence, + result.EventDate, + result.ResourceOwner, + ), + }, nil +} + +func (s *Server) GetDefaultDomainClaimedMessageText(ctx context.Context, req *admin_pb.GetDefaultDomainClaimedMessageTextRequest) (*admin_pb.GetDefaultDomainClaimedMessageTextResponse, error) { + msg, err := s.iam.GetDefaultMessageText(ctx, domain.DomainClaimedMessageType, req.Language) + if err != nil { + return nil, err + } + return &admin_pb.GetDefaultDomainClaimedMessageTextResponse{ + CustomText: text_grpc.ModelCustomMsgTextToPb(msg), + }, nil +} + +func (s *Server) SetDefaultDomainClaimedMessageText(ctx context.Context, req *admin_pb.SetDefaultDomainClaimedMessageTextRequest) (*admin_pb.SetDefaultDomainClaimedMessageTextResponse, error) { + result, err := s.command.SetDefaultMessageText(ctx, SetDomainClaimedCustomTextToDomain(req)) + if err != nil { + return nil, err + } + return &admin_pb.SetDefaultDomainClaimedMessageTextResponse{ + Details: object.ChangeToDetailsPb( + result.Sequence, + result.EventDate, + result.ResourceOwner, + ), + }, nil +} diff --git a/internal/api/grpc/admin/custom_text_converter.go b/internal/api/grpc/admin/custom_text_converter.go new file mode 100644 index 0000000000..05f7afe278 --- /dev/null +++ b/internal/api/grpc/admin/custom_text_converter.go @@ -0,0 +1,83 @@ +package admin + +import ( + "golang.org/x/text/language" + + "github.com/caos/zitadel/internal/domain" + admin_pb "github.com/caos/zitadel/pkg/grpc/admin" +) + +func SetInitCustomTextToDomain(msg *admin_pb.SetDefaultInitMessageTextRequest) *domain.CustomMessageText { + langTag := language.Make(msg.Language) + return &domain.CustomMessageText{ + MessageTextType: domain.InitCodeMessageType, + Language: langTag, + Title: msg.Title, + PreHeader: msg.PreHeader, + Subject: msg.Subject, + Greeting: msg.Greeting, + Text: msg.Text, + ButtonText: msg.ButtonText, + FooterText: msg.FooterText, + } +} + +func SetPasswordResetCustomTextToDomain(msg *admin_pb.SetDefaultPasswordResetMessageTextRequest) *domain.CustomMessageText { + langTag := language.Make(msg.Language) + return &domain.CustomMessageText{ + MessageTextType: domain.PasswordResetMessageType, + Language: langTag, + Title: msg.Title, + PreHeader: msg.PreHeader, + Subject: msg.Subject, + Greeting: msg.Greeting, + Text: msg.Text, + ButtonText: msg.ButtonText, + FooterText: msg.FooterText, + } +} + +func SetVerifyEmailCustomTextToDomain(msg *admin_pb.SetDefaultVerifyEmailMessageTextRequest) *domain.CustomMessageText { + langTag := language.Make(msg.Language) + return &domain.CustomMessageText{ + MessageTextType: domain.VerifyEmailMessageType, + Language: langTag, + Title: msg.Title, + PreHeader: msg.PreHeader, + Subject: msg.Subject, + Greeting: msg.Greeting, + Text: msg.Text, + ButtonText: msg.ButtonText, + FooterText: msg.FooterText, + } +} + +func SetVerifyPhoneCustomTextToDomain(msg *admin_pb.SetDefaultVerifyPhoneMessageTextRequest) *domain.CustomMessageText { + langTag := language.Make(msg.Language) + return &domain.CustomMessageText{ + MessageTextType: domain.VerifyPhoneMessageType, + Language: langTag, + Title: msg.Title, + PreHeader: msg.PreHeader, + Subject: msg.Subject, + Greeting: msg.Greeting, + Text: msg.Text, + ButtonText: msg.ButtonText, + FooterText: msg.FooterText, + } +} + +func SetDomainClaimedCustomTextToDomain(msg *admin_pb.SetDefaultDomainClaimedMessageTextRequest) *domain.CustomMessageText { + langTag := language.Make(msg.Language) + return &domain.CustomMessageText{ + MessageTextType: domain.DomainClaimedMessageType, + Language: langTag, + Title: msg.Title, + PreHeader: msg.PreHeader, + Subject: msg.Subject, + Greeting: msg.Greeting, + Text: msg.Text, + ButtonText: msg.ButtonText, + FooterText: msg.FooterText, + } +} diff --git a/internal/api/grpc/admin/features.go b/internal/api/grpc/admin/features.go index 3847857cef..21443cdb93 100644 --- a/internal/api/grpc/admin/features.go +++ b/internal/api/grpc/admin/features.go @@ -74,6 +74,7 @@ func setDefaultFeaturesRequestToDomain(req *admin_pb.SetDefaultFeaturesRequest) LabelPolicyPrivateLabel: req.LabelPolicy || req.LabelPolicyPrivateLabel, LabelPolicyWatermark: req.LabelPolicyWatermark, CustomDomain: req.CustomDomain, + CustomText: req.CustomText, } } @@ -94,5 +95,6 @@ func setOrgFeaturesRequestToDomain(req *admin_pb.SetOrgFeaturesRequest) *domain. LabelPolicyPrivateLabel: req.LabelPolicy || req.LabelPolicyPrivateLabel, LabelPolicyWatermark: req.LabelPolicyWatermark, CustomDomain: req.CustomDomain, + CustomText: req.CustomText, } } diff --git a/internal/api/grpc/features/features.go b/internal/api/grpc/features/features.go index 51d1102385..b2b47f9956 100644 --- a/internal/api/grpc/features/features.go +++ b/internal/api/grpc/features/features.go @@ -27,6 +27,7 @@ func FeaturesFromModel(features *features_model.FeaturesView) *features_pb.Featu CustomDomain: features.CustomDomain, LabelPolicyPrivateLabel: features.LabelPolicyPrivateLabel, LabelPolicyWatermark: features.LabelPolicyWatermark, + CustomText: features.CustomText, } } diff --git a/internal/api/grpc/management/custom_text.go b/internal/api/grpc/management/custom_text.go new file mode 100644 index 0000000000..c109f6c23a --- /dev/null +++ b/internal/api/grpc/management/custom_text.go @@ -0,0 +1,131 @@ +package management + +import ( + "context" + + "github.com/caos/zitadel/internal/api/authz" + "github.com/caos/zitadel/internal/api/grpc/object" + text_grpc "github.com/caos/zitadel/internal/api/grpc/text" + "github.com/caos/zitadel/internal/domain" + mgmt_pb "github.com/caos/zitadel/pkg/grpc/management" +) + +func (s *Server) GetCustomInitMessageText(ctx context.Context, req *mgmt_pb.GetCustomInitMessageTextRequest) (*mgmt_pb.GetCustomInitMessageTextResponse, error) { + msg, err := s.org.GetMessageText(ctx, authz.GetCtxData(ctx).OrgID, domain.InitCodeMessageType, req.Language) + if err != nil { + return nil, err + } + return &mgmt_pb.GetCustomInitMessageTextResponse{ + CustomText: text_grpc.ModelCustomMsgTextToPb(msg), + }, nil +} + +func (s *Server) SetCustomInitMessageText(ctx context.Context, req *mgmt_pb.SetCustomInitMessageTextRequest) (*mgmt_pb.SetCustomInitMessageTextResponse, error) { + result, err := s.command.SetOrgMessageText(ctx, authz.GetCtxData(ctx).OrgID, SetInitCustomTextToDomain(req)) + if err != nil { + return nil, err + } + return &mgmt_pb.SetCustomInitMessageTextResponse{ + Details: object.ChangeToDetailsPb( + result.Sequence, + result.EventDate, + result.ResourceOwner, + ), + }, nil +} + +func (s *Server) GetCustomPasswordResetMessageText(ctx context.Context, req *mgmt_pb.GetCustomPasswordResetMessageTextRequest) (*mgmt_pb.GetCustomPasswordResetMessageTextResponse, error) { + msg, err := s.org.GetMessageText(ctx, authz.GetCtxData(ctx).OrgID, domain.PasswordResetMessageType, req.Language) + if err != nil { + return nil, err + } + return &mgmt_pb.GetCustomPasswordResetMessageTextResponse{ + CustomText: text_grpc.ModelCustomMsgTextToPb(msg), + }, nil +} + +func (s *Server) SetCustomPasswordResetMessageText(ctx context.Context, req *mgmt_pb.SetCustomPasswordResetMessageTextRequest) (*mgmt_pb.SetCustomPasswordResetMessageTextResponse, error) { + result, err := s.command.SetOrgMessageText(ctx, authz.GetCtxData(ctx).OrgID, SetPasswordResetCustomTextToDomain(req)) + if err != nil { + return nil, err + } + return &mgmt_pb.SetCustomPasswordResetMessageTextResponse{ + Details: object.ChangeToDetailsPb( + result.Sequence, + result.EventDate, + result.ResourceOwner, + ), + }, nil +} + +func (s *Server) GetCustomVerifyEmailMessageText(ctx context.Context, req *mgmt_pb.GetCustomVerifyEmailMessageTextRequest) (*mgmt_pb.GetCustomVerifyEmailMessageTextResponse, error) { + msg, err := s.org.GetMessageText(ctx, authz.GetCtxData(ctx).OrgID, domain.VerifyEmailMessageType, req.Language) + if err != nil { + return nil, err + } + return &mgmt_pb.GetCustomVerifyEmailMessageTextResponse{ + CustomText: text_grpc.ModelCustomMsgTextToPb(msg), + }, nil +} + +func (s *Server) SetCustomVerifyEmailMessageText(ctx context.Context, req *mgmt_pb.SetCustomVerifyEmailMessageTextRequest) (*mgmt_pb.SetCustomVerifyEmailMessageTextResponse, error) { + result, err := s.command.SetOrgMessageText(ctx, authz.GetCtxData(ctx).OrgID, SetVerifyEmailCustomTextToDomain(req)) + if err != nil { + return nil, err + } + return &mgmt_pb.SetCustomVerifyEmailMessageTextResponse{ + Details: object.ChangeToDetailsPb( + result.Sequence, + result.EventDate, + result.ResourceOwner, + ), + }, nil +} + +func (s *Server) GetCustomVerifyPhoneMessageText(ctx context.Context, req *mgmt_pb.GetCustomVerifyPhoneMessageTextRequest) (*mgmt_pb.GetCustomVerifyPhoneMessageTextResponse, error) { + msg, err := s.org.GetMessageText(ctx, authz.GetCtxData(ctx).OrgID, domain.VerifyPhoneMessageType, req.Language) + if err != nil { + return nil, err + } + return &mgmt_pb.GetCustomVerifyPhoneMessageTextResponse{ + CustomText: text_grpc.ModelCustomMsgTextToPb(msg), + }, nil +} + +func (s *Server) SetCustomVerifyPhoneMessageText(ctx context.Context, req *mgmt_pb.SetCustomVerifyPhoneMessageTextRequest) (*mgmt_pb.SetCustomVerifyPhoneMessageTextResponse, error) { + result, err := s.command.SetOrgMessageText(ctx, authz.GetCtxData(ctx).OrgID, SetVerifyPhoneCustomTextToDomain(req)) + if err != nil { + return nil, err + } + return &mgmt_pb.SetCustomVerifyPhoneMessageTextResponse{ + Details: object.ChangeToDetailsPb( + result.Sequence, + result.EventDate, + result.ResourceOwner, + ), + }, nil +} + +func (s *Server) GetCustomDomainClaimedMessageText(ctx context.Context, req *mgmt_pb.GetCustomDomainClaimedMessageTextRequest) (*mgmt_pb.GetCustomDomainClaimedMessageTextResponse, error) { + msg, err := s.org.GetMessageText(ctx, authz.GetCtxData(ctx).OrgID, domain.DomainClaimedMessageType, req.Language) + if err != nil { + return nil, err + } + return &mgmt_pb.GetCustomDomainClaimedMessageTextResponse{ + CustomText: text_grpc.ModelCustomMsgTextToPb(msg), + }, nil +} + +func (s *Server) SetCustomDomainClaimedMessageText(ctx context.Context, req *mgmt_pb.SetCustomDomainClaimedMessageTextRequest) (*mgmt_pb.SetCustomDomainClaimedMessageTextResponse, error) { + result, err := s.command.SetOrgMessageText(ctx, authz.GetCtxData(ctx).OrgID, SetDomainClaimedCustomTextToDomain(req)) + if err != nil { + return nil, err + } + return &mgmt_pb.SetCustomDomainClaimedMessageTextResponse{ + Details: object.ChangeToDetailsPb( + result.Sequence, + result.EventDate, + result.ResourceOwner, + ), + }, nil +} diff --git a/internal/api/grpc/management/custom_text_converter.go b/internal/api/grpc/management/custom_text_converter.go new file mode 100644 index 0000000000..1a3d09c35b --- /dev/null +++ b/internal/api/grpc/management/custom_text_converter.go @@ -0,0 +1,83 @@ +package management + +import ( + "golang.org/x/text/language" + + "github.com/caos/zitadel/internal/domain" + mgmt_pb "github.com/caos/zitadel/pkg/grpc/management" +) + +func SetInitCustomTextToDomain(msg *mgmt_pb.SetCustomInitMessageTextRequest) *domain.CustomMessageText { + langTag := language.Make(msg.Language) + return &domain.CustomMessageText{ + MessageTextType: domain.InitCodeMessageType, + Language: langTag, + Title: msg.Title, + PreHeader: msg.PreHeader, + Subject: msg.Subject, + Greeting: msg.Greeting, + Text: msg.Text, + ButtonText: msg.ButtonText, + FooterText: msg.FooterText, + } +} + +func SetPasswordResetCustomTextToDomain(msg *mgmt_pb.SetCustomPasswordResetMessageTextRequest) *domain.CustomMessageText { + langTag := language.Make(msg.Language) + return &domain.CustomMessageText{ + MessageTextType: domain.PasswordResetMessageType, + Language: langTag, + Title: msg.Title, + PreHeader: msg.PreHeader, + Subject: msg.Subject, + Greeting: msg.Greeting, + Text: msg.Text, + ButtonText: msg.ButtonText, + FooterText: msg.FooterText, + } +} + +func SetVerifyEmailCustomTextToDomain(msg *mgmt_pb.SetCustomVerifyEmailMessageTextRequest) *domain.CustomMessageText { + langTag := language.Make(msg.Language) + return &domain.CustomMessageText{ + MessageTextType: domain.VerifyEmailMessageType, + Language: langTag, + Title: msg.Title, + PreHeader: msg.PreHeader, + Subject: msg.Subject, + Greeting: msg.Greeting, + Text: msg.Text, + ButtonText: msg.ButtonText, + FooterText: msg.FooterText, + } +} + +func SetVerifyPhoneCustomTextToDomain(msg *mgmt_pb.SetCustomVerifyPhoneMessageTextRequest) *domain.CustomMessageText { + langTag := language.Make(msg.Language) + return &domain.CustomMessageText{ + MessageTextType: domain.VerifyPhoneMessageType, + Language: langTag, + Title: msg.Title, + PreHeader: msg.PreHeader, + Subject: msg.Subject, + Greeting: msg.Greeting, + Text: msg.Text, + ButtonText: msg.ButtonText, + FooterText: msg.FooterText, + } +} + +func SetDomainClaimedCustomTextToDomain(msg *mgmt_pb.SetCustomDomainClaimedMessageTextRequest) *domain.CustomMessageText { + langTag := language.Make(msg.Language) + return &domain.CustomMessageText{ + MessageTextType: domain.DomainClaimedMessageType, + Language: langTag, + Title: msg.Title, + PreHeader: msg.PreHeader, + Subject: msg.Subject, + Greeting: msg.Greeting, + Text: msg.Text, + ButtonText: msg.ButtonText, + FooterText: msg.FooterText, + } +} diff --git a/internal/api/grpc/text/custom_text.go b/internal/api/grpc/text/custom_text.go new file mode 100644 index 0000000000..aea81e56b5 --- /dev/null +++ b/internal/api/grpc/text/custom_text.go @@ -0,0 +1,25 @@ +package text + +import ( + "github.com/caos/zitadel/internal/api/grpc/object" + "github.com/caos/zitadel/internal/iam/model" + text_pb "github.com/caos/zitadel/pkg/grpc/text" +) + +func ModelCustomMsgTextToPb(msg *model.MessageTextView) *text_pb.MessageCustomText { + return &text_pb.MessageCustomText{ + Title: msg.Title, + PreHeader: msg.PreHeader, + Subject: msg.Subject, + Greeting: msg.Greeting, + Text: msg.Text, + ButtonText: msg.ButtonText, + FooterText: msg.FooterText, + Details: object.ToViewDetailsPb( + msg.Sequence, + msg.CreationDate, + msg.ChangeDate, + "", //TODO: resourceowner + ), + } +} diff --git a/internal/auth/repository/eventsourcing/eventstore/auth_request.go b/internal/auth/repository/eventsourcing/eventstore/auth_request.go index c64a9a6aa7..c6ccd12ff5 100644 --- a/internal/auth/repository/eventsourcing/eventstore/auth_request.go +++ b/internal/auth/repository/eventsourcing/eventstore/auth_request.go @@ -465,7 +465,9 @@ func (repo *AuthRequestRepo) checkLoginName(ctx context.Context, request *domain if err != nil { return err } - + if user.State == int32(domain.UserStateInactive) { + return errors.ThrowPreconditionFailed(nil, "AUTH-2n8fs", "Errors.User.Inactive") + } request.SetUserInfo(user.ID, loginName, user.PreferredLoginName, "", "", user.ResourceOwner) return nil } diff --git a/internal/authz/repository/eventsourcing/eventstore/token_verifier.go b/internal/authz/repository/eventsourcing/eventstore/token_verifier.go index c0ebde5d95..862e298678 100644 --- a/internal/authz/repository/eventsourcing/eventstore/token_verifier.go +++ b/internal/authz/repository/eventsourcing/eventstore/token_verifier.go @@ -145,6 +145,18 @@ func checkFeatures(features *features_view_model.FeaturesView, requiredFeatures } continue } + if requiredFeature == domain.FeatureCustomDomain { + if !features.CustomDomain { + return MissingFeatureErr(requiredFeature) + } + continue + } + if requiredFeature == domain.FeatureCustomText { + if !features.CustomText { + return MissingFeatureErr(requiredFeature) + } + continue + } return MissingFeatureErr(requiredFeature) } return nil diff --git a/internal/command/custom_message_text_model.go b/internal/command/custom_message_text_model.go new file mode 100644 index 0000000000..225b4fbf0d --- /dev/null +++ b/internal/command/custom_message_text_model.go @@ -0,0 +1,167 @@ +package command + +import ( + "golang.org/x/text/language" + + "github.com/caos/zitadel/internal/domain" + "github.com/caos/zitadel/internal/eventstore" + "github.com/caos/zitadel/internal/repository/policy" +) + +type CustomMessageTextReadModel struct { + eventstore.WriteModel + + MessageTextType string + Language language.Tag + Title string + PreHeader string + Subject string + Greeting string + Text string + ButtonText string + FooterText string + + State domain.PolicyState +} + +func (wm *CustomMessageTextReadModel) Reduce() error { + for _, event := range wm.Events { + switch e := event.(type) { + case *policy.CustomTextSetEvent: + if e.Template != wm.MessageTextType || wm.Language != e.Language { + continue + } + if e.Key == domain.MessageSubject { + wm.Subject = e.Text + } + if e.Key == domain.MessageTitle { + wm.Title = e.Text + } + if e.Key == domain.MessagePreHeader { + wm.PreHeader = e.Text + } + if e.Key == domain.MessageText { + wm.Text = e.Text + } + if e.Key == domain.MessageGreeting { + wm.Greeting = e.Text + } + if e.Key == domain.MessageButtonText { + wm.ButtonText = e.Text + } + if e.Key == domain.MessageFooterText { + wm.FooterText = e.Text + } + wm.State = domain.PolicyStateActive + case *policy.CustomTextRemovedEvent: + if e.Key != wm.MessageTextType || wm.Language != e.Language { + continue + } + if e.Key == domain.MessageSubject { + wm.Subject = "" + } + if e.Key == domain.MessageTitle { + wm.Title = "" + } + if e.Key == domain.MessagePreHeader { + wm.PreHeader = "" + } + if e.Key == domain.MessageText { + wm.Text = "" + } + if e.Key == domain.MessageGreeting { + wm.Greeting = "" + } + if e.Key == domain.MessageButtonText { + wm.ButtonText = "" + } + if e.Key == domain.MessageFooterText { + wm.FooterText = "" + } + case *policy.CustomTextTemplateRemovedEvent: + wm.State = domain.PolicyStateRemoved + } + } + return wm.WriteModel.Reduce() +} + +type CustomMessageTemplatesReadModel struct { + eventstore.WriteModel + CustomMessageTemplate map[string]*CustomText +} + +type CustomText struct { + Template string + Language language.Tag + Title string + PreHeader string + Subject string + Greeting string + Text string + ButtonText string + FooterText string + State domain.PolicyState +} + +func (wm *CustomMessageTemplatesReadModel) Reduce() error { + for _, event := range wm.Events { + switch e := event.(type) { + case *policy.CustomTextSetEvent: + if _, ok := wm.CustomMessageTemplate[e.Template+e.Language.String()]; !ok { + wm.CustomMessageTemplate[e.Template+e.Language.String()] = &CustomText{Language: e.Language, Template: e.Template} + } + if e.Key == domain.MessageSubject { + wm.CustomMessageTemplate[e.Template+e.Language.String()].Subject = e.Text + } + if e.Key == domain.MessageTitle { + wm.CustomMessageTemplate[e.Template+e.Language.String()].Title = e.Text + } + if e.Key == domain.MessagePreHeader { + wm.CustomMessageTemplate[e.Template+e.Language.String()].PreHeader = e.Text + } + if e.Key == domain.MessageText { + wm.CustomMessageTemplate[e.Template+e.Language.String()].Text = e.Text + } + if e.Key == domain.MessageGreeting { + wm.CustomMessageTemplate[e.Template+e.Language.String()].Greeting = e.Text + } + if e.Key == domain.MessageButtonText { + wm.CustomMessageTemplate[e.Template+e.Language.String()].ButtonText = e.Text + } + if e.Key == domain.MessageFooterText { + wm.CustomMessageTemplate[e.Template+e.Language.String()].FooterText = e.Text + } + wm.CustomMessageTemplate[e.Template+e.Language.String()].State = domain.PolicyStateActive + case *policy.CustomTextRemovedEvent: + if _, ok := wm.CustomMessageTemplate[e.Template+e.Language.String()]; !ok { + wm.CustomMessageTemplate[e.Template+e.Language.String()] = new(CustomText) + } + if e.Key == domain.MessageSubject { + wm.CustomMessageTemplate[e.Template+e.Language.String()].Subject = "" + } + if e.Key == domain.MessageTitle { + wm.CustomMessageTemplate[e.Template+e.Language.String()].Title = "" + } + if e.Key == domain.MessagePreHeader { + wm.CustomMessageTemplate[e.Template+e.Language.String()].PreHeader = "" + } + if e.Key == domain.MessageText { + wm.CustomMessageTemplate[e.Template+e.Language.String()].Text = "" + } + if e.Key == domain.MessageGreeting { + wm.CustomMessageTemplate[e.Template+e.Language.String()].Greeting = "" + } + if e.Key == domain.MessageButtonText { + wm.CustomMessageTemplate[e.Template+e.Language.String()].ButtonText = "" + } + if e.Key == domain.MessageFooterText { + wm.CustomMessageTemplate[e.Template+e.Language.String()].FooterText = "" + } + case *policy.CustomTextTemplateRemovedEvent: + if _, ok := wm.CustomMessageTemplate[e.Template+e.Language.String()]; ok { + delete(wm.CustomMessageTemplate, e.Template) + } + } + } + return wm.WriteModel.Reduce() +} diff --git a/internal/command/custom_text_model.go b/internal/command/custom_text_model.go new file mode 100644 index 0000000000..152e330977 --- /dev/null +++ b/internal/command/custom_text_model.go @@ -0,0 +1,34 @@ +package command + +import ( + "golang.org/x/text/language" + + "github.com/caos/zitadel/internal/domain" + "github.com/caos/zitadel/internal/eventstore" + "github.com/caos/zitadel/internal/repository/policy" +) + +type CustomTextWriteModel struct { + eventstore.WriteModel + + Key string + Language language.Tag + Text string + State domain.CustomTextState +} + +func (wm *CustomTextWriteModel) Reduce() error { + for _, event := range wm.Events { + switch e := event.(type) { + case *policy.CustomTextSetEvent: + if wm.Key != e.Key || wm.Language != e.Language { + continue + } + wm.Text = e.Text + wm.State = domain.CustomTextStateActive + case *policy.CustomTextRemovedEvent: + wm.State = domain.CustomTextStateRemoved + } + } + return wm.WriteModel.Reduce() +} diff --git a/internal/command/features_model.go b/internal/command/features_model.go index ba4276ad31..62567a6f2e 100644 --- a/internal/command/features_model.go +++ b/internal/command/features_model.go @@ -26,6 +26,7 @@ type FeaturesWriteModel struct { LabelPolicyPrivateLabel bool LabelPolicyWatermark bool CustomDomain bool + CustomText bool } func (wm *FeaturesWriteModel) Reduce() error { @@ -81,6 +82,9 @@ func (wm *FeaturesWriteModel) Reduce() error { if e.CustomDomain != nil { wm.CustomDomain = *e.CustomDomain } + if e.CustomText != nil { + wm.CustomText = *e.CustomText + } case *features.FeaturesRemovedEvent: wm.State = domain.FeaturesStateRemoved } diff --git a/internal/command/iam_converter.go b/internal/command/iam_converter.go index e290260772..f1fcf3111a 100644 --- a/internal/command/iam_converter.go +++ b/internal/command/iam_converter.go @@ -69,21 +69,6 @@ func writeModelToMailTemplate(wm *MailTemplateWriteModel) *domain.MailTemplate { } } -func writeModelToMailText(wm *MailTextWriteModel) *domain.MailText { - return &domain.MailText{ - ObjectRoot: writeModelToObjectRoot(wm.WriteModel), - MailTextType: wm.MailTextType, - Language: wm.Language, - Title: wm.Title, - PreHeader: wm.PreHeader, - Subject: wm.Subject, - Greeting: wm.Greeting, - Text: wm.Text, - ButtonText: wm.ButtonText, - State: wm.State, - } -} - func writeModelToOrgIAMPolicy(wm *IAMOrgIAMPolicyWriteModel) *domain.OrgIAMPolicy { return &domain.OrgIAMPolicy{ ObjectRoot: writeModelToObjectRoot(wm.PolicyOrgIAMWriteModel.WriteModel), @@ -98,18 +83,13 @@ func writeModelToMailTemplatePolicy(wm *MailTemplateWriteModel) *domain.MailTemp } } -func writeModelToMailTextPolicy(wm *MailTextWriteModel) *domain.MailText { - return &domain.MailText{ - ObjectRoot: writeModelToObjectRoot(wm.WriteModel), - State: wm.State, - MailTextType: wm.MailTextType, - Language: wm.Language, - Title: wm.Title, - PreHeader: wm.PreHeader, - Subject: wm.Subject, - Greeting: wm.Greeting, - Text: wm.Text, - ButtonText: wm.ButtonText, +func writeModelToCustomText(wm *CustomTextWriteModel) *domain.CustomText { + return &domain.CustomText{ + ObjectRoot: writeModelToObjectRoot(wm.WriteModel), + State: wm.State, + Key: wm.Key, + Language: wm.Language, + Text: wm.Text, } } diff --git a/internal/command/iam_custom_message_text.go b/internal/command/iam_custom_message_text.go new file mode 100644 index 0000000000..96be6c8af9 --- /dev/null +++ b/internal/command/iam_custom_message_text.go @@ -0,0 +1,72 @@ +package command + +import ( + "context" + + "golang.org/x/text/language" + + "github.com/caos/zitadel/internal/domain" + caos_errs "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/eventstore" + "github.com/caos/zitadel/internal/repository/iam" +) + +func (c *Commands) SetDefaultMessageText(ctx context.Context, messageText *domain.CustomMessageText) (*domain.ObjectDetails, error) { + iamAgg := iam.NewAggregate() + events, existingMessageText, err := c.setDefaultMessageText(ctx, &iamAgg.Aggregate, messageText) + if err != nil { + return nil, err + } + pushedEvents, err := c.eventstore.PushEvents(ctx, events...) + if err != nil { + return nil, err + } + err = AppendAndReduce(existingMessageText, pushedEvents...) + if err != nil { + return nil, err + } + return writeModelToObjectDetails(&existingMessageText.WriteModel), nil +} + +func (c *Commands) setDefaultMessageText(ctx context.Context, iamAgg *eventstore.Aggregate, msg *domain.CustomMessageText) ([]eventstore.EventPusher, *IAMCustomMessageTextReadModel, error) { + if !msg.IsValid() { + return nil, nil, caos_errs.ThrowInvalidArgument(nil, "IAM-kd9fs", "Errors.CustomMessageText.Invalid") + } + + existingMessageText, err := c.defaultCustomMessageTextWriteModelByID(ctx, msg.MessageTextType, msg.Language) + if err != nil { + return nil, nil, err + } + events := make([]eventstore.EventPusher, 0) + if existingMessageText.Greeting != msg.Greeting { + events = append(events, iam.NewCustomTextSetEvent(ctx, iamAgg, msg.MessageTextType, domain.MessageGreeting, msg.Greeting, msg.Language)) + } + if existingMessageText.Subject != msg.Subject { + events = append(events, iam.NewCustomTextSetEvent(ctx, iamAgg, msg.MessageTextType, domain.MessageSubject, msg.Subject, msg.Language)) + } + if existingMessageText.Title != msg.Title { + events = append(events, iam.NewCustomTextSetEvent(ctx, iamAgg, msg.MessageTextType, domain.MessageTitle, msg.Title, msg.Language)) + } + if existingMessageText.PreHeader != msg.PreHeader { + events = append(events, iam.NewCustomTextSetEvent(ctx, iamAgg, msg.MessageTextType, domain.MessagePreHeader, msg.PreHeader, msg.Language)) + } + if existingMessageText.Text != msg.Text { + events = append(events, iam.NewCustomTextSetEvent(ctx, iamAgg, msg.MessageTextType, domain.MessageText, msg.Text, msg.Language)) + } + if existingMessageText.ButtonText != msg.ButtonText { + events = append(events, iam.NewCustomTextSetEvent(ctx, iamAgg, msg.MessageTextType, domain.MessageButtonText, msg.ButtonText, msg.Language)) + } + if existingMessageText.FooterText != msg.FooterText { + events = append(events, iam.NewCustomTextSetEvent(ctx, iamAgg, msg.MessageTextType, domain.MessageFooterText, msg.FooterText, msg.Language)) + } + return events, existingMessageText, nil +} + +func (c *Commands) defaultCustomMessageTextWriteModelByID(ctx context.Context, messageType string, lang language.Tag) (*IAMCustomMessageTextReadModel, error) { + writeModel := NewIAMCustomMessageTextWriteModel(messageType, lang) + err := c.eventstore.FilterToQueryReducer(ctx, writeModel) + if err != nil { + return nil, err + } + return writeModel, nil +} diff --git a/internal/command/iam_custom_message_text_model.go b/internal/command/iam_custom_message_text_model.go new file mode 100644 index 0000000000..dce661e3ce --- /dev/null +++ b/internal/command/iam_custom_message_text_model.go @@ -0,0 +1,47 @@ +package command + +import ( + "golang.org/x/text/language" + + "github.com/caos/zitadel/internal/domain" + "github.com/caos/zitadel/internal/eventstore" + "github.com/caos/zitadel/internal/repository/iam" +) + +type IAMCustomMessageTextReadModel struct { + CustomMessageTextReadModel +} + +func NewIAMCustomMessageTextWriteModel(messageTextType string, lang language.Tag) *IAMCustomMessageTextReadModel { + return &IAMCustomMessageTextReadModel{ + CustomMessageTextReadModel{ + WriteModel: eventstore.WriteModel{ + AggregateID: domain.IAMID, + ResourceOwner: domain.IAMID, + }, + MessageTextType: messageTextType, + Language: lang, + }, + } +} + +func (wm *IAMCustomMessageTextReadModel) AppendEvents(events ...eventstore.EventReader) { + for _, event := range events { + switch e := event.(type) { + case *iam.CustomTextSetEvent: + wm.CustomMessageTextReadModel.AppendEvents(&e.CustomTextSetEvent) + } + } +} + +func (wm *IAMCustomMessageTextReadModel) Reduce() error { + return wm.CustomMessageTextReadModel.Reduce() +} + +func (wm *IAMCustomMessageTextReadModel) Query() *eventstore.SearchQueryBuilder { + return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent, iam.AggregateType). + AggregateIDs(wm.CustomMessageTextReadModel.AggregateID). + ResourceOwner(wm.ResourceOwner). + EventTypes( + iam.CustomTextSetEventType) +} diff --git a/internal/command/iam_custom_message_text_test.go b/internal/command/iam_custom_message_text_test.go new file mode 100644 index 0000000000..910e6398b3 --- /dev/null +++ b/internal/command/iam_custom_message_text_test.go @@ -0,0 +1,163 @@ +package command + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "golang.org/x/text/language" + + "github.com/caos/zitadel/internal/domain" + caos_errs "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/eventstore" + "github.com/caos/zitadel/internal/eventstore/repository" + "github.com/caos/zitadel/internal/repository/iam" +) + +func TestCommandSide_SetDefaultMessageText(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + config *domain.CustomMessageText + } + type res struct { + want *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "invalid custom text, error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + config: &domain.CustomMessageText{}, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "custom text set all fields, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + expectPush( + []*repository.Event{ + eventFromEventPusher( + iam.NewCustomTextSetEvent(context.Background(), + &iam.NewAggregate().Aggregate, + "Template", + domain.MessageGreeting, + "Greeting", + language.English, + ), + ), + eventFromEventPusher( + iam.NewCustomTextSetEvent(context.Background(), + &iam.NewAggregate().Aggregate, + "Template", + domain.MessageSubject, + "Subject", + language.English, + ), + ), + eventFromEventPusher( + iam.NewCustomTextSetEvent(context.Background(), + &iam.NewAggregate().Aggregate, + "Template", + domain.MessageTitle, + "Title", + language.English, + ), + ), + eventFromEventPusher( + iam.NewCustomTextSetEvent(context.Background(), + &iam.NewAggregate().Aggregate, + "Template", + domain.MessagePreHeader, + "PreHeader", + language.English, + ), + ), + eventFromEventPusher( + iam.NewCustomTextSetEvent(context.Background(), + &iam.NewAggregate().Aggregate, + "Template", + domain.MessageText, + "Text", + language.English, + ), + ), + eventFromEventPusher( + iam.NewCustomTextSetEvent(context.Background(), + &iam.NewAggregate().Aggregate, + "Template", + domain.MessageButtonText, + "ButtonText", + language.English, + ), + ), + eventFromEventPusher( + iam.NewCustomTextSetEvent(context.Background(), + &iam.NewAggregate().Aggregate, + "Template", + domain.MessageFooterText, + "FooterText", + language.English, + ), + ), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + config: &domain.CustomMessageText{ + MessageTextType: "Template", + Language: language.English, + Greeting: "Greeting", + Subject: "Subject", + Title: "Title", + PreHeader: "PreHeader", + Text: "Text", + ButtonText: "ButtonText", + FooterText: "FooterText", + }, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "IAM", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + got, err := r.SetDefaultMessageText(tt.args.ctx, tt.args.config) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assert.Equal(t, tt.res.want, got) + } + }) + } +} diff --git a/internal/command/iam_features.go b/internal/command/iam_features.go index a63ce39883..c24eebfa96 100644 --- a/internal/command/iam_features.go +++ b/internal/command/iam_features.go @@ -49,6 +49,7 @@ func (c *Commands) setDefaultFeatures(ctx context.Context, existingFeatures *IAM features.LabelPolicyPrivateLabel, features.LabelPolicyWatermark, features.CustomDomain, + features.CustomText, ) if !hasChanged { return nil, caos_errs.ThrowPreconditionFailed(nil, "Features-GE4h2", "Errors.Features.NotChanged") diff --git a/internal/command/iam_features_model.go b/internal/command/iam_features_model.go index 8d47bab357..9b1042b441 100644 --- a/internal/command/iam_features_model.go +++ b/internal/command/iam_features_model.go @@ -64,7 +64,8 @@ func (wm *IAMFeaturesWriteModel) NewSetEvent( passwordComplexityPolicy, labelPolicyPrivateLabel, labelPolicyWatermark, - customDomain bool, + customDomain, + customText bool, ) (*iam.FeaturesSetEvent, bool) { changes := make([]features.FeaturesChanges, 0) @@ -111,6 +112,9 @@ func (wm *IAMFeaturesWriteModel) NewSetEvent( if wm.CustomDomain != customDomain { changes = append(changes, features.ChangeCustomDomain(customDomain)) } + if wm.CustomText != customText { + changes = append(changes, features.ChangeCustomText(customText)) + } if len(changes) == 0 { return nil, false diff --git a/internal/command/iam_policy_mail_text.go b/internal/command/iam_policy_mail_text.go deleted file mode 100644 index c65f7f21a9..0000000000 --- a/internal/command/iam_policy_mail_text.go +++ /dev/null @@ -1,106 +0,0 @@ -package command - -import ( - "context" - "github.com/caos/zitadel/internal/domain" - caos_errs "github.com/caos/zitadel/internal/errors" - "github.com/caos/zitadel/internal/eventstore" - iam_repo "github.com/caos/zitadel/internal/repository/iam" - "github.com/caos/zitadel/internal/telemetry/tracing" -) - -func (c *Commands) AddDefaultMailText(ctx context.Context, policy *domain.MailText) (*domain.MailText, error) { - addedPolicy := NewIAMMailTextWriteModel(policy.MailTextType, policy.Language) - iamAgg := IAMAggregateFromWriteModel(&addedPolicy.MailTextWriteModel.WriteModel) - event, err := c.addDefaultMailText(ctx, iamAgg, addedPolicy, policy) - if err != nil { - return nil, err - } - - pushedEvents, err := c.eventstore.PushEvents(ctx, event) - if err != nil { - return nil, err - } - err = AppendAndReduce(addedPolicy, pushedEvents...) - if err != nil { - return nil, err - } - return writeModelToMailTextPolicy(&addedPolicy.MailTextWriteModel), nil -} - -func (c *Commands) addDefaultMailText(ctx context.Context, iamAgg *eventstore.Aggregate, addedPolicy *IAMMailTextWriteModel, mailText *domain.MailText) (eventstore.EventPusher, error) { - if !mailText.IsValid() { - return nil, caos_errs.ThrowInvalidArgument(nil, "IAM-3n8fs", "Errors.IAM.MailText.Invalid") - } - err := c.eventstore.FilterToQueryReducer(ctx, addedPolicy) - if err != nil { - return nil, err - } - if addedPolicy.State == domain.PolicyStateActive { - return nil, caos_errs.ThrowAlreadyExists(nil, "IAM-9o0pM", "Errors.IAM.MailText.AlreadyExists") - } - - return iam_repo.NewMailTextAddedEvent( - ctx, - iamAgg, - mailText.MailTextType, - mailText.Language, - mailText.Title, - mailText.PreHeader, - mailText.Subject, - mailText.Greeting, - mailText.Text, - mailText.ButtonText), nil -} - -func (c *Commands) ChangeDefaultMailText(ctx context.Context, mailText *domain.MailText) (*domain.MailText, error) { - if !mailText.IsValid() { - return nil, caos_errs.ThrowInvalidArgument(nil, "IAM-kd9fs", "Errors.IAM.MailText.Invalid") - } - existingPolicy, err := c.defaultMailTextWriteModelByID(ctx, mailText.MailTextType, mailText.Language) - if err != nil { - return nil, err - } - - if existingPolicy.State == domain.PolicyStateUnspecified || existingPolicy.State == domain.PolicyStateRemoved { - return nil, caos_errs.ThrowNotFound(nil, "IAM-2N8fs", "Errors.IAM.MailText.NotFound") - } - - iamAgg := IAMAggregateFromWriteModel(&existingPolicy.MailTextWriteModel.WriteModel) - changedEvent, hasChanged := existingPolicy.NewChangedEvent( - ctx, - iamAgg, - mailText.MailTextType, - mailText.Language, - mailText.Title, - mailText.PreHeader, - mailText.Subject, - mailText.Greeting, - mailText.Text, - mailText.ButtonText) - if !hasChanged { - return nil, caos_errs.ThrowPreconditionFailed(nil, "IAM-m9L0s", "Errors.IAM.MailText.NotChanged") - } - - pushedEvents, err := c.eventstore.PushEvents(ctx, changedEvent) - if err != nil { - return nil, err - } - err = AppendAndReduce(existingPolicy, pushedEvents...) - if err != nil { - return nil, err - } - return writeModelToMailTextPolicy(&existingPolicy.MailTextWriteModel), nil -} - -func (c *Commands) defaultMailTextWriteModelByID(ctx context.Context, mailTextType, language string) (policy *IAMMailTextWriteModel, err error) { - ctx, span := tracing.NewSpan(ctx) - defer func() { span.EndWithError(err) }() - - writeModel := NewIAMMailTextWriteModel(mailTextType, language) - err = c.eventstore.FilterToQueryReducer(ctx, writeModel) - if err != nil { - return nil, err - } - return writeModel, nil -} diff --git a/internal/command/iam_policy_mail_text_model.go b/internal/command/iam_policy_mail_text_model.go deleted file mode 100644 index 6f18224517..0000000000 --- a/internal/command/iam_policy_mail_text_model.go +++ /dev/null @@ -1,91 +0,0 @@ -package command - -import ( - "context" - "github.com/caos/zitadel/internal/domain" - "github.com/caos/zitadel/internal/eventstore" - "github.com/caos/zitadel/internal/repository/iam" - "github.com/caos/zitadel/internal/repository/policy" -) - -type IAMMailTextWriteModel struct { - MailTextWriteModel -} - -func NewIAMMailTextWriteModel(mailTextType, language string) *IAMMailTextWriteModel { - return &IAMMailTextWriteModel{ - MailTextWriteModel{ - WriteModel: eventstore.WriteModel{ - AggregateID: domain.IAMID, - ResourceOwner: domain.IAMID, - }, - MailTextType: mailTextType, - Language: language, - }, - } -} - -func (wm *IAMMailTextWriteModel) AppendEvents(events ...eventstore.EventReader) { - for _, event := range events { - switch e := event.(type) { - case *iam.MailTextAddedEvent: - wm.MailTextWriteModel.AppendEvents(&e.MailTextAddedEvent) - case *iam.MailTextChangedEvent: - wm.MailTextWriteModel.AppendEvents(&e.MailTextChangedEvent) - } - } -} - -func (wm *IAMMailTextWriteModel) Reduce() error { - return wm.MailTextWriteModel.Reduce() -} - -func (wm *IAMMailTextWriteModel) Query() *eventstore.SearchQueryBuilder { - return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent, iam.AggregateType). - AggregateIDs(wm.MailTextWriteModel.AggregateID). - ResourceOwner(wm.ResourceOwner). - EventTypes( - iam.MailTextAddedEventType, - iam.MailTextChangedEventType) -} - -func (wm *IAMMailTextWriteModel) NewChangedEvent( - ctx context.Context, - aggregate *eventstore.Aggregate, - mailTextType, - language, - title, - preHeader, - subject, - greeting, - text, - buttonText string, -) (*iam.MailTextChangedEvent, bool) { - changes := make([]policy.MailTextChanges, 0) - if wm.Title != title { - changes = append(changes, policy.ChangeTitle(title)) - } - if wm.PreHeader != preHeader { - changes = append(changes, policy.ChangePreHeader(preHeader)) - } - if wm.Subject != subject { - changes = append(changes, policy.ChangeSubject(subject)) - } - if wm.Greeting != greeting { - changes = append(changes, policy.ChangeGreeting(greeting)) - } - if wm.Text != text { - changes = append(changes, policy.ChangeText(text)) - } - if wm.ButtonText != buttonText { - changes = append(changes, policy.ChangeButtonText(buttonText)) - } - if len(changes) == 0 { - return nil, false - } - changedEvent, err := iam.NewMailTextChangedEvent(ctx, aggregate, mailTextType, language, changes) - if err != nil { - return nil, false - } - return changedEvent, true -} diff --git a/internal/command/iam_policy_mail_text_test.go b/internal/command/iam_policy_mail_text_test.go deleted file mode 100644 index d63a1057f6..0000000000 --- a/internal/command/iam_policy_mail_text_test.go +++ /dev/null @@ -1,366 +0,0 @@ -package command - -import ( - "context" - "github.com/caos/zitadel/internal/domain" - caos_errs "github.com/caos/zitadel/internal/errors" - "github.com/caos/zitadel/internal/eventstore" - "github.com/caos/zitadel/internal/eventstore/repository" - "github.com/caos/zitadel/internal/eventstore/v1/models" - "github.com/caos/zitadel/internal/repository/iam" - "github.com/caos/zitadel/internal/repository/policy" - "github.com/stretchr/testify/assert" - "testing" -) - -func TestCommandSide_AddDefaultMailTextPolicy(t *testing.T) { - type fields struct { - eventstore *eventstore.Eventstore - } - type args struct { - ctx context.Context - policy *domain.MailText - } - type res struct { - want *domain.MailText - err func(error) bool - } - tests := []struct { - name string - fields fields - args args - res res - }{ - { - name: "mail text invalid, invalid argument error", - fields: fields{ - eventstore: eventstoreExpect( - t, - ), - }, - args: args{ - ctx: context.Background(), - policy: &domain.MailText{}, - }, - res: res{ - err: caos_errs.IsErrorInvalidArgument, - }, - }, - { - name: "mail text already existing, already exists error", - fields: fields{ - eventstore: eventstoreExpect( - t, - expectFilter( - eventFromEventPusher( - iam.NewMailTextAddedEvent(context.Background(), - &iam.NewAggregate().Aggregate, - "mail-text-type", - "de", - "title", - "pre-header", - "subject", - "greeting", - "text", - "button-text", - ), - ), - ), - ), - }, - args: args{ - ctx: context.Background(), - policy: &domain.MailText{ - MailTextType: "mail-text-type", - Language: "de", - Title: "title", - PreHeader: "pre-header", - Subject: "subject", - Greeting: "greeting", - Text: "text", - ButtonText: "button-text", - }, - }, - res: res{ - err: caos_errs.IsErrorAlreadyExists, - }, - }, - { - name: "add mail template,ok", - fields: fields{ - eventstore: eventstoreExpect( - t, - expectFilter(), - expectPush( - []*repository.Event{ - eventFromEventPusher( - iam.NewMailTextAddedEvent(context.Background(), - &iam.NewAggregate().Aggregate, - "mail-text-type", - "de", - "title", - "pre-header", - "subject", - "greeting", - "text", - "button-text", - ), - ), - }, - uniqueConstraintsFromEventConstraint(policy.NewAddMailTextUniqueConstraint("IAM", "mail-text-type", "de")), - ), - ), - }, - args: args{ - ctx: context.Background(), - policy: &domain.MailText{ - MailTextType: "mail-text-type", - Language: "de", - Title: "title", - PreHeader: "pre-header", - Subject: "subject", - Greeting: "greeting", - Text: "text", - ButtonText: "button-text", - }, - }, - res: res{ - want: &domain.MailText{ - ObjectRoot: models.ObjectRoot{ - AggregateID: "IAM", - ResourceOwner: "IAM", - }, - MailTextType: "mail-text-type", - Language: "de", - Title: "title", - PreHeader: "pre-header", - Subject: "subject", - Greeting: "greeting", - Text: "text", - ButtonText: "button-text", - State: domain.PolicyStateActive, - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - r := &Commands{ - eventstore: tt.fields.eventstore, - } - got, err := r.AddDefaultMailText(tt.args.ctx, tt.args.policy) - if tt.res.err == nil { - assert.NoError(t, err) - } - if tt.res.err != nil && !tt.res.err(err) { - t.Errorf("got wrong err: %v ", err) - } - if tt.res.err == nil { - assert.Equal(t, tt.res.want, got) - } - }) - } -} - -func TestCommandSide_ChangeDefaultMailTextPolicy(t *testing.T) { - type fields struct { - eventstore *eventstore.Eventstore - } - type args struct { - ctx context.Context - policy *domain.MailText - } - type res struct { - want *domain.MailText - err func(error) bool - } - tests := []struct { - name string - fields fields - args args - res res - }{ - { - name: "mailtext invalid, invalid argument error", - fields: fields{ - eventstore: eventstoreExpect( - t, - ), - }, - args: args{ - ctx: context.Background(), - policy: &domain.MailText{}, - }, - res: res{ - err: caos_errs.IsErrorInvalidArgument, - }, - }, - { - name: "mail text not existing, not found error", - fields: fields{ - eventstore: eventstoreExpect( - t, - expectFilter(), - ), - }, - args: args{ - ctx: context.Background(), - policy: &domain.MailText{ - MailTextType: "mail-text-type", - Language: "de", - Title: "title", - PreHeader: "pre-header", - Subject: "subject", - Greeting: "greeting", - Text: "text", - ButtonText: "button-text", - }, - }, - res: res{ - err: caos_errs.IsNotFound, - }, - }, - { - name: "no changes, precondition error", - fields: fields{ - eventstore: eventstoreExpect( - t, - expectFilter( - eventFromEventPusher( - iam.NewMailTextAddedEvent(context.Background(), - &iam.NewAggregate().Aggregate, - "mail-text-type", - "de", - "title", - "pre-header", - "subject", - "greeting", - "text", - "button-text", - ), - ), - ), - ), - }, - args: args{ - ctx: context.Background(), - policy: &domain.MailText{ - MailTextType: "mail-text-type", - Language: "de", - Title: "title", - PreHeader: "pre-header", - Subject: "subject", - Greeting: "greeting", - Text: "text", - ButtonText: "button-text", - }, - }, - res: res{ - err: caos_errs.IsPreconditionFailed, - }, - }, - { - name: "change, ok", - fields: fields{ - eventstore: eventstoreExpect( - t, - expectFilter( - eventFromEventPusher( - iam.NewMailTextAddedEvent(context.Background(), - &iam.NewAggregate().Aggregate, - "mail-text-type", - "de", - "title", - "pre-header", - "subject", - "greeting", - "text", - "button-text", - ), - ), - ), - expectPush( - []*repository.Event{ - eventFromEventPusher( - newDefaultMailTextPolicyChangedEvent( - context.Background(), - "mail-text-type", - "de", - "title-change", - "pre-header-change", - "subject-change", - "greeting-change", - "text-change", - "button-text-change"), - ), - }, - ), - ), - }, - args: args{ - ctx: context.Background(), - policy: &domain.MailText{ - MailTextType: "mail-text-type", - Language: "de", - Title: "title-change", - PreHeader: "pre-header-change", - Subject: "subject-change", - Greeting: "greeting-change", - Text: "text-change", - ButtonText: "button-text-change", - }, - }, - res: res{ - want: &domain.MailText{ - ObjectRoot: models.ObjectRoot{ - AggregateID: "IAM", - ResourceOwner: "IAM", - }, - MailTextType: "mail-text-type", - Language: "de", - Title: "title-change", - PreHeader: "pre-header-change", - Subject: "subject-change", - Greeting: "greeting-change", - Text: "text-change", - ButtonText: "button-text-change", - State: domain.PolicyStateActive, - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - r := &Commands{ - eventstore: tt.fields.eventstore, - } - got, err := r.ChangeDefaultMailText(tt.args.ctx, tt.args.policy) - if tt.res.err == nil { - assert.NoError(t, err) - } - if tt.res.err != nil && !tt.res.err(err) { - t.Errorf("got wrong err: %v ", err) - } - if tt.res.err == nil { - assert.Equal(t, tt.res.want, got) - } - }) - } -} - -func newDefaultMailTextPolicyChangedEvent(ctx context.Context, mailTextType, language, title, preHeader, subject, greeting, text, buttonText string) *iam.MailTextChangedEvent { - event, _ := iam.NewMailTextChangedEvent(ctx, - &iam.NewAggregate().Aggregate, - mailTextType, - language, - []policy.MailTextChanges{ - policy.ChangeTitle(title), - policy.ChangePreHeader(preHeader), - policy.ChangeSubject(subject), - policy.ChangeGreeting(greeting), - policy.ChangeText(text), - policy.ChangeButtonText(buttonText), - }, - ) - return event -} diff --git a/internal/command/org_custom_message_model.go b/internal/command/org_custom_message_model.go new file mode 100644 index 0000000000..2001c0a1a2 --- /dev/null +++ b/internal/command/org_custom_message_model.go @@ -0,0 +1,95 @@ +package command + +import ( + "golang.org/x/text/language" + + "github.com/caos/zitadel/internal/eventstore" + "github.com/caos/zitadel/internal/repository/org" +) + +type OrgCustomMessageTextReadModel struct { + CustomMessageTextReadModel +} + +func NewOrgCustomMessageTextWriteModel(orgID, messageTextType string, lang language.Tag) *OrgCustomMessageTextReadModel { + return &OrgCustomMessageTextReadModel{ + CustomMessageTextReadModel{ + WriteModel: eventstore.WriteModel{ + AggregateID: orgID, + ResourceOwner: orgID, + }, + MessageTextType: messageTextType, + Language: lang, + }, + } +} + +func (wm *OrgCustomMessageTextReadModel) AppendEvents(events ...eventstore.EventReader) { + for _, event := range events { + switch e := event.(type) { + case *org.CustomTextSetEvent: + wm.CustomMessageTextReadModel.AppendEvents(&e.CustomTextSetEvent) + case *org.CustomTextRemovedEvent: + wm.CustomMessageTextReadModel.AppendEvents(&e.CustomTextRemovedEvent) + case *org.CustomTextTemplateRemovedEvent: + wm.CustomMessageTextReadModel.AppendEvents(&e.CustomTextTemplateRemovedEvent) + } + } +} + +func (wm *OrgCustomMessageTextReadModel) Reduce() error { + return wm.CustomMessageTextReadModel.Reduce() +} + +func (wm *OrgCustomMessageTextReadModel) Query() *eventstore.SearchQueryBuilder { + return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent, org.AggregateType). + AggregateIDs(wm.CustomMessageTextReadModel.AggregateID). + ResourceOwner(wm.ResourceOwner). + EventTypes( + org.CustomTextSetEventType, + org.CustomTextRemovedEventType, + org.CustomTextTemplateRemovedEventType) +} + +type OrgCustomMessageTemplatesReadModel struct { + CustomMessageTemplatesReadModel +} + +func NewOrgCustomMessageTextsWriteModel(orgID string) *OrgCustomMessageTemplatesReadModel { + return &OrgCustomMessageTemplatesReadModel{ + CustomMessageTemplatesReadModel{ + WriteModel: eventstore.WriteModel{ + AggregateID: orgID, + ResourceOwner: orgID, + }, + CustomMessageTemplate: make(map[string]*CustomText), + }, + } +} + +func (wm *OrgCustomMessageTemplatesReadModel) AppendEvents(events ...eventstore.EventReader) { + for _, event := range events { + switch e := event.(type) { + case *org.CustomTextSetEvent: + wm.CustomMessageTemplatesReadModel.AppendEvents(&e.CustomTextSetEvent) + case *org.CustomTextRemovedEvent: + wm.CustomMessageTemplatesReadModel.AppendEvents(&e.CustomTextRemovedEvent) + case *org.CustomTextTemplateRemovedEvent: + wm.CustomMessageTemplatesReadModel.AppendEvents(&e.CustomTextTemplateRemovedEvent) + } + } +} + +func (wm *OrgCustomMessageTemplatesReadModel) Reduce() error { + return wm.CustomMessageTemplatesReadModel.Reduce() +} + +func (wm *OrgCustomMessageTemplatesReadModel) Query() *eventstore.SearchQueryBuilder { + return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent, org.AggregateType). + AggregateIDs(wm.CustomMessageTemplatesReadModel.AggregateID). + ResourceOwner(wm.ResourceOwner). + EventTypes( + org.CustomTextSetEventType, + org.CustomTextRemovedEventType, + org.CustomTextTemplateRemovedEventType) +} diff --git a/internal/command/org_custom_message_text.go b/internal/command/org_custom_message_text.go new file mode 100644 index 0000000000..52a40b4326 --- /dev/null +++ b/internal/command/org_custom_message_text.go @@ -0,0 +1,137 @@ +package command + +import ( + "context" + + "golang.org/x/text/language" + + "github.com/caos/zitadel/internal/domain" + caos_errs "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/eventstore" + "github.com/caos/zitadel/internal/repository/org" +) + +func (c *Commands) SetOrgMessageText(ctx context.Context, resourceOwner string, messageText *domain.CustomMessageText) (*domain.ObjectDetails, error) { + if resourceOwner == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-2biiR", "Errors.ResourceOwnerMissing") + } + orgAgg := org.NewAggregate(resourceOwner, resourceOwner) + events, existingMessageText, err := c.setOrgMessageText(ctx, &orgAgg.Aggregate, messageText) + if err != nil { + return nil, err + } + pushedEvents, err := c.eventstore.PushEvents(ctx, events...) + if err != nil { + return nil, err + } + err = AppendAndReduce(existingMessageText, pushedEvents...) + if err != nil { + return nil, err + } + return writeModelToObjectDetails(&existingMessageText.WriteModel), nil +} + +func (c *Commands) setOrgMessageText(ctx context.Context, orgAgg *eventstore.Aggregate, message *domain.CustomMessageText) ([]eventstore.EventPusher, *OrgCustomMessageTextReadModel, error) { + if !message.IsValid() { + return nil, nil, caos_errs.ThrowInvalidArgument(nil, "ORG-2jfsf", "Errors.CustomText.Invalid") + } + + existingMessageText, err := c.orgCustomMessageTextWriteModelByID(ctx, orgAgg.ID, message.MessageTextType, message.Language) + if err != nil { + return nil, nil, err + } + events := make([]eventstore.EventPusher, 0) + if existingMessageText.Greeting != message.Greeting { + if message.Greeting != "" { + events = append(events, org.NewCustomTextSetEvent(ctx, orgAgg, message.MessageTextType, domain.MessageGreeting, message.Greeting, message.Language)) + } else { + events = append(events, org.NewCustomTextRemovedEvent(ctx, orgAgg, message.MessageTextType, domain.MessageGreeting, message.Language)) + } + } + if existingMessageText.Subject != message.Subject { + if message.Subject != "" { + events = append(events, org.NewCustomTextSetEvent(ctx, orgAgg, message.MessageTextType, domain.MessageSubject, message.Subject, message.Language)) + } else { + events = append(events, org.NewCustomTextRemovedEvent(ctx, orgAgg, message.MessageTextType, domain.MessageSubject, message.Language)) + } + } + if existingMessageText.Title != message.Title { + if message.Title != "" { + events = append(events, org.NewCustomTextSetEvent(ctx, orgAgg, message.MessageTextType, domain.MessageTitle, message.Title, message.Language)) + } else { + events = append(events, org.NewCustomTextRemovedEvent(ctx, orgAgg, message.MessageTextType, domain.MessageTitle, message.Language)) + } + } + if existingMessageText.PreHeader != message.PreHeader { + if message.PreHeader != "" { + events = append(events, org.NewCustomTextSetEvent(ctx, orgAgg, message.MessageTextType, domain.MessagePreHeader, message.PreHeader, message.Language)) + } else { + events = append(events, org.NewCustomTextRemovedEvent(ctx, orgAgg, message.MessageTextType, domain.MessagePreHeader, message.Language)) + } + } + if existingMessageText.Text != message.Text { + if message.Text != "" { + events = append(events, org.NewCustomTextSetEvent(ctx, orgAgg, message.MessageTextType, domain.MessageText, message.Text, message.Language)) + } else { + events = append(events, org.NewCustomTextRemovedEvent(ctx, orgAgg, message.MessageTextType, domain.MessageText, message.Language)) + } + } + if existingMessageText.ButtonText != message.ButtonText { + if message.ButtonText != "" { + events = append(events, org.NewCustomTextSetEvent(ctx, orgAgg, message.MessageTextType, domain.MessageButtonText, message.ButtonText, message.Language)) + } else { + events = append(events, org.NewCustomTextRemovedEvent(ctx, orgAgg, message.MessageTextType, domain.MessageButtonText, message.Language)) + } + } + if existingMessageText.FooterText != message.FooterText { + if message.FooterText != "" { + events = append(events, org.NewCustomTextSetEvent(ctx, orgAgg, message.MessageTextType, domain.MessageFooterText, message.FooterText, message.Language)) + } else { + events = append(events, org.NewCustomTextRemovedEvent(ctx, orgAgg, message.MessageTextType, domain.MessageFooterText, message.Language)) + } + } + return events, existingMessageText, nil +} + +func (c *Commands) RemoveOrgMessageTexts(ctx context.Context, resourceOwner, messageTextType string, lang language.Tag) error { + if resourceOwner == "" { + return caos_errs.ThrowInvalidArgument(nil, "Org-3mfsf", "Errors.ResourceOwnerMissing") + } + if messageTextType == "" || lang == language.Und { + return caos_errs.ThrowInvalidArgument(nil, "Org-j59f", "Errors.CustomMessageText.Invalid") + } + customText, err := c.orgCustomMessageTextWriteModelByID(ctx, resourceOwner, messageTextType, lang) + if err != nil { + return err + } + if customText.State == domain.PolicyStateUnspecified || customText.State == domain.PolicyStateRemoved { + return caos_errs.ThrowNotFound(nil, "Org-3b8Jf", "Errors.CustomMessageText.NotFound") + } + orgAgg := OrgAggregateFromWriteModel(&customText.WriteModel) + _, err = c.eventstore.PushEvents(ctx, org.NewCustomTextTemplateRemovedEvent(ctx, orgAgg, messageTextType, lang)) + return err +} + +func (c *Commands) removeOrgMessageTextsIfExists(ctx context.Context, orgID string) ([]eventstore.EventPusher, error) { + msgTemplates := NewOrgCustomMessageTextsWriteModel(orgID) + err := c.eventstore.FilterToQueryReducer(ctx, msgTemplates) + if err != nil { + return nil, err + } + + orgAgg := OrgAggregateFromWriteModel(&msgTemplates.WriteModel) + events := make([]eventstore.EventPusher, 0, len(msgTemplates.CustomMessageTemplate)) + for _, tmpl := range msgTemplates.CustomMessageTemplate { + events = append(events, org.NewCustomTextTemplateRemovedEvent(ctx, orgAgg, tmpl.Template, tmpl.Language)) + } + return events, nil +} + +func (c *Commands) orgCustomMessageTextWriteModelByID(ctx context.Context, orgID, messageType string, lang language.Tag) (*OrgCustomMessageTextReadModel, error) { + writeModel := NewOrgCustomMessageTextWriteModel(orgID, messageType, lang) + err := c.eventstore.FilterToQueryReducer(ctx, writeModel) + if err != nil { + return nil, err + } + return writeModel, nil +} diff --git a/internal/command/org_custom_message_text_test.go b/internal/command/org_custom_message_text_test.go new file mode 100644 index 0000000000..2f52f4f36e --- /dev/null +++ b/internal/command/org_custom_message_text_test.go @@ -0,0 +1,514 @@ +package command + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "golang.org/x/text/language" + + "github.com/caos/zitadel/internal/domain" + caos_errs "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/eventstore" + "github.com/caos/zitadel/internal/eventstore/repository" + "github.com/caos/zitadel/internal/repository/org" +) + +func TestCommandSide_SetCustomMessageText(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + resourceOwner string + config *domain.CustomMessageText + } + type res struct { + want *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "no resource owner, error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + config: &domain.CustomMessageText{}, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "invalid custom text, error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + resourceOwner: "org1", + config: &domain.CustomMessageText{}, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "custom text set all fields, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + expectPush( + []*repository.Event{ + eventFromEventPusher( + org.NewCustomTextSetEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + "Template", + domain.MessageGreeting, + "Greeting", + language.English, + ), + ), + eventFromEventPusher( + org.NewCustomTextSetEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + "Template", + domain.MessageSubject, + "Subject", + language.English, + ), + ), + eventFromEventPusher( + org.NewCustomTextSetEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + "Template", + domain.MessageTitle, + "Title", + language.English, + ), + ), + eventFromEventPusher( + org.NewCustomTextSetEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + "Template", + domain.MessagePreHeader, + "PreHeader", + language.English, + ), + ), + eventFromEventPusher( + org.NewCustomTextSetEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + "Template", + domain.MessageText, + "Text", + language.English, + ), + ), + eventFromEventPusher( + org.NewCustomTextSetEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + "Template", + domain.MessageButtonText, + "ButtonText", + language.English, + ), + ), + eventFromEventPusher( + org.NewCustomTextSetEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + "Template", + domain.MessageFooterText, + "FooterText", + language.English, + ), + ), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + resourceOwner: "org1", + config: &domain.CustomMessageText{ + MessageTextType: "Template", + Language: language.English, + Greeting: "Greeting", + Subject: "Subject", + Title: "Title", + PreHeader: "PreHeader", + Text: "Text", + ButtonText: "ButtonText", + FooterText: "FooterText", + }, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "custom text remove all fields, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewCustomTextSetEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + "Template", + domain.MessageGreeting, + "Greeting", + language.English, + ), + ), + eventFromEventPusher( + org.NewCustomTextSetEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + "Template", + domain.MessageSubject, + "Subject", + language.English, + ), + ), + eventFromEventPusher( + org.NewCustomTextSetEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + "Template", + domain.MessageTitle, + "Title", + language.English, + ), + ), + eventFromEventPusher( + org.NewCustomTextSetEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + "Template", + domain.MessagePreHeader, + "PreHeader", + language.English, + ), + ), + eventFromEventPusher( + org.NewCustomTextSetEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + "Template", + domain.MessageText, + "Text", + language.English, + ), + ), + eventFromEventPusher( + org.NewCustomTextSetEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + "Template", + domain.MessageButtonText, + "ButtonText", + language.English, + ), + ), + eventFromEventPusher( + org.NewCustomTextSetEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + "Template", + domain.MessageFooterText, + "FooterText", + language.English, + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + org.NewCustomTextRemovedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + "Template", + domain.MessageGreeting, + language.English, + ), + ), + eventFromEventPusher( + org.NewCustomTextRemovedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + "Template", + domain.MessageSubject, + language.English, + ), + ), + eventFromEventPusher( + org.NewCustomTextRemovedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + "Template", + domain.MessageTitle, + language.English, + ), + ), + eventFromEventPusher( + org.NewCustomTextRemovedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + "Template", + domain.MessagePreHeader, + language.English, + ), + ), + eventFromEventPusher( + org.NewCustomTextRemovedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + "Template", + domain.MessageText, + language.English, + ), + ), + eventFromEventPusher( + org.NewCustomTextRemovedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + "Template", + domain.MessageButtonText, + language.English, + ), + ), + eventFromEventPusher( + org.NewCustomTextRemovedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + "Template", + domain.MessageFooterText, + language.English, + ), + ), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + resourceOwner: "org1", + config: &domain.CustomMessageText{ + MessageTextType: "Template", + Language: language.English, + Greeting: "", + Subject: "", + Title: "", + PreHeader: "", + Text: "", + ButtonText: "", + FooterText: "", + }, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + got, err := r.SetOrgMessageText(tt.args.ctx, tt.args.resourceOwner, tt.args.config) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assert.Equal(t, tt.res.want, got) + } + }) + } +} + +func TestCommandSide_RemoveCustomMessageText(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + resourceOwner string + mailTextType string + lang language.Tag + } + type res struct { + want *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "no resource owner, error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + mailTextType: "Template", + lang: language.English, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "no mail text type owner, error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + resourceOwner: "org1", + lang: language.English, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "no mail text type owner, error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + resourceOwner: "org1", + mailTextType: "Template", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "custom text remove all fields, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewCustomTextSetEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + "Template", + domain.MessageGreeting, + "Greeting", + language.English, + ), + ), + eventFromEventPusher( + org.NewCustomTextSetEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + "Template", + domain.MessageSubject, + "Subject", + language.English, + ), + ), + eventFromEventPusher( + org.NewCustomTextSetEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + "Template", + domain.MessageTitle, + "Title", + language.English, + ), + ), + eventFromEventPusher( + org.NewCustomTextSetEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + "Template", + domain.MessagePreHeader, + "PreHeader", + language.English, + ), + ), + eventFromEventPusher( + org.NewCustomTextSetEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + "Template", + domain.MessageText, + "Text", + language.English, + ), + ), + eventFromEventPusher( + org.NewCustomTextSetEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + "Template", + domain.MessageButtonText, + "ButtonText", + language.English, + ), + ), + eventFromEventPusher( + org.NewCustomTextSetEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + "Template", + domain.MessageFooterText, + "FooterText", + language.English, + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + org.NewCustomTextTemplateRemovedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + "Template", + language.English, + ), + ), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + resourceOwner: "org1", + mailTextType: "Template", + lang: language.English, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + err := r.RemoveOrgMessageTexts(tt.args.ctx, tt.args.resourceOwner, tt.args.mailTextType, tt.args.lang) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + }) + } +} diff --git a/internal/command/org_features.go b/internal/command/org_features.go index 47f28c6179..85be241c1f 100644 --- a/internal/command/org_features.go +++ b/internal/command/org_features.go @@ -40,6 +40,7 @@ func (c *Commands) SetOrgFeatures(ctx context.Context, resourceOwner string, fea features.LabelPolicyPrivateLabel, features.LabelPolicyWatermark, features.CustomDomain, + features.CustomText, ) if !hasChanged { return nil, caos_errs.ThrowPreconditionFailed(nil, "Features-GE4h2", "Errors.Features.NotChanged") @@ -126,6 +127,15 @@ func (c *Commands) ensureOrgSettingsToFeatures(ctx context.Context, orgID string events = append(events, removeCustomDomainsEvents...) } } + if !features.CustomText { + removeCustomTextEvents, err := c.removeOrgMessageTextsIfExists(ctx, orgID) + if err != nil { + return nil, err + } + if removeCustomTextEvents != nil { + events = append(events, removeCustomTextEvents...) + } + } return events, nil } diff --git a/internal/command/org_features_model.go b/internal/command/org_features_model.go index 956c021f52..7855fa8695 100644 --- a/internal/command/org_features_model.go +++ b/internal/command/org_features_model.go @@ -71,7 +71,8 @@ func (wm *OrgFeaturesWriteModel) NewSetEvent( passwordComplexityPolicy, labelPolicyPrivateLabel, labelPolicyWatermark, - customDomain bool, + customDomain, + customText bool, ) (*org.FeaturesSetEvent, bool) { changes := make([]features.FeaturesChanges, 0) @@ -121,6 +122,9 @@ func (wm *OrgFeaturesWriteModel) NewSetEvent( if wm.CustomDomain != customDomain { changes = append(changes, features.ChangeCustomDomain(customDomain)) } + if wm.CustomText != customText { + changes = append(changes, features.ChangeCustomText(customText)) + } if len(changes) == 0 { return nil, false diff --git a/internal/command/org_features_test.go b/internal/command/org_features_test.go index 5c43a6a2e2..f902c1f438 100644 --- a/internal/command/org_features_test.go +++ b/internal/command/org_features_test.go @@ -7,6 +7,7 @@ import ( "github.com/golang/mock/gomock" "github.com/stretchr/testify/assert" + "golang.org/x/text/language" "github.com/caos/zitadel/internal/domain" caos_errs "github.com/caos/zitadel/internal/errors" @@ -226,6 +227,18 @@ func TestCommandSide_SetOrgFeatures(t *testing.T) { ), ), ), + expectFilter( + eventFromEventPusher( + iam.NewCustomTextSetEvent( + context.Background(), + &iam.NewAggregate().Aggregate, + domain.InitCodeMessageType, + domain.MessageSubject, + "text", + language.English, + ), + ), + ), expectPush( []*repository.Event{ eventFromEventPusher( @@ -253,6 +266,7 @@ func TestCommandSide_SetOrgFeatures(t *testing.T) { LabelPolicyPrivateLabel: false, LabelPolicyWatermark: false, CustomDomain: false, + CustomText: false, }, }, res: res{ @@ -373,6 +387,18 @@ func TestCommandSide_SetOrgFeatures(t *testing.T) { ), ), ), + expectFilter( + eventFromEventPusher( + iam.NewCustomTextSetEvent( + context.Background(), + &iam.NewAggregate().Aggregate, + domain.InitCodeMessageType, + domain.MessageSubject, + "text", + language.English, + ), + ), + ), expectPush( []*repository.Event{ eventFromEventPusher( @@ -534,6 +560,18 @@ func TestCommandSide_SetOrgFeatures(t *testing.T) { ), ), ), + expectFilter( + eventFromEventPusher( + iam.NewCustomTextSetEvent( + context.Background(), + &iam.NewAggregate().Aggregate, + domain.InitCodeMessageType, + domain.MessageSubject, + "text", + language.English, + ), + ), + ), expectPush( []*repository.Event{ eventFromEventPusher( @@ -705,6 +743,18 @@ func TestCommandSide_SetOrgFeatures(t *testing.T) { ), ), ), + expectFilter( + eventFromEventPusher( + iam.NewCustomTextSetEvent( + context.Background(), + &iam.NewAggregate().Aggregate, + domain.InitCodeMessageType, + domain.MessageSubject, + "text", + language.English, + ), + ), + ), expectPush( []*repository.Event{ eventFromEventPusher( @@ -931,6 +981,18 @@ func TestCommandSide_SetOrgFeatures(t *testing.T) { ), ), ), + expectFilter( + eventFromEventPusher( + org.NewCustomTextSetEvent( + context.Background(), + &iam.NewAggregate().Aggregate, + domain.InitCodeMessageType, + domain.MessageSubject, + "text", + language.English, + ), + ), + ), expectPush( []*repository.Event{ eventFromEventPusher( @@ -951,6 +1013,9 @@ func TestCommandSide_SetOrgFeatures(t *testing.T) { eventFromEventPusher( org.NewLabelPolicyRemovedEvent(context.Background(), &org.NewAggregate("org1", "org1").Aggregate), ), + eventFromEventPusher( + org.NewCustomTextTemplateRemovedEvent(context.Background(), &org.NewAggregate("org1", "org1").Aggregate, domain.InitCodeMessageType, language.English), + ), eventFromEventPusher( newFeaturesSetEvent(context.Background(), "org1", "Test", domain.FeaturesStateActive, time.Hour), ), @@ -1144,6 +1209,18 @@ func TestCommandSide_RemoveOrgFeatures(t *testing.T) { ), ), ), + expectFilter( + eventFromEventPusher( + iam.NewCustomTextSetEvent( + context.Background(), + &iam.NewAggregate().Aggregate, + domain.InitCodeMessageType, + domain.MessageSubject, + "text", + language.English, + ), + ), + ), expectPush( []*repository.Event{ eventFromEventPusher( diff --git a/internal/command/org_policy_mail_text.go b/internal/command/org_policy_mail_text.go deleted file mode 100644 index c325c6091e..0000000000 --- a/internal/command/org_policy_mail_text.go +++ /dev/null @@ -1,114 +0,0 @@ -package command - -import ( - "context" - - "github.com/caos/zitadel/internal/domain" - caos_errs "github.com/caos/zitadel/internal/errors" - "github.com/caos/zitadel/internal/repository/org" -) - -func (c *Commands) AddMailText(ctx context.Context, resourceOwner string, mailText *domain.MailText) (*domain.MailText, error) { - if resourceOwner == "" { - return nil, caos_errs.ThrowInvalidArgument(nil, "Org-MFiig", "Errors.ResourceOwnerMissing") - } - if !mailText.IsValid() { - return nil, caos_errs.ThrowInvalidArgument(nil, "Org-4778u", "Errors.Org.MailText.Invalid") - } - addedPolicy := NewOrgMailTextWriteModel(resourceOwner, mailText.MailTextType, mailText.Language) - err := c.eventstore.FilterToQueryReducer(ctx, addedPolicy) - if err != nil { - return nil, err - } - if addedPolicy.State == domain.PolicyStateActive { - return nil, caos_errs.ThrowAlreadyExists(nil, "Org-9kufs", "Errors.Org.MailText.AlreadyExists") - } - - orgAgg := OrgAggregateFromWriteModel(&addedPolicy.MailTextWriteModel.WriteModel) - pushedEvents, err := c.eventstore.PushEvents( - ctx, - org.NewMailTextAddedEvent( - ctx, - orgAgg, - mailText.MailTextType, - mailText.Language, - mailText.Title, - mailText.PreHeader, - mailText.Subject, - mailText.Greeting, - mailText.Text, - mailText.ButtonText)) - if err != nil { - return nil, err - } - err = AppendAndReduce(addedPolicy, pushedEvents...) - if err != nil { - return nil, err - } - - return writeModelToMailText(&addedPolicy.MailTextWriteModel), nil -} - -func (c *Commands) ChangeMailText(ctx context.Context, resourceOwner string, mailText *domain.MailText) (*domain.MailText, error) { - if resourceOwner == "" { - return nil, caos_errs.ThrowInvalidArgument(nil, "Org-NFus3", "Errors.ResourceOwnerMissing") - } - if !mailText.IsValid() { - return nil, caos_errs.ThrowInvalidArgument(nil, "Org-3m9fs", "Errors.Org.MailText.Invalid") - } - existingPolicy := NewOrgMailTextWriteModel(resourceOwner, mailText.MailTextType, mailText.Language) - err := c.eventstore.FilterToQueryReducer(ctx, existingPolicy) - if err != nil { - return nil, err - } - if existingPolicy.State == domain.PolicyStateUnspecified || existingPolicy.State == domain.PolicyStateRemoved { - return nil, caos_errs.ThrowNotFound(nil, "Org-3n8fM", "Errors.Org.MailText.NotFound") - } - - orgAgg := OrgAggregateFromWriteModel(&existingPolicy.MailTextWriteModel.WriteModel) - changedEvent, hasChanged := existingPolicy.NewChangedEvent( - ctx, - orgAgg, - mailText.MailTextType, - mailText.Language, - mailText.Title, - mailText.PreHeader, - mailText.Subject, - mailText.Greeting, - mailText.Text, - mailText.ButtonText) - if !hasChanged { - return nil, caos_errs.ThrowPreconditionFailed(nil, "Org-2n9fs", "Errors.Org.MailText.NotChanged") - } - - pushedEvents, err := c.eventstore.PushEvents(ctx, changedEvent) - if err != nil { - return nil, err - } - err = AppendAndReduce(existingPolicy, pushedEvents...) - if err != nil { - return nil, err - } - - return writeModelToMailText(&existingPolicy.MailTextWriteModel), nil -} - -func (c *Commands) RemoveMailText(ctx context.Context, resourceOwner, mailTextType, language string) error { - if resourceOwner == "" { - return caos_errs.ThrowInvalidArgument(nil, "Org-2N7fd", "Errors.ResourceOwnerMissing") - } - if mailTextType == "" || language == "" { - return caos_errs.ThrowInvalidArgument(nil, "Org-N8fsf", "Errors.Org.MailText.Invalid") - } - existingPolicy := NewOrgMailTextWriteModel(resourceOwner, mailTextType, language) - err := c.eventstore.FilterToQueryReducer(ctx, existingPolicy) - if err != nil { - return err - } - if existingPolicy.State == domain.PolicyStateUnspecified || existingPolicy.State == domain.PolicyStateRemoved { - return caos_errs.ThrowNotFound(nil, "Org-3b8Jf", "Errors.Org.MailText.NotFound") - } - orgAgg := OrgAggregateFromWriteModel(&existingPolicy.WriteModel) - _, err = c.eventstore.PushEvents(ctx, org.NewMailTextRemovedEvent(ctx, orgAgg, mailTextType, language)) - return err -} diff --git a/internal/command/org_policy_mail_text_model.go b/internal/command/org_policy_mail_text_model.go deleted file mode 100644 index ae8a8cc9e5..0000000000 --- a/internal/command/org_policy_mail_text_model.go +++ /dev/null @@ -1,95 +0,0 @@ -package command - -import ( - "context" - "github.com/caos/zitadel/internal/eventstore" - "github.com/caos/zitadel/internal/repository/org" - "github.com/caos/zitadel/internal/repository/policy" -) - -type OrgMailTextWriteModel struct { - MailTextWriteModel -} - -func NewOrgMailTextWriteModel(orgID, mailTextType, language string) *OrgMailTextWriteModel { - return &OrgMailTextWriteModel{ - MailTextWriteModel{ - WriteModel: eventstore.WriteModel{ - AggregateID: orgID, - ResourceOwner: orgID, - }, - MailTextType: mailTextType, - Language: language, - }, - } -} - -func (wm *OrgMailTextWriteModel) AppendEvents(events ...eventstore.EventReader) { - for _, event := range events { - switch e := event.(type) { - case *org.MailTextAddedEvent: - wm.MailTextWriteModel.AppendEvents(&e.MailTextAddedEvent) - case *org.MailTextChangedEvent: - wm.MailTextWriteModel.AppendEvents(&e.MailTextChangedEvent) - case *org.MailTextRemovedEvent: - wm.MailTextWriteModel.AppendEvents(&e.MailTextRemovedEvent) - } - } -} - -func (wm *OrgMailTextWriteModel) Reduce() error { - return wm.MailTextWriteModel.Reduce() -} - -func (wm *OrgMailTextWriteModel) Query() *eventstore.SearchQueryBuilder { - query := eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent, org.AggregateType). - AggregateIDs(wm.MailTextWriteModel.AggregateID). - EventTypes(org.MailTextAddedEventType, - org.MailTextChangedEventType, - org.MailTextRemovedEventType) - if wm.ResourceOwner != "" { - query.ResourceOwner(wm.ResourceOwner) - } - return query -} - -func (wm *OrgMailTextWriteModel) NewChangedEvent( - ctx context.Context, - aggregate *eventstore.Aggregate, - mailTextType, - language, - title, - preHeader, - subject, - greeting, - text, - buttonText string, -) (*org.MailTextChangedEvent, bool) { - changes := make([]policy.MailTextChanges, 0) - if wm.Title != title { - changes = append(changes, policy.ChangeTitle(title)) - } - if wm.PreHeader != preHeader { - changes = append(changes, policy.ChangePreHeader(preHeader)) - } - if wm.Subject != subject { - changes = append(changes, policy.ChangeSubject(subject)) - } - if wm.Greeting != greeting { - changes = append(changes, policy.ChangeGreeting(greeting)) - } - if wm.Text != text { - changes = append(changes, policy.ChangeText(text)) - } - if wm.ButtonText != buttonText { - changes = append(changes, policy.ChangeButtonText(buttonText)) - } - if len(changes) == 0 { - return nil, false - } - changedEvent, err := org.NewMailTextChangedEvent(ctx, aggregate, mailTextType, language, changes) - if err != nil { - return nil, false - } - return changedEvent, true -} diff --git a/internal/command/org_policy_mail_text_test.go b/internal/command/org_policy_mail_text_test.go deleted file mode 100644 index b8ddfcbcc8..0000000000 --- a/internal/command/org_policy_mail_text_test.go +++ /dev/null @@ -1,563 +0,0 @@ -package command - -import ( - "context" - "testing" - - "github.com/stretchr/testify/assert" - - "github.com/caos/zitadel/internal/domain" - caos_errs "github.com/caos/zitadel/internal/errors" - "github.com/caos/zitadel/internal/eventstore" - "github.com/caos/zitadel/internal/eventstore/repository" - "github.com/caos/zitadel/internal/eventstore/v1/models" - "github.com/caos/zitadel/internal/repository/org" - "github.com/caos/zitadel/internal/repository/policy" -) - -func TestCommandSide_AddMailText(t *testing.T) { - type fields struct { - eventstore *eventstore.Eventstore - } - type args struct { - ctx context.Context - orgID string - policy *domain.MailText - } - type res struct { - want *domain.MailText - err func(error) bool - } - tests := []struct { - name string - fields fields - args args - res res - }{ - { - name: "org id missing, invalid argument error", - fields: fields{ - eventstore: eventstoreExpect( - t, - ), - }, - args: args{ - ctx: context.Background(), - policy: &domain.MailText{ - MailTextType: "mail-text-type", - Language: "de", - Title: "title", - PreHeader: "pre-header", - Subject: "subject", - Greeting: "greeting", - Text: "text", - ButtonText: "button-text", - }, - }, - res: res{ - err: caos_errs.IsErrorInvalidArgument, - }, - }, - { - name: "mail text already existing, already exists error", - fields: fields{ - eventstore: eventstoreExpect( - t, - expectFilter( - eventFromEventPusher( - org.NewMailTextAddedEvent(context.Background(), - &org.NewAggregate("org1", "org1").Aggregate, - "mail-text-type", - "de", - "title", - "pre-header", - "subject", - "greeting", - "text", - "button-text", - ), - ), - ), - ), - }, - args: args{ - ctx: context.Background(), - orgID: "org1", - policy: &domain.MailText{ - MailTextType: "mail-text-type", - Language: "de", - Title: "title", - PreHeader: "pre-header", - Subject: "subject", - Greeting: "greeting", - Text: "text", - ButtonText: "button-text", - }, - }, - res: res{ - err: caos_errs.IsErrorAlreadyExists, - }, - }, - { - name: "mail text already existing, already exists error", - fields: fields{ - eventstore: eventstoreExpect( - t, - expectFilter( - eventFromEventPusher( - org.NewMailTextAddedEvent(context.Background(), - &org.NewAggregate("org1", "org1").Aggregate, - "mail-text-type", - "de", - "title", - "pre-header", - "subject", - "greeting", - "text", - "button-text", - ), - ), - ), - ), - }, - args: args{ - ctx: context.Background(), - orgID: "org1", - policy: &domain.MailText{ - MailTextType: "mail-text-type", - Language: "de", - Title: "title", - PreHeader: "pre-header", - Subject: "subject", - Greeting: "greeting", - Text: "text", - ButtonText: "button-text", - }, - }, - res: res{ - err: caos_errs.IsErrorAlreadyExists, - }, - }, - { - name: "add policy,ok", - fields: fields{ - eventstore: eventstoreExpect( - t, - expectFilter(), - expectPush( - []*repository.Event{ - eventFromEventPusher( - org.NewMailTextAddedEvent(context.Background(), - &org.NewAggregate("org1", "org1").Aggregate, - "mail-text-type", - "de", - "title", - "pre-header", - "subject", - "greeting", - "text", - "button-text", - ), - ), - }, - uniqueConstraintsFromEventConstraint(policy.NewAddMailTextUniqueConstraint("org1", "mail-text-type", "de")), - ), - ), - }, - args: args{ - ctx: context.Background(), - orgID: "org1", - policy: &domain.MailText{ - MailTextType: "mail-text-type", - Language: "de", - Title: "title", - PreHeader: "pre-header", - Subject: "subject", - Greeting: "greeting", - Text: "text", - ButtonText: "button-text", - }, - }, - res: res{ - want: &domain.MailText{ - ObjectRoot: models.ObjectRoot{ - AggregateID: "org1", - ResourceOwner: "org1", - }, - MailTextType: "mail-text-type", - Language: "de", - Title: "title", - PreHeader: "pre-header", - Subject: "subject", - Greeting: "greeting", - Text: "text", - ButtonText: "button-text", - State: domain.PolicyStateActive, - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - r := &Commands{ - eventstore: tt.fields.eventstore, - } - got, err := r.AddMailText(tt.args.ctx, tt.args.orgID, tt.args.policy) - if tt.res.err == nil { - assert.NoError(t, err) - } - if tt.res.err != nil && !tt.res.err(err) { - t.Errorf("got wrong err: %v ", err) - } - if tt.res.err == nil { - assert.Equal(t, tt.res.want, got) - } - }) - } -} - -func TestCommandSide_ChangeMailText(t *testing.T) { - type fields struct { - eventstore *eventstore.Eventstore - } - type args struct { - ctx context.Context - orgID string - policy *domain.MailText - } - type res struct { - want *domain.MailText - err func(error) bool - } - tests := []struct { - name string - fields fields - args args - res res - }{ - { - name: "org id missing, invalid argument error", - fields: fields{ - eventstore: eventstoreExpect( - t, - ), - }, - args: args{ - ctx: context.Background(), - policy: &domain.MailText{ - MailTextType: "mail-text-type", - Language: "de", - Title: "title", - PreHeader: "pre-header", - Subject: "subject", - Greeting: "greeting", - Text: "text", - ButtonText: "button-text", - }, - }, - res: res{ - err: caos_errs.IsErrorInvalidArgument, - }, - }, - { - name: "mailtext invalid, invalid argument error", - fields: fields{ - eventstore: eventstoreExpect( - t, - ), - }, - args: args{ - ctx: context.Background(), - policy: &domain.MailText{}, - }, - res: res{ - err: caos_errs.IsErrorInvalidArgument, - }, - }, - { - name: "mail template not existing, not found error", - fields: fields{ - eventstore: eventstoreExpect( - t, - expectFilter(), - ), - }, - args: args{ - ctx: context.Background(), - orgID: "org1", - policy: &domain.MailText{ - MailTextType: "mail-text-type", - Language: "de", - Title: "title", - PreHeader: "pre-header", - Subject: "subject", - Greeting: "greeting", - Text: "text", - ButtonText: "button-text", - }, - }, - res: res{ - err: caos_errs.IsNotFound, - }, - }, - { - name: "no changes, precondition error", - fields: fields{ - eventstore: eventstoreExpect( - t, - expectFilter( - eventFromEventPusher( - org.NewMailTextAddedEvent(context.Background(), - &org.NewAggregate("org1", "org1").Aggregate, - "mail-text-type", - "de", - "title", - "pre-header", - "subject", - "greeting", - "text", - "button-text", - ), - ), - ), - ), - }, - args: args{ - ctx: context.Background(), - orgID: "org1", - policy: &domain.MailText{ - MailTextType: "mail-text-type", - Language: "de", - Title: "title", - PreHeader: "pre-header", - Subject: "subject", - Greeting: "greeting", - Text: "text", - ButtonText: "button-text", - }, - }, - res: res{ - err: caos_errs.IsPreconditionFailed, - }, - }, - { - name: "change, ok", - fields: fields{ - eventstore: eventstoreExpect( - t, - expectFilter( - eventFromEventPusher( - org.NewMailTextAddedEvent(context.Background(), - &org.NewAggregate("org1", "org1").Aggregate, - "mail-text-type", - "de", - "title", - "pre-header", - "subject", - "greeting", - "text", - "button-text", - ), - ), - ), - expectPush( - []*repository.Event{ - eventFromEventPusher( - newMailTextChangedEvent( - context.Background(), - "org1", - "mail-text-type", - "de", - "title-change", - "pre-header-change", - "subject-change", - "greeting-change", - "text-change", - "button-text-change"), - ), - }, - ), - ), - }, - args: args{ - ctx: context.Background(), - orgID: "org1", - policy: &domain.MailText{ - MailTextType: "mail-text-type", - Language: "de", - Title: "title-change", - PreHeader: "pre-header-change", - Subject: "subject-change", - Greeting: "greeting-change", - Text: "text-change", - ButtonText: "button-text-change", - }, - }, - res: res{ - want: &domain.MailText{ - ObjectRoot: models.ObjectRoot{ - AggregateID: "org1", - ResourceOwner: "org1", - }, - MailTextType: "mail-text-type", - Language: "de", - Title: "title-change", - PreHeader: "pre-header-change", - Subject: "subject-change", - Greeting: "greeting-change", - Text: "text-change", - ButtonText: "button-text-change", - State: domain.PolicyStateActive, - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - r := &Commands{ - eventstore: tt.fields.eventstore, - } - got, err := r.ChangeMailText(tt.args.ctx, tt.args.orgID, tt.args.policy) - if tt.res.err == nil { - assert.NoError(t, err) - } - if tt.res.err != nil && !tt.res.err(err) { - t.Errorf("got wrong err: %v ", err) - } - if tt.res.err == nil { - assert.Equal(t, tt.res.want, got) - } - }) - } -} - -func TestCommandSide_RemoveMailText(t *testing.T) { - type fields struct { - eventstore *eventstore.Eventstore - } - type args struct { - ctx context.Context - orgID string - mailTextType string - language string - } - type res struct { - want *domain.ObjectDetails - err func(error) bool - } - tests := []struct { - name string - fields fields - args args - res res - }{ - { - name: "org id missing, invalid argument error", - fields: fields{ - eventstore: eventstoreExpect( - t, - ), - }, - args: args{ - ctx: context.Background(), - }, - res: res{ - err: caos_errs.IsErrorInvalidArgument, - }, - }, - { - name: "policy not existing, not found error", - fields: fields{ - eventstore: eventstoreExpect( - t, - expectFilter(), - ), - }, - args: args{ - ctx: context.Background(), - orgID: "org1", - mailTextType: "mail-text-type", - language: "de", - }, - res: res{ - err: caos_errs.IsNotFound, - }, - }, - { - name: "remove, ok", - fields: fields{ - eventstore: eventstoreExpect( - t, - expectFilter( - eventFromEventPusher( - org.NewMailTextAddedEvent(context.Background(), - &org.NewAggregate("org1", "org1").Aggregate, - "mail-text-type", - "de", - "title", - "pre-header", - "subject", - "greeting", - "text", - "button-text", - ), - ), - ), - expectPush( - []*repository.Event{ - eventFromEventPusher( - org.NewMailTextRemovedEvent(context.Background(), - &org.NewAggregate("org1", "org1").Aggregate, - "mail-text-type", - "de"), - ), - }, - uniqueConstraintsFromEventConstraint(policy.NewRemoveMailTextUniqueConstraint("org1", "mail-text-type", "de")), - ), - ), - }, - args: args{ - ctx: context.Background(), - orgID: "org1", - mailTextType: "mail-text-type", - language: "de", - }, - res: res{ - want: &domain.ObjectDetails{ - ResourceOwner: "org1", - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - r := &Commands{ - eventstore: tt.fields.eventstore, - } - err := r.RemoveMailText(tt.args.ctx, tt.args.orgID, tt.args.mailTextType, tt.args.language) - if tt.res.err == nil { - assert.NoError(t, err) - } - if tt.res.err != nil && !tt.res.err(err) { - t.Errorf("got wrong err: %v ", err) - } - }) - } -} - -func newMailTextChangedEvent(ctx context.Context, orgID, mailTextType, language, title, preHeader, subject, greeting, text, buttonText string) *org.MailTextChangedEvent { - event, _ := org.NewMailTextChangedEvent(ctx, - &org.NewAggregate(orgID, orgID).Aggregate, - mailTextType, - language, - []policy.MailTextChanges{ - policy.ChangeTitle(title), - policy.ChangePreHeader(preHeader), - policy.ChangeSubject(subject), - policy.ChangeGreeting(greeting), - policy.ChangeText(text), - policy.ChangeButtonText(buttonText), - }, - ) - return event -} diff --git a/internal/command/org_policy_password_age.go b/internal/command/org_policy_password_age.go index 48fa754f44..045fa35522 100644 --- a/internal/command/org_policy_password_age.go +++ b/internal/command/org_policy_password_age.go @@ -65,7 +65,7 @@ func (c *Commands) ChangePasswordAgePolicy(ctx context.Context, resourceOwner st func (c *Commands) RemovePasswordAgePolicy(ctx context.Context, orgID string) (*domain.ObjectDetails, error) { if orgID == "" { - return nil, caos_errs.ThrowInvalidArgument(nil, "Org-2N8fs", "Errors.ResourceOwnerMissing") + return nil, caos_errs.ThrowInvalidArgument(nil, "Org-M58wd", "Errors.ResourceOwnerMissing") } existingPolicy := NewOrgPasswordAgePolicyWriteModel(orgID) err := c.eventstore.FilterToQueryReducer(ctx, existingPolicy) diff --git a/internal/command/policy_mail_text_model.go b/internal/command/policy_mail_text_model.go deleted file mode 100644 index 2d112bab8f..0000000000 --- a/internal/command/policy_mail_text_model.go +++ /dev/null @@ -1,65 +0,0 @@ -package command - -import ( - "github.com/caos/zitadel/internal/domain" - "github.com/caos/zitadel/internal/eventstore" - "github.com/caos/zitadel/internal/repository/policy" -) - -type MailTextWriteModel struct { - eventstore.WriteModel - - MailTextType string - Language string - Title string - PreHeader string - Subject string - Greeting string - Text string - ButtonText string - - State domain.PolicyState -} - -func (wm *MailTextWriteModel) Reduce() error { - for _, event := range wm.Events { - switch e := event.(type) { - case *policy.MailTextAddedEvent: - if wm.MailTextType != e.MailTextType || wm.Language != e.Language { - continue - } - wm.Title = e.Title - wm.PreHeader = e.PreHeader - wm.Subject = e.Subject - wm.Greeting = e.Greeting - wm.Text = e.Text - wm.ButtonText = e.ButtonText - wm.State = domain.PolicyStateActive - case *policy.MailTextChangedEvent: - if wm.MailTextType != e.MailTextType || wm.Language != e.Language { - continue - } - if e.Title != nil { - wm.Title = *e.Title - } - if e.PreHeader != nil { - wm.PreHeader = *e.PreHeader - } - if e.Subject != nil { - wm.Subject = *e.Subject - } - if e.Greeting != nil { - wm.Greeting = *e.Greeting - } - if e.Text != nil { - wm.Text = *e.Text - } - if e.ButtonText != nil { - wm.ButtonText = *e.ButtonText - } - case *policy.MailTextRemovedEvent: - wm.State = domain.PolicyStateRemoved - } - } - return wm.WriteModel.Reduce() -} diff --git a/internal/command/setup_step10.go b/internal/command/setup_step10.go index d442b60f70..a1d5e6550f 100644 --- a/internal/command/setup_step10.go +++ b/internal/command/setup_step10.go @@ -2,14 +2,15 @@ package command import ( "context" + "github.com/caos/logging" + "github.com/caos/zitadel/internal/domain" "github.com/caos/zitadel/internal/eventstore" ) type Step10 struct { DefaultMailTemplate domain.MailTemplate - DefaultMailTexts []domain.MailText } func (s *Step10) Step() domain.Step { @@ -30,13 +31,6 @@ func (c *Commands) SetupStep10(ctx context.Context, step *Step10) error { events := []eventstore.EventPusher{ mailTemplateEvent, } - for _, text := range step.DefaultMailTexts { - defaultTextEvent, err := c.addDefaultMailText(ctx, iamAgg, NewIAMMailTextWriteModel(text.MailTextType, text.Language), &text) - if err != nil { - return nil, err - } - events = append(events, defaultTextEvent) - } logging.Log("SETUP-3N9fs").Info("default mail template/text set up") return events, nil } diff --git a/internal/command/setup_step16.go b/internal/command/setup_step16.go new file mode 100644 index 0000000000..35e1ab47be --- /dev/null +++ b/internal/command/setup_step16.go @@ -0,0 +1,39 @@ +package command + +import ( + "context" + "github.com/caos/logging" + "github.com/caos/zitadel/internal/domain" + "github.com/caos/zitadel/internal/eventstore" +) + +type Step16 struct { + DefaultMessageTexts []domain.CustomMessageText +} + +func (s *Step16) Step() domain.Step { + return domain.Step16 +} + +func (s *Step16) execute(ctx context.Context, commandSide *Commands) error { + return commandSide.SetupStep16(ctx, s) +} + +func (c *Commands) SetupStep16(ctx context.Context, step *Step16) error { + fn := func(iam *IAMWriteModel) ([]eventstore.EventPusher, error) { + iamAgg := IAMAggregateFromWriteModel(&iam.WriteModel) + events := make([]eventstore.EventPusher, 0) + + for _, text := range step.DefaultMessageTexts { + mailEvents, _, err := c.setDefaultMessageText(ctx, iamAgg, &text) + if err != nil { + return nil, err + } + events = append(events, mailEvents...) + } + + logging.Log("SETUP-4k0LL").Info("default message text set up") + return events, nil + } + return c.setup(ctx, step, fn) +} diff --git a/internal/domain/custom_messge_text.go b/internal/domain/custom_messge_text.go new file mode 100644 index 0000000000..744118e37f --- /dev/null +++ b/internal/domain/custom_messge_text.go @@ -0,0 +1,42 @@ +package domain + +import ( + "golang.org/x/text/language" + + "github.com/caos/zitadel/internal/eventstore/v1/models" +) + +const ( + InitCodeMessageType = "InitCode" + PasswordResetMessageType = "PasswordReset" + VerifyEmailMessageType = "VerifyEmail" + VerifyPhoneMessageType = "VerifyPhone" + DomainClaimedMessageType = "DomainClaimed" + MessageTitle = "Title" + MessagePreHeader = "PreHeader" + MessageSubject = "Subject" + MessageGreeting = "Greeting" + MessageText = "Text" + MessageButtonText = "ButtonText" + MessageFooterText = "FooterText" +) + +type CustomMessageText struct { + models.ObjectRoot + + State PolicyState + Default bool + MessageTextType string + Language language.Tag + Title string + PreHeader string + Subject string + Greeting string + Text string + ButtonText string + FooterText string +} + +func (m *CustomMessageText) IsValid() bool { + return m.MessageTextType != "" && m.Language != language.Und +} diff --git a/internal/domain/custom_text.go b/internal/domain/custom_text.go new file mode 100644 index 0000000000..eb95df83e8 --- /dev/null +++ b/internal/domain/custom_text.go @@ -0,0 +1,32 @@ +package domain + +import ( + "golang.org/x/text/language" + + "github.com/caos/zitadel/internal/eventstore/v1/models" +) + +type CustomText struct { + models.ObjectRoot + + State CustomTextState + Default bool + Template string + Key string + Language language.Tag + Text string +} + +type CustomTextState int32 + +const ( + CustomTextStateUnspecified CustomTextState = iota + CustomTextStateActive + CustomTextStateRemoved + + customTextStateCount +) + +func (m *CustomText) IsValid() bool { + return m.Key != "" && m.Language != language.Und && m.Text != "" +} diff --git a/internal/domain/features.go b/internal/domain/features.go index e1dfeb84d2..ff49486e19 100644 --- a/internal/domain/features.go +++ b/internal/domain/features.go @@ -18,6 +18,7 @@ const ( FeatureLabelPolicy = "label_policy" FeatureLabelPolicyPrivateLabel = FeatureLabelPolicy + ".private_label" FeatureLabelPolicyWatermark = FeatureLabelPolicy + ".watermark" + FeatureCustomText = "custom_text" FeatureCustomDomain = "custom_domain" ) @@ -41,6 +42,7 @@ type Features struct { LabelPolicyPrivateLabel bool LabelPolicyWatermark bool CustomDomain bool + CustomText bool } type FeaturesState int32 diff --git a/internal/domain/policy_mail_text.go b/internal/domain/policy_mail_text.go deleted file mode 100644 index ce1e4b12b1..0000000000 --- a/internal/domain/policy_mail_text.go +++ /dev/null @@ -1,22 +0,0 @@ -package domain - -import "github.com/caos/zitadel/internal/eventstore/v1/models" - -type MailText struct { - models.ObjectRoot - - State PolicyState - Default bool - MailTextType string - Language string - Title string - PreHeader string - Subject string - Greeting string - Text string - ButtonText string -} - -func (m *MailText) IsValid() bool { - return m.MailTextType != "" && m.Language != "" && m.Title != "" && m.PreHeader != "" && m.Subject != "" && m.Greeting != "" && m.Text != "" && m.ButtonText != "" -} diff --git a/internal/domain/step.go b/internal/domain/step.go index 62d7edbac0..f93642cf8f 100644 --- a/internal/domain/step.go +++ b/internal/domain/step.go @@ -18,6 +18,7 @@ const ( Step13 Step14 Step15 + Step16 //StepCount marks the the length of possible steps (StepCount-1 == last possible step) StepCount ) diff --git a/internal/features/model/features_view.go b/internal/features/model/features_view.go index e72c9bf982..d2fdf24c3e 100644 --- a/internal/features/model/features_view.go +++ b/internal/features/model/features_view.go @@ -28,6 +28,7 @@ type FeaturesView struct { LabelPolicyPrivateLabel bool LabelPolicyWatermark bool CustomDomain bool + CustomText bool } func (f *FeaturesView) FeatureList() []string { @@ -62,6 +63,9 @@ func (f *FeaturesView) FeatureList() []string { if f.CustomDomain { list = append(list, domain.FeatureCustomDomain) } + if f.CustomText { + list = append(list, domain.FeatureCustomText) + } return list } diff --git a/internal/features/repository/view/model/features.go b/internal/features/repository/view/model/features.go index 1093ed05c2..ca8dc1af38 100644 --- a/internal/features/repository/view/model/features.go +++ b/internal/features/repository/view/model/features.go @@ -42,6 +42,7 @@ type FeaturesView struct { LabelPolicyPrivateLabel bool `json:"labelPolicyPrivateLabel" gorm:"column:label_policy_private_label"` LabelPolicyWatermark bool `json:"labelPolicyWatermark" gorm:"column:label_policy_watermark"` CustomDomain bool `json:"customDomain" gorm:"column:custom_domain"` + CustomText bool `json:"customText" gorm:"column:custom_text"` } func FeaturesToModel(features *FeaturesView) *features_model.FeaturesView { @@ -66,6 +67,7 @@ func FeaturesToModel(features *FeaturesView) *features_model.FeaturesView { LabelPolicyPrivateLabel: features.LabelPolicyPrivateLabel, LabelPolicyWatermark: features.LabelPolicyWatermark, CustomDomain: features.CustomDomain, + CustomText: features.CustomText, } } diff --git a/internal/iam/model/mail_text.go b/internal/iam/model/mail_text.go index 353ab70f1f..6a74587e6b 100644 --- a/internal/iam/model/mail_text.go +++ b/internal/iam/model/mail_text.go @@ -21,6 +21,7 @@ type MailText struct { Greeting string Text string ButtonText string + FooterText string } func (p *MailText) IsValid() bool { diff --git a/internal/iam/model/mail_text_view.go b/internal/iam/model/mail_text_view.go deleted file mode 100644 index ccc27e7522..0000000000 --- a/internal/iam/model/mail_text_view.go +++ /dev/null @@ -1,59 +0,0 @@ -package model - -import ( - "github.com/caos/zitadel/internal/domain" - "time" -) - -type MailTextsView struct { - Texts []*MailTextView - Default bool -} -type MailTextView struct { - AggregateID string - MailTextType string - Language string - Title string - PreHeader string - Subject string - Greeting string - Text string - ButtonText string - Default bool - - CreationDate time.Time - ChangeDate time.Time - Sequence uint64 -} - -type MailTextSearchRequest struct { - Offset uint64 - Limit uint64 - SortingColumn MailTextSearchKey - Asc bool - Queries []*MailTextSearchQuery -} - -type MailTextSearchKey int32 - -const ( - MailTextSearchKeyUnspecified MailTextSearchKey = iota - MailTextSearchKeyAggregateID - MailTextSearchKeyMailTextType - MailTextSearchKeyLanguage -) - -type MailTextSearchQuery struct { - Key MailTextSearchKey - Method domain.SearchMethod - Value interface{} -} - -type MailTextSearchResponse struct { - Offset uint64 - Limit uint64 - TotalResult uint64 - Result []*MailTextView - Sequence uint64 - Timestamp time.Time -} diff --git a/internal/iam/model/message_text_view.go b/internal/iam/model/message_text_view.go new file mode 100644 index 0000000000..86b42cc7a7 --- /dev/null +++ b/internal/iam/model/message_text_view.go @@ -0,0 +1,63 @@ +package model + +import ( + "time" + + "golang.org/x/text/language" + + "github.com/caos/zitadel/internal/domain" +) + +type MessageTextsView struct { + Texts []*MessageTextView + Default bool +} +type MessageTextView struct { + AggregateID string + MessageTextType string + Language language.Tag + Title string + PreHeader string + Subject string + Greeting string + Text string + ButtonText string + FooterText string + Default bool + + CreationDate time.Time + ChangeDate time.Time + Sequence uint64 +} + +type MessageTextSearchRequest struct { + Offset uint64 + Limit uint64 + SortingColumn MessageTextSearchKey + Asc bool + Queries []*MessageTextSearchQuery +} + +type MessageTextSearchKey int32 + +const ( + MessageTextSearchKeyUnspecified MessageTextSearchKey = iota + MessageTextSearchKeyAggregateID + MessageTextSearchKeyMessageTextType + MessageTextSearchKeyLanguage +) + +type MessageTextSearchQuery struct { + Key MessageTextSearchKey + Method domain.SearchMethod + Value interface{} +} + +type MessageTextSearchResponse struct { + Offset uint64 + Limit uint64 + TotalResult uint64 + Result []*MessageTextView + Sequence uint64 + Timestamp time.Time +} diff --git a/internal/iam/repository/eventsourcing/model/iam.go b/internal/iam/repository/eventsourcing/model/iam.go index 2d14dd37f2..d7867762d8 100644 --- a/internal/iam/repository/eventsourcing/model/iam.go +++ b/internal/iam/repository/eventsourcing/model/iam.go @@ -33,64 +33,23 @@ type IAM struct { DefaultLoginPolicy *LoginPolicy `json:"-"` DefaultLabelPolicy *LabelPolicy `json:"-"` DefaultMailTemplate *MailTemplate `json:"-"` - DefaultMailTexts []*MailText `json:"-"` DefaultOrgIAMPolicy *OrgIAMPolicy `json:"-"` DefaultPasswordComplexityPolicy *PasswordComplexityPolicy `json:"-"` DefaultPasswordAgePolicy *PasswordAgePolicy `json:"-"` DefaultPasswordLockoutPolicy *PasswordLockoutPolicy `json:"-"` } -func IAMFromModel(iam *model.IAM) *IAM { - members := IAMMembersFromModel(iam.Members) - idps := IDPConfigsFromModel(iam.IDPs) - mailTexts := MailTextsFromModel(iam.DefaultMailTexts) - converted := &IAM{ - ObjectRoot: iam.ObjectRoot, - SetUpStarted: Step(iam.SetUpStarted), - SetUpDone: Step(iam.SetUpDone), - GlobalOrgID: iam.GlobalOrgID, - IAMProjectID: iam.IAMProjectID, - Members: members, - IDPs: idps, - DefaultMailTexts: mailTexts, - } - if iam.DefaultLoginPolicy != nil { - converted.DefaultLoginPolicy = LoginPolicyFromModel(iam.DefaultLoginPolicy) - } - if iam.DefaultLabelPolicy != nil { - converted.DefaultLabelPolicy = LabelPolicyFromModel(iam.DefaultLabelPolicy) - } - if iam.DefaultMailTemplate != nil { - converted.DefaultMailTemplate = MailTemplateFromModel(iam.DefaultMailTemplate) - } - if iam.DefaultPasswordComplexityPolicy != nil { - converted.DefaultPasswordComplexityPolicy = PasswordComplexityPolicyFromModel(iam.DefaultPasswordComplexityPolicy) - } - if iam.DefaultPasswordAgePolicy != nil { - converted.DefaultPasswordAgePolicy = PasswordAgePolicyFromModel(iam.DefaultPasswordAgePolicy) - } - if iam.DefaultPasswordLockoutPolicy != nil { - converted.DefaultPasswordLockoutPolicy = PasswordLockoutPolicyFromModel(iam.DefaultPasswordLockoutPolicy) - } - if iam.DefaultOrgIAMPolicy != nil { - converted.DefaultOrgIAMPolicy = OrgIAMPolicyFromModel(iam.DefaultOrgIAMPolicy) - } - return converted -} - func IAMToModel(iam *IAM) *model.IAM { members := IAMMembersToModel(iam.Members) idps := IDPConfigsToModel(iam.IDPs) - mailTexts := MailTextsToModel(iam.DefaultMailTexts) converted := &model.IAM{ - ObjectRoot: iam.ObjectRoot, - SetUpStarted: domain.Step(iam.SetUpStarted), - SetUpDone: domain.Step(iam.SetUpDone), - GlobalOrgID: iam.GlobalOrgID, - IAMProjectID: iam.IAMProjectID, - Members: members, - IDPs: idps, - DefaultMailTexts: mailTexts, + ObjectRoot: iam.ObjectRoot, + SetUpStarted: domain.Step(iam.SetUpStarted), + SetUpDone: domain.Step(iam.SetUpDone), + GlobalOrgID: iam.GlobalOrgID, + IAMProjectID: iam.IAMProjectID, + Members: members, + IDPs: idps, } if iam.DefaultLoginPolicy != nil { converted.DefaultLoginPolicy = LoginPolicyToModel(iam.DefaultLoginPolicy) @@ -199,10 +158,6 @@ func (i *IAM) AppendEvent(event *es_models.Event) (err error) { return i.appendAddMailTemplateEvent(event) case MailTemplateChanged: return i.appendChangeMailTemplateEvent(event) - case MailTextAdded: - return i.appendAddMailTextEvent(event) - case MailTextChanged: - return i.appendChangeMailTextEvent(event) case PasswordComplexityPolicyAdded: return i.appendAddPasswordComplexityPolicyEvent(event) case PasswordComplexityPolicyChanged: diff --git a/internal/iam/repository/eventsourcing/model/mail_text.go b/internal/iam/repository/eventsourcing/model/mail_text.go index 27b85fef0a..853134a532 100644 --- a/internal/iam/repository/eventsourcing/model/mail_text.go +++ b/internal/iam/repository/eventsourcing/model/mail_text.go @@ -110,43 +110,6 @@ func (p *MailText) Changes(changed *MailText) map[string]interface{} { return changes } -func (i *IAM) appendAddMailTextEvent(event *es_models.Event) error { - mailText := &MailText{} - err := mailText.SetDataLabel(event) - if err != nil { - return err - } - mailText.ObjectRoot.CreationDate = event.CreationDate - i.DefaultMailTexts = append(i.DefaultMailTexts, mailText) - return nil -} - -func (i *IAM) appendChangeMailTextEvent(event *es_models.Event) error { - mailText := &MailText{} - err := mailText.SetDataLabel(event) - if err != nil { - return err - } - if n, m := GetMailText(i.DefaultMailTexts, mailText.MailTextType, mailText.Language); m != nil { - i.DefaultMailTexts[n] = mailText - } - return nil -} - -func (i *IAM) appendRemoveMailTextEvent(event *es_models.Event) error { - mailText := &MailText{} - err := mailText.SetDataLabel(event) - if err != nil { - return err - } - if n, m := GetMailText(i.DefaultMailTexts, mailText.MailTextType, mailText.Language); m != nil { - i.DefaultMailTexts[n] = i.DefaultMailTexts[len(i.DefaultMailTexts)-1] - i.DefaultMailTexts[len(i.DefaultMailTexts)-1] = nil - i.DefaultMailTexts = i.DefaultMailTexts[:len(i.DefaultMailTexts)-1] - } - return nil -} - func (p *MailText) SetDataLabel(event *es_models.Event) error { err := json.Unmarshal(event.Data, p) if err != nil { diff --git a/internal/iam/repository/eventsourcing/model/mail_text_test.go b/internal/iam/repository/eventsourcing/model/mail_text_test.go deleted file mode 100644 index 557be25d55..0000000000 --- a/internal/iam/repository/eventsourcing/model/mail_text_test.go +++ /dev/null @@ -1,134 +0,0 @@ -package model - -import ( - "encoding/json" - "testing" - - es_models "github.com/caos/zitadel/internal/eventstore/v1/models" -) - -func TestAppendAddMailTextEvent(t *testing.T) { - type args struct { - iam *IAM - mailText *MailText - event *es_models.Event - } - tests := []struct { - name string - args args - result *IAM - }{ - { - name: "append add mailText event", - args: args{ - iam: &IAM{}, - mailText: &MailText{ - MailTextType: "PasswordReset", - Language: "DE"}, - event: &es_models.Event{}, - }, - result: &IAM{DefaultMailTexts: []*MailText{&MailText{ - MailTextType: "PasswordReset", - Language: "DE"}}}, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if tt.args.mailText != nil { - data, _ := json.Marshal(tt.args.mailText) - tt.args.event.Data = data - } - tt.args.iam.appendAddMailTextEvent(tt.args.event) - if len(tt.args.iam.DefaultMailTexts) != 1 { - t.Errorf("got wrong result should have one mailText actual: %v ", len(tt.args.iam.DefaultMailTexts)) - } - if tt.args.iam.DefaultMailTexts[0] == tt.result.DefaultMailTexts[0] { - t.Errorf("got wrong result: expected: %v, actual: %v ", tt.result.DefaultMailTexts[0], tt.args.iam.DefaultMailTexts[0]) - } - }) - } -} - -func TestAppendChangeMailTextEvent(t *testing.T) { - type args struct { - iam *IAM - mailText *MailText - event *es_models.Event - } - tests := []struct { - name string - args args - result *IAM - }{ - { - name: "append change mailText event", - args: args{ - iam: &IAM{DefaultMailTexts: []*MailText{&MailText{ - MailTextType: "PasswordReset", - Language: "DE"}}}, - mailText: &MailText{ - MailTextType: "ChangedPasswordReset", - Language: "DE"}, - event: &es_models.Event{}, - }, - result: &IAM{DefaultMailTexts: []*MailText{&MailText{ - MailTextType: "PasswordReset", - Language: "ChangedDE"}}}, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if tt.args.mailText != nil { - data, _ := json.Marshal(tt.args.mailText) - tt.args.event.Data = data - } - tt.args.iam.appendChangeMailTextEvent(tt.args.event) - if len(tt.args.iam.DefaultMailTexts) != 1 { - t.Errorf("got wrong result should have one mailText actual: %v ", len(tt.args.iam.DefaultMailTexts)) - } - if tt.args.iam.DefaultMailTexts[0] == tt.result.DefaultMailTexts[0] { - t.Errorf("got wrong result: expected: %v, actual: %v ", tt.result.DefaultMailTexts[0], tt.args.iam.DefaultMailTexts[0]) - } - }) - } -} - -func TestAppendRemoveMailTextEvent(t *testing.T) { - type args struct { - iam *IAM - mailText *MailText - event *es_models.Event - } - tests := []struct { - name string - args args - result *IAM - }{ - { - name: "append remove mailText event", - args: args{ - iam: &IAM{DefaultMailTexts: []*MailText{&MailText{ - MailTextType: "PasswordReset", - Language: "DE", - Subject: "Subject"}}}, - mailText: &MailText{ - MailTextType: "PasswordReset", - Language: "DE"}, - event: &es_models.Event{}, - }, - result: &IAM{DefaultMailTexts: []*MailText{}}, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if tt.args.mailText != nil { - data, _ := json.Marshal(tt.args.mailText) - tt.args.event.Data = data - } - tt.args.iam.appendRemoveMailTextEvent(tt.args.event) - if len(tt.args.iam.DefaultMailTexts) != 0 { - t.Errorf("got wrong result should have no mailText actual: %v ", len(tt.args.iam.DefaultMailTexts)) - } - }) - } -} diff --git a/internal/iam/repository/eventsourcing/model/types.go b/internal/iam/repository/eventsourcing/model/types.go index 46a0cc4af4..d538aaed52 100644 --- a/internal/iam/repository/eventsourcing/model/types.go +++ b/internal/iam/repository/eventsourcing/model/types.go @@ -54,8 +54,9 @@ const ( MailTemplateAdded models.EventType = "iam.mail.template.added" MailTemplateChanged models.EventType = "iam.mail.template.changed" - MailTextAdded models.EventType = "iam.mail.text.added" - MailTextChanged models.EventType = "iam.mail.text.changed" + + CustomTextSet models.EventType = "iam.customtext.set" + CustomTextRemoved models.EventType = "iam.customtext.removed" PasswordComplexityPolicyAdded models.EventType = "iam.policy.password.complexity.added" PasswordComplexityPolicyChanged models.EventType = "iam.policy.password.complexity.changed" diff --git a/internal/iam/repository/view/mail_text_view.go b/internal/iam/repository/view/mail_text_view.go deleted file mode 100644 index ceb7ece1b6..0000000000 --- a/internal/iam/repository/view/mail_text_view.go +++ /dev/null @@ -1,54 +0,0 @@ -package view - -import ( - "github.com/caos/zitadel/internal/domain" - caos_errs "github.com/caos/zitadel/internal/errors" - iam_model "github.com/caos/zitadel/internal/iam/model" - "github.com/caos/zitadel/internal/iam/repository/view/model" - "github.com/caos/zitadel/internal/view/repository" - "github.com/jinzhu/gorm" - "strings" -) - -func GetMailTexts(db *gorm.DB, table string, aggregateID string) ([]*model.MailTextView, error) { - texts := make([]*model.MailTextView, 0) - queries := []*iam_model.MailTextSearchQuery{ - { - Key: iam_model.MailTextSearchKeyAggregateID, - Value: aggregateID, - Method: domain.SearchMethodEquals, - }, - } - query := repository.PrepareSearchQuery(table, model.MailTextSearchRequest{Queries: queries}) - _, err := query(db, &texts) - if err != nil { - return nil, err - } - return texts, nil -} - -func GetMailTextByIDs(db *gorm.DB, table, aggregateID string, textType string, language string) (*model.MailTextView, error) { - mailText := new(model.MailTextView) - aggregateIDQuery := &model.MailTextSearchQuery{Key: iam_model.MailTextSearchKeyAggregateID, Value: aggregateID, Method: domain.SearchMethodEquals} - textTypeQuery := &model.MailTextSearchQuery{Key: iam_model.MailTextSearchKeyMailTextType, Value: textType, Method: domain.SearchMethodEquals} - languageQuery := &model.MailTextSearchQuery{Key: iam_model.MailTextSearchKeyLanguage, Value: strings.ToUpper(language), Method: domain.SearchMethodEquals} - query := repository.PrepareGetByQuery(table, aggregateIDQuery, textTypeQuery, languageQuery) - err := query(db, mailText) - if caos_errs.IsNotFound(err) { - return nil, caos_errs.ThrowNotFound(nil, "VIEW-IiJjm", "Errors.IAM.MailText.NotExisting") - } - return mailText, err -} - -func PutMailText(db *gorm.DB, table string, mailText *model.MailTextView) error { - save := repository.PrepareSave(table) - return save(db, mailText) -} - -func DeleteMailText(db *gorm.DB, table, aggregateID string, textType string, language string) error { - aggregateIDSearch := repository.Key{Key: model.MailTextSearchKey(iam_model.MailTextSearchKeyAggregateID), Value: aggregateID} - textTypeSearch := repository.Key{Key: model.MailTextSearchKey(iam_model.MailTextSearchKeyMailTextType), Value: textType} - languageSearch := repository.Key{Key: model.MailTextSearchKey(iam_model.MailTextSearchKeyLanguage), Value: language} - delete := repository.PrepareDeleteByKeys(table, aggregateIDSearch, textTypeSearch, languageSearch) - return delete(db) -} diff --git a/internal/iam/repository/view/message_text_view.go b/internal/iam/repository/view/message_text_view.go new file mode 100644 index 0000000000..345664904f --- /dev/null +++ b/internal/iam/repository/view/message_text_view.go @@ -0,0 +1,54 @@ +package view + +import ( + "github.com/jinzhu/gorm" + + "github.com/caos/zitadel/internal/domain" + caos_errs "github.com/caos/zitadel/internal/errors" + iam_model "github.com/caos/zitadel/internal/iam/model" + "github.com/caos/zitadel/internal/iam/repository/view/model" + "github.com/caos/zitadel/internal/view/repository" +) + +func GetMessageTexts(db *gorm.DB, table string, aggregateID string) ([]*model.MessageTextView, error) { + texts := make([]*model.MessageTextView, 0) + queries := []*iam_model.MessageTextSearchQuery{ + { + Key: iam_model.MessageTextSearchKeyAggregateID, + Value: aggregateID, + Method: domain.SearchMethodEquals, + }, + } + query := repository.PrepareSearchQuery(table, model.MessageTextSearchRequest{Queries: queries}) + _, err := query(db, &texts) + if err != nil { + return nil, err + } + return texts, nil +} + +func GetMessageTextByIDs(db *gorm.DB, table, aggregateID, textType, lang string) (*model.MessageTextView, error) { + mailText := new(model.MessageTextView) + aggregateIDQuery := &model.MessageTextSearchQuery{Key: iam_model.MessageTextSearchKeyAggregateID, Value: aggregateID, Method: domain.SearchMethodEquals} + textTypeQuery := &model.MessageTextSearchQuery{Key: iam_model.MessageTextSearchKeyMessageTextType, Value: textType, Method: domain.SearchMethodEquals} + languageQuery := &model.MessageTextSearchQuery{Key: iam_model.MessageTextSearchKeyLanguage, Value: lang, Method: domain.SearchMethodEquals} + query := repository.PrepareGetByQuery(table, aggregateIDQuery, textTypeQuery, languageQuery) + err := query(db, mailText) + if caos_errs.IsNotFound(err) { + return nil, caos_errs.ThrowNotFound(nil, "VIEW-IiJjm", "Errors.IAM.CustomMessageText.NotExisting") + } + return mailText, err +} + +func PutMessageText(db *gorm.DB, table string, mailText *model.MessageTextView) error { + save := repository.PrepareSave(table) + return save(db, mailText) +} + +func DeleteMessageText(db *gorm.DB, table, aggregateID, textType, lang string) error { + aggregateIDSearch := repository.Key{Key: model.MessageTextSearchKey(iam_model.MessageTextSearchKeyAggregateID), Value: aggregateID} + textTypeSearch := repository.Key{Key: model.MessageTextSearchKey(iam_model.MessageTextSearchKeyMessageTextType), Value: textType} + languageSearch := repository.Key{Key: model.MessageTextSearchKey(iam_model.MessageTextSearchKeyLanguage), Value: lang} + delete := repository.PrepareDeleteByKeys(table, aggregateIDSearch, textTypeSearch, languageSearch) + return delete(db) +} diff --git a/internal/iam/repository/view/model/mail_text.go b/internal/iam/repository/view/model/mail_text.go deleted file mode 100644 index 6812ec8473..0000000000 --- a/internal/iam/repository/view/model/mail_text.go +++ /dev/null @@ -1,117 +0,0 @@ -package model - -import ( - "encoding/json" - "time" - - org_es_model "github.com/caos/zitadel/internal/org/repository/eventsourcing/model" - - es_model "github.com/caos/zitadel/internal/iam/repository/eventsourcing/model" - - "github.com/caos/logging" - caos_errs "github.com/caos/zitadel/internal/errors" - "github.com/caos/zitadel/internal/eventstore/v1/models" - "github.com/caos/zitadel/internal/iam/model" -) - -const ( - MailTextKeyAggregateID = "aggregate_id" - MailTextKeyMailTextType = "mail_text_type" - MailTextKeyLanguage = "language" -) - -type MailTextView struct { - AggregateID string `json:"-" gorm:"column:aggregate_id;primary_key"` - CreationDate time.Time `json:"-" gorm:"column:creation_date"` - ChangeDate time.Time `json:"-" gorm:"column:change_date"` - State int32 `json:"-" gorm:"column:mail_text_state"` - - MailTextType string `json:"mailTextType" gorm:"column:mail_text_type;primary_key"` - Language string `json:"language" gorm:"column:language;primary_key"` - Title string `json:"title" gorm:"column:title"` - PreHeader string `json:"preHeader" gorm:"column:pre_header"` - Subject string `json:"subject" gorm:"column:subject"` - Greeting string `json:"greeting" gorm:"column:greeting"` - Text string `json:"text" gorm:"column:text"` - ButtonText string `json:"buttonText" gorm:"column:button_text"` - Default bool `json:"-" gorm:"-"` - - Sequence uint64 `json:"-" gorm:"column:sequence"` -} - -func MailTextViewFromModel(template *model.MailTextView) *MailTextView { - return &MailTextView{ - AggregateID: template.AggregateID, - Sequence: template.Sequence, - CreationDate: template.CreationDate, - ChangeDate: template.ChangeDate, - MailTextType: template.MailTextType, - Language: template.Language, - Title: template.Title, - PreHeader: template.PreHeader, - Subject: template.Subject, - Greeting: template.Greeting, - Text: template.Text, - ButtonText: template.ButtonText, - Default: template.Default, - } -} - -func MailTextsViewToModel(textsIn []*MailTextView, defaultIn bool) *model.MailTextsView { - return &model.MailTextsView{ - Texts: mailTextsViewToModelArr(textsIn, defaultIn), - } -} - -func mailTextsViewToModelArr(texts []*MailTextView, defaultIn bool) []*model.MailTextView { - result := make([]*model.MailTextView, len(texts)) - for i, r := range texts { - r.Default = defaultIn - result[i] = MailTextViewToModel(r) - } - return result -} - -func MailTextViewToModel(template *MailTextView) *model.MailTextView { - return &model.MailTextView{ - AggregateID: template.AggregateID, - Sequence: template.Sequence, - CreationDate: template.CreationDate, - ChangeDate: template.ChangeDate, - MailTextType: template.MailTextType, - Language: template.Language, - Title: template.Title, - PreHeader: template.PreHeader, - Subject: template.Subject, - Greeting: template.Greeting, - Text: template.Text, - ButtonText: template.ButtonText, - Default: template.Default, - } -} - -func (i *MailTextView) AppendEvent(event *models.Event) (err error) { - i.Sequence = event.Sequence - switch event.Type { - case es_model.MailTextAdded, org_es_model.MailTextAdded: - i.setRootData(event) - i.CreationDate = event.CreationDate - err = i.SetData(event) - case es_model.MailTextChanged, org_es_model.MailTextChanged: - i.ChangeDate = event.CreationDate - err = i.SetData(event) - } - return err -} - -func (r *MailTextView) setRootData(event *models.Event) { - r.AggregateID = event.AggregateID -} - -func (r *MailTextView) SetData(event *models.Event) error { - if err := json.Unmarshal(event.Data, r); err != nil { - logging.Log("MODEL-UFqAG").WithError(err).Error("could not unmarshal event data") - return caos_errs.ThrowInternal(err, "MODEL-5CVaR", "Could not unmarshal data") - } - return nil -} diff --git a/internal/iam/repository/view/model/mail_text_query.go b/internal/iam/repository/view/model/mail_text_query.go deleted file mode 100644 index 6f3da80765..0000000000 --- a/internal/iam/repository/view/model/mail_text_query.go +++ /dev/null @@ -1,63 +0,0 @@ -package model - -import ( - "github.com/caos/zitadel/internal/domain" - iam_model "github.com/caos/zitadel/internal/iam/model" - "github.com/caos/zitadel/internal/view/repository" -) - -type MailTextSearchRequest iam_model.MailTextSearchRequest -type MailTextSearchQuery iam_model.MailTextSearchQuery -type MailTextSearchKey iam_model.MailTextSearchKey - -func (req MailTextSearchRequest) GetLimit() uint64 { - return req.Limit -} - -func (req MailTextSearchRequest) GetOffset() uint64 { - return req.Offset -} - -func (req MailTextSearchRequest) GetSortingColumn() repository.ColumnKey { - if req.SortingColumn == iam_model.MailTextSearchKeyUnspecified { - return nil - } - return MailTextSearchKey(req.SortingColumn) -} - -func (req MailTextSearchRequest) GetAsc() bool { - return req.Asc -} - -func (req MailTextSearchRequest) GetQueries() []repository.SearchQuery { - result := make([]repository.SearchQuery, len(req.Queries)) - for i, q := range req.Queries { - result[i] = MailTextSearchQuery{Key: q.Key, Value: q.Value, Method: q.Method} - } - return result -} - -func (req MailTextSearchQuery) GetKey() repository.ColumnKey { - return MailTextSearchKey(req.Key) -} - -func (req MailTextSearchQuery) GetMethod() domain.SearchMethod { - return req.Method -} - -func (req MailTextSearchQuery) GetValue() interface{} { - return req.Value -} - -func (key MailTextSearchKey) ToColumnName() string { - switch iam_model.MailTextSearchKey(key) { - case iam_model.MailTextSearchKeyAggregateID: - return MailTextKeyAggregateID - case iam_model.MailTextSearchKeyMailTextType: - return MailTextKeyMailTextType - case iam_model.MailTextSearchKeyLanguage: - return MailTextKeyLanguage - default: - return "" - } -} diff --git a/internal/iam/repository/view/model/message_text.go b/internal/iam/repository/view/model/message_text.go new file mode 100644 index 0000000000..82253fdba5 --- /dev/null +++ b/internal/iam/repository/view/model/message_text.go @@ -0,0 +1,184 @@ +package model + +import ( + "encoding/json" + "time" + + "golang.org/x/text/language" + + "github.com/caos/zitadel/internal/domain" + org_es_model "github.com/caos/zitadel/internal/org/repository/eventsourcing/model" + + es_model "github.com/caos/zitadel/internal/iam/repository/eventsourcing/model" + + "github.com/caos/logging" + + caos_errs "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/eventstore/v1/models" + "github.com/caos/zitadel/internal/iam/model" +) + +const ( + MessageTextKeyAggregateID = "aggregate_id" + MessageTextKeyMessageTextType = "message_text_type" + MessageTextKeyLanguage = "language" +) + +type MessageTextView struct { + AggregateID string `json:"-" gorm:"column:aggregate_id;primary_key"` + CreationDate time.Time `json:"-" gorm:"column:creation_date"` + ChangeDate time.Time `json:"-" gorm:"column:change_date"` + State int32 `json:"-" gorm:"column:message_text_state"` + + MessageTextType string `json:"-" gorm:"column:message_text_type;primary_key"` + Language string `json:"-" gorm:"column:language;primary_key"` + Title string `json:"-" gorm:"column:title"` + PreHeader string `json:"-" gorm:"column:pre_header"` + Subject string `json:"-" gorm:"column:subject"` + Greeting string `json:"-" gorm:"column:greeting"` + Text string `json:"-" gorm:"column:text"` + ButtonText string `json:"-" gorm:"column:button_text"` + FooterText string `json:"-" gorm:"column:footer_text"` + Default bool `json:"-" gorm:"-"` + + Sequence uint64 `json:"-" gorm:"column:sequence"` +} + +func MessageTextViewFromModel(template *model.MessageTextView) *MessageTextView { + return &MessageTextView{ + AggregateID: template.AggregateID, + Sequence: template.Sequence, + CreationDate: template.CreationDate, + ChangeDate: template.ChangeDate, + MessageTextType: template.MessageTextType, + Language: template.Language.String(), + Title: template.Title, + PreHeader: template.PreHeader, + Subject: template.Subject, + Greeting: template.Greeting, + Text: template.Text, + ButtonText: template.ButtonText, + FooterText: template.FooterText, + Default: template.Default, + } +} + +func MessageTextsViewToModel(textsIn []*MessageTextView, defaultIn bool) *model.MessageTextsView { + return &model.MessageTextsView{ + Texts: messageTextsViewToModelArr(textsIn, defaultIn), + } +} + +func messageTextsViewToModelArr(texts []*MessageTextView, defaultIn bool) []*model.MessageTextView { + result := make([]*model.MessageTextView, len(texts)) + for i, r := range texts { + r.Default = defaultIn + result[i] = MessageTextViewToModel(r) + } + return result +} + +func MessageTextViewToModel(template *MessageTextView) *model.MessageTextView { + lang := language.Make(template.Language) + return &model.MessageTextView{ + AggregateID: template.AggregateID, + Sequence: template.Sequence, + CreationDate: template.CreationDate, + ChangeDate: template.ChangeDate, + MessageTextType: template.MessageTextType, + Language: lang, + Title: template.Title, + PreHeader: template.PreHeader, + Subject: template.Subject, + Greeting: template.Greeting, + Text: template.Text, + ButtonText: template.ButtonText, + FooterText: template.FooterText, + Default: template.Default, + } +} + +func (i *MessageTextView) AppendEvent(event *models.Event) (err error) { + i.Sequence = event.Sequence + switch event.Type { + case es_model.CustomTextSet, org_es_model.CustomTextSet: + i.setRootData(event) + customText := new(CustomText) + err = customText.SetData(event) + if err != nil { + return err + } + if customText.Key == domain.MessageTitle { + i.Title = customText.Text + } + if customText.Key == domain.MessagePreHeader { + i.PreHeader = customText.Text + } + if customText.Key == domain.MessageSubject { + i.Subject = customText.Text + } + if customText.Key == domain.MessageGreeting { + i.Greeting = customText.Text + } + if customText.Key == domain.MessageText { + i.Text = customText.Text + } + if customText.Key == domain.MessageButtonText { + i.ButtonText = customText.Text + } + if customText.Key == domain.MessageFooterText { + i.FooterText = customText.Text + } + i.ChangeDate = event.CreationDate + case es_model.CustomTextRemoved, org_es_model.CustomTextRemoved: + customText := new(CustomText) + err = customText.SetData(event) + if err != nil { + return err + } + if customText.Key == domain.MessageTitle { + i.Title = "" + } + if customText.Key == domain.MessagePreHeader { + i.PreHeader = "" + } + if customText.Key == domain.MessageSubject { + i.Subject = "" + } + if customText.Key == domain.MessageGreeting { + i.Greeting = "" + } + if customText.Key == domain.MessageText { + i.Text = "" + } + if customText.Key == domain.MessageButtonText { + i.ButtonText = "" + } + if customText.Key == domain.MessageFooterText { + i.FooterText = "" + } + i.ChangeDate = event.CreationDate + case org_es_model.CustomTextMessageRemoved: + i.State = int32(model.PolicyStateRemoved) + } + return err +} + +func (r *MessageTextView) setRootData(event *models.Event) { + r.AggregateID = event.AggregateID +} + +type CustomText struct { + Template string `json:"template"` + Key string `json:"key"` + Language language.Tag `json:"language"` + Text string `json:"text"` +} + +func (r *CustomText) SetData(event *models.Event) error { + if err := json.Unmarshal(event.Data, r); err != nil { + logging.Log("MODEL-3n9fs").WithError(err).Error("could not unmarshal event data") + return caos_errs.ThrowInternal(err, "MODEL-5CVaR", "Could not unmarshal data") + } + return nil +} diff --git a/internal/iam/repository/view/model/message_text_query.go b/internal/iam/repository/view/model/message_text_query.go new file mode 100644 index 0000000000..90f120ddd3 --- /dev/null +++ b/internal/iam/repository/view/model/message_text_query.go @@ -0,0 +1,63 @@ +package model + +import ( + "github.com/caos/zitadel/internal/domain" + iam_model "github.com/caos/zitadel/internal/iam/model" + "github.com/caos/zitadel/internal/view/repository" +) + +type MessageTextSearchRequest iam_model.MessageTextSearchRequest +type MessageTextSearchQuery iam_model.MessageTextSearchQuery +type MessageTextSearchKey iam_model.MessageTextSearchKey + +func (req MessageTextSearchRequest) GetLimit() uint64 { + return req.Limit +} + +func (req MessageTextSearchRequest) GetOffset() uint64 { + return req.Offset +} + +func (req MessageTextSearchRequest) GetSortingColumn() repository.ColumnKey { + if req.SortingColumn == iam_model.MessageTextSearchKeyUnspecified { + return nil + } + return MessageTextSearchKey(req.SortingColumn) +} + +func (req MessageTextSearchRequest) GetAsc() bool { + return req.Asc +} + +func (req MessageTextSearchRequest) GetQueries() []repository.SearchQuery { + result := make([]repository.SearchQuery, len(req.Queries)) + for i, q := range req.Queries { + result[i] = MessageTextSearchQuery{Key: q.Key, Value: q.Value, Method: q.Method} + } + return result +} + +func (req MessageTextSearchQuery) GetKey() repository.ColumnKey { + return MessageTextSearchKey(req.Key) +} + +func (req MessageTextSearchQuery) GetMethod() domain.SearchMethod { + return req.Method +} + +func (req MessageTextSearchQuery) GetValue() interface{} { + return req.Value +} + +func (key MessageTextSearchKey) ToColumnName() string { + switch iam_model.MessageTextSearchKey(key) { + case iam_model.MessageTextSearchKeyAggregateID: + return MessageTextKeyAggregateID + case iam_model.MessageTextSearchKeyMessageTextType: + return MessageTextKeyMessageTextType + case iam_model.MessageTextSearchKeyLanguage: + return MessageTextKeyLanguage + default: + return "" + } +} diff --git a/internal/management/repository/eventsourcing/eventstore/org.go b/internal/management/repository/eventsourcing/eventstore/org.go index 852be5df17..cfc26963e5 100644 --- a/internal/management/repository/eventsourcing/eventstore/org.go +++ b/internal/management/repository/eventsourcing/eventstore/org.go @@ -548,19 +548,19 @@ func (repo *OrgRepository) GetMailTemplate(ctx context.Context) (*iam_model.Mail return iam_es_model.MailTemplateViewToModel(template), err } -func (repo *OrgRepository) GetDefaultMailTexts(ctx context.Context) (*iam_model.MailTextsView, error) { - texts, err := repo.View.MailTextsByAggregateID(repo.SystemDefaults.IamID) +func (repo *OrgRepository) GetDefaultMessageTexts(ctx context.Context) (*iam_model.MessageTextsView, error) { + texts, err := repo.View.MessageTextsByAggregateID(repo.SystemDefaults.IamID) if err != nil { return nil, err } - return iam_es_model.MailTextsViewToModel(texts, true), err + return iam_es_model.MessageTextsViewToModel(texts, true), err } -func (repo *OrgRepository) GetMailTexts(ctx context.Context) (*iam_model.MailTextsView, error) { +func (repo *OrgRepository) GetMessageTexts(ctx context.Context) (*iam_model.MessageTextsView, error) { defaultIn := false - texts, err := repo.View.MailTextsByAggregateID(authz.GetCtxData(ctx).OrgID) + texts, err := repo.View.MessageTextsByAggregateID(authz.GetCtxData(ctx).OrgID) if errors.IsNotFound(err) || len(texts) == 0 { - texts, err = repo.View.MailTextsByAggregateID(repo.SystemDefaults.IamID) + texts, err = repo.View.MessageTextsByAggregateID(repo.SystemDefaults.IamID) if err != nil { return nil, err } @@ -569,7 +569,31 @@ func (repo *OrgRepository) GetMailTexts(ctx context.Context) (*iam_model.MailTex if err != nil { return nil, err } - return iam_es_model.MailTextsViewToModel(texts, defaultIn), err + return iam_es_model.MessageTextsViewToModel(texts, defaultIn), err +} + +func (repo *OrgRepository) GetDefaultMessageText(ctx context.Context, textType, lang string) (*iam_model.MessageTextView, error) { + text, err := repo.View.MessageTextByIDs(repo.SystemDefaults.IamID, textType, lang) + if err != nil { + return nil, err + } + text.Default = true + return iam_es_model.MessageTextViewToModel(text), err +} + +func (repo *OrgRepository) GetMessageText(ctx context.Context, orgID, textType, lang string) (*iam_model.MessageTextView, error) { + text, err := repo.View.MessageTextByIDs(orgID, textType, lang) + if errors.IsNotFound(err) { + result, err := repo.GetDefaultMessageText(ctx, textType, lang) + if err != nil { + return nil, err + } + return result, nil + } + if err != nil { + return nil, err + } + return iam_es_model.MessageTextViewToModel(text), err } func (repo *OrgRepository) getOrgChanges(ctx context.Context, orgID string, lastSequence uint64, limit uint64, sortAscending bool, auditLogRetention time.Duration) (*org_model.OrgChanges, error) { diff --git a/internal/management/repository/eventsourcing/handler/handler.go b/internal/management/repository/eventsourcing/handler/handler.go index 1f063b2aa2..1610db3d36 100644 --- a/internal/management/repository/eventsourcing/handler/handler.go +++ b/internal/management/repository/eventsourcing/handler/handler.go @@ -77,8 +77,8 @@ func Register(configs Configs, bulkLimit, errorCount uint64, view *view.View, es handler{view, bulkLimit, configs.cycleDuration("OrgIAMPolicy"), errorCount, es}), newMailTemplate( handler{view, bulkLimit, configs.cycleDuration("MailTemplate"), errorCount, es}), - newMailText( - handler{view, bulkLimit, configs.cycleDuration("MailText"), errorCount, es}), + newMessageText( + handler{view, bulkLimit, configs.cycleDuration("MessageText"), errorCount, es}), newFeatures( handler{view, bulkLimit, configs.cycleDuration("Features"), errorCount, es}), } diff --git a/internal/management/repository/eventsourcing/handler/mail_text.go b/internal/management/repository/eventsourcing/handler/mail_text.go deleted file mode 100644 index c7ac8418d2..0000000000 --- a/internal/management/repository/eventsourcing/handler/mail_text.go +++ /dev/null @@ -1,111 +0,0 @@ -package handler - -import ( - "github.com/caos/logging" - "github.com/caos/zitadel/internal/eventstore/v1" - es_models "github.com/caos/zitadel/internal/eventstore/v1/models" - "github.com/caos/zitadel/internal/eventstore/v1/query" - "github.com/caos/zitadel/internal/eventstore/v1/spooler" - iam_es_model "github.com/caos/zitadel/internal/iam/repository/eventsourcing/model" - iam_model "github.com/caos/zitadel/internal/iam/repository/view/model" - "github.com/caos/zitadel/internal/org/repository/eventsourcing/model" -) - -type MailText struct { - handler - subscription *v1.Subscription -} - -func newMailText(handler handler) *MailText { - h := &MailText{ - handler: handler, - } - - h.subscribe() - - return h -} - -func (m *MailText) subscribe() { - m.subscription = m.es.Subscribe(m.AggregateTypes()...) - go func() { - for event := range m.subscription.Events { - query.ReduceEvent(m, event) - } - }() -} - -const ( - mailTextTable = "management.mail_texts" -) - -func (m *MailText) ViewModel() string { - return mailTextTable -} - -func (_ *MailText) AggregateTypes() []es_models.AggregateType { - return []es_models.AggregateType{model.OrgAggregate, iam_es_model.IAMAggregate} -} - -func (p *MailText) CurrentSequence() (uint64, error) { - sequence, err := p.view.GetLatestMailTextSequence() - if err != nil { - return 0, err - } - return sequence.CurrentSequence, nil -} - -func (m *MailText) EventQuery() (*es_models.SearchQuery, error) { - sequence, err := m.view.GetLatestMailTextSequence() - if err != nil { - return nil, err - } - return es_models.NewSearchQuery(). - AggregateTypeFilter(m.AggregateTypes()...). - LatestSequenceFilter(sequence.CurrentSequence), nil -} - -func (m *MailText) Reduce(event *es_models.Event) (err error) { - switch event.AggregateType { - case model.OrgAggregate, iam_es_model.IAMAggregate: - err = m.processMailText(event) - } - return err -} - -func (m *MailText) processMailText(event *es_models.Event) (err error) { - text := new(iam_model.MailTextView) - switch event.Type { - case iam_es_model.MailTextAdded, model.MailTextAdded: - err = text.AppendEvent(event) - case iam_es_model.MailTextChanged, model.MailTextChanged: - err = text.SetData(event) - if err != nil { - return err - } - text, err = m.view.MailTextByIDs(event.AggregateID, text.MailTextType, text.Language) - if err != nil { - return err - } - text.ChangeDate = event.CreationDate - err = text.AppendEvent(event) - case model.MailTextRemoved: - err = text.SetData(event) - return m.view.DeleteMailText(event.AggregateID, text.MailTextType, text.Language, event) - default: - return m.view.ProcessedMailTextSequence(event) - } - if err != nil { - return err - } - return m.view.PutMailText(text, event) -} - -func (m *MailText) OnError(event *es_models.Event, err error) error { - logging.LogWithFields("SPOOL-4Djo9", "id", event.AggregateID).WithError(err).Warn("something went wrong in label text handler") - return spooler.HandleError(event, err, m.view.GetLatestMailTextFailedEvent, m.view.ProcessedMailTextFailedEvent, m.view.ProcessedMailTextSequence, m.errorCountUntilSkip) -} - -func (o *MailText) OnSuccess() error { - return spooler.HandleSuccess(o.view.UpdateMailTextSpoolerRunTimestamp) -} diff --git a/internal/management/repository/eventsourcing/handler/message_text.go b/internal/management/repository/eventsourcing/handler/message_text.go new file mode 100644 index 0000000000..4943514484 --- /dev/null +++ b/internal/management/repository/eventsourcing/handler/message_text.go @@ -0,0 +1,122 @@ +package handler + +import ( + "github.com/caos/logging" + caos_errs "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/eventstore/v1" + es_models "github.com/caos/zitadel/internal/eventstore/v1/models" + "github.com/caos/zitadel/internal/eventstore/v1/query" + "github.com/caos/zitadel/internal/eventstore/v1/spooler" + iam_es_model "github.com/caos/zitadel/internal/iam/repository/eventsourcing/model" + iam_model "github.com/caos/zitadel/internal/iam/repository/view/model" + "github.com/caos/zitadel/internal/org/repository/eventsourcing/model" +) + +type MessageText struct { + handler + subscription *v1.Subscription +} + +func newMessageText(handler handler) *MessageText { + h := &MessageText{ + handler: handler, + } + + h.subscribe() + + return h +} + +func (m *MessageText) subscribe() { + m.subscription = m.es.Subscribe(m.AggregateTypes()...) + go func() { + for event := range m.subscription.Events { + query.ReduceEvent(m, event) + } + }() +} + +const ( + messageTextTable = "management.message_texts" +) + +func (m *MessageText) ViewModel() string { + return messageTextTable +} + +func (_ *MessageText) AggregateTypes() []es_models.AggregateType { + return []es_models.AggregateType{model.OrgAggregate, iam_es_model.IAMAggregate} +} + +func (p *MessageText) CurrentSequence() (uint64, error) { + sequence, err := p.view.GetLatestMessageTextSequence() + if err != nil { + return 0, err + } + return sequence.CurrentSequence, nil +} + +func (m *MessageText) EventQuery() (*es_models.SearchQuery, error) { + sequence, err := m.view.GetLatestMessageTextSequence() + if err != nil { + return nil, err + } + return es_models.NewSearchQuery(). + AggregateTypeFilter(m.AggregateTypes()...). + LatestSequenceFilter(sequence.CurrentSequence), nil +} + +func (m *MessageText) Reduce(event *es_models.Event) (err error) { + switch event.AggregateType { + case model.OrgAggregate, iam_es_model.IAMAggregate: + err = m.processMessageText(event) + } + return err +} + +func (m *MessageText) processMessageText(event *es_models.Event) (err error) { + message := new(iam_model.MessageTextView) + switch event.Type { + case iam_es_model.CustomTextSet, model.CustomTextSet, + iam_es_model.CustomTextRemoved, model.CustomTextRemoved: + text := new(iam_model.CustomText) + err = text.SetData(event) + if err != nil { + return err + } + message, err = m.view.MessageTextByIDs(event.AggregateID, text.Template, text.Language.String()) + if err != nil && !caos_errs.IsNotFound(err) { + return err + } + if caos_errs.IsNotFound(err) { + err = nil + message = new(iam_model.MessageTextView) + message.Language = text.Language.String() + message.MessageTextType = text.Template + message.CreationDate = event.CreationDate + } + err = message.AppendEvent(event) + case model.CustomTextMessageRemoved: + text := new(iam_model.CustomText) + err = text.SetData(event) + if err != nil { + return err + } + return m.view.DeleteMessageText(event.AggregateID, text.Template, text.Language.String(), event) + default: + return m.view.ProcessedMessageTextSequence(event) + } + if err != nil { + return err + } + return m.view.PutMessageText(message, event) +} + +func (m *MessageText) OnError(event *es_models.Event, err error) error { + logging.LogWithFields("SPOOL-4Djo9", "id", event.AggregateID).WithError(err).Warn("something went wrong in label text handler") + return spooler.HandleError(event, err, m.view.GetLatestMessageTextFailedEvent, m.view.ProcessedMessageTextFailedEvent, m.view.ProcessedMessageTextSequence, m.errorCountUntilSkip) +} + +func (o *MessageText) OnSuccess() error { + return spooler.HandleSuccess(o.view.UpdateMessageTextSpoolerRunTimestamp) +} diff --git a/internal/management/repository/eventsourcing/view/mail_texts.go b/internal/management/repository/eventsourcing/view/mail_texts.go deleted file mode 100644 index 7cae1a41d3..0000000000 --- a/internal/management/repository/eventsourcing/view/mail_texts.go +++ /dev/null @@ -1,57 +0,0 @@ -package view - -import ( - "github.com/caos/zitadel/internal/errors" - "github.com/caos/zitadel/internal/eventstore/v1/models" - "github.com/caos/zitadel/internal/iam/repository/view" - "github.com/caos/zitadel/internal/iam/repository/view/model" - global_view "github.com/caos/zitadel/internal/view/repository" -) - -const ( - mailTextTable = "management.mail_texts" -) - -func (v *View) MailTextsByAggregateID(aggregateID string) ([]*model.MailTextView, error) { - return view.GetMailTexts(v.Db, mailTextTable, aggregateID) -} - -func (v *View) MailTextByIDs(aggregateID string, textType string, language string) (*model.MailTextView, error) { - return view.GetMailTextByIDs(v.Db, mailTextTable, aggregateID, textType, language) -} - -func (v *View) PutMailText(template *model.MailTextView, event *models.Event) error { - err := view.PutMailText(v.Db, mailTextTable, template) - if err != nil { - return err - } - return v.ProcessedMailTextSequence(event) -} - -func (v *View) DeleteMailText(aggregateID string, textType string, language string, event *models.Event) error { - err := view.DeleteMailText(v.Db, mailTextTable, aggregateID, textType, language) - if err != nil && !errors.IsNotFound(err) { - return err - } - return v.ProcessedMailTextSequence(event) -} - -func (v *View) GetLatestMailTextSequence() (*global_view.CurrentSequence, error) { - return v.latestSequence(mailTextTable) -} - -func (v *View) ProcessedMailTextSequence(event *models.Event) error { - return v.saveCurrentSequence(mailTextTable, event) -} - -func (v *View) UpdateMailTextSpoolerRunTimestamp() error { - return v.updateSpoolerRunSequence(mailTextTable) -} - -func (v *View) GetLatestMailTextFailedEvent(sequence uint64) (*global_view.FailedEvent, error) { - return v.latestFailedEvent(mailTextTable, sequence) -} - -func (v *View) ProcessedMailTextFailedEvent(failedEvent *global_view.FailedEvent) error { - return v.saveFailedEvent(failedEvent) -} diff --git a/internal/management/repository/eventsourcing/view/message_texts.go b/internal/management/repository/eventsourcing/view/message_texts.go new file mode 100644 index 0000000000..05345936a6 --- /dev/null +++ b/internal/management/repository/eventsourcing/view/message_texts.go @@ -0,0 +1,57 @@ +package view + +import ( + "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/eventstore/v1/models" + "github.com/caos/zitadel/internal/iam/repository/view" + "github.com/caos/zitadel/internal/iam/repository/view/model" + global_view "github.com/caos/zitadel/internal/view/repository" +) + +const ( + messageTextTable = "management.message_texts" +) + +func (v *View) MessageTextsByAggregateID(aggregateID string) ([]*model.MessageTextView, error) { + return view.GetMessageTexts(v.Db, messageTextTable, aggregateID) +} + +func (v *View) MessageTextByIDs(aggregateID, textType, lang string) (*model.MessageTextView, error) { + return view.GetMessageTextByIDs(v.Db, messageTextTable, aggregateID, textType, lang) +} + +func (v *View) PutMessageText(template *model.MessageTextView, event *models.Event) error { + err := view.PutMessageText(v.Db, messageTextTable, template) + if err != nil { + return err + } + return v.ProcessedMessageTextSequence(event) +} + +func (v *View) DeleteMessageText(aggregateID, textType, lang string, event *models.Event) error { + err := view.DeleteMessageText(v.Db, messageTextTable, aggregateID, textType, lang) + if err != nil && !errors.IsNotFound(err) { + return err + } + return v.ProcessedMessageTextSequence(event) +} + +func (v *View) GetLatestMessageTextSequence() (*global_view.CurrentSequence, error) { + return v.latestSequence(messageTextTable) +} + +func (v *View) ProcessedMessageTextSequence(event *models.Event) error { + return v.saveCurrentSequence(messageTextTable, event) +} + +func (v *View) UpdateMessageTextSpoolerRunTimestamp() error { + return v.updateSpoolerRunSequence(messageTextTable) +} + +func (v *View) GetLatestMessageTextFailedEvent(sequence uint64) (*global_view.FailedEvent, error) { + return v.latestFailedEvent(messageTextTable, sequence) +} + +func (v *View) ProcessedMessageTextFailedEvent(failedEvent *global_view.FailedEvent) error { + return v.saveFailedEvent(failedEvent) +} diff --git a/internal/management/repository/org.go b/internal/management/repository/org.go index bae912a8af..a0e3c5a22a 100644 --- a/internal/management/repository/org.go +++ b/internal/management/repository/org.go @@ -44,8 +44,10 @@ type OrgRepository interface { GetDefaultMailTemplate(ctx context.Context) (*iam_model.MailTemplateView, error) GetMailTemplate(ctx context.Context) (*iam_model.MailTemplateView, error) - GetDefaultMailTexts(ctx context.Context) (*iam_model.MailTextsView, error) - GetMailTexts(ctx context.Context) (*iam_model.MailTextsView, error) + GetDefaultMessageTexts(ctx context.Context) (*iam_model.MessageTextsView, error) + GetMessageTexts(ctx context.Context) (*iam_model.MessageTextsView, error) + GetDefaultMessageText(ctx context.Context, textType string, language string) (*iam_model.MessageTextView, error) + GetMessageText(ctx context.Context, orgID, textType, language string) (*iam_model.MessageTextView, error) GetLabelPolicy(ctx context.Context) (*iam_model.LabelPolicyView, error) GetPreviewLabelPolicy(ctx context.Context) (*iam_model.LabelPolicyView, error) diff --git a/internal/notification/repository/eventsourcing/handler/notification.go b/internal/notification/repository/eventsourcing/handler/notification.go index f42312226f..9802bd7e2c 100644 --- a/internal/notification/repository/eventsourcing/handler/notification.go +++ b/internal/notification/repository/eventsourcing/handler/notification.go @@ -29,19 +29,19 @@ import ( ) const ( - notificationTable = "notification.notifications" - NotifyUserID = "NOTIFICATION" - labelPolicyTableOrg = "management.label_policies" - labelPolicyTableDef = "adminapi.label_policies" - mailTemplateTableOrg = "management.mail_templates" - mailTemplateTableDef = "adminapi.mail_templates" - mailTextTableOrg = "management.mail_texts" - mailTextTableDef = "adminapi.mail_texts" - mailTextTypeDomainClaimed = "DomainClaimed" - mailTextTypeInitCode = "InitCode" - mailTextTypePasswordReset = "PasswordReset" - mailTextTypeVerifyEmail = "VerifyEmail" - mailTextTypeVerifyPhone = "VerifyPhone" + notificationTable = "notification.notifications" + NotifyUserID = "NOTIFICATION" + labelPolicyTableOrg = "management.label_policies" + labelPolicyTableDef = "adminapi.label_policies" + mailTemplateTableOrg = "management.mail_templates" + mailTemplateTableDef = "adminapi.mail_templates" + messageTextTableOrg = "management.message_texts" + messageTextTableDef = "adminapi.message_texts" + messageTextTypeDomainClaimed = "DomainClaimed" + messageTextTypeInitCode = "InitCode" + messageTextTypePasswordReset = "PasswordReset" + messageTextTypeVerifyEmail = "VerifyEmail" + messageTextTypeVerifyPhone = "VerifyPhone" ) type Notification struct { @@ -146,7 +146,6 @@ func (n *Notification) handleInitUserCode(event *models.Event) (err error) { if err != nil || alreadyHandled { return err } - ctx := getSetNotifyContextData(event.ResourceOwner) colors, err := n.getLabelPolicy(ctx) if err != nil { @@ -163,7 +162,7 @@ func (n *Notification) handleInitUserCode(event *models.Event) (err error) { return err } - text, err := n.getMailText(ctx, mailTextTypeInitCode, user.PreferredLanguage) + text, err := n.getMessageText(user, messageTextTypeInitCode, user.PreferredLanguage) if err != nil { return err } @@ -202,7 +201,7 @@ func (n *Notification) handlePasswordCode(event *models.Event) (err error) { return err } - text, err := n.getMailText(ctx, mailTextTypePasswordReset, user.PreferredLanguage) + text, err := n.getMessageText(user, messageTextTypePasswordReset, user.PreferredLanguage) if err != nil { return err } @@ -224,7 +223,6 @@ func (n *Notification) handleEmailVerificationCode(event *models.Event) (err err if err != nil || alreadyHandled { return nil } - ctx := getSetNotifyContextData(event.ResourceOwner) colors, err := n.getLabelPolicy(ctx) if err != nil { @@ -241,7 +239,7 @@ func (n *Notification) handleEmailVerificationCode(event *models.Event) (err err return err } - text, err := n.getMailText(ctx, mailTextTypeVerifyEmail, user.PreferredLanguage) + text, err := n.getMessageText(user, messageTextTypeVerifyEmail, user.PreferredLanguage) if err != nil { return err } @@ -268,7 +266,11 @@ func (n *Notification) handlePhoneVerificationCode(event *models.Event) (err err if err != nil { return err } - err = types.SendPhoneVerificationCode(n.i18n, user, phoneCode, n.systemDefaults, n.AesCrypto) + text, err := n.getMessageText(user, messageTextTypeVerifyPhone, user.PreferredLanguage) + if err != nil { + return err + } + err = types.SendPhoneVerificationCode(text, user, phoneCode, n.systemDefaults, n.AesCrypto) if err != nil { return err } @@ -303,7 +305,7 @@ func (n *Notification) handleDomainClaimed(event *models.Event) (err error) { return err } - text, err := n.getMailText(ctx, mailTextTypeDomainClaimed, user.PreferredLanguage) + text, err := n.getMessageText(user, messageTextTypeDomainClaimed, user.PreferredLanguage) if err != nil { return err } @@ -395,26 +397,29 @@ func (n *Notification) getMailTemplate(ctx context.Context) (*iam_model.MailTemp } // Read organization specific texts -func (n *Notification) getMailText(ctx context.Context, textType string, lang string) (*iam_model.MailTextView, error) { +func (n *Notification) getMessageText(user *model.NotifyUser, textType, lang string) (*iam_model.MessageTextView, error) { langTag := language.Make(lang) if langTag == language.Und { - langTag = n.systemDefaults.DefaultLanguage + langTag = language.English } - base, _ := langTag.Base() + langBase, _ := langTag.Base() + + defaultMessageText, err := n.view.MessageTextByIDs(n.systemDefaults.IamID, textType, langBase.String(), messageTextTableDef) + if err != nil { + return nil, err + } + defaultMessageText.Default = true + // read from Org - mailText, err := n.view.MailTextByIDs(authz.GetCtxData(ctx).OrgID, textType, base.String(), mailTextTableOrg) + orgMessageText, err := n.view.MessageTextByIDs(user.ResourceOwner, textType, langBase.String(), messageTextTableOrg) if errors.IsNotFound(err) { - // read from default - mailText, err = n.view.MailTextByIDs(n.systemDefaults.IamID, textType, base.String(), mailTextTableDef) - if err != nil { - return nil, err - } - mailText.Default = true + return iam_es_model.MessageTextViewToModel(defaultMessageText), nil } if err != nil { return nil, err } - return iam_es_model.MailTextViewToModel(mailText), err + mergedText := mergeMessageTexts(defaultMessageText, orgMessageText) + return iam_es_model.MessageTextViewToModel(mergedText), err } func (n *Notification) getUserByID(userID string) (*model.NotifyUser, error) { @@ -440,3 +445,28 @@ func (n *Notification) getUserByID(userID string) (*model.NotifyUser, error) { } return &userCopy, nil } + +func mergeMessageTexts(defaultText *iam_es_model.MessageTextView, orgText *iam_es_model.MessageTextView) *iam_es_model.MessageTextView { + if orgText.Subject == "" { + orgText.Subject = defaultText.Subject + } + if orgText.Title == "" { + orgText.Title = defaultText.Title + } + if orgText.PreHeader == "" { + orgText.PreHeader = defaultText.PreHeader + } + if orgText.Text == "" { + orgText.Text = defaultText.Text + } + if orgText.Greeting == "" { + orgText.Greeting = defaultText.Greeting + } + if orgText.ButtonText == "" { + orgText.ButtonText = defaultText.ButtonText + } + if orgText.FooterText == "" { + orgText.FooterText = defaultText.FooterText + } + return orgText +} diff --git a/internal/notification/repository/eventsourcing/view/mail_text.go b/internal/notification/repository/eventsourcing/view/mail_text.go deleted file mode 100644 index 3413b73455..0000000000 --- a/internal/notification/repository/eventsourcing/view/mail_text.go +++ /dev/null @@ -1,10 +0,0 @@ -package view - -import ( - "github.com/caos/zitadel/internal/iam/repository/view" - "github.com/caos/zitadel/internal/iam/repository/view/model" -) - -func (v *View) MailTextByIDs(aggregateID string, textType string, language string, mailTextTableVar string) (*model.MailTextView, error) { - return view.GetMailTextByIDs(v.Db, mailTextTableVar, aggregateID, textType, language) -} diff --git a/internal/notification/repository/eventsourcing/view/message_text.go b/internal/notification/repository/eventsourcing/view/message_text.go new file mode 100644 index 0000000000..486895a475 --- /dev/null +++ b/internal/notification/repository/eventsourcing/view/message_text.go @@ -0,0 +1,10 @@ +package view + +import ( + "github.com/caos/zitadel/internal/iam/repository/view" + "github.com/caos/zitadel/internal/iam/repository/view/model" +) + +func (v *View) MessageTextByIDs(aggregateID, textType, lang, messageTextTableVar string) (*model.MessageTextView, error) { + return view.GetMessageTextByIDs(v.Db, messageTextTableVar, aggregateID, textType, lang) +} diff --git a/internal/notification/templates/templateData.go b/internal/notification/templates/templateData.go index 2b7f119740..3197b52c3b 100644 --- a/internal/notification/templates/templateData.go +++ b/internal/notification/templates/templateData.go @@ -53,7 +53,7 @@ func (data *TemplateData) Translate(i18n *i18n.Translator, args map[string]inter data.ButtonText = i18n.Localize(data.ButtonText, nil, langs...) } -func GetTemplateData(apiDomain, href string, text *iam_model.MailTextView, policy *iam_model.LabelPolicyView) TemplateData { +func GetTemplateData(apiDomain, href string, text *iam_model.MessageTextView, policy *iam_model.LabelPolicyView) TemplateData { templateData := TemplateData{ Title: text.Title, PreHeader: text.PreHeader, @@ -62,6 +62,7 @@ func GetTemplateData(apiDomain, href string, text *iam_model.MailTextView, polic Text: html.UnescapeString(text.Text), Href: href, ButtonText: text.ButtonText, + FooterText: text.FooterText, PrimaryColor: defaultPrimaryColor, BackgroundColor: defaultBackgroundColor, FontColor: defaultFontColor, diff --git a/internal/notification/types/domain_claimed.go b/internal/notification/types/domain_claimed.go index 0717f0ea70..58461125a6 100644 --- a/internal/notification/types/domain_claimed.go +++ b/internal/notification/types/domain_claimed.go @@ -15,18 +15,14 @@ type DomainClaimedData struct { URL string } -func SendDomainClaimed(mailhtml string, text *iam_model.MailTextView, user *view_model.NotifyUser, username string, systemDefaults systemdefaults.SystemDefaults, colors *iam_model.LabelPolicyView, apiDomain string) error { +func SendDomainClaimed(mailhtml string, text *iam_model.MessageTextView, user *view_model.NotifyUser, username string, systemDefaults systemdefaults.SystemDefaults, colors *iam_model.LabelPolicyView, apiDomain string) error { url, err := templates.ParseTemplateText(systemDefaults.Notifications.Endpoints.DomainClaimed, &UrlData{UserID: user.ID}) if err != nil { return err } - var args = map[string]interface{}{ - "FirstName": user.FirstName, - "LastName": user.LastName, - "Username": user.LastEmail, - "TempUsername": username, - "Domain": strings.Split(user.LastEmail, "@")[1], - } + var args = mapNotifyUserToArgs(user) + args["TempUsername"] = username + args["Domain"] = strings.Split(user.LastEmail, "@")[1] text.Greeting, err = templates.ParseTemplateText(text.Greeting, args) text.Text, err = templates.ParseTemplateText(text.Text, args) diff --git a/internal/notification/types/email_verification_code.go b/internal/notification/types/email_verification_code.go index 2654b4e897..4b88f17a91 100644 --- a/internal/notification/types/email_verification_code.go +++ b/internal/notification/types/email_verification_code.go @@ -16,7 +16,7 @@ type EmailVerificationCodeData struct { URL string } -func SendEmailVerificationCode(mailhtml string, text *iam_model.MailTextView, user *view_model.NotifyUser, code *es_model.EmailCode, systemDefaults systemdefaults.SystemDefaults, alg crypto.EncryptionAlgorithm, colors *iam_model.LabelPolicyView, apiDomain string) error { +func SendEmailVerificationCode(mailhtml string, text *iam_model.MessageTextView, user *view_model.NotifyUser, code *es_model.EmailCode, systemDefaults systemdefaults.SystemDefaults, alg crypto.EncryptionAlgorithm, colors *iam_model.LabelPolicyView, apiDomain string) error { codeString, err := crypto.DecryptString(code.Code, alg) if err != nil { return err @@ -25,11 +25,9 @@ func SendEmailVerificationCode(mailhtml string, text *iam_model.MailTextView, us if err != nil { return err } - var args = map[string]interface{}{ - "FirstName": user.FirstName, - "LastName": user.LastName, - "Code": codeString, - } + + var args = mapNotifyUserToArgs(user) + args["Code"] = codeString text.Greeting, err = templates.ParseTemplateText(text.Greeting, args) text.Text, err = templates.ParseTemplateText(text.Text, args) diff --git a/internal/notification/types/init_code.go b/internal/notification/types/init_code.go index 596af557d1..9fce037afe 100644 --- a/internal/notification/types/init_code.go +++ b/internal/notification/types/init_code.go @@ -22,7 +22,7 @@ type UrlData struct { PasswordSet bool } -func SendUserInitCode(mailhtml string, text *iam_model.MailTextView, user *view_model.NotifyUser, code *es_model.InitUserCode, systemDefaults systemdefaults.SystemDefaults, alg crypto.EncryptionAlgorithm, colors *iam_model.LabelPolicyView, apiDomain string) error { +func SendUserInitCode(mailhtml string, text *iam_model.MessageTextView, user *view_model.NotifyUser, code *es_model.InitUserCode, systemDefaults systemdefaults.SystemDefaults, alg crypto.EncryptionAlgorithm, colors *iam_model.LabelPolicyView, apiDomain string) error { codeString, err := crypto.DecryptString(code.Code, alg) if err != nil { return err @@ -31,12 +31,8 @@ func SendUserInitCode(mailhtml string, text *iam_model.MailTextView, user *view_ if err != nil { return err } - var args = map[string]interface{}{ - "FirstName": user.FirstName, - "LastName": user.LastName, - "Code": codeString, - "PreferredLoginName": user.PreferredLoginName, - } + var args = mapNotifyUserToArgs(user) + args["Code"] = codeString text.Greeting, err = templates.ParseTemplateText(text.Greeting, args) text.Text, err = templates.ParseTemplateText(text.Text, args) diff --git a/internal/notification/types/password_code.go b/internal/notification/types/password_code.go index bdcb478dca..1eb1179362 100644 --- a/internal/notification/types/password_code.go +++ b/internal/notification/types/password_code.go @@ -18,7 +18,7 @@ type PasswordCodeData struct { URL string } -func SendPasswordCode(mailhtml string, text *iam_model.MailTextView, user *view_model.NotifyUser, code *es_model.PasswordCode, systemDefaults systemdefaults.SystemDefaults, alg crypto.EncryptionAlgorithm, colors *iam_model.LabelPolicyView, apiDomain string) error { +func SendPasswordCode(mailhtml string, text *iam_model.MessageTextView, user *view_model.NotifyUser, code *es_model.PasswordCode, systemDefaults systemdefaults.SystemDefaults, alg crypto.EncryptionAlgorithm, colors *iam_model.LabelPolicyView, apiDomain string) error { codeString, err := crypto.DecryptString(code.Code, alg) if err != nil { return err @@ -27,11 +27,8 @@ func SendPasswordCode(mailhtml string, text *iam_model.MailTextView, user *view_ if err != nil { return err } - var args = map[string]interface{}{ - "FirstName": user.FirstName, - "LastName": user.LastName, - "Code": codeString, - } + var args = mapNotifyUserToArgs(user) + args["Code"] = codeString text.Greeting, err = templates.ParseTemplateText(text.Greeting, args) text.Text, err = templates.ParseTemplateText(text.Text, args) diff --git a/internal/notification/types/phone_verification_code.go b/internal/notification/types/phone_verification_code.go index 0cb1eb472c..28447ce2e4 100644 --- a/internal/notification/types/phone_verification_code.go +++ b/internal/notification/types/phone_verification_code.go @@ -3,7 +3,7 @@ package types import ( "github.com/caos/zitadel/internal/config/systemdefaults" "github.com/caos/zitadel/internal/crypto" - "github.com/caos/zitadel/internal/i18n" + iam_model "github.com/caos/zitadel/internal/iam/model" "github.com/caos/zitadel/internal/notification/templates" es_model "github.com/caos/zitadel/internal/user/repository/eventsourcing/model" view_model "github.com/caos/zitadel/internal/user/repository/view/model" @@ -13,19 +13,18 @@ type PhoneVerificationCodeData struct { UserID string } -func SendPhoneVerificationCode(i18n *i18n.Translator, user *view_model.NotifyUser, code *es_model.PhoneCode, systemDefaults systemdefaults.SystemDefaults, alg crypto.EncryptionAlgorithm) error { +func SendPhoneVerificationCode(text *iam_model.MessageTextView, user *view_model.NotifyUser, code *es_model.PhoneCode, systemDefaults systemdefaults.SystemDefaults, alg crypto.EncryptionAlgorithm) error { codeString, err := crypto.DecryptString(code.Code, alg) if err != nil { return err } - var args = map[string]interface{}{ - "FirstName": user.FirstName, - "LastName": user.LastName, - "Code": codeString, - } - systemDefaults.Notifications.TemplateData.VerifyPhone.Translate(i18n, args, user.PreferredLanguage) + var args = mapNotifyUserToArgs(user) + args["Code"] = codeString + + text.Text, err = templates.ParseTemplateText(text.Text, args) + codeData := &PhoneVerificationCodeData{UserID: user.ID} - template, err := templates.ParseTemplateText(systemDefaults.Notifications.TemplateData.VerifyPhone.Text, codeData) + template, err := templates.ParseTemplateText(text.Text, codeData) if err != nil { return err } diff --git a/internal/notification/types/user_email.go b/internal/notification/types/user_email.go index 974aabc60c..4dfe6c9ed0 100644 --- a/internal/notification/types/user_email.go +++ b/internal/notification/types/user_email.go @@ -42,3 +42,21 @@ func sendDebugEmail(message providers.Message, config systemdefaults.Notificatio } return provider.HandleMessage(message) } + +func mapNotifyUserToArgs(user *view_model.NotifyUser) map[string]interface{} { + return map[string]interface{}{ + "UserName": user.UserName, + "FirstName": user.FirstName, + "LastName": user.LastName, + "NickName": user.NickName, + "DisplayName": user.DisplayName, + "LastEmail": user.LastEmail, + "VerifiedEmail": user.VerifiedEmail, + "LastPhone": user.LastPhone, + "VerifiedPhone": user.VerifiedPhone, + "PreferredLoginName": user.PreferredLoginName, + "LoginNames": user.LoginNames, + "ChangeDate": user.ChangeDate, + "CreationDate": user.CreationDate, + } +} diff --git a/internal/org/repository/eventsourcing/model/mail_text.go b/internal/org/repository/eventsourcing/model/mail_text.go deleted file mode 100644 index 908228b364..0000000000 --- a/internal/org/repository/eventsourcing/model/mail_text.go +++ /dev/null @@ -1,44 +0,0 @@ -package model - -import ( - es_models "github.com/caos/zitadel/internal/eventstore/v1/models" - iam_es_model "github.com/caos/zitadel/internal/iam/repository/eventsourcing/model" -) - -func (o *Org) appendAddMailTextEvent(event *es_models.Event) error { - mailText := &iam_es_model.MailText{} - err := mailText.SetDataLabel(event) - if err != nil { - return err - } - mailText.ObjectRoot.CreationDate = event.CreationDate - o.MailTexts = append(o.MailTexts, mailText) - return nil -} - -func (o *Org) appendChangeMailTextEvent(event *es_models.Event) error { - mailText := &iam_es_model.MailText{} - err := mailText.SetDataLabel(event) - if err != nil { - return err - } - mailText.ObjectRoot.ChangeDate = event.CreationDate - if n, m := iam_es_model.GetMailText(o.MailTexts, mailText.MailTextType, mailText.Language); m != nil { - o.MailTexts[n] = mailText - } - return nil -} - -func (o *Org) appendRemoveMailTextEvent(event *es_models.Event) error { - mailText := &iam_es_model.MailText{} - err := mailText.SetDataLabel(event) - if err != nil { - return err - } - if n, m := iam_es_model.GetMailText(o.MailTexts, mailText.MailTextType, mailText.Language); m != nil { - o.MailTexts[n] = o.MailTexts[len(o.MailTexts)-1] - o.MailTexts[len(o.MailTexts)-1] = nil - o.MailTexts = o.MailTexts[:len(o.MailTexts)-1] - } - return nil -} diff --git a/internal/org/repository/eventsourcing/model/mail_text_test.go b/internal/org/repository/eventsourcing/model/mail_text_test.go deleted file mode 100644 index 49de9b3d6f..0000000000 --- a/internal/org/repository/eventsourcing/model/mail_text_test.go +++ /dev/null @@ -1,91 +0,0 @@ -package model - -import ( - "encoding/json" - "testing" - - es_models "github.com/caos/zitadel/internal/eventstore/v1/models" - iam_es_model "github.com/caos/zitadel/internal/iam/repository/eventsourcing/model" -) - -func TestAppendAddMailTextEvent(t *testing.T) { - type args struct { - org *Org - mailText *iam_es_model.MailText - event *es_models.Event - } - tests := []struct { - name string - args args - result *Org - }{ - { - name: "append add mail text event", - args: args{ - org: &Org{}, - mailText: &iam_es_model.MailText{MailTextType: "Type", Language: "DE"}, - event: &es_models.Event{}, - }, - result: &Org{MailTexts: []*iam_es_model.MailText{&iam_es_model.MailText{MailTextType: "Type", Language: "DE"}}}, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if tt.args.mailText != nil { - data, _ := json.Marshal(tt.args.mailText) - tt.args.event.Data = data - } - tt.args.org.appendAddMailTextEvent(tt.args.event) - if len(tt.args.org.MailTexts) != 1 { - t.Errorf("got wrong result should have one mailtext actual: %v ", len(tt.args.org.MailTexts)) - } - if tt.result.MailTexts[0].Language != tt.args.org.MailTexts[0].Language { - t.Errorf("got wrong result: expected: %v, actual: %v ", tt.result.MailTexts[0].Language, tt.args.org.MailTexts[0].Language) - } - }) - } -} - -func TestAppendChangeMailTextEvent(t *testing.T) { - type args struct { - org *Org - mailText *iam_es_model.MailText - event *es_models.Event - } - tests := []struct { - name string - args args - result *Org - }{ - { - name: "append change mail text event", - args: args{ - org: &Org{MailTexts: []*iam_es_model.MailText{&iam_es_model.MailText{ - Language: "DE", - MailTextType: "TypeX", - }}}, - mailText: &iam_es_model.MailText{MailTextType: "Type", Language: "DE"}, - event: &es_models.Event{}, - }, - result: &Org{MailTexts: []*iam_es_model.MailText{&iam_es_model.MailText{ - Language: "DE", - MailTextType: "Type", - }}}, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if tt.args.mailText != nil { - data, _ := json.Marshal(tt.args.mailText) - tt.args.event.Data = data - } - tt.args.org.appendChangeMailTextEvent(tt.args.event) - if len(tt.args.org.MailTexts) != 1 { - t.Errorf("got wrong result should have one mailtext actual: %v ", len(tt.args.org.MailTexts)) - } - if tt.result.MailTexts[0].Language != tt.args.org.MailTexts[0].Language { - t.Errorf("got wrong result: expected: %v, actual: %v ", tt.result.MailTexts[0].Language, tt.args.org.MailTexts[0].Language) - } - }) - } -} diff --git a/internal/org/repository/eventsourcing/model/org.go b/internal/org/repository/eventsourcing/model/org.go index 499e44a64e..422e6ac7e7 100644 --- a/internal/org/repository/eventsourcing/model/org.go +++ b/internal/org/repository/eventsourcing/model/org.go @@ -26,7 +26,6 @@ type Org struct { OrgIAMPolicy *iam_es_model.OrgIAMPolicy `json:"-"` LabelPolicy *iam_es_model.LabelPolicy `json:"-"` MailTemplate *iam_es_model.MailTemplate `json:"-"` - MailTexts []*iam_es_model.MailText `json:"-"` IDPs []*iam_es_model.IDPConfig `json:"-"` LoginPolicy *iam_es_model.LoginPolicy `json:"-"` PasswordComplexityPolicy *iam_es_model.PasswordComplexityPolicy `json:"-"` @@ -34,44 +33,6 @@ type Org struct { PasswordLockoutPolicy *iam_es_model.PasswordLockoutPolicy `json:"-"` } -func OrgFromModel(org *org_model.Org) *Org { - members := OrgMembersFromModel(org.Members) - domains := OrgDomainsFromModel(org.Domains) - idps := iam_es_model.IDPConfigsFromModel(org.IDPs) - mailTexts := iam_es_model.MailTextsFromModel(org.MailTexts) - converted := &Org{ - ObjectRoot: org.ObjectRoot, - Name: org.Name, - State: int32(org.State), - Domains: domains, - MailTexts: mailTexts, - Members: members, - IDPs: idps, - } - if org.OrgIamPolicy != nil { - converted.OrgIAMPolicy = iam_es_model.OrgIAMPolicyFromModel(org.OrgIamPolicy) - } - if org.LoginPolicy != nil { - converted.LoginPolicy = iam_es_model.LoginPolicyFromModel(org.LoginPolicy) - } - if org.LabelPolicy != nil { - converted.LabelPolicy = iam_es_model.LabelPolicyFromModel(org.LabelPolicy) - } - if org.MailTemplate != nil { - converted.MailTemplate = iam_es_model.MailTemplateFromModel(org.MailTemplate) - } - if org.PasswordComplexityPolicy != nil { - converted.PasswordComplexityPolicy = iam_es_model.PasswordComplexityPolicyFromModel(org.PasswordComplexityPolicy) - } - if org.PasswordAgePolicy != nil { - converted.PasswordAgePolicy = iam_es_model.PasswordAgePolicyFromModel(org.PasswordAgePolicy) - } - if org.PasswordLockoutPolicy != nil { - converted.PasswordLockoutPolicy = iam_es_model.PasswordLockoutPolicyFromModel(org.PasswordLockoutPolicy) - } - return converted -} - func OrgToModel(org *Org) *org_model.Org { converted := &org_model.Org{ ObjectRoot: org.ObjectRoot, @@ -79,7 +40,6 @@ func OrgToModel(org *Org) *org_model.Org { State: org_model.OrgState(org.State), Domains: OrgDomainsToModel(org.Domains), Members: OrgMembersToModel(org.Members), - MailTexts: iam_es_model.MailTextsToModel(org.MailTexts), IDPs: iam_es_model.IDPConfigsToModel(org.IDPs), } if org.OrgIAMPolicy != nil { @@ -216,12 +176,6 @@ func (o *Org) AppendEvent(event *es_models.Event) (err error) { err = o.appendChangeMailTemplateEvent(event) case MailTemplateRemoved: o.appendRemoveMailTemplateEvent(event) - case MailTextAdded: - err = o.appendAddMailTextEvent(event) - case MailTextChanged: - err = o.appendChangeMailTextEvent(event) - case MailTextRemoved: - o.appendRemoveMailTextEvent(event) case LoginPolicySecondFactorAdded: err = o.appendAddSecondFactorToLoginPolicyEvent(event) case LoginPolicySecondFactorRemoved: diff --git a/internal/org/repository/eventsourcing/model/types.go b/internal/org/repository/eventsourcing/model/types.go index 4a00b147da..17eda9952f 100644 --- a/internal/org/repository/eventsourcing/model/types.go +++ b/internal/org/repository/eventsourcing/model/types.go @@ -76,9 +76,10 @@ const ( MailTemplateAdded models.EventType = "org.mail.template.added" MailTemplateChanged models.EventType = "org.mail.template.changed" MailTemplateRemoved models.EventType = "org.mail.template.removed" - MailTextAdded models.EventType = "org.mail.text.added" - MailTextChanged models.EventType = "org.mail.text.changed" - MailTextRemoved models.EventType = "org.mail.text.removed" + + CustomTextSet models.EventType = "org.customtext.set" + CustomTextRemoved models.EventType = "org.customtext.removed" + CustomTextMessageRemoved models.EventType = "org.customtext.template.removed" PasswordComplexityPolicyAdded models.EventType = "org.policy.password.complexity.added" PasswordComplexityPolicyChanged models.EventType = "org.policy.password.complexity.changed" diff --git a/internal/project/model/project_grant_view.go b/internal/project/model/project_grant_view.go index 665f73d0b2..4ceb8b5225 100644 --- a/internal/project/model/project_grant_view.go +++ b/internal/project/model/project_grant_view.go @@ -81,7 +81,7 @@ func (r *ProjectGrantViewSearchRequest) AppendMyResourceOwnerQuery(orgID string) func (r *ProjectGrantViewSearchRequest) EnsureLimit(limit uint64) error { if r.Limit > limit { - return caos_errors.ThrowInvalidArgument(nil, "SEARCH-2n8fS", "Errors.Limit.ExceedsDefault") + return caos_errors.ThrowInvalidArgument(nil, "SEARCH-0fj3s", "Errors.Limit.ExceedsDefault") } if r.Limit == 0 { r.Limit = limit diff --git a/internal/repository/features/features.go b/internal/repository/features/features.go index b7dde676f4..d714209e0d 100644 --- a/internal/repository/features/features.go +++ b/internal/repository/features/features.go @@ -35,6 +35,7 @@ type FeaturesSetEvent struct { LabelPolicyPrivateLabel *bool `json:"labelPolicyPrivateLabel,omitempty"` LabelPolicyWatermark *bool `json:"labelPolicyWatermark,omitempty"` CustomDomain *bool `json:"customDomain,omitempty"` + CustomText *bool `json:"customText,omitempty"` } func (e *FeaturesSetEvent) Data() interface{} { @@ -153,6 +154,11 @@ func ChangeCustomDomain(customDomain bool) func(event *FeaturesSetEvent) { } } +func ChangeCustomText(customText bool) func(event *FeaturesSetEvent) { + return func(e *FeaturesSetEvent) { + e.CustomText = &customText + } +} func FeaturesSetEventMapper(event *repository.Event) (eventstore.EventReader, error) { e := &FeaturesSetEvent{ BaseEvent: *eventstore.BaseEventFromRepo(event), diff --git a/internal/repository/iam/custom_text.go b/internal/repository/iam/custom_text.go new file mode 100644 index 0000000000..0bec3d5b24 --- /dev/null +++ b/internal/repository/iam/custom_text.go @@ -0,0 +1,46 @@ +package iam + +import ( + "context" + + "golang.org/x/text/language" + + "github.com/caos/zitadel/internal/eventstore" + "github.com/caos/zitadel/internal/eventstore/repository" + "github.com/caos/zitadel/internal/repository/policy" +) + +var ( + CustomTextSetEventType = iamEventTypePrefix + policy.CustomTextSetEventType +) + +type CustomTextSetEvent struct { + policy.CustomTextSetEvent +} + +func NewCustomTextSetEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, + template, + key, + text string, + language language.Tag, +) *CustomTextSetEvent { + return &CustomTextSetEvent{ + CustomTextSetEvent: *policy.NewCustomTextSetEvent( + eventstore.NewBaseEventForPush(ctx, aggregate, CustomTextSetEventType), + template, + key, + text, + language), + } +} + +func CustomTextSetEventMapper(event *repository.Event) (eventstore.EventReader, error) { + e, err := policy.CustomTextSetEventMapper(event) + if err != nil { + return nil, err + } + + return &CustomTextSetEvent{CustomTextSetEvent: *e.(*policy.CustomTextSetEvent)}, nil +} diff --git a/internal/repository/iam/eventstore.go b/internal/repository/iam/eventstore.go index 61b9d8a6ef..bcfc96f563 100644 --- a/internal/repository/iam/eventstore.go +++ b/internal/repository/iam/eventstore.go @@ -56,5 +56,6 @@ func RegisterEventMappers(es *eventstore.Eventstore) { RegisterFilterEventMapper(MailTemplateChangedEventType, MailTemplateChangedEventMapper). RegisterFilterEventMapper(MailTextAddedEventType, MailTextAddedEventMapper). RegisterFilterEventMapper(MailTextChangedEventType, MailTextChangedEventMapper). + RegisterFilterEventMapper(CustomTextSetEventType, CustomTextSetEventMapper). RegisterFilterEventMapper(FeaturesSetEventType, FeaturesSetEventMapper) } diff --git a/internal/repository/org/custom_text.go b/internal/repository/org/custom_text.go new file mode 100644 index 0000000000..11a7ceb104 --- /dev/null +++ b/internal/repository/org/custom_text.go @@ -0,0 +1,107 @@ +package org + +import ( + "context" + + "golang.org/x/text/language" + + "github.com/caos/zitadel/internal/eventstore" + + "github.com/caos/zitadel/internal/eventstore/repository" + "github.com/caos/zitadel/internal/repository/policy" +) + +var ( + CustomTextSetEventType = orgEventTypePrefix + policy.CustomTextSetEventType + CustomTextRemovedEventType = orgEventTypePrefix + policy.CustomTextRemovedEventType + CustomTextTemplateRemovedEventType = orgEventTypePrefix + policy.CustomTextTemplateRemovedEventType +) + +type CustomTextSetEvent struct { + policy.CustomTextSetEvent +} + +func NewCustomTextSetEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, + template, + key, + text string, + language language.Tag, +) *CustomTextSetEvent { + return &CustomTextSetEvent{ + CustomTextSetEvent: *policy.NewCustomTextSetEvent( + eventstore.NewBaseEventForPush(ctx, aggregate, CustomTextSetEventType), + template, + key, + text, + language), + } +} + +func CustomTextSetEventMapper(event *repository.Event) (eventstore.EventReader, error) { + e, err := policy.CustomTextSetEventMapper(event) + if err != nil { + return nil, err + } + + return &CustomTextSetEvent{CustomTextSetEvent: *e.(*policy.CustomTextSetEvent)}, nil +} + +type CustomTextRemovedEvent struct { + policy.CustomTextRemovedEvent +} + +func NewCustomTextRemovedEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, + template, + key string, + language language.Tag, +) *CustomTextRemovedEvent { + return &CustomTextRemovedEvent{ + CustomTextRemovedEvent: *policy.NewCustomTextRemovedEvent( + eventstore.NewBaseEventForPush(ctx, aggregate, CustomTextRemovedEventType), + template, + key, + language, + ), + } +} + +func CustomTextRemovedEventMapper(event *repository.Event) (eventstore.EventReader, error) { + e, err := policy.CustomTextRemovedEventMapper(event) + if err != nil { + return nil, err + } + + return &CustomTextRemovedEvent{CustomTextRemovedEvent: *e.(*policy.CustomTextRemovedEvent)}, nil +} + +type CustomTextTemplateRemovedEvent struct { + policy.CustomTextTemplateRemovedEvent +} + +func NewCustomTextTemplateRemovedEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, + template string, + language language.Tag, +) *CustomTextTemplateRemovedEvent { + return &CustomTextTemplateRemovedEvent{ + CustomTextTemplateRemovedEvent: *policy.NewCustomTextTemplateRemovedEvent( + eventstore.NewBaseEventForPush(ctx, aggregate, CustomTextTemplateRemovedEventType), + template, + language, + ), + } +} + +func CustomTextTemplateRemovedEventMapper(event *repository.Event) (eventstore.EventReader, error) { + e, err := policy.CustomTextTemplateRemovedEventMapper(event) + if err != nil { + return nil, err + } + + return &CustomTextTemplateRemovedEvent{CustomTextTemplateRemovedEvent: *e.(*policy.CustomTextTemplateRemovedEvent)}, nil +} diff --git a/internal/repository/org/eventstore.go b/internal/repository/org/eventstore.go index e5df74644a..7602ad9e58 100644 --- a/internal/repository/org/eventstore.go +++ b/internal/repository/org/eventstore.go @@ -62,6 +62,9 @@ func RegisterEventMappers(es *eventstore.Eventstore) { RegisterFilterEventMapper(MailTextAddedEventType, MailTextAddedEventMapper). RegisterFilterEventMapper(MailTextChangedEventType, MailTextChangedEventMapper). RegisterFilterEventMapper(MailTextRemovedEventType, MailTextRemovedEventMapper). + RegisterFilterEventMapper(CustomTextSetEventType, CustomTextSetEventMapper). + RegisterFilterEventMapper(CustomTextRemovedEventType, CustomTextRemovedEventMapper). + RegisterFilterEventMapper(CustomTextTemplateRemovedEventType, CustomTextTemplateRemovedEventMapper). RegisterFilterEventMapper(IDPConfigAddedEventType, IDPConfigAddedEventMapper). RegisterFilterEventMapper(IDPConfigChangedEventType, IDPConfigChangedEventMapper). RegisterFilterEventMapper(IDPConfigRemovedEventType, IDPConfigRemovedEventMapper). diff --git a/internal/repository/policy/custom_text.go b/internal/repository/policy/custom_text.go new file mode 100644 index 0000000000..7ad6a5dc9c --- /dev/null +++ b/internal/repository/policy/custom_text.go @@ -0,0 +1,138 @@ +package policy + +import ( + "encoding/json" + + "golang.org/x/text/language" + + "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/eventstore" + "github.com/caos/zitadel/internal/eventstore/repository" +) + +const ( + customTextPrefix = "customtext." + CustomTextSetEventType = customTextPrefix + "set" + CustomTextRemovedEventType = customTextPrefix + "removed" + CustomTextTemplateRemovedEventType = customTextPrefix + "template.removed" +) + +type CustomTextSetEvent struct { + eventstore.BaseEvent `json:"-"` + + Template string `json:"template,omitempty"` + Key string `json:"key,omitempty"` + Language language.Tag `json:"language,omitempty"` + Text string `json:"text,omitempty"` +} + +func (e *CustomTextSetEvent) Data() interface{} { + return e +} + +func (e *CustomTextSetEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint { + return nil +} + +func NewCustomTextSetEvent( + base *eventstore.BaseEvent, + template, + key, + text string, + language language.Tag, +) *CustomTextSetEvent { + return &CustomTextSetEvent{ + BaseEvent: *base, + Template: template, + Key: key, + Language: language, + Text: text, + } +} + +func CustomTextSetEventMapper(event *repository.Event) (eventstore.EventReader, error) { + e := &CustomTextSetEvent{ + BaseEvent: *eventstore.BaseEventFromRepo(event), + } + + err := json.Unmarshal(event.Data, e) + if err != nil { + return nil, errors.ThrowInternal(err, "TEXT-28dwe", "unable to unmarshal custom text") + } + + return e, nil +} + +type CustomTextRemovedEvent struct { + eventstore.BaseEvent `json:"-"` + + Template string `json:"template,omitempty"` + Key string `json:"key,omitempty"` + Language language.Tag `json:"language,omitempty"` +} + +func (e *CustomTextRemovedEvent) Data() interface{} { + return e +} + +func (e *CustomTextRemovedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint { + return nil +} + +func NewCustomTextRemovedEvent(base *eventstore.BaseEvent, template, key string, language language.Tag) *CustomTextRemovedEvent { + return &CustomTextRemovedEvent{ + BaseEvent: *base, + Template: template, + Key: key, + Language: language, + } +} + +func CustomTextRemovedEventMapper(event *repository.Event) (eventstore.EventReader, error) { + e := &CustomTextRemovedEvent{ + BaseEvent: *eventstore.BaseEventFromRepo(event), + } + + err := json.Unmarshal(event.Data, e) + if err != nil { + return nil, errors.ThrowInternal(err, "TEXT-28sMf", "unable to unmarshal custom text removed") + } + + return e, nil +} + +type CustomTextTemplateRemovedEvent struct { + eventstore.BaseEvent `json:"-"` + + Template string `json:"template,omitempty"` + Language language.Tag `json:"language,omitempty"` +} + +func (e *CustomTextTemplateRemovedEvent) Data() interface{} { + return e +} + +func (e *CustomTextTemplateRemovedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint { + return nil +} + +func NewCustomTextTemplateRemovedEvent(base *eventstore.BaseEvent, template string, language language.Tag) *CustomTextTemplateRemovedEvent { + return &CustomTextTemplateRemovedEvent{ + BaseEvent: *base, + Template: template, + Language: language, + } +} + +func CustomTextTemplateRemovedEventMapper(event *repository.Event) (eventstore.EventReader, error) { + e := &CustomTextTemplateRemovedEvent{ + BaseEvent: *eventstore.BaseEventFromRepo(event), + } + + err := json.Unmarshal(event.Data, e) + if err != nil { + return nil, errors.ThrowInternal(err, "TEXT-mKKRs", "unable to unmarshal custom text message removed") + } + + return e, nil +} diff --git a/internal/setup/config.go b/internal/setup/config.go index d6100b2abc..8f8b44ab21 100644 --- a/internal/setup/config.go +++ b/internal/setup/config.go @@ -21,6 +21,7 @@ type IAMSetUp struct { Step13 *command.Step13 Step14 *command.Step14 Step15 *command.Step15 + Step16 *command.Step16 } func (setup *IAMSetUp) Steps(currentDone domain.Step) ([]command.Step, error) { @@ -42,6 +43,7 @@ func (setup *IAMSetUp) Steps(currentDone domain.Step) ([]command.Step, error) { setup.Step13, setup.Step14, setup.Step15, + setup.Step16, } { if step.Step() <= currentDone { continue diff --git a/internal/static/i18n/de.yaml b/internal/static/i18n/de.yaml index 3e7e80d8f2..90a688535e 100644 --- a/internal/static/i18n/de.yaml +++ b/internal/static/i18n/de.yaml @@ -162,11 +162,11 @@ Errors: NotChanged: Default Mail Template wurde nicht verändert AlreadyExists: Default Mail Template existiert bereits Invalid: Default Mail Template ist ungültig - MailText: - NotFound: Default Mail Text konnte nicht gefunden werden - NotChanged: Default Mail Text wurde nicht verändert - AlreadyExists: Default Mail Text existiert bereits - Invalid: Default Mail Text ist ungültig + CustomMessageText: + NotFound: Default Message Text konnte nicht gefunden werden + NotChanged: Default Message Text wurde nicht verändert + AlreadyExists: Default Message Text existiert bereits + Invalid: Default Message Text ist ungültig PasswordComplexity: NotFound: Password Komplexitäts Policy konnte nicht gefunden werden Empty: Passwort Komplexitäts Policy ist leer @@ -276,11 +276,11 @@ Errors: NotChanged: Default Mail Template wurde nicht verändert AlreadyExists: Default Mail Template existiert bereits Invalid: Default Mail Template ist ungültig - MailText: - NotFound: Default Mail Text konnte nicht gefunden werden - NotChanged: Default Mail Text wurde nicht verändert - AlreadyExists: Default Mail Text existiert bereits - Invalid: Default Mail Text ist ungültig + CustomMessageText: + NotFound: Default Message Text konnte nicht gefunden werden + NotChanged: Default Message Text wurde nicht verändert + AlreadyExists: Default Message Text existiert bereits + Invalid: Default Message Text ist ungültig PasswordComplexityPolicy: NotFound: Default Password Complexity Policy konnte nicht gefunden werden NotExisting: Default Password Complexity Policy existiert nicht @@ -351,6 +351,10 @@ Errors: AlreadyExists: Schritt ausgeführt existiert bereits Features: NotChanged: Feature hat nicht geändert + CustomText: + AlreadyExists: Kundenspezifischer Text existiert bereits + Invalid: Kundenspezifischer Text ist ungültig + NotFound: Kundenspezifischer Text nicht gefunden EventTypes: user: added: Benutzer hinzugefügt @@ -569,6 +573,11 @@ EventTypes: config: added: SAML IDP Konfiguration hinzugefügt changed: SAML IDP Konfiguration geändert + customtext: + set: Kundenspezifischer Text wurde gesetzt + removed: Kundenspezifischer Text wurde entfernt + template: + removed: Kundenspezifisches Text Template wurde entfernt policy: login: added: Login Richtlinie hinzugefügt @@ -715,6 +724,9 @@ EventTypes: config: added: SAML IDP Konfiguration hinzugefügt changed: SAML IDP Konfiguration geändert + customtext: + set: Text wurde gesetzt + removed: Text wurde entfernt policy: login: added: Default Login Policy hinzugefügt diff --git a/internal/static/i18n/en.yaml b/internal/static/i18n/en.yaml index f9f9438810..41365e2bb1 100644 --- a/internal/static/i18n/en.yaml +++ b/internal/static/i18n/en.yaml @@ -162,11 +162,11 @@ Errors: NotChanged: Default Mail Template has not been changed AlreadyExists: Default Mail Template already exists Invalid: Default Mail Template is invalid - MailText: - NotFound: Default Mail Text not found - NotChanged: Default Mail Text has not been changed - AlreadyExists: Default Mail Text already exists - Invalid: Default Mail Text is invalid + CustomMessageText: + NotFound: Default Message Text not found + NotChanged: Default Message Text has not been changed + AlreadyExists: Default Message Text already exists + Invalid: Default Message Text is invalid PasswordComplexity: NotFound: Password Complexity Policy not found Empty: Password Complexity Policy is empty @@ -276,11 +276,11 @@ Errors: NotChanged: Default Mail Template has not been changed AlreadyExists: Default Mail Template already exists Invalid: Default Mail Template is invalid - MailText: - NotFound: Default Mail Text not found - NotChanged: Default Mail Text has not been changed - AlreadyExists: Default Mail Text already exists - Invalid: Default Mail Text is invalid + CustomMessageText: + NotFound: Default Message Text not found + NotChanged: Default Message Text has not been changed + AlreadyExists: Default Message Text already exists + Invalid: Default Message Text is invalid PasswordComplexityPolicy: NotFound: Default Private Label Policy not found NotExisting: Default Password Complexity Policy not existing @@ -351,6 +351,10 @@ Errors: AlreadyExists: Step done already exists Features: NotChanged: Feature hat nicht geändert + CustomText: + AlreadyExists: Custom text already exists + Invalid: Custom text invalid + NotFound: Custom text not found EventTypes: user: added: User added diff --git a/internal/ui/login/handler/login_handler.go b/internal/ui/login/handler/login_handler.go index baffebfb99..2915e356ac 100644 --- a/internal/ui/login/handler/login_handler.go +++ b/internal/ui/login/handler/login_handler.go @@ -42,7 +42,7 @@ func (l *Login) handleLoginNameCheck(w http.ResponseWriter, r *http.Request) { data := new(loginData) authReq, err := l.getAuthRequestAndParseData(r, data) if err != nil { - l.renderError(w, r, authReq, err) + l.renderLogin(w, r, authReq, err) return } if data.Register { diff --git a/internal/ui/login/static/i18n/de.yaml b/internal/ui/login/static/i18n/de.yaml index b32ae723b2..22b4d2f2a6 100644 --- a/internal/ui/login/static/i18n/de.yaml +++ b/internal/ui/login/static/i18n/de.yaml @@ -235,6 +235,7 @@ Errors: RequestTypeNotSupported: Requesttyp wird nicht unterstürzt User: NotFound: Benutzer konnte nicht gefunden werden + Inactive: Benutzer ist inaktiv NotFoundOnOrg: Benutzer konnte in der gewünschten Organisation nicht gefunden werden NotAllowedOrg: Benutzer gehört nicht der benötigten Organisation an NotMatchingUserID: User stimm nicht mit User in Auth Request überein diff --git a/internal/ui/login/static/i18n/en.yaml b/internal/ui/login/static/i18n/en.yaml index 73df887929..3d1ad44c96 100644 --- a/internal/ui/login/static/i18n/en.yaml +++ b/internal/ui/login/static/i18n/en.yaml @@ -235,6 +235,7 @@ Errors: RequestTypeNotSupported: Request type is not supported User: NotFound: User could not be found + Inactive: User is inactive NotFoundOnOrg: User could not be found on chosen organisation NotAllowedOrg: User is no member of the required organisation NotMatchingUserID: User and user in authrequest don't match diff --git a/migrations/cockroach/V1.48__custom_text.sql b/migrations/cockroach/V1.48__custom_text.sql new file mode 100644 index 0000000000..756b638a88 --- /dev/null +++ b/migrations/cockroach/V1.48__custom_text.sql @@ -0,0 +1,53 @@ +ALTER TABLE adminapi.features ADD COLUMN custom_text BOOLEAN; +ALTER TABLE auth.features ADD COLUMN custom_text BOOLEAN; +ALTER TABLE authz.features ADD COLUMN custom_text BOOLEAN; +ALTER TABLE management.features ADD COLUMN custom_text BOOLEAN; + +CREATE TABLE adminapi.message_texts ( + aggregate_id TEXT, + + creation_date TIMESTAMPTZ, + change_date TIMESTAMPTZ, + message_text_state SMALLINT, + sequence BIGINT, + + message_text_type TEXT, + language TEXT, + title TEXT, + pre_header TEXT, + subject TEXT, + greeting TEXT, + text TEXT, + button_text TEXT, + footer_text TEXT, + + PRIMARY KEY (aggregate_id, message_text_type, language) +); + + +CREATE TABLE management.message_texts ( + aggregate_id TEXT, + + creation_date TIMESTAMPTZ, + change_date TIMESTAMPTZ, + message_text_state SMALLINT, + sequence BIGINT, + + message_text_type TEXT, + language TEXT, + title TEXT, + pre_header TEXT, + subject TEXT, + greeting TEXT, + text TEXT, + button_text TEXT, + footer_text TEXT, + + PRIMARY KEY (aggregate_id, message_text_type, language) +); + +GRANT SELECT ON TABLE adminapi.message_texts TO notification; +GRANT SELECT ON TABLE management.message_texts TO notification; +ALTER TABLE management.message_texts OWNER TO admin; +ALTER TABLE adminapi.message_texts OWNER TO admin; + diff --git a/proto/zitadel/admin.proto b/proto/zitadel/admin.proto index 9a82e90db7..a9c277ec0b 100644 --- a/proto/zitadel/admin.proto +++ b/proto/zitadel/admin.proto @@ -6,6 +6,7 @@ import "zitadel/object.proto"; import "zitadel/options.proto"; import "zitadel/org.proto"; import "zitadel/policy.proto"; +import "zitadel/text.proto"; import "zitadel/member.proto"; import "zitadel/features.proto"; @@ -1447,28 +1448,137 @@ service AdminService { option (zitadel.v1.auth_option) = { permission: "iam.policy.write"; }; + } - option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - tags: "policy"; - tags: "password policy"; - tags: "password lockout policy"; - responses: { - key: "200"; - value: { - description: "default password lockout policy updated"; - }; - }; - responses: { - key: "400"; - value: { - description: "invalid argument"; - schema: { - json_schema: { - ref: "#/definitions/rpcStatus"; - }; - }; - }; - }; + //Returns the custom text for initial message + rpc GetDefaultInitMessageText(GetDefaultInitMessageTextRequest) returns (GetDefaultInitMessageTextResponse) { + option (google.api.http) = { + get: "/text/message/init/{language}"; + }; + + option (zitadel.v1.auth_option) = { + permission: "iam.policy.read"; + }; + } + + //Sets the default custom text for initial message + // it impacts all organisations without customized initial message text + // The Following Variables can be used: + // {{.Code}} {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}} + rpc SetDefaultInitMessageText(SetDefaultInitMessageTextRequest) returns (SetDefaultInitMessageTextResponse) { + option (google.api.http) = { + put: "/text/message/init/{language}"; + body: "*"; + }; + + option (zitadel.v1.auth_option) = { + permission: "iam.policy.write"; + }; + } + + //Returns the custom text for password reset message + rpc GetDefaultPasswordResetMessageText(GetDefaultPasswordResetMessageTextRequest) returns (GetDefaultPasswordResetMessageTextResponse) { + option (google.api.http) = { + get: "/text/message/passwordreset/{language}"; + }; + + option (zitadel.v1.auth_option) = { + permission: "iam.policy.read"; + }; + } + + //Sets the default custom text for password reset message + // it impacts all organisations without customized password reset message text + // The Following Variables can be used: + // {{.Code}} {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}} + rpc SetDefaultPasswordResetMessageText(SetDefaultPasswordResetMessageTextRequest) returns (SetDefaultPasswordResetMessageTextResponse) { + option (google.api.http) = { + put: "/text/message/passwordreset/{language}"; + body: "*"; + }; + + option (zitadel.v1.auth_option) = { + permission: "iam.policy.write"; + }; + + } + + //Returns the custom text for verify email message + rpc GetDefaultVerifyEmailMessageText(GetDefaultVerifyEmailMessageTextRequest) returns (GetDefaultVerifyEmailMessageTextResponse) { + option (google.api.http) = { + get: "/text/message/verifyemail/{language}"; + }; + + option (zitadel.v1.auth_option) = { + permission: "iam.policy.read"; + }; + } + + //Sets the default custom text for verify email message + // it impacts all organisations without customized verify email message text + // The Following Variables can be used: + // {{.Code}} {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}} + rpc SetDefaultVerifyEmailMessageText(SetDefaultVerifyEmailMessageTextRequest) returns (SetDefaultVerifyEmailMessageTextResponse) { + option (google.api.http) = { + put: "/text/message/verifyemail/{language}"; + body: "*"; + }; + + option (zitadel.v1.auth_option) = { + permission: "iam.policy.write"; + }; + } + + //Returns the custom text for verify phone message + rpc GetDefaultVerifyPhoneMessageText(GetDefaultVerifyPhoneMessageTextRequest) returns (GetDefaultVerifyPhoneMessageTextResponse) { + option (google.api.http) = { + get: "/text/message/verifyphone/{language}"; + }; + + option (zitadel.v1.auth_option) = { + permission: "iam.policy.read"; + }; + + } + + //Sets the default custom text for verify phone message + // it impacts all organisations without customized verify phone message text + // The Following Variables can be used: + // {{.Code}} {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}} + rpc SetDefaultVerifyPhoneMessageText(SetDefaultVerifyPhoneMessageTextRequest) returns (SetDefaultVerifyPhoneMessageTextResponse) { + option (google.api.http) = { + put: "/text/message/verifyphone/{language}"; + body: "*"; + }; + + option (zitadel.v1.auth_option) = { + permission: "iam.policy.write"; + }; + } + + //Returns the custom text for domain claimed message + rpc GetDefaultDomainClaimedMessageText(GetDefaultDomainClaimedMessageTextRequest) returns (GetDefaultDomainClaimedMessageTextResponse) { + option (google.api.http) = { + get: "/text/message/domainclaimed/{language}"; + }; + + option (zitadel.v1.auth_option) = { + permission: "iam.policy.read"; + }; + } + + //Sets the default custom text for domain claimed phone message + // it impacts all organisations without customized verify phone message text + // The Following Variables can be used: + // {{.Domain}} {{.TempUsername}} {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}} + rpc SetDefaultDomainClaimedMessageText(SetDefaultDomainClaimedMessageTextRequest) returns (SetDefaultDomainClaimedMessageTextResponse) { + option (google.api.http) = { + put: "/text/message/verifyphone/{language}"; + body: "*"; + }; + + option (zitadel.v1.auth_option) = { + permission: "iam.policy.write"; }; } @@ -2286,6 +2396,7 @@ message SetDefaultFeaturesRequest { bool login_policy_password_reset = 14; bool label_policy_private_label = 15; bool label_policy_watermark = 16; + bool custom_text = 17; } message SetDefaultFeaturesResponse { @@ -2319,6 +2430,7 @@ message SetOrgFeaturesRequest { bool login_policy_password_reset = 15; bool label_policy_private_label = 16; bool label_policy_watermark = 17; + bool custom_text = 18; } message SetOrgFeaturesResponse { @@ -2779,6 +2891,151 @@ message UpdatePasswordLockoutPolicyResponse { zitadel.v1.ObjectDetails details = 1; } +//This is an empty request +message GetDefaultInitMessageTextRequest { + string language = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; +} + +message GetDefaultInitMessageTextResponse { + zitadel.text.v1.MessageCustomText custom_text = 1; +} + +message SetDefaultInitMessageTextRequest { + string language = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"de\"" + } + ]; + string title = 2 [(validate.rules).string = {max_len: 200}]; + string pre_header = 3 [(validate.rules).string = {max_len: 200}]; + string subject = 4 [(validate.rules).string = {max_len: 200}]; + string greeting = 5 [(validate.rules).string = {max_len: 200}]; + string text = 6 [(validate.rules).string = {max_len: 1000}]; + string button_text = 7 [(validate.rules).string = {max_len: 200}]; + string footer_text = 8 [(validate.rules).string = {max_len: 200}]; +} + +message SetDefaultInitMessageTextResponse { + zitadel.v1.ObjectDetails details = 1; +} + +//This is an empty request +message GetDefaultPasswordResetMessageTextRequest { + string language = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; +} + +message GetDefaultPasswordResetMessageTextResponse { + zitadel.text.v1.MessageCustomText custom_text = 1; +} + +message SetDefaultPasswordResetMessageTextRequest { + string language = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"de\"" + } + ]; + string title = 2 [(validate.rules).string = {max_len: 200}]; + string pre_header = 3 [(validate.rules).string = {max_len: 200}]; + string subject = 4 [(validate.rules).string = {max_len: 200}]; + string greeting = 5 [(validate.rules).string = {max_len: 200}]; + string text = 6 [(validate.rules).string = {max_len: 800}]; + string button_text = 7 [(validate.rules).string = {max_len: 200}]; + string footer_text = 8 [(validate.rules).string = {max_len: 200}]; +} + +message SetDefaultPasswordResetMessageTextResponse { + zitadel.v1.ObjectDetails details = 1; +} + +//This is an empty request +message GetDefaultVerifyEmailMessageTextRequest { + string language = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; +} + +message GetDefaultVerifyEmailMessageTextResponse { + zitadel.text.v1.MessageCustomText custom_text = 1; +} + +message SetDefaultVerifyEmailMessageTextRequest { + string language = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"de\"" + } + ]; + string title = 2 [(validate.rules).string = {max_len: 200}]; + string pre_header = 3 [(validate.rules).string = {max_len: 200}]; + string subject = 4 [(validate.rules).string = {max_len: 200}]; + string greeting = 5 [(validate.rules).string = {max_len: 200}]; + string text = 6 [(validate.rules).string = {max_len: 800}]; + string button_text = 7 [(validate.rules).string = {max_len: 200}]; + string footer_text = 8 [(validate.rules).string = {max_len: 200}]; +} + +message SetDefaultVerifyEmailMessageTextResponse { + zitadel.v1.ObjectDetails details = 1; +} + +//This is an empty request +message GetDefaultVerifyPhoneMessageTextRequest { + string language = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; +} + +message GetDefaultVerifyPhoneMessageTextResponse { + zitadel.text.v1.MessageCustomText custom_text = 1; +} + +message SetDefaultVerifyPhoneMessageTextRequest { + string language = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"de\"" + } + ]; + string title = 2 [(validate.rules).string = {max_len: 200}]; + string pre_header = 3 [(validate.rules).string = {max_len: 200}]; + string subject = 4 [(validate.rules).string = {max_len: 200}]; + string greeting = 5 [(validate.rules).string = {max_len: 200}]; + string text = 6 [(validate.rules).string = {max_len: 800}]; + string button_text = 7 [(validate.rules).string = {max_len: 200}]; + string footer_text = 8 [(validate.rules).string = {max_len: 200}]; +} + +message SetDefaultVerifyPhoneMessageTextResponse { + zitadel.v1.ObjectDetails details = 1; +} + +//This is an empty request +message GetDefaultDomainClaimedMessageTextRequest { + string language = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; +} + +message GetDefaultDomainClaimedMessageTextResponse { + zitadel.text.v1.MessageCustomText custom_text = 1; +} + +message SetDefaultDomainClaimedMessageTextRequest { + string language = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"de\"" + } + ]; + string title = 2 [(validate.rules).string = {max_len: 200}]; + string pre_header = 3 [(validate.rules).string = {max_len: 200}]; + string subject = 4 [(validate.rules).string = {max_len: 200}]; + string greeting = 5 [(validate.rules).string = {max_len: 200}]; + string text = 6 [(validate.rules).string = {max_len: 800}]; + string button_text = 7 [(validate.rules).string = {max_len: 200}]; + string footer_text = 8 [(validate.rules).string = {max_len: 200}]; +} + +message SetDefaultDomainClaimedMessageTextResponse { + zitadel.v1.ObjectDetails details = 1; +} + message AddIAMMemberRequest { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { json_schema: { diff --git a/proto/zitadel/features.proto b/proto/zitadel/features.proto index dd590d08df..346f5dde13 100644 --- a/proto/zitadel/features.proto +++ b/proto/zitadel/features.proto @@ -24,6 +24,7 @@ message Features { bool login_policy_password_reset = 13; bool label_policy_private_label = 14; bool label_policy_watermark = 15; + bool custom_text = 16; } message FeatureTier { diff --git a/proto/zitadel/management.proto b/proto/zitadel/management.proto index d61f7bfb52..0aecd1c925 100644 --- a/proto/zitadel/management.proto +++ b/proto/zitadel/management.proto @@ -9,6 +9,7 @@ import "zitadel/org.proto"; import "zitadel/member.proto"; import "zitadel/project.proto"; import "zitadel/policy.proto"; +import "zitadel/text.proto"; import "zitadel/message.proto"; import "zitadel/change.proto"; import "zitadel/auth_n_key.proto"; @@ -2077,6 +2078,200 @@ service ManagementService { }; } + //Returns the custom text for initial message + rpc GetCustomInitMessageText(GetCustomInitMessageTextRequest) returns (GetCustomInitMessageTextResponse) { + option (google.api.http) = { + get: "/text/message/init/{language}"; + }; + + option (zitadel.v1.auth_option) = { + permission: "iam.policy.read"; + }; + } + + //Sets the default custom text for initial message + // it impacts all organisations without customized initial message text + // The Following Variables can be used: + // {{.Code}} {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}} + rpc SetCustomInitMessageText(SetCustomInitMessageTextRequest) returns (SetCustomInitMessageTextResponse) { + option (google.api.http) = { + put: "/text/message/init/{language}"; + body: "*"; + }; + + option (zitadel.v1.auth_option) = { + permission: "iam.policy.write"; + feature: "custom_text" + }; + } + + // Removes the custom init message text of the organisation + // The default text of the IAM will trigger after + rpc ResetCustomInitMessageTextToDefault(ResetCustomInitMessageTextToDefaultRequest) returns (ResetCustomInitMessageTextToDefaultResponse) { + option (google.api.http) = { + delete: "/text/message/init/{language}" + }; + + option (zitadel.v1.auth_option) = { + permission: "policy.delete" + }; + } + //Returns the custom text for password reset message + rpc GetCustomPasswordResetMessageText(GetCustomPasswordResetMessageTextRequest) returns (GetCustomPasswordResetMessageTextResponse) { + option (google.api.http) = { + get: "/text/message/passwordreset/{language}"; + }; + + option (zitadel.v1.auth_option) = { + permission: "iam.policy.read"; + }; + } + + //Sets the default custom text for password reset message + // it impacts all organisations without customized password reset message text + // The Following Variables can be used: + // {{.Code}} {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}} + rpc SetCustomPasswordResetMessageText(SetCustomPasswordResetMessageTextRequest) returns (SetCustomPasswordResetMessageTextResponse) { + option (google.api.http) = { + put: "/text/message/passwordreset/{language}"; + body: "*"; + }; + + option (zitadel.v1.auth_option) = { + permission: "iam.policy.write"; + feature: "custom_text" + }; + } + + // Removes the custom init message text of the organisation + // The default text of the IAM will trigger after + rpc ResetCustomPasswordResetMessageTextToDefault(ResetCustomPasswordResetMessageTextToDefaultRequest) returns (ResetCustomPasswordResetMessageTextToDefaultResponse) { + option (google.api.http) = { + delete: "/text/message/verifyemail/{language}" + }; + + option (zitadel.v1.auth_option) = { + permission: "policy.delete" + }; + } + + //Returns the custom text for verify email message + rpc GetCustomVerifyEmailMessageText(GetCustomVerifyEmailMessageTextRequest) returns (GetCustomVerifyEmailMessageTextResponse) { + option (google.api.http) = { + get: "/text/message/verifyemail/{language}"; + }; + + option (zitadel.v1.auth_option) = { + permission: "iam.policy.read"; + }; + } + + //Sets the default custom text for verify email message + // it impacts all organisations without customized verify email message text + // The Following Variables can be used: + // {{.Code}} {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}} + rpc SetCustomVerifyEmailMessageText(SetCustomVerifyEmailMessageTextRequest) returns (SetCustomVerifyEmailMessageTextResponse) { + option (google.api.http) = { + put: "/text/message/verifyemail/{language}"; + body: "*"; + }; + + option (zitadel.v1.auth_option) = { + permission: "iam.policy.write"; + feature: "custom_text" + }; + } + + // Removes the custom init message text of the organisation + // The default text of the IAM will trigger after + rpc ResetCustomVerifyEmailMessageTextToDefault(ResetCustomVerifyEmailMessageTextToDefaultRequest) returns (ResetCustomVerifyEmailMessageTextToDefaultResponse) { + option (google.api.http) = { + delete: "/text/message/verifyemail/{language}" + }; + + option (zitadel.v1.auth_option) = { + permission: "policy.delete" + }; + } + + //Returns the custom text for verify email message + rpc GetCustomVerifyPhoneMessageText(GetCustomVerifyPhoneMessageTextRequest) returns (GetCustomVerifyPhoneMessageTextResponse) { + option (google.api.http) = { + get: "/text/message/verifyphone/{language}"; + }; + + option (zitadel.v1.auth_option) = { + permission: "iam.policy.read"; + }; + } + + //Sets the default custom text for verify email message + // it impacts all organisations without customized verify email message text + // The Following Variables can be used: + // {{.Code}} {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}} + rpc SetCustomVerifyPhoneMessageText(SetCustomVerifyPhoneMessageTextRequest) returns (SetCustomVerifyPhoneMessageTextResponse) { + option (google.api.http) = { + put: "/text/message/verifyphone/{language}"; + body: "*"; + }; + + option (zitadel.v1.auth_option) = { + permission: "iam.policy.write"; + feature: "custom_text" + }; + } + + // Removes the custom init message text of the organisation + // The default text of the IAM will trigger after + rpc ResetCustomVerifyPhoneMessageTextToDefault(ResetCustomVerifyPhoneMessageTextToDefaultRequest) returns (ResetCustomVerifyPhoneMessageTextToDefaultResponse) { + option (google.api.http) = { + delete: "/text/message/verifyphone/{language}" + }; + + option (zitadel.v1.auth_option) = { + permission: "policy.delete" + }; + } + + //Returns the custom text for domain claimed message + rpc GetCustomDomainClaimedMessageText(GetCustomDomainClaimedMessageTextRequest) returns (GetCustomDomainClaimedMessageTextResponse) { + option (google.api.http) = { + get: "/text/message/domainclaimed/{language}"; + }; + + option (zitadel.v1.auth_option) = { + permission: "iam.policy.read"; + }; + } + + // Sets the default custom text for domain claimed message + // it impacts all organisations without customized domain claimed message text + // The Following Variables can be used: + // {{.Domain}} {{.TempUsername}} {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}} + rpc SetCustomDomainClaimedMessageCustomText(SetCustomDomainClaimedMessageTextRequest) returns (SetCustomDomainClaimedMessageTextResponse) { + option (google.api.http) = { + put: "/text/message/domainclaimed/{language}"; + body: "*"; + }; + + option (zitadel.v1.auth_option) = { + permission: "iam.policy.write"; + feature: "custom_text" + }; + } + + // Removes the custom init message text of the organisation + // The default text of the IAM will trigger after + rpc ResetCustomDomainClaimedMessageTextToDefault(ResetCustomDomainClaimedMessageTextToDefaultRequest) returns (ResetCustomDomainClaimedMessageTextToDefaultResponse) { + option (google.api.http) = { + delete: "/text/message/domainclaimed/{language}" + }; + + option (zitadel.v1.auth_option) = { + permission: "policy.delete" + }; + } + // Returns a identity provider configuration of the organisation rpc GetOrgIDPByID(GetOrgIDPByIDRequest) returns (GetOrgIDPByIDResponse) { option (google.api.http) = { @@ -3838,6 +4033,196 @@ message ResetLabelPolicyToDefaultResponse { zitadel.v1.ObjectDetails details = 1; } +//This is an empty request +message GetCustomInitMessageTextRequest { + string language = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; +} + +message GetCustomInitMessageTextResponse { + zitadel.text.v1.MessageCustomText custom_text = 1; +} + +message SetCustomInitMessageTextRequest { + string language = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"de\"" + } + ]; + string title = 2 [(validate.rules).string = {max_len: 200}]; + string pre_header = 3 [(validate.rules).string = {max_len: 200}]; + string subject = 4 [(validate.rules).string = {max_len: 200}]; + string greeting = 5 [(validate.rules).string = {max_len: 200}]; + string text = 6 [(validate.rules).string = {max_len: 800}]; + string button_text = 7 [(validate.rules).string = {max_len: 200}]; + string footer_text = 8 [(validate.rules).string = {max_len: 200}]; +} + +message SetCustomInitMessageTextResponse { + zitadel.v1.ObjectDetails details = 1; +} + +//This is an empty request +message ResetCustomInitMessageTextToDefaultRequest { + string language = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; +} + +message ResetCustomInitMessageTextToDefaultResponse { + zitadel.v1.ObjectDetails details = 1; +} + +//This is an empty request +message GetCustomPasswordResetMessageTextRequest { + string language = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; +} + +message GetCustomPasswordResetMessageTextResponse { + zitadel.text.v1.MessageCustomText custom_text = 1; +} + +message SetCustomPasswordResetMessageTextRequest { + string language = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"de\"" + } + ]; + string title = 2 [(validate.rules).string = {max_len: 200}]; + string pre_header = 3 [(validate.rules).string = {max_len: 200}]; + string subject = 4 [(validate.rules).string = {max_len: 200}]; + string greeting = 5 [(validate.rules).string = {max_len: 200}]; + string text = 6 [(validate.rules).string = {max_len: 800}]; + string button_text = 7 [(validate.rules).string = {max_len: 200}]; + string footer_text = 8 [(validate.rules).string = {max_len: 200}]; +} + +message SetCustomPasswordResetMessageTextResponse { + zitadel.v1.ObjectDetails details = 1; +} + +//This is an empty request +message ResetCustomPasswordResetMessageTextToDefaultRequest { + string language = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; +} + +message ResetCustomPasswordResetMessageTextToDefaultResponse { + zitadel.v1.ObjectDetails details = 1; +} + +//This is an empty request +message GetCustomVerifyEmailMessageTextRequest { + string language = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; +} + +message GetCustomVerifyEmailMessageTextResponse { + zitadel.text.v1.MessageCustomText custom_text = 1; +} + +message SetCustomVerifyEmailMessageTextRequest { + string language = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"de\"" + } + ]; + string title = 2 [(validate.rules).string = {max_len: 200}]; + string pre_header = 3 [(validate.rules).string = {max_len: 200}]; + string subject = 4 [(validate.rules).string = {max_len: 200}]; + string greeting = 5 [(validate.rules).string = {max_len: 200}]; + string text = 6 [(validate.rules).string = {max_len: 800}]; + string button_text = 7 [(validate.rules).string = {max_len: 200}]; + string footer_text = 8 [(validate.rules).string = {max_len: 200}]; +} + +message SetCustomVerifyEmailMessageTextResponse { + zitadel.v1.ObjectDetails details = 1; +} + +//This is an empty request +message ResetCustomVerifyEmailMessageTextToDefaultRequest { + string language = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; +} + +message ResetCustomVerifyEmailMessageTextToDefaultResponse { + zitadel.v1.ObjectDetails details = 1; +} + +//This is an empty request +message GetCustomVerifyPhoneMessageTextRequest { + string language = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; +} + +message GetCustomVerifyPhoneMessageTextResponse { + zitadel.text.v1.MessageCustomText custom_text = 1; +} + +message SetCustomVerifyPhoneMessageTextRequest { + string language = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"de\"" + } + ]; + string title = 2 [(validate.rules).string = {max_len: 200}]; + string pre_header = 3 [(validate.rules).string = {max_len: 200}]; + string subject = 4 [(validate.rules).string = {max_len: 200}]; + string greeting = 5 [(validate.rules).string = {max_len: 200}]; + string text = 6 [(validate.rules).string = {max_len: 800}]; + string button_text = 7 [(validate.rules).string = {max_len: 200}]; + string footer_text = 8 [(validate.rules).string = {max_len: 200}]; +} + +message SetCustomVerifyPhoneMessageTextResponse { + zitadel.v1.ObjectDetails details = 1; +} + +//This is an empty request +message ResetCustomVerifyPhoneMessageTextToDefaultRequest { + string language = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; +} + +message ResetCustomVerifyPhoneMessageTextToDefaultResponse { + zitadel.v1.ObjectDetails details = 1; +} + +//This is an empty request +message GetCustomDomainClaimedMessageTextRequest { + string language = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; +} + +message GetCustomDomainClaimedMessageTextResponse { + zitadel.text.v1.MessageCustomText custom_text = 1; +} + +message SetCustomDomainClaimedMessageTextRequest { + string language = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"de\"" + } + ]; + string title = 2 [(validate.rules).string = {max_len: 200}]; + string pre_header = 3 [(validate.rules).string = {max_len: 200}]; + string subject = 4 [(validate.rules).string = {max_len: 200}]; + string greeting = 5 [(validate.rules).string = {max_len: 200}]; + string text = 6 [(validate.rules).string = {max_len: 800}]; + string button_text = 7 [(validate.rules).string = {max_len: 200}]; + string footer_text = 8 [(validate.rules).string = {max_len: 200}]; +} + +message SetCustomDomainClaimedMessageTextResponse { + zitadel.v1.ObjectDetails details = 1; +} + +//This is an empty request +message ResetCustomDomainClaimedMessageTextToDefaultRequest { + string language = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; +} + +message ResetCustomDomainClaimedMessageTextToDefaultResponse { + zitadel.v1.ObjectDetails details = 1; +} + message GetOrgIDPByIDRequest { string id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; } diff --git a/proto/zitadel/text.proto b/proto/zitadel/text.proto new file mode 100644 index 0000000000..0be3598dad --- /dev/null +++ b/proto/zitadel/text.proto @@ -0,0 +1,47 @@ +syntax = "proto3"; + +import "zitadel/object.proto"; +import "protoc-gen-openapiv2/options/annotations.proto"; + +package zitadel.text.v1; + +option go_package ="github.com/caos/zitadel/pkg/grpc/text"; + +message MessageCustomText { + zitadel.v1.ObjectDetails details = 1; + string title = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "custom text for email title" + } + ]; + string pre_header = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "custom text for email pre header" + } + ]; + string subject = 4 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "custom text for email subject" + } + ]; + string greeting = 5 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "custom text for email greeting" + } + ]; + string text = 6 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "custom text for email text" + } + ]; + string button_text = 7 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "custom text for email button_text" + } + ]; + string footer_text = 8 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "custom text for email footer_text" + } + ]; +}