mirror of
https://github.com/zkldi/Tachi.git
synced 2026-10-04 12:58:11 +03:00
game_stats -> game_profiles
This commit is contained in:
+2
-18
@@ -14,6 +14,8 @@ db-reset POSTGRES_DB=DEFAULT_DB:
|
||||
tachidb database create
|
||||
tachidb migrate run
|
||||
|
||||
just db-load-seeds "{{POSTGRES_DB}}"
|
||||
|
||||
# Show the status of all applied (and failed) migrations.
|
||||
db-migrate-status POSTGRES_DB=DEFAULT_DB:
|
||||
psql "{{POSTGRES_URL}}/{{POSTGRES_DB}}" -c \
|
||||
@@ -21,24 +23,6 @@ db-migrate-status POSTGRES_DB=DEFAULT_DB:
|
||||
FROM \"_migration\" \
|
||||
ORDER BY version"
|
||||
|
||||
# Create (or recreate) the bot test template database with all migrations applied.
|
||||
# Workers clone from this template rather than re-running migrations each time.
|
||||
bot-db-test-template-reset:
|
||||
#!/bin/bash
|
||||
export POSTGRES_URL="{{POSTGRES_URL}}/tachi_bot_test_template"
|
||||
tachidb database drop || true
|
||||
tachidb database create
|
||||
tachidb migrate run
|
||||
|
||||
# Create (or recreate) the server test template database with all migrations applied.
|
||||
# Workers clone from this template rather than re-running migrations each time.
|
||||
server-db-test-template-reset:
|
||||
#!/bin/bash
|
||||
export POSTGRES_URL="{{POSTGRES_URL}}/tachi_server_test_template"
|
||||
tachidb database drop || true
|
||||
tachidb database create
|
||||
tachidb migrate run
|
||||
|
||||
# Load the seeds into the database.
|
||||
db-load-seeds POSTGRES_DB=DEFAULT_DB:
|
||||
cd typescript/server && POSTGRES_URL="{{POSTGRES_URL}}/{{POSTGRES_DB}}" \
|
||||
|
||||
@@ -70,3 +70,25 @@ test-typescript FILTER="*":
|
||||
# Run all parity suites, or a single one if a name is given.
|
||||
test-parity suite="":
|
||||
TACHI_SERVER=https://127.0.0.1:8080 bun vitest run {{ if suite != "" { "tests/" + suite + ".test.ts" } else { "" } }}
|
||||
|
||||
# ==> Orchestration of parallel db tests
|
||||
|
||||
# Create (or recreate) the bot test template database with all migrations applied.
|
||||
# Workers clone from this template rather than re-running migrations each time.
|
||||
bot-db-test-template-reset:
|
||||
#!/bin/bash
|
||||
export POSTGRES_URL="{{POSTGRES_URL}}/tachi_bot_test_template"
|
||||
tachidb database drop || true
|
||||
tachidb database create
|
||||
tachidb migrate run
|
||||
|
||||
# Create (or recreate) the server test template database with all migrations applied.
|
||||
# Workers clone from this template rather than re-running migrations each time.
|
||||
server-db-test-template-reset:
|
||||
#!/bin/bash
|
||||
export POSTGRES_URL="{{POSTGRES_URL}}/tachi_server_test_template"
|
||||
tachidb database drop || true
|
||||
tachidb database create
|
||||
tachidb migrate run
|
||||
|
||||
# <== End of parallel db test justscripts
|
||||
@@ -549,7 +549,7 @@ CREATE TABLE "game_settings_showcase" (
|
||||
data JSONB NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE "game_stats" (
|
||||
CREATE TABLE "game_profile" (
|
||||
user_id BIGINT REFERENCES account(id) NOT NULL,
|
||||
game GAME NOT NULL,
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import { HumaniseError } from "#util/humanise-error";
|
||||
import { HistorySafeGoBack } from "#util/misc";
|
||||
import { useFormik } from "formik";
|
||||
import React, { useContext, useRef, useState } from "react";
|
||||
import { Button, Form } from "react-bootstrap";
|
||||
import { Alert, Button, Form } from "react-bootstrap";
|
||||
import ReCAPTCHA from "react-google-recaptcha";
|
||||
import toast from "react-hot-toast";
|
||||
import { Link, useHistory } from "react-router-dom";
|
||||
@@ -21,68 +21,99 @@ export default function LoginPage() {
|
||||
|
||||
const recaptchaRef = useRef<any>(null);
|
||||
|
||||
const performLogin = async (values: {
|
||||
username: string;
|
||||
"!password": string;
|
||||
captcha: string;
|
||||
}) => {
|
||||
setErr("");
|
||||
|
||||
const rj = await APIFetchV1(
|
||||
"/auth/login",
|
||||
{
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
username: values.username.trim(),
|
||||
"!password": values["!password"],
|
||||
captcha: values.captcha,
|
||||
}),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
},
|
||||
false,
|
||||
false,
|
||||
);
|
||||
|
||||
if (recaptchaRef.current) {
|
||||
recaptchaRef.current.reset();
|
||||
}
|
||||
|
||||
if (!rj.success) {
|
||||
setErr(HumaniseError(rj.description));
|
||||
return;
|
||||
}
|
||||
|
||||
const userRJ = await APIFetchV1<UserDocument>("/users/me");
|
||||
|
||||
if (userRJ.statusCode === 403) {
|
||||
setErr("You are banned.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!userRJ.success) {
|
||||
console.error("Error retrieving own user?");
|
||||
setErr("An internal server error has occurred.");
|
||||
return;
|
||||
}
|
||||
|
||||
toast.success("Logged in!");
|
||||
|
||||
setTimeout(() => {
|
||||
setUser(userRJ.body);
|
||||
localStorage.setItem("isLoggedIn", "true");
|
||||
|
||||
HistorySafeGoBack(history);
|
||||
}, 500);
|
||||
};
|
||||
|
||||
const formik = useFormik({
|
||||
initialValues: {
|
||||
username: "",
|
||||
"!password": "",
|
||||
captcha: "",
|
||||
},
|
||||
onSubmit: async (values) => {
|
||||
setErr("");
|
||||
|
||||
const rj = await APIFetchV1(
|
||||
"/auth/login",
|
||||
{
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
username: values.username.trim(),
|
||||
"!password": values["!password"],
|
||||
captcha: values.captcha,
|
||||
}),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
},
|
||||
false,
|
||||
false,
|
||||
);
|
||||
|
||||
if (recaptchaRef.current) {
|
||||
recaptchaRef.current.reset();
|
||||
}
|
||||
|
||||
if (!rj.success) {
|
||||
setErr(HumaniseError(rj.description));
|
||||
return;
|
||||
}
|
||||
|
||||
const userRJ = await APIFetchV1<UserDocument>("/users/me");
|
||||
|
||||
if (userRJ.statusCode === 403) {
|
||||
setErr("You are banned.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!userRJ.success) {
|
||||
console.error("Error retrieving own user?");
|
||||
setErr("An internal server error has occurred.");
|
||||
return;
|
||||
}
|
||||
|
||||
toast.success("Logged in!");
|
||||
|
||||
setTimeout(() => {
|
||||
setUser(userRJ.body);
|
||||
localStorage.setItem("isLoggedIn", "true");
|
||||
|
||||
HistorySafeGoBack(history);
|
||||
}, 500);
|
||||
},
|
||||
onSubmit: performLogin,
|
||||
});
|
||||
|
||||
return (
|
||||
<LoginPageLayout description={<Description />} heading="Log In">
|
||||
<Form className="d-flex flex-column gap-4 w-100" onSubmit={formik.handleSubmit}>
|
||||
{import.meta.env.VITE_IS_LOCAL_DEV && (
|
||||
<Alert variant="info" className="mb-0">
|
||||
<div className="d-flex flex-column gap-2">
|
||||
<span>
|
||||
You are in local development mode. You can login as the admin
|
||||
account.
|
||||
</span>
|
||||
<Button
|
||||
className="align-self-start"
|
||||
onClick={() =>
|
||||
void performLogin({
|
||||
username: "admin",
|
||||
"!password": "password",
|
||||
captcha: formik.values.captcha,
|
||||
})
|
||||
}
|
||||
size="sm"
|
||||
type="button"
|
||||
variant="outline-primary"
|
||||
>
|
||||
Log in as admin
|
||||
</Button>
|
||||
</div>
|
||||
</Alert>
|
||||
)}
|
||||
<Form.Group>
|
||||
<Form.Label>Username</Form.Label>
|
||||
<Form.Control
|
||||
|
||||
@@ -18,13 +18,13 @@ export { type default as ImportLockTable, type ImportLock, type NewImportLock, t
|
||||
export { type score_blacklist_row_id, type default as ScoreBlacklistTable, type ScoreBlacklist, type NewScoreBlacklist, type ScoreBlacklistUpdate } from './public/ScoreBlacklist';
|
||||
export { type default as PrivDiscordUserMapTable, type PrivDiscordUserMap, type NewPrivDiscordUserMap, type PrivDiscordUserMapUpdate } from './public/PrivDiscordUserMap';
|
||||
export { type default as TableFolderTable, type TableFolder, type NewTableFolder, type TableFolderUpdate } from './public/TableFolder';
|
||||
export { type game_stats_game, type default as GameStatsTable, type GameStats, type NewGameStats, type GameStatsUpdate } from './public/GameStats';
|
||||
export { type priv_oauth2_auth_token_token, type default as PrivOauth2AuthTokenTable, type PrivOauth2AuthToken, type NewPrivOauth2AuthToken, type PrivOauth2AuthTokenUpdate } from './public/PrivOauth2AuthToken';
|
||||
export { type import_session_row_id, type default as ImportSessionTable, type ImportSession, type NewImportSession, type ImportSessionUpdate } from './public/ImportSession';
|
||||
export { type priv_svc_fer_card_card_id, type default as PrivSvcFerCardTable, type PrivSvcFerCard, type NewPrivSvcFerCard, type PrivSvcFerCardUpdate } from './public/PrivSvcFerCard';
|
||||
export { type default as PbComposedFromTable, type PbComposedFrom, type NewPbComposedFrom, type PbComposedFromUpdate } from './public/PbComposedFrom';
|
||||
export { type bms_course_lookup_md5sums, type default as BmsCourseLookupTable, type BmsCourseLookup, type NewBmsCourseLookup, type BmsCourseLookupUpdate } from './public/BmsCourseLookup';
|
||||
export { type song_search_term_search_term, type default as SongSearchTermTable, type SongSearchTerm, type NewSongSearchTerm, type SongSearchTermUpdate } from './public/SongSearchTerm';
|
||||
export { type game_profile_game, type default as GameProfileTable, type GameProfile, type NewGameProfile, type GameProfileUpdate } from './public/GameProfile';
|
||||
export { type import_game_game, type default as ImportGameTable, type ImportGame, type NewImportGame, type ImportGameUpdate } from './public/ImportGame';
|
||||
export { type import_goal_row_id, type default as ImportGoalTable, type ImportGoal, type NewImportGoal, type ImportGoalUpdate } from './public/ImportGoal';
|
||||
export { type default as OrphanChartUserTable, type OrphanChartUser, type NewOrphanChartUser, type OrphanChartUserUpdate } from './public/OrphanChartUser';
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
// @generated
|
||||
// This file is automatically generated by Kanel. Do not modify manually.
|
||||
|
||||
import type { default as Game } from './Game';
|
||||
import type { account_id } from './Account';
|
||||
import type { ColumnType, Selectable, Insertable, Updateable } from 'kysely';
|
||||
|
||||
/** Identifier type for public.game_profile */
|
||||
export type game_profile_game = Game;
|
||||
|
||||
/** Represents the table public.game_profile */
|
||||
export default interface GameProfileTable {
|
||||
user_id: ColumnType<account_id, account_id, account_id>;
|
||||
|
||||
game: ColumnType<game_profile_game, game_profile_game, game_profile_game>;
|
||||
|
||||
ratings: ColumnType<unknown, unknown, unknown>;
|
||||
|
||||
classes: ColumnType<unknown, unknown, unknown>;
|
||||
}
|
||||
|
||||
export type GameProfile = Selectable<GameProfileTable>;
|
||||
|
||||
export type NewGameProfile = Insertable<GameProfileTable>;
|
||||
|
||||
export type GameProfileUpdate = Updateable<GameProfileTable>;
|
||||
@@ -18,13 +18,13 @@ import type { default as ImportLockTable } from './ImportLock';
|
||||
import type { default as ScoreBlacklistTable } from './ScoreBlacklist';
|
||||
import type { default as PrivDiscordUserMapTable } from './PrivDiscordUserMap';
|
||||
import type { default as TableFolderTable } from './TableFolder';
|
||||
import type { default as GameStatsTable } from './GameStats';
|
||||
import type { default as PrivOauth2AuthTokenTable } from './PrivOauth2AuthToken';
|
||||
import type { default as ImportSessionTable } from './ImportSession';
|
||||
import type { default as PrivSvcFerCardTable } from './PrivSvcFerCard';
|
||||
import type { default as PbComposedFromTable } from './PbComposedFrom';
|
||||
import type { default as BmsCourseLookupTable } from './BmsCourseLookup';
|
||||
import type { default as SongSearchTermTable } from './SongSearchTerm';
|
||||
import type { default as GameProfileTable } from './GameProfile';
|
||||
import type { default as ImportGameTable } from './ImportGame';
|
||||
import type { default as ImportGoalTable } from './ImportGoal';
|
||||
import type { default as OrphanChartUserTable } from './OrphanChartUser';
|
||||
@@ -104,8 +104,6 @@ export default interface PublicSchema {
|
||||
|
||||
table_folder: TableFolderTable;
|
||||
|
||||
game_stats: GameStatsTable;
|
||||
|
||||
priv_oauth2_auth_token: PrivOauth2AuthTokenTable;
|
||||
|
||||
import_session: ImportSessionTable;
|
||||
@@ -118,6 +116,8 @@ export default interface PublicSchema {
|
||||
|
||||
song_search_term: SongSearchTermTable;
|
||||
|
||||
game_profile: GameProfileTable;
|
||||
|
||||
import_game: ImportGameTable;
|
||||
|
||||
import_goal: ImportGoalTable;
|
||||
|
||||
+6
-6
@@ -8,15 +8,15 @@ import {
|
||||
} from "tachi-common";
|
||||
import { type Database } from "tachi-db";
|
||||
|
||||
export const SELECT_GAME_STATS = [
|
||||
"game_stats.user_id",
|
||||
"game_stats.game",
|
||||
"game_stats.ratings",
|
||||
"game_stats.classes",
|
||||
export const SELECT_GAME_PROFILE = [
|
||||
"game_profile.user_id",
|
||||
"game_profile.game",
|
||||
"game_profile.ratings",
|
||||
"game_profile.classes",
|
||||
] as const;
|
||||
|
||||
export function ToGameStatsDocument(
|
||||
row: Selection<Database, "game_stats", (typeof SELECT_GAME_STATS)[number]>,
|
||||
row: Selection<Database, "game_profile", (typeof SELECT_GAME_PROFILE)[number]>,
|
||||
): UserGameStats {
|
||||
const { game, playtype } = V3ToGamePT(row.game);
|
||||
|
||||
@@ -6,7 +6,7 @@ import { ACTION_ChangeUsername } from "#actions/change-username.js";
|
||||
import { ACTION_UpdateUser } from "#actions/update-user.js";
|
||||
import { GetRecentActivity } from "#lib/activity/activity";
|
||||
import { ONE_MONTH } from "#lib/constants/time";
|
||||
import { SELECT_GAME_STATS, ToGameStatsDocument } from "#lib/db-formats/game-stats.js";
|
||||
import { SELECT_GAME_PROFILE, ToGameStatsDocument } from "#lib/db-formats/game-profiles.js";
|
||||
import { log } from "#lib/log/log";
|
||||
import { GetRivalIDs } from "#lib/rivals/rivals";
|
||||
import prValidate from "#server/middleware/prudence-validate";
|
||||
@@ -153,8 +153,8 @@ router.get("/game-stats", async (req, res) => {
|
||||
{
|
||||
__rankingData?: Record<AnyProfileRatingAlg, { outOf: number; ranking: number }>;
|
||||
} & UserGameStats
|
||||
> = await DB.selectFrom("game_stats")
|
||||
.select(SELECT_GAME_STATS)
|
||||
> = await DB.selectFrom("game_profile")
|
||||
.select(SELECT_GAME_PROFILE)
|
||||
.where("user_id", "=", user.id)
|
||||
.execute()
|
||||
.then((res) => res.map(ToGameStatsDocument));
|
||||
@@ -567,12 +567,12 @@ router.get(
|
||||
|
||||
const user = GetUser(req);
|
||||
|
||||
const gameStatRows = await DB.selectFrom("game_stats")
|
||||
.select(SELECT_GAME_STATS)
|
||||
const gameProfileRows = await DB.selectFrom("game_profile")
|
||||
.select(SELECT_GAME_PROFILE)
|
||||
.where("user_id", "=", user.id)
|
||||
.execute();
|
||||
|
||||
const gpts = gameStatRows.map(ToGameStatsDocument);
|
||||
const gpts = gameProfileRows.map(ToGameStatsDocument);
|
||||
|
||||
const data: Partial<Record<GPTString, unknown>> = {};
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ async function seedGameStats(
|
||||
ktLampRating: number | null,
|
||||
game: "iidx-sp" = "iidx-sp",
|
||||
) {
|
||||
await DB.insertInto("game_stats")
|
||||
await DB.insertInto("game_profile")
|
||||
.values({
|
||||
user_id: userId,
|
||||
game,
|
||||
@@ -96,7 +96,7 @@ describe("GetUsersRankingAndOutOf", () => {
|
||||
const user1 = await seedUser({ username: "bpi_high" });
|
||||
const user2 = await seedUser({ username: "bpi_low" });
|
||||
|
||||
await DB.insertInto("game_stats")
|
||||
await DB.insertInto("game_profile")
|
||||
.values({
|
||||
user_id: user1.id,
|
||||
game: "iidx-sp",
|
||||
@@ -104,7 +104,7 @@ describe("GetUsersRankingAndOutOf", () => {
|
||||
classes: JSON.stringify({}),
|
||||
})
|
||||
.execute();
|
||||
await DB.insertInto("game_stats")
|
||||
await DB.insertInto("game_profile")
|
||||
.values({
|
||||
user_id: user2.id,
|
||||
game: "iidx-sp",
|
||||
@@ -139,7 +139,7 @@ describe("GetUsersRankingAndOutOf", () => {
|
||||
const sdvxUser = await seedUser({ username: "sdvx_player" });
|
||||
|
||||
await seedGameStats(iidxUser.id, 10, "iidx-sp");
|
||||
await DB.insertInto("game_stats")
|
||||
await DB.insertInto("game_profile")
|
||||
.values({
|
||||
user_id: sdvxUser.id,
|
||||
game: "sdvx",
|
||||
@@ -162,7 +162,7 @@ describe("GetAllRankings", () => {
|
||||
const user1 = await seedUser({ username: "top_player" });
|
||||
const user2 = await seedUser({ username: "bot_player" });
|
||||
|
||||
await DB.insertInto("game_stats")
|
||||
await DB.insertInto("game_profile")
|
||||
.values({
|
||||
user_id: user1.id,
|
||||
game: "iidx-sp",
|
||||
@@ -170,7 +170,7 @@ describe("GetAllRankings", () => {
|
||||
classes: JSON.stringify({}),
|
||||
})
|
||||
.execute();
|
||||
await DB.insertInto("game_stats")
|
||||
await DB.insertInto("game_profile")
|
||||
.values({
|
||||
user_id: user2.id,
|
||||
game: "iidx-sp",
|
||||
|
||||
@@ -231,7 +231,7 @@ export async function GetUsersRankingAndOutOf(
|
||||
const v3Game = GamePTToV3(stats.game, stats.playtype);
|
||||
const userRating = stats.ratings[ratingAlg] ?? null;
|
||||
|
||||
const result = await DB.selectFrom("game_stats")
|
||||
const result = await DB.selectFrom("game_profile")
|
||||
.select([
|
||||
(eb) => eb.fn.countAll().as("out_of"),
|
||||
sql<number>`COUNT(*) FILTER (WHERE (ratings->>${ratingAlg})::numeric > ${userRating})`.as(
|
||||
|
||||
Reference in New Issue
Block a user