Add very based authentication system + users table.

This commit is contained in:
flash 2018-01-16 08:26:29 +01:00
parent 7cf113d962
commit c01f05e2bf
13 changed files with 387 additions and 7 deletions

View file

@ -0,0 +1,52 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Misuzu\Database;
// phpcs:disable
class InitialUsersTable extends Migration
{
/**
* @SuppressWarnings(PHPMD)
*/
public function up()
{
$schema = Database::connection()->getSchemaBuilder();
$schema->create('users', function (Blueprint $table) {
$table->increments('user_id');
$table->string('username', 255)
->unique();
$table->string('password', 255)
->nullable()
->default(null);
$table->string('email', 255)
->unique();
$table->binary('register_ip');
$table->binary('last_ip');
$table->char('user_country', 2)
->default('XX');
$table->integer('user_registered')
->unsigned()
->default(0);
$table->string('user_chat_key', 32)
->nullable()
->default(null);
});
}
/**
* @SuppressWarnings(PHPMD)
*/
public function down()
{
$schema = Database::connection()->getSchemaBuilder();
$schema->drop('users');
}
}

View file

@ -15,9 +15,7 @@ use Illuminate\Filesystem\Filesystem;
require_once __DIR__ . '/misuzu.php';
$resolver = new ConnectionResolver(['database' => Database::connection()]);
$repository = new DatabaseMigrationRepository($resolver, 'migrations');
$repository->setSource('database');
$repository = new DatabaseMigrationRepository(Application::getInstance()->database->getDatabaseManager(), 'migrations');
$migrator = new Migrator($repository, $repository->getConnectionResolver(), new Filesystem);
if (!$migrator->repositoryExists()) {

View file

@ -1,8 +1,15 @@
<?php
use Aitemu\Route;
use Misuzu\Controllers\AuthController;
use Misuzu\Controllers\HomeController;
return [
Route::get('/', 'index', HomeController::class),
Route::get('/is_ready', 'isReady', HomeController::class),
Route::get('/auth/login', 'login', AuthController::class),
Route::post('/auth/login', 'login', AuthController::class),
Route::get('/auth/register', 'register', AuthController::class),
Route::post('/auth/register', 'register', AuthController::class),
Route::get('/auth/logout', 'logout', AuthController::class),
];

View file

@ -36,6 +36,9 @@ class Application extends ApplicationBase
ExceptionHandler::register();
ExceptionHandler::debug($this->debugMode);
$this->addModule('config', new ConfigManager($configFile));
// temporary session system
session_start();
}
public function __destruct()
@ -94,6 +97,7 @@ class Application extends ApplicationBase
$twig->addFilter('json_decode');
$twig->addFilter('byte_symbol');
$twig->addFunction('flashii_is_ready');
$twig->addFunction('byte_symbol');
$twig->addFunction('session_id');
$twig->addFunction('config', [$this->config, 'get']);
@ -101,7 +105,7 @@ class Application extends ApplicationBase
$twig->addFunction('git_hash', [Application::class, 'gitCommitHash']);
$twig->addFunction('git_branch', [Application::class, 'gitBranch']);
$twig->vars(['app' => $this]);
$twig->vars(['app' => $this, 'tsession' => $_SESSION]);
$twig->addPath('nova', __DIR__ . '/../views/nova');
}

View file

