Add very based authentication system + users table.
This commit is contained in:
parent
7cf113d962
commit
c01f05e2bf
13 changed files with 387 additions and 7 deletions
52
database/2018_01_16_063812_initial_users_table.php
Normal file
52
database/2018_01_16_063812_initial_users_table.php
Normal 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');
|
||||
}
|
||||
}
|
|
@ -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()) {
|
||||
|
|
|
@ -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),
|
||||
];
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
|
|
148
src/Controllers/AuthController.php
Normal file
148
src/Controllers/AuthController.php
Normal 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 '';
|
||||
}
|
||||
}
|
|
@ -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
10
src/Users/User.php
Normal file
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
namespace Misuzu\Users;
|
||||
|
||||
use Misuzu\Model;
|
||||
|
||||
class User extends Model
|
||||
{
|
||||
protected $primaryKey = 'user_id';
|
||||
public $timestamps = false;
|
||||
}
|
33
utility.php
33
utility.php
|
@ -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;
|
||||
|
|
29
views/nova/auth/login.twig
Normal file
29
views/nova/auth/login.twig
Normal 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 %}
|
58
views/nova/auth/master.twig
Normal file
58
views/nova/auth/master.twig
Normal 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 %}
|
25
views/nova/auth/register.twig
Normal file
25
views/nova/auth/register.twig
Normal 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 %}
|
|
@ -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 %}
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue