From 8dbea528c1251060ca14b9495ee1c08d984d7e24 Mon Sep 17 00:00:00 2001 From: didier amyot Date: Tue, 21 Nov 2023 09:21:43 -0500 Subject: [PATCH] Continued stupidly overengineered project. --- src/auth.ts | 7 +-- src/client.ts | 84 +++++++++++++++++++++++++++++-- src/server.ts | 134 ++++++++++++++++++++++++++++++++++++++------------ src/shared.ts | 23 +++++++-- 4 files changed, 205 insertions(+), 43 deletions(-) diff --git a/src/auth.ts b/src/auth.ts index bd48ef1..10284cb 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -28,15 +28,16 @@ function veryInsecureEquality(a: string, b: string): boolean{ return a === b; } -export async function verifyAuth(auth: string): Promise{ - let ret = false; +export async function verifyAuth(auth: string): Promise{ + let ret: User|null = null; const authHash = await hash(auth); for (const user of users) { if (veryInsecureEquality(user.hash, authHash)) { // do not do that at home - ret = true; + ret = user; } } return ret; } + console.log(await hash("2468")) \ No newline at end of file diff --git a/src/client.ts b/src/client.ts index b404369..ba4987f 100644 --- a/src/client.ts +++ b/src/client.ts @@ -1,11 +1,15 @@ // @deno-types="https://esm.sh/@types/mithril@2.2.4" import m from "https://esm.sh/mithril@2.2.2" import {verifyAuth} from "./auth.ts"; -import {Ballot, Question} from "./shared.ts"; +import {Ballot, BallotTally, Question, QuestionTally} from "./shared.ts"; let ballots: Ballot[] = []; +function isAdmin(): boolean{ + return window.localStorage.getItem("isAdmin") === 'true'; +} + type ProgressBarAttr = { question: Question, onchange: () => void, @@ -144,8 +148,10 @@ class PasscodeView implements m.ClassComponent { async verifyPasscode(event: Event): Promise { let password = event.target.value; - if(await verifyAuth(password)){ + let user = await verifyAuth(password) + if(user){ window.localStorage.setItem("userId", password); + window.localStorage.setItem("isAdmin", user.isAdmin); m.route.set(`/voter-home/`); } } @@ -160,9 +166,68 @@ class PasscodeView implements m.ClassComponent { } class ResultView implements m.ClassComponent{ - view(){ - return m("div", "one two three"); + ballotTallies: BallotTally[] = []; + allowedToView = true; + + async oninit(): Promise { + this.allowedToView = isAdmin() + if(!this.allowedToView){ + this.allowedToView = false; + return; + } + const userId = window.localStorage.getItem("userId"); + this.ballotTallies = await m.request(`/api/v1/results`); } + + resultTablesHeaders(bts: BallotTally[]): m.Attributes{ + return m('thead', + m("tr", m("th"), bts.map(bt => m("th", {colspan: bt.questionsTally.length}, bt.name))), + m("tr", m("th", "User"), bts.flatMap(bt => bt.questionsTally.map(qt => m("th", qt.name)))), + ); + } + + getUserIds(bts: BallotTally[]): Set{ + const ret = bts.flatMap(bt => bt.questionsTally) + .flatMap(qt => qt.answers) + .map(a => a.userId); + return new Set(ret) + } + + getUserResult(bt: BallotTally, userId: string): string[]{ + return bt.questionsTally.map(qt => qt.answers.find(a => a.userId === userId)?.value?.toString()??"") + } + + *getResultRowData(bts: BallotTally[]): Generator<{data: string[], userId: string }>{ + for(const userId of this.getUserIds(bts)){ + const data = bts.flatMap(bt => this.getUserResult(bt, userId)); + yield {data, userId}; + } + } + + userCell(userId: string): m.Attributes{ + return m('th', {style: {color: 'transparent', 'text-shadow': '0 0 8px #000'}, title: userId}, userId) + } + resultBody(bts: BallotTally[]): m.Attributes{ + const rowData = Array.from(this.getResultRowData(bts)); + return m('tbody', rowData.map(data => m("tr", this.userCell(data.userId), data.data.map(d => m("td", d))) )); + } + + resultTables(fakeResults: BallotTally[]): m.Attributes { + return m('table.table.is-fullwidth.is-bordered', + [this.resultTablesHeaders(fakeResults), + this.resultBody(fakeResults)]) + } + + view(){ + if(!this.allowedToView){ + return m('div.hero.is-fullheight', + m("div.hero-body.has-text-centered", + m("div.container", + m("h1.title.is-1", ":( you are not allow to view this page")))); + } + return m("div", this.resultTables(this.ballotTallies)); + } + } class FrameView> implements m.ClassComponent{ @@ -181,6 +246,13 @@ class FrameView> implements m.ClassComponent): m.Children { return m("div", [ m("nav.navbar.is-primary", [ @@ -188,8 +260,10 @@ class FrameView> implements m.ClassComponent this.logout()}, "logout"))) + m("a.is-clickable.button", {onclick: () => this.logout()}, "logout"))) ]), m(this.embedeed, vnode.attrs) ]); diff --git a/src/server.ts b/src/server.ts index 8a721b1..766433a 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,7 +1,7 @@ import { Application, Router } from "https://deno.land/x/oak/mod.ts"; import {createBundle} from "./bundler.ts"; import {Context} from "https://deno.land/x/oak@v12.2.0/context.ts"; -import {Ballot, createDefaultBallots, Question} from "./shared.ts"; +import {Ballot, createDefaultBallots, Question, BallotTally, QuestionTally} from "./shared.ts"; import {verifyAuth} from "./auth.ts"; type QuestionAnswer = { @@ -33,33 +33,50 @@ router.get("/", (context) => { `; }); +type IndexPrefix = ["soevs", "v1"]; -function getIndexPrefix(): string[]{ - return ["tatoo-vote", "v1"]; +type UserPrefix = [... IndexPrefix, "user", userId: string]; + +type UserBallotQuestionIndex = [...UserPrefix, "ballot", ballotId: string, "question", questionId: string]; + +type BallotIndex = [...IndexPrefix, "ballot", ballotId: string]; + +type BallotQuestionIndex = [...BallotIndex , "question", questionId: string]; + +type BallotQuestionUserIndex = [...BallotQuestionIndex, "user", userId: string]; + +function getIndexPrefix(): IndexPrefix{ + return ["soevs", "v1"]; } -function getUserIndex(userId: string): string[] { +function getUserIndex(userId: string): UserPrefix { const prefix = getIndexPrefix(); - const suffix = ["user", userId]; - return prefix.concat(suffix); + return [...prefix, "user", userId]; } -function getBallotQuestionIndex(ballot: Ballot, question: Question): string[]{ +function getBallotIndex(ballot: Ballot): BallotIndex{ const prefix = getIndexPrefix(); - const suffix = ["ballot", ballot.id, "question", question.id]; - return prefix.concat(suffix); + return [...prefix, "ballot", ballot.id]; } -function getBallotQuestionUserIndex(userId: string, ballot: Ballot, question: Question): string[]{ +function getBallotIndexByBallotId(ballotId: string): BallotIndex{ + const bogusBallot: Ballot = createDefaultBallots('bogus').find(x => x.id === ballotId)!; + return getBallotIndex(bogusBallot); +} + +function getBallotQuestionIndex(ballot: Ballot, question: Question): BallotQuestionIndex{ + const prefix = getBallotIndex(ballot); + return [...prefix, "question", question.id]; +} + +function getBallotQuestionUserIndex(userId: string, ballot: Ballot, question: Question): BallotQuestionUserIndex{ const prefix = getBallotQuestionIndex(ballot, question); - const suffix = ["user", userId]; - return prefix.concat(suffix); + return [...prefix, "user", userId]; } -function getUserBallotQuestionIndex(userId: string, ballot: Ballot, question: Question): string[]{ +function getUserBallotQuestionIndex(userId: string, ballot: Ballot, question: Question): UserBallotQuestionIndex{ const prefix = getUserIndex(userId); - const suffix = ["ballot", ballot.id, "question", question.id]; - return prefix.concat(suffix); + return [...prefix, "ballot", ballot.id, "question", question.id]; } async function persistBallots(userId: string, ballots: Ballot[]): Promise { @@ -73,9 +90,9 @@ async function persistBallots(userId: string, ballots: Ballot[]): Promise } } -function setValueInQuestion(ballots: Ballot[], key: string[], value: number){ - const questionId = key[7]; - const ballotId = key[5]; +function setValueInQuestion(ballots: Ballot[], key: UserBallotQuestionIndex, value: number){ + const questionId = key[7]; // UserBallotQuestionIndex.questionId + const ballotId = key[5]; // UserBallotQuestionIndex.ballotId for(const ballot of ballots){ for(const question of ballot.questions){ if(question.id === questionId && ballot.id === ballotId){ @@ -86,26 +103,78 @@ function setValueInQuestion(ballots: Ballot[], key: string[], value: number){ return ballots; } -async function getBallots(userId: string): Promise{ - let ballots = createDefaultBallots(); +async function getUserBallots(userId: string): Promise{ + let ballots = createDefaultBallots(userId); const kv = await Deno.openKv(); const entries = kv.list({ prefix: getUserIndex(userId) }); for await (const entry of entries) { - const key = entry.key; // ["users", "alice"] - const value = entry.value.value; // { name: "Alice" } - ballots = setValueInQuestion(ballots, key, value); + const key = entry.key; + const value = entry.value.value; + ballots = setValueInQuestion(ballots, key as UserBallotQuestionIndex, value); } return ballots; } -async function getBallotResults(){ - let ballots = createDefaultBallots(); - for(const ballot of ballots){ +function createEmptyResult(b: Ballot): BallotTally { + return { + ballotId: b.id, + name: b.name, + questionsTally: [], + }; +} +async function compactifyQuestionTally(tallies: QuestionTally[]): Promise{ + const ret: QuestionTally[] = []; + for(const subTallies of Object.values(Object.groupBy(tallies, (t) => t.questionId))){ + ret.push({ + questionId: subTallies[0].questionId, + name: subTallies[0].name, + longName: subTallies[0].longName, + answers: subTallies.map(t => t.answers).flat(), + }); + } + return ret; +} + +function getQuestionsMappedById(ballotId: string): Record{ + const templateBallot: Ballot = createDefaultBallots('bogus').find(x => x.id === ballotId)!; + return templateBallot.questions.reduce((acc, x: Question) => {acc[x.id] = x; return acc}, {}); +} + +function createSimpleQuestionTally(question: Question, userId: string, value: number): QuestionTally{ + return { + questionId: question.id, + name: question.name, + longName: question.longName, + answers: [{userId, value}], + } +} +async function* getBallotsByBallotId(ballotId: string): AsyncGenerator{ + const questions = getQuestionsMappedById(ballotId); + const kv = await Deno.openKv(); + const entries = kv.list({ prefix: getBallotIndexByBallotId(ballotId) }); + for await (const entry of entries) { + const key = entry.key as BallotQuestionUserIndex; + const questionId = key[5]; + const question = questions[questionId]; + yield createSimpleQuestionTally(question, key[7], entry.value.value); } } -router.post("/api/v1/vote/:userid", async (ctx: Context)=> { +async function getQuestionTallies(ballotId: string): Promise { + const ballots = getBallotsByBallotId(ballotId); + return compactifyQuestionTally(await Array.fromAsync(ballots)); +} + +async function getBallotTallies(userId: string): Promise{ + const ret = createDefaultBallots(userId).map(createEmptyResult); + for(const res of ret) { + res.questionsTally = await getQuestionTallies(res.ballotId); + } + return ret; +} + +router.post("/api/v1/vote/:userid", async (ctx): Promise => { const userId: string = ctx?.params?.userid??""; if(!await verifyAuth(userId)){ ctx.throw(403) @@ -119,16 +188,17 @@ router.post("/api/v1/vote/:userid", async (ctx: Context)=> { ctx.response.body = userId; }); -router.get("/api/v1/vote/:userid", async (ctx: Context)=> { +router.get("/api/v1/vote/:userid", async (ctx)=> { const userId = ctx?.params?.userid??""; if(!await verifyAuth(userId)){ ctx.throw(403) } - ctx.response.body = await getBallots(userId); + ctx.response.body = await getUserBallots(userId); }); -router.get("/api/v1/results", async (ctx: Context) =>{ - ctx.response.body = await getBallotResults(); +router.get("/api/v1/results", async (context): Promise => { + const userId = '1234'; + context.response.body = await getBallotTallies(userId); }); router.get("/client.js", async (context): Promise => { @@ -153,3 +223,5 @@ app.use((ctx) => { await app.listen({ port: 3333 }); + + diff --git a/src/shared.ts b/src/shared.ts index ec0df35..4ee2b18 100644 --- a/src/shared.ts +++ b/src/shared.ts @@ -9,9 +9,24 @@ export type Question = { export type Ballot = { id: string, name: string, + userId: string, questions: Question[], } +export type QuestionTally = { + name: string, + longName: string, + questionId: string, + answers: {userId: string, value: number}[] +} + +export type BallotTally = { + ballotId: string, + name: string, + questionsTally: QuestionTally[], + +} + function defaultQuestions(): Question[]{ return [ {id: "5242", name: "readme", value: 25, longName: "Qualité du README"}, @@ -21,10 +36,10 @@ function defaultQuestions(): Question[]{ } -export function createDefaultBallots(): Ballot[]{ +export function createDefaultBallots(userId: string): Ballot[]{ return [ - {id: "5gg64", name: "Les chacals de laval", questions: defaultQuestions()}, - {id: "ys68", name: "Les rapides de blainville", questions: defaultQuestions()}, - {id: "ssw", name: "Les pas-beaux du plateau", questions: defaultQuestions()}, + {id: "5gg64", name: "Les chacals de laval", userId: userId, questions: defaultQuestions()}, + {id: "ys68", name: "Les rapides de blainville", userId: userId, questions: defaultQuestions()}, + {id: "ssw", name: "Les pas-beaux du plateau", userId: userId, questions: defaultQuestions()}, ]; }