From 13bec235a123dfc1686a26f931e9733ec61771a9 Mon Sep 17 00:00:00 2001 From: stephb9959 Date: Mon, 25 Jul 2022 23:25:03 -0700 Subject: [PATCH] https://telecominfraproject.atlassian.net/browse/WIFI-10345 Signed-off-by: stephb9959 --- CMakeLists.txt | 2 +- OPERATOR.md | 37 ++ build | 2 +- src/ActionLinkManager.cpp | 30 +- src/AuthService.cpp | 65 ++- src/AuthService.h | 14 +- src/MFAServer.cpp | 7 +- src/MessagingTemplates.cpp | 8 + src/MessagingTemplates.h | 75 +++ src/RESTAPI/RESTAPI_action_links.cpp | 19 +- src/RESTAPI/RESTAPI_action_links.h | 2 + src/RESTAPI/RESTAPI_signup_handler.cpp | 7 +- src/SMTPMailerService.h | 6 +- templates/email_invitation.html | 6 +- templates/password_reset.html | 8 + templates/sample.html | 441 ++++++++++++++++++ templates/sub_email_verification.html | 0 templates/sub_email_verification.txt | 0 templates/sub_password_reset.html | 0 templates/sub_password_reset.txt | 0 templates/sub_verification_code.html | 0 templates/sub_verification_code.txt | 0 templates/verification_code.html | 0 wwwassets/access_policy.html | 2 +- wwwassets/email_verification_error.html | 2 +- wwwassets/email_verification_success.html | 2 +- wwwassets/logo.png | Bin 0 -> 4926 bytes wwwassets/password_policy.html | 2 +- wwwassets/password_reset.html | 2 +- wwwassets/password_reset_error.html | 2 +- wwwassets/password_reset_success.html | 2 +- wwwassets/signup_verification.html | 2 +- wwwassets/signup_verification_error.html | 2 +- wwwassets/signup_verification_success.html | 2 +- wwwassets/sub_404_error.css | 0 wwwassets/sub_404_error.html | 0 wwwassets/sub_access_policy.html | 0 wwwassets/sub_email_verification_error.html | 0 wwwassets/sub_email_verification_success.html | 0 wwwassets/sub_favicon.ico | Bin 0 -> 206910 bytes wwwassets/sub_logo.png | Bin 0 -> 4926 bytes wwwassets/sub_password_policy.html | 0 wwwassets/sub_password_reset.html | 0 wwwassets/sub_password_reset_error.html | 0 wwwassets/sub_password_reset_success.html | 0 45 files changed, 678 insertions(+), 71 deletions(-) create mode 100644 OPERATOR.md create mode 100644 src/MessagingTemplates.cpp create mode 100644 src/MessagingTemplates.h create mode 100644 templates/sample.html create mode 100644 templates/sub_email_verification.html create mode 100644 templates/sub_email_verification.txt create mode 100644 templates/sub_password_reset.html create mode 100644 templates/sub_password_reset.txt create mode 100644 templates/sub_verification_code.html create mode 100644 templates/sub_verification_code.txt create mode 100644 templates/verification_code.html create mode 100644 wwwassets/logo.png create mode 100644 wwwassets/sub_404_error.css create mode 100644 wwwassets/sub_404_error.html create mode 100644 wwwassets/sub_access_policy.html create mode 100644 wwwassets/sub_email_verification_error.html create mode 100644 wwwassets/sub_email_verification_success.html create mode 100644 wwwassets/sub_favicon.ico create mode 100644 wwwassets/sub_logo.png create mode 100644 wwwassets/sub_password_policy.html create mode 100644 wwwassets/sub_password_reset.html create mode 100644 wwwassets/sub_password_reset_error.html create mode 100644 wwwassets/sub_password_reset_success.html diff --git a/CMakeLists.txt b/CMakeLists.txt index 81b968f..1582525 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -124,7 +124,7 @@ add_executable( owsec src/storage/orm_actionLinks.cpp src/storage/orm_actionLinks.h src/storage/orm_avatar.cpp src/storage/orm_avatar.h src/SpecialUserHelpers.h - src/RESTAPI/RESTAPI_db_helpers.h src/storage/orm_logins.cpp src/storage/orm_logins.h src/RESTAPI/RESTAPI_totp_handler.cpp src/RESTAPI/RESTAPI_totp_handler.h src/TotpCache.h src/RESTAPI/RESTAPI_subtotp_handler.cpp src/RESTAPI/RESTAPI_subtotp_handler.h src/RESTAPI/RESTAPI_signup_handler.cpp src/RESTAPI/RESTAPI_signup_handler.h) + src/RESTAPI/RESTAPI_db_helpers.h src/storage/orm_logins.cpp src/storage/orm_logins.h src/RESTAPI/RESTAPI_totp_handler.cpp src/RESTAPI/RESTAPI_totp_handler.h src/TotpCache.h src/RESTAPI/RESTAPI_subtotp_handler.cpp src/RESTAPI/RESTAPI_subtotp_handler.h src/RESTAPI/RESTAPI_signup_handler.cpp src/RESTAPI/RESTAPI_signup_handler.h src/MessagingTemplates.cpp src/MessagingTemplates.h) if(NOT SMALL_BUILD) target_link_libraries(owsec PUBLIC diff --git a/OPERATOR.md b/OPERATOR.md new file mode 100644 index 0000000..3371478 --- /dev/null +++ b/OPERATOR.md @@ -0,0 +1,37 @@ +# Operator Support +In order to support multiple tenants and operators, you must prepare the security service to serve +customized e-mails and messages. + +## Structure for `templates` +Any file in the root of the directory will be used as defaults. The following files must be present: +- email_invitation.html/txt : This email message will be sent to a newly added user. +- email_verification.html/txt : This email is sent when an email verification is required. +- password_reset.html/txt : This is sent when a pasword reset is requested. +- verification_code.html/txt : This is used during MFA when email based. +- signup_verification.html/txt : This email is send to a new subscriber who signed up for service. +- sub_email_verification.html/txt : This is sent to a subscriber requiring an email verification. +- sub_verification_code.html/txt : This is used during MFA when email based for a subscriber. +- logo.jpg : The default logo to use in any of these emails. + +## Structure for `wwwassets` +Any file in the root of the directory will be used as defaults. The following files must be present: +- email_verification_error.html : Used when email verification has failed. +- email_verification_success.html : Used when emil verification has succeeded. +- invitation_error.html : +- invitation_success.html : +- password_policy.html : +- password_reset.html : +- password_reset_success.html : +- password_reset_error.html : +- signup_verification.html : +- signup_verification_error.html : +- signup_verification_success.html : +- favicon.ico : icon for the application +- 404_error.html : your customized 404 page +- the_logo : the logo to use. + +## For tenants +When creating a tenant/operator, you must create a subdirectory inside each `wwwassets` and `templates` and replicate +all the files that appear at the root level. You need to use the short Operator name (also known as RegistrantId in the API). This means +no spaces, all lowercase characters and numbers. No special characters: 0-9 and a-z. + diff --git a/build b/build index bf0d87a..62f9457 100644 --- a/build +++ b/build @@ -1 +1 @@ -4 \ No newline at end of file +6 \ No newline at end of file diff --git a/src/ActionLinkManager.cpp b/src/ActionLinkManager.cpp index bc3c9a5..7b0d50e 100644 --- a/src/ActionLinkManager.cpp +++ b/src/ActionLinkManager.cpp @@ -5,6 +5,7 @@ #include "ActionLinkManager.h" #include "StorageService.h" #include "RESTObjects/RESTAPI_SecurityObjects.h" +#include "MessagingTemplates.h" namespace OpenWifi { @@ -61,7 +62,7 @@ namespace OpenWifi { switch(i.action) { case OpenWifi::SecurityObjects::LinkActions::FORGOT_PASSWORD: { - if(AuthService::SendEmailToUser(i.id, UInfo.email, AuthService::FORGOT_PASSWORD)) { + if(AuthService::SendEmailToUser(i.id, UInfo.email, MessagingTemplates::FORGOT_PASSWORD)) { Logger().information(fmt::format("Send password reset link to {}",UInfo.email)); } StorageService()->ActionLinksDB().SentAction(i.id); @@ -69,15 +70,24 @@ namespace OpenWifi { break; case OpenWifi::SecurityObjects::LinkActions::VERIFY_EMAIL: { - if(AuthService::SendEmailToUser(i.id, UInfo.email, AuthService::EMAIL_VERIFICATION)) { + if(AuthService::SendEmailToUser(i.id, UInfo.email, MessagingTemplates::EMAIL_VERIFICATION)) { Logger().information(fmt::format("Send email verification link to {}",UInfo.email)); } StorageService()->ActionLinksDB().SentAction(i.id); } break; + case OpenWifi::SecurityObjects::LinkActions::EMAIL_INVITATION: { + if(AuthService::SendEmailToUser(i.id, UInfo.email, MessagingTemplates::EMAIL_INVITATION)) { + Logger().information(fmt::format("Send new subscriber email invitation link to {}",UInfo.email)); + } + StorageService()->ActionLinksDB().SentAction(i.id); + } + break; + case OpenWifi::SecurityObjects::LinkActions::SUB_FORGOT_PASSWORD: { - if(AuthService::SendEmailToSubUser(i.id, UInfo.email, AuthService::FORGOT_PASSWORD)) { + auto Signup = Poco::StringTokenizer(UInfo.signingUp,":"); + if(AuthService::SendEmailToSubUser(i.id, UInfo.email,MessagingTemplates::SUB_FORGOT_PASSWORD, Signup.count()==1 ? "" : Signup[0])) { Logger().information(fmt::format("Send subscriber password reset link to {}",UInfo.email)); } StorageService()->ActionLinksDB().SentAction(i.id); @@ -85,7 +95,8 @@ namespace OpenWifi { break; case OpenWifi::SecurityObjects::LinkActions::SUB_VERIFY_EMAIL: { - if(AuthService::SendEmailToSubUser(i.id, UInfo.email, AuthService::EMAIL_VERIFICATION)) { + auto Signup = Poco::StringTokenizer(UInfo.signingUp,":"); + if(AuthService::SendEmailToSubUser(i.id, UInfo.email, MessagingTemplates::SUB_EMAIL_VERIFICATION, Signup.count()==1 ? "" : Signup[0])) { Logger().information(fmt::format("Send subscriber email verification link to {}",UInfo.email)); } StorageService()->ActionLinksDB().SentAction(i.id); @@ -93,21 +104,14 @@ namespace OpenWifi { break; case OpenWifi::SecurityObjects::LinkActions::SUB_SIGNUP: { - if(AuthService::SendEmailToSubUser(i.id, UInfo.email, AuthService::SIGNUP_VERIFICATION)) { + auto Signup = Poco::StringTokenizer(UInfo.signingUp,":"); + if(AuthService::SendEmailToSubUser(i.id, UInfo.email, MessagingTemplates::SIGNUP_VERIFICATION, Signup.count()==1 ? "" : Signup[0])) { Logger().information(fmt::format("Send new subscriber email verification link to {}",UInfo.email)); } StorageService()->ActionLinksDB().SentAction(i.id); } break; - case OpenWifi::SecurityObjects::LinkActions::EMAIL_INVITATION: { - if(AuthService::SendEmailToSubUser(i.id, UInfo.email, AuthService::EMAIL_INVITATION)) { - Logger().information(fmt::format("Send new subscriber email invitation link to {}",UInfo.email)); - } - StorageService()->ActionLinksDB().SentAction(i.id); - } - break; - default: { StorageService()->ActionLinksDB().SentAction(i.id); } diff --git a/src/AuthService.cpp b/src/AuthService.cpp index b36e9fe..d1a732b 100644 --- a/src/AuthService.cpp +++ b/src/AuthService.cpp @@ -20,14 +20,10 @@ #include "SMTPMailerService.h" #include "MFAServer.h" +#include "MessagingTemplates.h" namespace OpenWifi { - inline const static std::vector EmailTemplateNames = { "password_reset" , - "email_verification", - "signuo_verification", - "email_invitation" }; - AuthService::ACCESS_TYPE AuthService::IntToAccessType(int C) { switch (C) { case 1: return USERNAME; @@ -38,7 +34,6 @@ namespace OpenWifi { } } - int AuthService::AccessTypeToInt(ACCESS_TYPE T) { switch (T) { case USERNAME: return 1; @@ -519,7 +514,6 @@ namespace OpenWifi { return SUCCESS; } - return INVALID_CREDENTIALS; } @@ -569,40 +563,62 @@ namespace OpenWifi { return INVALID_CREDENTIALS; } - bool AuthService::SendEmailToUser(const std::string &LinkId, std::string &Email, EMAIL_REASON Reason) { + bool AuthService::SendEmailChallengeCode(const SecurityObjects::UserInfoAndPolicy &UInfo, const std::string &Challenge) { + auto OperatorParts = Poco::StringTokenizer(UInfo.userinfo.signingUp,":"); + if(UInfo.userinfo.signingUp.empty() || OperatorParts.count()!=2) { + MessageAttributes Attrs; + Attrs[RECIPIENT_EMAIL] = UInfo.userinfo.email; + Attrs[LOGO] = AuthService::GetLogoAssetURI(); + Attrs[SUBJECT] = "Login validation code"; + Attrs[CHALLENGE_CODE] = Challenge; + return SMTPMailerService()->SendMessage(UInfo.userinfo.email, MessagingTemplates::TemplateName(MessagingTemplates::VERIFICATION_CODE), Attrs); + } else { + MessageAttributes Attrs; + Attrs[RECIPIENT_EMAIL] = UInfo.userinfo.email; + Attrs[LOGO] = AuthService::GetLogoAssetURI(); + Attrs[SUBJECT] = "Login validation code"; + Attrs[CHALLENGE_CODE] = Challenge; + return SMTPMailerService()->SendMessage(UInfo.userinfo.email, MessagingTemplates::TemplateName(MessagingTemplates::SUB_VERIFICATION_CODE,OperatorParts[0]), Attrs); + } + } + + bool AuthService::SendEmailToUser(const std::string &LinkId, std::string &Email, MessagingTemplates::EMAIL_REASON Reason) { SecurityObjects::UserInfo UInfo; if(StorageService()->UserDB().GetUserByEmail(Email,UInfo)) { switch (Reason) { - case FORGOT_PASSWORD: { + case MessagingTemplates::FORGOT_PASSWORD: { MessageAttributes Attrs; Attrs[RECIPIENT_EMAIL] = UInfo.email; Attrs[LOGO] = GetLogoAssetURI(); Attrs[SUBJECT] = "Password reset link"; Attrs[ACTION_LINK] = MicroService::instance().GetPublicAPIEndPoint() + "/actionLink?action=password_reset&id=" + LinkId ; - SMTPMailerService()->SendMessage(UInfo.email, EmailTemplateNames[FORGOT_PASSWORD], Attrs); + Attrs[ACTION_LINK_HTML] = "/api/v1/actionLink?action=password_reset&id=" + LinkId ; + SMTPMailerService()->SendMessage(UInfo.email, MessagingTemplates::TemplateName(MessagingTemplates::FORGOT_PASSWORD), Attrs); } break; - case EMAIL_VERIFICATION: { + case MessagingTemplates::EMAIL_VERIFICATION: { MessageAttributes Attrs; Attrs[RECIPIENT_EMAIL] = UInfo.email; Attrs[LOGO] = GetLogoAssetURI(); Attrs[SUBJECT] = "e-mail Address Verification"; Attrs[ACTION_LINK] = MicroService::instance().GetPublicAPIEndPoint() + "/actionLink?action=email_verification&id=" + LinkId ; - SMTPMailerService()->SendMessage(UInfo.email, EmailTemplateNames[EMAIL_VERIFICATION], Attrs); + Attrs[ACTION_LINK_HTML] = "/api/v1/actionLink?action=email_verification&id=" + LinkId ; + SMTPMailerService()->SendMessage(UInfo.email, MessagingTemplates::TemplateName(MessagingTemplates::EMAIL_VERIFICATION), Attrs); UInfo.waitingForEmailCheck = true; } break; - case EMAIL_INVITATION: { + case MessagingTemplates::EMAIL_INVITATION: { MessageAttributes Attrs; Attrs[RECIPIENT_EMAIL] = UInfo.email; Attrs[LOGO] = GetLogoAssetURI(); Attrs[SUBJECT] = "e-mail Invitation"; Attrs[ACTION_LINK] = MicroService::instance().GetPublicAPIEndPoint() + "/actionLink?action=email_invitation&id=" + LinkId ; - SMTPMailerService()->SendMessage(UInfo.email, EmailTemplateNames[EMAIL_INVITATION], Attrs); + Attrs[ACTION_LINK_HTML] = "/api/v1/actionLink?action=email_invitation&id=" + LinkId ; + SMTPMailerService()->SendMessage(UInfo.email, MessagingTemplates::TemplateName(MessagingTemplates::EMAIL_INVITATION), Attrs); UInfo.waitingForEmailCheck = true; } break; @@ -615,40 +631,43 @@ namespace OpenWifi { return false; } - bool AuthService::SendEmailToSubUser(const std::string &LinkId, std::string &Email, EMAIL_REASON Reason) { + bool AuthService::SendEmailToSubUser(const std::string &LinkId, std::string &Email, MessagingTemplates::EMAIL_REASON Reason, const std::string &OperatorName ) { SecurityObjects::UserInfo UInfo; if(StorageService()->SubDB().GetUserByEmail(Email,UInfo)) { switch (Reason) { - case FORGOT_PASSWORD: { + case MessagingTemplates::SUB_FORGOT_PASSWORD: { MessageAttributes Attrs; Attrs[RECIPIENT_EMAIL] = UInfo.email; Attrs[LOGO] = GetLogoAssetURI(); Attrs[SUBJECT] = "Password reset link"; - Attrs[ACTION_LINK] = MicroService::instance().GetPublicAPIEndPoint() + "/actionLink?action=password_reset&id=" + LinkId ; - SMTPMailerService()->SendMessage(UInfo.email, EmailTemplateNames[FORGOT_PASSWORD], Attrs); + Attrs[ACTION_LINK] = MicroService::instance().GetPublicAPIEndPoint() + "/actionLink?action=sub_password_reset&id=" + LinkId ; + Attrs[ACTION_LINK_HTML] = "/api/v1/actionLink?action=sub_password_reset&id=" + LinkId ; + SMTPMailerService()->SendMessage(UInfo.email, MessagingTemplates::TemplateName(MessagingTemplates::SUB_FORGOT_PASSWORD, OperatorName), Attrs); } break; - case EMAIL_VERIFICATION: { + case MessagingTemplates::SUB_EMAIL_VERIFICATION: { MessageAttributes Attrs; Attrs[RECIPIENT_EMAIL] = UInfo.email; Attrs[LOGO] = GetLogoAssetURI(); Attrs[SUBJECT] = "e-mail Address Verification"; - Attrs[ACTION_LINK] = MicroService::instance().GetPublicAPIEndPoint() + "/actionLink?action=email_verification&id=" + LinkId ; - SMTPMailerService()->SendMessage(UInfo.email, EmailTemplateNames[EMAIL_VERIFICATION], Attrs); + Attrs[ACTION_LINK] = MicroService::instance().GetPublicAPIEndPoint() + "/actionLink?action=sub_email_verification&id=" + LinkId ; + Attrs[ACTION_LINK_HTML] = "/api/v1/actionLink?action=sub_email_verification&id=" + LinkId ; + SMTPMailerService()->SendMessage(UInfo.email, MessagingTemplates::TemplateName(MessagingTemplates::SUB_EMAIL_VERIFICATION, OperatorName), Attrs); UInfo.waitingForEmailCheck = true; } break; - case SIGNUP_VERIFICATION: { + case MessagingTemplates::SIGNUP_VERIFICATION: { MessageAttributes Attrs; Attrs[RECIPIENT_EMAIL] = UInfo.email; Attrs[LOGO] = GetLogoAssetURI(); Attrs[SUBJECT] = "Signup e-mail Address Verification"; Attrs[ACTION_LINK] = MicroService::instance().GetPublicAPIEndPoint() + "/actionLink?action=signup_verification&id=" + LinkId ; - SMTPMailerService()->SendMessage(UInfo.email, EmailTemplateNames[SIGNUP_VERIFICATION], Attrs); + Attrs[ACTION_LINK_HTML] = "/api/v1/actionLink?action=signup_verification&id=" + LinkId ; + SMTPMailerService()->SendMessage(UInfo.email, MessagingTemplates::TemplateName(MessagingTemplates::SIGNUP_VERIFICATION, OperatorName), Attrs); UInfo.waitingForEmailCheck = true; } break; diff --git a/src/AuthService.h b/src/AuthService.h index 438fa43..cc1ee59 100644 --- a/src/AuthService.h +++ b/src/AuthService.h @@ -22,6 +22,7 @@ #include "framework/MicroService.h" #include "RESTObjects/RESTAPI_SecurityObjects.h" +#include "MessagingTemplates.h" namespace OpenWifi{ @@ -36,13 +37,6 @@ namespace OpenWifi{ CUSTOM }; - enum EMAIL_REASON { - FORGOT_PASSWORD = 0, - EMAIL_VERIFICATION, - SIGNUP_VERIFICATION, - EMAIL_INVITATION - }; - static ACCESS_TYPE IntToAccessType(int C); static int AccessTypeToInt(ACCESS_TYPE T); @@ -91,10 +85,12 @@ namespace OpenWifi{ [[nodiscard]] static bool VerifyEmail(SecurityObjects::UserInfo &UInfo); [[nodiscard]] static bool VerifySubEmail(SecurityObjects::UserInfo &UInfo); - [[nodiscard]] static bool SendEmailToUser(const std::string &LinkId, std::string &Email, EMAIL_REASON Reason); - [[nodiscard]] static bool SendEmailToSubUser(const std::string &LinkId, std::string &Email, EMAIL_REASON Reason); + [[nodiscard]] static bool SendEmailToUser(const std::string &LinkId, std::string &Email, MessagingTemplates::EMAIL_REASON Reason); + [[nodiscard]] static bool SendEmailToSubUser(const std::string &LinkId, std::string &Email, MessagingTemplates::EMAIL_REASON Reason, const std::string &OperatorName); [[nodiscard]] bool RequiresMFA(const SecurityObjects::UserInfoAndPolicy &UInfo); + [[nodiscard]] bool SendEmailChallengeCode(const SecurityObjects::UserInfoAndPolicy &UInfo, const std::string &code); + bool DeleteUserFromCache(const std::string &UserName); bool DeleteSubUserFromCache(const std::string &UserName); void RevokeToken(std::string & Token); diff --git a/src/MFAServer.cpp b/src/MFAServer.cpp index 5bc9399..9d952c4 100644 --- a/src/MFAServer.cpp +++ b/src/MFAServer.cpp @@ -44,12 +44,7 @@ namespace OpenWifi { std::string Message = "This is your login code: " + Challenge + " Please enter this in your login screen."; return SMSSender()->Send(UInfo.userinfo.userTypeProprietaryInfo.mobiles[0].number, Message); } else if(Method==MFAMETHODS::EMAIL && SMTPMailerService()->Enabled() && !UInfo.userinfo.email.empty()) { - MessageAttributes Attrs; - Attrs[RECIPIENT_EMAIL] = UInfo.userinfo.email; - Attrs[LOGO] = AuthService::GetLogoAssetURI(); - Attrs[SUBJECT] = "Login validation code"; - Attrs[CHALLENGE_CODE] = Challenge; - return SMTPMailerService()->SendMessage(UInfo.userinfo.email, "verification_code.txt", Attrs); + return AuthService()->SendEmailChallengeCode(UInfo,Challenge); } else if(Method==MFAMETHODS::AUTHENTICATOR && !UInfo.userinfo.userTypeProprietaryInfo.authenticatorSecret.empty()) { return true; } diff --git a/src/MessagingTemplates.cpp b/src/MessagingTemplates.cpp new file mode 100644 index 0000000..2352674 --- /dev/null +++ b/src/MessagingTemplates.cpp @@ -0,0 +1,8 @@ +// +// Created by stephane bourque on 2022-07-25. +// + +#include "MessagingTemplates.h" + +namespace OpenWifi { +} // OpenWifi \ No newline at end of file diff --git a/src/MessagingTemplates.h b/src/MessagingTemplates.h new file mode 100644 index 0000000..7289a1d --- /dev/null +++ b/src/MessagingTemplates.h @@ -0,0 +1,75 @@ +// +// Created by stephane bourque on 2022-07-25. +// + +#pragma once + +#include +#include + +namespace OpenWifi { + + class MessagingTemplates { + public: + static MessagingTemplates & instance() { + static auto instance = new MessagingTemplates; + return *instance; + } + + enum EMAIL_REASON { + FORGOT_PASSWORD = 0, + EMAIL_VERIFICATION, + SIGNUP_VERIFICATION, + EMAIL_INVITATION, + VERIFICATION_CODE, + SUB_FORGOT_PASSWORD, + SUB_EMAIL_VERIFICATION, + SUB_VERIFICATION_CODE + }; + + static std::string AddOperator(const std::string & filename, const std::string &OperatorName) { + if(OperatorName.empty()) + return "/" + filename; + return "/" + OperatorName + "/" + filename; + } + + static std::string TemplateName( EMAIL_REASON r , const std::string &OperatorName="") { + switch (r) { + case FORGOT_PASSWORD: return AddOperator(EmailTemplateNames[FORGOT_PASSWORD],OperatorName); + case EMAIL_VERIFICATION: return AddOperator(EmailTemplateNames[EMAIL_VERIFICATION],OperatorName); + case SIGNUP_VERIFICATION: return AddOperator(EmailTemplateNames[SIGNUP_VERIFICATION],OperatorName); + case EMAIL_INVITATION: return AddOperator(EmailTemplateNames[EMAIL_INVITATION],OperatorName); + case VERIFICATION_CODE: return AddOperator(EmailTemplateNames[VERIFICATION_CODE],OperatorName); + case SUB_FORGOT_PASSWORD: return AddOperator(EmailTemplateNames[SUB_FORGOT_PASSWORD],OperatorName); + case SUB_EMAIL_VERIFICATION: return AddOperator(EmailTemplateNames[SUB_EMAIL_VERIFICATION],OperatorName); + case SUB_VERIFICATION_CODE: return AddOperator(EmailTemplateNames[SUB_VERIFICATION_CODE],OperatorName); + default: + return ""; + } + } + + static std::string Logo(const std::string &OperatorName = "" ) { + return AddOperator("logo.jpg", OperatorName); + } + + static std::string SubLogo(const std::string &OperatorName = "" ) { + return AddOperator("sub_logo.jpg", OperatorName); + } + + private: + inline const static std::vector EmailTemplateNames = { + "password_reset", + "email_verification", + "signup_verification", + "email_invitation", + "verification_code", + "sub_password_reset", + "sub_email_verification", + "sub_verification_code" + }; + }; + + inline MessagingTemplates & MessagingTemplates() { return MessagingTemplates::instance(); } + +} // OpenWifi + diff --git a/src/RESTAPI/RESTAPI_action_links.cpp b/src/RESTAPI/RESTAPI_action_links.cpp index 57cd654..80e9d8b 100644 --- a/src/RESTAPI/RESTAPI_action_links.cpp +++ b/src/RESTAPI/RESTAPI_action_links.cpp @@ -23,8 +23,12 @@ namespace OpenWifi { if(Action=="password_reset") return RequestResetPassword(Link); + else if(Action=="sub_password_reset") + return RequestSubResetPassword(Link); else if(Action=="email_verification") return DoEmailVerification(Link); + else if(Action=="sub_email_verification") + return DoSubEmailVerification(Link); else if(Action=="signup_verification") return DoNewSubVerification(Link); else @@ -36,6 +40,8 @@ namespace OpenWifi { if(Action=="password_reset") return CompleteResetPassword(); + else if(Action=="sub_password_reset") + return CompleteResetPassword(); else if(Action=="signup_completion") return CompleteSubVerification(); else if(Action=="email_invitation") @@ -201,10 +207,11 @@ namespace OpenWifi { // Send the update to the provisioning service Poco::JSON::Object Body; - Body.set("signupUUID", UInfo.signingUp); + auto RawSignup = Poco::StringTokenizer(UInfo.signingUp,":"); + Body.set("signupUUID", RawSignup.count()==1 ? UInfo.signingUp : RawSignup[1]); OpenAPIRequestPut ProvRequest(uSERVICE_PROVISIONING,"/api/v1/signup", { - {"signupUUID", UInfo.signingUp} , + {"signupUUID", RawSignup.count()==1 ? UInfo.signingUp : RawSignup[1]} , {"operation", "emailVerified"} }, Body,30000); @@ -269,4 +276,12 @@ namespace OpenWifi { /// TODO: } + void RESTAPI_action_links::RequestSubResetPassword(SecurityObjects::ActionLink &Link) { + + } + + void RESTAPI_action_links::DoSubEmailVerification(SecurityObjects::ActionLink &Link) { + + } + } diff --git a/src/RESTAPI/RESTAPI_action_links.h b/src/RESTAPI/RESTAPI_action_links.h index e804a48..f05495c 100644 --- a/src/RESTAPI/RESTAPI_action_links.h +++ b/src/RESTAPI/RESTAPI_action_links.h @@ -22,9 +22,11 @@ namespace OpenWifi { true, RateLimit{.Interval=1000,.MaxCalls=10}) {} static auto PathName() { return std::list{"/api/v1/actionLink"}; }; void RequestResetPassword(SecurityObjects::ActionLink &Link); + void RequestSubResetPassword(SecurityObjects::ActionLink &Link); void CompleteResetPassword(); void CompleteSubVerification(); void DoEmailVerification(SecurityObjects::ActionLink &Link); + void DoSubEmailVerification(SecurityObjects::ActionLink &Link); void DoReturnA404(); void DoNewSubVerification(SecurityObjects::ActionLink &Link); void CompleteEmailInvitation(); diff --git a/src/RESTAPI/RESTAPI_signup_handler.cpp b/src/RESTAPI/RESTAPI_signup_handler.cpp index ff0fbd2..5eef231 100644 --- a/src/RESTAPI/RESTAPI_signup_handler.cpp +++ b/src/RESTAPI/RESTAPI_signup_handler.cpp @@ -13,8 +13,9 @@ namespace OpenWifi { auto UserName = GetParameter("email"); auto signupUUID = GetParameter("signupUUID"); auto owner = GetParameter("owner"); - if(UserName.empty() || signupUUID.empty() || owner.empty()) { - Logger().error("Signup requires: email, signupUUID, and owner."); + auto operatorName = GetParameter("operatorName"); + if(UserName.empty() || signupUUID.empty() || owner.empty() || operatorName.empty()) { + Logger().error("Signup requires: email, signupUUID, operatorName, and owner."); return BadRequest(RESTAPI::Errors::MissingOrInvalidParameters); } @@ -37,7 +38,7 @@ namespace OpenWifi { } SecurityObjects::UserInfo NewSub; - NewSub.signingUp = signupUUID; + NewSub.signingUp = operatorName + ":" + signupUUID; NewSub.waitingForEmailCheck = true; NewSub.name = UserName; NewSub.modified = OpenWifi::Now(); diff --git a/src/SMTPMailerService.h b/src/SMTPMailerService.h index 93e7f37..e1739b1 100644 --- a/src/SMTPMailerService.h +++ b/src/SMTPMailerService.h @@ -27,7 +27,8 @@ namespace OpenWifi { LOGO, TEXT, CHALLENGE_CODE, - SENDER + SENDER, + ACTION_LINK_HTML }; static const std::map @@ -44,7 +45,8 @@ namespace OpenWifi { { LOGO, "LOGO"}, { TEXT, "TEXT"}, { CHALLENGE_CODE, "CHALLENGE_CODE"}, - { SENDER, "SENDER"} + { SENDER, "SENDER"}, + { ACTION_LINK_HTML, "ACTION_LINK_HTML"}, }; inline const std::string & MessageAttributeToVar(MESSAGE_ATTRIBUTES Attr) { diff --git a/templates/email_invitation.html b/templates/email_invitation.html index 566549b..65e6d90 100644 --- a/templates/email_invitation.html +++ b/templates/email_invitation.html @@ -2,7 +2,11 @@ - Title + eMail Invitation + + + + diff --git a/templates/password_reset.html b/templates/password_reset.html index 226a88e..5a3ee06 100644 --- a/templates/password_reset.html +++ b/templates/password_reset.html @@ -4,6 +4,14 @@ Password Reset +
+ +

