Login access code via email

This commit is contained in:
dignajar 2015-10-20 00:14:28 -03:00
parent fca3ff9ab0
commit c4d8825151
26 changed files with 237 additions and 47 deletions

View File

@ -17,14 +17,14 @@ function editUser($args)
} }
} }
function setPassword($new, $confirm) function setPassword($username, $new_password, $confirm_password)
{ {
global $dbUsers; global $dbUsers;
global $Language; global $Language;
if( ($new===$confirm) && !Text::isEmpty($new) ) if( ($new_password===$confirm_password) && !Text::isEmpty($new_password) )
{ {
if( $dbUsers->setPassword($new) ) { if( $dbUsers->setPassword($username, $new_password) ) {
Alert::set($Language->g('The changes have been saved')); Alert::set($Language->g('The changes have been saved'));
} }
else { else {
@ -93,7 +93,7 @@ if( $_SERVER['REQUEST_METHOD'] == 'POST' )
deleteUser($_POST, false); deleteUser($_POST, false);
} }
elseif( !empty($_POST['new-password']) && !empty($_POST['confirm-password']) ) { elseif( !empty($_POST['new-password']) && !empty($_POST['confirm-password']) ) {
setPassword($_POST['new-password'], $_POST['confirm-password']); setPassword($_POST['username'], $_POST['new-password'], $_POST['confirm-password']);
} }
else { else {
editUser($_POST); editUser($_POST);

View File

@ -11,8 +11,67 @@
function checkPost($args) function checkPost($args)
{ {
global $Security; global $Security;
global $Login;
global $Language; global $Language;
global $dbUsers;
global $Site;
if($Security->isBlocked()) {
Alert::set($Language->g('IP address has been blocked').'<br>'.$Language->g('Try again in a few minutes'));
return false;
}
// Remove illegal characters from email
$email = Sanitize::email($args['email']);
if(Valid::email($email))
{
$user = $dbUsers->getByEmail($email);
if($user!=false)
{
// Generate the token and the token expiration date.
$token = $dbUsers->generateTokenEmail($user['username']);
// ---- EMAIL ----
$link = $Site->url().'admin/login-email?tokenEmail='.$token.'&username='.$user['username'];
$subject = $Language->g('BLUDIT Login access code');
$message = Text::replaceAssoc(
array(
'{{WEBSITE_NAME}}'=>$Site->title(),
'{{LINK}}'=>'<a href="'.$link.'">'.$link.'</a>'
),
$Language->g('email-notification-login-access-code')
);
$sent = Email::send(array(
'from'=>$Site->emailFrom(),
'to'=>$email,
'subject'=>$subject,
'message'=>$message
));
if($sent) {
Alert::set($Language->g('check-your-inbox-for-your-login-access-code'));
return true;
}
else {
Alert::set($Language->g('There was a problem sending the email'));
return false;
}
}
}
// Bruteforce protection, add IP to blacklist.
$Security->addLoginFail();
Alert::set($Language->g('check-your-inbox-for-your-login-access-code'));
return false;
}
function checkGet($args)
{
global $Security;
global $Language;
global $Login;
if($Security->isBlocked()) { if($Security->isBlocked()) {
Alert::set($Language->g('IP address has been blocked').'<br>'.$Language->g('Try again in a few minutes')); Alert::set($Language->g('IP address has been blocked').'<br>'.$Language->g('Try again in a few minutes'));
@ -20,9 +79,9 @@ function checkPost($args)
} }
// Verify User sanitize the input // Verify User sanitize the input
if( $Login->verifyUser($_POST['username'], $_POST['password']) ) if( $Login->verifyUserByToken($args['username'], $args['tokenEmail']) )
{ {
// Renew the token. This token will be the same inside the session for multiple forms. // Renew the tokenCRFS. This token will be the same inside the session for multiple forms.
$Security->generateToken(); $Security->generateToken();
Redirect::page('admin', 'dashboard'); Redirect::page('admin', 'dashboard');
@ -31,8 +90,6 @@ function checkPost($args)
// Bruteforce protection, add IP to blacklist. // Bruteforce protection, add IP to blacklist.
$Security->addLoginFail(); $Security->addLoginFail();
Alert::set($Language->g('Username or password incorrect'));
return false; return false;
} }
@ -40,6 +97,16 @@ function checkPost($args)
// Main before POST // Main before POST
// ============================================================================ // ============================================================================
// ============================================================================
// GET Method
// ============================================================================
if( !empty($_GET['tokenEmail']) && !empty($_GET['username']) )
{
checkGet($_GET);
}
// ============================================================================ // ============================================================================
// POST Method // POST Method
// ============================================================================ // ============================================================================

View File

@ -6,7 +6,7 @@ HTML::formOpen(array('class'=>'uk-form-horizontal'));
// Security token // Security token
HTML::formInputHidden(array( HTML::formInputHidden(array(
'name'=>'token', 'name'=>'tokenCSRF',
'value'=>$Security->getToken() 'value'=>$Security->getToken()
)); ));

View File

@ -6,7 +6,7 @@ HTML::formOpen(array('id'=>'jsformplugin'));
// Security token // Security token
HTML::formInputHidden(array( HTML::formInputHidden(array(
'name'=>'token', 'name'=>'tokenCSRF',
'value'=>$Security->getToken() 'value'=>$Security->getToken()
)); ));

View File

@ -6,7 +6,7 @@ HTML::formOpen(array('class'=>'uk-form-stacked'));
// Security token // Security token
HTML::formInputHidden(array( HTML::formInputHidden(array(
'name'=>'token', 'name'=>'tokenCSRF',
'value'=>$Security->getToken() 'value'=>$Security->getToken()
)); ));

View File

@ -6,7 +6,7 @@ HTML::formOpen(array('class'=>'uk-form-stacked'));
// Security token // Security token
HTML::formInputHidden(array( HTML::formInputHidden(array(
'name'=>'token', 'name'=>'tokenCSRF',
'value'=>$Security->getToken() 'value'=>$Security->getToken()
)); ));

View File

@ -6,7 +6,7 @@ HTML::formOpen(array('class'=>'uk-form-horizontal'));
// Security token // Security token
HTML::formInputHidden(array( HTML::formInputHidden(array(
'name'=>'token', 'name'=>'tokenCSRF',
'value'=>$Security->getToken() 'value'=>$Security->getToken()
)); ));

View File

@ -1,19 +1,19 @@
<div class="login-form"> <div class="login-form">
<form method="post" action="<?php echo HTML_PATH_ADMIN_ROOT.'login' ?>" class="uk-form" autocomplete="off"> <form method="post" action="<?php echo HTML_PATH_ADMIN_ROOT.'login-email' ?>" class="uk-form" autocomplete="off">
<input type="hidden" id="jstoken" name="token" value="<?php $Security->printToken() ?>"> <input type="hidden" id="jstoken" name="tokenCSRF" value="<?php $Security->printToken() ?>">
<div class="uk-form-row"> <div class="uk-form-row">
<input name="email" class="uk-width-1-1 uk-form-large" placeholder="<?php $L->p('Email') ?>" type="text"> <input name="email" class="uk-width-1-1 uk-form-large" placeholder="<?php $L->p('Email') ?>" type="text">
</div> </div>
<div class="uk-form-row"> <div class="uk-form-row">
<button type="submit" class="uk-width-1-1 uk-button uk-button-primary uk-button-large">Send me the email</button> <button type="submit" class="uk-width-1-1 uk-button uk-button-primary uk-button-large"><?php $L->p('Get login access code') ?></button>
</div> </div>
</form> </form>
</div> </div>
<a class="login-email" href="<?php echo HTML_PATH_ADMIN_ROOT.'login' ?>"> Back to login form</a> <a class="login-email" href="<?php echo HTML_PATH_ADMIN_ROOT.'login' ?>"><i class="uk-icon-chevron-left"></i> <?php $L->p('Back to login form') ?></a>

View File

@ -2,14 +2,14 @@
<form method="post" action="<?php echo HTML_PATH_ADMIN_ROOT.'login' ?>" class="uk-form" autocomplete="off"> <form method="post" action="<?php echo HTML_PATH_ADMIN_ROOT.'login' ?>" class="uk-form" autocomplete="off">
<input type="hidden" id="jstoken" name="token" value="<?php $Security->printToken() ?>"> <input type="hidden" id="jstoken" name="tokenCSRF" value="<?php $Security->printToken() ?>">
<div class="uk-form-row"> <div class="uk-form-row">
<input name="username" class="uk-width-1-1 uk-form-large" placeholder="<?php $L->p('Username') ?>" type="text"> <input name="username" class="uk-width-1-1 uk-form-large" placeholder="<?php $L->p('Username') ?>" type="text">
</div> </div>
<div class="uk-form-row"> <div class="uk-form-row">
<input name="password" class="uk-width-1-1 uk-form-large" placeholder="<?php $L->p('Password') ?>" type="text"> <input name="password" class="uk-width-1-1 uk-form-large" placeholder="<?php $L->p('Password') ?>" type="password">
</div> </div>
<div class="uk-form-row"> <div class="uk-form-row">
@ -20,4 +20,4 @@
</div> </div>
<a class="login-email" href="<?php echo HTML_PATH_ADMIN_ROOT.'login-email' ?>"><i class="uk-icon-envelope-o"></i> Log in with email</a> <a class="login-email" href="<?php echo HTML_PATH_ADMIN_ROOT.'login-email' ?>"><i class="uk-icon-envelope-o"></i> <?php $L->p('Send me a login access code') ?></a>

View File

@ -6,7 +6,7 @@ HTML::formOpen(array('class'=>'uk-form-stacked'));
// Security token // Security token
HTML::formInputHidden(array( HTML::formInputHidden(array(
'name'=>'token', 'name'=>'tokenCSRF',
'value'=>$Security->getToken() 'value'=>$Security->getToken()
)); ));

View File

@ -6,7 +6,7 @@ HTML::formOpen(array('class'=>'uk-form-stacked'));
// Security token // Security token
HTML::formInputHidden(array( HTML::formInputHidden(array(
'name'=>'token', 'name'=>'tokenCSRF',
'value'=>$Security->getToken() 'value'=>$Security->getToken()
)); ));

View File

@ -5,7 +5,7 @@ HTML::title(array('title'=>$L->g('Settings'), 'icon'=>'cogs'));
HTML::formOpen(array('class'=>'uk-form-horizontal')); HTML::formOpen(array('class'=>'uk-form-horizontal'));
HTML::formInputHidden(array( HTML::formInputHidden(array(
'name'=>'token', 'name'=>'tokenCSRF',
'value'=>$Security->getToken() 'value'=>$Security->getToken()
)); ));
@ -46,11 +46,21 @@ HTML::formOpen(array('class'=>'uk-form-horizontal'));
'tip'=>$L->g('enable-the-command-line-mode-if-you-add-edit') 'tip'=>$L->g('enable-the-command-line-mode-if-you-add-edit')
)); ));
HTML::legend(array('value'=>$L->g('Email account settings')));
HTML::formInputText(array(
'name'=>'emailFrom',
'label'=>$L->g('Sender email'),
'value'=>$Site->emailFrom(),
'class'=>'uk-width-1-2 uk-form-medium',
'tip'=>$L->g('Emails will be sent from this address')
));
HTML::legend(array('value'=>$L->g('URL Filters'))); HTML::legend(array('value'=>$L->g('URL Filters')));
HTML::formInputText(array( HTML::formInputText(array(
'name'=>'uriPost', 'name'=>'uriPost',
'label'=>$L->g('Posts'), 'label'=>$L->g('Email'),
'value'=>$Site->uriFilters('post'), 'value'=>$Site->uriFilters('post'),
'class'=>'uk-width-1-2 uk-form-medium', 'class'=>'uk-width-1-2 uk-form-medium',
'tip'=>'' 'tip'=>''

View File

@ -6,7 +6,7 @@ HTML::formOpen(array('class'=>'uk-form-horizontal'));
// Security token // Security token
HTML::formInputHidden(array( HTML::formInputHidden(array(
'name'=>'token', 'name'=>'tokenCSRF',
'value'=>$Security->getToken() 'value'=>$Security->getToken()
)); ));

View File

@ -5,7 +5,7 @@ HTML::title(array('title'=>$L->g('Settings'), 'icon'=>'cogs'));
HTML::formOpen(array('class'=>'uk-form-horizontal')); HTML::formOpen(array('class'=>'uk-form-horizontal'));
HTML::formInputHidden(array( HTML::formInputHidden(array(
'name'=>'token', 'name'=>'tokenCSRF',
'value'=>$Security->getToken() 'value'=>$Security->getToken()
)); ));

View File

@ -69,6 +69,9 @@ define('CLI_STATUS', 'published');
// Database format date // Database format date
define('DB_DATE_FORMAT', 'Y-m-d H:i'); define('DB_DATE_FORMAT', 'Y-m-d H:i');
// Token time to live for login via email. The offset is defined by http://php.net/manual/en/datetime.modify.php
define('TOKEN_TTL', '+1 day');
// Charset, default UTF-8. // Charset, default UTF-8.
define('CHARSET', 'UTF-8'); define('CHARSET', 'UTF-8');
@ -112,6 +115,7 @@ include(PATH_HELPERS.'session.class.php');
include(PATH_HELPERS.'redirect.class.php'); include(PATH_HELPERS.'redirect.class.php');
include(PATH_HELPERS.'sanitize.class.php'); include(PATH_HELPERS.'sanitize.class.php');
include(PATH_HELPERS.'valid.class.php'); include(PATH_HELPERS.'valid.class.php');
include(PATH_HELPERS.'email.class.php');
include(PATH_HELPERS.'filesystem.class.php'); include(PATH_HELPERS.'filesystem.class.php');
include(PATH_HELPERS.'alert.class.php'); include(PATH_HELPERS.'alert.class.php');
include(PATH_HELPERS.'paginator.class.php'); include(PATH_HELPERS.'paginator.class.php');

View File

@ -18,11 +18,11 @@
if( $_SERVER['REQUEST_METHOD'] == 'POST' ) if( $_SERVER['REQUEST_METHOD'] == 'POST' )
{ {
$token = isset($_POST['token']) ? Sanitize::html($_POST['token']) : false; $token = isset($_POST['tokenCSRF']) ? Sanitize::html($_POST['tokenCSRF']) : false;
if( !$Security->validateToken($token) ) if( !$Security->validateToken($token) )
{ {
Log::set(__METHOD__.LOG_SEP.'Error occurred when trying validate the token. Token ID: '.$token); Log::set(__METHOD__.LOG_SEP.'Error occurred when trying validate the tokenCSRF. Token CSRF ID: '.$token);
// Destroy the session. // Destroy the session.
Session::destroy(); Session::destroy();
@ -32,7 +32,7 @@ if( $_SERVER['REQUEST_METHOD'] == 'POST' )
} }
else else
{ {
unset($_POST['token']); unset($_POST['tokenCSRF']);
} }
} }

View File

@ -18,7 +18,8 @@ class dbSite extends dbJSON
'uriPost'=> array('inFile'=>false, 'value'=>'/post/'), 'uriPost'=> array('inFile'=>false, 'value'=>'/post/'),
'uriTag'=> array('inFile'=>false, 'value'=>'/tag/'), 'uriTag'=> array('inFile'=>false, 'value'=>'/tag/'),
'url'=> array('inFile'=>false, 'value'=>''), 'url'=> array('inFile'=>false, 'value'=>''),
'cliMode'=> array('inFile'=>false, 'value'=>true) 'cliMode'=> array('inFile'=>false, 'value'=>true),
'emailFrom'=> array('inFile'=>false, 'value'=>'')
); );
function __construct() function __construct()
@ -92,6 +93,11 @@ class dbSite extends dbJSON
return $this->db['title']; return $this->db['title'];
} }
public function emailFrom()
{
return $this->db['emailFrom'];
}
// Returns the site slogan. // Returns the site slogan.
public function slogan() public function slogan()
{ {

View File

@ -10,7 +10,9 @@ class dbUsers extends dbJSON
'password'=> array('inFile'=>false, 'value'=>''), 'password'=> array('inFile'=>false, 'value'=>''),
'salt'=> array('inFile'=>false, 'value'=>'!Pink Floyd!Welcome to the machine!'), 'salt'=> array('inFile'=>false, 'value'=>'!Pink Floyd!Welcome to the machine!'),
'email'=> array('inFile'=>false, 'value'=>''), 'email'=> array('inFile'=>false, 'value'=>''),
'registered'=> array('inFile'=>false, 'value'=>'1985-03-15 10:00') 'registered'=> array('inFile'=>false, 'value'=>'1985-03-15 10:00'),
'tokenEmail'=> array('inFile'=>false, 'value'=>''),
'tokenEmailTTL'=>array('inFile'=>false, 'value'=>'2009-03-15 14:00')
); );
function __construct() function __construct()
@ -18,7 +20,7 @@ class dbUsers extends dbJSON
parent::__construct(PATH_DATABASES.'users.php'); parent::__construct(PATH_DATABASES.'users.php');
} }
// Return an array with the username databases // Return an array with the username databases, filtered by username.
public function getDb($username) public function getDb($username)
{ {
if($this->userExists($username)) if($this->userExists($username))
@ -31,6 +33,18 @@ class dbUsers extends dbJSON
return false; return false;
} }
// Return an array with the username databases, filtered by email address.
public function getByEmail($email)
{
foreach($this->db as $user) {
if($user['email']==$email) {
return $user;
}
}
return false;
}
// Return TRUE if the user exists, FALSE otherwise. // Return TRUE if the user exists, FALSE otherwise.
public function userExists($username) public function userExists($username)
{ {
@ -42,13 +56,32 @@ class dbUsers extends dbJSON
return $this->db; return $this->db;
} }
public function setPassword($password) public function generateTokenEmail($username)
{
// Random hash
$token = sha1(Text::randomText(SALT_LENGTH).time());
$this->db[$username]['tokenEmail'] = $token;
// Token time to live, defined by TOKEN_TTL
$this->db[$username]['tokenEmailTTL'] = Date::currentOffset(DB_DATE_FORMAT, TOKEN_TTL);
// Save the database
if( $this->save() === false ) {
Log::set(__METHOD__.LOG_SEP.'Error occurred when trying to save the database file.');
return false;
}
return $token;
}
public function setPassword($username, $password)
{ {
$salt = Text::randomText(SALT_LENGTH); $salt = Text::randomText(SALT_LENGTH);
$hash = sha1($password.$salt); $hash = sha1($password.$salt);
$args['salt'] = $salt; $args['username'] = $username;
$args['password'] = $hash; $args['salt'] = $salt;
$args['password'] = $hash;
return $this->set($args); return $this->set($args);
} }

View File

@ -15,6 +15,13 @@ class Date {
return $Date->format($format); return $Date->format($format);
} }
public static function currentOffset($format, $offset)
{
$Date = new DateTime();
$Date->modify($offset);
return $Date->format($format);
}
// Format a local time/date according to locale settings. // Format a local time/date according to locale settings.
public static function format($date, $currentFormat, $outputFormat) public static function format($date, $currentFormat, $outputFormat)
{ {

View File

@ -7,18 +7,18 @@ class Email {
{ {
$headers = array(); $headers = array();
$headers[] = 'MIME-Version: 1.0'; $headers[] = 'MIME-Version: 1.0';
$headers[] = 'Content-type: text/plain; charset=utf-8'; $headers[] = 'Content-type: text/html; charset=utf-8';
$headers[] = 'From: '.$args['from']; $headers[] = 'From: '.$args['from'];
$headers[] = 'X-Mailer: PHP/'.phpversion(); $headers[] = 'X-Mailer: PHP/'.phpversion();
$message = '<html> $message = '<html>
<head> <head>
<title>Bludit</title> <title>BLUDIT</title>
</head> </head>
<body> <body>
<div style="margin: 0px auto; border: 1px solid #2672ec; padding: 10px;"> <div style="margin: 0px auto; border: 1px solid #2672ec; padding: 10px;">
<div style="font-size: 26px; padding: 10px; background-color: #2672ec;">Bludit</div> <div style="font-size: 26px; padding: 10px; background-color: #2672ec; color: #FFFFFF;">BLUDIT</div>
<p>'.$args['message'].'</p> '.$args['message'].'
</div> </div>
</body> </body>
</html>'; </html>';

View File

@ -55,6 +55,7 @@ class Sanitize {
return true; return true;
} }
// Returns the email without illegal characters.
public static function email($email) public static function email($email)
{ {
return( filter_var($email, FILTER_SANITIZE_EMAIL) ); return( filter_var($email, FILTER_SANITIZE_EMAIL) );

View File

@ -111,6 +111,11 @@ class Text {
return $text; return $text;
} }
public static function replaceAssoc(array $replace, $text)
{
return str_replace(array_keys($replace), array_values($replace), $text);
}
public static function cleanUrl($string, $separator='-') public static function cleanUrl($string, $separator='-')
{ {
// Transliterate characters to ASCII // Transliterate characters to ASCII

View File

@ -7,6 +7,7 @@ class Valid {
return filter_var($ip, FILTER_VALIDATE_IP); return filter_var($ip, FILTER_VALIDATE_IP);
} }
// Returns the email filtered or FALSE if the filter fails.
public static function email($email) public static function email($email)
{ {
return filter_var($email, FILTER_VALIDATE_EMAIL); return filter_var($email, FILTER_VALIDATE_EMAIL);

View File

@ -59,7 +59,7 @@ class Login {
$password = trim($password); $password = trim($password);
if(empty($username) || empty($password)) { if(empty($username) || empty($password)) {
Log::set(__METHOD__.LOG_SEP.'Username or Password empty. Username: '.$username.' - Password: '.$password); Log::set(__METHOD__.LOG_SEP.'Username or password empty. Username: '.$username.' - Password: '.$password);
return false; return false;
} }
@ -75,6 +75,8 @@ class Login {
{ {
$this->setLogin($username, $user['role']); $this->setLogin($username, $user['role']);
Log::set(__METHOD__.LOG_SEP.'User logged succeeded by username and password - Username: '.$username);
return true; return true;
} }
else { else {
@ -84,6 +86,50 @@ class Login {
return false; return false;
} }
public function verifyUserByToken($username, $token)
{
$username = Sanitize::html($username);
$token = Sanitize::html($token);
$username = trim($username);
$token = trim($token);
if(empty($username) || empty($token)) {
Log::set(__METHOD__.LOG_SEP.'Username or Token-email empty. Username: '.$username.' - Token-email: '.$token);
return false;
}
$user = $this->dbUsers->getDb($username);
if($user==false) {
Log::set(__METHOD__.LOG_SEP.'Username does not exist: '.$username);
return false;
}
$currentTime = Date::current(DB_DATE_FORMAT);
if($user['tokenEmailTTL']<$currentTime) {
Log::set(__METHOD__.LOG_SEP.'Token-email expired: '.$username);
return false;
}
if($token === $user['tokenEmail'])
{
// Set the user loggued.
$this->setLogin($username, $user['role']);
// Invalidate the current token.
$this->dbUsers->generateTokenEmail($username);
Log::set(__METHOD__.LOG_SEP.'User logged succeeded by Token-email - Username: '.$username);
return true;
}
else {
Log::set(__METHOD__.LOG_SEP.'Token-email incorrect.');
}
return false;
}
public function fingerPrint($random=false) public function fingerPrint($random=false)
{ {
// User agent // User agent

View File

@ -23,13 +23,13 @@ class Security extends dbJSON
$token = Text::randomText(8); $token = Text::randomText(8);
$token = sha1($token); $token = sha1($token);
Session::set('token', $token); Session::set('tokenCSRF', $token);
} }
// Validate the token. // Validate the token.
public function validateToken($token) public function validateToken($token)
{ {
$sessionToken = Session::get('token'); $sessionToken = Session::get('tokenCSRF');
return ( !empty($sessionToken) && ($sessionToken===$token) ); return ( !empty($sessionToken) && ($sessionToken===$token) );
} }
@ -37,12 +37,12 @@ class Security extends dbJSON
// Returns the token. // Returns the token.
public function getToken() public function getToken()
{ {
return Session::get('token'); return Session::get('tokenCSRF');
} }
public function printToken() public function printToken()
{ {
echo Session::get('token'); echo Session::get('tokenCSRF');
} }
// ==================================================== // ====================================================

View File

@ -179,5 +179,15 @@
"published": "Published", "published": "Published",
"scheduled-posts": "Scheduled posts", "scheduled-posts": "Scheduled posts",
"statics": "Statics", "statics": "Statics",
"name": "Name" "name": "Name",
"email-account-settings":"Email account settings",
"sender-email": "Sender email",
"emails-will-be-sent-from-this-address":"Emails will be sent from this address.",
"bludit-login-access-code": "BLUDIT - Login access code",
"check-your-inbox-for-your-login-access-code":"Check your inbox for your log in access code",
"there-was-a-problem-sending-the-email":"There was a problem sending the email",
"back-to-login-form": "Back to login form",
"send-me-a-login-access-code": "Send me a login access code",
"get-login-access-code": "Get login access code",
"email-notification-login-access-code": "<p>This is a notification from your website {{WEBSITE_NAME}}</p><p>You request a login access code, follow the next link:</p><p>{{LINK}}</p>"
} }