Continued stupidly overengineered project.
This commit is contained in:
1 parent
c448efa8fa
commit
8dbea528c1
4 files changed
+205
-43
No files matched your search
+4
-3
@@ -28,15 +28,16 @@ function veryInsecureEquality(a: string, b: string): boolean{
|
|||||||
return a === b;
|
return a === b;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function verifyAuth(auth: string): Promise<boolean>{
|
export async function verifyAuth(auth: string): Promise<User|null>{
|
||||||
let ret = false;
|
let ret: User|null = null;
|
||||||
const authHash = await hash(auth);
|
const authHash = await hash(auth);
|
||||||
for (const user of users) {
|
for (const user of users) {
|
||||||
if (veryInsecureEquality(user.hash, authHash)) { // do not do that at home
|
if (veryInsecureEquality(user.hash, authHash)) { // do not do that at home
|
||||||
ret = true;
|
ret = user;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
console.log(await hash("2468"))
|
console.log(await hash("2468"))
|
||||||
+79
-5
@@ -1,11 +1,15 @@
|
|||||||
// @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 {Ballot, Question} from "./shared.ts";
|
import {Ballot, BallotTally, Question, QuestionTally} from "./shared.ts";
|
||||||
|
|
||||||
|
|
||||||
let ballots: Ballot[] = [];
|
let ballots: Ballot[] = [];
|
||||||
|
|
||||||
|
function isAdmin(): boolean{
|
||||||
|
return window.localStorage.getItem("isAdmin") === 'true';
|
||||||
|
}
|
||||||
|
|
||||||
type ProgressBarAttr = {
|
type ProgressBarAttr = {
|
||||||
question: Question,
|
question: Question,
|
||||||
onchange: () => void,
|
onchange: () => void,
|
||||||
@@ -144,8 +148,10 @@ class PasscodeView implements m.ClassComponent<unknown> {
|
|||||||
|
|
||||||
async verifyPasscode(event: Event): Promise<void> {
|
async verifyPasscode(event: Event): Promise<void> {
|
||||||
let password = event.target.value;
|
let password = event.target.value;
|
||||||
if(await verifyAuth(password)){
|
let user = await verifyAuth(password)
|
||||||
|
if(user){
|
||||||
window.localStorage.setItem("userId", password);
|
window.localStorage.setItem("userId", password);
|
||||||
|
window.localStorage.setItem("isAdmin", user.isAdmin);
|
||||||
m.route.set(`/voter-home/`);
|
m.route.set(`/voter-home/`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -160,9 +166,68 @@ class PasscodeView implements m.ClassComponent<unknown> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class ResultView implements m.ClassComponent<unknown>{
|
class ResultView implements m.ClassComponent<unknown>{
|
||||||
view(){
|
ballotTallies: BallotTally[] = [];
|
||||||
return m("div", "one two three");
|
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>{
|
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();
|
window.localStorage.clear();
|
||||||
m.route.set("/");
|
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 {
|
view(vnode: m.Vnode<unknown>): m.Children {
|
||||||
return m("div", [
|
return m("div", [
|
||||||
m("nav.navbar.is-primary", [
|
m("nav.navbar.is-primary", [
|
||||||
@@ -188,8 +260,10 @@ class FrameView<T extends m.ClassComponent<any>> implements m.ClassComponent<unk
|
|||||||
m("img", {src: "https://all-dressed-programming.com/images/pizza.svg"}),
|
m("img", {src: "https://all-dressed-programming.com/images/pizza.svg"}),
|
||||||
m("span.ml-1.title.is-5", "Stupidly Over Engineered Voting System")),
|
m("span.ml-1.title.is-5", "Stupidly Over Engineered Voting System")),
|
||||||
m("div.navbar-end",
|
m("div.navbar-end",
|
||||||
|
m("div.navbar-item",
|
||||||
|
this.resultButton()),
|
||||||
m("div.navbar-item" ,
|
m("div.navbar-item" ,
|
||||||
m("a.is-clickable", {onclick: () => this.logout()}, "logout")))
|
m("a.is-clickable.button", {onclick: () => this.logout()}, "logout")))
|
||||||
]),
|
]),
|
||||||
m(this.embedeed, vnode.attrs)
|
m(this.embedeed, vnode.attrs)
|
||||||
]);
|
]);
|
||||||
|
|||||||
+103
-31
@@ -1,7 +1,7 @@
|
|||||||
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, createDefaultBallots, Question} from "./shared.ts";
|
import {Ballot, createDefaultBallots, Question, BallotTally, QuestionTally} from "./shared.ts";
|
||||||
import {verifyAuth} from "./auth.ts";
|
import {verifyAuth} from "./auth.ts";
|
||||||
|
|
||||||
type QuestionAnswer = {
|
type QuestionAnswer = {
|
||||||
@@ -33,33 +33,50 @@ router.get("/", (context) => {
|
|||||||
`;
|
`;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
type IndexPrefix = ["soevs", "v1"];
|
||||||
|
|
||||||
function getIndexPrefix(): string[]{
|
type UserPrefix = [... IndexPrefix, "user", userId: string];
|
||||||
return ["tatoo-vote", "v1"];
|
|
||||||
|
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 prefix = getIndexPrefix();
|
||||||
const suffix = ["user", userId];
|
return [...prefix, "user", userId];
|
||||||
return prefix.concat(suffix);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getBallotQuestionIndex(ballot: Ballot, question: Question): string[]{
|
function getBallotIndex(ballot: Ballot): BallotIndex{
|
||||||
const prefix = getIndexPrefix();
|
const prefix = getIndexPrefix();
|
||||||
const suffix = ["ballot", ballot.id, "question", question.id];
|
return [...prefix, "ballot", ballot.id];
|
||||||
return prefix.concat(suffix);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 prefix = getBallotQuestionIndex(ballot, question);
|
||||||
const suffix = ["user", userId];
|
return [...prefix, "user", userId];
|
||||||
return prefix.concat(suffix);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getUserBallotQuestionIndex(userId: string, ballot: Ballot, question: Question): string[]{
|
function getUserBallotQuestionIndex(userId: string, ballot: Ballot, question: Question): UserBallotQuestionIndex{
|
||||||
const prefix = getUserIndex(userId);
|
const prefix = getUserIndex(userId);
|
||||||
const suffix = ["ballot", ballot.id, "question", question.id];
|
return [...prefix, "ballot", ballot.id, "question", question.id];
|
||||||
return prefix.concat(suffix);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function persistBallots(userId: string, ballots: Ballot[]): Promise<void> {
|
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){
|
function setValueInQuestion(ballots: Ballot[], key: UserBallotQuestionIndex, value: number){
|
||||||
const questionId = key[7];
|
const questionId = key[7]; // UserBallotQuestionIndex.questionId
|
||||||
const ballotId = key[5];
|
const ballotId = key[5]; // UserBallotQuestionIndex.ballotId
|
||||||
for(const ballot of ballots){
|
for(const ballot of ballots){
|
||||||
for(const question of ballot.questions){
|
for(const question of ballot.questions){
|
||||||
if(question.id === questionId && ballot.id === ballotId){
|
if(question.id === questionId && ballot.id === ballotId){
|
||||||
@@ -86,26 +103,78 @@ function setValueInQuestion(ballots: Ballot[], key: string[], value: number){
|
|||||||
return ballots;
|
return ballots;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getBallots(userId: string): Promise<Ballot[]>{
|
async function getUserBallots(userId: string): Promise<Ballot[]>{
|
||||||
let ballots = createDefaultBallots();
|
let ballots = createDefaultBallots(userId);
|
||||||
const kv = await Deno.openKv();
|
const kv = await Deno.openKv();
|
||||||
const entries = kv.list<QuestionAnswer>({ prefix: getUserIndex(userId) });
|
const entries = kv.list<QuestionAnswer>({ prefix: getUserIndex(userId) });
|
||||||
for await (const entry of entries) {
|
for await (const entry of entries) {
|
||||||
const key = entry.key; // ["users", "alice"]
|
const key = entry.key;
|
||||||
const value = entry.value.value; // { name: "Alice" }
|
const value = entry.value.value;
|
||||||
ballots = setValueInQuestion(ballots, key, value);
|
ballots = setValueInQuestion(ballots, key as UserBallotQuestionIndex, value);
|
||||||
}
|
}
|
||||||
return ballots;
|
return ballots;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getBallotResults(){
|
function createEmptyResult(b: Ballot): BallotTally {
|
||||||
let ballots = createDefaultBallots();
|
return {
|
||||||
for(const ballot of ballots){
|
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??"";
|
const userId: string = ctx?.params?.userid??"";
|
||||||
if(!await verifyAuth(userId)){
|
if(!await verifyAuth(userId)){
|
||||||
ctx.throw(403)
|
ctx.throw(403)
|
||||||
@@ -119,16 +188,17 @@ router.post("/api/v1/vote/:userid", async (ctx: Context)=> {
|
|||||||
ctx.response.body = userId;
|
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??"";
|
const userId = ctx?.params?.userid??"";
|
||||||
if(!await verifyAuth(userId)){
|
if(!await verifyAuth(userId)){
|
||||||
ctx.throw(403)
|
ctx.throw(403)
|
||||||
}
|
}
|
||||||
ctx.response.body = await getBallots(userId);
|
ctx.response.body = await getUserBallots(userId);
|
||||||
});
|
});
|
||||||
|
|
||||||
router.get("/api/v1/results", async (ctx: Context) =>{
|
router.get("/api/v1/results", async (context): Promise<void> => {
|
||||||
ctx.response.body = await getBallotResults();
|
const userId = '1234';
|
||||||
|
context.response.body = await getBallotTallies(userId);
|
||||||
});
|
});
|
||||||
|
|
||||||
router.get("/client.js", async (context): Promise<void> => {
|
router.get("/client.js", async (context): Promise<void> => {
|
||||||
@@ -153,3 +223,5 @@ app.use((ctx) => {
|
|||||||
await app.listen({ port: 3333 });
|
await app.listen({ port: 3333 });
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
+19
-4
@@ -9,9 +9,24 @@ export type Question = {
|
|||||||
export type Ballot = {
|
export type Ballot = {
|
||||||
id: string,
|
id: string,
|
||||||
name: string,
|
name: string,
|
||||||
|
userId: string,
|
||||||
questions: Question[],
|
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[]{
|
function defaultQuestions(): Question[]{
|
||||||
return [
|
return [
|
||||||
{id: "5242", name: "readme", value: 25, longName: "Qualité du README"},
|
{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 [
|
return [
|
||||||
{id: "5gg64", name: "Les chacals de laval", questions: defaultQuestions()},
|
{id: "5gg64", name: "Les chacals de laval", userId: userId, questions: defaultQuestions()},
|
||||||
{id: "ys68", name: "Les rapides de blainville", questions: defaultQuestions()},
|
{id: "ys68", name: "Les rapides de blainville", userId: userId, questions: defaultQuestions()},
|
||||||
{id: "ssw", name: "Les pas-beaux du plateau", questions: defaultQuestions()},
|
{id: "ssw", name: "Les pas-beaux du plateau", userId: userId, questions: defaultQuestions()},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user