From 9de31fac1b6c6c36dee1a60ef9a26ac2d898e816 Mon Sep 17 00:00:00 2001 From: didier amyot Date: Mon, 20 Nov 2023 20:24:47 -0500 Subject: [PATCH] going somewhere --- src/auth.ts | 45 +++++++++++++++++-- src/client.ts | 114 ++++++++++++++++++++++++++++--------------------- src/server.ts | 73 +++++++++++++++++++++++-------- src/shared.ts | 35 +++++++++++---- src/users.json | 3 ++ 5 files changed, 193 insertions(+), 77 deletions(-) create mode 100644 src/users.json diff --git a/src/auth.ts b/src/auth.ts index 33c1f31..bd48ef1 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -1,3 +1,42 @@ -export function verifyAuth(auth: string): boolean{ - return auth.length === 4; -} \ No newline at end of file +import { encodeBase64 } from "https://deno.land/std/encoding/base64.ts" + +const salt = "Honey?I_am/home"; + +type User = { + hash: string, + isAdmin: boolean, +} +const users: User[] = [ + {hash: "2jztmK1WMRc4ZSUN/jxbnsO/IjlWMFvxh1i/AMeLM3c=", isAdmin: true}, + {hash: "mipaAgOTc14kkI8oJltUCJjoUEBufS79H8IJK1w3zBE=", isAdmin: false}, +] + +export async function hash(msg: string): Promise{ + const enc = new TextEncoder(); + const key = await crypto.subtle.importKey( + "raw", + enc.encode(salt), + {name: "HMAC", hash: "SHA-256"}, + true, + ["sign", "verify"], + ) + const ret = await crypto.subtle.sign("HMAC", key, enc.encode(msg)); + return encodeBase64(new Uint8Array(ret)); +} + +function veryInsecureEquality(a: string, b: string): boolean{ + return a === b; +} + +export async function verifyAuth(auth: string): Promise{ + let ret = false; + const authHash = await hash(auth); + for (const user of users) { + if (veryInsecureEquality(user.hash, authHash)) { // do not do that at home + ret = true; + } + } + return ret; +} + +console.log(await hash("2468")) \ No newline at end of file diff --git a/src/client.ts b/src/client.ts index 296cf63..c263b3f 100644 --- a/src/client.ts +++ b/src/client.ts @@ -1,27 +1,25 @@ // @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 {Scores, Ballot} from "./shared.ts"; +import {Ballot, Question} from "./shared.ts"; -const scores: Scores = { - readme: 25, - diagrams: 50, - convinced: 75 -}; - -const ballots: Ballot[] = [ - {id: 8787, name: "Les chacals de laval", scores: {...scores}}, - {id: 18167, name: "Les rapides de blainville", scores: {...scores}}, - {id: 17165, name: "Les pas-beaux du plateau", scores: {...scores}}, -] +let ballots: Ballot[] = []; type ProgressBarAttr = { - key: keyof Scores, - scores: Scores, + question: Question, onchange: () => void, } +function getBallotURL(): string{ + const userId = window.localStorage.getItem("userId"); + if(!userId){ + m.route.set("/"); + throw new Error("User not logged in"); + } + return `/api/v1/vote/${userId}`; +} + class ProgressBar implements m.ClassComponent{ closestFivePercent(n: number): number{ @@ -43,41 +41,34 @@ class ProgressBar implements m.ClassComponent{ } view({attrs}: m.CVnode){ const onclickFn = (e: Event) => { - attrs.scores[attrs.key] = this.computeClickedScored(e); + attrs.question.value = this.computeClickedScored(e); attrs.onchange(); } - const value = attrs.scores[attrs.key]; + const value = attrs.question.value; return m("progress.progress.is-primary", {value: value, max: 100, onclick: onclickFn, title: `${value}%`}, `${value}%`) } } -type VoteViewAttr = { - ballotId: number, +type BallotViewAttr = { + ballot: Ballot, onchange?: () => void, }; -class BallotView implements m.ClassComponent { +class BallotView implements m.ClassComponent { - voteFor(ballot: Ballot, title: string, key: string, onchange: () => void){ + voteFor(question: Question, onchange: () => void){ return m("div.columns", - m("div.column", title, ":"), + m("div.column", question.longName, ":"), m("div.column.is-two-thirds", - m(new ProgressBar(), {scores: ballot.scores, key: key, onchange: onchange}))); + m(new ProgressBar(), {question: question, onchange: onchange}))); } - view(vnode: m.CVnode){ - const ballotId = vnode.attrs.ballotId; + view(vnode: m.CVnode){ const onchange = vnode.attrs.onchange ?? function(){}; - const ballot = ballots.find(ballot => ballot.id.toString() === ballotId.toString()); - if(!ballot){ - return []; - } - return [ - this.voteFor(ballot, "Qualité du README", 'readme', onchange), - this.voteFor(ballot, "Qualité des diagrammes", 'diagrams', onchange), - this.voteFor(ballot, "Êtes-vous convaincu?", 'convinced', onchange)] + const questions = vnode.attrs.ballot.questions??[]; + return questions.map(question => this.voteFor(question, onchange)); } } @@ -96,6 +87,10 @@ type MainVoteViewAttr = { class MainVoteView implements m.ClassComponent{ + async oninit(){ + ballots = await m.request(getBallotURL()); + } + async persistBallots(): Promise{ const params: RequestInit = { method: 'POST', @@ -104,28 +99,40 @@ class MainVoteView implements m.ClassComponent{ }, body: JSON.stringify(ballots) }; - const resp = await fetch("/api/v1/vote/kkk", params); + const resp = await fetch(getBallotURL(), params); console.log(resp.status) } - view(vnode: m.CVnode){ - const name = "Les chacals de laval" + emptyState(): m.Vnode{ + return m("div", "No ballot found"); + } + + view(vnode: m.CVnode): m.Vnode { + const ballot = ballots.find( ballot => ballot.id.toString() === vnode.attrs.ballotId.toString()); + if(!ballot){ + return this.emptyState(); + } return m("div.columns.is-centered", m("div.column.is-two-thirds", - m("h1.title.is-1", m(m.route.Link, {href: "/"}, `Votons - ${name}`)), - m(new BallotView(), {ballotId: vnode.attrs.ballotId, onchange: this.persistBallots}), + m("h1.title.is-1", m(m.route.Link, {href: "/voter-home/"}, `Votons - ${ballot.name}`)), + m(new BallotView(), {ballotId: vnode.attrs.ballotId, + ballot: ballot, + onchange: this.persistBallots}), m(new Submit()))); } } -class MainView{ +class MainView implements m.ClassComponent{ + + async oninit(){ + ballots = await m.request(getBallotURL()); + } + createBallotRow(ballot: Ballot){ - return m('tr', [ - m('td', m(m.route.Link, {href: `${m.route.get()}vote/${ballot.id}/`}, ballot.name)), - m('td', ballot.scores.readme), - m('td', ballot.scores.diagrams), - m('td', ballot.scores.convinced), - ]) + const ballotURL = `${m.route.get()}vote/${ballot.id}/`; + return m('tr', + m('td', m(m.route.Link, {href: ballotURL}, ballot.name)), + ballot.questions.map(q => m("td", q.value))); } votesTable(ballots: Ballot[]){ @@ -137,15 +144,15 @@ class MainView{ view(){ return m("div.columns.is-centered", m("div.column.is-two-thirds", - this.votesTable(ballots))) + this.votesTable(ballots))); } } -class PasscodeView{ +class PasscodeView implements m.ClassComponent { - verifyPasscode(event: Event): void { + async verifyPasscode(event: Event): Promise { let password = event.target.value; - if(verifyAuth(password)){ + if(await verifyAuth(password)){ window.localStorage.setItem("userId", password); m.route.set(`/voter-home/`); } @@ -160,6 +167,17 @@ class PasscodeView{ } } +class FrameView implements m.ClassComponent{ + + view(vnode: m.Vnode): m.Children { + return m("div", [ + "hello world", + + ]) + } + + +} m.route(window.document.body, "/", { diff --git a/src/server.ts b/src/server.ts index 345a1c5..530c524 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,7 +1,12 @@ 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} from "./shared.ts"; +import {Ballot, createDefaultBallots, Question} from "./shared.ts"; +import {verifyAuth} from "./auth.ts"; + +type QuestionAnswer = { + value: number +} const router = new Router(); @@ -28,41 +33,73 @@ router.get("/", (context) => { `; }); -function getUserIndex(userId: string, ballotId: string, questionId: string){ - return ["tatoo-vote", - "v1", - "results", - "user", - userId, - "ballot", - ballotId, - "question", - questionId] +function getUserIndex2(userId: string): string[] { + return ["tatoo-vote", "v1", "results", "user", userId]; } -async function persistBallots(userId: any, ballots: Ballot[]): Promise { +function getUserIndex(userId: string, ballot: Ballot, question: Question): string[]{ + const prefix = getUserIndex2(userId); + const suffix = ["ballot", ballot.id, "question", question.id]; + return prefix.concat(suffix); +} + +async function persistBallots(userId: string, ballots: Ballot[]): Promise { const kv = await Deno.openKv(); for(const ballot of ballots){ - for(const question of Object.keys(ballot.scores)){ - const k = getUserIndex(userId, ballot.id, question); - const v = {value: ballot.scores[question]}; + for(const question of ballot.questions){ + const k = getUserIndex(userId, ballot, question); + const v: QuestionAnswer = {value: question.value}; await kv.set(k, v); } - } } +function setValueInQuestion(ballots: Ballot[], key: string[], value: number){ + const questionId = key[8]; + const ballotId = key[6]; + for(const ballot of ballots){ + for(const question of ballot.questions){ + if(question.id === questionId && ballot.id === ballotId){ + question.value = value; + } + } + } + return ballots; +} + +async function getBallots(userId: string): Promise{ + let ballots = createDefaultBallots(); + const kv = await Deno.openKv(); + const entries = kv.list({ prefix: getUserIndex2(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); + } + return ballots; + +} + router.post("/api/v1/vote/:userid", async (ctx: Context)=> { - const userId = ctx?.params?.userid??""; + const userId: string = ctx?.params?.userid??""; + if(!await verifyAuth(userId)){ + ctx.throw(403) + } if(!ctx.request.hasBody){ ctx.throw(415); } const body = await ctx.request.body().value; const ballots = body as Ballot[]; await persistBallots(userId, ballots); - console.log(body); ctx.response.body = userId; +}); +router.get("/api/v1/vote/:userid", async (ctx: Context)=> { + const userId = ctx?.params?.userid??""; + if(!await verifyAuth(userId)){ + ctx.throw(403) + } + ctx.response.body = await getBallots(userId); }); router.get("/client.js", async (context): Promise => { diff --git a/src/shared.ts b/src/shared.ts index 0c52fe4..ec0df35 100644 --- a/src/shared.ts +++ b/src/shared.ts @@ -1,11 +1,30 @@ -export type Scores = { - readme: number, - diagrams: number, - convinced: number, -}; + +export type Question = { + name: string, + longName: string, + id: string, + value: number, +} export type Ballot = { - id: number, + id: string, name: string, - scores: Scores, -} \ No newline at end of file + questions: Question[], +} + +function defaultQuestions(): Question[]{ + return [ + {id: "5242", name: "readme", value: 25, longName: "Qualité du README"}, + {id: "shs72g3", name: "diagrams", value: 50, longName: "Qualité des diagrammes"}, + {id: "y76sfg", name: "convinced", value: 75, longName: "Êtes-vous convaincu?"}, + ]; +} + + +export function createDefaultBallots(): 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()}, + ]; +} diff --git a/src/users.json b/src/users.json new file mode 100644 index 0000000..2de2463 --- /dev/null +++ b/src/users.json @@ -0,0 +1,3 @@ +[ + {"hash": "", "isAdmin": true} +] \ No newline at end of file