mirror of
git://f0xx.org/android_cast
synced 2026-07-29 03:38:52 +03:00
sync be
This commit is contained in:
@@ -5,7 +5,8 @@ if (!is_file($__platformDir . '/footer.php')) {
|
||||
}
|
||||
require_once $__platformDir . '/footer.php';
|
||||
$__sessionFile = $__platformDir . '/shared_session.php';
|
||||
$hubCrashesBase = '/app/androidcast_project/crashes';
|
||||
$hubProjectBase = '/app/androidcast_project';
|
||||
$hubIssuesBase = '/app/androidcast_project/issues';
|
||||
$hubUsername = '';
|
||||
if (is_file($__sessionFile) && function_exists('session_start')) {
|
||||
require_once $__sessionFile;
|
||||
@@ -35,7 +36,7 @@ function hub_h(string $s): string {
|
||||
if (l === 'en' || l === 'ru') document.documentElement.setAttribute('lang', l);
|
||||
})();
|
||||
</script>
|
||||
<link rel="stylesheet" href="/app/androidcast_project/crashes/assets/css/app.css">
|
||||
<link rel="stylesheet" href="/app/androidcast_project/issues/assets/css/app.css">
|
||||
<link rel="stylesheet" href="hub.css?v=20260610overlay">
|
||||
<link rel="stylesheet" href="hub-logo-layers.css?v=20260610overlay">
|
||||
<link rel="stylesheet" href="hub-landing.css?v=20260613layout">
|
||||
@@ -46,12 +47,12 @@ function hub_h(string $s): string {
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<script src="/app/androidcast_project/crashes/assets/js/nav_shell.js" defer></script>
|
||||
<script src="/app/androidcast_project/issues/assets/js/nav_shell.js" defer></script>
|
||||
<script src="assets/hub-i18n.js" defer></script>
|
||||
<script src="assets/hub-landing.js" defer></script>
|
||||
<script src="/app/androidcast_project/crashes/assets/js/cookie_consent.js" defer></script>
|
||||
<script src="/app/androidcast_project/issues/assets/js/cookie_consent.js" defer></script>
|
||||
<script src="assets/analytics.config.js"></script>
|
||||
<script src="/app/androidcast_project/crashes/assets/js/analytics.js" defer></script>
|
||||
<script src="/app/androidcast_project/issues/assets/js/analytics.js" defer></script>
|
||||
</head>
|
||||
<body class="hub-page hub-landing">
|
||||
<div id="lcd-alphabet-host" hidden aria-hidden="true"></div>
|
||||
@@ -139,15 +140,15 @@ function hub_h(string $s): string {
|
||||
<span class="nav-icon nav-icon--menu" aria-hidden="true"></span>
|
||||
</button>
|
||||
<ul class="nav-list">
|
||||
<li><a href="crashes/?view=home" class="nav-link" data-i18n-title="nav.console" title="Console"><span class="nav-icon nav-icon--home"></span><span class="nav-text"><span class="nav-label" data-i18n="nav.console">Console</span><span class="nav-desc" data-i18n="nav.console_desc">Crash triage home and workspace cards.</span></span></a></li>
|
||||
<li><a href="issues/?view=home" class="nav-link" data-i18n-title="nav.console" title="Console"><span class="nav-icon nav-icon--home"></span><span class="nav-text"><span class="nav-label" data-i18n="nav.console">Console</span><span class="nav-desc" data-i18n="nav.console_desc">Crash triage home and workspace cards.</span></span></a></li>
|
||||
<li><a href="git/" class="nav-link" data-i18n-title="nav.git" title="Git"><span class="nav-icon nav-icon--git"></span><span class="nav-text"><span class="nav-label" data-i18n="nav.git">Git</span><span class="nav-desc" data-i18n="nav.git_desc">Browse repos, branches, and tags.</span></span></a></li>
|
||||
<li><a href="crashes/?view=reports" class="nav-link" data-i18n-title="nav.issues" title="Issues"><span class="nav-icon nav-icon--reports"></span><span class="nav-text"><span class="nav-label" data-i18n="nav.issues">Issues</span><span class="nav-desc" data-i18n="nav.issues_desc">Browse and triage crash reports.</span></span></a></li>
|
||||
<li><a href="crashes/?view=tickets" class="nav-link" data-i18n-title="nav.tickets" title="Tickets"><span class="nav-icon nav-icon--tickets"></span><span class="nav-text"><span class="nav-label" data-i18n="nav.tickets">Tickets</span><span class="nav-desc" data-i18n="nav.tickets_desc">Roadmap tasks, QA items, and tags.</span></span></a></li>
|
||||
<li><a href="issues/?view=reports" class="nav-link" data-i18n-title="nav.issues" title="Issues"><span class="nav-icon nav-icon--reports"></span><span class="nav-text"><span class="nav-label" data-i18n="nav.issues">Issues</span><span class="nav-desc" data-i18n="nav.issues_desc">Browse and triage crash reports.</span></span></a></li>
|
||||
<li><a href="issues/?view=tickets" class="nav-link" data-i18n-title="nav.tickets" title="Tickets"><span class="nav-icon nav-icon--tickets"></span><span class="nav-text"><span class="nav-label" data-i18n="nav.tickets">Tickets</span><span class="nav-desc" data-i18n="nav.tickets_desc">Roadmap tasks, QA items, and tags.</span></span></a></li>
|
||||
<li><a href="graphs/" class="nav-link" data-i18n-title="nav.graphs" title="Analytics"><span class="nav-icon nav-icon--graphs"></span><span class="nav-text"><span class="nav-label" data-i18n="nav.graphs">Analytics</span><span class="nav-desc" data-i18n="nav.graphs_desc">Sessions, live cast stats, and device activity.</span></span></a></li>
|
||||
<li><a href="crashes/?view=live_sessions" class="nav-link" data-i18n-title="nav.live_sessions" title="Live sessions"><span class="nav-icon nav-icon--live"></span><span class="nav-text"><span class="nav-label" data-i18n="nav.live_sessions">Live sessions</span><span class="nav-desc" data-i18n="nav.live_sessions_desc">Cast intents, join stats, session history.</span></span></a></li>
|
||||
<li><a href="crashes/live/education" class="nav-link" data-i18n-title="nav.education" title="Education demo"><span class="nav-icon nav-icon--education"></span><span class="nav-text"><span class="nav-label" data-i18n="nav.education">Education (demo)</span><span class="nav-desc" data-i18n="nav.education_desc">Browser screen-share trial (5 min).</span></span></a></li>
|
||||
<li><a href="crashes/?view=remote_access" class="nav-link" data-i18n-title="nav.remote" title="Remote access"><span class="nav-icon nav-icon--remote"></span><span class="nav-text"><span class="nav-label" data-i18n="nav.remote">Remote access</span><span class="nav-desc" data-i18n="nav.remote_desc">WireGuard sessions and device reachability.</span></span></a></li>
|
||||
<li><a href="crashes/?view=short_links" class="nav-link" data-i18n-title="nav.short_links" title="Short links"><span class="nav-icon nav-icon--link"></span><span class="nav-text"><span class="nav-label" data-i18n="nav.short_links">Short links</span><span class="nav-desc" data-i18n="nav.short_links_desc">Mint bearer tokens and shorten URLs for s.f0xx.org.</span></span></a></li>
|
||||
<li><a href="issues/?view=live_sessions" class="nav-link" data-i18n-title="nav.live_sessions" title="Live sessions"><span class="nav-icon nav-icon--live"></span><span class="nav-text"><span class="nav-label" data-i18n="nav.live_sessions">Live sessions</span><span class="nav-desc" data-i18n="nav.live_sessions_desc">Cast intents, join stats, session history.</span></span></a></li>
|
||||
<li><a href="issues/live/education" class="nav-link" data-i18n-title="nav.education" title="Education demo"><span class="nav-icon nav-icon--education"></span><span class="nav-text"><span class="nav-label" data-i18n="nav.education">Education (demo)</span><span class="nav-desc" data-i18n="nav.education_desc">Browser screen-share trial (5 min).</span></span></a></li>
|
||||
<li><a href="issues/?view=remote_access" class="nav-link" data-i18n-title="nav.remote" title="Remote access"><span class="nav-icon nav-icon--remote"></span><span class="nav-text"><span class="nav-label" data-i18n="nav.remote">Remote access</span><span class="nav-desc" data-i18n="nav.remote_desc">WireGuard sessions and device reachability.</span></span></a></li>
|
||||
<li><a href="issues/?view=short_links" class="nav-link" data-i18n-title="nav.short_links" title="Short links"><span class="nav-icon nav-icon--link"></span><span class="nav-text"><span class="nav-label" data-i18n="nav.short_links">Short links</span><span class="nav-desc" data-i18n="nav.short_links_desc">Mint bearer tokens and shorten URLs for s.f0xx.org.</span></span></a></li>
|
||||
<li><a href="build/" class="nav-link" data-i18n-title="nav.builder" title="Builder"><span class="nav-icon nav-icon--builder"></span><span class="nav-text"><span class="nav-label" data-i18n="nav.builder">Builder</span><span class="nav-desc" data-i18n="nav.builder_desc">Docker CI for APK baking and OTA artifacts.</span></span></a></li>
|
||||
</ul>
|
||||
<div class="nav-user">
|
||||
@@ -156,14 +157,14 @@ function hub_h(string $s): string {
|
||||
<span class="nav-icon nav-icon--user" aria-hidden="true"></span>
|
||||
<span class="nav-label"><?= hub_h($hubUsername) ?></span>
|
||||
</span>
|
||||
<a href="<?= hub_h($hubCrashesBase) ?>/account-security"
|
||||
<a href="<?= hub_h($hubIssuesBase) ?>/account-security"
|
||||
class="nav-link"
|
||||
data-i18n-aria="nav.security" data-i18n-title="nav.security"
|
||||
aria-label="Security" title="Security">
|
||||
<span class="nav-icon nav-icon--user" aria-hidden="true"></span>
|
||||
<span class="nav-label" data-i18n="nav.security">Security</span>
|
||||
</a>
|
||||
<a href="<?= hub_h($hubCrashesBase) ?>/logout"
|
||||
<a href="<?= hub_h($hubProjectBase) ?>/logout"
|
||||
class="nav-link nav-link--logout"
|
||||
data-i18n-aria="nav.logout" data-i18n-title="nav.logout"
|
||||
aria-label="Logout" title="Logout">
|
||||
@@ -171,7 +172,7 @@ function hub_h(string $s): string {
|
||||
<span class="nav-label" data-i18n="nav.logout">Logout</span>
|
||||
</a>
|
||||
<?php else: ?>
|
||||
<a href="<?= hub_h($hubCrashesBase) ?>/login"
|
||||
<a href="<?= hub_h($hubProjectBase) ?>/login"
|
||||
class="nav-link"
|
||||
data-i18n-aria="nav.sign_in" data-i18n-title="nav.sign_in"
|
||||
aria-label="Sign in" title="Sign in">
|
||||
@@ -414,7 +415,7 @@ function hub_h(string $s): string {
|
||||
var docsToc = document.getElementById('hub-docs-toc');
|
||||
var docsPanels = document.getElementById('hub-docs-panels');
|
||||
var compassLayer = document.getElementById('hub-compass-layer');
|
||||
var docsApi = '/app/androidcast_project/crashes/api/hub_deploy_docs.php';
|
||||
var docsApi = '/app/androidcast_project/issues/api/hub_deploy_docs.php';
|
||||
var docsQueue = [];
|
||||
var docsLoading = false;
|
||||
var docsCache = {};
|
||||
|
||||
@@ -69,7 +69,7 @@
|
||||
<div class="landing-page-inner card card--lift">
|
||||
<h2 data-i18n="page6.title">Resources</h2>
|
||||
<p class="landing-lead" data-i18n="page6.lead">Documentation, deployment guides, and integration patterns for your environment.</p>
|
||||
<p><a class="btn" href="crashes/">Crashes console</a> <a class="btn" href="build/">Builder</a> <a class="btn" href="git/">Git browser</a></p>
|
||||
<p><a class="btn" href="issues/">Issues console</a> <a class="btn" href="build/">Builder</a> <a class="btn" href="git/">Git browser</a></p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -11,9 +11,12 @@
|
||||
* Digest: SHA256 fcd4a89ef4201736923d0e7e1ad88868bba7e39eadb9fcf0ffb9d1ba7efecd12
|
||||
*/
|
||||
return [
|
||||
'app_name' => 'Android Cast Issues',
|
||||
// Production: '/app/androidcast_project/crashes' — Local php -S: use '' (empty string)
|
||||
'base_path' => '/app/androidcast_project/crashes',
|
||||
'app_name' => 'Android Cast',
|
||||
// Console (issues/tickets/…) — production path
|
||||
'project_base_path' => '/app/androidcast_project',
|
||||
'base_path' => '/app/androidcast_project/issues',
|
||||
// Legacy alias kept for redirects/nginx; do not use in new links
|
||||
'legacy_base_path' => '/app/androidcast_project/crashes',
|
||||
'db' => [
|
||||
// sqlite = default file DB; mysql = MariaDB/MySQL (recommended on BE for concurrency)
|
||||
'driver' => 'sqlite', // sqlite | mysql | mariadb (alias)
|
||||
|
||||
@@ -11,8 +11,9 @@
|
||||
* Digest: SHA256 8b7149a4cd8a0f7d13ee8a22535c3697e919e35b9fe5bb8a80764769cc6c6853
|
||||
*/
|
||||
return [
|
||||
'app_name' => 'Android Cast Crashes',
|
||||
'base_path' => '/app/androidcast_project/crashes',
|
||||
'app_name' => 'Android Cast',
|
||||
'project_base_path' => '/app/androidcast_project',
|
||||
'base_path' => '/app/androidcast_project/issues',
|
||||
'db' => [
|
||||
'driver' => 'mysql', // sqlite | mysql
|
||||
'sqlite_path' => __DIR__ . '/../data/crashes.sqlite',
|
||||
|
||||
@@ -33,40 +33,56 @@ server {
|
||||
try_files $uri $uri/ =404;
|
||||
}
|
||||
|
||||
location = /app/androidcast_project/issues {
|
||||
return 301 /app/androidcast_project/issues/;
|
||||
}
|
||||
|
||||
location = /app/androidcast_project/crashes {
|
||||
return 301 /app/androidcast_project/crashes/;
|
||||
return 301 /app/androidcast_project/issues/;
|
||||
}
|
||||
|
||||
location ~ ^/app/androidcast_project/crashes(/.*)?$ {
|
||||
return 301 /app/androidcast_project/issues$1;
|
||||
}
|
||||
|
||||
location ~ ^/app/androidcast_project/(login|logout|register|two-factor|verify-email)(/|$) {
|
||||
include fastcgi_params;
|
||||
fastcgi_pass unix:/run/php-fpm.socket;
|
||||
fastcgi_param SCRIPT_FILENAME /var/www/localhost/htdocs/apps/app/androidcast_project/android_cast/examples/crash_reporter/backend/public/index.php;
|
||||
fastcgi_param SCRIPT_NAME /app/androidcast_project/issues/index.php;
|
||||
fastcgi_param REQUEST_URI $request_uri;
|
||||
}
|
||||
|
||||
location ^~ /app/androidcast_project/issues/assets/ {
|
||||
alias /var/www/localhost/htdocs/apps/app/androidcast_project/android_cast/examples/crash_reporter/backend/public/assets/;
|
||||
}
|
||||
|
||||
location = /app/androidcast_project/issues/api/upload.php {
|
||||
include fastcgi_params;
|
||||
fastcgi_pass unix:/run/php-fpm.socket;
|
||||
fastcgi_param SCRIPT_FILENAME /var/www/localhost/htdocs/apps/app/androidcast_project/android_cast/examples/crash_reporter/backend/public/api/upload.php;
|
||||
fastcgi_param SCRIPT_NAME /app/androidcast_project/issues/api/upload.php;
|
||||
fastcgi_param REQUEST_URI $request_uri;
|
||||
client_max_body_size 4m;
|
||||
}
|
||||
|
||||
location ^~ /app/androidcast_project/issues/ {
|
||||
include fastcgi_params;
|
||||
fastcgi_pass unix:/run/php-fpm.socket;
|
||||
fastcgi_param SCRIPT_FILENAME /var/www/localhost/htdocs/apps/app/androidcast_project/android_cast/examples/crash_reporter/backend/public/index.php;
|
||||
fastcgi_param SCRIPT_NAME /app/androidcast_project/issues/index.php;
|
||||
fastcgi_param REQUEST_URI $request_uri;
|
||||
client_max_body_size 4m;
|
||||
}
|
||||
|
||||
location = /app/androidcast_project/graphs {
|
||||
return 301 /app/androidcast_project/graphs/;
|
||||
}
|
||||
|
||||
location ^~ /app/androidcast_project/crashes/assets/ {
|
||||
alias /var/www/localhost/htdocs/apps/app/androidcast_project/android_cast/examples/crash_reporter/backend/public/assets/;
|
||||
}
|
||||
|
||||
location ^~ /app/androidcast_project/graphs/assets/ {
|
||||
alias /var/www/localhost/htdocs/apps/app/androidcast_project/android_cast/examples/crash_reporter/backend/public/assets/;
|
||||
}
|
||||
|
||||
location = /app/androidcast_project/crashes/api/upload.php {
|
||||
include fastcgi_params;
|
||||
fastcgi_pass unix:/run/php-fpm.socket;
|
||||
fastcgi_param SCRIPT_FILENAME /var/www/localhost/htdocs/apps/app/androidcast_project/android_cast/examples/crash_reporter/backend/public/api/upload.php;
|
||||
fastcgi_param SCRIPT_NAME /app/androidcast_project/crashes/api/upload.php;
|
||||
fastcgi_param REQUEST_URI $request_uri;
|
||||
client_max_body_size 4m;
|
||||
}
|
||||
|
||||
location ^~ /app/androidcast_project/crashes/ {
|
||||
include fastcgi_params;
|
||||
fastcgi_pass unix:/run/php-fpm.socket;
|
||||
fastcgi_param SCRIPT_FILENAME /var/www/localhost/htdocs/apps/app/androidcast_project/android_cast/examples/crash_reporter/backend/public/index.php;
|
||||
fastcgi_param SCRIPT_NAME /app/androidcast_project/crashes/index.php;
|
||||
fastcgi_param REQUEST_URI $request_uri;
|
||||
client_max_body_size 4m;
|
||||
}
|
||||
|
||||
location = /app/androidcast_project/graphs/api/graphs.php {
|
||||
include fastcgi_params;
|
||||
fastcgi_pass unix:/run/php-fpm.socket;
|
||||
|
||||
@@ -2317,6 +2317,14 @@ button.report-tag--filter:hover {
|
||||
}
|
||||
|
||||
/* Nav icons: live + education */
|
||||
.nav-icon--live {
|
||||
width: 18px;
|
||||
height: 16px;
|
||||
}
|
||||
.nav-icon--education {
|
||||
width: 18px;
|
||||
height: 16px;
|
||||
}
|
||||
.nav-icon--live::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
@@ -2361,3 +2369,17 @@ button.report-tag--filter:hover {
|
||||
background: rgba(52, 211, 153, 0.15);
|
||||
color: #6ee7b7;
|
||||
}
|
||||
|
||||
.twofa-qr-wrap {
|
||||
margin: 12px 0 16px;
|
||||
text-align: center;
|
||||
}
|
||||
.twofa-qr {
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
padding: 6px;
|
||||
}
|
||||
.twofa-qr-wrap figcaption {
|
||||
margin-top: 8px;
|
||||
font-size: 0.88rem;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
})();
|
||||
@@ -15,10 +15,7 @@ require_once __DIR__ . '/../src/bootstrap.php';
|
||||
|
||||
$uri = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH) ?: '/';
|
||||
$base = Auth::basePath();
|
||||
if ($base !== '' && str_starts_with($uri, $base)) {
|
||||
$uri = substr($uri, strlen($base)) ?: '/';
|
||||
}
|
||||
$route = rtrim($uri, '/') ?: '/';
|
||||
$route = resolve_console_route($uri);
|
||||
|
||||
if ($route === '/api/upload.php' || str_ends_with($route, '/api/upload.php')) {
|
||||
require __DIR__ . '/api/upload.php';
|
||||
@@ -152,7 +149,7 @@ if ($route === '/api/auth_register.php' || str_ends_with($route, '/api/auth_regi
|
||||
|
||||
if ($route === '/logout') {
|
||||
Auth::logout();
|
||||
header('Location: ' . $base . '/login');
|
||||
header('Location: ' . Auth::authUrl('/login'));
|
||||
exit;
|
||||
}
|
||||
|
||||
@@ -165,7 +162,7 @@ if ($route === '/login' && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
exit;
|
||||
}
|
||||
if ($result === 'pending_2fa') {
|
||||
header('Location: ' . $base . '/two-factor');
|
||||
header('Location: ' . Auth::authUrl('/two-factor'));
|
||||
exit;
|
||||
}
|
||||
$loginError = 'Invalid credentials';
|
||||
@@ -183,7 +180,7 @@ if ($route === '/login' && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
|
||||
if ($route === '/login') {
|
||||
if (Auth::pending2faUserId() > 0) {
|
||||
header('Location: ' . $base . '/two-factor');
|
||||
header('Location: ' . Auth::authUrl('/two-factor'));
|
||||
exit;
|
||||
}
|
||||
require __DIR__ . '/../views/login.php';
|
||||
@@ -235,7 +232,7 @@ if ($route === '/verify-email') {
|
||||
|
||||
if ($route === '/two-factor' && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
if (Auth::pending2faUserId() <= 0) {
|
||||
header('Location: ' . $base . '/login');
|
||||
header('Location: ' . Auth::authUrl('/login'));
|
||||
exit;
|
||||
}
|
||||
$code = trim($_POST['code'] ?? '');
|
||||
@@ -250,9 +247,10 @@ if ($route === '/two-factor' && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
|
||||
if ($route === '/two-factor') {
|
||||
if (Auth::pending2faUserId() <= 0) {
|
||||
header('Location: ' . $base . '/login');
|
||||
header('Location: ' . Auth::authUrl('/login'));
|
||||
exit;
|
||||
}
|
||||
$twofaQr = AuthTwoFactorPage::qrPayload();
|
||||
require __DIR__ . '/../views/two_factor_challenge.php';
|
||||
exit;
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ from pathlib import Path
|
||||
frag, apps_path = map(Path, sys.argv[1:3])
|
||||
text = apps_path.read_text()
|
||||
start = text.index("server {\n\tlisten 80;")
|
||||
end = text.index("\n}\n\n\nserver {\n\tlisten 443")
|
||||
end = text.index("\nserver {\n\tlisten 443")
|
||||
apps_path.write_text(text[:start] + frag.read_text().strip() + text[end:])
|
||||
print(f"Updated listen 80 in {apps_path}")
|
||||
PY
|
||||
|
||||
@@ -36,7 +36,7 @@ final class Auth {
|
||||
|
||||
public static function check(): void {
|
||||
if (!self::user()) {
|
||||
header('Location: ' . self::basePath() . '/login');
|
||||
header('Location: ' . self::authUrl('/login'));
|
||||
exit;
|
||||
}
|
||||
}
|
||||
@@ -141,4 +141,27 @@ final class Auth {
|
||||
$bp = cfg('base_path', '');
|
||||
return rtrim($bp, '/');
|
||||
}
|
||||
|
||||
/** Shared project prefix, e.g. /app/androidcast_project */
|
||||
public static function projectBasePath(): string {
|
||||
$p = trim((string) cfg('project_base_path', ''));
|
||||
if ($p !== '') {
|
||||
return rtrim($p, '/');
|
||||
}
|
||||
$bp = self::basePath();
|
||||
if (preg_match('#^(.+)/(?:crashes|issues|graphs|build)$#', $bp, $m)) {
|
||||
return $m[1];
|
||||
}
|
||||
return '/app/androidcast_project';
|
||||
}
|
||||
|
||||
/** Login/logout/register/2FA live at project root, not under /issues/. */
|
||||
public static function authUrl(string $path = ''): string {
|
||||
$root = self::projectBasePath();
|
||||
$path = '/' . ltrim($path, '/');
|
||||
if ($path === '/' || $path === '') {
|
||||
return $root;
|
||||
}
|
||||
return $root . $path;
|
||||
}
|
||||
}
|
||||
|
||||
35
examples/crash_reporter/backend/src/AuthTwoFactorPage.php
Normal file
35
examples/crash_reporter/backend/src/AuthTwoFactorPage.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/** 2FA challenge page helpers (QR short link for mobile handoff). */
|
||||
final class AuthTwoFactorPage {
|
||||
/** @return array{short_url?:string,qr_url?:string,long_url:string}|null */
|
||||
public static function qrPayload(): ?array {
|
||||
$uid = Auth::pending2faUserId();
|
||||
if ($uid <= 0) {
|
||||
return null;
|
||||
}
|
||||
$project = Auth::projectBasePath();
|
||||
$longUrl = 'https://apps.f0xx.org' . $project . '/?ref=2fa&uid=' . $uid;
|
||||
if (!UrlShortenerDatabase::enabled() || !UrlShortenerDatabase::ping()) {
|
||||
return ['long_url' => $longUrl];
|
||||
}
|
||||
$bearers = ShortLinksRepository::listBearers();
|
||||
if ($bearers === []) {
|
||||
return ['long_url' => $longUrl];
|
||||
}
|
||||
$bearerId = (int) ($bearers[0]['id'] ?? 0);
|
||||
if ($bearerId <= 0) {
|
||||
return ['long_url' => $longUrl];
|
||||
}
|
||||
$mint = ShortLinksRepository::createLink($bearerId, $longUrl, 900);
|
||||
if (empty($mint['ok'])) {
|
||||
return ['long_url' => $longUrl];
|
||||
}
|
||||
return [
|
||||
'long_url' => $longUrl,
|
||||
'short_url' => (string) ($mint['short_url'] ?? ''),
|
||||
'qr_url' => (string) ($mint['qr_url'] ?? ''),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -334,8 +334,8 @@ final class GraphRepository {
|
||||
return [
|
||||
'hub' => $prefix . '/',
|
||||
'graphs' => $prefix . '/graphs/',
|
||||
'crashes' => $prefix . '/crashes/?view=reports',
|
||||
'tickets' => $prefix . '/crashes/?view=tickets',
|
||||
'issues' => $prefix . '/issues/?view=reports',
|
||||
'tickets' => $prefix . '/issues/?view=tickets',
|
||||
'build' => $prefix . '/build/',
|
||||
'git' => $prefix . '/git/',
|
||||
];
|
||||
|
||||
@@ -72,6 +72,7 @@ require_once __DIR__ . '/WireGuardAddressPool.php';
|
||||
require_once __DIR__ . '/WireGuardTrafficStats.php';
|
||||
require_once __DIR__ . '/RsshSessionProvisioner.php';
|
||||
require_once __DIR__ . '/RsshBastionProvisioner.php';
|
||||
require_once __DIR__ . '/AuthTwoFactorPage.php';
|
||||
require_once __DIR__ . '/AnalyticsHead.php';
|
||||
|
||||
function cfg(string $key, $default = null) {
|
||||
@@ -108,6 +109,23 @@ function json_out(array $data, int $code = 200): void {
|
||||
exit;
|
||||
}
|
||||
|
||||
/** Resolve web route: auth pages at project root; console pages under base_path. */
|
||||
function resolve_console_route(string $uri): string {
|
||||
$projectRoot = rtrim((string) cfg('project_base_path', '/app/androidcast_project'), '/');
|
||||
$base = rtrim((string) cfg('base_path', ''), '/');
|
||||
if ($projectRoot !== '' && str_starts_with($uri, $projectRoot)) {
|
||||
$suffix = substr($uri, strlen($projectRoot)) ?: '/';
|
||||
$candidate = rtrim(strtok($suffix, '?') ?: '/', '/') ?: '/';
|
||||
if (preg_match('#^/(login|logout|register|two-factor|verify-email)(?:/|$)#', $candidate)) {
|
||||
return $candidate;
|
||||
}
|
||||
}
|
||||
if ($base !== '' && str_starts_with($uri, $base)) {
|
||||
$uri = substr($uri, strlen($base)) ?: '/';
|
||||
}
|
||||
return rtrim(strtok($uri, '?') ?: '/', '/') ?: '/';
|
||||
}
|
||||
|
||||
/** Public URL prefix for the active console view (graphs vs crashes). */
|
||||
function console_base_path(?string $view = null): string {
|
||||
$view = $view ?? (string) ($_GET['view'] ?? '');
|
||||
@@ -117,6 +135,9 @@ function console_base_path(?string $view = null): string {
|
||||
return rtrim($graphs, '/');
|
||||
}
|
||||
$crashes = rtrim((string) cfg('base_path', ''), '/');
|
||||
if (str_ends_with($crashes, '/issues')) {
|
||||
return substr($crashes, 0, -strlen('/issues')) . '/graphs';
|
||||
}
|
||||
if (str_ends_with($crashes, '/crashes')) {
|
||||
return substr($crashes, 0, -strlen('/crashes')) . '/graphs';
|
||||
}
|
||||
|
||||
@@ -220,7 +220,7 @@
|
||||
<span class="nav-icon nav-icon--user" aria-hidden="true"></span>
|
||||
<span class="nav-label" data-i18n="nav.security">Security</span>
|
||||
</a>
|
||||
<a href="<?= h(Auth::basePath()) ?>/logout"
|
||||
<a href="<?= h(Auth::authUrl('/logout')) ?>"
|
||||
class="nav-link nav-link--logout"
|
||||
data-i18n-aria="nav.logout" data-i18n-title="nav.logout"
|
||||
aria-label="Logout"
|
||||
|
||||
@@ -22,7 +22,7 @@ $bp = Auth::basePath();
|
||||
<body class="live-page" data-base-path="<?= h($bp) ?>" data-view="live_education">
|
||||
<main id="live-education-app" class="live-shell">
|
||||
<header class="live-header">
|
||||
<a href="<?= h($bp) ?>/" class="live-brand"><?= h(cfg('app_name')) ?></a>
|
||||
<a href="<?= h(Auth::projectBasePath()) ?>/" class="live-brand">← Back to the console</a>
|
||||
<span class="tag-pill">Education demo</span>
|
||||
</header>
|
||||
<section class="card card--lift live-card">
|
||||
|
||||
@@ -29,7 +29,7 @@ $role = trim((string) ($_GET['role'] ?? ''));
|
||||
data-direct="<?= $direct ? '1' : '0' ?>"
|
||||
data-role="<?= h($role) ?>">
|
||||
<header class="live-header">
|
||||
<a href="<?= h($bp) ?>/" class="live-brand"><?= h(cfg('app_name')) ?></a>
|
||||
<a href="<?= h(Auth::projectBasePath()) ?>/" class="live-brand">← Back to the console</a>
|
||||
<span class="tag-pill tag-pill--ok">Live viewer</span>
|
||||
</header>
|
||||
<section class="card card--lift live-card">
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
* Digest: SHA256 9bef983f4d34cb790922cda355ccf5d14b565b29273d2ebfe48b3173cd2e8f9f
|
||||
*/
|
||||
$bp = Auth::basePath();
|
||||
$auth = Auth::authUrl();
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-theme="dark">
|
||||
@@ -40,7 +41,7 @@ $bp = Auth::basePath();
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<form class="login-card" method="post" action="<?= h($bp) ?>/login">
|
||||
<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)): ?>
|
||||
@@ -50,7 +51,7 @@ $bp = Auth::basePath();
|
||||
<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($bp) ?>/register" data-i18n="login.register">Register</a></p>
|
||||
<p class="hint"><a href="<?= h($auth) ?>/register" data-i18n="login.register">Register</a></p>
|
||||
</form>
|
||||
<?php platform_render_footer(); ?>
|
||||
</body>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<?php
|
||||
$bp = Auth::basePath();
|
||||
$auth = Auth::authUrl();
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-theme="dark">
|
||||
@@ -29,7 +30,7 @@ $bp = Auth::basePath();
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<form class="login-card" method="post" action="<?= h($bp) ?>/register">
|
||||
<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)): ?>
|
||||
@@ -45,7 +46,7 @@ $bp = Auth::basePath();
|
||||
<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($bp) ?>/login" data-i18n="register.back_login">Back to sign in</a></p>
|
||||
<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>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
$bp = Auth::basePath();
|
||||
$auth = Auth::authUrl();
|
||||
$twofaQr = AuthTwoFactorPage::qrPayload();
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-theme="dark">
|
||||
@@ -17,6 +19,7 @@ $bp = Auth::basePath();
|
||||
</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">
|
||||
@@ -28,16 +31,22 @@ $bp = Auth::basePath();
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<form class="login-card" method="post" action="<?= h($bp) ?>/two-factor">
|
||||
<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 name="code" inputmode="numeric" pattern="[0-9]{6}" maxlength="6" autocomplete="one-time-code" required autofocus></label>
|
||||
<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($bp) ?>/login" data-i18n="twofa.cancel">Cancel</a></p>
|
||||
<p class="hint"><a href="<?= h($auth) ?>/login" data-i18n="twofa.cancel">Cancel</a></p>
|
||||
</form>
|
||||
<?php platform_render_footer(); ?>
|
||||
</body>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<?php
|
||||
$bp = Auth::basePath();
|
||||
$auth = Auth::authUrl();
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
@@ -21,10 +22,10 @@ $bp = Auth::basePath();
|
||||
<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($bp) ?>/login" data-i18n="verify.sign_in">Sign in</a></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($bp) ?>/register" data-i18n="verify.register_again">Register again</a></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(); ?>
|
||||
|
||||
Reference in New Issue
Block a user