From ea8236eadccce8deeacaa79e0edc78ae09b08562 Mon Sep 17 00:00:00 2001 From: grumd Date: Sun, 4 Jan 2026 17:07:05 +0100 Subject: [PATCH 1/2] Add dev login --- .gitignore | 1 + packages/api/src/services/auth/devLogin.ts | 48 ++++++++++++++++ packages/api/src/trpc/routes/auth.ts | 14 ++++- packages/api/src/trpc/trpc.ts | 10 ++++ packages/web/src/features/login/DevLogin.tsx | 59 ++++++++++++++++++++ packages/web/src/features/login/Login.tsx | 3 + 6 files changed, 134 insertions(+), 1 deletion(-) create mode 100644 packages/api/src/services/auth/devLogin.ts create mode 100644 packages/web/src/features/login/DevLogin.tsx diff --git a/.gitignore b/.gitignore index 0710e51c..469a3bd6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ node_modules .env .vscode +.mcp.json *.tsbuildinfo .tsbuildinfo diff --git a/packages/api/src/services/auth/devLogin.ts b/packages/api/src/services/auth/devLogin.ts new file mode 100644 index 00000000..d93caa24 --- /dev/null +++ b/packages/api/src/services/auth/devLogin.ts @@ -0,0 +1,48 @@ +import crypto from 'crypto'; +import { db } from 'db'; +import createDebug from 'debug'; + +const debug = createDebug('backend-ts:auth'); + +function generateSessionId(): string { + return crypto.randomBytes(16).toString('hex'); +} + +export async function devLogin(playerId: number): Promise<{ session: string }> { + debug(`Dev login for player ID: ${playerId}`); + + // Verify player exists + const player = await db + .selectFrom('players') + .select(['id', 'nickname']) + .where('id', '=', playerId) + .executeTakeFirst(); + + if (!player) { + throw new Error(`Player with ID ${playerId} not found`); + } + + debug(`Found player: ${player.nickname} (${player.id})`); + + // Delete old sessions for this player + await db.deleteFrom('sessions').where('player', '=', player.id).execute(); + + // Create new session (valid for 2 weeks) + const now = new Date(); + const sessionId = generateSessionId(); + const validUntil = new Date(now.getTime() + 14 * 24 * 60 * 60 * 1000); + + await db + .insertInto('sessions') + .values({ + id: sessionId, + player: player.id, + established: now, + valid_until: validUntil, + }) + .execute(); + + debug(`Created dev session for player ${player.id}`); + + return { session: sessionId }; +} diff --git a/packages/api/src/trpc/routes/auth.ts b/packages/api/src/trpc/routes/auth.ts index db080c0f..c99d41e5 100644 --- a/packages/api/src/trpc/routes/auth.ts +++ b/packages/api/src/trpc/routes/auth.ts @@ -1,8 +1,9 @@ +import { devLogin as devLoginService } from 'services/auth/devLogin'; import { getRegistrationToken as getRegistrationTokenService } from 'services/auth/getRegistrationToken'; import { loginWithGoogleCredential } from 'services/auth/googleLogin'; import { logout as logoutService } from 'services/auth/logout'; import { registerPlayer } from 'services/auth/register'; -import { publicProcedure, router } from 'trpc/trpc'; +import { devProcedure, publicProcedure, router } from 'trpc/trpc'; import { z } from 'zod'; export const loginGoogle = publicProcedure @@ -44,9 +45,20 @@ export const logout = publicProcedure.mutation(async ({ ctx }) => { } }); +export const devLogin = devProcedure + .input( + z.object({ + playerId: z.number(), + }) + ) + .mutation(({ input }) => { + return devLoginService(input.playerId); + }); + export const auth = router({ loginGoogle, getRegistrationToken, register, logout, + devLogin, }); diff --git a/packages/api/src/trpc/trpc.ts b/packages/api/src/trpc/trpc.ts index afa79c62..65b180dc 100644 --- a/packages/api/src/trpc/trpc.ts +++ b/packages/api/src/trpc/trpc.ts @@ -66,3 +66,13 @@ export const adminProcedure = t.procedure.use(async ({ ctx, next }) => { } return next({ ctx: { user: ctx.user } }); }); + +/** + * Dev procedure - only available in development mode + */ +export const devProcedure = t.procedure.use(async ({ next }) => { + if (process.env.NODE_ENV !== 'development') { + throw new TRPCError({ code: 'FORBIDDEN', message: 'Only available in development mode' }); + } + return next(); +}); diff --git a/packages/web/src/features/login/DevLogin.tsx b/packages/web/src/features/login/DevLogin.tsx new file mode 100644 index 00000000..aa9c4b54 --- /dev/null +++ b/packages/web/src/features/login/DevLogin.tsx @@ -0,0 +1,59 @@ +import { Select } from '@mantine/core'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import cookies from 'browser-cookies'; +import { useState } from 'react'; + +import { api } from 'utils/trpc'; + +export function DevLogin() { + if (!import.meta.env.DEV) { + return null; + } + + const [error, setError] = useState(null); + const queryClient = useQueryClient(); + + const playersQuery = useQuery(api.players.list.queryOptions({})); + + const devLoginMutation = useMutation( + api.auth.devLogin.mutationOptions({ + onSuccess: (data) => { + cookies.set('session', data.session, { expires: 14 }); + queryClient.invalidateQueries(api.user.current.queryFilter()); + }, + onError: (err) => { + console.error('Dev login error:', err); + setError(err.message); + }, + }) + ); + + const players = playersQuery.data ?? []; + const selectData = players.map((player) => ({ + value: String(player.id), + label: player.nickname, + })); + + const handleChange = (value: string | null) => { + if (value) { + setError(null); + devLoginMutation.mutate({ playerId: Number(value) }); + } + }; + + return ( +
+ - + {playerIndex === 0 ? : `#${playerIndex + 1}`} - {player.exp != null ? : null} + {player.exp != null ? : null} {player.region ? : null} @@ -83,16 +83,18 @@ export default function RankingList({ ranking, isLoading, preferences }: Ranking {player.accuracy ? `${player.accuracy.toFixed(2)}%` : ''} - { - if (preferences) { - preferencesMutation.mutate( - _.set(['playersHiddenStatus', player.id], !isHidden, preferences) - ); - } - }} - checked={!isHidden} - /> +
+ { + if (preferences) { + preferencesMutation.mutate( + _.set(['playersHiddenStatus', player.id], !isHidden, preferences) + ); + } + }} + checked={!isHidden} + /> +
); diff --git a/packages/web/src/features/ranking/ranking.scss b/packages/web/src/features/ranking/ranking.scss index c6918f16..45f2b8de 100644 --- a/packages/web/src/features/ranking/ranking.scss +++ b/packages/web/src/features/ranking/ranking.scss @@ -73,8 +73,11 @@ padding-bottom: 0; color: #888; font-weight: normal; + text-align: left; + &.grades { text-align: center; + img { height: 12px; } @@ -112,6 +115,7 @@ } &.exp-rank { img { + vertical-align: middle; width: 2em; } } @@ -164,10 +168,9 @@ &.hide-col { padding-right: 0.75em; - > .toggle-checkbox { + > .switch-wrapper { display: flex; - justify-content: end; - align-items: stretch; + justify-content: flex-end; } } &.change { @@ -194,57 +197,56 @@ @media screen and (max-width: 900px) { .ranking-page { - font-size: 3.2vw; + font-size: 14px; .ranking-list { - padding: 0.4em; + padding: 4px; } table { - border-spacing: 0 1vw; + border-spacing: 0 4px; } td, th { - font-size: 80%; + padding: 0.4rem 0.3rem; &.grades { display: none; - img { - height: 1.5vw; - } } &.name-piu { display: none; } + &.playcount, &.accuracy { - padding-right: 1vw; + display: none; } } tr.player { - height: 7.4vw; + height: auto; } td { &.place { - padding-left: 1.5vw; + padding-left: 6px; padding-right: 0; - font-size: 80%; + font-size: 90%; > svg { - font-size: 140%; + font-size: 130%; } } &.exp-rank { - font-size: 75%; + padding: 4px; + img { + width: 24px; + height: 24px; + } } - &.total-score { - padding: 1vw 0.5vw; + &.name { + font-size: 15px; + width: auto; } &.rating { - font-size: 100%; - } - &.rating-change-cell { - font-size: 100%; + font-size: 15px; + font-weight: bold; } &.hide-col { - .toggle-checkbox { - font-size: 100%; - } + padding: 0 6px; } } } diff --git a/packages/web/src/features/root/index.scss b/packages/web/src/features/root/index.scss index 8e63348d..26af4741 100644 --- a/packages/web/src/features/root/index.scss +++ b/packages/web/src/features/root/index.scss @@ -18,7 +18,7 @@ body { @media screen and (max-width: 900px) { :root { - font-size: 2vw; + font-size: 14px; } }