Browse Source

implement users registration

main
ghost 11 months ago
parent
commit
c6fef9bb4b
  1. 12
      .env
  2. 1
      README.md
  3. 4
      config/services.yaml
  4. 48
      public/css/default.css
  5. 646
      src/Controller/UserController.php
  6. 17
      templates/default/user/join.html.twig
  7. 15
      templates/default/user/login.html.twig

12
.env

@ -58,12 +58,24 @@ APP_KEVACOIN_ROOM_NAMESPACES_READONLY=
# Redirect from index page to default room # Redirect from index page to default room
APP_KEVACOIN_ROOM_NAMESPACE_DEFAULT=EDIT_ME APP_KEVACOIN_ROOM_NAMESPACE_DEFAULT=EDIT_ME
# Session expire
APP_SESSION_DEFAULT_TIMEOUT=2592000
# Online session expiration timeout # Online session expiration timeout
APP_SESSION_ONLINE_TIMEOUT=900 APP_SESSION_ONLINE_TIMEOUT=900
# Moderators IP with extra permissions, separated with | # Moderators IP with extra permissions, separated with |
APP_MODERATOR_REMOTE_IP= APP_MODERATOR_REMOTE_IP=
# User name regex condition
APP_ADD_USER_NAME_REGEX=/^[0-9A-z-]{2,16}$/ui
# User name reserved (case insensitive) separated with |
APP_ADD_USER_NAME_BLACKLIST=KevaChat|admin|moderator|test
# Delay before have ability to create new username again
APP_ADD_USER_REMOTE_IP_DELAY=86400
# Allow remotes to create new rooms (namespaces) # Allow remotes to create new rooms (namespaces)
APP_ADD_ROOM_REMOTE_IP_REGEX=/.*/ APP_ADD_ROOM_REMOTE_IP_REGEX=/.*/

1
README.md

@ -47,6 +47,7 @@ Application package contain settings preset, just few steps required to launch:
* Copy `rpcuser` to `env`.`APP_KEVACOIN_USERNAME` and `rpcpassword` to `env`.`APP_KEVACOIN_PASSWORD` * Copy `rpcuser` to `env`.`APP_KEVACOIN_USERNAME` and `rpcpassword` to `env`.`APP_KEVACOIN_PASSWORD`
* Generate new address using CLI `kevacoin-cli getnewaddress` and copy to `env`.`APP_KEVACOIN_BOOST_ADDRESS` * Generate new address using CLI `kevacoin-cli getnewaddress` and copy to `env`.`APP_KEVACOIN_BOOST_ADDRESS`
* Send few coins to this address and wait for new block to continue * Send few coins to this address and wait for new block to continue
* To allow users registration, create namespace `kevacoin-cli keva_namespace "_KEVACHAT_USERS_"`
* Create at least one room namespace with Web UI or CLI `kevacoin-cli keva_namespace "sandbox"` * Create at least one room namespace with Web UI or CLI `kevacoin-cli keva_namespace "sandbox"`
* Provide at least one namespace for default chat room to `env`.`APP_KEVACOIN_ROOM_NAMESPACE_DEFAULT` (for homepage redirects) * Provide at least one namespace for default chat room to `env`.`APP_KEVACOIN_ROOM_NAMESPACE_DEFAULT` (for homepage redirects)

4
config/services.yaml

