Adding Sub Signup

This commit is contained in:
stephb9959
2022-02-20 23:15:09 -08:00
parent 634b079f45
commit 31a550514a
21 changed files with 638 additions and 14 deletions

View File

@@ -122,7 +122,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_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)
if(NOT SMALL_BUILD)
target_link_libraries(owsec PUBLIC

2
build
View File

@@ -1 +1 @@
3
6

View File

@@ -1484,6 +1484,32 @@ paths:
403:
$ref: '#/components/responses/Unauthorized'
/signup:
post:
tags:
- Subscriber Registration
summary: This call allows a new subscriber to register themselves and their devices.
operationId: postSignup
parameters:
- in: query
name: email
schema:
type: string
format: email
required: true
- in: query
name: serialNumber
schema:
type: string
required: true
responses:
200:
$ref: '#/components/responses/Success'
400:
$ref: '#/components/responses/BadRequest'
404:
$ref: '#/components/responses/Unauthorized'
#########################################################################################
##
## These are endpoints that all services in the uCentral stack must provide

View File

@@ -86,6 +86,14 @@ namespace OpenWifi {
}
break;
case OpenWifi::SecurityObjects::LinkActions::SUB_SIGNUP: {
if(AuthService::SendEmailToSubUser(i.id, UInfo.email, AuthService::SIGNUP_VERIFICATION)) {
Logger().information(Poco::format("Send new subscriber email verification link to %s",UInfo.email));
}
StorageService()->ActionLinksDB().SentAction(i.id);
}
break;
default: {
StorageService()->ActionLinksDB().SentAction(i.id);
}

View File

@@ -16,17 +16,18 @@ namespace OpenWifi {
FORGOT_PASSWORD,
VERIFY_EMAIL,
SUB_FORGOT_PASSWORD,
SUB_VERIFY_EMAIL
SUB_VERIFY_EMAIL,
SUB_SIGNUP
};
*/
static ActionLinkManager * instance() {
static auto * instance_ = new ActionLinkManager;
static auto instance_ = new ActionLinkManager;
return instance_;
}
int Start() final;
void Stop() final;
void run();
void run() final;
private:
Poco::Thread Thr_;

View File

@@ -546,6 +546,17 @@ namespace OpenWifi {
}
break;
case SIGNUP_VERIFICATION: {
MessageAttributes Attrs;
Attrs[RECIPIENT_EMAIL] = UInfo.email;
Attrs[LOGO] = GetLogoAssetURI();
Attrs[SUBJECT] = "EMail Address Verification";
Attrs[ACTION_LINK] = MicroService::instance().GetPublicAPIEndPoint() + "/actionLink?action=signup_verification&id=" + LinkId ;
SMTPMailerService()->SendMessage(UInfo.email, "signup_verification.txt", Attrs);
UInfo.waitingForEmailCheck = true;
}
break;
default:
break;
}

View File

