Files

295 lines
9.3 KiB
JavaScript
Executable File

let currentUser = null;
let currentChallenge = null;
let authMethod = null;
function escapeHtml(str) {
const div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
}
// --- Matrix auth ---
function goToMatrixLogin() {
const slug = window.location.pathname.split('/').filter(Boolean)[0];
window.location.href = '/' + slug + '/matrix-login';
}
function signOut() {
sessionStorage.removeItem('matrix_session_token');
currentUser = null;
authMethod = null;
window.location.reload();
}
function showNostrAuth() {
document.getElementById('auth_method_select').style.display = 'none';
document.getElementById('nostr_auth_section').style.display = 'block';
}
async function checkMatrixSession() {
const sessionToken = sessionStorage.getItem('matrix_session_token');
if (!sessionToken) return false;
try {
const slug = window.location.pathname.split('/').filter(Boolean)[0];
const resp = await fetch('/' + slug + '/matrixVerify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ session_token: sessionToken })
});
if (resp.ok) {
const data = await resp.json();
currentUser = { displayName: data.display_name, sessionToken: sessionToken };
authMethod = 'matrix';
document.getElementById('auth_section').style.display = 'none';
document.getElementById('vote_section').style.display = 'block';
document.getElementById('logged_in_as').textContent = 'Signed in as: ' + data.display_name;
loadSubmissions();
return true;
} else {
sessionStorage.removeItem('matrix_session_token');
}
} catch (e) {
sessionStorage.removeItem('matrix_session_token');
}
return false;
}
// --- Nostr auth ---
function hasNip07() {
return typeof window.nostr !== 'undefined' && window.nostr !== null;
}
async function connectNip07() {
if (!hasNip07()) {
setAuthStatus('No Nostr browser extension detected. Enter your npub manually.', true);
return;
}
try {
const hexKey = await window.nostr.getPublicKey();
const resp = await fetch('encode_npub', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ hex: hexKey })
});
if (resp.ok) {
const data = await resp.json();
document.getElementById('npub_input').value = data.npub;
document.getElementById('nip07_badge').style.display = 'block';
setAuthStatus('Connected! Click "Sign In to Vote" to continue.');
} else {
document.getElementById('npub_input').value = hexKey;
setAuthStatus('Connected (using hex key). Click "Sign In to Vote".');
}
} catch (err) {
setAuthStatus('Error: ' + err.message, true);
}
}
async function authenticate() {
const npub = document.getElementById('npub_input').value.trim();
if (!npub) {
setAuthStatus('Enter your npub or connect AlbyHub first.', true);
return;
}
setAuthStatus('Requesting challenge...');
try {
const resp = await fetch('challenge', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ npub: npub })
});
if (resp.status === 404) {
setAuthStatus('Npub not linked. Please <a href="link">link your identity</a> first.', true);
return;
}
if (!resp.ok) {
const text = await resp.text();
setAuthStatus('Error: ' + text, true);
return;
}
const data = await resp.json();
currentChallenge = data.challenge;
if (!hasNip07()) {
setAuthStatus('No Nostr extension. Please install AlbyHub to sign.', true);
return;
}
setAuthStatus('Please sign the challenge in your extension...');
const signedEvent = await window.nostr.signEvent({
created_at: Math.floor(Date.now() / 1000),
kind: 1,
tags: [],
content: currentChallenge
});
currentUser = {
npub: npub,
displayName: data.display_name,
signedEvent: signedEvent
};
authMethod = 'nostr';
document.getElementById('auth_section').style.display = 'none';
document.getElementById('vote_section').style.display = 'block';
document.getElementById('logged_in_as').textContent = 'Signed in as: ' + data.display_name;
loadSubmissions();
} catch (err) {
if (err.message && err.message.includes('denied')) {
setAuthStatus('Signature request denied.', true);
} else {
setAuthStatus('Error: ' + err.message, true);
}
}
}
function setAuthStatus(msg, isError) {
const el = document.getElementById('auth_status');
el.innerHTML = msg;
el.className = isError ? 'error' : 'success';
}
// --- Submissions ---
async function loadSubmissions() {
try {
const response = await fetch('submissions');
const data = await response.json();
const submissionList = document.getElementById('suggestion_list');
submissionList.innerHTML = '';
if (data.Submissions && data.Submissions.length > 0) {
data.Submissions.forEach((sub) => {
const li = document.createElement('li');
li.innerHTML = `
<span class="suggestor">${escapeHtml(sub.submitter)} SUGGESTED</span> :
<span class="suggestion">${escapeHtml(sub.submission)}</span>
<input type="radio" name="vote" value="${escapeHtml(sub.submission)}" data-submitter="${escapeHtml(sub.submitter)}" data-time="${sub.submission_time}">
`;
submissionList.appendChild(li);
});
} else {
submissionList.innerHTML = '<li>No submissions available</li>';
}
} catch (error) {
console.error('Error loading submissions:', error);
document.getElementById('suggestion_list').innerHTML = '<li>Error loading submissions</li>';
}
}
// --- Voting ---
async function submitVote() {
if (!currentUser) {
alert('Please sign in first');
return;
}
const selectedRadio = document.querySelector('input[name="vote"]:checked');
if (!selectedRadio) {
alert('Please select a title to vote for');
return;
}
const voteData = {
selected_submission: selectedRadio.value,
submitter: selectedRadio.dataset.submitter,
submission_time: parseInt(selectedRadio.dataset.time)
};
try {
if (authMethod === 'matrix') {
const resp = await fetch('matrixVote', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
session_token: currentUser.sessionToken,
...voteData
})
});
if (resp.ok) {
alert('Vote submitted successfully!');
selectedRadio.checked = false;
} else {
const text = await resp.text();
alert('Error: ' + text);
}
} else if (authMethod === 'nostr') {
const challengeResp = await fetch('challenge', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ npub: currentUser.npub })
});
if (!challengeResp.ok) {
const text = await challengeResp.text();
alert('Error getting challenge: ' + text);
return;
}
const challengeData = await challengeResp.json();
const challengeMsg = challengeData.challenge;
const signedEvent = await window.nostr.signEvent({
created_at: Math.floor(Date.now() / 1000),
kind: 1,
tags: [],
content: challengeMsg
});
const voteResp = await fetch('verifyVote', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
npub: currentUser.npub,
challenge: challengeMsg,
...voteData,
signed_event: signedEvent
})
});
if (voteResp.ok) {
alert('Vote submitted successfully!');
selectedRadio.checked = false;
} else {
const text = await voteResp.text();
alert('Error: ' + text);
}
}
} catch (error) {
console.error('Error submitting vote:', error);
alert('Error submitting vote: ' + error.message);
}
}
// --- Init ---
document.addEventListener('DOMContentLoaded', async () => {
setInterval(() => {
if (currentUser) loadSubmissions();
}, 30000);
const matrixAuthed = await checkMatrixSession();
if (matrixAuthed) return;
if (hasNip07()) {
document.getElementById('nip07_badge').style.display = 'block';
}
});