add vote uniqueness, public key caching, relative poll timeframe, auth/rate limiting, modern UI styling, and error handling
This commit is contained in:
94
server/api/polls/[id].ts
Normal file
94
server/api/polls/[id].ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import * as Y from 'yjs';
|
||||
// server/api/polls/[id].ts
|
||||
export default defineEventHandler(async (event) => {
|
||||
const method = event.node.req.method;
|
||||
const pollId = getRouterParam(event, 'id');
|
||||
|
||||
// We use Nitro's built-in storage.
|
||||
// 'polls' is the storage namespace.
|
||||
const storage = useStorage('polls');
|
||||
|
||||
if (!pollId) {
|
||||
throw createError({ statusCode: 400, statusMessage: 'Poll ID required' });
|
||||
}
|
||||
|
||||
// GET: Fetch the saved Yjs document state
|
||||
if (method === 'GET') {
|
||||
const data = await storage.getItem(`poll:${pollId}`);
|
||||
|
||||
// Fetch all user public keys to include in response
|
||||
const userStorage = useStorage('users');
|
||||
const publicKeys: Record<string, string> = {};
|
||||
|
||||
// Get all user keys from storage
|
||||
const keys = await userStorage.getKeys();
|
||||
for (const key of keys) {
|
||||
if (key.startsWith('user:')) {
|
||||
const userId = key.replace('user:', '');
|
||||
const publicKey = await userStorage.getItem(key);
|
||||
if (publicKey) {
|
||||
publicKeys[userId] = String(publicKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Return the array of numbers (or null if it doesn't exist yet) along with public keys
|
||||
return { update: data || null, publicKeys };
|
||||
}
|
||||
|
||||
// POST: Save a new Yjs document state
|
||||
if (method === 'POST') {
|
||||
const body = await readBody(event);
|
||||
|
||||
if (body.update && Array.isArray(body.update)) {
|
||||
// create a temp Y.Doc to encode the Data
|
||||
const tempDoc = new Y.Doc();
|
||||
Y.applyUpdate(tempDoc, new Uint8Array(body.update));
|
||||
const yMap = tempDoc.getMap('shared-poll');
|
||||
const pollData = yMap.toJSON();
|
||||
|
||||
// verify pollData
|
||||
for(var option in pollData){
|
||||
const votes = pollData[option] || [];
|
||||
var pubKeys: CryptoKey[] = [];
|
||||
|
||||
const verifyAllVotesForOption = async (votes: SignedData<VoteData>[]) => {
|
||||
console.log("verifying votes for option " + option,votes);
|
||||
// check last votes first. if there is something wrong, its likely in the last vote.
|
||||
for (let i = votes.length-1; i >= 0 ; i--) {
|
||||
const userStorage = useStorage('users');
|
||||
const votePubKeyString = await userStorage.getItem(`user:${votes[i]?.data.userid}`);
|
||||
|
||||
// Check if public key exists for this user
|
||||
if (!votePubKeyString) {
|
||||
console.error(`Error! No public key found for user: ${votes[i]?.data.userid}`);
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: `Vote from unknown user: ${votes[i]?.data.userid}. User must be registered to vote.`
|
||||
});
|
||||
}
|
||||
|
||||
const votePubKey = await stringToCryptoKey(String(votePubKeyString),'public')
|
||||
const isValid = await verifyChainedVote(votes, i,votePubKey);
|
||||
if(!isValid){
|
||||
console.error("Error! Invalid Vote at: " + i,votes)
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
const verified = await verifyAllVotesForOption(votes);
|
||||
if(!verified){
|
||||
console.error("Failed to verify option: "+option)
|
||||
throw createError({ statusCode: 400, statusMessage: 'PollData contains unverifyable content!' });
|
||||
}
|
||||
}
|
||||
|
||||
// Save the binary update (sent as an array of numbers) to storage
|
||||
await storage.setItem(`poll:${pollId}`, body.update);
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
throw createError({ statusCode: 400, statusMessage: 'Invalid update payload' });
|
||||
}
|
||||
});
|
||||
15
server/api/polls/index.get.ts
Normal file
15
server/api/polls/index.get.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
// server/api/polls/index.get.ts
|
||||
export default defineEventHandler(async () => {
|
||||
const storage = useStorage('polls');
|
||||
|
||||
// Get all keys in the 'polls' namespace
|
||||
const allKeys = await storage.getKeys();
|
||||
|
||||
// Filter for our specific poll prefix and strip it for the UI
|
||||
// poll:my-id -> my-id
|
||||
const polls = allKeys
|
||||
.filter(key => key.startsWith('poll:'))
|
||||
.map(key => key.replace('poll:', ''));
|
||||
|
||||
return { polls };
|
||||
});
|
||||
Reference in New Issue
Block a user