Added new website
This commit is contained in:
1 parent
293b7727e2
commit
df26a3fe96
34 files changed
+10488
No files matched your search
@@ -0,0 +1,323 @@
|
||||
import { Game, PreGame } from "./types.ts";
|
||||
import {
|
||||
assertEquals,
|
||||
assertNotEquals,
|
||||
} from "https://deno.land/std@0.224.0/assert/mod.ts";
|
||||
|
||||
type GamePrimaryKey = ["phenix", "games", string];
|
||||
|
||||
type date = [number, number, number];
|
||||
type time = [number, number];
|
||||
type DateIndex = ["phenix", "indexes", "game-date", ...date, ...time];
|
||||
|
||||
export class GameService {
|
||||
readonly kv: Deno.Kv;
|
||||
private readonly DATE_VALIDATOR =
|
||||
/^(?<year>\d{4})-(?<month>\d{1,2})-(?<day>\d{1,2})$/;
|
||||
private readonly TIME_VALIDATOR = /^(?<hour>\d{1,2})(:|h)(?<minute>\d{1,2})$/;
|
||||
private readonly VALID_TEAMS: string[] = [
|
||||
"jaune",
|
||||
"rouge",
|
||||
"bleu",
|
||||
"gris",
|
||||
"vert",
|
||||
"noir",
|
||||
"orange",
|
||||
"mauve",
|
||||
];
|
||||
|
||||
constructor(kv: Deno.Kv) {
|
||||
this.kv = kv;
|
||||
}
|
||||
|
||||
private chronologicalGameOrder(game1: Game, game2: Game): number {
|
||||
if (game1.date < game2.date) {
|
||||
return 1;
|
||||
}
|
||||
if (game1.date > game2.date) {
|
||||
return -1;
|
||||
}
|
||||
if (game1.time < game2.time) {
|
||||
return 1;
|
||||
}
|
||||
if (game1.time > game2.time) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
async getAllGames(): Promise<Game[]> {
|
||||
const ite = this.kv.list<Game>({ prefix: ["phenix", "games"] });
|
||||
return (await Array.fromAsync(ite))
|
||||
.map((e) => e.value)
|
||||
.sort(this.chronologicalGameOrder);
|
||||
}
|
||||
|
||||
private gamePrimaryKey(id: string): GamePrimaryKey {
|
||||
return ["phenix", "games", id];
|
||||
}
|
||||
|
||||
private dateIndex(date: string, time: string): DateIndex | null {
|
||||
let destructedDate = this.DATE_VALIDATOR.exec(date)?.groups;
|
||||
let destructedTime = this.TIME_VALIDATOR.exec(time)?.groups;
|
||||
|
||||
if (!destructedDate || !destructedTime) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
"phenix",
|
||||
"indexes",
|
||||
"game-date",
|
||||
parseInt(destructedDate.year),
|
||||
parseInt(destructedDate.month),
|
||||
parseInt(destructedDate.day),
|
||||
parseInt(destructedTime.hour),
|
||||
parseInt(destructedTime.minute),
|
||||
];
|
||||
}
|
||||
|
||||
private cleanTime(raw: string): string {
|
||||
const groups = this.TIME_VALIDATOR.exec(raw)!.groups!;
|
||||
return `${groups.hour.padStart(2, "0")}:${groups.minute.padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
private cleanDate(raw: string): string {
|
||||
const groups = this.DATE_VALIDATOR.exec(raw)!.groups!;
|
||||
return `${groups.year}-${groups.month.padStart(2, "0")}-${groups.day.padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
private extractYearFromDate(date: string): number {
|
||||
return parseInt(this.DATE_VALIDATOR.exec(date)!.groups!.year);
|
||||
}
|
||||
|
||||
private createGame(preGame: PreGame): Game {
|
||||
const ret = {
|
||||
...preGame,
|
||||
id: crypto.randomUUID(),
|
||||
practice: false,
|
||||
rain: false,
|
||||
year: this.extractYearFromDate(preGame.date),
|
||||
homeScore: null,
|
||||
awayScore: null,
|
||||
};
|
||||
ret.time = this.cleanTime(ret.time);
|
||||
ret.date = this.cleanDate(ret.date);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
private validatePregate(preGame: PreGame): string {
|
||||
if (!this.DATE_VALIDATOR.test(preGame.date)) {
|
||||
return `Invalide date format ${preGame.date}`;
|
||||
}
|
||||
if (!this.TIME_VALIDATOR.test(preGame.time)) {
|
||||
return `Invalide time format ${preGame.time}`;
|
||||
}
|
||||
if (!this.VALID_TEAMS.includes(preGame.away)) {
|
||||
return `Invalide away format ${preGame.away}`;
|
||||
}
|
||||
if (!this.VALID_TEAMS.includes(preGame.home)) {
|
||||
return `Invalide home format ${preGame.home}`;
|
||||
}
|
||||
if (preGame.home === preGame.away) {
|
||||
return `Home and away teams must be different (received ${preGame.home} for both)`;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
async createNewGame(preGame: PreGame): Promise<[Game | null, string]> {
|
||||
if (this.validatePregate(preGame) !== "") {
|
||||
return [null, this.validatePregate(preGame)];
|
||||
}
|
||||
const game = this.createGame(preGame);
|
||||
const dateIndex = this.dateIndex(game.date, game.time);
|
||||
if (!dateIndex) {
|
||||
return [null, "Cound not parse date or time"];
|
||||
}
|
||||
const precondition = await this.kv.get<string>(dateIndex);
|
||||
if (precondition.value !== null) {
|
||||
return [null, "game already exists " + precondition.value];
|
||||
}
|
||||
|
||||
const res = await this.kv
|
||||
.atomic()
|
||||
.check(precondition)
|
||||
.set(this.gamePrimaryKey(game.id), game)
|
||||
.set(dateIndex, game.id)
|
||||
.commit();
|
||||
if (!res.ok) {
|
||||
return [null, "Transaction failed"];
|
||||
}
|
||||
|
||||
return [game, ""];
|
||||
}
|
||||
|
||||
async getGameById(id: string): Promise<Game | null> {
|
||||
const res = await this.kv.get<Game>(this.gamePrimaryKey(id));
|
||||
return res.value;
|
||||
}
|
||||
|
||||
async deleteGameById(id: string): Promise<boolean> {
|
||||
const precond = await this.kv.get<Game>(this.gamePrimaryKey(id));
|
||||
if (precond.value === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const res = await this.kv
|
||||
.atomic()
|
||||
.check(precond)
|
||||
.delete(this.gamePrimaryKey(precond.value.id))
|
||||
.delete(this.dateIndex(precond.value.date, precond.value.time)!)
|
||||
.commit();
|
||||
return res.ok;
|
||||
}
|
||||
|
||||
async updateGame(id: string, partial: Partial<Game>): Promise<boolean> {
|
||||
delete partial.id;
|
||||
const precond = await this.kv.get<Game>(this.gamePrimaryKey(id));
|
||||
if (precond.value === null) {
|
||||
return false;
|
||||
}
|
||||
const res = await this.kv
|
||||
.atomic()
|
||||
.check(precond)
|
||||
.delete(this.gamePrimaryKey(precond.value.id))
|
||||
.delete(this.dateIndex(precond.value.date, precond.value.time)!)
|
||||
.set(
|
||||
this.dateIndex(precond.value.date, precond.value.time)!,
|
||||
precond.value.id,
|
||||
)
|
||||
.set(this.gamePrimaryKey(precond.value.id), {
|
||||
...precond.value,
|
||||
...partial,
|
||||
})
|
||||
.commit();
|
||||
return res.ok;
|
||||
}
|
||||
}
|
||||
|
||||
Deno.test("saving simple games", async (t) => {
|
||||
const kv = await Deno.openKv();
|
||||
const service = new GameService(kv);
|
||||
|
||||
async function deleteAllKeys() {
|
||||
const keys = kv.list({ prefix: [] });
|
||||
for await (const entry of keys) {
|
||||
await kv.delete(entry.key);
|
||||
}
|
||||
}
|
||||
|
||||
function validePreGame(seed: number): PreGame {
|
||||
let year = ((645 * seed + 162) % 10000) + 1;
|
||||
let month = ((335 * seed + 71) % 12) + 1;
|
||||
let day = ((72625 * seed + 176) % 28) + 1;
|
||||
return {
|
||||
date: `${(year + "").padStart(4, "0")}-${(month + "").padStart(2, "0")}-${(day + "").padStart(2, "0")}`,
|
||||
time: "18:45",
|
||||
away: "jaune",
|
||||
home: "rouge",
|
||||
};
|
||||
}
|
||||
|
||||
await deleteAllKeys();
|
||||
|
||||
await t.step("creating games", async () => {
|
||||
const preGame = validePreGame(7276);
|
||||
const [game, error] = await service.createNewGame(preGame);
|
||||
assertEquals(error, "");
|
||||
const ret = await service.getGameById(game!.id);
|
||||
assertEquals(game, ret);
|
||||
});
|
||||
|
||||
await deleteAllKeys();
|
||||
|
||||
await t.step("creating games with old date format", async () => {
|
||||
const preGame = validePreGame(7276);
|
||||
preGame.date = "2017-8-5";
|
||||
const [game, error] = await service.createNewGame(preGame);
|
||||
assertEquals(error, "");
|
||||
assertEquals(game?.date, "2017-08-05");
|
||||
});
|
||||
|
||||
await deleteAllKeys();
|
||||
|
||||
await t.step("creating games with old time format", async () => {
|
||||
const preGame = validePreGame(7276);
|
||||
preGame.time = "9:00";
|
||||
const [game, error] = await service.createNewGame(preGame);
|
||||
assertEquals(error, "");
|
||||
assertEquals(game?.time, "09:00");
|
||||
});
|
||||
|
||||
await deleteAllKeys();
|
||||
|
||||
await t.step(
|
||||
"saving two games in the same timeslot should fail",
|
||||
async () => {
|
||||
const preGame = validePreGame(7276);
|
||||
let [_, error] = await service.createNewGame(preGame);
|
||||
assertEquals(error, "");
|
||||
[_, error] = await service.createNewGame(preGame);
|
||||
assertNotEquals(error, "");
|
||||
},
|
||||
);
|
||||
|
||||
await deleteAllKeys();
|
||||
|
||||
await t.step("saving games with bad fields", async () => {
|
||||
let i = 0;
|
||||
for (const key of Object.keys(validePreGame(7276))) {
|
||||
let [_, error] = await service.createNewGame({
|
||||
...validePreGame(7262 + i),
|
||||
[key]: `Broken ${key}`,
|
||||
});
|
||||
assertNotEquals(error, "");
|
||||
i++;
|
||||
}
|
||||
});
|
||||
|
||||
await t.step("saving games with the same home and away teams", async () => {
|
||||
const preGame = validePreGame(7276);
|
||||
preGame.home = preGame.away;
|
||||
let [_, error] = await service.createNewGame(preGame);
|
||||
assertNotEquals(error, "");
|
||||
});
|
||||
|
||||
await deleteAllKeys();
|
||||
|
||||
await t.step("listing games", async () => {
|
||||
for (let i = 0; i < 10; i++) {
|
||||
await service.createNewGame(validePreGame(666 + i));
|
||||
}
|
||||
assertEquals((await service.getAllGames()).length, 10);
|
||||
});
|
||||
|
||||
await deleteAllKeys();
|
||||
|
||||
await t.step("deleting games", async () => {
|
||||
let [game, error] = await service.createNewGame(validePreGame(8992));
|
||||
await service.deleteGameById(game!.id);
|
||||
assertEquals((await service.getAllGames()).length, 0);
|
||||
|
||||
[game, error] = await service.createNewGame(validePreGame(8992));
|
||||
assertEquals(error, "");
|
||||
});
|
||||
|
||||
await deleteAllKeys();
|
||||
|
||||
await t.step("updating game scores", async () => {
|
||||
let [game, _] = await service.createNewGame(validePreGame(8992));
|
||||
const ret = await service.updateGame(game!.id, {
|
||||
homeScore: 10,
|
||||
awayScore: 32,
|
||||
});
|
||||
assertEquals(ret, true);
|
||||
game = await service.getGameById(game!.id);
|
||||
assertEquals(game!.homeScore, 10);
|
||||
assertEquals(game!.awayScore, 32);
|
||||
});
|
||||
|
||||
await deleteAllKeys();
|
||||
await kv.close();
|
||||
});
|
||||
@@ -0,0 +1,95 @@
|
||||
import {
|
||||
assert,
|
||||
assertFalse,
|
||||
} from "https://deno.land/std@0.224.0/assert/mod.ts";
|
||||
|
||||
type User = { username: string; passwordHashWithSalt: string };
|
||||
|
||||
const officialUsers: User[] = [
|
||||
{
|
||||
username: "marqueur",
|
||||
passwordHashWithSalt:
|
||||
"5603007580f73044864e7a0b66744f405406eaaf28fa514d724ccf11d010bd89:070b1a477e6a877d44f31961dc0402a9f3258465eed73ab6a9110c461ac68714",
|
||||
},
|
||||
];
|
||||
|
||||
export class LoginService {
|
||||
private readonly users: User[];
|
||||
|
||||
constructor(users: User[]) {
|
||||
this.users = users;
|
||||
}
|
||||
|
||||
public static officialLoginService(): LoginService {
|
||||
return new LoginService(officialUsers);
|
||||
}
|
||||
|
||||
private async hashMessage(message: string): Promise<string> {
|
||||
const msgUint8 = new TextEncoder().encode(message); // encode as (utf-8) Uint8Array
|
||||
const hashBuffer = await crypto.subtle.digest("SHA-256", msgUint8); // hash the message
|
||||
const hashArray = Array.from(new Uint8Array(hashBuffer)); // convert buffer to byte array
|
||||
const hashHex = hashArray
|
||||
.map((b) => b.toString(16).padStart(2, "0"))
|
||||
.join(""); // convert bytes to hex string
|
||||
return hashHex;
|
||||
}
|
||||
|
||||
private async shittyHmac(message: string, salt: string): Promise<string> {
|
||||
const h1 = await this.hashMessage(message + salt);
|
||||
const h2 = await this.hashMessage(salt + message);
|
||||
return await this.hashMessage(h1 + h2);
|
||||
}
|
||||
|
||||
async hashPassword(password: string): Promise<string> {
|
||||
const salt = await this.hashMessage(`${Math.random()}`);
|
||||
const hash = await this.shittyHmac(password, salt);
|
||||
return `${salt}:${hash}`;
|
||||
}
|
||||
|
||||
async verifyPassword(
|
||||
password: string,
|
||||
passwordHashWithSalt: string,
|
||||
): Promise<boolean> {
|
||||
const [passwordSalt, passwordHash] = passwordHashWithSalt.split(":");
|
||||
return passwordHash === (await this.shittyHmac(password, passwordSalt));
|
||||
}
|
||||
|
||||
async validateCredentials(
|
||||
username: string,
|
||||
password: string,
|
||||
): Promise<boolean> {
|
||||
for (const user of this.users) {
|
||||
if (user.username === username) {
|
||||
return this.verifyPassword(password, user.passwordHashWithSalt);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async login(ctx: any): Promise<void> {
|
||||
const nDays = 90;
|
||||
const maxAgesInSecond = nDays * 24 * 3600;
|
||||
const maxUnixEpoch = Math.floor(Date.now() / 1000) + maxAgesInSecond;
|
||||
|
||||
await ctx.cookies.set("phenix-session", `${maxUnixEpoch}`, {
|
||||
maxAge: maxAgesInSecond,
|
||||
});
|
||||
}
|
||||
|
||||
async isLoggedIn(ctx: any): Promise<boolean> {
|
||||
const expiration = await ctx.cookies.get("phenix-session");
|
||||
if (!expiration || !/^\d+$/.test(expiration)) {
|
||||
return false;
|
||||
}
|
||||
return Date.now() < parseInt(expiration) * 1000;
|
||||
}
|
||||
}
|
||||
|
||||
Deno.test("login service", async (t) => {
|
||||
await t.step("hashing password", async () => {
|
||||
const service = new LoginService([]);
|
||||
const passwordHashWithSalt = await service.hashPassword("qwerty");
|
||||
assert(await service.verifyPassword("qwerty", passwordHashWithSalt));
|
||||
assertFalse(await service.verifyPassword("ytrewq", passwordHashWithSalt));
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
export type Game = {
|
||||
id: string;
|
||||
date: string;
|
||||
time: string;
|
||||
home: string;
|
||||
away: string;
|
||||
year: number;
|
||||
homeScore: number | null;
|
||||
awayScore: number | null;
|
||||
practice: boolean;
|
||||
rain: boolean;
|
||||
};
|
||||
|
||||
export type PreGame = {
|
||||
date: string;
|
||||
time: string;
|
||||
home: string;
|
||||
away: string;
|
||||
};
|
||||
Reference in new issue
Block a user