From fca5b423a44145fc15423eb4d3dbdb8500716908 Mon Sep 17 00:00:00 2001 From: flashwave Date: Sun, 27 May 2018 02:20:35 +0200 Subject: [PATCH] A bunch of housekeeping. --- assets/less/manage/classes/avatar.less | 11 + assets/less/manage/classes/user-listing.less | 14 +- assets/less/manage/main.less | 1 + misuzu.php | 4 + public/auth.php | 185 ++++-------- public/index.php | 4 +- public/manage/users.php | 67 +++-- public/news.php | 33 ++- public/robots.txt | 2 +- public/settings.php | 287 ++++++++----------- src/Users/login_attempt.php | 16 ++ src/Users/profile.php | 141 +++++++++ src/Users/role.php | 17 ++ src/Users/session.php | 47 +++ src/Users/user.php | 55 ++++ views/manage/master.twig | 2 + views/manage/users/listing.twig | 8 +- views/mio/home/landing.twig | 2 +- views/mio/settings/account.twig | 2 +- 19 files changed, 555 insertions(+), 343 deletions(-) create mode 100644 assets/less/manage/classes/avatar.less create mode 100644 src/Users/profile.php create mode 100644 src/Users/role.php create mode 100644 src/Users/session.php create mode 100644 src/Users/user.php diff --git a/assets/less/manage/classes/avatar.less b/assets/less/manage/classes/avatar.less new file mode 100644 index 00000000..e39d6be8 --- /dev/null +++ b/assets/less/manage/classes/avatar.less @@ -0,0 +1,11 @@ +.avatar { + flex-shrink: 0; + background-color: #333; + background-size: cover; + background-repeat: no-repeat; + background-position: center; + display: block; + border: 1px solid #444; + max-height: 200px; + max-width: 200px; +} diff --git a/assets/less/manage/classes/user-listing.less b/assets/less/manage/classes/user-listing.less index 6591e1a6..effb74c2 100644 --- a/assets/less/manage/classes/user-listing.less +++ b/assets/less/manage/classes/user-listing.less @@ -9,7 +9,19 @@ &__content { display: flex; - align-items: flex-start; } } + + &__info { + display: flex; + flex-grow: 1; + flex-shrink: 1; + } + + &__avatar { + width: 46px; + height: 46px; + flex-grow: 0; + flex-shrink: 0; + } } diff --git a/assets/less/manage/main.less b/assets/less/manage/main.less index 81c91cd1..645d9923 100644 --- a/assets/less/manage/main.less +++ b/assets/less/manage/main.less @@ -19,6 +19,7 @@ body { color: #fff; } +@import "classes/avatar"; @import "classes/button"; @import "classes/container"; @import "classes/footer"; diff --git a/misuzu.php b/misuzu.php index 97d375ad..cc094b7a 100644 --- a/misuzu.php +++ b/misuzu.php @@ -9,6 +9,10 @@ require_once __DIR__ . '/src/Forum/post.php'; require_once __DIR__ . '/src/Forum/topic.php'; require_once __DIR__ . '/src/Forum/validate.php'; require_once __DIR__ . '/src/Users/login_attempt.php'; +require_once __DIR__ . '/src/Users/profile.php'; +require_once __DIR__ . '/src/Users/role.php'; +require_once __DIR__ . '/src/Users/session.php'; +require_once __DIR__ . '/src/Users/user.php'; require_once __DIR__ . '/src/Users/validation.php'; $app = new Application( diff --git a/public/auth.php b/public/auth.php index 464e4a4f..4e75706d 100644 --- a/public/auth.php +++ b/public/auth.php @@ -10,31 +10,28 @@ $db = Database::connection(); $config = $app->getConfig(); $templating = $app->getTemplating(); -$username_validation_errors = [ +$usernameValidationErrors = [ 'trim' => 'Your username may not start or end with spaces!', - 'short' => "Your username is too short, it has to be at least " . MSZ_USERNAME_MIN_LENGTH . " characters!", - 'long' => "Your username is too long, it can't be longer than " . MSZ_USERNAME_MAX_LENGTH . " characters!", + 'short' => sprintf('Your username is too short, it has to be at least %d characters!', MSZ_USERNAME_MIN_LENGTH), + 'long' => sprintf("Your username is too long, it can't be longer than %d characters!", MSZ_USERNAME_MAX_LENGTH), 'double-spaces' => "Your username can't contain double spaces.", 'invalid' => 'Your username contains invalid characters.', 'spacing' => 'Please use either underscores or spaces, not both!', 'in-use' => 'This username is already taken!', ]; -$mode = $_GET['m'] ?? 'login'; -$prevent_registration = $config->get('Auth', 'prevent_registration', 'bool', false); -$templating->var('auth_mode', $mode); +$authMode = $_GET['m'] ?? 'login'; +$preventRegistration = $config->get('Auth', 'prevent_registration', 'bool', false); $templating->addPath('auth', __DIR__ . '/../views/auth'); -$templating->var('prevent_registration', $prevent_registration); -if (!empty($_REQUEST['username'])) { - $templating->var('auth_username', $_REQUEST['username']); -} +$templating->vars([ + 'prevent_registration' => $preventRegistration, + 'auth_mode' => $authMode, + 'auth_username' => $_REQUEST['username'] ?? '', + 'auth_email' => $_REQUEST['email'] ?? '', +]); -if (!empty($_REQUEST['email'])) { - $templating->var('auth_email', $_REQUEST['email']); -} - -switch ($mode) { +switch ($authMode) { case 'logout': if (!$app->hasActiveSession()) { header('Location: /'); @@ -44,12 +41,7 @@ switch ($mode) { if (isset($_GET['s']) && tmp_csrf_verify($_GET['s'])) { set_cookie_m('uid', '', -3600); set_cookie_m('sid', '', -3600); - $deleteSession = $db->prepare(' - DELETE FROM `msz_sessions` - WHERE `session_id` = :session_id - '); - $deleteSession->bindValue('session_id', $app->getSessionId()); - $deleteSession->execute(); + user_session_delete($app->getSessionId()); header('Location: /'); return; } @@ -63,35 +55,24 @@ switch ($mode) { break; } - $user_agent = $_SERVER['HTTP_USER_AGENT'] ?? ''; - $auth_login_error = ''; + $userAgent = $_SERVER['HTTP_USER_AGENT'] ?? ''; + $authLoginError = ''; while ($_SERVER['REQUEST_METHOD'] === 'POST') { $ipAddress = IPAddress::remote()->getString(); if (!isset($_POST['username'], $_POST['password'])) { - $auth_login_error = "You didn't fill all the forms!"; + $authLoginError = "You didn't fill all the forms!"; break; } - $fetchRemainingAttempts = $db->prepare(' - SELECT 5 - COUNT(`attempt_id`) - FROM `msz_login_attempts` - WHERE `was_successful` = false - AND `created_at` > NOW() - INTERVAL 1 HOUR - AND `attempt_ip` = INET6_ATON(:remote_ip) - '); - $fetchRemainingAttempts->bindValue('remote_ip', $ipAddress); - $remainingAttempts = $fetchRemainingAttempts->execute() - ? (int)$fetchRemainingAttempts->fetchColumn() - : 0; + $remainingAttempts = user_login_attempts_remaining($ipAddress); if ($remainingAttempts < 1) { - $auth_login_error = 'Too many failed login attempts, try again later.'; + $authLoginError = 'Too many failed login attempts, try again later.'; break; } - $remainingAttempts -= 1; $username = $_POST['username'] ?? ''; $password = $_POST['password'] ?? ''; @@ -106,44 +87,29 @@ switch ($mode) { $userData = $getUser->execute() ? $getUser->fetch() : []; $userId = (int)($userData['user_id'] ?? 0); - $auth_error_str = "Invalid username or password, {$remainingAttempts} attempt(s) remaining."; + $loginFailedError = sprintf( + "Invalid username or password, %d attempt%s remaining.", + $remainingAttempts - 1, + $remainingAttempts === 2 ? '' : 's' + ); if ($userId < 1) { - user_login_attempt_record(false, null, $ipAddress, $user_agent); - $auth_login_error = $auth_error_str; + user_login_attempt_record(false, null, $ipAddress, $userAgent); + $authLoginError = $loginFailedError; break; } if (!password_verify($password, $userData['password'])) { - user_login_attempt_record(false, $userId, $ipAddress, $user_agent); - $auth_login_error = $auth_error_str; + user_login_attempt_record(false, $userId, $ipAddress, $userAgent); + $authLoginError = $loginFailedError; break; } - user_login_attempt_record(true, $userId, $ipAddress, $user_agent); + user_login_attempt_record(true, $userId, $ipAddress, $userAgent); + $sessionKey = user_session_create($userId, $ipAddress, $userAgent); - $sessionKey = bin2hex(random_bytes(32)); - - $createSession = $db->prepare(' - INSERT INTO `msz_sessions` - ( - `user_id`, `session_ip`, `session_country`, - `user_agent`, `session_key`, `created_at`, `expires_on` - ) - VALUES - ( - :user_id, INET6_ATON(:session_ip), :session_country, - :user_agent, :session_key, NOW(), NOW() + INTERVAL 1 MONTH - ) - '); - $createSession->bindValue('user_id', $userId); - $createSession->bindValue('session_ip', $ipAddress); - $createSession->bindValue('session_country', get_country_code($ipAddress)); - $createSession->bindValue('user_agent', $user_agent); - $createSession->bindValue('session_key', $sessionKey); - - if (!$createSession->execute()) { - $auth_login_error = 'Unable to create new session, contact an administrator.'; + if ($sessionKey === '') { + $authLoginError = 'Unable to create new session, contact an administrator ASAP.'; break; } @@ -152,30 +118,21 @@ switch ($mode) { set_cookie_m('uid', $userId, $cookieLife); set_cookie_m('sid', $sessionKey, $cookieLife); - // Temporary key generation for chat login. - // Should eventually be replaced with a callback login system. - // Also uses different cookies since $httponly is required to be false for these. - $chatKey = bin2hex(random_bytes(16)); + if (strpos($_SERVER['HTTP_HOST'], 'flashii.net') !== false) { + $chatKey = user_generate_chat_key($userId); - $setChatKey = $db->prepare(' - UPDATE `msz_users` - SET `user_chat_key` = :user_chat_key - WHERE `user_id` = :user_id - '); - $setChatKey->bindValue('user_chat_key', $chatKey); - $setChatKey->bindValue('user_id', $userId); - - if ($setChatKey->execute()) { - setcookie('msz_tmp_id', $userId, $cookieLife, '/', '.flashii.net'); - setcookie('msz_tmp_key', $chatKey, $cookieLife, '/', '.flashii.net'); + if ($chatKey !== '') { + setcookie('msz_tmp_id', $userId, $cookieLife, '/', '.flashii.net'); + setcookie('msz_tmp_key', $chatKey, $cookieLife, '/', '.flashii.net'); + } } header('Location: /'); return; } - if (!empty($auth_login_error)) { - $templating->var('auth_login_error', $auth_login_error); + if (!empty($authLoginError)) { + $templating->var('auth_login_error', $authLoginError); } echo $templating->render('auth'); @@ -186,16 +143,16 @@ switch ($mode) { header('Location: /'); } - $auth_register_error = ''; + $authRegistrationError = ''; while ($_SERVER['REQUEST_METHOD'] === 'POST') { - if ($prevent_registration) { - $auth_register_error = 'Registration is not allowed on this instance.'; + if ($preventRegistration) { + $authRegistrationError = 'Registration is not allowed on this instance.'; break; } if (!isset($_POST['username'], $_POST['password'], $_POST['email'])) { - $auth_register_error = "You didn't fill all the forms!"; + $authRegistrationError = "You didn't fill all the forms!"; break; } @@ -203,65 +160,45 @@ switch ($mode) { $password = $_POST['password'] ?? ''; $email = $_POST['email'] ?? ''; - $username_validate = user_validate_username($username, true); - if ($username_validate !== '') { - $auth_register_error = $username_validation_errors[$username_validate]; + $usernameValidation = user_validate_username($username, true); + if ($usernameValidation !== '') { + $authRegistrationError = $usernameValidationErrors[$usernameValidation]; break; } - $email_validate = user_validate_email($email, true); - if ($email_validate !== '') { - $auth_register_error = $email_validate === 'in-use' + $emailValidation = user_validate_email($email, true); + if ($emailValidation !== '') { + $authRegistrationError = $emailValidation === 'in-use' ? 'This e-mail address has already been used!' : 'The e-mail address you entered is invalid!'; break; } if (user_validate_password($password) !== '') { - $auth_register_error = 'Your password is too weak!'; + $authRegistrationError = 'Your password is too weak!'; break; } - $ipAddress = IPAddress::remote()->getString(); - $createUser = $db->prepare(' - INSERT INTO `msz_users` - ( - `username`, `password`, `email`, `register_ip`, - `last_ip`, `user_country`, `created_at`, `display_role` - ) - VALUES - ( - :username, :password, :email, INET6_ATON(:register_ip), - INET6_ATON(:last_ip), :user_country, NOW(), 1 - ) - '); - $createUser->bindValue('username', $username); - $createUser->bindValue('password', password_hash($password, PASSWORD_ARGON2I)); - $createUser->bindValue('email', $email); - $createUser->bindValue('register_ip', $ipAddress); - $createUser->bindValue('last_ip', $ipAddress); - $createUser->bindValue('user_country', get_country_code($ipAddress)); + $createUser = user_create( + $username, + $password, + $email, + IPAddress::remote()->getString() + ); - if (!$createUser->execute()) { - $auth_register_error = 'Something happened?'; + if ($createUser < 1) { + $authRegistrationError = 'Something happened?'; break; } - $addRole = $db->prepare(' - INSERT INTO `msz_user_roles` - (`user_id`, `role_id`) - VALUES - (:user_id, 1) - '); - $addRole->bindValue('user_id', $db->lastInsertId()); - $addRole->execute(); + user_role_add($createUser, MSZ_ROLE_MAIN); $templating->var('auth_register_message', 'Welcome to Flashii! You may now log in.'); break; } - if (!empty($auth_register_error)) { - $templating->var('auth_register_error', $auth_register_error); + if (!empty($authRegistrationError)) { + $templating->var('auth_register_error', $authRegistrationError); } echo $templating->render('@auth.auth'); diff --git a/public/index.php b/public/index.php index c1921365..f1beeb94 100644 --- a/public/index.php +++ b/public/index.php @@ -21,4 +21,6 @@ $featuredNews = Database::connection() //var_dump(Database::connection()->query('SHOW SESSION STATUS LIKE "Questions"')->fetch()['Value']); -echo $app->getTemplating()->render('home.landing', compact('featuredNews')); +echo $app->getTemplating()->render('home.landing', [ + 'featured_news' => $featuredNews, +]); diff --git a/public/manage/users.php b/public/manage/users.php index 49319887..bd913cc2 100644 --- a/public/manage/users.php +++ b/public/manage/users.php @@ -6,9 +6,8 @@ require_once __DIR__ . '/../../misuzu.php'; $db = Database::connection(); $templating = $app->getTemplating(); -$is_post_request = $_SERVER['REQUEST_METHOD'] === 'POST'; +$isPostRequest = $_SERVER['REQUEST_METHOD'] === 'POST'; $queryQffset = (int)($_GET['o'] ?? 0); -$page_id = (int)($_GET['p'] ?? 1); switch ($_GET['v'] ?? null) { case 'listing': @@ -42,9 +41,9 @@ switch ($_GET['v'] ?? null) { break; case 'view': - $user_id = $_GET['u'] ?? null; + $userId = $_GET['u'] ?? null; - if ($user_id === null || ($user_id = (int)$user_id) < 1) { + if ($userId === null || ($userId = (int)$userId) < 1) { echo 'no'; break; } @@ -60,7 +59,7 @@ switch ($_GET['v'] ?? null) { ON u.`display_role` = r.`role_id` WHERE `user_id` = :user_id '); - $getUser->bindValue('user_id', $user_id); + $getUser->bindValue('user_id', $userId); $getUser->execute(); $manageUser = $getUser->execute() ? $getUser->fetch() : []; @@ -106,9 +105,9 @@ switch ($_GET['v'] ?? null) { break; case 'role': - $role_id = $_GET['r'] ?? null; + $roleId = $_GET['r'] ?? null; - if ($is_post_request) { + if ($isPostRequest) { if (!tmp_csrf_verify($_POST['csrf'] ?? '')) { echo 'csrf err'; break; @@ -119,27 +118,27 @@ switch ($_GET['v'] ?? null) { break; } - $role_name = $_POST['role']['name'] ?? ''; - $role_name_length = strlen($role_name); + $roleName = $_POST['role']['name'] ?? ''; + $roleNameLength = strlen($roleName); - if ($role_name_length < 1 || $role_name_length > 255) { + if ($roleNameLength < 1 || $roleNameLength > 255) { echo 'invalid name length'; break; } - $role_secret = !empty($_POST['role']['secret']); + $roleSecret = !empty($_POST['role']['secret']); - $role_hierarchy = (int)($_POST['role']['hierarchy'] ?? -1); + $roleHierarchy = (int)($_POST['role']['hierarchy'] ?? -1); - if ($role_hierarchy < 1 || $role_hierarchy > 100) { + if ($roleHierarchy < 1 || $roleHierarchy > 100) { echo 'Invalid hierarchy value.'; break; } - $role_colour = colour_create(); + $roleColour = colour_create(); if (!empty($_POST['role']['colour']['inherit'])) { - colour_set_inherit($role_colour); + colour_set_inherit($roleColour); } else { foreach (['red', 'green', 'blue'] as $key) { $value = (int)($_POST['role']['colour'][$key] ?? -1); @@ -150,18 +149,18 @@ switch ($_GET['v'] ?? null) { break 2; } - $func($role_colour, $value); + $func($roleColour, $value); } } - $role_description = $_POST['role']['description'] ?? ''; + $roleDescription = $_POST['role']['description'] ?? ''; - if (strlen($role_description) > 1000) { + if (strlen($roleDescription) > 1000) { echo 'description is too long'; break; } - if ($role_id < 1) { + if ($roleId < 1) { $updateRole = $db->prepare(' INSERT INTO `msz_roles` (`role_name`, `role_hierarchy`, `role_secret`, `role_colour`, `role_description`, `created_at`) @@ -178,26 +177,26 @@ switch ($_GET['v'] ?? null) { `role_description` = :role_description WHERE `role_id` = :role_id '); - $updateRole->bindValue('role_id', $role_id); + $updateRole->bindValue('role_id', $roleId); } - $updateRole->bindValue('role_name', $role_name); - $updateRole->bindValue('role_hierarchy', $role_hierarchy); - $updateRole->bindValue('role_secret', $role_secret ? 1 : 0); - $updateRole->bindValue('role_colour', $role_colour); - $updateRole->bindValue('role_description', $role_description); + $updateRole->bindValue('role_name', $roleName); + $updateRole->bindValue('role_hierarchy', $roleHierarchy); + $updateRole->bindValue('role_secret', $roleSecret ? 1 : 0); + $updateRole->bindValue('role_colour', $roleColour); + $updateRole->bindValue('role_description', $roleDescription); $updateRole->execute(); - if ($role_id < 1) { - $role_id = (int)$db->lastInsertId(); + if ($roleId < 1) { + $roleId = (int)$db->lastInsertId(); } - header("Location: ?v=role&r={$role_id}"); + header("Location: ?v=role&r={$roleId}"); break; } - if ($role_id !== null) { - if ($role_id < 1) { + if ($roleId !== null) { + if ($roleId < 1) { echo 'no'; break; } @@ -207,15 +206,15 @@ switch ($_GET['v'] ?? null) { FROM `msz_roles` WHERE `role_id` = :role_id '); - $getEditRole->bindValue('role_id', $role_id); - $edit_role = $getEditRole->execute() ? $getEditRole->fetch() : []; + $getEditRole->bindValue('role_id', $roleId); + $editRole = $getEditRole->execute() ? $getEditRole->fetch() : []; - if (!$edit_role) { + if (!$editRole) { echo 'invalid role'; break; } - $templating->vars(compact('edit_role')); + $templating->vars(['edit_role' => $editRole]); } echo $templating->render('@manage.users.roles_create'); diff --git a/public/news.php b/public/news.php index da7dad1f..c0f4e0fc 100644 --- a/public/news.php +++ b/public/news.php @@ -6,14 +6,17 @@ require_once __DIR__ . '/../misuzu.php'; $db = Database::connection(); $templating = $app->getTemplating(); -$category_id = isset($_GET['c']) ? (int)$_GET['c'] : null; -$post_id = isset($_GET['p']) ? (int)$_GET['p'] : (isset($_GET['n']) ? (int)$_GET['n'] : null); -$posts_offset = (int)($_GET['o'] ?? 0); -$posts_take = 5; +$categoryId = isset($_GET['c']) ? (int)$_GET['c'] : null; +$postId = isset($_GET['p']) ? (int)$_GET['p'] : (isset($_GET['n']) ? (int)$_GET['n'] : null); +$postsOffset = (int)($_GET['o'] ?? 0); +$postsTake = 5; -$templating->vars(compact('posts_offset', 'posts_take')); +$templating->vars([ + 'posts_offset' => $postsOffset, + 'posts_take' => $postsTake, +]); -if ($post_id !== null) { +if ($postId !== null) { $getPost = $db->prepare(' SELECT p.`post_id`, p.`post_title`, p.`post_text`, p.`created_at`, @@ -29,7 +32,7 @@ if ($post_id !== null) { ON u.`display_role` = r.`role_id` WHERE `post_id` = :post_id '); - $getPost->bindValue(':post_id', $post_id, PDO::PARAM_INT); + $getPost->bindValue(':post_id', $postId, PDO::PARAM_INT); $post = $getPost->execute() ? $getPost->fetch() : false; if ($post === false) { @@ -41,7 +44,7 @@ if ($post_id !== null) { return; } -if ($category_id !== null) { +if ($categoryId !== null) { $getCategory = $db->prepare(' SELECT c.`category_id`, c.`category_name`, c.`category_description`, @@ -52,10 +55,10 @@ if ($category_id !== null) { WHERE c.`category_id` = :category_id GROUP BY c.`category_id` '); - $getCategory->bindValue(':category_id', $category_id, PDO::PARAM_INT); + $getCategory->bindValue(':category_id', $categoryId, PDO::PARAM_INT); $category = $getCategory->execute() ? $getCategory->fetch() : false; - if ($category === false || $posts_offset < 0 || $posts_offset >= $category['posts_count']) { + if ($category === false || $postsOffset < 0 || $postsOffset >= $category['posts_count']) { echo render_error(404); return; } @@ -77,8 +80,8 @@ if ($category_id !== null) { ORDER BY `created_at` DESC LIMIT :offset, :take '); - $getPosts->bindValue('offset', $posts_offset); - $getPosts->bindValue('take', $posts_take); + $getPosts->bindValue('offset', $postsOffset); + $getPosts->bindValue('take', $postsTake); $getPosts->bindValue('category_id', $category['category_id'], PDO::PARAM_INT); $posts = $getPosts->execute() ? $getPosts->fetchAll() : false; @@ -121,7 +124,7 @@ $postsCount = (int)$db->query(' $templating->var('posts_count', $postsCount); -if ($posts_offset < 0 || $posts_offset >= $postsCount) { +if ($postsOffset < 0 || $postsOffset >= $postsCount) { echo render_error(404); return; } @@ -144,8 +147,8 @@ $getPosts = $db->prepare(' ORDER BY p.`created_at` DESC LIMIT :offset, :take '); -$getPosts->bindValue('offset', $posts_offset); -$getPosts->bindValue('take', $posts_take); +$getPosts->bindValue('offset', $postsOffset); +$getPosts->bindValue('take', $postsTake); $posts = $getPosts->execute() ? $getPosts->fetchAll() : []; if (!$posts) { diff --git a/public/robots.txt b/public/robots.txt index eb053628..c1438652 100644 --- a/public/robots.txt +++ b/public/robots.txt @@ -1,2 +1,2 @@ User-agent: * -Disallow: +Disallow: /manage/ diff --git a/public/settings.php b/public/settings.php index d6124a5f..8e49d884 100644 --- a/public/settings.php +++ b/public/settings.php @@ -7,138 +7,90 @@ require_once __DIR__ . '/../misuzu.php'; $db = Database::connection(); $templating = $app->getTemplating(); -$query_offset = (int)($_GET['o'] ?? 0); -$query_take = 15; +$queryOffset = (int)($_GET['o'] ?? 0); +$queryTake = 15; if (!$app->hasActiveSession()) { echo render_error(403); return; } -$csrf_error_str = "Couldn't verify you, please refresh the page and retry."; +$csrfErrorString = "Couldn't verify you, please refresh the page and retry."; -$settings_profile_fields = [ - 'twitter' => [ - 'name' => 'Twitter', - 'regex' => '#^(?:https?://(?:www\.)?twitter.com/(?:\#!\/)?)?@?([A-Za-z0-9_]{1,20})/?$#u', - 'no-match' => 'Twitter field was invalid.', - ], - 'osu' => [ - 'name' => 'osu!', - 'regex' => '#^(?:https?://osu.ppy.sh/u(?:sers)?/)?([a-zA-Z0-9-\[\]_ ]{1,20})/?$#u', - 'no-match' => 'osu! field was invalid.', - ], - 'website' => [ - 'name' => 'Website', - 'type' => 'url', - 'regex' => '#^((?:https?)://.{1,240})$#u', - 'no-match' => 'Website field was invalid.', - ], - 'youtube' => [ - 'name' => 'Youtube', - 'regex' => '#^(?:https?://(?:www.)?youtube.com/(?:(?:user|c|channel)/)?)?(UC[a-zA-Z0-9-_]{1,22}|[a-zA-Z0-9-_%]{1,100})/?$#u', - 'no-match' => 'Youtube field was invalid.', - ], - 'steam' => [ - 'name' => 'Steam', - 'regex' => '#^(?:https?://(?:www.)?steamcommunity.com/(?:id|profiles)/)?([a-zA-Z0-9_-]{2,100})/?$#u', - 'no-match' => 'Steam field was invalid.', - ], - 'twitchtv' => [ - 'name' => 'Twitch.tv', - 'regex' => '#^(?:https?://(?:www.)?twitch.tv/)?([0-9A-Za-z_]{3,25})/?$#u', - 'no-match' => 'Twitch.tv field was invalid.', - ], - 'lastfm' => [ - 'name' => 'Last.fm', - 'regex' => '#^(?:https?://(?:www.)?last.fm/user/)?([a-zA-Z]{1}[a-zA-Z0-9_-]{1,14})/?$#u', - 'no-match' => 'Last.fm field was invalid.', - ], - 'github' => [ - 'name' => 'Github', - 'regex' => '#^(?:https?://(?:www.)?github.com/?)?([a-zA-Z0-9](?:[a-zA-Z0-9]|-(?=[a-zA-Z0-9])){0,38})/?$#u', - 'no-match' => 'Github field was invalid.', - ], - 'skype' => [ - 'name' => 'Skype', - 'regex' => '#^((?:live:)?[a-zA-Z][\w\.,\-_@]{1,100})$#u', - 'no-match' => 'Skype field was invalid.', - ], - 'discord' => [ - 'name' => 'Discord', - 'regex' => '#^(.{1,32}\#[0-9]{4})$#u', - 'no-match' => 'Discord field was invalid.', - ], -]; - -$settings_modes = [ +$settingsModes = [ 'account' => 'Account', 'avatar' => 'Avatar', 'sessions' => 'Sessions', 'login-history' => 'Login History', ]; -$settings_mode = $_GET['m'] ?? key($settings_modes); +$settingsMode = $_GET['m'] ?? key($settingsModes); -$templating->vars(compact('settings_mode', 'settings_modes')); +$templating->vars([ + 'settings_mode' => $settingsMode, + 'settings_modes' => $settingsModes, +]); -if (!array_key_exists($settings_mode, $settings_modes)) { +if (!array_key_exists($settingsMode, $settingsModes)) { http_response_code(404); $templating->var('settings_title', 'Not Found'); echo $templating->render('settings.notfound'); return; } -$settings_errors = []; +$settingsErrors = []; -$prevent_registration = $app->getConfig()->get('Auth', 'prevent_registration', 'bool', false); -$avatar_filename = "{$app->getUserId()}.msz"; -$avatar_max_width = $app->getConfig()->get('Avatar', 'max_width', 'int', 4000); -$avatar_max_height = $app->getConfig()->get('Avatar', 'max_height', 'int', 4000); -$avatar_max_filesize = $app->getConfig()->get('Avatar', 'max_filesize', 'int', 1000000); -$avatar_max_filesize_human = byte_symbol($avatar_max_filesize, true); +$disableAccountOptions = $app->getConfig()->get('Auth', 'prevent_registration', 'bool', false); +$avatarFileName = "{$app->getUserId()}.msz"; +$avatarWidthMax = $app->getConfig()->get('Avatar', 'max_width', 'int', 4000); +$avatarHeightMax = $app->getConfig()->get('Avatar', 'max_height', 'int', 4000); +$avatarFileSizeMax = $app->getConfig()->get('Avatar', 'max_filesize', 'int', 1000000); if ($_SERVER['REQUEST_METHOD'] === 'POST') { - switch ($settings_mode) { + switch ($settingsMode) { case 'account': if (!tmp_csrf_verify($_POST['csrf'] ?? '')) { - $settings_errors[] = $csrf_error_str; + $settingsErrors[] = $csrfErrorString; break; } - $updatedUserFields = []; - if (isset($_POST['profile']) && is_array($_POST['profile'])) { - foreach ($settings_profile_fields as $name => $props) { - if (isset($_POST['profile'][$name])) { - $field_value = ''; + $setUserFieldErrors = user_profile_fields_set($app->getUserId(), $_POST['profile']); - if (!empty($_POST['profile'][$name])) { - $field_regex = preg_match( - $props['regex'], - $_POST['profile'][$name], - $field_matches - ); - - if ($field_regex !== 1) { - $settings_errors[] = $props['no-match']; + if (count($setUserFieldErrors) > 0) { + foreach ($setUserFieldErrors as $name => $error) { + switch ($error) { + case MSZ_USER_PROFILE_INVALID_FIELD: + $settingsErrors[] = sprintf("Field '%s' does not exist!", $name); break; - } - $field_value = $field_matches[1]; + case MSZ_USER_PROFILE_FILTER_FAILED: + $settingsErrors[] = sprintf( + '%s field was invalid!', + user_profile_field_get_display_name($name) + ); + break; + + case MSZ_USER_PROFILE_UPDATE_FAILED: + $settingsErrors[] = 'Failed to update values, contact an administator.'; + break; + + default: + $settingsErrors[] = 'An unexpected error occurred, contact an administator.'; + break; } - - $updatedUserFields["user_{$name}"] = $field_value; } } } - if (!$prevent_registration) { + if (!$disableAccountOptions) { if (!empty($_POST['current_password']) || ( - (isset($_POST['password']) || isset($_OST['email'])) + (isset($_POST['password']) || isset($_POST['email'])) && (!empty($_POST['password']['new']) || !empty($_POST['email']['new'])) ) ) { + $updateAccountFields = []; + $fetchPassword = $db->prepare(' SELECT `password` FROM `msz_users` @@ -148,19 +100,19 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { $currentPassword = $fetchPassword->execute() ? $fetchPassword->fetchColumn() : null; if (empty($currentPassword)) { - $settings_errors[] = 'Something went horribly wrong.'; + $settingsErrors[] = 'Something went horribly wrong.'; break; } if (!password_verify($_POST['current_password'], $currentPassword)) { - $settings_errors[] = 'Your current password was incorrect.'; + $settingsErrors[] = 'Your current password was incorrect.'; break; } if (!empty($_POST['email']['new'])) { if (empty($_POST['email']['confirm']) || $_POST['email']['new'] !== $_POST['email']['confirm']) { - $settings_errors[] = 'The given e-mail addresses did not match.'; + $settingsErrors[] = 'The given e-mail addresses did not match.'; break; } @@ -175,7 +127,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { : false; if ($isAlreadySet) { - $settings_errors[] = 'This is your e-mail address already!'; + $settingsErrors[] = 'This is your e-mail address already!'; break; } @@ -184,66 +136,66 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { if ($email_validate !== '') { switch ($email_validate) { case 'dns': - $settings_errors[] = 'No valid MX record exists for this domain.'; + $settingsErrors[] = 'No valid MX record exists for this domain.'; break; case 'format': - $settings_errors[] = 'The given e-mail address was incorrectly formatted.'; + $settingsErrors[] = 'The given e-mail address was incorrectly formatted.'; break; case 'in-use': - $settings_errors[] = 'This e-mail address has already been used by another user.'; + $settingsErrors[] = 'This e-mail address has already been used by another user.'; break; default: - $settings_errors[] = 'Unknown e-mail validation error.'; + $settingsErrors[] = 'Unknown e-mail validation error.'; } break; } - $updatedUserFields['email'] = strtolower($_POST['email']['new']); + $updateAccountFields['email'] = strtolower($_POST['email']['new']); } if (!empty($_POST['password']['new'])) { if (empty($_POST['password']['confirm']) || $_POST['password']['new'] !== $_POST['password']['confirm']) { - $settings_errors[] = "The given passwords did not match."; + $settingsErrors[] = "The given passwords did not match."; break; } $password_validate = user_validate_password($_POST['password']['new']); if ($password_validate !== '') { - $settings_errors[] = "The given passwords was too weak."; + $settingsErrors[] = "The given passwords was too weak."; break; } - $updatedUserFields['password'] = password_hash($_POST['password']['new'], PASSWORD_ARGON2I); + $updateAccountFields['password'] = user_password_hash($_POST['password']['new']); + } + + if (count($updateAccountFields) > 0) { + $updateUser = $db->prepare(' + UPDATE `msz_users` + SET ' . pdo_prepare_array_update($updateAccountFields, true) . ' + WHERE `user_id` = :user_id + '); + $updateAccountFields['user_id'] = $app->getUserId(); + $updateUser->execute($updateAccountFields); } } } - - if (count($settings_errors) < 1 && count($updatedUserFields) > 0) { - $updateUser = $db->prepare(' - UPDATE `msz_users` - SET ' . pdo_prepare_array_update($updatedUserFields, true) . ' - WHERE `user_id` = :user_id - '); - $updatedUserFields['user_id'] = $app->getUserId(); - $updateUser->execute($updatedUserFields); - } break; case 'avatar': if (isset($_POST['delete'])) { if (!tmp_csrf_verify($_POST['delete'])) { - $settings_errors[] = $csrf_error_str; + $settingsErrors[] = $csrfErrorString; break; } $delete_this = [ - $app->getStore('avatars/original')->filename($avatar_filename), - $app->getStore('avatars/200x200')->filename($avatar_filename), + $app->getStore('avatars/original')->filename($avatarFileName), + $app->getStore('avatars/200x200')->filename($avatarFileName), ]; foreach ($delete_this as $delete_avatar) { @@ -256,7 +208,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { if (isset($_POST['upload'])) { if (!tmp_csrf_verify($_POST['upload'])) { - $settings_errors[] = $csrf_error_str; + $settingsErrors[] = $csrfErrorString; break; } @@ -265,30 +217,33 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { break; case UPLOAD_ERR_NO_FILE: - $settings_errors[] = 'Select a file before hitting upload!'; + $settingsErrors[] = 'Select a file before hitting upload!'; break; case UPLOAD_ERR_PARTIAL: - $settings_errors[] = 'The upload was interrupted, please try again!'; + $settingsErrors[] = 'The upload was interrupted, please try again!'; break; case UPLOAD_ERR_INI_SIZE: case UPLOAD_ERR_FORM_SIZE: - $settings_errors[] = "Your avatar is not allowed to be larger in filesize than {$avatar_max_filesize_human}!"; + $settingsErrors[] = sprintf( + 'Your avatar is not allowed to be larger in filesize than %s', + byte_symbol($avatarFileSizeMax, true) + ); break; case UPLOAD_ERR_NO_TMP_DIR: case UPLOAD_ERR_CANT_WRITE: - $settings_errors[] = 'Unable to save your avatar, contact an administator!'; + $settingsErrors[] = 'Unable to save your avatar, contact an administator!'; break; case UPLOAD_ERR_EXTENSION: default: - $settings_errors[] = 'Something happened?'; + $settingsErrors[] = 'Something happened?'; break; } - if (count($settings_errors) > 0) { + if (count($settingsErrors) > 0) { break; } @@ -299,24 +254,33 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { || !in_array($upload_meta[2], [IMAGETYPE_GIF, IMAGETYPE_JPEG, IMAGETYPE_PNG], true) || $upload_meta[0] < 1 || $upload_meta[1] < 1) { - $settings_errors[] = 'Please provide a valid image.'; + $settingsErrors[] = 'Please provide a valid image.'; break; } - if ($upload_meta[0] > $avatar_max_width || $upload_meta[1] > $avatar_max_height) { - $settings_errors[] = "Your avatar can't be larger than {$avatar_max_width}x{$avatar_max_height}, yours was {$upload_meta[0]}x{$upload_meta[1]}"; + if ($upload_meta[0] > $avatarWidthMax || $upload_meta[1] > $avatarHeightMax) { + $settingsErrors[] = sprintf( + "Your avatar can't be larger than %dx%d, yours was %dx%d", + $avatarWidthMax, + $avatarHeightMax, + $upload_meta[0], + $upload_meta[1] + ); break; } - if (filesize($upload_path) > $avatar_max_filesize) { - $settings_errors[] = "Your avatar is not allowed to be larger in filesize than {$avatar_max_filesize_human}!"; + if (filesize($upload_path) > $avatarFileSizeMax) { + $settingsErrors[] = sprintf( + 'Your avatar is not allowed to be larger in filesize than %s!', + byte_symbol($avatarFileSizeMax, true) + ); break; } - $avatar_path = $app->getStore('avatars/original')->filename($avatar_filename); + $avatar_path = $app->getStore('avatars/original')->filename($avatarFileName); move_uploaded_file($upload_path, $avatar_path); - $crop_path = $app->getStore('avatars/200x200')->filename($avatar_filename); + $crop_path = $app->getStore('avatars/200x200')->filename($avatarFileName); if (File::exists($crop_path)) { File::delete($crop_path); @@ -324,19 +288,19 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { break; } - $settings_errors[] = "You shouldn't have done that."; + $settingsErrors[] = "You shouldn't have done that."; break; case 'sessions': if (!tmp_csrf_verify($_POST['csrf'] ?? '')) { - $settings_errors[] = $csrf_error_str; + $settingsErrors[] = $csrfErrorString; break; } $session_id = (int)($_POST['session'] ?? 0); if ($session_id < 1) { - $settings_errors[] = 'Invalid session.'; + $settingsErrors[] = 'Invalid session.'; break; } @@ -349,7 +313,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { $session = $findSession->execute() ? $findSession->fetch() : null; if (!$session || (int)$session['user_id'] !== $app->getUserId()) { - $settings_errors[] = 'You may only end your own sessions.'; + $settingsErrors[] = 'You may only end your own sessions.'; break; } @@ -358,42 +322,41 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { return; } - $deleteSession = $db->prepare(' - DELETE FROM `msz_sessions` - WHERE `session_id` = :session_id - '); - $deleteSession->bindValue('session_id', $session['session_id']); - $deleteSession->execute(); + user_session_delete($session['session_id']); break; } } -$templating->vars(compact('settings_errors')); -$templating->var('settings_title', $settings_modes[$settings_mode]); +$templating->var('settings_title', $settingsModes[$settingsMode]); +$templating->var('settings_errors', $settingsErrors); -switch ($settings_mode) { +switch ($settingsMode) { case 'account': + $profileFields = user_profile_fields_get(); $getUserFields = $db->prepare(' - SELECT ' . pdo_prepare_array($settings_profile_fields, true, '`user_%s`') . ' + SELECT ' . pdo_prepare_array($profileFields, true, '`user_%s`') . ' FROM `msz_users` WHERE `user_id` = :user_id '); $getUserFields->bindValue('user_id', $app->getUserId()); $userFields = $getUserFields->execute() ? $getUserFields->fetch() : []; - $templating->var('settings_profile_values', $userFields); - $templating->vars(compact('settings_profile_fields', 'prevent_registration')); + $templating->vars([ + 'settings_profile_fields' => $profileFields, + 'settings_profile_values' => $userFields, + 'settings_disable_account_options' => $disableAccountOptions, + ]); break; case 'avatar': - $user_has_avatar = File::exists($app->getStore('avatars/original')->filename($avatar_filename)); - $templating->var('avatar_user_id', $app->getUserId()); - $templating->vars(compact( - 'avatar_max_width', - 'avatar_max_height', - 'avatar_max_filesize', - 'user_has_avatar' - )); + $userHasAvatar = File::exists($app->getStore('avatars/original')->filename($avatarFileName)); + $templating->vars([ + 'avatar_user_id' => $app->getUserId(), + 'avatar_max_width' => $avatarWidthMax, + 'avatar_max_height' => $avatarHeightMax, + 'avatar_max_filesize' => $avatarFileSizeMax, + 'user_has_avatar' => $userHasAvatar, + ]); break; case 'sessions': @@ -414,16 +377,16 @@ switch ($settings_mode) { ORDER BY `session_id` DESC LIMIT :offset, :take '); - $getSessions->bindValue('offset', $query_offset); - $getSessions->bindValue('take', $query_take); + $getSessions->bindValue('offset', $queryOffset); + $getSessions->bindValue('take', $queryTake); $getSessions->bindValue('user_id', $app->getUserId()); $sessions = $getSessions->execute() ? $getSessions->fetchAll() : []; $templating->vars([ 'active_session_id' => $app->getSessionId(), 'user_sessions' => $sessions, - 'sessions_offset' => $query_offset, - 'sessions_take' => $query_take, + 'sessions_offset' => $queryOffset, + 'sessions_take' => $queryTake, 'sessions_count' => $sessionCount, ]); break; @@ -446,18 +409,18 @@ switch ($settings_mode) { ORDER BY `attempt_id` DESC LIMIT :offset, :take '); - $getLoginAttempts->bindValue('offset', $query_offset); - $getLoginAttempts->bindValue('take', $query_take); + $getLoginAttempts->bindValue('offset', $queryOffset); + $getLoginAttempts->bindValue('take', $queryTake); $getLoginAttempts->bindValue('user_id', $app->getUserId()); $loginAttempts = $getLoginAttempts->execute() ? $getLoginAttempts->fetchAll() : []; $templating->vars([ 'user_login_attempts' => $loginAttempts, - 'login_attempts_offset' => $query_offset, - 'login_attempts_take' => $query_take, + 'login_attempts_offset' => $queryOffset, + 'login_attempts_take' => $queryTake, 'login_attempts_count' => $loginAttemptsCount, ]); break; } -echo $templating->render("settings.{$settings_mode}"); +echo $templating->render("settings.{$settingsMode}"); diff --git a/src/Users/login_attempt.php b/src/Users/login_attempt.php index bbf28bd3..491e0743 100644 --- a/src/Users/login_attempt.php +++ b/src/Users/login_attempt.php @@ -17,3 +17,19 @@ function user_login_attempt_record(bool $success, ?int $userId, string $ipAddres $storeAttempt->bindValue('user_id', $userId, $userId === null ? PDO::PARAM_NULL : PDO::PARAM_INT); $storeAttempt->execute(); } + +function user_login_attempts_remaining(string $ipAddress): int +{ + $getRemaining = Database::connection()->prepare(' + SELECT 5 - COUNT(`attempt_id`) + FROM `msz_login_attempts` + WHERE `was_successful` = false + AND `created_at` > NOW() - INTERVAL 1 HOUR + AND `attempt_ip` = INET6_ATON(:remote_ip) + '); + $getRemaining->bindValue('remote_ip', $ipAddress); + + return $getRemaining->execute() + ? (int)$getRemaining->fetchColumn() + : 0; +} diff --git a/src/Users/profile.php b/src/Users/profile.php new file mode 100644 index 00000000..054209c8 --- /dev/null +++ b/src/Users/profile.php @@ -0,0 +1,141 @@ + [ + 'name' => 'Twitter', + 'regex' => '#^(?:https?://(?:www\.)?twitter.com/(?:\#!\/)?)?@?([A-Za-z0-9_]{1,20})/?$#u', + ], + 'osu' => [ + 'name' => 'osu!', + 'regex' => '#^(?:https?://osu.ppy.sh/u(?:sers)?/)?([a-zA-Z0-9-\[\]_ ]{1,20})/?$#u', + ], + 'website' => [ + 'name' => 'Website', + 'type' => 'url', + 'regex' => '#^((?:https?)://.{1,240})$#u', + ], + 'youtube' => [ + 'name' => 'Youtube', + 'regex' => '#^(?:https?://(?:www.)?youtube.com/(?:(?:user|c|channel)/)?)?(UC[a-zA-Z0-9-_]{1,22}|[a-zA-Z0-9-_%]{1,100})/?$#u', + ], + 'steam' => [ + 'name' => 'Steam', + 'regex' => '#^(?:https?://(?:www.)?steamcommunity.com/(?:id|profiles)/)?([a-zA-Z0-9_-]{2,100})/?$#u', + ], + 'twitchtv' => [ + 'name' => 'Twitch.tv', + 'regex' => '#^(?:https?://(?:www.)?twitch.tv/)?([0-9A-Za-z_]{3,25})/?$#u', + ], + 'lastfm' => [ + 'name' => 'Last.fm', + 'regex' => '#^(?:https?://(?:www.)?last.fm/user/)?([a-zA-Z]{1}[a-zA-Z0-9_-]{1,14})/?$#u', + ], + 'github' => [ + 'name' => 'Github', + 'regex' => '#^(?:https?://(?:www.)?github.com/?)?([a-zA-Z0-9](?:[a-zA-Z0-9]|-(?=[a-zA-Z0-9])){0,38})/?$#u', + ], + 'skype' => [ + 'name' => 'Skype', + 'regex' => '#^((?:live:)?[a-zA-Z][\w\.,\-_@]{1,100})$#u', + ], + 'discord' => [ + 'name' => 'Discord', + 'regex' => '#^(.{1,32}\#[0-9]{4})$#u', + ], +]); + +function user_profile_field_is_valid(string $name): bool +{ + return array_key_exists($name, MSZ_USER_PROFILE_FIELDS); +} + +function user_profile_field_get_display_name(string $name): string +{ + return MSZ_USER_PROFILE_FIELDS[$name]['name'] ?? ''; +} + +function user_profile_fields_get(): array +{ + return MSZ_USER_PROFILE_FIELDS; +} + +function user_profile_field_get_regex(string $name): string +{ + return MSZ_USER_PROFILE_FIELDS[$name]['regex'] ?? ''; +} + +// === NULL if field is invalid +function user_profile_field_filter(string $name, string $value): ?string +{ + if (!user_profile_field_is_valid($name)) { + return null; + } + + $regex = user_profile_field_get_regex($name); + + if (empty($regex) || empty($value)) { + return $value; + } + + $checkRegex = preg_match($regex, $value, $matches); + + if (!$checkRegex || empty($matches[1])) { + return null; + } + + return $matches[1]; +} + +function user_profile_fields_set(int $userId, array $fields): array +{ + if (count($fields) < 1) { + return []; + } + + $errors = []; + $values = []; + + foreach ($fields as $name => $value) { + // should these just be ignored? + if (!user_profile_field_is_valid($name)) { + $errors[$name] = MSZ_USER_PROFILE_INVALID_FIELD; + continue; + } + + $value = user_profile_field_filter($name, $value); + + if ($value === null) { + $errors[$name] = MSZ_USER_PROFILE_FILTER_FAILED; + continue; + } + + $values[sprintf(MSZ_USER_PROFILE_FIELD_FORMAT, $name)] = $value; + } + + if (count($values) > 0) { + $updateFields = Database::connection()->prepare(' + UPDATE `msz_users` + SET ' . pdo_prepare_array_update($values, true) . ' + WHERE `user_id` = :user_id + '); + $values['user_id'] = $userId; + + if (!$updateFields->execute($values)) { + $errors[MSZ_USER_PROFILE_SET_ERROR] = MSZ_USER_PROFILE_UPDATE_FAILED; + } + } + + return $errors; +} diff --git a/src/Users/role.php b/src/Users/role.php new file mode 100644 index 00000000..5a657f68 --- /dev/null +++ b/src/Users/role.php @@ -0,0 +1,17 @@ +prepare(' + INSERT INTO `msz_user_roles` + (`user_id`, `role_id`) + VALUES + (:user_id, :role_id) + '); + $addRole->bindValue('user_id', $userId); + $addRole->bindValue('role_id', $roleId); + return $addRole->execute(); +} diff --git a/src/Users/session.php b/src/Users/session.php new file mode 100644 index 00000000..993cc908 --- /dev/null +++ b/src/Users/session.php @@ -0,0 +1,47 @@ +prepare(' + INSERT INTO `msz_sessions` + ( + `user_id`, `session_ip`, `session_country`, + `user_agent`, `session_key`, `created_at`, `expires_on` + ) + VALUES + ( + :user_id, INET6_ATON(:session_ip), :session_country, + :user_agent, :session_key, NOW(), NOW() + INTERVAL 1 MONTH + ) + '); + $createSession->bindValue('user_id', $userId); + $createSession->bindValue('session_ip', $ipAddress); + $createSession->bindValue('session_country', get_country_code($ipAddress)); + $createSession->bindValue('user_agent', $userAgent); + $createSession->bindValue('session_key', $sessionKey); + + return $createSession->execute() ? $sessionKey : ''; +} + +function user_session_delete(int $sessionId): bool +{ + $deleteSession = Database::connection()->prepare(' + DELETE FROM `msz_sessions` + WHERE `session_id` = :session_id + '); + $deleteSession->bindValue('session_id', $sessionId); + return $deleteSession->execute(); +} + +function user_session_generate_key(): string +{ + return bin2hex(random_bytes(MSZ_SESSION_KEY_SIZE / 2)); +} diff --git a/src/Users/user.php b/src/Users/user.php new file mode 100644 index 00000000..e6eed203 --- /dev/null +++ b/src/Users/user.php @@ -0,0 +1,55 @@ +prepare(' + INSERT INTO `msz_users` + ( + `username`, `password`, `email`, `register_ip`, + `last_ip`, `user_country`, `created_at`, `display_role` + ) + VALUES + ( + :username, :password, :email, INET6_ATON(:register_ip), + INET6_ATON(:last_ip), :user_country, NOW(), 1 + ) + '); + $createUser->bindValue('username', $username); + $createUser->bindValue('password', user_password_hash($password)); + $createUser->bindValue('email', $email); + $createUser->bindValue('register_ip', $ipAddress); + $createUser->bindValue('last_ip', $ipAddress); + $createUser->bindValue('user_country', get_country_code($ipAddress)); + + return $createUser->execute() ? (int)$dbc->lastInsertId() : 0; +} + +function user_password_hash(string $password): string +{ + return password_hash($password, MSZ_USERS_PASSWORD_HASH_ALGO); +} + +// Temporary key generation for chat login. +// Should eventually be replaced with a callback login system. +function user_generate_chat_key(int $userId): string +{ + $chatKey = bin2hex(random_bytes(16)); + + $setChatKey = Database::connection()->prepare(' + UPDATE `msz_users` + SET `user_chat_key` = :user_chat_key + WHERE `user_id` = :user_id + '); + $setChatKey->bindValue('user_chat_key', $chatKey); + $setChatKey->bindValue('user_id', $userId); + + return $setChatKey->execute() ? $chatKey : ''; +} diff --git a/views/manage/master.twig b/views/manage/master.twig index b129a41b..d943a3e6 100644 --- a/views/manage/master.twig +++ b/views/manage/master.twig @@ -105,6 +105,7 @@ Flashii Broom Closet + @@ -164,5 +165,6 @@ {{ link('https://github.com/flashwave/misuzu/tree/' ~ git_branch(), git_branch(), 'footer__link') }} # {{ link('https://github.com/flashwave/misuzu/commit/' ~ git_hash(true), git_hash(), 'footer__link') }} + diff --git a/views/manage/users/listing.twig b/views/manage/users/listing.twig index 0594bd3b..28237551 100644 --- a/views/manage/users/listing.twig +++ b/views/manage/users/listing.twig @@ -6,10 +6,12 @@ {% for user in manage_users %}
-
- {{ user.username }} +
+
+ {{ user.username }} +
-
+
{% endfor %} diff --git a/views/mio/home/landing.twig b/views/mio/home/landing.twig index 40a9b96f..c0b54821 100644 --- a/views/mio/home/landing.twig +++ b/views/mio/home/landing.twig @@ -18,7 +18,7 @@ Featured News
- {% for post in featuredNews %} + {% for post in featured_news %} {{ news_preview(post) }} {% endfor %}
diff --git a/views/mio/settings/account.twig b/views/mio/settings/account.twig index cdd8355a..fe87806c 100644 --- a/views/mio/settings/account.twig +++ b/views/mio/settings/account.twig @@ -18,7 +18,7 @@ {% endfor %}
- {% if prevent_registration %} + {% if settings_disable_account_options %}