Link Device
Enter the 12-character connection code from your account dashboard.
GENERATE CODE
Your one-time unlock code:
IMPORTANT: Copy this code now! It will only be shown once.
//
//
//
// Make sure a user signs in via Google/other providers before reaching this page
// (or implement sign-in flow directly on this page if not handled elsewhere).
// This script will assume `firebase.auth()` and `firebase.firestore()` are available.
// If using modular SDK, imports would look like:
// import { getAuth, onAuthStateChanged } from 'firebase/auth';
// import { getFirestore, collection, query, where, getDocs } from 'firebase/firestore';
document.addEventListener('DOMContentLoaded', () => {
const generateCodeBtn = document.getElementById('generateCodeBtn');
const messageElement = document.getElementById('message');
const generatedCodeDisplay = document.getElementById('generatedCodeDisplay');
const displayCodeInput = document.getElementById('displayCode');
const copyCodeBtn = document.getElementById('copyCodeBtn');
const codeExpiryElement = document.getElementById('codeExpiry');
const manageCodesSection = document.getElementById('manageCodesSection');
const activeCodesList = document.getElementById('activeCodesList');
// TODO: Update these endpoints to your deployed Cloud Functions URLs
const CLOUD_FUNCTIONS_BASE_URL = 'https://us-central1-project-zirconium.cloudfunctions.net';
const GENERATE_CODE_ENDPOINT = `${CLOUD_FUNCTIONS_BASE_URL}/generateCode`;
const REVOKE_CODE_ENDPOINT = `${CLOUD_FUNCTIONS_BASE_URL}/revokeCode`;
// Function to get Firebase ID token, if authenticated
async function getAuthToken() {
try {
// Check if Firebase Auth is initialized and a user is signed in
if (typeof firebase !== 'undefined' && firebase.auth() && firebase.auth().currentUser) {
return await firebase.auth().currentUser.getIdToken();
} else {
// console.warn('Firebase Auth not initialized or user not signed in.');
return null;
}
} catch (error) {
console.error('Error getting auth token:', error);
return null;
}
}
generateCodeBtn.addEventListener('click', async () => {
messageElement.textContent = 'Generating code...';
messageElement.className = 'mt-4 text-center text-sm text-yellow-500';
generatedCodeDisplay.classList.add('hidden');
try {
const idToken = await getAuthToken();
if (!idToken) {
messageElement.textContent = 'Please sign in to generate a code.';
messageElement.className = 'mt-4 text-center text-sm text-red-500';
return;
}
const response = await fetch(GENERATE_CODE_ENDPOINT, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${idToken}` // Include auth token for function authorization
},
body: JSON.stringify({})
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.message || 'Failed to generate code.');
}
const data = await response.json();
if (data.success) {
displayCodeInput.value = data.code;
codeExpiryElement.textContent = `Expires: ${new Date(data.expiresAt).toLocaleString()}`;
messageElement.textContent = '';
generatedCodeDisplay.classList.remove('hidden');
displayCodeInput.select();
displayCodeInput.setSelectionRange(0, 99999);
try {
await navigator.clipboard.writeText(data.code);
messageElement.textContent = 'Code generated and copied to clipboard!';
messageElement.className = 'mt-4 text-center text-sm text-green-500';
} catch (err) {
console.warn('Failed to copy to clipboard automatically:', err);
messageElement.textContent = 'Code generated. Please copy it manually.';
messageElement.className = 'mt-4 text-center text-sm text-blue-500';
}
// Refresh the list of active codes
await fetchActiveCodes();
} else {
throw new Error(data.message || 'Unknown error during code generation.');
}
} catch (error) {
console.error('Error generating code:', error);
messageElement.textContent = `Error: ${error.message}`;
messageElement.className = 'mt-4 text-center text-sm text-red-500';
}
});
copyCodeBtn.addEventListener('click', async () => {
try {
await navigator.clipboard.writeText(displayCodeInput.value);
messageElement.textContent = 'Code copied to clipboard!';
messageElement.className = 'mt-4 text-center text-sm text-green-500';
} catch (err) {
console.error('Failed to copy code:', err);
messageElement.textContent = 'Failed to copy code. Please select and copy manually.';
messageElement.className = 'mt-4 text-center text-sm text-red-500';
}
});
// Function to fetch and display active codes for the current user
async function fetchActiveCodes() {
activeCodesList.innerHTML = 'Loading codes...
';
manageCodesSection.classList.remove('hidden');
try {
const idToken = await getAuthToken();
if (!idToken || !firebase.auth().currentUser) {
activeCodesList.innerHTML = 'Please sign in to view your codes.
';
return;
}
// Direct Firestore query example (requires security rule: allow read: if request.auth.uid == resource.data.userId; )
// This rule would need to be added to `other-required/firestore.rules` for this to work
const codesSnapshot = await firebase.firestore().collection('unlockCodes')
.where('userId', '==', firebase.auth().currentUser.uid)
.where('used', '==', false)
.where('revoked', '==', false)
.get();
if (codesSnapshot.empty) {
activeCodesList.innerHTML = 'No active codes found.
';
return;
}
activeCodesList.innerHTML = ''; // Clear loading message
codesSnapshot.forEach(doc => {
const codeData = doc.data();
const codeHash = doc.id; // The document ID is the code hash
const expiration = codeData.expiresAt ? new Date(codeData.expiresAt.toDate()).toLocaleString() : 'Never';
const deviceStatus = codeData.deviceFingerprint ? 'Linked' : 'Not linked';
const codeItem = document.createElement('div');
codeItem.className = 'bg-custom-darkest-gray p-3 rounded-lg flex items-center justify-between';
codeItem.innerHTML = `
${codeHash.substring(0, 8)}...
Expires: ${expiration} | Device: ${deviceStatus}
Revoke
`;
activeCodesList.appendChild(codeItem);
});
// Add event listeners for revoke buttons
document.querySelectorAll('.revoke-btn').forEach(button => {
button.addEventListener('click', async (event) => {
const hashToRevoke = event.target.dataset.codeHash;
messageElement.textContent = `Revoking code ${hashToRevoke.substring(0, 8)}...`;
messageElement.className = 'mt-4 text-center text-sm text-yellow-500';
try {
const idToken = await getAuthToken();
if (!idToken) {
messageElement.textContent = 'Please sign in to revoke codes.';
messageElement.className = 'mt-4 text-center text-sm text-red-500';
return;
}
const response = await fetch(REVOKE_CODE_ENDPOINT, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${idToken}`
},
body: JSON.stringify({ codeHash: hashToRevoke })
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.message || 'Failed to revoke code.');
}
const data = await response.json();
if (data.success) {
messageElement.textContent = `Code ${hashToRevoke.substring(0, 8)} revoked successfully!`;
messageElement.className = 'mt-4 text-center text-sm text-green-500';
await fetchActiveCodes(); // Refresh list
} else {
throw new Error(data.message || 'Unknown error during revocation.');
}
} catch (error) {
console.error('Error revoking code:', error);
messageElement.textContent = `Error: ${error.message}`;
messageElement.className = 'mt-4 text-center text-sm text-red-500';
}
});
});
} catch (error) {
console.error('Error fetching active codes:', error);
activeCodesList.innerHTML = `Error loading codes: ${error.message}. Ensure Firestore rules permit access.
`;
}
}
// Initial load of active codes
// We need to wait for Firebase Auth to be ready.
if (typeof firebase !== 'undefined' && firebase.auth()) {
firebase.auth().onAuthStateChanged(user => {
if (user) {
fetchActiveCodes();
} else {
activeCodesList.innerHTML = 'Sign in to manage your codes.
';
manageCodesSection.classList.remove('hidden');
}
});
} else {
activeCodesList.innerHTML = 'Firebase Auth not available. Cannot manage codes.
';
manageCodesSection.classList.remove('hidden');
}
});