BREAK IT: the damn auth login (and fix neutralino downloads too)

This commit is contained in:
Julien Maille 2026-02-15 20:55:32 +01:00
parent db66767dde
commit 094ae91af9
6 changed files with 17 additions and 448 deletions

View file

@ -4632,7 +4632,6 @@
</footer>
</div>
<script src="https://cdn.jsdelivr.net/npm/pocketbase@0.21.3/dist/pocketbase.umd.js"></script>
<script src="/neutralino.js"></script>
<script type="module" src="/js/app.js"></script>
</body>
</html>

View file

@ -22,24 +22,6 @@ export class AuthManager {
init() {
if (!auth) return;
// Persist Neutralino Env params across navigation via sessionStorage
window.MonochromeEnv = window.MonochromeEnv || {};
const initUrlParams = new URLSearchParams(window.location.search);
// Only update sessionStorage if params are present in URL (e.g. fresh launch)
if (initUrlParams.has('nl_port')) sessionStorage.setItem('NL_PORT', initUrlParams.get('nl_port'));
if (initUrlParams.has('nl_token')) sessionStorage.setItem('NL_TOKEN', initUrlParams.get('nl_token'));
if (initUrlParams.has('os')) sessionStorage.setItem('NL_OS', initUrlParams.get('os'));
if (initUrlParams.has('mode')) sessionStorage.setItem('NL_MODE', initUrlParams.get('mode'));
// Populate window.MonochromeEnv from sessionStorage
window.MonochromeEnv.nl_port = sessionStorage.getItem('NL_PORT');
window.MonochromeEnv.nl_token = sessionStorage.getItem('NL_TOKEN');
window.MonochromeEnv.os = sessionStorage.getItem('NL_OS');
console.log('[Auth] Initializing. Current URL:', window.location.href);
console.log('[Auth] Persisted Env (SessionStorage):', window.MonochromeEnv);
this.unsubscribe = onAuthStateChanged(auth, (user) => {
this.user = user;
this.updateUI(user);
@ -68,90 +50,6 @@ export class AuthManager {
return;
}
console.log('[Auth] URL Debug:', window.location.href);
// Check for Neutralino mode
// We trust NL_MODE or specific params.
const isNeutralino =
window.NL_MODE === true ||
sessionStorage.getItem('NL_MODE') === 'neutralino' ||
(window.Neutralino && typeof window.Neutralino === 'object');
// Check for OS/Port/Token from URL params OR persisted env
const urlParams = new URLSearchParams(window.location.search);
// Populate from env if not in URL
const nlPort = urlParams.get('nl_port') || window.MonochromeEnv?.nl_port;
const nlToken = urlParams.get('nl_token') || window.MonochromeEnv?.nl_token;
console.log('[Auth] Starting Google Sign-In. Mode:', isNeutralino ? 'Neutralino' : 'Web');
if (isNeutralino) {
// Neutralino (Desktop) Mode
// We use the External Auth Bridge for ALL desktop platforms (Windows/Linux/Mac)
// This avoids issues with internal webview restrictions (e.g. Google blocking embedded webviews)
if (!nlPort || !nlToken) {
alert('Missing Neutralino connection parameters. Cannot launch external auth.');
return;
}
console.log('[Auth] Desktop detected. Launching external browser for authentication...');
// Construct the local URL for the bridge file
// Use window.location.origin to ensure we use the correct server (Vite in dev, Neutralino in prod)
const bridgeUrl = `${window.location.origin}/auth_bridge.html?port=${nlPort}&token=${nlToken}`;
try {
await window.Neutralino.os.open(bridgeUrl);
// Show a waiting UI
const connectBtn = document.getElementById('firebase-connect-btn');
if (connectBtn) {
connectBtn.textContent = 'Waiting for browser...';
connectBtn.disabled = true;
}
// Setup one-time listener for the success event
const authHandler = async (detail) => {
// We received a raw ID token (Google ID Token) or similar?
// The bridge sends { uid, email, accessToken }.
// We need to create a credential from it.
try {
const { GoogleAuthProvider, signInWithCredential } =
await import('https://www.gstatic.com/firebasejs/10.7.1/firebase-auth.js');
// Create credential using Google's tokens passed from the bridge
const credential = GoogleAuthProvider.credential(detail.idToken, detail.accessToken);
const result = await signInWithCredential(auth, credential);
console.log('[Auth] External Login successful');
this.user = result.user;
this.updateUI(result.user);
this.authListeners.forEach((listener) => listener(result.user));
// Cleanup
window.Neutralino.events.off('externalAuthSuccess', authHandler);
} catch (e) {
console.error('[Auth] Failed to sign in with external credential:', e);
alert('Failed to complete login from external browser.');
if (connectBtn) {
connectBtn.textContent = 'Connect with Google';
connectBtn.disabled = false;
}
}
};
window.Neutralino.events.on('externalAuthSuccess', authHandler);
} catch (e) {
console.error('[Auth] Failed to open external browser:', e);
alert('Failed to launch external browser.');
}
return;
}
// Web Mode
try {
const result = await signInWithPopup(auth, provider);

View file

@ -316,56 +316,19 @@ document.addEventListener('DOMContentLoaded', async () => {
initTracker(player);
// Initialize desktop features if in Neutralino mode
// We only assume Neutralino mode if explicitly flagged or if specific params are present
const isNeutralinoMode =
if (
typeof window !== 'undefined' &&
(window.NL_MODE ||
window.location.search.includes('mode=neutralino') ||
window.location.search.includes('nl_port='));
if (isNeutralinoMode) {
window.location.search.includes('nl_port='))
) {
window.NL_MODE = true;
// Function to restore env vars for Neutralino
const restoreNeutralinoEnv = () => {
if (window.MonochromeEnv) {
if (window.MonochromeEnv.nl_port) window.NL_PORT = window.MonochromeEnv.nl_port;
if (window.MonochromeEnv.nl_token) window.NL_TOKEN = window.MonochromeEnv.nl_token;
} else {
// Fallback direct read
const p = sessionStorage.getItem('NL_PORT');
const t = sessionStorage.getItem('NL_TOKEN');
if (p) window.NL_PORT = p;
if (t) window.NL_TOKEN = t;
}
// Polyfill NL_ARGS to prevent crash in neutralino.js (it checks for debug flags)
window.NL_ARGS = window.NL_ARGS || [];
console.log('[App] Restored Neutralino Env:', { port: window.NL_PORT, token: !!window.NL_TOKEN });
};
// Ensure Neutralino global is available
if (typeof window.Neutralino === 'undefined') {
console.log('[App] Neutralino global not found. Injecting script...');
try {
// Dynamically load neutralino.js from the server root
const script = document.createElement('script');
script.src = '/neutralino.js';
script.onload = () => {
console.log('[App] neutralino.js loaded.');
restoreNeutralinoEnv(); // Restore BEFORE init
window.Neutralino.init();
};
document.body.appendChild(script);
} catch (e) {
console.error('[App] Failed to inject neutralino.js:', e);
}
} else {
// Already present
restoreNeutralinoEnv(); // Restore BEFORE init
window.Neutralino.init();
try {
const desktopModule = await import('./desktop/desktop.js');
await desktopModule.initDesktop(player);
} catch (err) {
console.error('Failed to load desktop module:', err);
}
import('./desktop/desktop.js').then((m) => m.initDesktop(player));
}
const castBtn = document.getElementById('cast-btn');

View file

@ -229,7 +229,14 @@ export function initializeSettings(scrobbler, player, api, ui) {
const { token, url } = await scrobbler.lastfm.getAuthUrl();
if (window.Neutralino) {
await Neutralino.os.open(url);
try {
await Neutralino.os.open(url);
} catch (e) {
// Fallback if os.open fails
console.error('Neutralino open failed, falling back to window.open', e);
if (!authWindow) authWindow = window.open(url, '_blank');
else authWindow.location.href = url;
}
} else if (authWindow) {
authWindow.location.href = url;
} else {
@ -271,7 +278,6 @@ export function initializeSettings(scrobbler, player, api, ui) {
lastfmToggle.checked = true;
updateLastFMUI();
lastfmConnectBtn.disabled = false;
alert(`Successfully connected to Last.fm as ${result.username}!`);
}
} catch {
// Still waiting
@ -396,7 +402,6 @@ export function initializeSettings(scrobbler, player, api, ui) {
updateLastFMUI();
// Clear password for security
if (lastfmPasswordInput) lastfmPasswordInput.value = '';
alert(`Successfully connected to Last.fm as ${result.username}!`);
}
} catch (error) {
console.error('Last.fm credential login failed:', error);

View file

@ -1,249 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Monochrome Login</title>
<style>
body {
background-color: #121212;
color: #ffffff;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
text-align: center;
}
.container {
background: #1e1e1e;
padding: 2rem;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
max-width: 400px;
width: 90%;
}
h1 {
margin-bottom: 1.5rem;
}
p {
color: #aaaaaa;
margin-bottom: 2rem;
}
.btn {
background-color: #ffffff;
color: #000000;
border: none;
padding: 10px 20px;
border-radius: 4px;
font-size: 1rem;
cursor: pointer;
font-weight: 500;
transition: background-color 0.2s;
}
.btn:hover {
background-color: #e0e0e0;
}
.error {
color: #ff5252;
margin-top: 1rem;
display: none;
}
.success {
color: #4caf50;
margin-top: 1rem;
display: none;
}
</style>
</head>
<body>
<div class="container">
<h1 id="status-title">Monochrome Login</h1>
<p id="status-text">Please sign in to continue.</p>
<button id="login-btn" class="btn">Sign in with Google</button>
<div id="error-msg" class="error"></div>
<div id="success-msg" class="success">Login successful! You can close this window.</div>
</div>
<script type="module">
import { initializeApp } from 'https://www.gstatic.com/firebasejs/10.7.1/firebase-app.js';
import {
getAuth,
GoogleAuthProvider,
signInWithPopup,
signInWithRedirect,
getRedirectResult,
} from 'https://www.gstatic.com/firebasejs/10.7.1/firebase-auth.js';
// Duplicate config from config.js - safer to keep standalone here to avoid import issues in external browser
const firebaseConfig = {
apiKey: 'AIzaSyDPU-unAjuLtQJt4IkGS5faG50UCF7lYyA',
authDomain: 'monochrome-database.firebaseapp.com',
projectId: 'monochrome-database',
storageBucket: 'monochrome-database.firebasestorage.app',
messagingSenderId: '895657412760',
appId: '1:895657412760:web:e81c5044c7f4e9b799e8ed',
};
const app = initializeApp(firebaseConfig);
const auth = getAuth(app);
const provider = new GoogleAuthProvider();
const urlParams = new URLSearchParams(window.location.search);
const NL_PORT = urlParams.get('port');
const NL_TOKEN = urlParams.get('token');
const btn = document.getElementById('login-btn');
const errorMsg = document.getElementById('error-msg');
const successMsg = document.getElementById('success-msg');
const statusTitle = document.getElementById('status-title');
const statusText = document.getElementById('status-text');
// Handle Redirect Result (if we came back from a redirect login)
getRedirectResult(auth)
.then((result) => {
if (result) {
console.log('Redirect Login Successful:', result.user.email);
sendAuthToApp(result);
}
})
.catch((error) => {
console.error('Redirect Login Failed:', error);
showError(error.message);
});
// Simple Neutralino WebSocket Client (just enough to send an event)
function sendAuthToApp(result) {
if (!NL_PORT || !NL_TOKEN) {
showError('Missing connection parameters (port/token). Launch from app.');
return;
}
// Extract user and credential from the UserCredential result
const user = result.user;
const credential = GoogleAuthProvider.credentialFromResult(result);
if (!credential) {
showError('Failed to retrieve Google Auth Credentials.');
return;
}
// Neutralino expects connectToken to be the second part of the NL_TOKEN
// NL_TOKEN format: access_token.connect_token
const parts = NL_TOKEN.split('.');
if (parts.length < 2) {
showError('Invalid Token format.');
return;
}
const connectToken = parts[1];
const ws = new WebSocket(`ws://localhost:${NL_PORT}?connectToken=${connectToken}`);
ws.onopen = async () => {
console.log('Connected to Monochrome App');
// Payload must follow Neutralino protocol:
// { id: uuid, method: "events.broadcast", data: { ... }, accessToken: NL_TOKEN }
const uuid = crypto.randomUUID ? crypto.randomUUID() : 'auth-' + Date.now();
const payload = {
id: uuid,
method: 'events.broadcast',
accessToken: NL_TOKEN, // Full token required here
data: {
event: 'externalAuthSuccess',
data: {
uid: user.uid,
email: user.email,
// Pass Google OAuth tokens to create a credential in the main app
idToken: credential.idToken,
accessToken: credential.accessToken,
},
},
};
ws.send(JSON.stringify(payload));
showSuccess();
statusTitle.textContent = "You're signed in";
statusText.textContent = 'Return to Monochrome to see your library.';
btn.style.display = 'none';
// Close after a brief delay
setTimeout(() => ws.close(), 1000);
};
ws.send(JSON.stringify(payload));
showSuccess();
statusTitle.textContent = "You're signed in";
statusText.textContent = 'Return to Monochrome to see your library.';
btn.style.display = 'none';
// Close after a brief delay
setTimeout(() => ws.close(), 1000);
};
ws.onerror = (e) => {
console.error('WebSocket Error:', e);
showError('Failed to connect to Monochrome App. Is it running?');
};
}
function showError(msg) {
errorMsg.textContent = msg;
errorMsg.style.display = 'block';
successMsg.style.display = 'none';
}
function showSuccess() {
successMsg.style.display = 'block';
errorMsg.style.display = 'none';
}
btn.addEventListener('click', async () => {
try {
const result = await signInWithPopup(auth, provider);
console.log('User signed in via popup:', result.user.email);
sendAuthToApp(result);
} catch (error) {
console.error('Popup Login failed:', error);
// Fallback to redirect for almost any error (popup blocked, closed, network error, internal error)
// unless it's a specific user-cancellation that we know is intentional?
// 'auth/cancelled-popup-request' usually means the user closed it or the browser killed it.
// 'auth/internal-error' often happens if scripts are blocked.
console.log('Falling back to Redirect Login...');
showError('Popup failed (' + error.code + '). Redirecting...');
try {
await signInWithRedirect(auth, provider);
} catch (redirectError) {
console.error('Redirect Login failed:', redirectError);
showError('Login failed completely: ' + redirectError.message + ' (' + redirectError.code + ')');
}
}
});
if (!NL_PORT || !NL_TOKEN) {
showError('Invalid launch parameters. Please open this page from within Monochrome.');
btn.disabled = true;
}
</script>
</body>
</html>

View file

@ -92,52 +92,11 @@
// Static Dev Port
const DEV_PORT = '5173';
// NL_PORT and NL_TOKEN are populated by neutralino.js upon init (or pre-init)
// We should ensure they are present.
let port = window.NL_PORT || sessionStorage.getItem('NL_PORT') || '5050';
let token = window.NL_TOKEN || sessionStorage.getItem('NL_TOKEN') || '';
console.log('[Shell] Neutralino Globals - Port:', port, 'Token:', token ? '***' : 'Missing');
const iframe = document.getElementById('app-frame');
const targetPort = isDev ? DEV_PORT : port;
let targetUrl = `http://localhost:${targetPort}/?mode=neutralino`;
// Pass global args to app for bridge links
if (port) targetUrl += `&nl_port=${port}`;
if (token) targetUrl += `&nl_token=${token}`;
try {
const envs = await Neutralino.os.getEnvs();
console.log('[Shell] Envs:', envs);
// Heuristic for Linux:
// If XDG_SESSION_TYPE or SHELL is present and generic OS isn't 'Windows_NT'
let detectedOS = null;
if (envs.OS)
detectedOS = envs.OS; // Windows usually sets 'OS' env param to 'Windows_NT'
else if (envs.XDG_SESSION_TYPE || envs.SHELL) detectedOS = 'Linux'; // Rough guess
if (detectedOS) {
console.log('[Shell] Detected OS (Env):', detectedOS);
targetUrl += `&os=${encodeURIComponent(detectedOS)}`;
} else {
// Fallback: try computer.getOSInfo again with better error logging?
// Or just assume non-Linux if we can't tell.
try {
const osInfo = await Neutralino.computer.getOSInfo();
if (osInfo && osInfo.name) {
console.log('[Shell] Detected OS (API):', osInfo.name);
targetUrl += `&os=${encodeURIComponent(osInfo.name)}`;
}
} catch (err) {
console.log('[Shell] OS Detection failed completley.');
}
}
} catch (e) {
console.error('[Shell] Failed to get envs:', e);
}
const targetUrl = `http://localhost:${targetPort}/?mode=neutralino`;
if (isDev) {
console.log(`[Shell] Dev mode detected via NL_ARGS. Waiting 2s for Vite on port ${targetPort}...`);
@ -176,12 +135,6 @@
Neutralino.events.on('windowFocus', () => forwardEvent('windowFocus'));
Neutralino.events.on('windowBlur', () => forwardEvent('windowBlur'));
// Forward the external auth success event to the app
Neutralino.events.on('externalAuthSuccess', (event) => {
console.log('[Shell] Received external auth success', event.detail);
forwardEvent('externalAuthSuccess', event.detail);
});
// Handle commands from the Iframe (via Bridge)
window.addEventListener('message', async (event) => {
const { type, eventName, data, extensionId } = event.data;