Files
core2026/app/gateway-api/src/router.ts
T

169 lines
5.8 KiB
TypeScript

import { TRPCError } from '@trpc/server';
import { z } from 'zod';
import { procedure, router } from './trpc.js';
import { toPublicUser } from './auth/userRepository.js';
const zUsername = z.string().min(2).max(32);
const zPassword = z.string().min(6).max(128);
const zProfile = z.string().min(1).max(64);
export const appRouter = router({
health: router({
ping: procedure.query(() => ({
ok: true,
now: new Date().toISOString(),
})),
}),
auth: router({
register: procedure
.input(
z.object({
username: zUsername,
password: zPassword,
displayName: z.string().min(2).max(40).optional(),
})
)
.mutation(async ({ ctx, input }) => {
const existing = await ctx.users.findByUsername(input.username);
if (existing) {
throw new TRPCError({
code: 'CONFLICT',
message: 'Username already exists.',
});
}
let created = null;
try {
created = await ctx.users.createUser(input);
} catch (error) {
throw new TRPCError({
code: 'CONFLICT',
message: 'Username already exists.',
cause: error,
});
}
const session = await ctx.sessions.createSession(created);
return {
user: toPublicUser(created),
sessionToken: session.sessionToken,
issuedAt: session.issuedAt,
};
}),
login: procedure
.input(
z.object({
username: zUsername,
password: zPassword,
})
)
.mutation(async ({ ctx, input }) => {
const user = await ctx.users.findByUsername(input.username);
if (!user) {
throw new TRPCError({
code: 'UNAUTHORIZED',
message: 'Invalid username or password.',
});
}
const ok = await ctx.users.verifyPassword(user, input.password);
if (!ok) {
throw new TRPCError({
code: 'UNAUTHORIZED',
message: 'Invalid username or password.',
});
}
const session = await ctx.sessions.createSession(user);
return {
user: toPublicUser(user),
sessionToken: session.sessionToken,
issuedAt: session.issuedAt,
};
}),
me: procedure
.input(
z.object({
sessionToken: z.string().min(1),
})
)
.query(async ({ ctx, input }) => {
const session = await ctx.sessions.getSession(input.sessionToken);
if (!session) {
return null;
}
return {
user: {
id: session.userId,
username: session.username,
displayName: session.displayName,
},
issuedAt: session.issuedAt,
};
}),
logout: procedure
.input(
z.object({
sessionToken: z.string().min(1),
})
)
.mutation(async ({ ctx, input }) => {
await ctx.sessions.revokeSession(input.sessionToken, { revokeGames: true });
return { ok: true };
}),
issueGameSession: procedure
.input(
z.object({
sessionToken: z.string().min(1),
profile: zProfile,
})
)
.mutation(async ({ ctx, input }) => {
const gameSession = await ctx.sessions.createGameSession(
input.sessionToken,
input.profile
);
if (!gameSession) {
throw new TRPCError({
code: 'UNAUTHORIZED',
message: 'Session is not valid.',
});
}
return {
profile: gameSession.profile,
gameToken: gameSession.gameToken,
issuedAt: gameSession.issuedAt,
};
}),
validateGameSession: procedure
.input(
z.object({
profile: zProfile,
gameToken: z.string().min(1),
})
)
.query(async ({ ctx, input }) => {
const gameSession = await ctx.sessions.getGameSession(
input.profile,
input.gameToken
);
if (!gameSession) {
return null;
}
const session = await ctx.sessions.getSession(gameSession.sessionToken);
if (!session) {
return null;
}
return {
profile: gameSession.profile,
sessionToken: gameSession.sessionToken,
user: {
id: gameSession.userId,
username: gameSession.username,
displayName: gameSession.displayName,
},
issuedAt: gameSession.issuedAt,
};
}),
}),
});
export type AppRouter = typeof appRouter;