From b3741f86c8b41804bd11f8de21e7c78930d4dc34 Mon Sep 17 00:00:00 2001 From: fabio Date: Sun, 5 Apr 2026 20:46:35 +0200 Subject: [PATCH] Add signup and email templates for user registration and password reset - Created a new HTML file for the signup page at `backend/spa/signup/index.html`. - Added HTML and plain text templates for password reset emails at `backend/templates/mailTemplates/password_reset.html.tmpl` and `backend/templates/mailTemplates/password_reset.txt.tmpl`. - Added HTML and plain text templates for registration confirmation emails at `backend/templates/mailTemplates/registration.html.tmpl` and `backend/templates/mailTemplates/registration.txt.tmpl`. --- README.md | 7 - backend/GeneratedCode/generatedTypescript.ts | 472 +++++++++--------- backend/cmd/server/main.go | 27 +- backend/configs/config.json | 3 +- backend/data/data.db | Bin 946176 -> 946176 bytes .../admin.go => admin/controller.go} | 15 +- backend/internal/admin/routes.go | 19 + .../{controller/auth.go => controller.go} | 106 ++-- .../internal/auth/{model/auth.go => model.go} | 23 +- .../internal/auth/{service => }/password.go | 2 +- backend/internal/auth/{model => }/request.go | 2 +- .../internal/auth/{endpoint => }/routes.go | 11 +- .../auth/{service/auth.go => service.go} | 46 +- backend/internal/authorization/controller.go | 85 ++++ backend/internal/config/config.go | 5 +- .../helpers.go => helpers/services.go} | 6 +- .../http/controllers/authorization.go | 223 --------- .../http/controllers/response_types.go | 6 - backend/internal/http/routes/admin_routes.go | 17 - backend/internal/http/routes/routes.go | 27 - .../response.go => responses/services.go} | 7 +- backend/internal/roles/main_roles.go | 19 +- backend/internal/routes/register.go | 29 ++ backend/internal/seed/seed.go | 4 +- .../routes.go} | 6 +- .../tokenhash.go => tokens/services.go} | 2 +- .../user.go => user/controller.go} | 58 ++- .../routes/user_routes.go => user/routes.go} | 14 +- .../validation.go => validation/services.go} | 2 +- .../http/static => }/spa/about/index.html | 0 .../spa/assets/AboutUsPage-2zvnAMYl.js | 0 .../spa/assets/AboutUsPage-BH0yEEbx.css | 0 .../spa/assets/AdminLayout-DN9f9DOf.js | 0 .../spa/assets/ApiEndpointsPage-JesCyaiy.js | 0 .../spa/assets/ApiEndpointsPage-gOL3JcKs.css | 0 .../spa/assets/ClosePopup-DV5_zFD6.js | 0 .../spa/assets/ContactUsPage-B3eNo5Fm.js | 0 .../spa/assets/ContactUsPage-B5Y6EbXq.css | 0 .../spa/assets/DevLayout-6orPgqWc.js | 0 .../spa/assets/DoctorDetailPage-Cjwb6JKF.css | 0 .../spa/assets/DoctorDetailPage-DNWM4IpO.js | 0 .../spa/assets/DoctorsPage-DHzbb8aG.css | 0 .../spa/assets/DoctorsPage-Mx2NaaFf.js | 0 .../spa/assets/ErrorNotFound-C1ZsCLAD.js | 0 .../spa/assets/HomeHeader-CzzvZK70.css | 0 .../spa/assets/HomeHeader-oAwoUp8W.js | 0 .../spa/assets/IndexPage-DfNuzFub.js | 0 .../spa/assets/IndexPage-DnAgFSQ-.css | 0 .../spa/assets/IndexPage-HuzieOt1.js | 0 .../spa/assets/IndexPage-JYcDosFI.css | 0 .../spa/assets/IndexPage-qHfbNmL8.js | 0 ...ylUAMQXC89YmC2DPNWuYjalmUiAw-BepdiOnY.woff | Bin ...ylUAMQXC89YmC2DPNWuZtalmUiAw-4ZhHFPot.woff | Bin ...ylUAMQXC89YmC2DPNWuaabVmUiAw-CNa4tw4G.woff | Bin ...ylUAMQXC89YmC2DPNWub2bVmUiAw-CHKg1YId.woff | Bin ...ylUAMQXC89YmC2DPNWubEbFmUiAw-yBxCyPWP.woff | Bin ...ylUAMQXC89YmC2DPNWubEbVmUiAw-3fZ6d7DD.woff | Bin .../spa/assets/LoginPage-BZ9IbcZT.css | 0 .../spa/assets/LoginPage-BgmpoZun.js | 0 .../spa/assets/MailDebugPage-DxWNUslA.js | 0 .../spa/assets/MailDebugPage-_Fae84OI.css | 0 .../spa/assets/MainLayout-BUVfGOmu.css | 0 .../spa/assets/MainLayout-BqadnCMs.js | 0 .../static => }/spa/assets/QBadge-2h1D8yZx.js | 0 .../spa/assets/QDrawer-1VLwP6kh.js | 0 .../static => }/spa/assets/QForm-CgLsd63I.js | 0 .../spa/assets/QLayout-DZVoSyXn.js | 0 .../spa/assets/QLinearProgress-BW3XWqZ_.js | 0 .../static => }/spa/assets/QPage-B68JuyjH.js | 0 .../spa/assets/QResizeObserver-CenQZ1Fm.js | 0 .../spa/assets/QSelect-Bmx_AjWr.js | 0 .../spa/assets/QToolbar-DdNWxglI.js | 0 .../assets/RecoverPasswordPage-CJDwxHNJ.js | 0 .../assets/RecoverPasswordPage-DrvyyIKj.css | 0 .../spa/assets/ResetPasswordPage-DC-tQ0cd.js | 0 .../spa/assets/ResetPasswordPage-DMBJffyk.css | 0 .../spa/assets/ServicesPage-CEUh_7ZB.js | 0 .../spa/assets/ServicesPage-DAWy2Xhp.css | 0 .../spa/assets/SignupPage-B_w5JCSL.js | 0 .../spa/assets/SignupPage-BdSQ-zc-.css | 0 .../spa/assets/UsersPage-COHOm8T7.js | 0 .../spa/assets/UsersPage-CyhBpEuA.css | 0 .../_plugin-vue_export-helper-DlAUqK2U.js | 0 .../spa/assets/about-img-1-Bg9mkIhK.jpg | Bin .../spa/assets/about-img-2-BboiNeez.jpg | Bin .../static => }/spa/assets/api-RGUeM09o.js | 0 .../spa/assets/cta-img-1-Bi1YsVtQ.png | Bin .../spa/assets/cta-img-2-Brfqav_y.png | Bin ...flUhRq6tzZclQEJ-Vdg-IuiaDsNa-Dr0goTwe.woff | Bin ...tzZclQEJ-Vdg-IuiaDsNcIhQ8tQ-D-x-0Q06.woff2 | Bin .../static => }/spa/assets/format-DaKpMo2W.js | 0 .../spa/assets/hero-img-D6ekzwy-.png | Bin .../static => }/spa/assets/i18n-BdDWi-np.js | 0 .../spa/assets/icon-about-info-2-xhkDUeRt.svg | 0 .../spa/assets/icon-about-info-3-DWJgo5gs.svg | 0 .../spa/assets/icon-faq-cta-HccIooJi.svg | 0 .../spa/assets/icon-service-2-CyvmgH6D.svg | 0 .../static => }/spa/assets/index-CxY4d-3p.css | 0 .../static => }/spa/assets/index-DAvBaevK.js | 0 .../spa/assets/instagram-t8lEWnnA.svg | 0 .../static => }/spa/assets/logo-7g001S5W.png | Bin .../static => }/spa/assets/logo-DdmK5n0b.js | 0 .../spa/assets/position-engine-BHgB6lrx.js | 0 .../spa/assets/selection-DrSF90ET.js | 0 .../spa/assets/service-img-1-DW0zEdQo.jpg | Bin .../spa/assets/service-img-2-C5-3A23e.jpg | Bin .../spa/assets/service-img-3-Dqdyjcu5.jpg | Bin .../spa/assets/team-1-CKnEIDo_.jpg | Bin .../static => }/spa/assets/team-1-CMaNLVo5.js | 0 .../spa/assets/team-2-tlSyplqu.jpg | Bin .../spa/assets/team-3-UsSQDZ5X.jpg | Bin .../static => }/spa/assets/team-4-BDlfXLz_.js | 0 .../spa/assets/team-4-CKCxoRxO.jpg | Bin .../static => }/spa/assets/touch-BjYP5sR0.js | 0 .../spa/assets/use-quasar-B5tVCAcV.js | 0 .../spa/assets/work-step-img-1-BTXGLo3T.jpg | Bin .../spa/assets/work-step-img-2-BmLBMPhT.jpg | Bin .../spa/assets/work-step-img-3-B1BPr4WH.jpg | Bin .../spa/assets/work-step-img-4-BbeUh9vy.jpg | Bin .../http/static => }/spa/contact/index.html | 0 .../static => }/spa/doctordetails/index.html | 0 .../http/static => }/spa/doctors/index.html | 0 .../http/static => }/spa/favicon.ico | Bin .../static => }/spa/icons/favicon-128x128.png | Bin .../static => }/spa/icons/favicon-16x16.png | Bin .../static => }/spa/icons/favicon-32x32.png | Bin .../static => }/spa/icons/favicon-96x96.png | Bin .../{internal/http/static => }/spa/index.html | 0 .../http/static => }/spa/login/index.html | 0 .../spa/recoverpassword/index.html | 0 .../http/static => }/spa/services/index.html | 0 .../http/static => }/spa/signup/index.html | 0 .../mailTemplates}/password_reset.html.tmpl | 0 .../mailTemplates}/password_reset.txt.tmpl | 0 .../mailTemplates}/registration.html.tmpl | 0 .../mailTemplates}/registration.txt.tmpl | 0 136 files changed, 532 insertions(+), 711 deletions(-) rename backend/internal/{http/controllers/admin.go => admin/controller.go} (88%) create mode 100644 backend/internal/admin/routes.go rename backend/internal/auth/{controller/auth.go => controller.go} (77%) rename backend/internal/auth/{model/auth.go => model.go} (55%) rename backend/internal/auth/{service => }/password.go (98%) rename backend/internal/auth/{model => }/request.go (97%) rename backend/internal/auth/{endpoint => }/routes.go (78%) rename backend/internal/auth/{service/auth.go => service.go} (71%) create mode 100644 backend/internal/authorization/controller.go rename backend/internal/{http/controllers/helpers.go => helpers/services.go} (98%) delete mode 100644 backend/internal/http/controllers/authorization.go delete mode 100644 backend/internal/http/controllers/response_types.go delete mode 100644 backend/internal/http/routes/admin_routes.go delete mode 100644 backend/internal/http/routes/routes.go rename backend/internal/{http/controllers/response.go => responses/services.go} (69%) create mode 100644 backend/internal/routes/register.go rename backend/internal/{http/routes/system_routes.go => systemUtils/routes.go} (95%) rename backend/internal/{http/controllers/tokenhash.go => tokens/services.go} (91%) rename backend/internal/{http/controllers/user.go => user/controller.go} (83%) rename backend/internal/{http/routes/user_routes.go => user/routes.go} (66%) rename backend/internal/{http/controllers/validation.go => validation/services.go} (97%) rename backend/{internal/http/static => }/spa/about/index.html (100%) rename backend/{internal/http/static => }/spa/assets/AboutUsPage-2zvnAMYl.js (100%) rename backend/{internal/http/static => }/spa/assets/AboutUsPage-BH0yEEbx.css (100%) rename backend/{internal/http/static => }/spa/assets/AdminLayout-DN9f9DOf.js (100%) rename backend/{internal/http/static => }/spa/assets/ApiEndpointsPage-JesCyaiy.js (100%) rename backend/{internal/http/static => }/spa/assets/ApiEndpointsPage-gOL3JcKs.css (100%) rename backend/{internal/http/static => }/spa/assets/ClosePopup-DV5_zFD6.js (100%) rename backend/{internal/http/static => }/spa/assets/ContactUsPage-B3eNo5Fm.js (100%) rename backend/{internal/http/static => }/spa/assets/ContactUsPage-B5Y6EbXq.css (100%) rename backend/{internal/http/static => }/spa/assets/DevLayout-6orPgqWc.js (100%) rename backend/{internal/http/static => }/spa/assets/DoctorDetailPage-Cjwb6JKF.css (100%) rename backend/{internal/http/static => }/spa/assets/DoctorDetailPage-DNWM4IpO.js (100%) rename backend/{internal/http/static => }/spa/assets/DoctorsPage-DHzbb8aG.css (100%) rename backend/{internal/http/static => }/spa/assets/DoctorsPage-Mx2NaaFf.js (100%) rename backend/{internal/http/static => }/spa/assets/ErrorNotFound-C1ZsCLAD.js (100%) rename backend/{internal/http/static => }/spa/assets/HomeHeader-CzzvZK70.css (100%) rename backend/{internal/http/static => }/spa/assets/HomeHeader-oAwoUp8W.js (100%) rename backend/{internal/http/static => }/spa/assets/IndexPage-DfNuzFub.js (100%) rename backend/{internal/http/static => }/spa/assets/IndexPage-DnAgFSQ-.css (100%) rename backend/{internal/http/static => }/spa/assets/IndexPage-HuzieOt1.js (100%) rename backend/{internal/http/static => }/spa/assets/IndexPage-JYcDosFI.css (100%) rename backend/{internal/http/static => }/spa/assets/IndexPage-qHfbNmL8.js (100%) rename backend/{internal/http/static => }/spa/assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWuYjalmUiAw-BepdiOnY.woff (100%) rename backend/{internal/http/static => }/spa/assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWuZtalmUiAw-4ZhHFPot.woff (100%) rename backend/{internal/http/static => }/spa/assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWuaabVmUiAw-CNa4tw4G.woff (100%) rename backend/{internal/http/static => }/spa/assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWub2bVmUiAw-CHKg1YId.woff (100%) rename backend/{internal/http/static => }/spa/assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWubEbFmUiAw-yBxCyPWP.woff (100%) rename backend/{internal/http/static => }/spa/assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWubEbVmUiAw-3fZ6d7DD.woff (100%) rename backend/{internal/http/static => }/spa/assets/LoginPage-BZ9IbcZT.css (100%) rename backend/{internal/http/static => }/spa/assets/LoginPage-BgmpoZun.js (100%) rename backend/{internal/http/static => }/spa/assets/MailDebugPage-DxWNUslA.js (100%) rename backend/{internal/http/static => }/spa/assets/MailDebugPage-_Fae84OI.css (100%) rename backend/{internal/http/static => }/spa/assets/MainLayout-BUVfGOmu.css (100%) rename backend/{internal/http/static => }/spa/assets/MainLayout-BqadnCMs.js (100%) rename backend/{internal/http/static => }/spa/assets/QBadge-2h1D8yZx.js (100%) rename backend/{internal/http/static => }/spa/assets/QDrawer-1VLwP6kh.js (100%) rename backend/{internal/http/static => }/spa/assets/QForm-CgLsd63I.js (100%) rename backend/{internal/http/static => }/spa/assets/QLayout-DZVoSyXn.js (100%) rename backend/{internal/http/static => }/spa/assets/QLinearProgress-BW3XWqZ_.js (100%) rename backend/{internal/http/static => }/spa/assets/QPage-B68JuyjH.js (100%) rename backend/{internal/http/static => }/spa/assets/QResizeObserver-CenQZ1Fm.js (100%) rename backend/{internal/http/static => }/spa/assets/QSelect-Bmx_AjWr.js (100%) rename backend/{internal/http/static => }/spa/assets/QToolbar-DdNWxglI.js (100%) rename backend/{internal/http/static => }/spa/assets/RecoverPasswordPage-CJDwxHNJ.js (100%) rename backend/{internal/http/static => }/spa/assets/RecoverPasswordPage-DrvyyIKj.css (100%) rename backend/{internal/http/static => }/spa/assets/ResetPasswordPage-DC-tQ0cd.js (100%) rename backend/{internal/http/static => }/spa/assets/ResetPasswordPage-DMBJffyk.css (100%) rename backend/{internal/http/static => }/spa/assets/ServicesPage-CEUh_7ZB.js (100%) rename backend/{internal/http/static => }/spa/assets/ServicesPage-DAWy2Xhp.css (100%) rename backend/{internal/http/static => }/spa/assets/SignupPage-B_w5JCSL.js (100%) rename backend/{internal/http/static => }/spa/assets/SignupPage-BdSQ-zc-.css (100%) rename backend/{internal/http/static => }/spa/assets/UsersPage-COHOm8T7.js (100%) rename backend/{internal/http/static => }/spa/assets/UsersPage-CyhBpEuA.css (100%) rename backend/{internal/http/static => }/spa/assets/_plugin-vue_export-helper-DlAUqK2U.js (100%) rename backend/{internal/http/static => }/spa/assets/about-img-1-Bg9mkIhK.jpg (100%) rename backend/{internal/http/static => }/spa/assets/about-img-2-BboiNeez.jpg (100%) rename backend/{internal/http/static => }/spa/assets/api-RGUeM09o.js (100%) rename backend/{internal/http/static => }/spa/assets/cta-img-1-Bi1YsVtQ.png (100%) rename backend/{internal/http/static => }/spa/assets/cta-img-2-Brfqav_y.png (100%) rename backend/{internal/http/static => }/spa/assets/flUhRq6tzZclQEJ-Vdg-IuiaDsNa-Dr0goTwe.woff (100%) rename backend/{internal/http/static => }/spa/assets/flUhRq6tzZclQEJ-Vdg-IuiaDsNcIhQ8tQ-D-x-0Q06.woff2 (100%) rename backend/{internal/http/static => }/spa/assets/format-DaKpMo2W.js (100%) rename backend/{internal/http/static => }/spa/assets/hero-img-D6ekzwy-.png (100%) rename backend/{internal/http/static => }/spa/assets/i18n-BdDWi-np.js (100%) rename backend/{internal/http/static => }/spa/assets/icon-about-info-2-xhkDUeRt.svg (100%) rename backend/{internal/http/static => }/spa/assets/icon-about-info-3-DWJgo5gs.svg (100%) rename backend/{internal/http/static => }/spa/assets/icon-faq-cta-HccIooJi.svg (100%) rename backend/{internal/http/static => }/spa/assets/icon-service-2-CyvmgH6D.svg (100%) rename backend/{internal/http/static => }/spa/assets/index-CxY4d-3p.css (100%) rename backend/{internal/http/static => }/spa/assets/index-DAvBaevK.js (100%) rename backend/{internal/http/static => }/spa/assets/instagram-t8lEWnnA.svg (100%) rename backend/{internal/http/static => }/spa/assets/logo-7g001S5W.png (100%) rename backend/{internal/http/static => }/spa/assets/logo-DdmK5n0b.js (100%) rename backend/{internal/http/static => }/spa/assets/position-engine-BHgB6lrx.js (100%) rename backend/{internal/http/static => }/spa/assets/selection-DrSF90ET.js (100%) rename backend/{internal/http/static => }/spa/assets/service-img-1-DW0zEdQo.jpg (100%) rename backend/{internal/http/static => }/spa/assets/service-img-2-C5-3A23e.jpg (100%) rename backend/{internal/http/static => }/spa/assets/service-img-3-Dqdyjcu5.jpg (100%) rename backend/{internal/http/static => }/spa/assets/team-1-CKnEIDo_.jpg (100%) rename backend/{internal/http/static => }/spa/assets/team-1-CMaNLVo5.js (100%) rename backend/{internal/http/static => }/spa/assets/team-2-tlSyplqu.jpg (100%) rename backend/{internal/http/static => }/spa/assets/team-3-UsSQDZ5X.jpg (100%) rename backend/{internal/http/static => }/spa/assets/team-4-BDlfXLz_.js (100%) rename backend/{internal/http/static => }/spa/assets/team-4-CKCxoRxO.jpg (100%) rename backend/{internal/http/static => }/spa/assets/touch-BjYP5sR0.js (100%) rename backend/{internal/http/static => }/spa/assets/use-quasar-B5tVCAcV.js (100%) rename backend/{internal/http/static => }/spa/assets/work-step-img-1-BTXGLo3T.jpg (100%) rename backend/{internal/http/static => }/spa/assets/work-step-img-2-BmLBMPhT.jpg (100%) rename backend/{internal/http/static => }/spa/assets/work-step-img-3-B1BPr4WH.jpg (100%) rename backend/{internal/http/static => }/spa/assets/work-step-img-4-BbeUh9vy.jpg (100%) rename backend/{internal/http/static => }/spa/contact/index.html (100%) rename backend/{internal/http/static => }/spa/doctordetails/index.html (100%) rename backend/{internal/http/static => }/spa/doctors/index.html (100%) rename backend/{internal/http/static => }/spa/favicon.ico (100%) rename backend/{internal/http/static => }/spa/icons/favicon-128x128.png (100%) rename backend/{internal/http/static => }/spa/icons/favicon-16x16.png (100%) rename backend/{internal/http/static => }/spa/icons/favicon-32x32.png (100%) rename backend/{internal/http/static => }/spa/icons/favicon-96x96.png (100%) rename backend/{internal/http/static => }/spa/index.html (100%) rename backend/{internal/http/static => }/spa/login/index.html (100%) rename backend/{internal/http/static => }/spa/recoverpassword/index.html (100%) rename backend/{internal/http/static => }/spa/services/index.html (100%) rename backend/{internal/http/static => }/spa/signup/index.html (100%) rename backend/{internal/http/templates => templates/mailTemplates}/password_reset.html.tmpl (100%) rename backend/{internal/http/templates => templates/mailTemplates}/password_reset.txt.tmpl (100%) rename backend/{internal/http/templates => templates/mailTemplates}/registration.html.tmpl (100%) rename backend/{internal/http/templates => templates/mailTemplates}/registration.txt.tmpl (100%) diff --git a/README.md b/README.md index d25075b..76f4321 100644 --- a/README.md +++ b/README.md @@ -2,10 +2,3 @@ bakend in GO frontend quasar framework con generazione delle pagine statiche per la parte public - -internal - auth - model - controller - service - endpoint diff --git a/backend/GeneratedCode/generatedTypescript.ts b/backend/GeneratedCode/generatedTypescript.ts index 4d24791..1a0ff9d 100644 --- a/backend/GeneratedCode/generatedTypescript.ts +++ b/backend/GeneratedCode/generatedTypescript.ts @@ -4,7 +4,7 @@ // // This file was generated by github.com/millevolte/ts-rpc // -// Apr 05, 2026 17:08:11 UTC +// Apr 05, 2026 20:12:24 UTC // export interface ApiRestResponse { @@ -281,11 +281,178 @@ export type Nullable = T | null; export type Record = { [P in K]: T }; // -// package model +// package systemUtils // -export interface RefreshRequest { - refresh_token: string; +// Typescript: TSEndpoint= path=/metrics; name=metrics; method=GET; response=string +// internal/systemUtils/routes.go Line: 37 +export const metrics = async (): Promise<{ + data: string; + error: Nullable; +}> => { + return (await api.GET("/metrics")) as { + data: string; + error: Nullable; + }; +}; + +// Typescript: TSEndpoint= path=/maildebug; name=mailDebug; method=GET; response=routes.[]MailDebugItem +// internal/systemUtils/routes.go Line: 48 +export const mailDebug = async (): Promise<{ + data: MailDebugItem[]; + error: Nullable; +}> => { + return (await api.GET("/maildebug")) as { + data: MailDebugItem[]; + error: Nullable; + }; +}; + +// Typescript: TSEndpoint= path=/health; name=health; method=GET; response=string +// internal/systemUtils/routes.go Line: 34 +export const health = async (): Promise<{ + data: string; + error: Nullable; +}> => { + return (await api.GET("/health")) as { + data: string; + error: Nullable; + }; +}; + +export interface MailDebugItem { + name: string; + content: string; +} + +// +// package admin +// + +// Typescript: TSEndpoint= path=/admin/users; name=listUsers; method=POST; request=admin.ListUsersRequest; response=models.[]UserShort +// internal/admin/routes.go Line: 12 + +export const listUsers = async ( + data: ListUsersRequest, +): Promise<{ data: UserShort[]; error: Nullable }> => { + return (await api.POST("/admin/users", data)) as { + data: UserShort[]; + error: Nullable; + }; +}; + +// Typescript: TSEndpoint= path=/admin/users/:uuid/block; name=blockUser; method=PUT; request=admin.BlockUserRequest; response=models.UserShort +// internal/admin/routes.go Line: 16 + +export const blockUser = async ( + data: BlockUserRequest, +): Promise<{ data: UserShort; error: Nullable }> => { + return (await api.PUT("/admin/users/:uuid/block", data)) as { + data: UserShort; + error: Nullable; + }; +}; + +export interface BlockUserRequest { + action: string; +} + +export interface ListUsersRequest { + page: number; + pageSize: number; +} + +// +// package auth +// + +// Typescript: TSEndpoint= path=/auth/refresh; name=refresh; method=POST; request=model.RefreshRequest; response=model.TokenPair +// internal/auth/routes.go Line: 23 + +export const refresh = async ( + data: RefreshRequest, +): Promise<{ data: TokenPair; error: Nullable }> => { + return (await api.POST("/auth/refresh", data)) as { + data: TokenPair; + error: Nullable; + }; +}; + +// Typescript: TSEndpoint= path=/auth/me; name=me; method=GET; response=models.UserShort +// internal/auth/routes.go Line: 26 +export const me = async (): Promise<{ + data: UserShort; + error: Nullable; +}> => { + return (await api.GET("/auth/me")) as { + data: UserShort; + error: Nullable; + }; +}; + +// Typescript: TSEndpoint= path=/auth/register; name=register; method=POST; request=models.UserCreateInput; response=models.UserShort +// internal/auth/routes.go Line: 29 + +export const register = async ( + data: UserCreateInput, +): Promise<{ data: UserShort; error: Nullable }> => { + return (await api.POST("/auth/register", data)) as { + data: UserShort; + error: Nullable; + }; +}; + +// Typescript: TSEndpoint= path=/auth/password/forgot; name=forgotPassword; method=POST; request=model.ForgotPasswordRequest; response=controllers.SimpleResponse +// internal/auth/routes.go Line: 32 + +export const forgotPassword = async ( + data: ForgotPasswordRequest, +): Promise<{ data: SimpleResponse; error: Nullable }> => { + return (await api.POST("/auth/password/forgot", data)) as { + data: SimpleResponse; + error: Nullable; + }; +}; + +// Typescript: TSEndpoint= path=/auth/password/reset; name=resetPassword; method=POST; request=model.ResetPasswordRequest; response=controllers.SimpleResponse +// internal/auth/routes.go Line: 35 + +export const resetPassword = async ( + data: ResetPasswordRequest, +): Promise<{ data: SimpleResponse; error: Nullable }> => { + return (await api.POST("/auth/password/reset", data)) as { + data: SimpleResponse; + error: Nullable; + }; +}; + +// Typescript: TSEndpoint= path=/auth/password/valid; name=validToken; method=POST; request=string; response=controllers.SimpleResponse +// internal/auth/routes.go Line: 38 + +export const validToken = async ( + data: string, +): Promise<{ data: SimpleResponse; error: Nullable }> => { + return (await api.POST("/auth/password/valid", data)) as { + data: SimpleResponse; + error: Nullable; + }; +}; + +// Typescript: TSEndpoint= path=/auth/login; name=login; method=POST; request=model.LoginRequest; response=model.TokenPair +// internal/auth/routes.go Line: 20 + +export const login = async ( + data: LoginRequest, +): Promise<{ data: TokenPair; error: Nullable }> => { + return (await api.POST("/auth/login", data)) as { + data: TokenPair; + error: Nullable; + }; +}; + +export interface ResetPasswordRequest { + token: string; + password: string; } export interface TokenPair { @@ -302,27 +469,60 @@ export interface LoginRequest { password: string; } -export interface ResetPasswordRequest { - token: string; - password: string; +export interface RefreshRequest { + refresh_token: string; } // -// package controllers +// package user // -export interface BlockUserRequest { - action: string; -} +// Typescript: TSEndpoint= path=/users/:uuid; name=updateUser; method=PUT; request=controllers.UpdateUserRequest; response=models.UserProfile +// internal/user/routes.go Line: 18 -export interface ListUsersRequest { - page: number; - pageSize: number; -} +export const updateUser = async ( + data: UpdateUserRequest, +): Promise<{ data: UserProfile; error: Nullable }> => { + return (await api.PUT("/users/:uuid", data)) as { + data: UserProfile; + error: Nullable; + }; +}; -export interface SimpleResponse { - message: string; -} +// Typescript: TSEndpoint= path=/users/:uuid; name=deleteUser; method=DELETE; response=controllers.SimpleResponse +// internal/user/routes.go Line: 21 + +export const deleteUser = async ( + uuid: string, +): Promise<{ data: SimpleResponse; error: Nullable }> => { + return (await api.DELETE(`/users/${uuid}`)) as { + data: SimpleResponse; + error: Nullable; + }; +}; + +// Typescript: TSEndpoint= path=/users/:uuid; name=getUser; method=GET; response=models.UserProfile +// internal/user/routes.go Line: 12 +export const getUser = async ( + uuid: string, +): Promise<{ data: UserProfile; error: Nullable }> => { + return (await api.GET(`/users/${uuid}`)) as { + data: UserProfile; + error: Nullable; + }; +}; + +// Typescript: TSEndpoint= path=/users; name=createUser; method=POST; request=models.UserCreateInput; response=models.UserProfile +// internal/user/routes.go Line: 15 + +export const createUser = async ( + data: UserCreateInput, +): Promise<{ data: UserProfile; error: Nullable }> => { + return (await api.POST("/users", data)) as { + data: UserProfile; + error: Nullable; + }; +}; export interface UpdateUserRequest { name: string; @@ -337,115 +537,16 @@ export interface UpdateUserRequest { } // -// package routes +// package responses // -// Typescript: TSEndpoint= path=/metrics; name=metrics; method=GET; response=string -// internal/http/routes/system_routes.go Line: 37 -export const metrics = async (): Promise<{ - data: string; - error: Nullable; -}> => { - return (await api.GET("/metrics")) as { - data: string; - error: Nullable; - }; -}; +export interface SimpleResponse { + message: string; +} -// Typescript: TSEndpoint= path=/maildebug; name=mailDebug; method=GET; response=routes.[]MailDebugItem -// internal/http/routes/system_routes.go Line: 48 -export const mailDebug = async (): Promise<{ - data: MailDebugItem[]; - error: Nullable; -}> => { - return (await api.GET("/maildebug")) as { - data: MailDebugItem[]; - error: Nullable; - }; -}; - -// Typescript: TSEndpoint= path=/users/:uuid; name=getUser; method=GET; response=models.UserProfile -// internal/http/routes/user_routes.go Line: 13 -export const getUser = async ( - uuid: string, -): Promise<{ data: UserProfile; error: Nullable }> => { - return (await api.GET(`/users/${uuid}`)) as { - data: UserProfile; - error: Nullable; - }; -}; - -// Typescript: TSEndpoint= path=/users/:uuid; name=updateUser; method=PUT; request=controllers.UpdateUserRequest; response=models.UserProfile -// internal/http/routes/user_routes.go Line: 19 - -export const updateUser = async ( - data: UpdateUserRequest, -): Promise<{ data: UserProfile; error: Nullable }> => { - return (await api.PUT("/users/:uuid", data)) as { - data: UserProfile; - error: Nullable; - }; -}; - -// Typescript: TSEndpoint= path=/admin/users; name=listUsers; method=POST; request=controllers.ListUsersRequest; response=models.[]UserShort -// internal/http/routes/admin_routes.go Line: 12 - -export const listUsers = async ( - data: ListUsersRequest, -): Promise<{ data: UserShort[]; error: Nullable }> => { - return (await api.POST("/admin/users", data)) as { - data: UserShort[]; - error: Nullable; - }; -}; - -// Typescript: TSEndpoint= path=/admin/users/:uuid/block; name=blockUser; method=PUT; request=controllers.BlockUserRequest; response=models.UserShort -// internal/http/routes/admin_routes.go Line: 15 - -export const blockUser = async ( - data: BlockUserRequest, -): Promise<{ data: UserShort; error: Nullable }> => { - return (await api.PUT("/admin/users/:uuid/block", data)) as { - data: UserShort; - error: Nullable; - }; -}; - -// Typescript: TSEndpoint= path=/users; name=createUser; method=POST; request=models.UserCreateInput; response=models.UserProfile -// internal/http/routes/user_routes.go Line: 16 - -export const createUser = async ( - data: UserCreateInput, -): Promise<{ data: UserProfile; error: Nullable }> => { - return (await api.POST("/users", data)) as { - data: UserProfile; - error: Nullable; - }; -}; - -// Typescript: TSEndpoint= path=/users/:uuid; name=deleteUser; method=DELETE; response=controllers.SimpleResponse -// internal/http/routes/user_routes.go Line: 22 - -export const deleteUser = async ( - uuid: string, -): Promise<{ data: SimpleResponse; error: Nullable }> => { - return (await api.DELETE(`/users/${uuid}`)) as { - data: SimpleResponse; - error: Nullable; - }; -}; - -// Typescript: TSEndpoint= path=/health; name=health; method=GET; response=string -// internal/http/routes/system_routes.go Line: 34 -export const health = async (): Promise<{ - data: string; - error: Nullable; -}> => { - return (await api.GET("/health")) as { - data: string; - error: Nullable; - }; -}; +// +// package routes +// export interface FormRequest { req: string; @@ -456,99 +557,6 @@ export interface FormResponse { test: string; } -export interface MailDebugItem { - name: string; - content: string; -} - -// -// package endpoint -// - -// Typescript: TSEndpoint= path=/auth/password/forgot; name=forgotPassword; method=POST; request=model.ForgotPasswordRequest; response=controllers.SimpleResponse -// internal/auth/endpoint/routes.go Line: 34 - -export const forgotPassword = async ( - data: ForgotPasswordRequest, -): Promise<{ data: SimpleResponse; error: Nullable }> => { - return (await api.POST("/auth/password/forgot", data)) as { - data: SimpleResponse; - error: Nullable; - }; -}; - -// Typescript: TSEndpoint= path=/auth/password/reset; name=resetPassword; method=POST; request=model.ResetPasswordRequest; response=controllers.SimpleResponse -// internal/auth/endpoint/routes.go Line: 37 - -export const resetPassword = async ( - data: ResetPasswordRequest, -): Promise<{ data: SimpleResponse; error: Nullable }> => { - return (await api.POST("/auth/password/reset", data)) as { - data: SimpleResponse; - error: Nullable; - }; -}; - -// Typescript: TSEndpoint= path=/auth/password/valid; name=validToken; method=POST; request=string; response=controllers.SimpleResponse -// internal/auth/endpoint/routes.go Line: 40 - -export const validToken = async ( - data: string, -): Promise<{ data: SimpleResponse; error: Nullable }> => { - return (await api.POST("/auth/password/valid", data)) as { - data: SimpleResponse; - error: Nullable; - }; -}; - -// Typescript: TSEndpoint= path=/auth/login; name=login; method=POST; request=model.LoginRequest; response=model.TokenPair -// internal/auth/endpoint/routes.go Line: 22 - -export const login = async ( - data: LoginRequest, -): Promise<{ data: TokenPair; error: Nullable }> => { - return (await api.POST("/auth/login", data)) as { - data: TokenPair; - error: Nullable; - }; -}; - -// Typescript: TSEndpoint= path=/auth/refresh; name=refresh; method=POST; request=model.RefreshRequest; response=model.TokenPair -// internal/auth/endpoint/routes.go Line: 25 - -export const refresh = async ( - data: RefreshRequest, -): Promise<{ data: TokenPair; error: Nullable }> => { - return (await api.POST("/auth/refresh", data)) as { - data: TokenPair; - error: Nullable; - }; -}; - -// Typescript: TSEndpoint= path=/auth/me; name=me; method=GET; response=models.UserShort -// internal/auth/endpoint/routes.go Line: 28 -export const me = async (): Promise<{ - data: UserShort; - error: Nullable; -}> => { - return (await api.GET("/auth/me")) as { - data: UserShort; - error: Nullable; - }; -}; - -// Typescript: TSEndpoint= path=/auth/register; name=register; method=POST; request=models.UserCreateInput; response=models.UserShort -// internal/auth/endpoint/routes.go Line: 31 - -export const register = async ( - data: UserCreateInput, -): Promise<{ data: UserShort; error: Nullable }> => { - return (await api.POST("/auth/register", data)) as { - data: UserShort; - error: Nullable; - }; -}; - // // package models // @@ -565,17 +573,6 @@ export interface UserCreateInput { preferences: Nullable; } -export interface UserDetailsShort { - title: string; - firstName: string; - lastName: string; - address: string; - city: string; - zipCode: string; - country: string; - phone: string; -} - export interface UserPreferencesShort { useIdle: boolean; idleTimeout: number; @@ -587,6 +584,17 @@ export interface UserPreferencesShort { language: string; } +export interface UserDetailsShort { + title: string; + firstName: string; + lastName: string; + address: string; + city: string; + zipCode: string; + country: string; + phone: string; +} + export interface UserShort { email: string; name: string; @@ -598,14 +606,14 @@ export interface UserShort { avatar: Nullable; } -export type UserRoles = string[]; - -export type UserStatus = (typeof EnumUserStatus)[keyof typeof EnumUserStatus]; - export type UserTypes = string[]; export type UsersShort = UserShort[]; +export type UserRoles = string[]; + +export type UserStatus = (typeof EnumUserStatus)[keyof typeof EnumUserStatus]; + export const EnumUserStatus = { UserStatusPending: "pending", UserStatusActive: "active", diff --git a/backend/cmd/server/main.go b/backend/cmd/server/main.go index b17541f..46bbf9e 100644 --- a/backend/cmd/server/main.go +++ b/backend/cmd/server/main.go @@ -11,14 +11,13 @@ import ( "syscall" "time" - authmodel "server/internal/auth/model" - authservice "server/internal/auth/service" + "server/internal/auth" + "server/internal/authorization" "server/internal/config" "server/internal/db" - "server/internal/http/controllers" - "server/internal/http/routes" + "server/internal/routes" + "server/internal/mail" - "server/internal/roles" "server/internal/seed" "github.com/gofiber/fiber/v3" @@ -55,7 +54,7 @@ func main() { log.Fatalf("init db: %v", err) } - authService, err := authservice.New(authmodel.Config{ + authService, err := auth.NewAuthService(auth.Config{ Secret: cfg.Auth.Secret, Issuer: cfg.Auth.Issuer, AccessTokenExpiry: time.Duration(cfg.Auth.AccessTokenExpiryMinutes) * time.Minute, @@ -84,19 +83,6 @@ func main() { log.Fatalf("setup mail: %v", err) } - roleConfigPath := cfg.RolesConfigPath - if envRoleConfig := os.Getenv("ROLES_CONFIG_PATH"); envRoleConfig != "" { - roleConfigPath = envRoleConfig - } - if roleConfigPath == "" { - roleConfigPath = envOrDefault("ROLES_CONFIG_PATH", "configs/roles.json") - } - roleResolver, err := controllers.LoadRoleConfig(roleConfigPath) - if err != nil { - log.Fatalf("load role config: %v", err) - } - roles.CheckUserRoleConsistency(dbConn, roleResolver) - app := fiber.New(fiber.Config{ AppName: cfg.AppName, ReadTimeout: time.Duration(cfg.ReadTimeoutSeconds) * time.Second, @@ -139,7 +125,8 @@ func main() { return c.Next() }) - app.Use(controllers.RequireEndpointPermission(roleResolver, authService)) + app.Use(authorization.RequireEndpointPermission(authService, dbConn)) + routes.Register(app, authService, mailService) port := envOrDefault("PORT", "3000") diff --git a/backend/configs/config.json b/backend/configs/config.json index 2050b34..dc6d4af 100644 --- a/backend/configs/config.json +++ b/backend/configs/config.json @@ -14,7 +14,8 @@ "mode": "file", "from": "noreply@example.local", "debug_dir": "data/mail-debug", - "templates_dir": "internal/http/templates", + "templates_dir": "templates", + "mail_templates_dir": "templates/mailTemplates", "frontend_base_url": "http://localhost:9000", "reset_password_path": "/#reset-password", "smtp": { diff --git a/backend/data/data.db b/backend/data/data.db index 6d803aa512ec0fb87230382ae636ae03c1922c59..8f896a6e10256c4a627429932f3ea1aa196533c5 100644 GIT binary patch delta 3257 zcmbtWZERCj7`|t>U$?jIRdDh+LlB53Mbu4xf?SLr%eOsA` zZ#A6@UUpw`bp*}@_V|v`yNIIxVY9Y>I$Za7$x zCBu+(Th~n}%eE=$7zrAd9n-S;$6Z4~U@0aRZBs!ATdE>zl1e^AT_m51Zm1B6j;(2i zEDJ;$S_q1|YG@*~?YL~KSTLf~YE(l&*oKO%KphdQGDMQ*Sf(W*WZ4R`9UCD-K~PW) z$50g;L##mJxF}eLWQ%RNj`J7TT9Oco-W9y;+sX~NI(%12I;Pw`^ey)lI+v^dIbI0d zor0!-L>*zIYETsnUBU)}s$yYPQ%ni#2DU97N}_`dlcYkBbzQcRX<&o+t64@da#>SV z4MU4$MAjrEt0I<>V5^pc9jrppF?7qcbxBcl9YJI{x`R|lQ{-;JDda~eMlK7YVCtr= z$)@f=RksuwBjSdjnn))}7m181I*uqK-4>uDYcj%)V{!bLaW>~02TZ+k52QpT-D##`w9({0j*T%xxi`G$0WRGJu6&0DlU?xZAZp7 zXYm1WnNHW%WPitCO?Gr2aObb=2W#nkA_+!4bb5VdcIRWDIp1i26v%(-fDb@^)C3;^ zKIryQJ}N@7jm#lB34Wj=A?&^Cd6J^Y#=>8^He8~=c>rt&jmx#!bZvc!%!jX)CsS7< zb09%vnwGN@_zraB-|7RKKpdr!0Z zp?#o}c2N{KmYu|)YA){w0ce<0<;kDeL(DIu>UIjaV3jLGG1LvJqA!F47uXHVi1(VO z%XN~bQyA)Rk#wif$gNvq3t4uW2wX2%CX^%IMSudc{i zaYD~2STMoo_tP~^i;nR4*CdD8$qC@8Tda0|#*%VegAy@%o{`uC93g*k|D7a;m4L#8@O>UZFRa$ecJ^p3KG)nU*ig3Tn-U z*`T^+f_mpm@KQNJtuHaw^c697?>Zj2Jpl++5TXgW2)PM)2zd$l2r-2G`P&n$KkP2N z2wxouOtORm`M_lGW-1UQYZZZ;RA?x0lk~)EfmGVh&$=J4A6^}gQxy(2lD%8{`mu$1 zbH{@Y2j`w+^^xW0>RdhF@ro#1h}!0-lDh}?ZVGqqw)%UMR#@fXrf|2tcVJIg2|qfJ z+-bGPpYBvpcjm@X@kO#*_=n@YMslYeeuj*u{$#r%X*?PGvp26;zBA7iG6~U=7weAw z;r-E>9&*2vaApwR{~KcZ2~lm#MY8x!mEAH3`es^4Z0@cLF)MaM|#Kp|4i{D A+5i9m delta 5004 zcmZu!d&uM1b^n}iX6NzE%m)iecJtiLZnB$Pk}K&-x>8EA*p_Yip@(hRmPHd*uWMVD z<(DK&N~n`bm^7q?OonD?OKI{)OKAza+e!Ch+tN-;^LHCs=z~%=gc1sYQV3~i|EWHf zKv-&Yf1rD=&pqedbARXP3vY^FcvJlPdvAaC<@n2TeB%C#xboT8ULL^;c=S3{ zUw-P!`H#!_llPtf@A*&9|Lgq6SKFt5^$zjmwfo=xjYqG2oj-qgoK>z0<%*}CE8*Qw zm+-D9OL*svnqCA@eEFI>V$F5$zM@cbn__u$#@JpbtL|LAv4pT2y3_7a}C zgb!ZA2QJ}5mrEXe=atDRxbe3SuDv=bciG-}@bVisAN}(i;E7Xk<)sJL9@7s#`S?Az zZh!yk53js*>y2Cc>%Utr`@!j>m;UB^XYVSPoZSBY_33$X<)!QYe)FqmKe{@(`ol-h z{GD=o|J8qb^wK|k@9Y$uKlR|+o0rP}^v@%9&QDJi?%22f`s}$IU%r0ziF;SCo&M@OZk&AW z{NO!Twj?5H3vBCwK1NK0W)& z&Fj~{dhO=bU%zsvjQUScuiX7SIDh!$cPqd3u=6LCA3S-t_}DM*+wGoQeCGAa^3sKm z{-WSA^8a)3mO(GRUGST4l!DW{pMT-uPhPD&bJ6~@$}g7+@{jy{UAH{COU?iI=at}6 z^M`*f`1zUc|Dse-#`yX3hh>c4e{uhhE1#*{dt5&6xcB(<-s3Cx9$&rp_=$Uuuiblm z{odmn_a5JT{qfngXRdy{^z}zyx%K8*Ic{Ctdh_;Aj>mH9q~7W-0|GM-qoPBLJY*Fp zIE4^!#&+Tq-Mtu01ianoP}5SlPSeB{2^@`a2N8qa;8ndS1`WQg=IyR?$kI$TJT8%Q zeYhL*T%TfE2e$BqChNL7uN&_B{8F_9M>^w7%~dg z?Xs^_QD+0~Fbt*9(V9)tWnbA;opo2VvaC;Wi)P5J8J`<5`3TnS z7Nv6QWnb8JWdJw6h#3IbJrG0&f<-Z80bFbgLGo()d>nzEA(;gM2gdpZA| z@u9jj9Ulu;9Qr6HVY@ta+jjAh;o785Uc|)9x{V;szXjy zMNtd|Tpn+cumdqLj*YP~Z3mz^06xDRK_42;79S3{UT4BNUI+|}=J>3)wsTt;4I^Wm zdwZVVP6~6pjxZk8dYkn$LU1*;Sh@_(l*A%}ux((N0TCnK1v!>UYN198FPy4{TDiof zd^~euYT{$q=Adw>?;+?eVcZVpgMpmwS-QcFNu9_ep0p>n8g`PNWKT`rcln+(1rju4 zuk=0@)3nK_@t7xkYas44%`g^ps;&|csqo+jR!Rkw2MBlIA?#Oi-NSuU>_wJoGc?yP zj`1LBn{+trXY49%1X%W{trd=1-fW5!t>=i-nK-CfT+-PrU=d1W=G!5iXEQzy70h^3 zL5~Iww{P<_fFwsIP9}JA;K4?5dk(#wCzeQBs}{1dv?jDVI0(G7?Z?b&wDMc<=*>a4 zUkGCohtfFl;e0cvBtF>IMV?hO0+))e>peq8)&07cVRE2XY>TbK+C~BKa+N|LMX*q+ z8RW&Dcc|)7i`m;Uj~roFQ(enbromW~Ms~vsJvUb8f@2GDOKdoZ@TheZs!puc8`g3z zMO<6*G^OFVt3hdGzcC#^KUT3@nv|?Fde0!EYm?z-jT-hUY<8ME$LXfAr18^0Kk$km zX>MJi%`zi3u4CqlAomZuX_WDif}Kc@leDw%_(*0BWT&a6wgqMx;@(Ep z+W|iChq0$EVry)ywU&x!h}kb%p4PM6M$g(emukcY{5%>jlq!pORWHQIwL{nFv(6L` zq5<|56*=m}C^{^^8q7T+&h!pfjmeD7w&~uYjwS(K6YwyU%%L>!ytyc2f0PlgznP7R z-ZC?t?@j4JH0E$`icLD@_fvu$_OQ$fnumFj=6Ia|QQxj}WG`rl#}J$46|@5g_bmix zQwV#=1i85DV^&?JNoQY;AjLu4v5!Qn4~zowhq^{c+Bn3KNMeWKw+5al^ChDuu`x2e zaGh^ER4vZu0a*nNYL!upKixY6ubRMOO8X31s5Lb`j7Q5+kAz)6n-I_L`KV@Eo>~l0 zZ28`zr)5$?v!pwM)~=g-(oy8y@GulpkH#}Rn4%30sVrMW^Q0lR)<}#LKi-jGM)5J( zJ5ZUri-XEIbujYSRpM#3IvPEIkrWx6mA0hAfmZ20#^o zFxi(h9?B`7HHmn%RUBBXdT4-*(Pzy81Y!${pf3WDQaaHj0~PrMlw2*kXcnSQ;3HL^ zYNVU&TH4B!Rs+=|bF>^6i6GN(xm9z2QP@^$wV*2uC2l2qbzUAU4=iAHe&BeDJuh|{ zJx`Y%=2*yh$wqUdmNHNn9YoOVG2T20xE6@|%|c1jwYSo=4c|-s)^6b<82SEAXzoCZ zOayCL2m);F_G8ecK@BrynQ=sGkESq@5cKB;f`%s>SvEc}V8bY@o;5R9?C_+lR$5Z?kc{=9tZ=g7ZlQ=Fh1}&teBkHcV#hZWHa6 z5Y7@27E2Kd7KB3sqrmbZUX`5SwooEOq#?EX(GllPHEK^WfO56XLa?Gekyl%~o=UY8 znu~FnJPPM+9TQM=f+V&qEP#>hlF2u`O(RdyH*P~6dd+q*nD-in!jR0emPzcZoemYZ z8lk`i`)%D$2HVG_(`{2(9V|uR9VoMsoJ-ChZok230=FhWjLXCZx(>IN ze#s3Of}`ae5)+51-rdlEA}lWZP)FxUEft2A)}C(oex4VKXw>Tk-bk7_Vne0YHJ7v- zFyo1|QO84$H_Xj?Ffl~gD)JUx6wX*>pu(97k*Zh3visnG|qrs70cUS3J{iMO&dG zXz>y_IG>U^kThoNGE^gL9=*3p5${9M!MU$FsBXPcu zA?Od}wrX`d)i4`~8+>47&gP&rXc?hWkY3jYBQDlZZ_7u9!l@~Trz{K=ccYj-A7@ed z&4D6N(3)ctp4$&m5wSdDGwG5@kTvZB?#uf+(SC^`$U$+(1mL>*dSq6xW=@GgrxlTK?vfA2%P3afi8>wpk%nTM*IOt{e>vz z(=w5(kic9aDe4LuVWm|`!6_Y=kEjb#ka8yy~Ba6II7bDQ&Jld5L!^3;oc zSSDx5PGx~8_E0c=$YwO;T#^zvGj12|VJZze7OF4K3p(=*dhD19R=Ljp(2t8m#)#&aT7d3QxE#U92WF zuqn!G#A}GAypy4M6B==V%q&M;7V2#rNGx!q2_54YBs3Jt5+<^OXwyRx@>EjE6OosiF!r6@4hp