polygon-website-foss/my/account.php

401 lines
16 KiB
PHP

<?php require $_SERVER['DOCUMENT_ROOT'].'/api/private/core.php';
use pizzaboxer\ProjectPolygon\Database;
use pizzaboxer\ProjectPolygon\Polygon;
use pizzaboxer\ProjectPolygon\Users;
use pizzaboxer\ProjectPolygon\Discord;
use pizzaboxer\ProjectPolygon\Password;
use pizzaboxer\ProjectPolygon\PageBuilder;
use Sonata\GoogleAuthenticator\GoogleAuthenticator;
Users::RequireLogin();
$pageBuilder = new PageBuilder(["title" => "My Account"]);
$panel = "Settings";
$userinfo = (object)SESSION["user"];
$discordinfo = (object)
[
"info" => NULL,
"key" => $userinfo->discordKey,
"timeVerified" => $userinfo->discordVerifiedTime
];
if ($discordinfo->key == NULL)
{
$discordinfo->key = generateUUID();
Database::singleton()->run(
"UPDATE users SET discordKey = :key WHERE id = :id",
[":key" => $discordinfo->key, ":id" => $userinfo->id]
);
}
else if ($userInfo->discordID != NULL)
{
$discordinfo->info = Discord::GetUserInfo($userinfo->discordID);
}
$gauth = new GoogleAuthenticator();
$twofa = SESSION["user"]["twofa"];
$twofaSecret = $userinfo->twofaSecret;
$sessions = Database::singleton()->run(
"SELECT * FROM sessions WHERE userId = :uid AND valid AND created+157700000 > UNIX_TIMESTAMP() AND lastonline+432000 > UNIX_TIMESTAMP() ORDER BY created DESC",
[":uid" => $userinfo->id]
);
$Fields = (object)
[
"Code" => "",
"Password" => ""
];
$Errors = (object)
[
"Code" => false,
"Password" => false
];
$RequestSent = false;
//2fa stuff is not done via ajax cuz am lazy
if ($_SERVER["REQUEST_METHOD"] == "POST")
{
$RequestSent = true;
$panel = "2FA";
$csrf = $_POST['polygon_csrf'] ?? false;
$Fields->Code = $_POST['code'] ?? "false";
$Fields->Password = $_POST['password'] ?? "false";
$auth = new Password($Fields->Password);
if($csrf != SESSION["csrfToken"]) $Errors->Password = "An unexpected error occurred";
if(!$gauth->checkCode($twofaSecret, $Fields->Code, 1)) $Errors->Code = "Incorrect code";
if(!$auth->verify($userInfo->password)) $Errors->Password = "Incorrect password";
if(!$Errors->Code && !$Errors->Password)
{
$twofa = !SESSION["user"]["twofa"];
Database::singleton()->run(
"UPDATE users SET twofa = :2fa WHERE id = :uid",
[":2fa" => (int)$twofa, ":uid" => SESSION["user"]["id"]]
);
if ($twofa)
{
$recoveryCodes = str_split(bin2hex(random_bytes(60)), 12);
Database::singleton()->run(
"UPDATE users SET twofaRecoveryCodes = :json WHERE id = :uid",
[":json" => json_encode(array_fill_keys($recoveryCodes, true)), ":uid" => SESSION["user"]["id"]]
);
ob_start();
?>
Congratulations! Your account is now more secure. But before you go, there's one last thing:
<br><br>
If you can't get a code from your 2FA app for whatever reason, you can use a 2FA recovery code.
<br><br>
<div class="row" style="max-width: 16rem; margin: auto;">
<?php foreach($recoveryCodes as $code) { ?>
<div class="col-6">
<code><?=$code?></code>
</div>
<?php } ?>
</div>
<br>
These are a set of static, one-time use codes that never expire unless they are used or you disable 2FA. You can use these to get back into your account without the need of a 2FA app.
<br><br>
This is the only time you'll ever see these here, so write them down somewhere <b>now</b>.
<?php
$pageBuilder->showStaticModal([
"header" => "Two-Factor Authentication is active",
"body" => ob_get_clean(),
"buttons" => [["class" => "btn btn-primary", "dismiss" => true, "text" => "I understand"]],
"options" => ["show" => true, "backdrop" => "static"]
]);
}
}
}
if (!$twofa)
{
$twofaSecret = $gauth->generateSecret();
Database::singleton()->run(
"UPDATE users SET twofaSecret = :secret WHERE id = :uid",
[":secret" => $twofaSecret, ":uid" => SESSION["user"]["id"]]
);
}
$pageBuilder->buildHeader();
?>
<h2 class="font-weight-normal">My Account</h2>
<div class="row pt-2">
<div class="col-lg-2 col-md-3 pl-3 pr-md-0 pb-3 divider-right">
<ul class="nav nav-tabs flex-column" id="generalTabs" role="tablist">
<li class="nav-item">
<a class="nav-link<?=$panel=="Settings"?" active":""?>" id="settings-tab" data-toggle="tab" href="#settings" role="tab" aria-controls="settings" aria-selected="true">Settings</a>
</li>
<li class="nav-item">
<a class="nav-link" id="discord-tab" data-toggle="tab" href="#discord" role="tab" aria-controls="discord" aria-selected="false">Discord</a>
</li>
<li class="nav-item">
<a class="nav-link" id="changepwd-tab" data-toggle="tab" href="#changepwd" role="tab" aria-controls="changepwd" aria-selected="true">Password</a>
</li>
<li class="nav-item">
<a class="nav-link<?=$panel=="2FA"?" active":""?>" id="twofa-tab" data-toggle="tab" href="#twofa" role="tab" aria-controls="twofa" aria-selected="false">2FA</a>
</li>
<li class="nav-item">
<a class="nav-link" id="sessions-tab" data-toggle="tab" href="#sessions" role="tab" aria-controls="sessions" aria-selected="false">Sessions</a>
</li>
<li class="nav-item">
<a class="nav-link" id="data-tab" data-toggle="tab" href="#data" role="tab" aria-controls="data" aria-selected="false">Data</a>
</li>
</ul>
</div>
<div class="col-lg-10 col-md-9 px-3">
<div id="generalTabsContent" class="tab-content">
<div class="tab-pane<?=$panel=="Settings"?" active":""?>" id="settings" role="tabpanel" aria-labelledby="settings-tab" style="max-width:36rem;">
<div class="form-group row">
<label for="blurb" class="col-sm-3 col-form-label">Blurb</label>
<div class="col-sm-9">
<textarea type="text" class="form-control form-control-sm" id="blurb"><?=$userinfo->blurb?></textarea>
<small class="text-muted">1000 characters max</small>
</div>
</div>
<div class="form-group row">
<label for="blurb" class="col-sm-3 col-form-label">Theme</label>
<div class="col-sm-9">
<select class="form-control form-control-sm" id="theme">
<option value="light">Light</option>
<option value="dark"<?=$userInfo->theme == "dark"?' selected="selected"':''?>>Dark</option>
<option value="2014"<?=$userInfo->theme == "2014"?' selected="selected"':''?>>2014</option>
<option value="hitius"<?=$userInfo->theme == "hitius"?' selected="selected"':''?>>???</option>
</select>
<small class="text-muted">Note: the 2014 theme has some visual style issues</small>
</div>
</div>
<div class="form-group row">
<div class="col-sm-3">Filter</div>
<div class="col-sm-9">
<select class="form-control form-control-sm" id="filter">
<option value="enabled"<?=$userInfo->filter ? ' selected="selected"' : ''?>>Enabled</option>
<option value="disabled"<?=!$userInfo->filter ? ' selected="selected"' : ''?>>Disabled</option>
</select>
</div>
</div>
<button class="btn btn-primary update-settings"><span class="spinner-border spinner-border-sm d-none"></span> Update Account Settings</button>
</div>
<div class="tab-pane" id="discord" role="tabpanel" aria-labelledby="discord-tab">
<div class="row" style="padding-left:7px;padding-right:7px;">
<div class="col px-2">
<?php if ($discordinfo->info == NULL) { ?>
<h2 class="font-weight-normal">Looks like you're not yet verified.</h2>
<p class="mb-1">If you haven't joined the Discord server yet, join via the widget over by the side.</p>
<p class="mb-1">Once you join, the verification bot should DM you asking for your key, which is here:</p>
<code class="mb-1">PolygonVerify:<?=$discordinfo->key?></code>
<p class="mb-1">Copy and send this to the bot in DMs, and you'll be in!</p>
<p>If the bot hasn't DMed you, it may be down. When it comes back online, just send a DM to the bot with your key.</p>
<?php } else { ?>
<div class="card bg-light mb-3">
<div class="card-body">
<div class="row">
<div class="col-3">
<img class="img-fluid rounded-circle" src="<?=$discordinfo->info->avatar?>">
</div>
<div class="col-9">
<h4 class="font-weight-normal"><?=$discordinfo->info->username?><span class="text-muted">#<?=$discordinfo->info->tag?></span></h4>
<h5 class="font-weight-normal">Verified <?=GetReadableTime($discordinfo->timeVerified)?></h5>
<small class="text-muted">ID <?=SESSION["user"]["discordID"]?></small>
</div>
</div>
</div>
</div>
<p>If you wish to have your Discord account unverified so you can use another account, message an admin.</p>
<?php } ?>
</div>
<div class="col-lg-auto col-sm-12 px-2">
<iframe class="w-100" src="https://discord.com/widget?id=754743899200684192&theme=<?=SESSION["user"]["theme"] == "dark" ? "dark" : "light"?>&username=<?=SESSION["user"]["username"]?>" width="350" height="500" allowtransparency="true" frameborder="0" sandbox="allow-popups allow-popups-to-escape-sandbox allow-same-origin allow-scripts"></iframe>
</div>
</div>
</div>
<div class="tab-pane" id="changepwd" role="tabpanel" aria-labelledby="changepwd-tab" style="max-width:36rem;">
<div class="form-group row">
<label for="currentpwd" class="col-sm-4 col-form-label">Current Password</label>
<div class="col-sm-8">
<input type="password" class="form-control" id="currentpwd">
</div>
</div>
<div class="form-group row">
<label for="newpwd" class="col-sm-4 col-form-label">New Password</label>
<div class="col-sm-8">
<input type="password" class="form-control" id="newpwd">
<small class="form-text text-muted">8 - 64 characters, must have at least 6 characters and 2 numbers</small>
</div>
</div>
<div class="form-group row">
<label for="newpwd" class="col-sm-4 col-form-label">Confirm Password</label>
<div class="col-sm-8">
<input type="password" class="form-control" id="confnewpwd">
</div>
</div>
<button class="btn btn-warning update-password"><span class="spinner-border spinner-border-sm d-none"></span> Change Password</button>
</div>
<div class="tab-pane<?=$panel=="2FA"?" active":""?>" id="twofa" role="tabpanel" aria-labelledby="twofa-tab" style="max-width:36rem;">
<?php if($twofa) { ?>
<p>Two-Factor Authentication is currently active. If you wish to disable it, just fill in the fields below.</p>
<p>Keep in mind that disabling 2FA will invalidate your recovery codes. If you re-enable it, use the new ones it gives you.</p>
<?php } else { ?>
<p>Use a two-factor authentication app that has backups (Authy is a good one), so you don't have to worry about being unable to log in if you lose your device.</p>
<div class="row">
<div class="col-lg-5 text-center">
<img class="img-fluid pb-3" style="display:inline" src="<?=Sonata\GoogleAuthenticator\GoogleQrUrl::generate(SESSION["user"]["username"], $twofaSecret, "Project Polygon")?>">
</div>
<div class="col-lg-7">
<p>Scan the QR code with your authenticator app of choice.</p>
<p>This changes with every page refresh, so be careful.</p>
<p>There's also a manual key here if you prefer that: <code><?=$twofaSecret?></code></p>
</div>
</div>
<?php } ?>
<form method="post">
<div class="form-group row">
<label for="code" class="col-sm-3 col-form-label">2FA Code:</label>
<div class="col-sm-9">
<input type="number" max="99999999" class="form-control" name="code" id="code" placeholder="00000000 - get this from your 2FA app" value="<?=htmlspecialchars($Fields->Code)?>">
<?php if($Errors->Code != false) { ?><small class="text-danger"><?=$Errors->Code?></small><?php } ?>
</div>
</div>
<div class="form-group row">
<label for="password" class="col-sm-3 col-form-label">Password:</label>
<div class="col-sm-9">
<input type="password" class="form-control" name="password" id="password" placeholder="just your normal account password" value="<?=htmlspecialchars($Fields->Password)?>">
<?php if($Errors->Password != false) { ?><small class="text-danger"><?=$Errors->Password?></small><?php } ?>
</div>
</div>
<input type="hidden" name="polygon_csrf" value="<?=SESSION["csrfToken"]?>">
<input type="hidden" name="twofa_state" value="<?=$twofa?>">
<button type="submit" name="2fa" class="btn btn-<?=$twofa?'danger':'success'?> toggle-2fa"><span class="spinner-border spinner-border-sm d-none"></span> <?=$twofa?'Dis':'En'?>able Two-Factor Authentication</button>
</form>
</div>
<div class="tab-pane" id="sessions" role="tabpanel" aria-labelledby="sessions-tab">
<div class="row" style="padding-left:7px;padding-right:7px;">
<?php while($session = $sessions->fetch(\PDO::FETCH_OBJ)) { /* $ipInfo = Polygon::getIpInfo($session->loginIp); */ $browserInfo = get_browser($session->userAgent); ?>
<div class="col-xl-3 col-lg-4 col-md-6 mb-3 px-2 <?=$session->sessionKey == $_COOKIE['polygon_session']?'current-session':'session'?>">
<div class="card">
<div class="card-body px-0 text-center">
<h5 class="font-weight-normal"><?=$session->IsGameClient ? "Polygon Game Client" : "{$browserInfo->device_type} / {$browserInfo->browser}"?></h5>
<?=$session->IsGameClient ? htmlspecialchars($session->userAgent) : $browserInfo->platform?><br>
Started <span title="<?=date('j/n/Y h:i:s A', $session->created)?>"><?=timeSince("@".$session->created)?></span><br>
<?php if($session->sessionKey == $_COOKIE['polygon_session']){ ?><span class="text-success">This is your current session</span><?php } else { ?>
Last seen <span title="<?=date('j/n/Y h:i:s A', $session->lastonline)?>"><?=timeSince("@".$session->lastonline)?></span><?php } ?>
<br>
</div>
</div>
</div>
<?php } ?>
</div>
<button class="btn btn-danger destroy-sessions"><span class="spinner-border spinner-border-sm d-none"></span> Invalidate all other sessions</button>
</div>
<div class="tab-pane" id="data" role="tabpanel" aria-labelledby="data-tab">
<h2>Data</h2>
<p>If there's any data you wish to download and archive, you can do so here.</p>
<p>All forum data and archived Roblox audio assets will be publicly released in the Discord server.</p>
<div class="input-group data-select-wrapper" style="max-width:20rem;">
<select class="form-control form-control-sm data-select">
<option disabled selected>Select what to download...</option>
<option value="friends">Friends</option>
<option value="groups">Owned Groups</option>
<option value="inventory">Inventory</option>
<option value="transactions">Transactions</option>
</select>
<div class="input-group-append">
<a class="btn btn-sm btn-success disabled data-select-download" href="#"><i class="far fa-download"></i> Download</a>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
//account.js
$('.update-settings').click(function()
{
var button = this;
var options =
{
"blurb": $("#blurb").val(),
"theme": $("#theme").val(),
"filter": $("#filter").val()
};
polygon.button.busy(button);
$.post('/api/account/update-settings', options, function(data)
{
if(data.success){ toastr["success"]("Your settings have been updated"); }
else{ toastr["error"](data.message); }
polygon.button.active(button);
});
});
$('.update-password').click(function()
{
var button = this;
var options =
{
"currentpwd": $("#currentpwd").val(),
"newpwd": $("#newpwd").val(),
"confnewpwd": $("#confnewpwd").val()
};
polygon.button.busy(button);
$.post('/api/account/update-password', options, function(data)
{
if(data.success){ toastr["success"]("Your password has been updated"); }
else{ toastr["error"](data.message); }
polygon.button.active(button);
});
});
$('.destroy-sessions').click(function()
{
var button = this;
polygon.button.busy(button);
$.post('/api/account/destroy-sessions', function(data)
{
if(data.success)
{
toastr["success"](data.message);
$(".session").fadeOut(500);
}
else
{
toastr["error"](data.message);
}
polygon.button.active(button);
});
});
$('.toggle-2fa').click(function(event)
{
event.preventDefault();
polygon.button.busy(this);
$("form").submit();
});
$(".data-select").change(function() {
data = $(this).val();
$(".data-select-download").attr("href", "/api/account/data/" + data).removeClass("disabled");
});
</script>
<?php $pageBuilder->buildFooter(); ?>