@ -0,0 +1,148 @@
<?php
namespace Misuzu\Controllers;
use Aitemu\RouterResponse;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Misuzu\Application;
use Misuzu\Database;
use Misuzu\Net\IP;
use Misuzu\Users\User;
class AuthController extends Controller
{
public function login()
{
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
$app = Application::getInstance();
$twig = $app->templating;
return $twig->render('auth.login');
}
if (!isset($_POST['username'], $_POST['password'])) {
return ['error' => "You didn't fill all the forms!"];
}
$username = $_POST['username'] ?? '';
$password = $_POST['password'] ?? '';
try {
$user = User::where('username', $username)->firstOrFail();
} catch (ModelNotFoundException $e) {
return ['error' => 'Invalid username or password!'];
}
if (!password_verify($password, $user->password)) {
return ['error' => 'Invalid username or password!'];
}
$_SESSION['user_id'] = $user->user_id;
$_SESSION['username'] = $user->username;
$user->user_chat_key = $_SESSION['chat_key'] = bin2hex(random_bytes(16));
$user->save();
setcookie('msz_tmp_id', $_SESSION['user_id'], time() + 604800, '/');
setcookie('msz_tmp_key', $_SESSION['chat_key'], time() + 604800, '/');
return ['error' => 'You are now logged in!', 'next' => '/'];
}
public function register()
{
if (!flashii_is_ready()) {
return "not yet!";
}
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
$app = Application::getInstance();
$twig = $app->templating;
return $twig->render('auth.register');
}
if (!isset($_POST['username'], $_POST['password'], $_POST['email'])) {
return ['error' => "You didn't fill all the forms!"];
}
$username = $_POST['username'] ?? '';
$username_validate = $this->validateUsername($username);
$password = $_POST['password'] ?? '';
$email = $_POST['email'] ?? '';
if ($username_validate !== '') {
return ['error' => $username_validate];
}
try {
$existing = User::where('username', $username)->firstOrFail();
if ($existing->user_id > 0) {
return ['error' => 'This username is already taken!'];
}
} catch (ModelNotFoundException $e) {
}
if (!filter_var($email, FILTER_VALIDATE_EMAIL) || !check_mx_record($email)) {
return ['error' => 'The e-mail address you entered is invalid!'];
}
try {
$existing = User::where('email', $email)->firstOrFail();
if ($existing->user_id > 0) {
return ['error' => 'This e-mail address has already been used!'];
}
} catch (ModelNotFoundException $e) {
}
if (password_entropy($password) < 32) {
return ['error' => 'Your password is considered too weak!'];
}
$user = new User;
$user->username = $username;
$user->password = password_hash($password, PASSWORD_ARGON2I);
$user->email = $email;
$user->register_ip = IP::unpack(IP::remote());
$user->last_ip = IP::unpack(IP::remote());
$user->user_country = get_country_code(IP::remote());
$user->user_registered = time();
$user->save();
return ['error' => 'Welcome to Flashii! You may now log in.', 'next' => '/auth/login'];
}
public function logout()
{
session_destroy();
return 'Logged out.<meta http-equiv="refresh" content="0; url=/">';
}
private function validateUsername(string $username): string
{
$username_length = strlen($username);
if (($username ?? '') !== trim($username)) {
return 'Your username may not start or end with spaces!';
}
if ($username_length < 3) {
return "Your username is too short, it has to be at least 3 characters!";
}
if ($username_length > 16) {
return "Your username is too long, it can't be longer than 16 characters!";
}
if (strpos($username, ' ') !== false || !preg_match('#^[A-Za-z0-9-\[\]_ ]+$#u', $username)) {
return 'Your username contains invalid characters.';
}
if (strpos($username, '_') !== false && strpos($username, ' ') !== false) {
return 'Please use either underscores or spaces, not both!';
}
return '';
}
}

View file

@ -16,6 +16,6 @@ class HomeController extends Controller
public function isReady(): string
{
return 'no';
return flashii_is_ready() ? 'yes' : 'no';
}
}

10
src/Users/User.php Normal file
View file

@ -0,0 +1,10 @@
<?php
namespace Misuzu\Users;
use Misuzu\Model;
class User extends Model
{
protected $primaryKey = 'user_id';
public $timestamps = false;
}

View file