@@ -38,7 +38,8 @@ namespace OpenWifi{
enum EMAIL_REASON {
FORGOT_PASSWORD,
EMAIL_VERIFICATION
EMAIL_VERIFICATION,
SIGNUP_VERIFICATION
};
static ACCESS_TYPE IntToAccessType(int C);

View File

@@ -25,6 +25,8 @@ namespace OpenWifi {
return RequestResetPassword(Link);
else if(Action=="email_verification")
return DoEmailVerification(Link);
else if(Action=="signup_verification")
return DoNewSubVerification(Link);
else
return DoReturnA404();
}
@@ -34,6 +36,8 @@ namespace OpenWifi {
if(Action=="password_reset")
return CompleteResetPassword();
else if(Action=="signup_completion")
return CompleteSubVerification();
else
return DoReturnA404();
}
@@ -46,6 +50,14 @@ namespace OpenWifi {
SendHTMLFileBack(FormFile,FormVars);
}
void RESTAPI_action_links::DoNewSubVerification(SecurityObjects::ActionLink &Link) {
Logger_.information(Poco::format("REQUEST-SUB-SIGNUP(%s): For ID=%s", Request->clientAddress().toString(), Link.userId));
Poco::File FormFile{ Daemon()->AssetDir() + "/signup_verification.html"};
Types::StringPairVec FormVars{ {"UUID", Link.id},
{"PASSWORD_VALIDATION", AuthService()->PasswordValidationExpression()}};
SendHTMLFileBack(FormFile,FormVars);
}
void RESTAPI_action_links::CompleteResetPassword() {
// form has been posted...
RESTAPI_PartHandler PartHandler;
@@ -53,7 +65,7 @@ namespace OpenWifi {
if (!Form.empty()) {
auto Password1 = Form.get("password1","bla");
auto Password2 = Form.get("password1","blu");
auto Password2 = Form.get("password2","blu");
auto Id = Form.get("id","");
auto Now = std::time(nullptr);
@@ -118,6 +130,77 @@ namespace OpenWifi {
}
}
void RESTAPI_action_links::CompleteSubVerification() {
RESTAPI_PartHandler PartHandler;
Poco::Net::HTMLForm Form(*Request, Request->stream(), PartHandler);
if (!Form.empty()) {
auto Password1 = Form.get("password1","bla");
auto Password2 = Form.get("password2","blu");
auto Id = Form.get("id","");
auto Now = std::time(nullptr);
SecurityObjects::ActionLink Link;
if(!StorageService()->ActionLinksDB().GetActionLink(Id,Link)) {
return DoReturnA404();
}
if(Now > Link.expires) {
StorageService()->ActionLinksDB().CancelAction(Id);
return DoReturnA404();
}
if(Password1!=Password2 || !AuthService()->ValidateSubPassword(Password1)) {
Poco::File FormFile{ Daemon()->AssetDir() + "/password_reset_error.html"};
Types::StringPairVec FormVars{ {"UUID", Id},
{"ERROR_TEXT", "For some reason, the passwords entered do not match or they do not comply with"
" accepted password creation restrictions. Please consult our on-line help"
" to look at the our password policy. If you would like to contact us, please mention"
" id(" + Id + ")"}};
return SendHTMLFileBack(FormFile,FormVars);
}
SecurityObjects::UserInfo UInfo;
bool Found = StorageService()->SubDB().GetUserById(Link.userId,UInfo);
if(!Found) {
Poco::File FormFile{ Daemon()->AssetDir() + "/signup_verification_error.html"};
Types::StringPairVec FormVars{ {"UUID", Id},
{"ERROR_TEXT", "This request does not contain a valid user ID. Please contact your system administrator."}};
return SendHTMLFileBack(FormFile,FormVars);
}
if(UInfo.blackListed || UInfo.suspended) {
Poco::File FormFile{ Daemon()->AssetDir() + "/signup_verification_error.html"};
Types::StringPairVec FormVars{ {"UUID", Id},
{"ERROR_TEXT", "Please contact our system administrators. We have identified an error in your account that must be resolved first."}};
return SendHTMLFileBack(FormFile,FormVars);
}
bool GoodPassword = AuthService()->SetSubPassword(Password1,UInfo);
if(!GoodPassword) {
Poco::File FormFile{ Daemon()->AssetDir() + "/signup_verification_error.html"};
Types::StringPairVec FormVars{ {"UUID", Id},
{"ERROR_TEXT", "You cannot reuse one of your recent passwords."}};
return SendHTMLFileBack(FormFile,FormVars);
}
UInfo.modified = std::time(nullptr);
UInfo.changePassword = false;
UInfo.lastEmailCheck = std::time(nullptr);
UInfo.waitingForEmailCheck = false;
StorageService()->SubDB().UpdateUserInfo(UInfo.email,Link.userId,UInfo);
Poco::File FormFile{ Daemon()->AssetDir() + "/signup_verification_success.html"};
Types::StringPairVec FormVars{ {"UUID", Id},
{"USERNAME", UInfo.email} };
StorageService()->ActionLinksDB().CompleteAction(Id);
SendHTMLFileBack(FormFile,FormVars);
} else {
DoReturnA404();
}
}
void RESTAPI_action_links::DoEmailVerification(SecurityObjects::ActionLink &Link) {
auto Now = std::time(nullptr);

View File

@@ -23,8 +23,10 @@ namespace OpenWifi {
static const std::list<const char *> PathName() { return std::list<const char *>{"/api/v1/actionLink"}; };
void RequestResetPassword(SecurityObjects::ActionLink &Link);
void CompleteResetPassword();
void CompleteSubVerification();
void DoEmailVerification(SecurityObjects::ActionLink &Link);
void DoReturnA404();
void DoNewSubVerification(SecurityObjects::ActionLink &Link);
void DoGet() final;
void DoPost() final;

View File

@@ -24,6 +24,7 @@
#include "RESTAPI/RESTAPI_submfa_handler.h"
#include "RESTAPI/RESTAPI_totp_handler.h"
#include "RESTAPI/RESTAPI_subtotp_handler.h"
#include "RESTAPI/RESTAPI_signup_handler.h"
namespace OpenWifi {
@@ -48,7 +49,8 @@ namespace OpenWifi {
RESTAPI_subusers_handler,
RESTAPI_submfa_handler,
RESTAPI_totp_handler,
RESTAPI_subtotp_handler
RESTAPI_subtotp_handler,
RESTAPI_signup_handler
>(Path, Bindings, L, S,TransactionId);
}
@@ -67,7 +69,8 @@ namespace OpenWifi {
RESTAPI_preferences,
RESTAPI_subpreferences,
RESTAPI_suboauth2_handler,
RESTAPI_submfa_handler
RESTAPI_submfa_handler,
RESTAPI_signup_handler
>(Path, Bindings, L, S, TransactionId);
}
}

View File

@@ -0,0 +1,68 @@
//
// Created by stephane bourque on 2022-02-20.
//
#include "RESTAPI_signup_handler.h"
#include "StorageService.h"
#include "RESTObjects/RESTAPI_SecurityObjects.h"
namespace OpenWifi {
void RESTAPI_signup_handler::DoPost() {
auto UserName = GetParameter("email","");
auto SerialNumber = GetParameter("serialNumber","");
if(UserName.empty() || SerialNumber.empty()) {
return BadRequest(RESTAPI::Errors::MissingOrInvalidParameters);
}
if(!Utils::ValidEMailAddress(UserName)) {
return BadRequest(RESTAPI::Errors::InvalidEmailAddress);
}
if(!Utils::ValidSerialNumber(SerialNumber)) {
return BadRequest(RESTAPI::Errors::InvalidSerialNumber);
}
// Do we already exist? Can only signup once...
SecurityObjects::UserInfo Existing;
if(StorageService()->SubDB().GetUserByEmail(UserName,Existing)) {
if(Existing.signingUp.empty()) {
return BadRequest(1, "Subscriber already signed up.");
}
if(Existing.waitingForEmailCheck) {
return BadRequest(2, "Waiting for email check completion.");
}
return BadRequest(3, "Waiting for device:" + Existing.signingUp);
}
SecurityObjects::UserInfo NewSub;
NewSub.signingUp = SerialNumber;
NewSub.waitingForEmailCheck = true;
NewSub.modified = std::time(nullptr);
NewSub.creationDate = std::time(nullptr);
NewSub.id = MicroService::instance().CreateUUID();
NewSub.email = UserName;
NewSub.userRole = SecurityObjects::SUBSCRIBER;
NewSub.changePassword = true;
StorageService()->SubDB().CreateRecord(NewSub);
Logger_.information(Poco::format("SIGNUP-PASSWORD(%s): Request for %s", Request->clientAddress().toString(), UserName));
SecurityObjects::ActionLink NewLink;
NewLink.action = OpenWifi::SecurityObjects::LinkActions::SUB_SIGNUP;
NewLink.id = MicroService::CreateUUID();
NewLink.userId = NewSub.id;
NewLink.created = std::time(nullptr);
NewLink.expires = NewLink.created + (1*60*60); // 1 hour
NewLink.userAction = false;
StorageService()->ActionLinksDB().CreateAction(NewLink);
return OK();
}
}

View File

@@ -0,0 +1,38 @@
//
// Created by stephane bourque on 2022-02-20.
//
#pragma once
#include "framework/MicroService.h"
namespace OpenWifi {
class RESTAPI_signup_handler : public RESTAPIHandler {
public:
RESTAPI_signup_handler(const RESTAPIHandler::BindingMap &bindings, Poco::Logger &L, RESTAPI_GenericServer & Server, uint64_t TransactionId, bool Internal)
: RESTAPIHandler(bindings, L,
std::vector<std::string>{
Poco::Net::HTTPRequest::HTTP_POST,
Poco::Net::HTTPRequest::HTTP_OPTIONS},
Server,
TransactionId,
Internal, false, true ){}
static const std::list<const char *> PathName() { return std::list<const char *>{"/api/v1/action"}; };
/* inline bool RoleIsAuthorized(std::string & Reason) {
if(UserInfo_.userinfo.userRole != SecurityObjects::USER_ROLE::SUBSCRIBER) {
Reason = "User must be a subscriber";
return false;
}
return true;
}
*/
void DoGet() final {};
void DoPost() final;
void DoPut() final {};
void DoDelete() final {};
private:
};
}

View File

@@ -257,6 +257,7 @@ namespace OpenWifi::SecurityObjects {
field_to_json(Obj,"oauthType",oauthType);
field_to_json(Obj,"oauthUserInfo",oauthUserInfo);
field_to_json(Obj,"modified",modified);
field_to_json(Obj,"signingUp",signingUp);
};
bool UserInfo::from_json(const Poco::JSON::Object::Ptr &Obj) {
@@ -292,6 +293,7 @@ namespace OpenWifi::SecurityObjects {
field_from_json(Obj,"oauthType",oauthType);
field_from_json(Obj,"oauthUserInfo",oauthUserInfo);
field_from_json(Obj,"modified",modified);
field_from_json(Obj,"signingUp",signingUp);
return true;
} catch (const Poco::Exception &E) {

View File

@@ -138,6 +138,7 @@ namespace OpenWifi {
std::string oauthType;
std::string oauthUserInfo;
uint64_t modified;
std::string signingUp;
void to_json(Poco::JSON::Object &Obj) const;
bool from_json(const Poco::JSON::Object::Ptr &Obj);
@@ -233,7 +234,8 @@ namespace OpenWifi {
FORGOT_PASSWORD=1,
VERIFY_EMAIL,
SUB_FORGOT_PASSWORD,
SUB_VERIFY_EMAIL
SUB_VERIFY_EMAIL,
SUB_SIGNUP
};
struct ActionLink {

View File

@@ -73,7 +73,8 @@ namespace OpenWifi {
ORM::Field{"lastPasswords", ORM::FieldType::FT_TEXT},
ORM::Field{"oauthType", ORM::FieldType::FT_TEXT},
ORM::Field{"oauthUserInfo", ORM::FieldType::FT_TEXT},
ORM::Field{"modified", ORM::FieldType::FT_TEXT}
ORM::Field{"modified", ORM::FieldType::FT_TEXT},
ORM::Field{"signingUp", ORM::FieldType::FT_TEXT}
};
static ORM::IndexVec MakeIndices(const std::string & shortname) {
@@ -96,7 +97,8 @@ namespace OpenWifi {
bool BaseUserDB::Upgrade(uint32_t from, uint32_t &to) {
std::vector<std::string> Statements{
"alter table " + TableName_ + " add column modified BIGINT;"
"alter table " + TableName_ + " add column modified BIGINT;",
"alter table " + TableName_ + " add column signingUp TEXT default '';"
};
RunScript(Statements);
to = CurrentVersion;
@@ -314,6 +316,7 @@ template<> void ORM::DB<OpenWifi::UserInfoRecordTuple,
U.oauthType = T.get<28>();
U.oauthUserInfo = T.get<29>();
U.modified = T.get<30>();
U.signingUp = T.get<31>();
}
template<> void ORM::DB< OpenWifi::UserInfoRecordTuple,
@@ -350,4 +353,5 @@ template<> void ORM::DB< OpenWifi::UserInfoRecordTuple,
T.set<28>(U.oauthType);
T.set<29>(U.oauthUserInfo);
T.set<30>(U.modified);
T.set<31>(U.signingUp);
}

View File

@@ -40,7 +40,8 @@ namespace OpenWifi {
std::string, // lastPasswords;
std::string, // oauthType;
std::string, // oauthUserInfo;
uint64_t // modified
uint64_t, // modified
std::string // signingUp;
> UserInfoRecordTuple;
typedef std::vector <UserInfoRecordTuple> UserInfoRecordTupleList;
@@ -62,7 +63,7 @@ namespace OpenWifi {
class BaseUserDB : public ORM::DB<UserInfoRecordTuple, SecurityObjects::UserInfo> {
public:
const uint32_t CurrentVersion = 1;
const uint32_t CurrentVersion = 2;
BaseUserDB( const std::string &name, const std::string &shortname, OpenWifi::DBType T, Poco::Data::SessionPool & P, Poco::Logger &L, UserCache * Cache, bool users);
bool CreateUser(const std::string & Admin, SecurityObjects::UserInfo & NewUser, bool PasswordHashedAlready = false );

View File

@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
</body>
</html>

View File

@@ -0,0 +1,10 @@
Dear ${RECIPIENT_EMAIL},
Before you can access the system, you must validate your e-mail address. Please click on the link below
to complete this task.
${ACTION_LINK}
And follow the instructions.
Thank you!

View File

@@ -0,0 +1,162 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body {
font-family: Arial,
Helvetica, sans-serif;
background-color: #ebedef;
}
.body {
background-color: #ebedef;
}
input[type=text], input[type=password] {
width: 90%;
padding: 12px 20px;
margin: 8px 0;
display: inline-block;
border: 1px solid #ccc;
box-sizing: border-box;
}
button {
background-color: #04AA6D;
color: white;
padding: 14px 20px;
margin: 8px 0;
border: none;
cursor: pointer;
width: 40%;
font-size: medium;
}
button:hover {
opacity: 0.8;
}
.imgcontainer {
width: 100%;
margin-top: 5%;
text-align: center;
display: block;
}
img.avatar {
width: 40%;
border-radius: 50%;
}
.grid-container {
display: grid;
grid-template-columns: 15% 70% 15%;
grid-column-gap: 5px;
background-color: rgba(255, 255, 255, 0.8);
text-align: center;
padding: 30px;
box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2);
display: block;
width: 50%;
border: 1em;
background-color: white;
width: 40%;
height: auto;
margin-left: auto;
margin-right: auto;
margin-bottom: auto;
margin-top: 50px;
position: relative;
}
.passwordtext {
float: left;
margin-left: 5%;
}
.password-input {
margin-top: 0px;
}
.rulestext {
width: 95%;
margin: auto;
display: inline-block;
text-align: left;
text-justify: none;
margin: 5px 0 5px 0;
grid-column-start: 2;
grid-column-end: 2;
}
ul {
display: inline-block;
text-align: left;
font-size: small;
}
span.password1 {
float: right;
padding-top: 16px;
}
/* Change styles for span and cancel button on extra small screens */
@media screen and (max-width: 300px) {
span.password1 {
display: block;
float: none;
}
.cancelbtn {
width: 100%;
}
}
</style>
</head>
<body>
<div class="imgcontainer">
<img src="/wwwassets/the_logo.png" alt="OpenWifi">
</div>
<form action="/api/v1/actionLink?action=signup_completion" method="post" onsubmit="return validatePassword()">
<input type="hidden" id="custId" name="id" value="${UUID}">
<div class="grid-container">
<h2>Reset Password</h2>
<div class="passwordlabel">
<label class="passwordtext" for="password1" ><b>New Password</b></label>
<input className="password-input" id="password1" type="password" placeholder="New Password" name="password1" pattern="${PASSWORD_VALIDATION}" required>
</div>
<div class="passwordlabel">
<label class="passwordtext" for="password2"><b>Retype Password</b></label>
<input className="password-input" id="password2" type="password" placeholder="Retype Password" name="password2" pattern="${PASSWORD_VALIDATION}" required>
</div>
<div class="passwordlabel">
<button type="submit">Reset Password</button>
</div>
<div class="rulestext">
<ul>
<li>Must be at least 8 characters long</li>
<li>Must contain 1 uppercase letter</li>
<li>Must contain 1 lowercase letter</li>
<li>Must contain 1 digit</li>
<li>Must contain 1 special character</li>
</ul>
</div>
</div>
</form>
<script>
function validatePassword() {
if(document.getElementById("password1").value == document.getElementById("password2").value) {
return true;
} else {
alert("The 2 passwords did not match. The passwords must match to reset your new password.");
return false;
}
}
</script>
</body>
</html>

View File

@@ -0,0 +1,116 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body {font-family: Arial, Helvetica, sans-serif;}
form {border: 3px solid #f1f1f1;}
input[type=text], input[type=password] {
width: 100%;
padding: 12px 20px;
margin: 8px 0;
display: inline-block;
border: 1px solid #ccc;
box-sizing: border-box;
}
button {
background-color: #04AA6D;
color: white;
padding: 14px 20px;
margin: 8px 0;
border: none;
cursor: pointer;
width: 100%;
}
button:hover {
opacity: 0.8;
}
.imgcontainer {
width: 100%;
margin-top: 5%;
text-align: center;
display: block;
}
img.avatar {
width: 40%;
border-radius: 50%;
}
.container {
padding: 16px;
}
.info-card {
padding: 30px;
box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2);
display: block;
width: 50%;
border: 1em;
background-color: white;
width: 40%;
height: auto;
margin-left: auto;
margin-right: auto;
margin-bottom: auto;
margin-top: 50px;
position: relative;
}
.info-list {
width: 80%;
margin: auto;
}
.info-title {
padding-bottom: 20px;
width: 80%;
margin: auto;
}
.body {
background-color: #ebedef;
}
span.password1 {
float: right;
padding-top: 16px;
}
/* Change styles for span and cancel button on extra small screens */
@media screen and (max-width: 300px) {
span.password1 {
display: block;
float: none;
}
.cancelbtn {
width: 100%;
}
}
</style>
</head>
<body class="body">
<div class="imgcontainer">
<img src="/wwwassets/the_logo.png" alt="OpenWifi">
</div>
<div class="info-card">
<h1 class="info-title">Reset Password Failed</h1>
<div>
<h3>ID</h3>
<b>${UUID}</b>
</div>
<div>
<h3>Error</h3>
<b>${ERROR_TEXT}</b>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,76 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body {font-family: Arial, Helvetica, sans-serif;}
form {border: 3px solid #f1f1f1;}
input[type=text], input[type=password] {
width: 100%;
padding: 12px 20px;
margin: 8px 0;
display: inline-block;
border: 1px solid #ccc;
box-sizing: border-box;
}
button {
background-color: #04AA6D;
color: white;
padding: 14px 20px;
margin: 8px 0;
border: none;
cursor: pointer;
width: 100%;
}
button:hover {
opacity: 0.8;
}
.imgcontainer {
text-align: center;
margin: 24px 0 12px 0;
}
img.avatar {
width: 40%;
border-radius: 50%;
}
.container {
padding: 16px;
}
span.password1 {
float: right;
padding-top: 16px;
}
/* Change styles for span and cancel button on extra small screens */
@media screen and (max-width: 300px) {
span.password1 {
display: block;
float: none;
}
.cancelbtn {
width: 100%;
}
}
</style>
</head>
<body>
<div class="imgcontainer">
<img src="/wwwassets/the_logo.png" alt="Avatar" class="avatar">
</div>
<h1>Signup was successfully done</h1>
<div>
<h3>Thank you ${USERNAME} for signing up for service.</h3>
</div>
<div>
You can access your account using the mobile app.
</div>
</body>
</html>