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;
|
||||
}
|
||||
|
||||
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
@@ -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", [
|
||||
@@ -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("span.ml-1.title.is-5", "Stupidly Over Engineered Voting System")),
|
||||
m("div.navbar-end",
|
||||
m("div.navbar-item",
|
||||
this.resultButton()),
|
||||
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)
|
||||
]);
|
||||
|
||||
+103
-31
@@ -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
@@ -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()},
|
||||
];
|
||||
}
|
||||
Reference in new issue
Block a user