1
0
mirror of git://f0xx.org/android_cast synced 2026-07-29 03:38:52 +03:00
This commit is contained in:
Anton Afanasyeu
2026-06-20 21:26:51 +02:00
parent 462962b8dc
commit 26642f46c6
22 changed files with 284 additions and 77 deletions

View File

@@ -3,8 +3,10 @@ package com.foxx.androidcast.network;
/** Canonical production BE URLs on {@code apps.f0xx.org} (network self-test + defaults). */
public final class BackendEndpoints {
public static final String APPS_HOST = "apps.f0xx.org";
public static final String CRASHES_BASE =
"https://" + APPS_HOST + "/app/androidcast_project/crashes";
public static final String ISSUES_BASE =
"https://" + APPS_HOST + "/app/androidcast_project/issues";
/** @deprecated use {@link #ISSUES_BASE}; nginx 301 redirects legacy path. */
public static final String CRASHES_BASE = ISSUES_BASE;
public static final String GRAPHS_BASE =
"https://" + APPS_HOST + "/app/androidcast_project/graphs";
public static final String HEARTBEAT_URL = CRASHES_BASE + "/api/heartbeat.php";
@@ -27,9 +29,15 @@ public final class BackendEndpoints {
return trimmed;
}
if (trimmed.contains("://f0xx.org/app/androidcast_project/crashes")
|| trimmed.contains("://foxx.org/app/androidcast_project/crashes")) {
|| trimmed.contains("://foxx.org/app/androidcast_project/crashes")
|| trimmed.contains("://f0xx.org/app/androidcast_project/issues")
|| trimmed.contains("://foxx.org/app/androidcast_project/issues")) {
return trimmed.replace("://f0xx.org/", "://" + APPS_HOST + "/")
.replace("://foxx.org/", "://" + APPS_HOST + "/");
.replace("://foxx.org/", "://" + APPS_HOST + "/")
.replace("/crashes/", "/issues/");
}
if (trimmed.contains("/crashes/")) {
return trimmed.replace("/crashes/", "/issues/");
}
return trimmed;
}
@@ -40,8 +48,9 @@ public final class BackendEndpoints {
return GRAPH_UPLOAD_URL;
}
String trimmed = normalizeCrashesUrl(crashesUrl.trim());
if (trimmed.contains("/crashes/")) {
return trimmed.replace("/crashes/", "/graphs/").replace("/api/upload.php", "/api/graph_upload.php");
if (trimmed.contains("/issues/")) {
return trimmed.replace("/issues/", "/graphs/")
.replace("/api/upload.php", "/api/graph_upload.php");
}
return GRAPH_UPLOAD_URL;
}

View File

@@ -9,7 +9,7 @@ public class BackendEndpointsTest {
@Test
public void canonicalUrlsUseAppsHost() {
assertTrue(BackendEndpoints.HEARTBEAT_URL.startsWith("https://apps.f0xx.org/"));
assertTrue(BackendEndpoints.UPLOAD_URL.contains("/api/upload.php"));
assertTrue(BackendEndpoints.UPLOAD_URL.contains("/issues/api/upload.php"));
assertTrue(BackendEndpoints.ENTITLEMENTS_FETCH_URL.contains("/entitlements/api/fetch.php"));
}

View File

@@ -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 = {};

View File

@@ -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>

View File

@@ -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)

View File

@@ -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',

View File

@@ -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;

View File

@@ -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;
}

View 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();
}
})();

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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;
}
}

View 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'] ?? ''),
];
}
}

View File

@@ -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/',
];

View File

@@ -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';
}

View File

@@ -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"

View File

@@ -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">

View File

@@ -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">

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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(); ?>