@ -22,7 +22,11 @@ parameters:
app.kevacoin.explorer.url: '%env(APP_KEVACOIN_EXPLORER_URL)%' app.kevacoin.explorer.url: '%env(APP_KEVACOIN_EXPLORER_URL)%'
app.kevacoin.mine.pool.url: '%env(APP_KEVACOIN_MINE_POOL_URL)%' app.kevacoin.mine.pool.url: '%env(APP_KEVACOIN_MINE_POOL_URL)%'
app.kevacoin.mine.solo.url: '%env(APP_KEVACOIN_MINE_SOLO_URL)%' app.kevacoin.mine.solo.url: '%env(APP_KEVACOIN_MINE_SOLO_URL)%'
app.session.default.timeout: '%env(APP_SESSION_DEFAULT_TIMEOUT)%'
app.session.online.timeout: '%env(APP_SESSION_ONLINE_TIMEOUT)%' app.session.online.timeout: '%env(APP_SESSION_ONLINE_TIMEOUT)%'
app.add.user.name.regex: '%env(APP_ADD_USER_NAME_REGEX)%'
app.add.user.name.blacklist: '%env(APP_ADD_USER_NAME_BLACKLIST)%'
app.add.user.remote.ip.delay: '%env(APP_ADD_USER_REMOTE_IP_DELAY)%'
app.add.room.remote.ip.regex: '%env(APP_ADD_ROOM_REMOTE_IP_REGEX)%' app.add.room.remote.ip.regex: '%env(APP_ADD_ROOM_REMOTE_IP_REGEX)%'
app.add.room.remote.ip.delay: '%env(APP_ADD_ROOM_REMOTE_IP_DELAY)%' app.add.room.remote.ip.delay: '%env(APP_ADD_ROOM_REMOTE_IP_DELAY)%'
app.add.room.remote.ip.denied: '%env(APP_ADD_ROOM_REMOTE_IP_DENIED)%' app.add.room.remote.ip.denied: '%env(APP_ADD_ROOM_REMOTE_IP_DENIED)%'

48
public/css/default.css

