Files
427e7578-d7bf-49c8-aee9-2dd…/app/app.vue

308 lines
9.9 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<style>
/* Modern dark theme with glassmorphism */
body {
font-family: system-ui, -apple-system, sans-serif;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
color: #fff;
margin: 0;
min-height: 100vh;
display: flex;
justify-content: center;
padding: 2rem;
}
header {
margin-bottom: 2rem;
text-align: left;
}
h1 {
margin: 0 0 0.5rem 0;
font-size: 2.5rem;
font-weight: bold;
color: #fff;
}
h2 {
margin: 0.5rem 0;
color: #fff;
}
input {
flex-grow: 1;
padding: 0.75rem;
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 8px;
font-size: 1rem;
background: rgba(255, 255, 255, 0.1);
color: #fff;
backdrop-filter: blur(10px);
}
input::placeholder {
color: rgba(255, 255, 255, 0.5);
}
button,
.button {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
padding: 0.75rem 1.5rem;
border-radius: 8px;
cursor: pointer;
font-weight: 600;
transition: all 0.3s ease;
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
}
button:hover,
.button:hover {
background: linear-gradient(135deg, #764ba2 0%, #667eea 100%);
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.6);
}
.status {
font-size: 0.9rem;
color: rgba(255, 255, 255, 0.7);
}
.status .connected {
color: #10b981;
font-weight: bold;
text-shadow: 0 0 10px rgba(16, 185, 129, 0.5);
}
.connectionFailed {
color: #ff6b6b;
font-weight: bold;
text-shadow: 0 0 10px rgba(255, 107, 107, 0.5);
}
.poll-container {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
padding: 2.5rem;
border-radius: 20px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
border: 1px solid rgba(255, 255, 255, 0.2);
width: 100%;
max-width: 600px;
}
.back-btn {
margin-left: 1rem;
padding: 0.5rem 1rem;
font-size: 0.85rem;
background: rgba(100, 116, 139, 0.8);
backdrop-filter: blur(10px);
}
.back-btn:hover {
background: rgba(100, 116, 139, 1);
}
/* Hide the actual file input */
input[type="file"] {
display: none;
}
/* Add subtle animations */
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.poll-container {
animation: fadeIn 0.5s ease-out;
}
/* Responsive design */
@media (max-width: 640px) {
body {
padding: 1rem;
}
.poll-container {
padding: 1.5rem;
}
h1 {
font-size: 2rem;
}
}
</style>
<template>
<div class="poll-container">
<header>
<h1 @click="activePollId = null" style="cursor:pointer">P2P Polling App 🗳</h1>
<div class="status">
<button v-if="activePollId" @click="activePollId = null" class="back-btn"> Back To List</button>
<span :class="{ 'connected': isConnected }">
{{ isConnected ? 'Synced' : 'Waiting for other Peers...' }}
</span>
<span> | Peers online: {{ connectedPeers }}</span>
<h2 v-if="connectionAttempFailed" class="connectionFailed"> Connection to Signaling Server Failed!</h2>
<div v-if="user===null" style="margin-top: 10px;">
<button @click="createUser">Create New User</button>
Or
<label title="Select Key File">
<span class="button">Login</span>
<input
type="file"
accept=".pem"
@change="loadUser"
/>
</label>
<div style="margin-top: 10px;">
<label title="Register Public Key">
<span class="button" style="font-size: 0.8rem; padding: 0.5rem 1rem;">Register Public Key</span>
<input
type="file"
accept=".pem"
@change="registerPublicKey"
/>
</label>
</div>
</div>
</div>
</header>
<main>
<PollList v-if="!activePollId" :userid="user?.userid" @select-poll="selectPoll" />
<Poll v-else :activePollId="activePollId" :userid="user?.userid" :poll-data="pollData" :addOption="addOption" :vote="vote"/>
</main>
</div>
</template>
<script setup lang="ts">
import { v4 as uuidv4 } from 'uuid';
import { generateUserKeyPair, exportPrivateKey, savePrivateKeyToFile, exportPublicKey, stringToCryptoKey } from '~/utils/crypto';
const activePollId = ref<string | null>(null);
const user = shallowRef<UserData | null>(null);
const { pollData, isConnected, connectionAttempFailed, connectedPeers, addOption, vote } = usePoll(activePollId,user);
const selectPoll = (id: string) => {
activePollId.value = id;
};
const createUser = async () => {
try {
const keypair : CryptoKeyPair = await generateUserKeyPair();
console.log('keypair:', keypair);
const uuid = uuidv4();
user.value = {
userid: uuid,
private_key: keypair.privateKey,
public_key: keypair.publicKey,
};
const prvKeyString = await exportPrivateKey(keypair.privateKey);
await savePrivateKeyToFile(prvKeyString,uuid+".pem")
const pubKeyString = await exportPublicKey(keypair.publicKey);
// Save public key to server
await $fetch(`/api/users/${uuid}`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.ADMIN_API_KEY || 'default-admin-key-change-in-production'}`
},
body: { public_key: pubKeyString }
});
// Also save public key to a file for backup
const pubPemHeader = "-----BEGIN PUBLIC KEY-----\n";
const pubPemFooter = "\n-----END PUBLIC KEY-----";
const pubFileContent = pubPemHeader + pubKeyString + pubPemFooter;
const blob = new Blob([pubFileContent], { type: "text/plain" });
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = uuid + "_public.pem";
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
console.log("User created successfully. Please save both key files.");
} catch (err) {
user.value = null
console.error("Failed to create new User!", err);
}
};
const loadUser = async (event: Event) => {
const target = event.target as HTMLInputElement;
const file = target.files?.[0];
if (file) {
try {
const content = await file.text();
console.log("File loaded: ");
if (file.name && content) {
try {
const uuid = file.name.replace(".pem", "").replace("_public", "");
// Standardize the string for the importer
const pkBase64 = content.replace(/-----BEGIN PRIVATE KEY-----|-----END PRIVATE KEY-----/g, "").replace(/\s+/g, "");
const key = await stringToCryptoKey(pkBase64, "private");
user.value = {
userid: uuid,
private_key: key,
public_key: undefined,
};
console.log("Login successful for:", uuid);
} catch (err) {
console.error("Crypto Import Error:", err);
alert("The file content is not a valid Private Key.");
}
}
} catch (e) {
console.error("Failed to read file", e);
}
}
};
const registerPublicKey = async (event: Event) => {
const target = event.target as HTMLInputElement;
const file = target.files?.[0];
if (file) {
try {
const content = await file.text();
if (file.name && content) {
try {
const uuid = file.name.replace(".pem", "").replace("_public", "");
console.log("Attempting to register public key for user:", uuid);
// Standardize the string for the importer
const pubKeyBase64 = content.replace(/-----BEGIN PUBLIC KEY-----|-----END PUBLIC KEY-----/g, "").replace(/\s+/g, "");
console.log("Public key length:", pubKeyBase64.length);
// Save public key to server
await $fetch(`/api/users/${uuid}`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.ADMIN_API_KEY || 'default-admin-key-change-in-production'}`
},
body: { public_key: pubKeyBase64 }
});
alert(`Public key registered successfully for user: ${uuid}`);
} catch (err: any) {
console.error("Registration Error:", err);
const errorMsg = err.data?.message || err.statusMessage || err.message || "Unknown error";
alert(`Failed to register public key: ${errorMsg}`);
}
}
} catch (e) {
console.error("Failed to read file", e);
alert("Failed to read file.");
}
}
};
</script>