Implemented Flashii ID logins.
This commit is contained in:
parent
a074aceddd
commit
ce7c0f5bd7
11 changed files with 140 additions and 454 deletions
333
public/index.php
333
public/index.php
|
@ -5,6 +5,18 @@ use Exception;
|
|||
|
||||
require_once __DIR__ . '/../startup.php';
|
||||
|
||||
if(!function_exists('base64uri_encode')) {
|
||||
function base64uri_encode(string $string): string {
|
||||
return rtrim(strtr(base64_encode($string), '+/', '-_'), '=');
|
||||
}
|
||||
}
|
||||
|
||||
if(!function_exists('base64uri_decode')) {
|
||||
function base64uri_decode(string $string, bool $strict = false): string|false {
|
||||
return base64_decode(str_pad(strtr($string, '-_', '+/'), strlen($string) % 4, '=', STR_PAD_RIGHT));
|
||||
}
|
||||
}
|
||||
|
||||
function page_url(string $path, array $params = []): string {
|
||||
if(isset($params['p']))
|
||||
unset($params['p']);
|
||||
|
@ -55,14 +67,10 @@ function html_header(array $vars = []): string {
|
|||
['text' => 'Settings', 'link' => page_url('/settings')],
|
||||
];
|
||||
|
||||
if(Config::get('user.invite_only', Config::TYPE_BOOL))
|
||||
$userMenu[] = ['text' => 'Invites', 'link' => page_url('/settings/invites')];
|
||||
|
||||
$userMenu[] = ['text' => 'Log out', 'link' => page_url('/auth/logout', ['s' => UserSession::instance()->getSmallToken()])];
|
||||
} else {
|
||||
$userMenu = [
|
||||
['text' => 'Log in', 'link' => page_url('/auth/login')],
|
||||
['text' => 'Register', 'link' => page_url('/auth/register')],
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -861,7 +869,7 @@ if(preg_match('#^/uploads/([a-zA-Z0-9-_]{16}).json$#', $reqPath, $matches)) {
|
|||
|
||||
if($reqPath === '/settings') {
|
||||
if(!UserSession::hasInstance()) {
|
||||
http_response_code(404);
|
||||
http_response_code(403);
|
||||
echo html_information('You must be logged in to access this page.');
|
||||
return;
|
||||
}
|
||||
|
@ -872,62 +880,6 @@ if($reqPath === '/settings') {
|
|||
return;
|
||||
}
|
||||
|
||||
if($reqPath === '/settings/invites') {
|
||||
if(!UserSession::hasInstance()) {
|
||||
http_response_code(404);
|
||||
echo html_information('You must be logged in to access this page.');
|
||||
return;
|
||||
}
|
||||
|
||||
$currentUser = UserSession::instance()->getUser();
|
||||
$createdInvites = UserInvite::fromCreator($currentUser);
|
||||
$createToken = $currentUser->getId() !== 1 /*&& count($createdInvites) >= 5*/ ? '' : UserSession::instance()->getSmallToken(11);
|
||||
|
||||
if($reqMethod === 'POST') {
|
||||
if(empty($createToken)) {
|
||||
$inviteError = 'You\'ve reached the maximum amount of invites you can generate.';
|
||||
} elseif($createToken !== filter_input(INPUT_POST, 'invite_token')) {
|
||||
$inviteError = 'Cannot create invite.';
|
||||
} else {
|
||||
try {
|
||||
$createdInvites[] = $createdInvite = UserInvite::create($currentUser);
|
||||
$inviteError = $createdInvite->getToken();
|
||||
} catch(UserInviteCreationFailedException $ex) {
|
||||
$inviteError = 'Invite creation failed.';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(isset($inviteError))
|
||||
$inviteError = Template::renderRaw('error', [
|
||||
'error_text' => $inviteError,
|
||||
]);
|
||||
|
||||
if(!empty($createToken))
|
||||
$inviteCreate = Template::renderRaw('settings/invites-create', [
|
||||
'create_token' => $createToken,
|
||||
]);
|
||||
|
||||
$invitesItems = [];
|
||||
foreach($createdInvites as $inviteItem) {
|
||||
$invitesItems[] = [
|
||||
'invite_created' => date('c', $inviteItem->getCreated()),
|
||||
'invite_used' => (($_used = $inviteItem->getUsed()) === null ? 'Unused' : date('c', $_used)),
|
||||
'invite_user' => (($_user = $inviteItem->getUser()) === null ? 'Unused' : sprintf('<a href="/@%1$s">%1$s</a>', $_user->getUsername())),
|
||||
'invite_token' => $inviteItem->getToken(),
|
||||
];
|
||||
}
|
||||
|
||||
echo html_header(['title' => 'Invites - YTKNS']);
|
||||
Template::render('settings/invites', [
|
||||
'invite_error' => $inviteError ?? '',
|
||||
'invite_create' => $inviteCreate ?? '',
|
||||
'invite_list' => Template::renderSet('settings/invites-item', $invitesItems),
|
||||
]);
|
||||
echo html_footer();
|
||||
return;
|
||||
}
|
||||
|
||||
if($reqPath === '/auth/login') {
|
||||
if(YTKNS_MAINTENANCE) {
|
||||
http_response_code(503);
|
||||
|
@ -941,161 +893,130 @@ if($reqPath === '/auth/login') {
|
|||
return;
|
||||
}
|
||||
|
||||
if($reqMethod === 'POST') {
|
||||
$loginUsername = filter_input(INPUT_POST, 'username');
|
||||
$loginPassword = filter_input(INPUT_POST, 'password');
|
||||
$loginRemember = !empty(filter_input(INPUT_POST, 'remember'));
|
||||
if(filter_has_var(INPUT_GET, 'state')) {
|
||||
$state = base64uri_decode((string)filter_input(INPUT_GET, 'state'));
|
||||
if($state === false || strlen($state) !== 60) {
|
||||
http_response_code(400);
|
||||
echo html_information('Provided state is invalid.');
|
||||
return;
|
||||
}
|
||||
|
||||
if(empty($loginUsername) || empty($loginPassword)) {
|
||||
$authError = 'Username or password missing.';
|
||||
} else {
|
||||
$signature = hash_hmac('sha256', substr($state, 32), YTKNS_OA2_STATE_SECRET, true);
|
||||
if(!hash_equals($signature, substr($state, 0, 32))) {
|
||||
http_response_code(403);
|
||||
echo html_information('Request verification failed.');
|
||||
return;
|
||||
}
|
||||
|
||||
$state = unpack('a32hash/Jtime/a20verifier', $state);
|
||||
if($state === false) {
|
||||
http_response_code(500);
|
||||
echo html_information('State unpack failed.');
|
||||
return;
|
||||
}
|
||||
|
||||
if($state['time'] < strtotime('-15 minutes') || $state['time'] > strtotime('+15 minutes')) {
|
||||
http_response_code(403);
|
||||
echo html_information('Authorisation request timestamp is no longer valid.');
|
||||
return;
|
||||
}
|
||||
|
||||
if(filter_has_var(INPUT_GET, 'error')) {
|
||||
$error = (string)filter_input(INPUT_GET, 'error');
|
||||
$text = (string)filter_input(INPUT_GET, 'error_description');
|
||||
if($text === '')
|
||||
$text = match($error) {
|
||||
'access_denied' => 'You rejected the authorisation request.',
|
||||
'invalid_request' => 'YTKNS sent an invalid request to the authorisation server, please report this!',
|
||||
'invalid_scope' => 'YTKNS sent an invalid request to the authorisation server, please report this!',
|
||||
'server_error' => 'Something went wrong on the authorisation server, please try again later.',
|
||||
default => sprintf('An unexpected error occurred: %s', $error),
|
||||
};
|
||||
|
||||
http_response_code(400);
|
||||
echo html_information(htmlspecialchars($text));
|
||||
return;
|
||||
}
|
||||
|
||||
if(filter_has_var(INPUT_GET, 'code')) {
|
||||
try {
|
||||
$loginUser = User::forLogin($loginUsername);
|
||||
} catch(\Exception $ex) {}
|
||||
$postData = sprintf(
|
||||
'grant_type=authorization_code&code=%s&code_verifier=%s',
|
||||
rawurlencode((string)filter_input(INPUT_GET, 'code')),
|
||||
rawurldecode($state['verifier'])
|
||||
);
|
||||
$authz = sprintf('Basic %s', base64_encode(sprintf('%s:%s', YTKNS_OA2_CLIENT_ID, YTKNS_OA2_CLIENT_SECRET)));
|
||||
|
||||
if(empty($loginUser) || empty($loginUser->password) || !password_verify($loginPassword, $loginUser->password)) {
|
||||
$authError = 'Username or password was invalid.';
|
||||
} else {
|
||||
$session = UserSession::create($loginUser, $loginRemember);
|
||||
$tokenInfo = json_decode(file_get_contents('https://api.flashii.net/oauth2/token', false, stream_context_create([
|
||||
'http' => [
|
||||
'method' => 'POST',
|
||||
'header' => implode("\r\n", [
|
||||
sprintf('Authorization: %s', $authz),
|
||||
'Content-Type: application/x-www-form-urlencoded',
|
||||
sprintf('Content-Length: %d', strlen($postData)),
|
||||
'User-Agent: YTKNS',
|
||||
]),
|
||||
'content' => $postData,
|
||||
],
|
||||
])));
|
||||
|
||||
if(isset($tokenInfo->access_token)) {
|
||||
$fUserInfo = json_decode(file_get_contents('https://api.flashii.net/v1/me', false, stream_context_create([
|
||||
'http' => [
|
||||
'method' => 'GET',
|
||||
'header' => implode("\r\n", [
|
||||
sprintf('Authorization: Bearer %s', $tokenInfo->access_token),
|
||||
'User-Agent: YTKNS',
|
||||
]),
|
||||
],
|
||||
])));
|
||||
|
||||
if(empty($fUserInfo->id)) {
|
||||
http_response_code(500);
|
||||
echo html_information('Authentication failed.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$userInfo = User::byRemoteId($fUserInfo->id);
|
||||
$loginMessage = 'You are now logged in!';
|
||||
} catch(UserNotFoundException) {
|
||||
try {
|
||||
$userInfo = User::create($fUserInfo->id, $fUserInfo->name);
|
||||
} catch(\PDOException) {
|
||||
$userInfo = User::create($fUserInfo->id, sprintf('%s_%04d', $fUserInfo->name, random_int(0, 9999)));
|
||||
}
|
||||
|
||||
$loginMessage = 'Your account been created!';
|
||||
}
|
||||
|
||||
// leaving session bumping off for now, the implementation needs to be better for that
|
||||
$session = UserSession::create($userInfo, false);
|
||||
$session->setInstance();
|
||||
setcookie('ytkns_login', $session->getToken(), $session->getExpires(), '/', '.' . Config::get('domain.main'), false, true);
|
||||
echo html_information('You are now logged in!', 'Welcome', '/');
|
||||
echo html_information($loginMessage, 'Welcome', '/');
|
||||
return;
|
||||
}
|
||||
} catch(\Exception $ex) {
|
||||
http_response_code(500);
|
||||
echo html_information('Authorisation request failed, please try again.');
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(isset($authError))
|
||||
$authError = Template::renderRaw('error', [
|
||||
'error_text' => $authError,
|
||||
]);
|
||||
$verifier = random_bytes(20);
|
||||
$time = pack('J', time());
|
||||
$signature = hash_hmac('sha256', $time . $verifier, YTKNS_OA2_STATE_SECRET, true);
|
||||
$state = base64uri_encode($signature . $time . $verifier);
|
||||
|
||||
$authFields = [
|
||||
[
|
||||
'field_title' => 'Username or E-Mail Address',
|
||||
'field_type' => 'text',
|
||||
'field_name' => 'username',
|
||||
'field_value' => ($loginUsername ?? ''),
|
||||
],
|
||||
[
|
||||
'field_title' => 'Password',
|
||||
'field_type' => 'password',
|
||||
'field_name' => 'password',
|
||||
'field_value' => '',
|
||||
],
|
||||
];
|
||||
|
||||
echo html_header(['title' => 'Log in - YTKNS']);
|
||||
Template::render('auth/login' . (isset($_GET['new']) ? '2' : ''), [
|
||||
'auth_error' => $authError ?? '',
|
||||
'auth_fields' => Template::renderSet('auth/field', $authFields),
|
||||
'auth_remember' => ($loginRemember ?? false) ? ' checked' : '',
|
||||
]);
|
||||
echo html_footer();
|
||||
return;
|
||||
}
|
||||
|
||||
if($reqPath === '/auth/register') {
|
||||
if(YTKNS_MAINTENANCE) {
|
||||
http_response_code(503);
|
||||
echo html_information('You cannot register during maintenance.');
|
||||
return;
|
||||
}
|
||||
|
||||
if(UserSession::hasInstance()) {
|
||||
http_response_code(404);
|
||||
echo html_information('You are logged in already.');
|
||||
return;
|
||||
}
|
||||
|
||||
$inviteOnly = Config::get('user.invite_only', Config::TYPE_BOOL);
|
||||
|
||||
if($reqMethod === 'POST') {
|
||||
$registerUsername = filter_input(INPUT_POST, 'username');
|
||||
$registerPassword = filter_input(INPUT_POST, 'password');
|
||||
$registerEMail = filter_input(INPUT_POST, 'email');
|
||||
$registerInvite = filter_input(INPUT_POST, 'invite');
|
||||
|
||||
if(empty($registerUsername) || empty($registerPassword) || empty($registerEMail) || ($inviteOnly && empty($registerInvite))) {
|
||||
$authError = 'You must fill in all fields.';
|
||||
} else {
|
||||
if($inviteOnly) {
|
||||
try {
|
||||
$userInvite = UserInvite::byToken($registerInvite);
|
||||
|
||||
if($userInvite->isUsed()) {
|
||||
$authError = 'Invalid invite token.';
|
||||
}
|
||||
} catch(UserInviteNotFoundException $ex) {
|
||||
$authError = 'Invalid invite token.';
|
||||
}
|
||||
}
|
||||
|
||||
if(!isset($authError)) {
|
||||
try {
|
||||
$createdUser = User::create(
|
||||
$registerUsername,
|
||||
$registerPassword,
|
||||
$registerEMail
|
||||
);
|
||||
|
||||
if(isset($userInvite))
|
||||
$userInvite->markUsed($createdUser);
|
||||
} catch(UserCreationInvalidNameException $ex) {
|
||||
$authError = 'Your username contains invalid characters or is too short or long.<br/>Must be between 1 and 20 characters and may only contains alphanumeric characters as well as - and _.';
|
||||
} catch(UserCreationInvalidPasswordException $ex) {
|
||||
$authError = 'Your password must have at least 6 unique characters.';
|
||||
} catch(UserCreationInvalidMailException $ex) {
|
||||
$authError = 'Your e-mail address isn\'t real.';
|
||||
} catch(UserCreationFailedException $ex) {
|
||||
$authError = 'Failed to create user.';
|
||||
}
|
||||
}
|
||||
}
|
||||
} elseif($reqMethod === 'GET') {
|
||||
$registerInvite = filter_input(INPUT_GET, 'inv', FILTER_SANITIZE_STRING);
|
||||
}
|
||||
|
||||
$authFields = [
|
||||
[
|
||||
'field_title' => 'Username',
|
||||
'field_type' => 'text',
|
||||
'field_name' => 'username',
|
||||
'field_value' => ($registerUsername ?? ''),
|
||||
],
|
||||
[
|
||||
'field_title' => 'Password',
|
||||
'field_type' => 'password',
|
||||
'field_name' => 'password',
|
||||
'field_value' => '',
|
||||
],
|
||||
[
|
||||
'field_title' => 'E-Mail Address',
|
||||
'field_type' => 'email',
|
||||
'field_name' => 'email',
|
||||
'field_value' => ($registerEMail ?? ''),
|
||||
],
|
||||
];
|
||||
|
||||
if($inviteOnly)
|
||||
$authFields[] = [
|
||||
'field_title' => 'Invitation',
|
||||
'field_type' => 'password',
|
||||
'field_name' => 'invite',
|
||||
'field_value' => ($registerInvite ?? ''),
|
||||
];
|
||||
|
||||
if(isset($authError))
|
||||
$authError = Template::renderRaw('error', [
|
||||
'error_text' => $authError,
|
||||
]);
|
||||
|
||||
echo html_header(['title' => 'Register - YTKNS']);
|
||||
Template::render('auth/register', [
|
||||
'auth_error' => $authError ?? '',
|
||||
'auth_fields' => Template::renderSet('auth/field', $authFields),
|
||||
]);
|
||||
echo html_footer();
|
||||
header(sprintf(
|
||||
'Location: https://id.flashii.net/oauth2/authorise?response_type=code&scope=identify&code_challenge_method=S256&client_id=%s&state=%s&code_challenge=%s&redirect_uri=%s',
|
||||
rawurlencode(YTKNS_OA2_CLIENT_ID),
|
||||
rawurlencode($state),
|
||||
rawurlencode(base64uri_encode(hash('sha256', $verifier, true))),
|
||||
rawurlencode('https://ytkns.com/auth/login'),
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
57
src/User.php
57
src/User.php
|
@ -5,9 +5,6 @@ use Exception;
|
|||
|
||||
class UserNotFoundException extends Exception {}
|
||||
class UserCreationFailedException extends Exception {}
|
||||
class UserCreationInvalidNameException extends Exception {}
|
||||
class UserCreationInvalidPasswordException extends Exception {}
|
||||
class UserCreationInvalidMailException extends Exception {}
|
||||
|
||||
#[\AllowDynamicProperties]
|
||||
class User {
|
||||
|
@ -21,13 +18,17 @@ class User {
|
|||
$this->user_id = $userId;
|
||||
}
|
||||
|
||||
public function getRemoteId(): string {
|
||||
return (string)($this->remote_id ?? '');
|
||||
}
|
||||
|
||||
public function getUsername(): string {
|
||||
return $this->username ?? '';
|
||||
}
|
||||
|
||||
public static function byId(int $userId): self {
|
||||
$getUser = DB::prepare('
|
||||
SELECT `user_id`, `username`, `email`, `password`, `user_created`
|
||||
SELECT `user_id`, `remote_id`, `username`, `user_created`
|
||||
FROM `ytkns_users`
|
||||
WHERE `user_id` = :user
|
||||
');
|
||||
|
@ -41,15 +42,13 @@ class User {
|
|||
return $user;
|
||||
}
|
||||
|
||||
public static function forLogin(string $usernameOrEmail): self {
|
||||
public static function byRemoteId(string $remoteId): self {
|
||||
$getUser = DB::prepare('
|
||||
SELECT `user_id`, `username`, `email`, `password`, `user_created`
|
||||
SELECT `user_id`, `remote_id`, `username`, `user_created`
|
||||
FROM `ytkns_users`
|
||||
WHERE LOWER(`username`) = LOWER(:username)
|
||||
OR LOWER(`email`) = LOWER(:email)
|
||||
WHERE `remote_id` = :remote
|
||||
');
|
||||
$getUser->bindValue('username', $usernameOrEmail);
|
||||
$getUser->bindValue('email', $usernameOrEmail);
|
||||
$getUser->bindValue('remote', $remoteId);
|
||||
$getUser->execute();
|
||||
$user = $getUser->fetchObject(self::class);
|
||||
|
||||
|
@ -61,7 +60,7 @@ class User {
|
|||
|
||||
public static function forProfile(string $username): self {
|
||||
$getUser = DB::prepare('
|
||||
SELECT `user_id`, `username`, `email`, `password`, `user_created`
|
||||
SELECT `user_id`, `remote_id`, `username`, `user_created`
|
||||
FROM `ytkns_users`
|
||||
WHERE LOWER(`username`) = LOWER(:username)
|
||||
');
|
||||
|
@ -75,44 +74,16 @@ class User {
|
|||
return $user;
|
||||
}
|
||||
|
||||
public static function validatePassword(string $password): bool {
|
||||
$chars = [];
|
||||
$length = mb_strlen($password);
|
||||
|
||||
for($i = 0; $i < $length; $i++) {
|
||||
$current = mb_substr($password, $i, 1);
|
||||
|
||||
if(!in_array($current, $chars, true))
|
||||
$chars[] = $current;
|
||||
}
|
||||
|
||||
return count($chars) >= 6;
|
||||
}
|
||||
|
||||
public static function hashPassword(string $password): string {
|
||||
return password_hash($password, PASSWORD_ARGON2ID);
|
||||
}
|
||||
|
||||
public static function create(string $userName, string $password, string $email): self {
|
||||
if(!preg_match('#^([a-zA-Z0-9-_]{1,20})$#', $userName))
|
||||
throw new UserCreationInvalidNameException;
|
||||
if(!filter_var($email, FILTER_VALIDATE_EMAIL))
|
||||
throw new UserCreationInvalidMailException;
|
||||
if(!self::validatePassword($password))
|
||||
throw new UserCreationInvalidPasswordException;
|
||||
|
||||
$password = self::hashPassword($password);
|
||||
|
||||
public static function create(string $remoteId, string $userName): self {
|
||||
$createUser = DB::prepare('
|
||||
INSERT INTO `ytkns_users` (
|
||||
`username`, `email`, `password`
|
||||
`remote_id`, `username`
|
||||
) VALUES (
|
||||
:username, :email, :password
|
||||
:remote, :username
|
||||
)
|
||||
');
|
||||
$createUser->bindValue('remote', $remoteId);
|
||||
$createUser->bindValue('username', $userName);
|
||||
$createUser->bindValue('email', $email);
|
||||
$createUser->bindValue('password', $password);
|
||||
$userId = $createUser->execute() ? (int)DB::lastInsertId() : 0;
|
||||
|
||||
try {
|
||||
|
|
|
@ -1,123 +0,0 @@
|
|||
<?php
|
||||
namespace YTKNS;
|
||||
|
||||
use Exception;
|
||||
|
||||
class UserInviteNotFoundException extends Exception {}
|
||||
class UserInviteCreationFailedException extends Exception {}
|
||||
|
||||
#[\AllowDynamicProperties]
|
||||
final class UserInvite {
|
||||
private const TOKEN_LENGTH = 16;
|
||||
|
||||
public function getToken(): string {
|
||||
return $this->invite_token;
|
||||
}
|
||||
|
||||
public function getCreatorId(): int {
|
||||
return $this->created_by ?? 0;
|
||||
}
|
||||
public function getCreator(): User {
|
||||
return User::byId($this->getCreatedById());
|
||||
}
|
||||
|
||||
public function getUserId(): ?int {
|
||||
return $this->used_by ?? null;
|
||||
}
|
||||
public function getUser(): ?User {
|
||||
$userId = $this->getUserId();
|
||||
|
||||
if($userId === null)
|
||||
return null;
|
||||
|
||||
try {
|
||||
return User::byId($this->getUserId());
|
||||
} catch(UserNotFoundException $ex) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public function getCreated(): int {
|
||||
return $this->invite_created ?? 0;
|
||||
}
|
||||
|
||||
public function getUsed(): ?int {
|
||||
return $this->invite_used ?? null;
|
||||
}
|
||||
|
||||
public function isUsed(): bool {
|
||||
return !empty($this->used_by) || !empty($this->invite_used);
|
||||
}
|
||||
|
||||
public function markUsed(User $user): void {
|
||||
$markUsed = DB::prepare('
|
||||
UPDATE `ytkns_users_invites`
|
||||
SET `used_by` = :user
|
||||
WHERE `invite_token` = UNHEX(:token)
|
||||
');
|
||||
$markUsed->bindValue('user', $user->getId());
|
||||
$markUsed->bindValue('token', $this->getToken());
|
||||
$markUsed->execute();
|
||||
}
|
||||
|
||||
public static function fromCreator(User $creator): array {
|
||||
$getInvites = DB::prepare('
|
||||
SELECT `created_by`, `used_by`,
|
||||
LOWER(HEX(`invite_token`)) AS `invite_token`,
|
||||
UNIX_TIMESTAMP(`invite_created`) AS `invite_created`,
|
||||
UNIX_TIMESTAMP(`invite_used`) AS `invite_used`
|
||||
FROM `ytkns_users_invites`
|
||||
WHERE `created_by` = :creator
|
||||
');
|
||||
$getInvites->bindValue('creator', $creator->getId());
|
||||
$getInvites->execute();
|
||||
$invites = [];
|
||||
|
||||
while($invite = $getInvites->fetchObject(self::class))
|
||||
$invites[] = $invite;
|
||||
|
||||
return $invites;
|
||||
}
|
||||
|
||||
public static function byToken(string $token, bool $isHex = true): self {
|
||||
$getInvite = DB::prepare(sprintf('
|
||||
SELECT `created_by`, `used_by`,
|
||||
LOWER(HEX(`invite_token`)) AS `invite_token`,
|
||||
UNIX_TIMESTAMP(`invite_created`) AS `invite_created`,
|
||||
UNIX_TIMESTAMP(`invite_used`) AS `invite_used`
|
||||
FROM `ytkns_users_invites`
|
||||
WHERE `invite_token` = %s(:token)
|
||||
', $isHex ? 'UNHEX' : ''));
|
||||
$getInvite->bindValue('token', $token);
|
||||
$invite = $getInvite->execute() ? $getInvite->fetchObject(self::class) : false;
|
||||
|
||||
if(!$invite)
|
||||
throw new UserInviteNotFoundException;
|
||||
|
||||
return $invite;
|
||||
}
|
||||
|
||||
public static function generateToken(): string {
|
||||
return bin2hex(random_bytes(self::TOKEN_LENGTH));
|
||||
}
|
||||
|
||||
public static function create(User $creator): self {
|
||||
$inviteToken = self::generateToken();
|
||||
$insertInvite = DB::prepare('
|
||||
INSERT INTO `ytkns_users_invites` (
|
||||
`invite_token`, `created_by`
|
||||
) VALUES (
|
||||
UNHEX(:token), :creator
|
||||
)
|
||||
');
|
||||
$insertInvite->bindValue('token', $inviteToken);
|
||||
$insertInvite->bindValue('creator', $creator->getId());
|
||||
$insertInvite->execute();
|
||||
|
||||
try {
|
||||
return self::byToken($inviteToken);
|
||||
} catch(UserInviteNotFoundException $ex) {
|
||||
throw new UserInviteCreationFailedException;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
<label class="auth-field">
|
||||
<div class="auth-field-label">
|
||||
:field_title
|
||||
</div>
|
||||
<div class="auth-field-value">
|
||||
<input type=":field_type" name=":field_name" class="auth-field-value-input" value=":field_value"/>
|
||||
</div>
|
||||
</label>
|
|
@ -1,11 +0,0 @@
|
|||
<h1 class="page-title">Log in</h1>
|
||||
<form action="/auth/login" method="post" class="auth">
|
||||
:auth_error
|
||||
:auth_fields
|
||||
<label class="auth-option">
|
||||
<input type="checkbox" name="remember" class="auth-option-input" :auth_remember/> Stay logged in
|
||||
</label>
|
||||
<div class="auth-buttons">
|
||||
<input type="submit" value="Log in" class="auth-buttons-button"/>
|
||||
</div>
|
||||
</form>
|
|
@ -1,17 +0,0 @@
|
|||
<h1 class="page-title">Log in</h1>
|
||||
<form action="/auth/fid" method="post" class="auth auth-fid">
|
||||
:auth_error
|
||||
<div class="auth-buttons">
|
||||
<input type="submit" value="Log in with Flashii ID" class="auth-buttons-button auth-buttons-button-fid"/>
|
||||
<input type="button" value="Log in with YTKNS details" class="auth-buttons-button" onclick="document.getElementById('_login').classList.remove('hidden');this.classList.add('hidden');" />
|
||||
</div>
|
||||
</form>
|
||||
<form action="/auth/login" method="post" class="auth hidden" id="_login">
|
||||
:auth_fields
|
||||
<label class="auth-option">
|
||||
<input type="checkbox" name="remember" class="auth-option-input" :auth_remember/> Stay logged in
|
||||
</label>
|
||||
<div class="auth-buttons">
|
||||
<input type="submit" value="Log in" class="auth-buttons-button"/>
|
||||
</div>
|
||||
</form>
|
|
@ -1,8 +0,0 @@
|
|||
<h1 class="page-title">Register</h1>
|
||||
<form action="/auth/register" method="post" class="auth">
|
||||
:auth_error
|
||||
:auth_fields
|
||||
<div class="auth-buttons">
|
||||
<input type="submit" value="Create account" class="auth-buttons-button"/>
|
||||
</div>
|
||||
</form>
|
|
@ -1,7 +1,8 @@
|
|||
</div>
|
||||
<div class="footer" id="ytkns-footer">
|
||||
YTKNS by <a href="//flash.moe">Flashwave</a> 2019-:footer_year |
|
||||
<a href="#" onclick="alert(['e&', '.m&o', 'h', 's', 'la', '#&f', 's', 't&kn', 'y', '+&&', 'e', '&m'].reverse().join('&').replace(/&/g, '').replace('#', '@'));">Contact</a> |
|
||||
YTKNS by <a href="//flash.moe">flashwave</a> 2019-:footer_year |
|
||||
<a href="#" onclick="alert(['e&', '.m&o', 'h', 's', 'la', '#&f', 's', 't&kn', 'y'].reverse().join('&').replace(/&/g, '').replace('#', '@'));">Contact</a> |
|
||||
<a href="//patchii.net/flash/ytkns" target="_blank" rel="noopener">Source</a> |
|
||||
Loaded in :footer_took seconds
|
||||
</div>
|
||||
</div>:scripts
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
<form method="post" action="/settings/invites" class="invites-create">
|
||||
<input type="hidden" name="invite_token" value=":create_token"/>
|
||||
<input type="submit" value="Create Invite" class="invites-create-button"/>
|
||||
</form>
|
|
@ -1,14 +0,0 @@
|
|||
<div class="invites-list-item">
|
||||
<div class="invites-list-item-created">
|
||||
:invite_created
|
||||
</div>
|
||||
<div class="invites-list-item-used">
|
||||
:invite_used
|
||||
</div>
|
||||
<div class="invites-list-item-user">
|
||||
:invite_user
|
||||
</div>
|
||||
<div class="invites-list-item-token">
|
||||
<code onclick="navigator.clipboard.writeText('https://ytkns.com/auth/register?inv=:invite_token');" title="Click to copy registration link with invite">:invite_token</code>
|
||||
</div>
|
||||
</div>
|
|
@ -1,22 +0,0 @@
|
|||
<h1 class="page-title">Invites</h1>
|
||||
<div class="invites">
|
||||
:invite_error
|
||||
:invite_create
|
||||
<div class="invites-list">
|
||||
<div class="invites-list-item">
|
||||
<div class="invites-list-item-created">
|
||||
Created on
|
||||
</div>
|
||||
<div class="invites-list-item-used">
|
||||
Used on
|
||||
</div>
|
||||
<div class="invites-list-item-user">
|
||||
Used by
|
||||
</div>
|
||||
<div class="invites-list-item-token">
|
||||
Invitation
|
||||
</div>
|
||||
</div>
|
||||
:invite_list
|
||||
</div>
|
||||
</div>
|
Loading…
Reference in a new issue