going somewhere

This commit is contained in:
didier amyot committed 2023-11-20 20:24:47 -05:00
1 parent ae7e65ba44
commit 9de31fac1b
5 files changed
+193 -77

No files matched your search

+42 -3
View File
@@ -1,3 +1,42 @@
export function verifyAuth(auth: string): boolean{ import { encodeBase64 } from "https://deno.land/std/encoding/base64.ts"
return auth.length === 4;
} 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<string>{
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<boolean>{
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"))
+66 -48
View File
@@ -1,27 +1,25 @@
// @deno-types="https://esm.sh/@types/mithril@2.2.4" // @deno-types="https://esm.sh/@types/mithril@2.2.4"
import m from "https://esm.sh/mithril@2.2.2" import m from "https://esm.sh/mithril@2.2.2"
import {verifyAuth} from "./auth.ts"; import {verifyAuth} from "./auth.ts";
import {Scores, Ballot} from "./shared.ts"; import {Ballot, Question} from "./shared.ts";
const scores: Scores = { let ballots: Ballot[] = [];
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}},
]
type ProgressBarAttr = { type ProgressBarAttr = {
key: keyof Scores, question: Question,
scores: Scores,
onchange: () => void, 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<ProgressBarAttr>{ class ProgressBar implements m.ClassComponent<ProgressBarAttr>{
closestFivePercent(n: number): number{ closestFivePercent(n: number): number{
@@ -43,41 +41,34 @@ class ProgressBar implements m.ClassComponent<ProgressBarAttr>{
} }
view({attrs}: m.CVnode<ProgressBarAttr>){ view({attrs}: m.CVnode<ProgressBarAttr>){
const onclickFn = (e: Event) => { const onclickFn = (e: Event) => {
attrs.scores[attrs.key] = this.computeClickedScored(e); attrs.question.value = this.computeClickedScored(e);
attrs.onchange(); attrs.onchange();
} }
const value = attrs.scores[attrs.key]; const value = attrs.question.value;
return m("progress.progress.is-primary", return m("progress.progress.is-primary",
{value: value, max: 100, onclick: onclickFn, title: `${value}%`}, {value: value, max: 100, onclick: onclickFn, title: `${value}%`},
`${value}%`) `${value}%`)
} }
} }
type VoteViewAttr = { type BallotViewAttr = {
ballotId: number, ballot: Ballot,
onchange?: () => void, onchange?: () => void,
}; };
class BallotView implements m.ClassComponent<VoteViewAttr> { class BallotView implements m.ClassComponent<BallotViewAttr> {
voteFor(ballot: Ballot, title: string, key: string, onchange: () => void){ voteFor(question: Question, onchange: () => void){
return m("div.columns", return m("div.columns",
m("div.column", title, ":"), m("div.column", question.longName, ":"),
m("div.column.is-two-thirds", 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<VoteViewAttr>){ view(vnode: m.CVnode<BallotViewAttr>){
const ballotId = vnode.attrs.ballotId;
const onchange = vnode.attrs.onchange ?? function(){}; const onchange = vnode.attrs.onchange ?? function(){};
const ballot = ballots.find(ballot => ballot.id.toString() === ballotId.toString()); const questions = vnode.attrs.ballot.questions??[];
if(!ballot){ return questions.map(question => this.voteFor(question, onchange));
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)]
} }
} }
@@ -96,6 +87,10 @@ type MainVoteViewAttr = {
class MainVoteView implements m.ClassComponent<MainVoteViewAttr>{ class MainVoteView implements m.ClassComponent<MainVoteViewAttr>{
async oninit(){
ballots = await m.request(getBallotURL());
}
async persistBallots(): Promise<void>{ async persistBallots(): Promise<void>{
const params: RequestInit = { const params: RequestInit = {
method: 'POST', method: 'POST',
@@ -104,28 +99,40 @@ class MainVoteView implements m.ClassComponent<MainVoteViewAttr>{
}, },
body: JSON.stringify(ballots) body: JSON.stringify(ballots)
}; };
const resp = await fetch("/api/v1/vote/kkk", params); const resp = await fetch(getBallotURL(), params);
console.log(resp.status) console.log(resp.status)
} }
view(vnode: m.CVnode<MainVoteViewAttr>){ emptyState(): m.Vnode{
const name = "Les chacals de laval" return m("div", "No ballot found");
}
view(vnode: m.CVnode<MainVoteViewAttr>): 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", return m("div.columns.is-centered",
m("div.column.is-two-thirds", m("div.column.is-two-thirds",
m("h1.title.is-1", m(m.route.Link, {href: "/"}, `Votons - ${name}`)), m("h1.title.is-1", m(m.route.Link, {href: "/voter-home/"}, `Votons - ${ballot.name}`)),
m(new BallotView(), {ballotId: vnode.attrs.ballotId, onchange: this.persistBallots}), m(new BallotView(), {ballotId: vnode.attrs.ballotId,
ballot: ballot,
onchange: this.persistBallots}),
m(new Submit()))); m(new Submit())));
} }
} }
class MainView{ class MainView implements m.ClassComponent<unknown>{
async oninit(){
ballots = await m.request(getBallotURL());
}
createBallotRow(ballot: Ballot){ createBallotRow(ballot: Ballot){
return m('tr', [ const ballotURL = `${m.route.get()}vote/${ballot.id}/`;
m('td', m(m.route.Link, {href: `${m.route.get()}vote/${ballot.id}/`}, ballot.name)), return m('tr',
m('td', ballot.scores.readme), m('td', m(m.route.Link, {href: ballotURL}, ballot.name)),
m('td', ballot.scores.diagrams), ballot.questions.map(q => m("td", q.value)));
m('td', ballot.scores.convinced),
])
} }
votesTable(ballots: Ballot[]){ votesTable(ballots: Ballot[]){
@@ -137,15 +144,15 @@ class MainView{
view(){ view(){
return m("div.columns.is-centered", return m("div.columns.is-centered",
m("div.column.is-two-thirds", m("div.column.is-two-thirds",
this.votesTable(ballots))) this.votesTable(ballots)));
} }
} }
class PasscodeView{ class PasscodeView implements m.ClassComponent<unknown> {
verifyPasscode(event: Event): void { async verifyPasscode(event: Event): Promise<void> {
let password = event.target.value; let password = event.target.value;
if(verifyAuth(password)){ if(await verifyAuth(password)){
window.localStorage.setItem("userId", password); window.localStorage.setItem("userId", password);
m.route.set(`/voter-home/`); m.route.set(`/voter-home/`);
} }
@@ -160,6 +167,17 @@ class PasscodeView{
} }
} }
class FrameView<Foo> implements m.ClassComponent<unknown>{
view(vnode: m.Vnode<unknown,this>): m.Children {
return m("div", [
"hello world",
])
}
}
m.route(window.document.body, "/", { m.route(window.document.body, "/", {
+55 -18
View File
@@ -1,7 +1,12 @@
import { Application, Router } from "https://deno.land/x/oak/mod.ts"; import { Application, Router } from "https://deno.land/x/oak/mod.ts";
import {createBundle} from "./bundler.ts"; import {createBundle} from "./bundler.ts";
import {Context} from "https://deno.land/x/oak@v12.2.0/context.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(); const router = new Router();
@@ -28,41 +33,73 @@ router.get("/", (context) => {
`; `;
}); });
function getUserIndex(userId: string, ballotId: string, questionId: string){ function getUserIndex2(userId: string): string[] {
return ["tatoo-vote", return ["tatoo-vote", "v1", "results", "user", userId];
"v1",
"results",
"user",
userId,
"ballot",
ballotId,
"question",
questionId]
} }
async function persistBallots(userId: any, ballots: Ballot[]): Promise<void> { 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<void> {
const kv = await Deno.openKv(); const kv = await Deno.openKv();
for(const ballot of ballots){ for(const ballot of ballots){
for(const question of Object.keys(ballot.scores)){ for(const question of ballot.questions){
const k = getUserIndex(userId, ballot.id, question); const k = getUserIndex(userId, ballot, question);
const v = {value: ballot.scores[question]}; const v: QuestionAnswer = {value: question.value};
await kv.set(k, v); 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<Ballot[]>{
let ballots = createDefaultBallots();
const kv = await Deno.openKv();
const entries = kv.list<QuestionAnswer>({ 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)=> { 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){ if(!ctx.request.hasBody){
ctx.throw(415); ctx.throw(415);
} }
const body = await ctx.request.body().value; const body = await ctx.request.body().value;
const ballots = body as Ballot[]; const ballots = body as Ballot[];
await persistBallots(userId, ballots); await persistBallots(userId, ballots);
console.log(body);
ctx.response.body = userId; 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<void> => { router.get("/client.js", async (context): Promise<void> => {
+27 -8
View File
@@ -1,11 +1,30 @@
export type Scores = {
readme: number, export type Question = {
diagrams: number, name: string,
convinced: number, longName: string,
}; id: string,
value: number,
}
export type Ballot = { export type Ballot = {
id: number, id: string,
name: string, name: string,
scores: Scores, 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()},
];
}
+3
View File
@@ -0,0 +1,3 @@
[
{"hash": "", "isAdmin": true}
]