mirror of
https://github.com/typemill/typemill.git
synced 2025-08-01 11:50:28 +02:00
Add auth code feature, add simpleMail model and refactor system settings
This commit is contained in:
@@ -7,6 +7,7 @@ use Psr\Http\Message\ResponseInterface as Response;
|
||||
use Slim\Routing\RouteContext;
|
||||
use Typemill\Models\Validation;
|
||||
use Typemill\Models\User;
|
||||
use Typemill\Models\SimpleMail;
|
||||
use Typemill\Static\Translations;
|
||||
|
||||
class ControllerWebAuth extends Controller
|
||||
@@ -21,68 +22,215 @@ class ControllerWebAuth extends Controller
|
||||
|
||||
public function login(Request $request, Response $response)
|
||||
{
|
||||
/*
|
||||
if( ( null !== $request->getattribute('csrf_result') ) OR ( $request->getattribute('csrf_result') === false ) )
|
||||
{
|
||||
$this->c->flash->addMessage('error', 'The form has a timeout, please try again.');
|
||||
|
||||
return $response->withHeader('Location', $this->routeParser->urlFor('auth.show'));
|
||||
}
|
||||
*/
|
||||
|
||||
$input = $request->getParsedBody();
|
||||
$validation = new Validation();
|
||||
# $settings = $this->c->get('settings');
|
||||
$securitylog = $this->settings['securitylog'] ?? false;
|
||||
$authcodeactive = $this->settings['authcode'] ?? false;
|
||||
|
||||
if($validation->signin($input) === true)
|
||||
if($validation->signin($input) !== true)
|
||||
{
|
||||
$user = new User();
|
||||
|
||||
if(!$user->setUserWithPassword($input['username']))
|
||||
if($securitylog)
|
||||
{
|
||||
$this->c->get('flash')->addMessage('error', Translations::translate('Wrong password or username, please try again.'));
|
||||
|
||||
return $response->withHeader('Location', $this->routeParser->urlFor('auth.show'))->withStatus(302);
|
||||
\Typemill\Static\Helpers::addLogEntry('login: invalid data');
|
||||
}
|
||||
|
||||
$userdata = $user->getUserData();
|
||||
$this->c->get('flash')->addMessage('error', Translations::translate('Wrong password or username, please try again.'));
|
||||
|
||||
if($userdata && password_verify($input['password'], $userdata['password']))
|
||||
{
|
||||
# check if user has confirmed the account
|
||||
if(isset($userdata['optintoken']) && $userdata['optintoken'])
|
||||
{
|
||||
$this->c->get('flash')->addMessage('error', Translations::translate('Your registration is not confirmed yet. Please check your e-mails and use the confirmation link.'));
|
||||
return $response->withHeader('Location', $this->routeParser->urlFor('auth.show'))->withStatus(302);
|
||||
}
|
||||
|
||||
$user->login();
|
||||
|
||||
# return $response->withHeader('Location', $this->routeParser->urlFor('settings.show'))->withStatus(302);
|
||||
|
||||
# if user is allowed to view content-area
|
||||
$acl = $this->c->get('acl');
|
||||
if($acl->hasRole($userdata['userrole']) && $acl->isAllowed($userdata['userrole'], 'content', 'view'))
|
||||
{
|
||||
$editor = (isset($this->settings['editor']) && $this->settings['editor'] == 'visual') ? 'visual' : 'raw';
|
||||
|
||||
return $response->withHeader('Location', $this->routeParser->urlFor('content.' . $editor))->withStatus(302);
|
||||
}
|
||||
|
||||
return $response->withHeader('Location', $this->routeParser->urlFor('user.account'))->withStatus(302);
|
||||
}
|
||||
return $response->withHeader('Location', $this->routeParser->urlFor('auth.show'))->withStatus(302);
|
||||
}
|
||||
|
||||
if(isset($this->settings['securitylog']) && $this->settings['securitylog'])
|
||||
$user = new User();
|
||||
|
||||
if(!$user->setUserWithPassword($input['username']))
|
||||
{
|
||||
\Typemill\Static\Helpers::addLogEntry('wrong login');
|
||||
if($securitylog)
|
||||
{
|
||||
\Typemill\Static\Helpers::addLogEntry('login: user not found');
|
||||
}
|
||||
|
||||
$this->c->get('flash')->addMessage('error', Translations::translate('Wrong password or username, please try again.'));
|
||||
|
||||
return $response->withHeader('Location', $this->routeParser->urlFor('auth.show'))->withStatus(302);
|
||||
}
|
||||
|
||||
$this->c->get('flash')->addMessage('error', Translations::translate('Wrong password or username, please try again.'));
|
||||
$userdata = $user->getUserData();
|
||||
$authcodedata = $this->checkAuthcode($userdata);
|
||||
|
||||
return $response->withHeader('Location', $this->routeParser->urlFor('auth.show'))->withStatus(302);
|
||||
if($userdata && !password_verify($input['password'], $userdata['password']))
|
||||
{
|
||||
if($securitylog)
|
||||
{
|
||||
\Typemill\Static\Helpers::addLogEntry('login: wrong password');
|
||||
}
|
||||
|
||||
# always show authcode page, so attacker does not know if email or password was wrong or mail was send.
|
||||
if($authcodeactive && !$authcodedata['valid'])
|
||||
{
|
||||
# a bit slower because send mail takes some time usually
|
||||
usleep(rand(100000, 200000));
|
||||
|
||||
# show authcode page
|
||||
return $this->c->get('view')->render($response, 'auth/authcode.twig', [
|
||||
'username' => $userdata['username'],
|
||||
]);
|
||||
}
|
||||
|
||||
$this->c->get('flash')->addMessage('error', Translations::translate('Wrong password or username, please try again.'));
|
||||
|
||||
return $response->withHeader('Location', $this->routeParser->urlFor('auth.show'))->withStatus(302);
|
||||
}
|
||||
|
||||
# check device fingerprint
|
||||
if($authcodeactive)
|
||||
{
|
||||
$fingerprint = $this->generateDeviceFingerprint();
|
||||
if(!$this->findDeviceFingerprint($fingerprint, $userdata))
|
||||
{
|
||||
# invalidate authcodedata so user has to use a new authcode again
|
||||
$authcodedata['valid'] = false;
|
||||
$authcodedata['validated'] = 12345;
|
||||
}
|
||||
}
|
||||
|
||||
if($authcodeactive && !$authcodedata['valid'] )
|
||||
{
|
||||
# generate new authcode
|
||||
$authcodevalue = rand(10000, 99999);
|
||||
|
||||
$mail = new SimpleMail($settings);
|
||||
|
||||
$subject = Translations::translate('Your authentication code for Typemill');
|
||||
$message = Translations::translate('Use the following authentication code to login into Typemill cms') . ': ' . $authcodevalue;
|
||||
|
||||
$send = $mail->send($userdata['email'], $subject, $message);
|
||||
|
||||
$send = true;
|
||||
|
||||
if(!$send)
|
||||
{
|
||||
$title = Translations::translate('Error sending email');
|
||||
$message = Translations::translate('Dear ') . $userdata['username'] . ', ' . Translations::translate('we could not send the email with the authentication code to your address. Reason: ') . $mail->error;
|
||||
}
|
||||
else
|
||||
{
|
||||
# store authcode
|
||||
$user->setValue('authcodedata', $authcodevalue . ':' . time() . ':' . $authcodedata['validated']);
|
||||
$user->updateUser();
|
||||
}
|
||||
|
||||
# show authcode page
|
||||
return $this->c->get('view')->render($response, 'auth/authcode.twig', [
|
||||
'username' => $userdata['username'],
|
||||
]);
|
||||
}
|
||||
|
||||
# check if user has confirmed the account
|
||||
if(isset($userdata['optintoken']) && $userdata['optintoken'])
|
||||
{
|
||||
if($securitylog)
|
||||
{
|
||||
\Typemill\Static\Helpers::addLogEntry('login: user not confirmed yet.');
|
||||
}
|
||||
|
||||
$this->c->get('flash')->addMessage('error', Translations::translate('Your registration is not confirmed yet. Please check your e-mails and use the confirmation link.'));
|
||||
|
||||
return $response->withHeader('Location', $this->routeParser->urlFor('auth.show'))->withStatus(302);
|
||||
}
|
||||
|
||||
$user->login();
|
||||
|
||||
# return $response->withHeader('Location', $this->routeParser->urlFor('settings.show'))->withStatus(302);
|
||||
|
||||
# if user is allowed to view content-area
|
||||
$acl = $this->c->get('acl');
|
||||
if($acl->hasRole($userdata['userrole']) && $acl->isAllowed($userdata['userrole'], 'content', 'view'))
|
||||
{
|
||||
$editor = (isset($this->settings['editor']) && $this->settings['editor'] == 'visual') ? 'visual' : 'raw';
|
||||
|
||||
return $response->withHeader('Location', $this->routeParser->urlFor('content.' . $editor))->withStatus(302);
|
||||
}
|
||||
|
||||
return $response->withHeader('Location', $this->routeParser->urlFor('user.account'))->withStatus(302);
|
||||
}
|
||||
|
||||
|
||||
# login user with valid authcode
|
||||
public function loginWithAuthcode(Request $request, Response $response)
|
||||
{
|
||||
$input = $request->getParsedBody();
|
||||
$validation = new Validation();
|
||||
$securitylog = $this->settings['securitylog'] ?? false;
|
||||
|
||||
if($validation->authcode($input) !== true)
|
||||
{
|
||||
if($securitylog)
|
||||
{
|
||||
\Typemill\Static\Helpers::addLogEntry('login: invalid authcode format');
|
||||
}
|
||||
|
||||
$this->c->get('flash')->addMessage('error', Translations::translate('Invalid authcode format, please try again.'));
|
||||
|
||||
return $response->withHeader('Location', $this->routeParser->urlFor('auth.show'))->withStatus(302);
|
||||
}
|
||||
|
||||
$user = new User();
|
||||
|
||||
if(!$user->setUserWithPassword($input['username']))
|
||||
{
|
||||
if($securitylog)
|
||||
{
|
||||
\Typemill\Static\Helpers::addLogEntry('login: user not found');
|
||||
}
|
||||
|
||||
$this->c->get('flash')->addMessage('error', Translations::translate('Wrong password or username, please try again.'));
|
||||
|
||||
return $response->withHeader('Location', $this->routeParser->urlFor('auth.show'))->withStatus(302);
|
||||
}
|
||||
|
||||
$userdata = $user->getUserData();
|
||||
$authcodevalue = $input['code-1'] . $input['code-2'] . $input['code-3'] . $input['code-4'] . $input['code-5'];
|
||||
$authcodedata = $this->checkAuthcode($userdata);
|
||||
|
||||
if(!$this->validateAuthcode($authcodevalue, $authcodedata))
|
||||
{
|
||||
if($securitylog)
|
||||
{
|
||||
\Typemill\Static\Helpers::addLogEntry('login: authcode wrong or outdated.');
|
||||
}
|
||||
|
||||
$this->c->get('flash')->addMessage('error', Translations::translate('The authcode was wrong or outdated, please start again.'));
|
||||
|
||||
return $response->withHeader('Location', $this->routeParser->urlFor('auth.show'))->withStatus(302);
|
||||
}
|
||||
|
||||
# add the device fingerprint if not set yet
|
||||
$fingerprints = $userdata['fingerprints'] ?? [];
|
||||
$fingerprint = $this->generateDeviceFingerprint();
|
||||
if(!$this->findDeviceFingerprint($fingerprint, $fingerprints))
|
||||
{
|
||||
$fingerprints[] = $fingerprint;
|
||||
$user->setValue('fingerprints', $fingerprints);
|
||||
}
|
||||
|
||||
# update authcode lastValidation and store
|
||||
$user->setValue('authcodedata', $authcodevalue . ':' . $authcodedata['generated'] . ':' . time());
|
||||
$user->updateUser();
|
||||
|
||||
$user->login();
|
||||
|
||||
# if user is allowed to view content-area
|
||||
$acl = $this->c->get('acl');
|
||||
if($acl->hasRole($userdata['userrole']) && $acl->isAllowed($userdata['userrole'], 'content', 'view'))
|
||||
{
|
||||
$editor = (isset($this->settings['editor']) && $this->settings['editor'] == 'visual') ? 'visual' : 'raw';
|
||||
|
||||
return $response->withHeader('Location', $this->routeParser->urlFor('content.' . $editor))->withStatus(302);
|
||||
}
|
||||
|
||||
return $response->withHeader('Location', $this->routeParser->urlFor('user.account'))->withStatus(302);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* log out a user
|
||||
*
|
||||
@@ -97,4 +245,106 @@ class ControllerWebAuth extends Controller
|
||||
|
||||
return $response->withHeader('Location', $this->routeParser->urlFor('auth.show'))->withStatus(302);
|
||||
}
|
||||
|
||||
|
||||
# check if the stored authcode in userdata is valid and/or fresh
|
||||
private function checkAuthcode($userdata)
|
||||
{
|
||||
# format: 12345:time(generated):time(validated)
|
||||
|
||||
$authcodedata = $userdata['authcodedata'] ?? false;
|
||||
|
||||
if(!$authcodedata)
|
||||
{
|
||||
return $authcode = [
|
||||
'value' => 12345,
|
||||
'generated' => 12345,
|
||||
'validated' => 12345,
|
||||
'valid' => false,
|
||||
'fresh' => false
|
||||
];
|
||||
}
|
||||
|
||||
$validation = new Validation();
|
||||
$authcodedata = explode(":", $authcodedata);
|
||||
|
||||
# validate format here, do we need it?
|
||||
|
||||
$now = time();
|
||||
$lastValidation = 60 * 60 * 24;
|
||||
$lastGeneration = 60 * 5;
|
||||
$valid = false;
|
||||
$fresh = false;
|
||||
|
||||
# if last validation is less than 24 hours old
|
||||
if($now - $lastValidation < $authcodedata[2])
|
||||
{
|
||||
$valid = true;
|
||||
}
|
||||
|
||||
# if last generation is less than 5 minutes old
|
||||
if($now - $lastGeneration < $authcodedata[1])
|
||||
{
|
||||
$fresh = true;
|
||||
}
|
||||
|
||||
$authcode = [
|
||||
'value' => $authcodedata[0],
|
||||
'generated' => $authcodedata[1],
|
||||
'validated' => $authcodedata[2],
|
||||
'valid' => $valid,
|
||||
'fresh' => $fresh
|
||||
];
|
||||
|
||||
return $authcode;
|
||||
}
|
||||
|
||||
# check if the submitted authcode is the same as the stored authcode
|
||||
private function validateAuthcode($authcodevalue, $authcodedata)
|
||||
{
|
||||
if($authcodedata['valid'] === true)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if($authcodedata['fresh'] === false)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if($authcodevalue == $authcodedata['value'])
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
# create a simple device fingerprint
|
||||
private function generateDeviceFingerprint()
|
||||
{
|
||||
$userAgent = $_SERVER['HTTP_USER_AGENT'];
|
||||
$ipAddress = $_SERVER['REMOTE_ADDR'];
|
||||
$acceptLanguage = isset($_SERVER['HTTP_ACCEPT_LANGUAGE']) ? $_SERVER['HTTP_ACCEPT_LANGUAGE'] : '';
|
||||
|
||||
$fingerprint = md5($userAgent . $ipAddress . $acceptLanguage);
|
||||
|
||||
return $fingerprint;
|
||||
}
|
||||
|
||||
# create a simple device fingerprint
|
||||
private function findDeviceFingerprint($fingerprint, $userdata)
|
||||
{
|
||||
if(!isset($userdata['fingerprints']) or empty($userdata['fingerprints']))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if(!in_array($fingerprint, $userdata['fingerprints']))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
@@ -7,6 +7,7 @@ use Psr\Http\Message\ResponseInterface as Response;
|
||||
use Slim\Routing\RouteContext;
|
||||
use Typemill\Models\User;
|
||||
use Typemill\Models\Validation;
|
||||
use Typemill\Models\SimpleMail;
|
||||
use Typemill\Static\Translations;
|
||||
use Typemill\Extensions\ParsedownExtension;
|
||||
|
||||
@@ -50,15 +51,9 @@ class ControllerWebRecover extends Controller
|
||||
$link = '<a href="'. $url . '">' . $url . '</a>';
|
||||
|
||||
# define the headers
|
||||
$headers = 'Content-Type: text/html; charset=utf-8' . "\r\n";
|
||||
$headers .= 'Content-Transfer-Encoding: base64' . "\r\n";
|
||||
if(isset($settings['recoverfrom']) && $settings['recoverfrom'] != '')
|
||||
{
|
||||
$headers .= 'From: ' . $settings['recoverfrom'];
|
||||
}
|
||||
$mail = new SimpleMail($settings);
|
||||
|
||||
$subjectline = (isset($settings['recoversubject']) && ($settings['recoversubject'] != '') ) ? $settings['recoversubject'] : 'Recover your password';
|
||||
$subject = '=?UTF-8?B?' . base64_encode($subjectline) . '?=';
|
||||
$subject = (isset($settings['recoversubject']) && ($settings['recoversubject'] != '') ) ? $settings['recoversubject'] : 'Recover your password';
|
||||
|
||||
$messagetext = Translations::translate('Dear user');
|
||||
$messagetext .= ",<br/><br/>";
|
||||
@@ -72,16 +67,14 @@ class ControllerWebRecover extends Controller
|
||||
$messagetext = $parsedown->markup($contentArray);
|
||||
}
|
||||
|
||||
$message = base64_encode($messagetext . "<br/><br/>" . $link);
|
||||
$message = $messagetext . "<br/><br/>" . $link;
|
||||
|
||||
# $send = mail($requiredUser['email'], $subject, $message, $headers);
|
||||
$send = $mail->send($requiredUser['email'], $subject, $message);
|
||||
|
||||
$send = false;
|
||||
|
||||
if($send == 'delete')
|
||||
if(!$send)
|
||||
{
|
||||
$title = Translations::translate('Error sending email');
|
||||
$message = Translations::translate('Dear ') . $requiredUser['username'] . ', ' . Translations::translate('we could not send the email with the password instructions to your address. Please contact the website owner and ask for help.');
|
||||
$message = Translations::translate('Dear ') . $requiredUser['username'] . ', ' . Translations::translate('we could not send the email with the password instructions to your address. Reason: ') . $mail->error;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@@ -95,7 +95,11 @@ class ControllerWebSetup extends Controller
|
||||
|
||||
# create initial settings file
|
||||
$settingsModel = new Settings();
|
||||
$settingsModel->createSettings();
|
||||
$settingsModel->createSettings([
|
||||
'author' => $params['username'],
|
||||
'mailfrom' => $params['email'],
|
||||
'mailfromname' => $params['username']
|
||||
]);
|
||||
|
||||
$urlinfo = $this->c->get('urlinfo');
|
||||
$route = $urlinfo['baseurl'] . '/tm/system';
|
||||
|
@@ -224,18 +224,26 @@ class Settings
|
||||
return $settingsDefinitions;
|
||||
}
|
||||
|
||||
public function createSettings()
|
||||
public function createSettings(array $defaultSettings = NULL)
|
||||
{
|
||||
$language = Translations::whichLanguage();
|
||||
$defaults = [
|
||||
'language' => Translations::whichLanguage()
|
||||
];
|
||||
|
||||
if($defaultSettings)
|
||||
{
|
||||
$defaults = array_merge($defaults, $defaultSettings);
|
||||
}
|
||||
|
||||
$initialSettings = $this->storage->updateYaml('settingsFolder', '', 'settings.yaml', [
|
||||
'language' => $language
|
||||
$defaults
|
||||
]);
|
||||
|
||||
if($initialSettings)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
63
system/typemill/Models/SimpleMail.php
Normal file
63
system/typemill/Models/SimpleMail.php
Normal file
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace Typemill\Models;
|
||||
|
||||
class SimpleMail
|
||||
{
|
||||
private $from = false;
|
||||
|
||||
private $reply = false;
|
||||
|
||||
public $error;
|
||||
|
||||
public function __construct($settings)
|
||||
{
|
||||
if(isset($settings['mailfrom']) && $settings['mailfrom'] != '')
|
||||
{
|
||||
$this->from = trim($settings['mailfrom']);
|
||||
|
||||
if(isset($settings['mailfromname']) && $settings['mailfromname'] != '')
|
||||
{
|
||||
$this->from = '=?UTF-8?B?' . base64_encode($settings['mailfromname']) . '?= <' . trim($settings['mailfrom']) . '>';
|
||||
}
|
||||
}
|
||||
|
||||
if(isset($settings['mailreply']) && $settings['mailreply'] != '')
|
||||
{
|
||||
$this->reply = trim($settings['mailreply']);
|
||||
}
|
||||
}
|
||||
|
||||
public function sendEmail(string $to, string $subject, string $message)
|
||||
{
|
||||
if(!$this->from)
|
||||
{
|
||||
$this->error = 'You need to add a email address into the settings.';
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
# 'Reply-To: webmaster@example.com' . "\r\n" .
|
||||
|
||||
$headers = 'Content-Type: text/html; charset=utf-8' . "\r\n";
|
||||
$headers .= 'Content-Transfer-Encoding: base64' . "\r\n";
|
||||
$headers .= 'From: ' . $this->from . "\r\n";
|
||||
if($this->$reply)
|
||||
{
|
||||
$headers .= 'Reply-To: base64' . $this->reply . "\r\n";
|
||||
}
|
||||
$headers .= 'X-Mailer: PHP/' . phpversion();
|
||||
|
||||
$subject = '=?UTF-8?B?' . base64_encode($subject) . '?=';
|
||||
$message = base64_encode($message);
|
||||
|
||||
$send = mail($to, $subject, $message, $headers);
|
||||
|
||||
if($send !== true)
|
||||
{
|
||||
$this->error = error_get_last()['message'];
|
||||
}
|
||||
|
||||
return $send;
|
||||
}
|
||||
}
|
@@ -274,6 +274,32 @@ class Validation
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* validation for authcode confirmation
|
||||
*
|
||||
* @param array $params with form data.
|
||||
* @return obj $v the validation object passed to a result method.
|
||||
*/
|
||||
|
||||
public function authcode(array $params)
|
||||
{
|
||||
$v = new Validator($params);
|
||||
$v->rule('required', ['username', 'code-1', 'code-2', 'code-3', 'code-4', 'code-5'])->message("Required");
|
||||
$v->rule('alphaNum', 'username')->message("Invalid characters");
|
||||
$v->rule('regex', 'code-1', '/^[0-9]{1}$/')->message("Must be 1-9");
|
||||
$v->rule('regex', 'code-2', '/^[0-9]{1}$/')->message("Must be 1-9");
|
||||
$v->rule('regex', 'code-3', '/^[0-9]{1}$/')->message("Must be 1-9");
|
||||
$v->rule('regex', 'code-4', '/^[0-9]{1}$/')->message("Must be 1-9");
|
||||
$v->rule('regex', 'code-5', '/^[0-9]{1}$/')->message("Must be 1-9");
|
||||
|
||||
if($v->validate())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* validation for setup user (in backoffice)
|
||||
|
@@ -194,23 +194,6 @@ abstract class Plugin implements EventSubscriberInterface
|
||||
$this->container->get('assets')->addJS($JS);
|
||||
}
|
||||
|
||||
/*
|
||||
protected function addEditorJS($JS)
|
||||
{
|
||||
$this->container->get('assets')->addEditorJS($JS);
|
||||
}
|
||||
|
||||
protected function addEditorInlineJS($JS)
|
||||
{
|
||||
$this->container->get('assets')->addEditorInlineJS($JS);
|
||||
}
|
||||
|
||||
protected function addEditorCSS($CSS)
|
||||
{
|
||||
$this->container->get('assets')->addEditorCSS($CSS);
|
||||
}
|
||||
*/
|
||||
|
||||
protected function addInlineJS($JS)
|
||||
{
|
||||
$this->container->get('assets')->addInlineJS($JS);
|
||||
|
@@ -51,7 +51,6 @@ class Helpers{
|
||||
return $ip;
|
||||
}
|
||||
|
||||
|
||||
public static function addLogEntry($action)
|
||||
{
|
||||
$line = self::getUserIP();
|
||||
|
103
system/typemill/author/auth/authcode.twig
Normal file
103
system/typemill/author/auth/authcode.twig
Normal file
@@ -0,0 +1,103 @@
|
||||
{% extends 'layouts/layoutAuth.twig' %}
|
||||
|
||||
{% block title %}Login Authentication Code{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="lg:flex-row flex flex-col-reverse justify-end">
|
||||
|
||||
<div class="lg:w-1/2 bg-teal-600 text-white min-h-screen flex justify-center items-center">
|
||||
<div class="max-w-md content-center">
|
||||
|
||||
<h1 class="text-4xl py-5">Authentication Code</h1>
|
||||
|
||||
<p>Enter the auth code from the e-mail you got:</a>
|
||||
|
||||
<form method="POST" action="{{ url_for("auth.authcode") }}" autocomplete="off">
|
||||
|
||||
<fieldset>
|
||||
|
||||
<div class="flex justify-between my-2">
|
||||
<input
|
||||
type="text"
|
||||
name="code-1"
|
||||
pattern="[0-9]"
|
||||
maxlength="1"
|
||||
oninput="moveToNextField(this)"
|
||||
class="mr-2 form-control block w-full px-3 py-3 text-xl text-center font-normal text-gray-700 bg-white bg-clip-padding border border-solid border-gray-300 transition ease-in-out m-0 focus:text-gray-700 focus:bg-white focus:border-blue-600 focus:outline-non"
|
||||
required>
|
||||
<input
|
||||
type="text"
|
||||
name="code-2"
|
||||
pattern="[0-9]"
|
||||
maxlength="1"
|
||||
oninput="moveToNextField(this)"
|
||||
class="mr-2 form-control block w-full px-3 py-3 text-xl text-center font-normal text-gray-700 bg-white bg-clip-padding border border-solid border-gray-300 transition ease-in-out m-0 focus:text-gray-700 focus:bg-white focus:border-blue-600 focus:outline-non"
|
||||
required>
|
||||
<input
|
||||
type="text"
|
||||
name="code-3"
|
||||
pattern="[0-9]"
|
||||
maxlength="1"
|
||||
oninput="moveToNextField(this)"
|
||||
class="mr-2 form-control block w-full px-3 py-3 text-xl text-center font-normal text-gray-700 bg-white bg-clip-padding border border-solid border-gray-300 transition ease-in-out m-0 focus:text-gray-700 focus:bg-white focus:border-blue-600 focus:outline-non"
|
||||
required>
|
||||
<input
|
||||
type="text"
|
||||
name="code-4"
|
||||
pattern="[0-9]"
|
||||
maxlength="1"
|
||||
oninput="moveToNextField(this)"
|
||||
class="mr-2 form-control block w-full px-3 py-3 text-xl text-center font-normal text-gray-700 bg-white bg-clip-padding border border-solid border-gray-300 transition ease-in-out m-0 focus:text-gray-700 focus:bg-white focus:border-blue-600 focus:outline-non"
|
||||
required>
|
||||
<input
|
||||
type="text"
|
||||
name="code-5"
|
||||
pattern="[0-9]"
|
||||
maxlength="1"
|
||||
oninput="moveToNextField(this)"
|
||||
class="mr-2 form-control block w-full px-3 py-3 text-xl text-center font-normal text-gray-700 bg-white bg-clip-padding border border-solid border-gray-300 transition ease-in-out m-0 focus:text-gray-700 focus:bg-white focus:border-blue-600 focus:outline-non"
|
||||
required>
|
||||
</div>
|
||||
|
||||
<input type="hidden" value="{{username}}" name="username">
|
||||
|
||||
<div class="personal-mail hidden">
|
||||
<label>Personal Mail</label>
|
||||
<input type="text" name="personal-honey-mail">
|
||||
</div>
|
||||
|
||||
<input
|
||||
type="submit"
|
||||
value="{{ translate('Confirm') }}"
|
||||
class="block w-full mt-6 px-3 py-3 border-0 font-medium text-xs leading-tight uppercase border-2 border-stone-50 text-stone-50 pointer hover:bg-stone-50 hover:text-stone-800 cursor-pointer focus:outline-none focus:ring-0 transition duration-100 ease-in-out"
|
||||
/>
|
||||
|
||||
</fieldset>
|
||||
<script>
|
||||
function moveToNextField(currentField) {
|
||||
var inputValue = currentField.value;
|
||||
var inputLength = inputValue.length;
|
||||
if (inputLength === 1)
|
||||
{
|
||||
var nextField = currentField.nextElementSibling;
|
||||
if (nextField)
|
||||
{
|
||||
nextField.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="lg:w-1/2 lg:bg-white lg:text-black p-5 bg-teal-600 text-white border-b border-white content-center flex justify-center items-center">
|
||||
<div class="max-w-md content-center">
|
||||
<h2 class="text-4xl py-5">{{ translate('Auth code missing?') }}</h2>
|
||||
<p>{{ translate('If you did not receive an email with an authentication code, then the username or password you entered was wrong. Please try again.') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
@@ -18,6 +18,11 @@ $app->group('/tm', function (RouteCollectorProxy $group) use ($settings) {
|
||||
$group->get('/login', ControllerWebAuth::class . ':show')->setName('auth.show');
|
||||
$group->post('/login', ControllerWebAuth::class . ':login')->setName('auth.login');
|
||||
|
||||
if(isset($settings['authcode']) && $settings['authcode'])
|
||||
{
|
||||
$group->post('/authcode', ControllerWebAuth::class . ':loginWithAuthcode')->setName('auth.authcode');
|
||||
}
|
||||
|
||||
if(isset($settings['recoverpw']) && $settings['recoverpw'])
|
||||
{
|
||||
$group->get('/recover', ControllerWebRecover::class . ':showRecoverForm')->setName('auth.recoverform');
|
||||
@@ -50,6 +55,7 @@ $app->group('/tm', function (RouteCollectorProxy $group) use ($routeParser,$acl)
|
||||
|
||||
$app->redirect('/tm', $routeParser->urlFor('auth.show'), 302);
|
||||
$app->redirect('/tm/', $routeParser->urlFor('auth.show'), 302);
|
||||
$app->redirect('/tm/authcode', $routeParser->urlFor('auth.show'), 302);
|
||||
|
||||
# downloads
|
||||
$app->get('/media/files[/{params:.*}]', ControllerWebDownload::class . ':download')->setName('download.file');
|
||||
|
@@ -85,6 +85,7 @@ meta:
|
||||
reference:
|
||||
type: text
|
||||
label: Reference to page
|
||||
placeholder: '/path/to/internal/page or https://exgernal-page.org'
|
||||
maxlength: 200
|
||||
referencetype:
|
||||
type: radio
|
||||
|
@@ -4,19 +4,20 @@ fieldsetsystem:
|
||||
fields:
|
||||
title:
|
||||
type: text
|
||||
label: Website title
|
||||
label: 'Website title'
|
||||
maxlength: 60
|
||||
css: lg:w-half
|
||||
author:
|
||||
type: text
|
||||
label: Website owner
|
||||
label: 'Website owner'
|
||||
css: lg:w-half
|
||||
maxlength: 60
|
||||
copyright:
|
||||
type: select
|
||||
label: Copyright
|
||||
label: 'Copyright'
|
||||
css: lg:w-half
|
||||
maxlength: 60
|
||||
description: 'Used for copyright and year in footer.'
|
||||
options:
|
||||
'©': '©'
|
||||
'CC-BY': 'CC-BY'
|
||||
@@ -30,11 +31,13 @@ fieldsetsystem:
|
||||
label: Year
|
||||
css: lg:w-half
|
||||
maxlength: 4
|
||||
description: 'Used for copyright and year in footer.'
|
||||
language:
|
||||
type: select
|
||||
label: Language (author area)
|
||||
label: 'Language (author area)'
|
||||
css: lg:w-half
|
||||
maxlength: 60
|
||||
description: 'Used for translations in author area, themes, and plugins.'
|
||||
options:
|
||||
'en': 'English'
|
||||
'ru': 'Russian'
|
||||
@@ -44,15 +47,16 @@ fieldsetsystem:
|
||||
'fr': 'French'
|
||||
langattr:
|
||||
type: text
|
||||
label: Language attribute (website)
|
||||
label: 'Language attribute (website)'
|
||||
css: lg:w-half
|
||||
maxlength: 5
|
||||
description: Please use ISO 639-1 codes like "en"
|
||||
description: 'Used for frontend language attribute. Please use ISO 639-1 codes like "en".'
|
||||
sitemap:
|
||||
type: text
|
||||
label: Google sitemap (readonly)
|
||||
label: 'Google sitemap (readonly)'
|
||||
css: lg:w-half
|
||||
disabled: true
|
||||
description: 'Submit the url above in google search console to support indexing.'
|
||||
fieldsetmedia:
|
||||
type: fieldset
|
||||
legend: Media
|
||||
@@ -63,48 +67,48 @@ fieldsetmedia:
|
||||
favicon:
|
||||
type: image
|
||||
label: Favicon
|
||||
description: Only PNG format will work.
|
||||
description: 'Only PNG format will work.'
|
||||
liveimagewidth:
|
||||
type: number
|
||||
label: Standard width for live pictures
|
||||
label: 'Standard width for live pictures'
|
||||
placeholder: 820
|
||||
description: Default width of live images is 820px. Changes will apply to future uploads.
|
||||
description: 'Default width of live images is 820px. Changes will apply to future uploads.'
|
||||
css: lg:w-half
|
||||
liveimageheight:
|
||||
type: number
|
||||
label: Standard height for live pictures
|
||||
description: If you add a value for the height, then the image will be cropped.
|
||||
label: 'Standard height for live pictures'
|
||||
description: 'If you add a value for the height, then the image will be cropped.'
|
||||
css: lg:w-half
|
||||
maximageuploads:
|
||||
type: number
|
||||
label: Maximum size for image uploads in MB
|
||||
description: The maximum image size might be limited by your server settings.
|
||||
label: 'Maximum size for image uploads in MB'
|
||||
description: 'The maximum image size might be limited by your server settings.'
|
||||
allowsvg:
|
||||
type: checkbox
|
||||
label: Allow svg
|
||||
checkboxlabel: Allow the upload of svg images
|
||||
checkboxlabel: 'Allow the upload of svg images'
|
||||
convertwebp:
|
||||
type: checkbox
|
||||
label: Convert to webp
|
||||
checkboxlabel: Try to convert uploaded images into the webp-format
|
||||
label: 'Convert to webp'
|
||||
checkboxlabel: 'Try to convert uploaded images into the webp-format for better performance.'
|
||||
maxfileuploads:
|
||||
type: number
|
||||
label: Maximum size for file uploads in MB
|
||||
description: The maximum file size might be limited by your server settings.
|
||||
label: 'Maximum size for file uploads in MB'
|
||||
description: 'The maximum file size might be limited by your server settings.'
|
||||
fieldsetwriting:
|
||||
type: fieldset
|
||||
legend: Writing
|
||||
fields:
|
||||
editor:
|
||||
type: radio
|
||||
label: Standard editor mode
|
||||
label: 'Standard editor mode'
|
||||
css: lg:w-half
|
||||
options:
|
||||
'visual': 'visual editor'
|
||||
'raw': 'raw editor'
|
||||
formats:
|
||||
type: checkboxlist
|
||||
label: Format options for visual editor
|
||||
label: 'Format options for visual editor'
|
||||
css: lg:w-half
|
||||
options:
|
||||
'markdown': 'markdown'
|
||||
@@ -124,12 +128,12 @@ fieldsetwriting:
|
||||
'shortcode': 'shortcode'
|
||||
headlineanchors:
|
||||
type: checkbox
|
||||
label: Headline anchors
|
||||
checkboxlabel: Show anchors next to headline in frontend
|
||||
label: 'Headline anchors'
|
||||
checkboxlabel: 'Show anchors next to headline in frontend'
|
||||
urlschemes:
|
||||
type: text
|
||||
label: Url schemes
|
||||
description: Add more url schemes for external links e.g. like dict:// (comma separated list)
|
||||
label: 'Url schemes'
|
||||
description: 'Add more url schemes for external links e.g. like dict:// (comma separated list)'
|
||||
maxlength: 60
|
||||
fieldsetaccess:
|
||||
type: fieldset
|
||||
@@ -137,57 +141,83 @@ fieldsetaccess:
|
||||
fields:
|
||||
access:
|
||||
type: checkbox
|
||||
label: Website restriction
|
||||
checkboxlabel: Show the website only to authenticated users and redirect all other users to the login page.
|
||||
label: 'Website restriction'
|
||||
checkboxlabel: 'Show the website only to authenticated users and redirect all other users to the login page.'
|
||||
pageaccess:
|
||||
type: checkbox
|
||||
label: Page restriction
|
||||
checkboxlabel: Activate individual restrictions for pages in the meta-tab of each page.
|
||||
label: 'Page restriction'
|
||||
checkboxlabel: 'Activate individual restrictions for pages in the meta-tab of each page.'
|
||||
hrdelimiter:
|
||||
type: checkbox
|
||||
label: Content break
|
||||
checkboxlabel: Cut restricted content after the first hr-element on a page (per default content will be cut after title).
|
||||
label: 'Content break'
|
||||
checkboxlabel: 'Cut restricted content after the first hr-element on a page (per default content will be cut after title).'
|
||||
restrictionnotice:
|
||||
type: textarea
|
||||
label: Restriction notice (use markdown)
|
||||
label: 'Restriction notice (use markdown)'
|
||||
maxlength: 2000
|
||||
wraprestrictionnotice:
|
||||
type: checkbox
|
||||
label: Wrap restriction notice
|
||||
checkboxlabel: Wrap the restriction notice above into a notice-4 element (which can be designed as special box)
|
||||
label: 'Wrap restriction notice'
|
||||
checkboxlabel: 'Wrap the restriction notice above into a notice-4 element (which can be designed as special box)'
|
||||
fieldsetmail:
|
||||
type: fieldset
|
||||
legend: Email
|
||||
fields:
|
||||
mailfrom:
|
||||
type: text
|
||||
label: 'Mail From (required)'
|
||||
placeholder: sender@yourmail.org
|
||||
maxlength: 60
|
||||
description: 'Enter an email address that should send emails (sender).'
|
||||
mailfromname:
|
||||
type: text
|
||||
label: 'Mail From Name (optional)'
|
||||
placeholder: sender name
|
||||
maxlength: 60
|
||||
description: 'Optionally enter a name for the sender address. If not set, the from-address will be visible.'
|
||||
replyto:
|
||||
type: text
|
||||
label: 'Reply To (optional)'
|
||||
placeholder: noreply@yourmail.org
|
||||
maxlength: 60
|
||||
description: 'Optionally enter a "reply to" address for answers from the receiver. If not set, answers will go to the from-address.'
|
||||
fieldsetrecover:
|
||||
type: fieldset
|
||||
legend: Recover
|
||||
fields:
|
||||
recoverpw:
|
||||
type: checkbox
|
||||
label: 'Recover password'
|
||||
checkboxlabel: 'Activate a password recovery in the login form.'
|
||||
recoversubject:
|
||||
type: text
|
||||
label: 'Email subject'
|
||||
placeholder: 'Recover your password'
|
||||
maxlength: 60
|
||||
recovermessage:
|
||||
type: textarea
|
||||
label: 'Text before recover link in email message'
|
||||
maxlength: 2000
|
||||
fieldsetsecurity:
|
||||
type: fieldset
|
||||
legend: Security
|
||||
fields:
|
||||
securitylog:
|
||||
type: checkbox
|
||||
label: Security log
|
||||
checkboxlabel: Track spam and suspicious actions in a logfile
|
||||
label: 'Security log'
|
||||
checkboxlabel: 'Track spam and suspicious actions in a logfile'
|
||||
authcode:
|
||||
type: checkbox
|
||||
label: 'Authentication code (recommended)'
|
||||
checkboxlabel: 'Send a 5-digit authentication code by email to confirm login.'
|
||||
description: 'The authentication code will be valid for 5 minutes. Be aware that device fingerprints will be stored in the user accounts. Make sure this complies with privacy legislation in your country.'
|
||||
authcaptcha:
|
||||
type: radio
|
||||
label: Use captcha in authentication forms
|
||||
label: 'Use captcha in authentication forms'
|
||||
options:
|
||||
disabled: Disable
|
||||
standard: Always show
|
||||
aftererror: Show after first wrong input
|
||||
recoverpw:
|
||||
type: checkbox
|
||||
label: Recover password
|
||||
checkboxlabel: Activate the password recovery.
|
||||
recoverfrom:
|
||||
type: text
|
||||
label: Sender email
|
||||
placeholder: your@email.org
|
||||
maxlength: 60
|
||||
recoversubject:
|
||||
type: text
|
||||
label: Email subject
|
||||
placeholder: Recover your password
|
||||
maxlength: 60
|
||||
recovermessage:
|
||||
type: textarea
|
||||
label: Text before recover link in email message
|
||||
maxlength: 2000
|
||||
disabled: 'Disable'
|
||||
standard: 'Always show'
|
||||
aftererror: 'Show after first wrong input'
|
||||
fieldsetdeveloper:
|
||||
type: fieldset
|
||||
legend: Developer
|
||||
|
Reference in New Issue
Block a user