@ -44,6 +44,8 @@ body
font-size: 12px; font-size: 12px;
} }
/* header */
header header
{ {
background-color: Canvas; background-color: Canvas;
@ -95,6 +97,8 @@ header > form > input[type="text"]
width: 100%; width: 100%;
} }
/* main */
main main
{ {
display: block; display: block;
@ -158,6 +162,50 @@ main ul li span > svg
float: right; float: right;
} }
main > form
{
box-sizing: border-box;
display: block;
margin: 16px 0;
overflow: hidden;
width: 100%;
}
main > form > input[type="text"],
main > form > input[type="password"],
main > form > textarea
{
box-sizing: border-box;
display: block;
margin-bottom: 10px;
padding: 4px;
width: 100%;
}
main > form > label
{
clear: both;
display: inline-block;
margin-bottom: 8px;
}
main > form > button
{
cursor: pointer;
float: right;
padding: 2px 8px;
}
main > form > output
{
color: var(--color-error);
display: block;
font-weight: bolder;
margin-bottom: 16px;
}
/* footer */
footer footer
{ {
background-color: Canvas; background-color: Canvas;

646
src/Controller/UserController.php

@ -0,0 +1,646 @@
<?php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Contracts\Translation\TranslatorInterface;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;
class UserController extends AbstractController
{
private $_algorithm = PASSWORD_BCRYPT;
private $_options =
[
'cost' => 12
];
#[Route(
'/user',
name: 'user_index',
methods:
[
'GET'
]
)]
public function index(
?Request $request
): Response
{
return $this->redirectToRoute(
'user_login'
);
}
#[Route(
'/join',
name: 'user_join',
methods:
[
'GET'
]
)]
public function join(
?Request $request
): Response
{
// Check user session does not exist to continue
if (!empty($request->cookies->get('KEVACHAT_SESSION')))
{
// Redirect to logout
return $this->redirectToRoute(
'user_logout'
);
}
return $this->render(
'default/user/join.html.twig',
[
'request' => $request
]
);
}
#[Route(
'/login',
name: 'user_login',
methods:
[
'GET'
]
)]
public function login(
?Request $request
): Response
{
// Check user session does not exist to continue
if (!empty($request->cookies->get('KEVACHAT_SESSION')))
{
// Redirect to logout
return $this->redirectToRoute(
'user_logout'
);
}
return $this->render(
'default/user/login.html.twig',
[
'request' => $request
]
);
}
#[Route(
'/logout',
name: 'user_logout',
methods:
[
'GET'
]
)]
public function logout(
?Request $request
): Response
{
// Connect memcached
$memcached = new \Memcached();
$memcached->addServer(
$this->getParameter('app.memcached.host'),
$this->getParameter('app.memcached.port')
);
// Make sure cookies exist
if (!empty($request->cookies->get('KEVACHAT_SESSION')) && preg_match('/[A-z0-9]{32}/', $request->cookies->get('KEVACHAT_SESSION')))
{
// Delete from memory
$memcached->delete($session);
// Delete cookies
setcookie('KEVACHAT_SESSION', '', -1);
}
// Redirect to main page
return $this->redirectToRoute(
'user_login'
);
}
#[Route(
'/join',
name: 'user_add',
methods:
[
'POST'
]
)]
public function add(
Request $request,
TranslatorInterface $translator
): Response
{
// Check maintenance mode disabled
if ($this->getParameter('app.maintenance'))
{
return $this->redirectToRoute(
'user_add',
[
'username' => $request->get('username'),
'error' => $translator->trans('Maintenance, please try again later!')
]
);
}
// Check user session does not exist to continue
if (!empty($request->cookies->get('KEVACHAT_SESSION')))
{
// Redirect to logout
return $this->redirectToRoute(
'user_logout'
);
}
// Trim extra spaces from username
$username = trim(
$request->get('username')
);
// Connect memcached
$memcached = new \Memcached();
$memcached->addServer(
$this->getParameter('app.memcached.host'),
$this->getParameter('app.memcached.port')
);
// Create IP delay record
$memory = md5(
sprintf(
'%s.UserController::add:add.user.remote.ip.delay:%s',
__DIR__,
$request->getClientIp(),
),
);
// Validate remote IP limits
if ($delay = (int) $memcached->get($memory))
{
return $this->redirectToRoute(
'user_add',
[
'username' => $request->get('username'),
'error' => sprintf(
$translator->trans('Please wait %s seconds before register new username!'),
(int) $this->getParameter('app.add.user.remote.ip.delay') - (time() - $delay)
)
]
);
}
// Check client connection
if (!$client = $this->_client())
{
return $this->redirectToRoute(
'user_add',
[
'username' => $request->get('username'),
'error' => $translator->trans('Could not connect wallet database!')
]
);
}
// Check users database accessible
if (!$namespace = $this->_namespace($client))
{
return $this->redirectToRoute(
'user_add',
[
'username' => $request->get('username'),
'error' => $translator->trans('Could not access user database, try again later!')
]
);
}
// Validate kevacoin key requirements
if (mb_strlen($username) < 1 || mb_strlen($username) > 520)
{
return $this->redirectToRoute(
'user_add',
[
'username' => $request->get('username'),
'error' => $translator->trans('Username length out of KevaCoin protocol limits!')
]
);
}
// Validate meta NS
if (str_starts_with($username, '_'))
{
return $this->redirectToRoute(
'user_add',
[
'username' => $request->get('username'),
'error' => $translator->trans('Username contain meta format!')
]
);
}
// Validate username regex
if (!preg_match($this->getParameter('app.add.user.name.regex'), $username))
{
return $this->redirectToRoute(
'user_add',
[
'username' => $request->get('username'),
'error' => sprintf(
$translator->trans('Username does not match node requirements: %s!'),
$this->getParameter('app.add.user.name.regex')
)
]
);
}
// Validate username blacklist (reserved)
if (in_array($username, (array) explode('|', $this->getParameter('app.add.user.name.blacklist'))))
{
return $this->redirectToRoute(
'user_add',
[
'username' => $request->get('username'),
'error' => $translator->trans('Username reserved by node!')
]
);
}
// Validate username exist
if ($this->_hash($client, $namespace, $username))
{
return $this->redirectToRoute(
'user_add',
[
'username' => $request->get('username'),
'error' => $translator->trans('Username already taken!')
]
);
}
// Validate password length
if (mb_strlen($request->get('password')) <= 12)
{
return $this->redirectToRoute(
'user_add',
[
'username' => $request->get('username'),
'error' => $translator->trans('Please, provide stronger password!')
]
);
}
// Validate passwords match
if ($request->get('password') !== $request->get('repeat'))
{
return $this->redirectToRoute(
'user_add',
[
'username' => $request->get('username'),
'error' => $translator->trans('Password repeat incorrect!')
]
);
}
// Generate password hash
if (!$hash = password_hash($request->get('password'), $this->_algorithm, $this->_options))
{
return $this->redirectToRoute(
'user_add',
[
'username' => $request->get('username'),
'error' => $translator->trans('Could not generate password hash!')
]
);
}
// Auth success, add user to DB
if (!$this->_add($client, $namespace, $username, $hash))
{
return $this->redirectToRoute(
'user_add',
[
'username' => $request->get('username'),
'error' => $translator->trans('Could not create user in blockchain!')
]
);
}
// Register event time
$memcached->set(
$memory,
time(),
(int) $this->getParameter('app.add.user.remote.ip.delay') // auto remove on cache expire
);
// Auth success, create user session
$session = md5(
sprintf(
'%s.%s.%s',
$request->get('username'),
$request->getClientIp(),
rand()
)
);
// Save session to memory
if (!$memcached->set($session, $request->get('username'), (int) time() + $this->getParameter('app.session.default.timeout')))
{
return $this->redirectToRoute(
'user_login',
[
'username' => $request->get('username'),
'error' => $translator->trans('Could not save user session!')
]
);
}
// Save session to user cookies
if (!setcookie('KEVACHAT_SESSION', $session, time() + $this->getParameter('app.session.default.timeout')))
{
return $this->redirectToRoute(
'user_login',
[
'username' => $request->get('username'),
'error' => $translator->trans('Could not create session cookie!')
]
);
}
// Redirect to main page
return $this->redirectToRoute(
'room_index'
);
// Redirect to login page
return $this->redirectToRoute(
'user_login',
[
'username' => $request->get('username')
]
);
}
#[Route(
'/login',
name: 'user_auth',
methods:
[
'POST'
]
)]
public function auth(
Request $request,
TranslatorInterface $translator
): Response
{
// Check maintenance mode disabled
if ($this->getParameter('app.maintenance'))
{
return $this->redirectToRoute(
'user_login',
[
'username' => $request->get('username'),
'error' => $translator->trans('Maintenance, please try again later!')
]
);
}
// Check user session does not exist to continue
if (!empty($request->cookies->get('KEVACHAT_SESSION')))
{
// Redirect to logout
return $this->redirectToRoute(
'user_logout'
);
}
// Connect memcached
$memcached = new \Memcached();
$memcached->addServer(
$this->getParameter('app.memcached.host'),
$this->getParameter('app.memcached.port')
);
// Check client connection
if (!$client = $this->_client())
{
return $this->redirectToRoute(
'user_login',
[
'username' => $request->get('username'),
'error' => $translator->trans('Could not connect wallet database!')
]
);
}
// Check username namespace accessible
if (!$namespace = $this->_namespace($client))
{
return $this->redirectToRoute(
'user_login',
[
'username' => $request->get('username'),
'error' => $translator->trans('Could not access user database, try again later!')
]
);
}
// Trim extra spaces from username
$username = trim(
$request->get('username')
);
// Validate kevacoin key requirements
if (mb_strlen($username) < 1 || mb_strlen($username) > 520)
{
return $this->redirectToRoute(
'user_login',
[
'username' => $request->get('username'),
'error' => $translator->trans('Username length out of KevaCoin protocol limits!')
]
);
}
// Validate meta NS
if (str_starts_with($username, '_'))
{
return $this->redirectToRoute(
'user_login',
[
'username' => $request->get('username'),
'error' => $translator->trans('Username contain meta format!')
]
);
}
// Validate username regex
if (!preg_match($this->getParameter('app.add.user.name.regex'), $username))
{
return $this->redirectToRoute(
'user_login',
[
'username' => $request->get('username'),
'error' => sprintf(
$translator->trans('Username does not match node requirements: %s!'),
$this->getParameter('app.add.user.name.regex')
)
]
);
}
// Validate username blacklist
if (in_array($username, (array) explode('|', $this->getParameter('app.add.user.name.blacklist'))))
{
return $this->redirectToRoute(
'user_login',
[
'username' => $request->get('username'),
'error' => $translator->trans('Username reserved by node!')
]
);
}
// Validate username exist
if (!$hash = $this->_hash($client, $namespace, $username))
{
return $this->redirectToRoute(
'user_login',
[
'username' => $request->get('username'),
'error' => $translator->trans('Username not found!')
]
);
}
// Validate password
if (!password_verify($request->get('password'), $hash))
{
return $this->redirectToRoute(
'user_login',
[
'username' => $request->get('username'),
'error' => $translator->trans('Password invalid!')
]
);
}
// Validate password algo
if (password_needs_rehash($hash, $this->_algorithm, $this->_options))
{
return $this->redirectToRoute(
'user_login',
[
'username' => $request->get('username'),
'error' => $translator->trans('Password needs rehash!')
]
);
}
// Auth success, create user session
$session = md5(
sprintf(
'%s.%s.%s',
$request->get('username'),
$request->getClientIp(),
rand()
)
);
// Save session to memory
if (!$memcached->set($session, $request->get('username'), (int) time() + $this->getParameter('app.session.default.timeout')))
{
return $this->redirectToRoute(
'user_login',
[
'username' => $request->get('username'),
'error' => $translator->trans('Could not save user session!')
]
);
}
// Save session to user cookies
if (!setcookie('KEVACHAT_SESSION', $session, time() + $this->getParameter('app.session.default.timeout')))
{
return $this->redirectToRoute(
'user_login',
[
'username' => $request->get('username'),
'error' => $translator->trans('Could not create session cookie!')
]
);
}
// Redirect to main page
return $this->redirectToRoute(
'room_index'
);
}
private function _client(): \Kevachat\Kevacoin\Client
{
$client = new \Kevachat\Kevacoin\Client(
$this->getParameter('app.kevacoin.protocol'),
$this->getParameter('app.kevacoin.host'),
$this->getParameter('app.kevacoin.port'),
$this->getParameter('app.kevacoin.username'),
$this->getParameter('app.kevacoin.password')
);
return $client;
}
private function _namespace(
\Kevachat\Kevacoin\Client $client
): ?string
{
foreach ((array) $client->kevaListNamespaces() as $value)
{
if ($value['displayName'] === '_KEVACHAT_USERS_')
{
return $value['namespaceId'];
}
}
return null;
}
private function _hash(
\Kevachat\Kevacoin\Client $client,
string $namespace,
string $username
): ?string
{
if ($user = $client->kevaGet($namespace, $username))
{
if (!empty($user['value']))
{
return (string) $user['value'];
}
}
return null;
}
private function _add(
\Kevachat\Kevacoin\Client $client,
string $namespace,
string $username,
string $hash
): ?string
{
return $client->kevaPut(
$namespace,
$username,
$hash
);
}
}