+ +

+ +
+ diff --git a/templates/sample.html b/templates/sample.html new file mode 100644 index 0000000..420c52b --- /dev/null +++ b/templates/sample.html @@ -0,0 +1,441 @@ + + + Project Groups now available to join - Telecom Infra Project + + + + + + + + + + + + =09 + + + + +   + + + + \ No newline at end of file diff --git a/templates/sub_email_verification.html b/templates/sub_email_verification.html new file mode 100644 index 0000000..e69de29 diff --git a/templates/sub_email_verification.txt b/templates/sub_email_verification.txt new file mode 100644 index 0000000..e69de29 diff --git a/templates/sub_password_reset.html b/templates/sub_password_reset.html new file mode 100644 index 0000000..e69de29 diff --git a/templates/sub_password_reset.txt b/templates/sub_password_reset.txt new file mode 100644 index 0000000..e69de29 diff --git a/templates/sub_verification_code.html b/templates/sub_verification_code.html new file mode 100644 index 0000000..e69de29 diff --git a/templates/sub_verification_code.txt b/templates/sub_verification_code.txt new file mode 100644 index 0000000..e69de29 diff --git a/templates/verification_code.html b/templates/verification_code.html new file mode 100644 index 0000000..e69de29 diff --git a/wwwassets/access_policy.html b/wwwassets/access_policy.html index 7b953a5..043eb51 100644 --- a/wwwassets/access_policy.html +++ b/wwwassets/access_policy.html @@ -96,7 +96,7 @@
-
OpenWifi
+
OpenWifi
diff --git a/wwwassets/email_verification_error.html b/wwwassets/email_verification_error.html index 1b5b56a..229d301 100644 --- a/wwwassets/email_verification_error.html +++ b/wwwassets/email_verification_error.html @@ -103,7 +103,7 @@
-
OpenWifi
+
OpenWifi
diff --git a/wwwassets/email_verification_success.html b/wwwassets/email_verification_success.html index 2ebc078..790db57 100644 --- a/wwwassets/email_verification_success.html +++ b/wwwassets/email_verification_success.html @@ -103,7 +103,7 @@
-
OpenWifi
+
OpenWifi
diff --git a/wwwassets/logo.png b/wwwassets/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..c64a547dc79aa390a6125b85facf4ee6bb84ce65 GIT binary patch literal 4926 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 literal 0 HcmV?d00001 diff --git a/wwwassets/password_policy.html b/wwwassets/password_policy.html index a17a3a5..0a66e39 100644 --- a/wwwassets/password_policy.html +++ b/wwwassets/password_policy.html @@ -101,7 +101,7 @@
-
OpenWifi
+
OpenWifi
diff --git a/wwwassets/password_reset.html b/wwwassets/password_reset.html index 882881c..e237b7f 100644 --- a/wwwassets/password_reset.html +++ b/wwwassets/password_reset.html @@ -122,7 +122,7 @@
-
OpenWifi
+
OpenWifi
diff --git a/wwwassets/password_reset_error.html b/wwwassets/password_reset_error.html index 61823d4..12b0ddb 100644 --- a/wwwassets/password_reset_error.html +++ b/wwwassets/password_reset_error.html @@ -101,7 +101,7 @@
-
OpenWifi
+
OpenWifi
diff --git a/wwwassets/password_reset_success.html b/wwwassets/password_reset_success.html index 0d7d3a3..f3f2e42 100644 --- a/wwwassets/password_reset_success.html +++ b/wwwassets/password_reset_success.html @@ -70,7 +70,7 @@
-
OpenWifi
+
OpenWifi
diff --git a/wwwassets/signup_verification.html b/wwwassets/signup_verification.html index b9f55c0..94d93b6 100644 --- a/wwwassets/signup_verification.html +++ b/wwwassets/signup_verification.html @@ -122,7 +122,7 @@
-
OpenWifi
+
OpenWifi
diff --git a/wwwassets/signup_verification_error.html b/wwwassets/signup_verification_error.html index 89ceb64..8992f46 100644 --- a/wwwassets/signup_verification_error.html +++ b/wwwassets/signup_verification_error.html @@ -101,7 +101,7 @@
-
OpenWifi
+
OpenWifi
diff --git a/wwwassets/signup_verification_success.html b/wwwassets/signup_verification_success.html index a49ff75..011d226 100644 --- a/wwwassets/signup_verification_success.html +++ b/wwwassets/signup_verification_success.html @@ -69,7 +69,7 @@
-
OpenWifi
+
OpenWifi
diff --git a/wwwassets/sub_404_error.css b/wwwassets/sub_404_error.css new file mode 100644 index 0000000..e69de29 diff --git a/wwwassets/sub_404_error.html b/wwwassets/sub_404_error.html new file mode 100644 index 0000000..e69de29 diff --git a/wwwassets/sub_access_policy.html b/wwwassets/sub_access_policy.html new file mode 100644 index 0000000..e69de29 diff --git a/wwwassets/sub_email_verification_error.html b/wwwassets/sub_email_verification_error.html new file mode 100644 index 0000000..e69de29 diff --git a/wwwassets/sub_email_verification_success.html b/wwwassets/sub_email_verification_success.html new file mode 100644 index 0000000..e69de29 diff --git a/wwwassets/sub_favicon.ico b/wwwassets/sub_favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..d3f1ee8f9bd0a7a1b86f163684166dedc8c84ed2 GIT binary patch literal 206910 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�yUy$-iB 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 literal 0 HcmV?d00001 diff --git a/wwwassets/sub_password_policy.html b/wwwassets/sub_password_policy.html new file mode 100644 index 0000000..e69de29 diff --git a/wwwassets/sub_password_reset.html b/wwwassets/sub_password_reset.html new file mode 100644 index 0000000..e69de29 diff --git a/wwwassets/sub_password_reset_error.html b/wwwassets/sub_password_reset_error.html new file mode 100644 index 0000000..e69de29 diff --git a/wwwassets/sub_password_reset_success.html b/wwwassets/sub_password_reset_success.html new file mode 100644 index 0000000..e69de29