going somewhere
This commit is contained in:
1 parent
ae7e65ba44
commit
9de31fac1b
5 files changed
+191
-75
No files matched your search
+41
-2
@@ -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
@@ -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
@@ -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> => {
|
||||||
|
|||||||
+26
-7
@@ -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()},
|
||||||
|
];
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
[
|
||||||
|
{"hash": "", "isAdmin": true}
|
||||||
|
]
|
||||||
Reference in new issue
Block a user