17
templates/default/user/join.html.twig

@ -0,0 +1,17 @@
{% extends 'default/layout.html.twig' %}
{% block head_title_content %}{{ 'Join' | trans }} - {{ 'KevaChat' | trans }}{% endblock %}
{% block main_content %}
<form name="join" action="{{ path('user_add') }}" method="post">
{% if request.get('error') %}
<output name="error">{{ request.get('error') }}</output>
{% endif %}
<label for="username">{{ 'Username' | trans }}</label>
<input type="text" name="username" id="username" value="{{ request.get('username') }}" placeholder="{{ 'Your public identity for this instance, permanently stored in blockchain' | trans }}" />
<label for="password">{{ 'Password' | trans }}</label>
<input type="password" name="password" id="password" value="" placeholder="{{ 'Bcrypt hash of this password permanently stored in blockchain' | trans }}" />
<label for="repeat">{{ 'Repeat password' | trans }}</label>
<input type="password" name="repeat" id="repeat" value="" placeholder="{{ 'Make sure your password is correct, you can not reset it later!' | trans }}" />
<a href="{{ path('user_login') }}">{{ 'I have account' | trans }}</a>
<button type="submit">{{ 'join' | trans }}</button>
</form>
{% endblock %}

15
templates/default/user/login.html.twig

@ -0,0 +1,15 @@
{% extends 'default/layout.html.twig' %}
{% block head_title_content %}{{ 'Login' | trans }} - {{ 'KevaChat' | trans }}{% endblock %}
{% block main_content %}
<form name="login" action="{{ path('user_auth') }}" method="post">
{% if request.get('error') %}
<output name="error">{{ request.get('error') }}</output>
{% endif %}
<label for="username">{{ 'Username' | trans }}</label>
<input type="text" name="username" id="username" value="{{ request.get('username') }}" />
<label for="password">{{ 'Password' | trans }}</label>
<input type="password" name="password" id="password" value="" />
<a href="{{ path('user_join') }}">{{ 'Create account' | trans }}</a>
<button type="submit">{{ 'login' | trans }}</button>
</form>
{% endblock %}
Loading…
Cancel
Save