@ -16,6 +16,17 @@ if (!function_exists('ends_with')) {
}
}
function password_entropy(string $password): int
{
return count(count_chars(utf8_decode($password), 1)) * log(256, 2);
}
function check_mx_record(string $email): bool
{
$domain = substr(strstr($email, '@'), 1);
return checkdnsrr($domain, 'MX') || checkdnsrr($domain, 'A');
}
function dechex_pad(int $value, int $padding = 2): string
{
return str_pad(dechex($value), $padding, '0', STR_PAD_LEFT);
@ -51,6 +62,28 @@ function byte_symbol($bytes, $decimal = false)
return sprintf("%.2f %s%sB", $bytes, $symbol, $symbol !== '' && !$decimal ? 'i' : '');
}
function flashii_is_ready()
{
$ipAddr = \Misuzu\Net\IP::remote();
return in_array($ipAddr, ['83.85.244.163', '127.0.0.1', '::1']) || time() > 1517443200;
}
function get_country_code(string $ipAddr, string $fallback = 'XX'): string
{
if (function_exists("geoip_country_code_by_name")) {
try {
$code = geoip_country_code_by_name($ipAddr);
if ($code) {
return $code;
}
} catch (\Exception $e) {
}
}
return $fallback;
}
function is_int_ex($value, int $boundary_low, int $boundary_high): bool
{
return is_int($value) && $value >= $boundary_low && $value <= $boundary_high;

View file

@ -0,0 +1,29 @@
{% extends '@nova/auth/master.twig' %}
{% set banner_classes = 'banner--large landing__banner' %}
{% block banner_content %}
<h1 style="align-self: center; text-align: left; flex-grow: 1; padding-left: 2em">Welcome back!</h1>
{% endblock %}
{% block content %}
<div class="platform form" id="auth-form">
<div>
{% if flashii_is_ready() %}
<a href="/auth/register"><button class="button" type="button">Click here if you don't have an account yet</button></a>
{% else %}
<p style="padding: .2em">You can't create an account yet, but you may log in if you have one already!</p>
{% endif %}
</div>
<div>
<input class="form__text" type="text" name="username" placeholder="Username">
</div>
<div>
<input class="form__text" type="password" name="password" placeholder="Password">
</div>
<div>
<button class="button">Login!</button>
</div>
</div>
{{ parent() }}
{% endblock %}

View file

@ -0,0 +1,58 @@
{% extends '@nova/master.twig' %}
{% block content %}
<script>
var authHttp;
window.addEventListener('load', function () {
var authf = document.getElementById('auth-form');
if (!authf)
return;
authHttp = new XMLHttpRequest();
authHttp.addEventListener('readystatechange', function () {
if (authHttp.readyState === 4 && authHttp.status === 200)
authfHandle(JSON.parse(authHttp.responseText));
});
authf.addEventListener('keydown', function (e) {
if (e.keyCode === 13)
authfSubmit();
});
var buttons = document.getElementsByTagName('button');
buttons[buttons.length - 1].addEventListener('click', function () { authfSubmit(); });
});
function authfHandle(obj)
{
if (obj.error)
alert(obj.error);
if (obj.next)
location.assign(obj.next);
}
function authfSubmit()
{
authHttp.open('POST', location.pathname, true);
authHttp.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
authHttp.send(authfForms());
}
function authfForms()
{
var elems = document.getElementsByTagName('input');
var str = "";
for (var i = 0; i < elems.length; i++) {
var elem = elems[i];
str += encodeURIComponent(elem.name) + "=" + encodeURIComponent(elem.value) + "&";
}
return str.slice(0, -1);
}
</script>
{% endblock %}

View file

@ -0,0 +1,25 @@
{% extends '@nova/auth/master.twig' %}
{% set banner_classes = 'banner--large landing__banner' %}
{% block banner_content %}
<h1 style="align-self: center; text-align: left; flex-grow: 1; padding-left: 2em">Welcome, thanks for dropping by!</h1>
{% endblock %}
{% block content %}
<div class="platform form" id="auth-form">
<div>
<input class="form__text" type="text" name="username" placeholder="Username">
</div>
<div>
<input class="form__text" type="password" name="password" placeholder="Password">
</div>
<div>
<input class="form__text" type="text" name="email" placeholder="E-mail">
</div>
<div>
<button class="button">Create your account!</button>
</div>
</div>
{{ parent() }}
{% endblock %}

View file

@ -180,7 +180,7 @@
checkClient = new XMLHttpRequest();
checkClient.addEventListener('readystatechange', function () {
if (checkClient.readyState === 4 && (checkClient.status !== 200 || checkClient.responseText !== 'no'))
if (checkClient.readyState === 4 && checkClient.responseText !== 'no')
location.reload(true);
});
}
@ -196,9 +196,17 @@
var container = document.createElement('div');
container.className = 'countdown-container';
document.getElementById('countdown').appendChild(container);
countdownSetup(container, new Date("Mon, 1 27 2018 1:00:00 +0100"), function () {
countdownSetup(container, new Date("Mon, 2 01 2018 1:00:00 +0100"), function () {
checkIfFlashiiReady(true);
});
});
</script>
{% endblock %}
{% block content %}
<div class="platform" style="text-align: left;">
<p>So I have to be honest, when the timer runs out you shouldn't expect a fully functional site. A chat, registration, login and a basic session system will most likely be at most in place though. On the position side of things you get to see the site evolve, so yay!!</p>
<p>In any case I hope you all will enjoy what I have in store going forward. I've also decided to push launch back to the 1st or February because I'll need the time.</p>
<p>Lastly I'd like to add that none of the designs are final, most of this I put together ages ago as an eventual redesign for the old site, since then I haven't done much aside from the work in the last month.</p>
</div>
{% endblock %}

View file

@ -13,6 +13,14 @@
<div class="container">
<nav class="header">
<div class="header__inner">
<div class="header__navigation">
<a class="header__entry fa-home" href="/">home</a>
<a class="header__entry fa-comments" href="https://chat.flashii.net">chat</a>
</div>
<a class="header__user" href="/auth/{{ tsession is defined and tsession.username is defined ? 'logout' : 'login' }}">
<div class="header__username">{{ tsession is defined and tsession.username is defined ? tsession.username : 'login' }}</div>
<div class="header__avatar" style="background-image: url('https://static.flash.moe/images/{{ tsession is defined and tsession.username is defined ? 'discord-logo' : 'nova-none' }}.png')"></div>
</a>
</div>
</nav>