Continued stupidly overengineered project.

This commit is contained in:
didier amyot committed 2023-11-21 09:21:43 -05:00
1 parent c448efa8fa
commit 8dbea528c1
4 files changed
+205 -43

No files matched your search

+4 -3
View File
@@ -28,15 +28,16 @@ function veryInsecureEquality(a: string, b: string): boolean{
return a === b;
}
export async function verifyAuth(auth: string): Promise<boolean>{
let ret = false;
export async function verifyAuth(auth: string): Promise<User|null>{
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"))
+79 -5
View File
@@ -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<unknown> {
async verifyPasscode(event: Event): Promise<void> {
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<unknown> {
}
class ResultView implements m.ClassComponent<unknown>{
view(){
return m("div", "one two three");
ballotTallies: BallotTally[] = [];
allowedToView = true;
async oninit(): Promise<void> {
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<string>{
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<T extends m.ClassComponent<any>> implements m.ClassComponent<unknown>{
@@ -181,6 +246,13 @@ class FrameView<T extends m.ClassComponent<any>> implements m.ClassComponent<unk
window.localStorage.clear();
m.route.set("/");
}
resultButton(){
if(!isAdmin()){
return m("div");
}
return m(m.route.Link, {class: "button", href: "/results"}, `Results`)
}
view(vnode: m.Vnode<unknown>): m.Children {
return m("div", [
m("nav.navbar.is-primary", [
@@ -189,7 +261,9 @@ class FrameView<T extends m.ClassComponent<any>> implements m.ClassComponent<unk
m("span.ml-1.title.is-5", "Stupidly Over Engineered Voting System")),
m("div.navbar-end",
m("div.navbar-item",
m("a.is-clickable", {onclick: () => this.logout()}, "logout")))
this.resultButton()),
m("div.navbar-item" ,
m("a.is-clickable.button", {onclick: () => this.logout()}, "logout")))
]),
m(this.embedeed, vnode.attrs)
]);
+103 -31
View File
@@ -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<void> {
@@ -73,9 +90,9 @@ async function persistBallots(userId: string, ballots: Ballot[]): Promise<void>
}
}
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<Ballot[]>{
let ballots = createDefaultBallots();
async function getUserBallots(userId: string): Promise<Ballot[]>{
let ballots = createDefaultBallots(userId);
const kv = await Deno.openKv();
const entries = kv.list<QuestionAnswer>({ 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<QuestionTally[]>{
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<string, Question>{
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<QuestionTally>{
const questions = getQuestionsMappedById(ballotId);
const kv = await Deno.openKv();
const entries = kv.list<QuestionAnswer>({ 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<QuestionTally[]> {
const ballots = getBallotsByBallotId(ballotId);
return compactifyQuestionTally(await Array.fromAsync(ballots));
}
async function getBallotTallies(userId: string): Promise<BallotTally[]>{
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<void> => {
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<void> => {
const userId = '1234';
context.response.body = await getBallotTallies(userId);
});
router.get("/client.js", async (context): Promise<void> => {
@@ -153,3 +223,5 @@ app.use((ctx) => {
await app.listen({ port: 3333 });
+19 -4
View File
@@ -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()},
];
}