mirror of
git://f0xx.org/ac/ac-be-auth
synced 2026-07-29 04:58:42 +03:00
initial
This commit is contained in:
3
README.md
Normal file
3
README.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# ac-be-auth
|
||||||
|
|
||||||
|
Thin BE UI. Remote: `git://f0xx.org/ac/ac-be-auth`
|
||||||
19
composer.json
Normal file
19
composer.json
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"name": "androidcast/be-auth",
|
||||||
|
"description": "Login/register shell",
|
||||||
|
"require": {
|
||||||
|
"php": ">=8.1",
|
||||||
|
"androidcast/platform-php": "dev-next",
|
||||||
|
"androidcast/platform-web": "dev-next"
|
||||||
|
},
|
||||||
|
"repositories": [
|
||||||
|
{
|
||||||
|
"type": "vcs",
|
||||||
|
"url": "git://f0xx.org/ac/ac-platform-php"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "vcs",
|
||||||
|
"url": "git://f0xx.org/ac/ac-platform-web"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
66
public/assets/js/two_factor.js
Normal file
66
public/assets/js/two_factor.js
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
(function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
function boot() {
|
||||||
|
var form = document.querySelector('form.login-card');
|
||||||
|
var input = document.getElementById('twofa-code-input');
|
||||||
|
if (!form || !input) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
input.focus();
|
||||||
|
input.select();
|
||||||
|
|
||||||
|
function digitsOnly(val) {
|
||||||
|
return String(val || '').replace(/\D/g, '').slice(0, 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
function maybeSubmit() {
|
||||||
|
if (input.value.length === 6) {
|
||||||
|
if (typeof form.requestSubmit === 'function') {
|
||||||
|
form.requestSubmit();
|
||||||
|
} else {
|
||||||
|
form.submit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
input.addEventListener('input', function () {
|
||||||
|
var next = digitsOnly(input.value);
|
||||||
|
if (next !== input.value) {
|
||||||
|
input.value = next;
|
||||||
|
}
|
||||||
|
maybeSubmit();
|
||||||
|
});
|
||||||
|
|
||||||
|
input.addEventListener('paste', function (ev) {
|
||||||
|
ev.preventDefault();
|
||||||
|
var text = (ev.clipboardData && ev.clipboardData.getData('text')) || '';
|
||||||
|
input.value = digitsOnly(text);
|
||||||
|
maybeSubmit();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('keydown', function (ev) {
|
||||||
|
if (!/^[0-9]$/.test(ev.key)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var ae = document.activeElement;
|
||||||
|
if (ae === input) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var tag = ae && ae.tagName ? ae.tagName.toLowerCase() : '';
|
||||||
|
if (tag === 'input' || tag === 'textarea' || tag === 'select' || (ae && ae.isContentEditable)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ev.preventDefault();
|
||||||
|
input.focus();
|
||||||
|
input.value = digitsOnly(input.value + ev.key);
|
||||||
|
maybeSubmit();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', boot);
|
||||||
|
} else {
|
||||||
|
boot();
|
||||||
|
}
|
||||||
|
})();
|
||||||
2
public/index.php
Normal file
2
public/index.php
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
<?php
|
||||||
|
require __DIR__ . '/../views/layout.php';
|
||||||
69
views/account_security.php
Normal file
69
views/account_security.php
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
<?php
|
||||||
|
$bp = Auth::basePath();
|
||||||
|
$user = Auth::user();
|
||||||
|
$hasTotp = AuthFactors::hasTotp((int) ($user['id'] ?? 0));
|
||||||
|
$issuer = (string) cfg('app_name', 'Android Cast');
|
||||||
|
$account = (string) ($user['username'] ?? '');
|
||||||
|
$otpUri = $totpSecret ? AuthTotp::provisioningUri($totpSecret, $account, $issuer) : '';
|
||||||
|
$qrUrl = $otpUri !== '' ? AuthTotp::qrImageUrl($otpUri) : '';
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Account security — <?= h(cfg('app_name')) ?></title>
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
var l = localStorage.getItem('crash_console_lang');
|
||||||
|
if (l === 'en' || l === 'ru') document.documentElement.setAttribute('lang', l);
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
<link rel="stylesheet" href="<?= h($bp) ?>/assets/css/app.css">
|
||||||
|
<script src="<?= h($bp) ?>/assets/js/i18n.js" defer></script>
|
||||||
|
</head>
|
||||||
|
<body class="login-page" data-base-path="<?= h($bp) ?>">
|
||||||
|
<div class="login-card" style="max-width:28rem">
|
||||||
|
<h1 data-i18n="security.title">Account security</h1>
|
||||||
|
<p class="muted"><?= h($account) ?></p>
|
||||||
|
<?php if (!empty($securityError)): ?>
|
||||||
|
<div class="alert"><?= h($securityError) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if (!empty($securitySuccess)): ?>
|
||||||
|
<div class="alert alert--ok"><?= h($securitySuccess) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ($hasTotp): ?>
|
||||||
|
<p data-i18n="security.totp_enabled">Authenticator app is enrolled.</p>
|
||||||
|
<form method="post" action="<?= h($bp) ?>/account-security" onsubmit="return confirm('Remove authenticator?');">
|
||||||
|
<input type="hidden" name="action" value="remove_totp">
|
||||||
|
<button type="submit" class="btn btn--secondary" data-i18n="security.remove_totp">Remove authenticator</button>
|
||||||
|
</form>
|
||||||
|
<?php elseif ($totpSecret): ?>
|
||||||
|
<p data-i18n="security.scan_qr">Scan this QR code with your authenticator app, then enter a code to confirm.</p>
|
||||||
|
<?php if ($qrUrl): ?>
|
||||||
|
<p style="text-align:center"><img src="<?= h($qrUrl) ?>" width="200" height="200" alt="TOTP QR" loading="lazy"></p>
|
||||||
|
<?php endif; ?>
|
||||||
|
<details class="hint">
|
||||||
|
<summary data-i18n="security.manual_secret">Can’t scan?</summary>
|
||||||
|
<code style="word-break:break-all"><?= h($totpSecret) ?></code>
|
||||||
|
</details>
|
||||||
|
<form method="post" action="<?= h($bp) ?>/account-security">
|
||||||
|
<input type="hidden" name="action" value="confirm_totp">
|
||||||
|
<label><span data-i18n="twofa.code">Authentication code</span>
|
||||||
|
<input name="code" inputmode="numeric" pattern="[0-9]{6}" maxlength="6" required></label>
|
||||||
|
<button type="submit" data-i18n="security.confirm_totp">Confirm enrollment</button>
|
||||||
|
</form>
|
||||||
|
<?php else: ?>
|
||||||
|
<p data-i18n="security.totp_intro">Protect your account with a 6-digit authenticator app (recommended for alpha).</p>
|
||||||
|
<form method="post" action="<?= h($bp) ?>/account-security">
|
||||||
|
<input type="hidden" name="action" value="start_totp">
|
||||||
|
<button type="submit" data-i18n="security.start_totp">Set up authenticator</button>
|
||||||
|
</form>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<p class="hint" style="margin-top:1.5rem"><a href="<?= h($bp) ?>/" data-i18n="security.back_console">Back to console</a></p>
|
||||||
|
</div>
|
||||||
|
<?php platform_render_footer(); ?>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
58
views/login.php
Normal file
58
views/login.php
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
<?php
|
||||||
|
/*
|
||||||
|
* package examples/crash_reporter/backend/views/login.php
|
||||||
|
* login.php
|
||||||
|
* Created at: Wed 20 May 2026 14:31:55 +0200
|
||||||
|
* Updated at: Wed 20 May 2026 15:17:13 +0200 by Anton Afanasyeu <a.afanasieff@gmail.com>
|
||||||
|
* Commit: 5d8e82d2e60a21fff3138d2a394ee4e8b4c6dcb8
|
||||||
|
* Contributors:
|
||||||
|
* - Anton Afanasyeu <a.afanasieff@gmail.com> (2 commits, 27 lines)
|
||||||
|
* - Cursor Agent (project assistant)
|
||||||
|
* Digest: SHA256 9bef983f4d34cb790922cda355ccf5d14b565b29273d2ebfe48b3173cd2e8f9f
|
||||||
|
*/
|
||||||
|
$bp = Auth::basePath();
|
||||||
|
$auth = Auth::authUrl();
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en" data-theme="dark">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Login — <?= h(cfg('app_name')) ?></title>
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
var t = localStorage.getItem('crash_console_theme');
|
||||||
|
if (t === 'light' || t === 'dark') document.documentElement.setAttribute('data-theme', t);
|
||||||
|
var l = localStorage.getItem('crash_console_lang');
|
||||||
|
if (l === 'en' || l === 'ru') document.documentElement.setAttribute('lang', l);
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
<link rel="stylesheet" href="<?= h($bp) ?>/assets/css/app.css">
|
||||||
|
<script src="<?= h($bp) ?>/assets/js/i18n.js" defer></script>
|
||||||
|
<?php AnalyticsHead::render('crashes_login'); ?>
|
||||||
|
</head>
|
||||||
|
<body class="login-page" data-base-path="<?= h($bp) ?>">
|
||||||
|
<div class="login-locale">
|
||||||
|
<label class="toolbar-select locale-toolbar-select" data-i18n-title="lang.label" title="Language">
|
||||||
|
<span class="locale-flag" aria-hidden="true">🇬🇧</span>
|
||||||
|
<select class="lang-select" data-i18n-aria="lang.label" aria-label="Language">
|
||||||
|
<option value="en">EN</option>
|
||||||
|
<option value="ru">RU</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<form class="login-card" method="post" action="<?= h($auth) ?>/login">
|
||||||
|
<h1><?= h(cfg('app_name')) ?></h1>
|
||||||
|
<p class="muted" data-i18n="login.sign_in_hint">Sign in to view anonymous issue reports</p>
|
||||||
|
<?php if (!empty($loginError)): ?>
|
||||||
|
<div class="alert" data-i18n="login.error"><?= h($loginError) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<label><span data-i18n="login.username">Username</span><input name="username" autocomplete="username" required></label>
|
||||||
|
<label><span data-i18n="login.password">Password</span><input name="password" type="password" autocomplete="current-password" required></label>
|
||||||
|
<button type="submit" data-i18n="login.submit">Sign in</button>
|
||||||
|
<p class="hint" data-i18n="login.hint_default">Default: admin / admin</p>
|
||||||
|
<p class="hint"><a href="<?= h($auth) ?>/register" data-i18n="login.register">Register</a></p>
|
||||||
|
</form>
|
||||||
|
<?php platform_render_footer(); ?>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
58
views/partials/console_home_landing.php
Normal file
58
views/partials/console_home_landing.php
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
$bp = Auth::basePath();
|
||||||
|
$proj = '/app/androidcast_project';
|
||||||
|
?>
|
||||||
|
<div class="console-home">
|
||||||
|
<div class="toolbar reports-toolbar console-home-toolbar">
|
||||||
|
<h1 data-i18n="home.title">Console</h1>
|
||||||
|
<div class="toolbar-actions">
|
||||||
|
<label class="toolbar-select">
|
||||||
|
<span data-i18n="theme.label">Theme</span>
|
||||||
|
<select id="theme-select" aria-label="UI theme" data-i18n-options="theme">
|
||||||
|
<option value="dark" data-i18n="theme.dark">Dark</option>
|
||||||
|
<option value="light" data-i18n="theme.light">Light</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="console-home-intro" data-i18n="home.intro">Issue triage, tickets, and session metrics — pick a workspace below.</p>
|
||||||
|
|
||||||
|
<div class="cards cards--lift console-home-cards">
|
||||||
|
<a href="<?= h($bp) ?>/?view=reports" class="card card--lift console-home-card">
|
||||||
|
<span class="nav-icon nav-icon--reports" aria-hidden="true"></span>
|
||||||
|
<h2 data-i18n="home.card_issues">Issues</h2>
|
||||||
|
<p class="muted" data-i18n="home.card_issues_desc">Browse and triage issues, tags, and fingerprints.</p>
|
||||||
|
</a>
|
||||||
|
<button type="button" class="card card--lift console-home-card console-home-card--btn js-new-issue-btn">
|
||||||
|
<span class="nav-icon nav-icon--reports" aria-hidden="true"></span>
|
||||||
|
<h2 data-i18n="issues.new">New issue</h2>
|
||||||
|
<p class="muted" data-i18n="home.card_new_issue_desc">Log a new issue or task in one step.</p>
|
||||||
|
</button>
|
||||||
|
<a href="<?= h($bp) ?>/?view=tickets" class="card card--lift console-home-card">
|
||||||
|
<span class="nav-icon nav-icon--tickets" aria-hidden="true"></span>
|
||||||
|
<h2 data-i18n="nav.tickets">Tickets</h2>
|
||||||
|
<p class="muted" data-i18n="home.card_tickets_desc">Roadmap tasks, QA items, and workflow tags.</p>
|
||||||
|
</a>
|
||||||
|
<button type="button" class="card card--lift console-home-card console-home-card--btn js-new-ticket-btn">
|
||||||
|
<span class="nav-icon nav-icon--tickets" aria-hidden="true"></span>
|
||||||
|
<h2 data-i18n="tickets.new">New ticket</h2>
|
||||||
|
<p class="muted" data-i18n="home.card_new_ticket_desc">Create a roadmap or QA ticket in one step.</p>
|
||||||
|
</button>
|
||||||
|
<a href="<?= h($proj) ?>/graphs/" class="card card--lift console-home-card">
|
||||||
|
<span class="nav-icon nav-icon--graphs" aria-hidden="true"></span>
|
||||||
|
<h2 data-i18n="home.card_graphs">Graphs</h2>
|
||||||
|
<p class="muted" data-i18n="home.card_graphs_desc">Sessions, issues, and device activity over time.</p>
|
||||||
|
</a>
|
||||||
|
<?php if (Rbac::can('short_links_view')): ?>
|
||||||
|
<a href="<?= h($bp) ?>/?view=short_links" class="card card--lift console-home-card">
|
||||||
|
<span class="nav-icon nav-icon--link" aria-hidden="true"></span>
|
||||||
|
<h2 data-i18n="home.card_short_links">Short links</h2>
|
||||||
|
<p class="muted" data-i18n="home.card_short_links_desc">Mint bearer tokens and shorten URLs for s.f0xx.org.</p>
|
||||||
|
</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 class="console-home-more" data-i18n="home.more">All services</h2>
|
||||||
|
<?php $skip_home_link = true; require __DIR__ . '/console_quick_links.php'; ?>
|
||||||
|
</div>
|
||||||
37
views/partials/console_quick_links.php
Normal file
37
views/partials/console_quick_links.php
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
/**
|
||||||
|
* Horizontal service links with the same icons as the left nav (8px gap).
|
||||||
|
* @var string|null $extra_class optional class on <nav>
|
||||||
|
* @var bool $skip_home_link omit "Console home" (use on home view)
|
||||||
|
*/
|
||||||
|
$bp = Auth::basePath();
|
||||||
|
$proj = '/app/androidcast_project';
|
||||||
|
$navClass = 'console-quick-links graphs-quick-links' . (isset($extra_class) ? ' ' . $extra_class : '');
|
||||||
|
$items = [];
|
||||||
|
if (empty($skip_home_link)) {
|
||||||
|
$items[] = ['Console home', $bp . '/?view=home', 'nav-icon--home'];
|
||||||
|
}
|
||||||
|
$items[] = ['Issues', $bp . '/?view=reports', 'nav-icon--reports'];
|
||||||
|
$items[] = ['Tickets', $bp . '/?view=tickets', 'nav-icon--tickets'];
|
||||||
|
if (Rbac::can('remote_access_view')) {
|
||||||
|
$items[] = ['Remote access', $bp . '/?view=remote_access', 'nav-icon--remote'];
|
||||||
|
}
|
||||||
|
if (Rbac::can('short_links_view')) {
|
||||||
|
$items[] = ['Short links', $bp . '/?view=short_links', 'nav-icon--link'];
|
||||||
|
}
|
||||||
|
if (Rbac::canManageRbac()) {
|
||||||
|
$items[] = ['Access control', $bp . '/?view=rbac', 'nav-icon--tickets'];
|
||||||
|
}
|
||||||
|
$items[] = ['Graphs', $proj . '/graphs/', 'nav-icon--graphs'];
|
||||||
|
$items[] = ['Builder', $proj . '/build/', 'nav-icon--builder'];
|
||||||
|
$items[] = ['Hub', $proj . '/', 'nav-icon--home'];
|
||||||
|
?>
|
||||||
|
<nav class="<?= h($navClass) ?>" aria-label="Related services">
|
||||||
|
<?php foreach ($items as [$label, $href, $icon]): ?>
|
||||||
|
<a href="<?= h($href) ?>" class="console-quick-link">
|
||||||
|
<span class="nav-icon <?= h($icon) ?>" aria-hidden="true"></span>
|
||||||
|
<span><?= h($label) ?></span>
|
||||||
|
</a>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</nav>
|
||||||
15
views/partials/cookie_consent.php
Normal file
15
views/partials/cookie_consent.php
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<?php declare(strict_types=1); ?>
|
||||||
|
<div id="cookie-consent-banner" class="cookie-consent" hidden role="dialog" aria-labelledby="cookie-consent-title" aria-modal="false">
|
||||||
|
<div class="cookie-consent-inner">
|
||||||
|
<p id="cookie-consent-title" class="cookie-consent-title">Cookies on AndroidCast</p>
|
||||||
|
<p class="cookie-consent-text muted">
|
||||||
|
We use necessary session cookies to sign you in. With your consent we also load analytics to improve the service.
|
||||||
|
You can change this later in your browser by clearing site data.
|
||||||
|
</p>
|
||||||
|
<div class="cookie-consent-actions">
|
||||||
|
<button type="button" class="btn btn--primary" id="cookie-consent-all">Accept all</button>
|
||||||
|
<button type="button" class="btn" id="cookie-consent-necessary">Necessary only</button>
|
||||||
|
<button type="button" class="btn btn--ghost" id="cookie-consent-reject">Reject optional</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
53
views/register.php
Normal file
53
views/register.php
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
<?php
|
||||||
|
$bp = Auth::basePath();
|
||||||
|
$auth = Auth::authUrl();
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en" data-theme="dark">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Register — <?= h(cfg('app_name')) ?></title>
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
var t = localStorage.getItem('crash_console_theme');
|
||||||
|
if (t === 'light' || t === 'dark') document.documentElement.setAttribute('data-theme', t);
|
||||||
|
var l = localStorage.getItem('crash_console_lang');
|
||||||
|
if (l === 'en' || l === 'ru') document.documentElement.setAttribute('lang', l);
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
<link rel="stylesheet" href="<?= h($bp) ?>/assets/css/app.css">
|
||||||
|
<script src="<?= h($bp) ?>/assets/js/i18n.js" defer></script>
|
||||||
|
<?php AnalyticsHead::render('crashes_register'); ?>
|
||||||
|
</head>
|
||||||
|
<body class="login-page" data-base-path="<?= h($bp) ?>">
|
||||||
|
<div class="login-locale">
|
||||||
|
<label class="toolbar-select locale-toolbar-select" data-i18n-title="lang.label" title="Language">
|
||||||
|
<span class="locale-flag" aria-hidden="true">🇬🇧</span>
|
||||||
|
<select class="lang-select" data-i18n-aria="lang.label" aria-label="Language">
|
||||||
|
<option value="en">EN</option>
|
||||||
|
<option value="ru">RU</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<form class="login-card" method="post" action="<?= h($auth) ?>/register">
|
||||||
|
<h1 data-i18n="register.title">Create account</h1>
|
||||||
|
<p class="muted" data-i18n="register.hint">We will email a verification link before you can sign in.</p>
|
||||||
|
<?php if (!empty($registerError)): ?>
|
||||||
|
<div class="alert"><?= h($registerError) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if (!empty($registerSuccess)): ?>
|
||||||
|
<div class="alert alert--ok"><?= h($registerSuccess) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<label><span data-i18n="register.email">Email</span>
|
||||||
|
<input name="email" type="email" autocomplete="email" required value="<?= h($registerEmail ?? '') ?>"></label>
|
||||||
|
<label><span data-i18n="register.username">Username</span>
|
||||||
|
<input name="username" autocomplete="username" value="<?= h($registerUsername ?? '') ?>"></label>
|
||||||
|
<label><span data-i18n="register.password">Password</span>
|
||||||
|
<input name="password" type="password" autocomplete="new-password" minlength="10" required></label>
|
||||||
|
<button type="submit" data-i18n="register.submit">Register</button>
|
||||||
|
<p class="hint"><a href="<?= h($auth) ?>/login" data-i18n="register.back_login">Back to sign in</a></p>
|
||||||
|
</form>
|
||||||
|
<?php platform_render_footer(); ?>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
53
views/two_factor_challenge.php
Normal file
53
views/two_factor_challenge.php
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
<?php
|
||||||
|
$bp = Auth::basePath();
|
||||||
|
$auth = Auth::authUrl();
|
||||||
|
$twofaQr = AuthTwoFactorPage::qrPayload();
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en" data-theme="dark">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Two-factor — <?= h(cfg('app_name')) ?></title>
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
var t = localStorage.getItem('crash_console_theme');
|
||||||
|
if (t === 'light' || t === 'dark') document.documentElement.setAttribute('data-theme', t);
|
||||||
|
var l = localStorage.getItem('crash_console_lang');
|
||||||
|
if (l === 'en' || l === 'ru') document.documentElement.setAttribute('lang', l);
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
<link rel="stylesheet" href="<?= h($bp) ?>/assets/css/app.css">
|
||||||
|
<script src="<?= h($bp) ?>/assets/js/i18n.js" defer></script>
|
||||||
|
<script src="<?= h($bp) ?>/assets/js/two_factor.js" defer></script>
|
||||||
|
</head>
|
||||||
|
<body class="login-page" data-base-path="<?= h($bp) ?>">
|
||||||
|
<div class="login-locale">
|
||||||
|
<label class="toolbar-select locale-toolbar-select" data-i18n-title="lang.label" title="Language">
|
||||||
|
<span class="locale-flag" aria-hidden="true">🇬🇧</span>
|
||||||
|
<select class="lang-select" data-i18n-aria="lang.label" aria-label="Language">
|
||||||
|
<option value="en">EN</option>
|
||||||
|
<option value="ru">RU</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<form class="login-card" method="post" action="<?= h($auth) ?>/two-factor" id="twofa-form">
|
||||||
|
<h1 data-i18n="twofa.title">Two-factor authentication</h1>
|
||||||
|
<p class="muted" data-i18n="twofa.hint">Enter the 6-digit code from your authenticator app.</p>
|
||||||
|
<?php if (!empty($twofaError)): ?>
|
||||||
|
<div class="alert" data-i18n="twofa.error"><?= h($twofaError) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if (is_array($twofaQr) && !empty($twofaQr['qr_url'])): ?>
|
||||||
|
<figure class="twofa-qr-wrap">
|
||||||
|
<img src="<?= h((string) $twofaQr['qr_url']) ?>" width="160" height="160" alt="QR code for mobile sign-in link" class="twofa-qr">
|
||||||
|
<figcaption class="muted">Scan to open the hub on your phone<?php if (!empty($twofaQr['short_url'])): ?> · <a href="<?= h((string) $twofaQr['short_url']) ?>" rel="noopener"><?= h((string) $twofaQr['short_url']) ?></a><?php endif; ?></figcaption>
|
||||||
|
</figure>
|
||||||
|
<?php endif; ?>
|
||||||
|
<label><span data-i18n="twofa.code">Authentication code</span>
|
||||||
|
<input id="twofa-code-input" name="code" inputmode="numeric" pattern="[0-9]{6}" maxlength="6" autocomplete="one-time-code" required autofocus></label>
|
||||||
|
<button type="submit" data-i18n="twofa.submit">Continue</button>
|
||||||
|
<p class="hint"><a href="<?= h($auth) ?>/login" data-i18n="twofa.cancel">Cancel</a></p>
|
||||||
|
</form>
|
||||||
|
<?php platform_render_footer(); ?>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
33
views/verify_email.php
Normal file
33
views/verify_email.php
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
$bp = Auth::basePath();
|
||||||
|
$auth = Auth::authUrl();
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Verify email — <?= h(cfg('app_name')) ?></title>
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
var l = localStorage.getItem('crash_console_lang');
|
||||||
|
if (l === 'en' || l === 'ru') document.documentElement.setAttribute('lang', l);
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
<link rel="stylesheet" href="<?= h($bp) ?>/assets/css/app.css">
|
||||||
|
<script src="<?= h($bp) ?>/assets/js/i18n.js" defer></script>
|
||||||
|
</head>
|
||||||
|
<body class="login-page" data-base-path="<?= h($bp) ?>">
|
||||||
|
<div class="login-card">
|
||||||
|
<h1 data-i18n="verify.title">Email verification</h1>
|
||||||
|
<?php if (!empty($verifyOk)): ?>
|
||||||
|
<p class="alert alert--ok" data-i18n="verify.ok">Your email is verified. You can sign in and enroll two-factor authentication.</p>
|
||||||
|
<p><a class="btn" href="<?= h($auth) ?>/login" data-i18n="verify.sign_in">Sign in</a></p>
|
||||||
|
<?php else: ?>
|
||||||
|
<p class="alert" data-i18n="verify.fail"><?= h($verifyError ?? 'Invalid or expired link.') ?></p>
|
||||||
|
<p class="hint"><a href="<?= h($auth) ?>/register" data-i18n="verify.register_again">Register again</a></p>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<?php platform_render_footer(); ?>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user