minesweeper/backend/controller/userController.ts

233 lines
7.3 KiB
TypeScript

import { z } from "zod";
import { createController, createEndpoint } from "./controller";
import {
getUserCount,
getUserSettings,
loginUser,
registerUser,
upsertUserSettings,
} from "../repositories/userRepository";
import crypto from "crypto";
import { resetSessionUser, setSessionUser } from "../router";
import { userSettings } from "../../shared/user-settings";
import { UnauthorizedError } from "../errors/UnauthorizedError";
import { getGems, removeGems } from "../repositories/gemsRepository";
import {
getCollection,
upsertCollection,
} from "../repositories/collectionRepository";
import { getWeight, lootboxes } from "../../shared/lootboxes";
import { weightedPickRandom } from "../../shared/utils";
import { emit } from "../events";
import { Game } from "../schema";
import { and, count, eq, gt, max, not, sum } from "drizzle-orm";
import dayjs from "dayjs";
const secret = process.env.SECRET!;
const signString = (payload: string) => {
return crypto.createHmac("sha256", secret).update(payload).digest("hex");
};
export const userController = createController({
getSelf: createEndpoint(z.null(), async (_, { user }) => {
return user || null;
}),
login: createEndpoint(
z.object({ username: z.string(), password: z.string() }),
async (input, { db, ws }) => {
const { name: user } = await loginUser(
db,
input.username,
input.password,
);
const session = { user, expires: Date.now() + 1000 * 60 * 60 * 24 * 14 };
const sig = signString(JSON.stringify(session));
setSessionUser(ws, user);
return { token: JSON.stringify({ session, sig }) };
},
),
loginWithToken: createEndpoint(
z.object({ token: z.string() }),
async (input, { ws }) => {
const { session, sig } = JSON.parse(input.token);
const { user } = session;
if (sig !== signString(JSON.stringify(session))) {
return { success: false };
}
if (Date.now() > session.expires) {
return { success: false };
}
setSessionUser(ws, user);
return { success: true };
},
),
logout: createEndpoint(z.null(), async (_, { ws }) => {
resetSessionUser(ws);
}),
register: createEndpoint(
z.object({
username: z
.string()
.min(3, "Username must be at least 3 characters")
.max(15, "Username cannot be longer than 15 characters"),
password: z.string().min(6, "Password must be at least 6 characters"),
}),
async (input, { db, ws }) => {
await registerUser(db, input.username, input.password);
const user = input.username;
const session = { user, expires: Date.now() + 1000 * 60 * 60 * 24 * 14 };
const sig = signString(JSON.stringify(session));
setSessionUser(ws, user);
return { token: JSON.stringify({ session, sig }) };
},
),
getSettings: createEndpoint(z.null(), async (_, { db, user }) => {
if (!user) throw new UnauthorizedError("Unauthorized");
const settings = await getUserSettings(db, user);
return settings;
}),
updateSettings: createEndpoint(
userSettings.partial(),
async (input, { db, user }) => {
if (!user) throw new UnauthorizedError("Unauthorized");
const settings = await getUserSettings(db, user);
const newSettings = { ...settings, ...input };
await upsertUserSettings(db, user, newSettings);
return newSettings;
},
),
getUserCount: createEndpoint(z.null(), async (_, { db }) => {
const count = await getUserCount(db);
return count;
}),
getOwnGems: createEndpoint(z.null(), async (_, { db, user }) => {
if (!user) throw new UnauthorizedError("Unauthorized");
const gems = await getGems(db, user);
return gems;
}),
getOwnCollection: createEndpoint(z.null(), async (_, { db, user }) => {
if (!user) throw new UnauthorizedError("Unauthorized");
const collection = await getCollection(db, user);
return collection;
}),
selectCollectionEntry: createEndpoint(
z.object({
id: z.string(),
}),
async ({ id }, { db, user }) => {
if (!user) throw new UnauthorizedError("Unauthorized");
const collection = await getCollection(db, user);
if (!collection.entries.some((e) => e.id === id)) {
throw new Error("Entry not found");
}
for (const entry of collection.entries) {
entry.selected = entry.id === id;
}
await upsertCollection(db, user, collection);
},
),
addCollectionEntryToShuffle: createEndpoint(
z.object({
id: z.string(),
}),
async ({ id }, { db, user }) => {
if (!user) throw new UnauthorizedError("Unauthorized");
const collection = await getCollection(db, user);
const entry = collection.entries.find((e) => e.id === id);
if (!entry) {
throw new Error("Entry not found");
}
entry.selected = true;
await upsertCollection(db, user, collection);
},
),
openLootbox: createEndpoint(
z.object({
id: z.string(),
}),
async ({ id }, { db, user }) => {
if (!user) throw new UnauthorizedError("Unauthorized");
const collection = await getCollection(db, user);
const lootbox = lootboxes.find((l) => l.id === id);
if (!lootbox) {
throw new Error("Lootbox not found");
}
let itemsCopy = [...lootbox.items];
if (lootbox.noDuplicates) {
itemsCopy = itemsCopy.filter(
(i) => !collection.entries.some((e) => e.id === i.id),
);
}
if (itemsCopy.length === 0) {
throw new Error("No items left");
}
await removeGems(db, user, lootbox.price);
const result = weightedPickRandom(itemsCopy, (i) => getWeight(i.rarity));
collection.entries.push({
id: result.id,
aquired: Date.now(),
selected: false,
});
await upsertCollection(db, user, collection);
emit({
type: "lootboxPurchased",
user,
lootbox: lootbox.id,
reward: result.id,
rarity: result.rarity,
});
},
),
getHeatmap: createEndpoint(
z.object({
id: z.string(),
}),
async ({ id }, { db }) => {
const now = dayjs();
const firstOfYear = now.startOf("year");
const gamesOfUser = await db.query.Game.findMany({
where: and(eq(Game.user, id), gt(Game.finished, firstOfYear.valueOf())),
});
const heat = Array.from<number>({
length: now.diff(firstOfYear, "days") + 1,
}).fill(0);
gamesOfUser.forEach((game) => {
const day = dayjs(game.finished).diff(firstOfYear, "days");
heat[day] += 1;
});
return heat;
},
),
getProfile: createEndpoint(
z.object({
id: z.string(),
}),
async ({ id }, { db }) => {
const [{ value: totalGames }] = await db
.select({
value: count(),
})
.from(Game)
.where(and(eq(Game.user, id), not(eq(Game.finished, 0))));
const [{ value: highestStage }] = await db
.select({
value: max(Game.stage),
})
.from(Game)
.where(and(eq(Game.user, id), not(eq(Game.finished, 0))));
const [{ value: totalStages }] = await db
.select({
value: sum(Game.stage),
})
.from(Game)
.where(and(eq(Game.user, id), not(eq(Game.finished, 0))));
return {
totalGames,
highestStage,
averageStage: Number(totalStages) / totalGames,
};
},
),
});