refactor: migrate session management to TRPC and remove legacy API endpoints

This commit is contained in:
Justin Lin committed 2025-04-23 16:37:12 +08:00
1 parent b4b8f8baee
commit 92173bb7e9
7 files changed
+61 -181

No files matched your search

+3 -2
View File
@@ -1,7 +1,6 @@
import { defineStore } from 'pinia'
import { FileGraph, FileObject } from '@linux-odyssey/file-graph'
import { IResponse, ITask } from '@linux-odyssey/game'
import { createSession, getActiveSession } from '../utils/api'
import Socket from '../utils/socket'
import SocketTerminal from '../utils/terminal'
import { Session } from '../types'
@@ -50,7 +49,9 @@ const useSession = defineStore('session', {
term.focus()
},
async createSession() {
const session = await createSession(this.questId)
const session = await trpc.session.createSession.mutate({
questId: this.questId,
})
await this.setSession(session)
socket.emit('terminal', 'echo start\n')
},
-14
View File
@@ -9,17 +9,3 @@ const api = axios.create({
withCredentials: true,
})
export default api
export async function createSession(questId: string): Promise<Session> {
const res = await api.post<Session>('/sessions', { questId })
return res.data
}
export async function getActiveSession(
questId: string
): Promise<Session | null> {
const res = await api.get<Session | null>('/sessions/active', {
params: { questId },
})
return res.data
}
@@ -1,90 +0,0 @@
import { matchedData } from 'express-validator'
import type { Express, Request, Response } from 'express'
import { User, Session, ISession } from '@linux-odyssey/models'
import {
createNewSession,
isQuestUnlocked,
} from '../../models/sessionManager.js'
import { asyncHandler } from '../../middleware/error.js'
import config from '../../config.js'
function sessionSummary(session: ISession) {
return {
_id: session._id,
user: session.user,
quest: session.quest,
status: session.status,
createdAt: session.createdAt,
lastActivityAt: session.lastActivityAt,
}
}
function sessionDetail(session: ISession) {
return {
...sessionSummary(session),
events: session.completedEvents,
}
}
export const getSessionList = asyncHandler(
async (req: Request, res: Response) => {
const user = req.user as Express.ExistingUser
const { questId, status } = matchedData(req)
const query = { user: user.id, quest: questId, status }
const sessions = await Session.find(query)
res.json(sessions.map(sessionSummary))
}
)
export const createSession = asyncHandler(
async (req: Request, res: Response) => {
const { questId } = matchedData(req)
const user = await User.findById((req.user as Express.ExistingUser).id)
if (
user &&
((await isQuestUnlocked(user, questId)) || config.testing.enabled)
) {
const session = await createNewSession(user, questId)
res.status(201).json(sessionDetail(session))
} else {
res.status(403).json({ message: 'Quest is locked.' })
}
}
)
export const getSessionById = asyncHandler(
async (req: Request, res: Response) => {
const { sessionId } = matchedData(req)
const session = await Session.findOne({
_id: sessionId,
user: (req.user as Express.ExistingUser).id,
})
if (!session) {
res.status(404).json({ message: 'Session not found.' })
return
}
res.json(sessionDetail(session))
}
)
export const getActiveSessionHandler = asyncHandler(
async (req: Request, res: Response) => {
const { questId } = matchedData(req)
const user = await User.findById((req.user as Express.ExistingUser).id)
if (
user &&
((await isQuestUnlocked(user, questId)) || config.testing.enabled)
) {
const session = await Session.findOne({
user: user.id,
quest: questId,
status: 'active',
})
res.json(session ? sessionDetail(session) : null)
} else {
res.status(403).json({ message: 'Quest is locked.' })
}
}
)
-2
View File
@@ -1,6 +1,5 @@
import { Router } from 'express'
import passport from 'passport'
import sessions from './sessionRouter.js'
import commands from './commandRouter.js'
import authRouter from './authRouter.js'
import authRequired from '../../middleware/authRequired.js'
@@ -25,7 +24,6 @@ router.use(
commands
)
router.use('/auth', authRouter)
router.use('/sessions', authRequired, sessions)
router.get('/leaderboard', leaderboardController)
-31
View File
@@ -1,31 +0,0 @@
import { Router } from 'express'
import * as sessionController from '../controllers/sessionController.js'
import { sessionRateLimit } from '../../middleware/rateLimiter.js'
import {
createSessionValidator,
activeSessionValidator,
sessionListValidator,
sessionDetailValidator,
} from '../validators/sessionValidator.js'
const router = Router()
router.get('/', sessionListValidator, sessionController.getSessionList)
router.post(
'/',
createSessionValidator,
sessionRateLimit,
sessionController.createSession
)
router.get(
'/active',
activeSessionValidator,
sessionController.getActiveSessionHandler
)
router.get(
'/:sessionId',
sessionDetailValidator,
sessionController.getSessionById
)
export default router
+16 -14
View File
@@ -1,11 +1,11 @@
import { HydratedDocument, Types } from 'mongoose'
import { HydratedDocument } from 'mongoose'
import { Session, UserProfile } from '@linux-odyssey/models'
import type { ISession, IUser } from '@linux-odyssey/models'
import { createContainer, deleteContainer } from '../containers/docker.js'
import logger from '../utils/logger.js'
import { questManager } from './quest.js'
async function deactivateSessions(userId: Types.ObjectId, quest: string) {
async function deactivateSessions(userId: string, quest: string) {
const sessions = await Session.find({
user: userId,
quest,
@@ -28,7 +28,7 @@ async function deactivateSessions(userId: Types.ObjectId, quest: string) {
}
export async function createNewSession(
user: HydratedDocument<IUser>,
userId: string,
questId: string
): Promise<ISession> {
const quest = await questManager.get(questId)
@@ -37,27 +37,29 @@ export async function createNewSession(
}
// deactivate all active sessions
deactivateSessions(user._id, quest.id).catch((err) => {
deactivateSessions(userId, quest.id).catch((err) => {
logger.error(err)
})
const userProfile = await UserProfile.findOne({ user: userId }).populate<{
user: IUser
}>('user')
if (!userProfile) {
throw new Error(`UserProfile ${userId} not found`)
}
const container = await createContainer(
`quest-${quest.id}-${user.username}-${Date.now()}`,
`quest-${quest.id}-${userProfile.user.username}-${Date.now()}`,
quest.id,
quest.image
)
const newSession = new Session({
user,
user: userId,
quest: quest.id,
containerId: container.id,
})
const userProfile = await UserProfile.findOne({ user: user._id })
if (!userProfile) {
throw new Error(`UserProfile ${user._id} not found`)
}
const progress = userProfile.progress.get(quest.id)
if (!progress) {
userProfile.progress.set(quest.id, {
@@ -97,12 +99,12 @@ export async function finishSession(
}
export async function isQuestUnlocked(
user: HydratedDocument<IUser>,
userId: string,
questId: string
): Promise<boolean> {
const userProfile = await UserProfile.findOne({ user: user.id })
const userProfile = await UserProfile.findOne({ user: userId })
if (!userProfile) {
throw new Error(`UserProfile ${user.id} not found`)
throw new Error(`UserProfile ${userId} not found`)
}
const quest = await questManager.get(questId)
if (!quest) {
+42 -28
View File
@@ -1,37 +1,19 @@
import { TRPCError } from '@trpc/server'
import { Session } from '@linux-odyssey/models'
import { ISession, Session } from '@linux-odyssey/models'
import { GameSession, VoidFileExistenceChecker } from '@linux-odyssey/game'
import { z } from 'zod'
import { isQuestUnlocked } from '../models/sessionManager.js'
import { createNewSession, isQuestUnlocked } from '../models/sessionManager.js'
import config from '../config.js'
import { protectedProcedure, router } from '../trpc.js'
import { questManager } from '../models/quest.js'
export const sessionRouter = router({
getActiveSession: protectedProcedure
.input(
z.object({
questId: z.string(),
})
)
.query(async (opts) => {
const { questId } = opts.input
const { user } = opts.ctx
if (
user &&
((await isQuestUnlocked(user, questId)) || config.testing.enabled)
) {
const session = await Session.findOne({
user: user.id,
quest: questId,
status: 'active',
})
if (!session) {
return null
}
const quest = await questManager.get(questId)
function sessionDetail(session: ISession) {
const quest = questManager.get(session.quest)
if (!quest) {
return null
throw new TRPCError({
code: 'NOT_FOUND',
message: 'Quest not found.',
})
}
const gameSession = new GameSession(
session,
@@ -50,9 +32,41 @@ export const sessionRouter = router({
tasks: gameSession.getTasks(),
}
}
export const sessionRouter = router({
createSession: protectedProcedure
.input(z.object({ questId: z.string() }))
.mutation(async (opts) => {
const { questId } = opts.input
const { user } = opts.ctx
console.log('createSession', user, questId)
if ((await isQuestUnlocked(user.id, questId)) || config.testing.enabled) {
const session = await createNewSession(user.id, questId)
return sessionDetail(session)
}
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'Quest is locked',
code: 'FORBIDDEN',
message: 'Quest is locked.',
})
}),
getActiveSession: protectedProcedure
.input(
z.object({
questId: z.string(),
})
)
.query(async (opts) => {
const { questId } = opts.input
const { user } = opts.ctx
const session = await Session.findOne({
user: user.id,
quest: questId,
status: 'active',
})
if (!session) {
return null
}
return sessionDetail(session)
}),
})