mirror of
https://github.com/linux-odyssey/linux-odyssey.git
synced 2026-11-05 19:20:12 +08:00
refactor: migrate session management to TRPC and remove legacy API endpoints
This commit is contained in:
1 parent
b4b8f8baee
commit
92173bb7e9
7 files changed
+71
-191
No files matched your search
@@ -1,7 +1,6 @@
|
|||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { FileGraph, FileObject } from '@linux-odyssey/file-graph'
|
import { FileGraph, FileObject } from '@linux-odyssey/file-graph'
|
||||||
import { IResponse, ITask } from '@linux-odyssey/game'
|
import { IResponse, ITask } from '@linux-odyssey/game'
|
||||||
import { createSession, getActiveSession } from '../utils/api'
|
|
||||||
import Socket from '../utils/socket'
|
import Socket from '../utils/socket'
|
||||||
import SocketTerminal from '../utils/terminal'
|
import SocketTerminal from '../utils/terminal'
|
||||||
import { Session } from '../types'
|
import { Session } from '../types'
|
||||||
@@ -50,7 +49,9 @@ const useSession = defineStore('session', {
|
|||||||
term.focus()
|
term.focus()
|
||||||
},
|
},
|
||||||
async createSession() {
|
async createSession() {
|
||||||
const session = await createSession(this.questId)
|
const session = await trpc.session.createSession.mutate({
|
||||||
|
questId: this.questId,
|
||||||
|
})
|
||||||
await this.setSession(session)
|
await this.setSession(session)
|
||||||
socket.emit('terminal', 'echo start\n')
|
socket.emit('terminal', 'echo start\n')
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -9,17 +9,3 @@ const api = axios.create({
|
|||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
})
|
})
|
||||||
export default api
|
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.' })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import { Router } from 'express'
|
import { Router } from 'express'
|
||||||
import passport from 'passport'
|
import passport from 'passport'
|
||||||
import sessions from './sessionRouter.js'
|
|
||||||
import commands from './commandRouter.js'
|
import commands from './commandRouter.js'
|
||||||
import authRouter from './authRouter.js'
|
import authRouter from './authRouter.js'
|
||||||
import authRequired from '../../middleware/authRequired.js'
|
import authRequired from '../../middleware/authRequired.js'
|
||||||
@@ -25,7 +24,6 @@ router.use(
|
|||||||
commands
|
commands
|
||||||
)
|
)
|
||||||
router.use('/auth', authRouter)
|
router.use('/auth', authRouter)
|
||||||
router.use('/sessions', authRequired, sessions)
|
|
||||||
|
|
||||||
router.get('/leaderboard', leaderboardController)
|
router.get('/leaderboard', leaderboardController)
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
import { HydratedDocument, Types } from 'mongoose'
|
import { HydratedDocument } from 'mongoose'
|
||||||
import { Session, UserProfile } from '@linux-odyssey/models'
|
import { Session, UserProfile } from '@linux-odyssey/models'
|
||||||
import type { ISession, IUser } from '@linux-odyssey/models'
|
import type { ISession, IUser } from '@linux-odyssey/models'
|
||||||
import { createContainer, deleteContainer } from '../containers/docker.js'
|
import { createContainer, deleteContainer } from '../containers/docker.js'
|
||||||
import logger from '../utils/logger.js'
|
import logger from '../utils/logger.js'
|
||||||
import { questManager } from './quest.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({
|
const sessions = await Session.find({
|
||||||
user: userId,
|
user: userId,
|
||||||
quest,
|
quest,
|
||||||
@@ -28,7 +28,7 @@ async function deactivateSessions(userId: Types.ObjectId, quest: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function createNewSession(
|
export async function createNewSession(
|
||||||
user: HydratedDocument<IUser>,
|
userId: string,
|
||||||
questId: string
|
questId: string
|
||||||
): Promise<ISession> {
|
): Promise<ISession> {
|
||||||
const quest = await questManager.get(questId)
|
const quest = await questManager.get(questId)
|
||||||
@@ -37,27 +37,29 @@ export async function createNewSession(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// deactivate all active sessions
|
// deactivate all active sessions
|
||||||
deactivateSessions(user._id, quest.id).catch((err) => {
|
deactivateSessions(userId, quest.id).catch((err) => {
|
||||||
logger.error(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(
|
const container = await createContainer(
|
||||||
`quest-${quest.id}-${user.username}-${Date.now()}`,
|
`quest-${quest.id}-${userProfile.user.username}-${Date.now()}`,
|
||||||
quest.id,
|
quest.id,
|
||||||
quest.image
|
quest.image
|
||||||
)
|
)
|
||||||
|
|
||||||
const newSession = new Session({
|
const newSession = new Session({
|
||||||
user,
|
user: userId,
|
||||||
quest: quest.id,
|
quest: quest.id,
|
||||||
containerId: container.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)
|
const progress = userProfile.progress.get(quest.id)
|
||||||
if (!progress) {
|
if (!progress) {
|
||||||
userProfile.progress.set(quest.id, {
|
userProfile.progress.set(quest.id, {
|
||||||
@@ -97,12 +99,12 @@ export async function finishSession(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function isQuestUnlocked(
|
export async function isQuestUnlocked(
|
||||||
user: HydratedDocument<IUser>,
|
userId: string,
|
||||||
questId: string
|
questId: string
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
const userProfile = await UserProfile.findOne({ user: user.id })
|
const userProfile = await UserProfile.findOne({ user: userId })
|
||||||
if (!userProfile) {
|
if (!userProfile) {
|
||||||
throw new Error(`UserProfile ${user.id} not found`)
|
throw new Error(`UserProfile ${userId} not found`)
|
||||||
}
|
}
|
||||||
const quest = await questManager.get(questId)
|
const quest = await questManager.get(questId)
|
||||||
if (!quest) {
|
if (!quest) {
|
||||||
|
|||||||
@@ -1,13 +1,55 @@
|
|||||||
import { TRPCError } from '@trpc/server'
|
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 { GameSession, VoidFileExistenceChecker } from '@linux-odyssey/game'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { isQuestUnlocked } from '../models/sessionManager.js'
|
import { createNewSession, isQuestUnlocked } from '../models/sessionManager.js'
|
||||||
import config from '../config.js'
|
import config from '../config.js'
|
||||||
import { protectedProcedure, router } from '../trpc.js'
|
import { protectedProcedure, router } from '../trpc.js'
|
||||||
import { questManager } from '../models/quest.js'
|
import { questManager } from '../models/quest.js'
|
||||||
|
|
||||||
|
function sessionDetail(session: ISession) {
|
||||||
|
const quest = questManager.get(session.quest)
|
||||||
|
if (!quest) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: 'NOT_FOUND',
|
||||||
|
message: 'Quest not found.',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const gameSession = new GameSession(
|
||||||
|
session,
|
||||||
|
quest,
|
||||||
|
new VoidFileExistenceChecker()
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
_id: session._id.toString(),
|
||||||
|
user: session.user,
|
||||||
|
quest: session.quest,
|
||||||
|
status: session.status,
|
||||||
|
createdAt: session.createdAt,
|
||||||
|
lastActivityAt: session.lastActivityAt,
|
||||||
|
completedEvents: session.completedEvents,
|
||||||
|
responses: gameSession.getResponses(),
|
||||||
|
tasks: gameSession.getTasks(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const sessionRouter = router({
|
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: 'FORBIDDEN',
|
||||||
|
message: 'Quest is locked.',
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
|
||||||
getActiveSession: protectedProcedure
|
getActiveSession: protectedProcedure
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
@@ -17,42 +59,14 @@ export const sessionRouter = router({
|
|||||||
.query(async (opts) => {
|
.query(async (opts) => {
|
||||||
const { questId } = opts.input
|
const { questId } = opts.input
|
||||||
const { user } = opts.ctx
|
const { user } = opts.ctx
|
||||||
if (
|
const session = await Session.findOne({
|
||||||
user &&
|
user: user.id,
|
||||||
((await isQuestUnlocked(user, questId)) || config.testing.enabled)
|
quest: questId,
|
||||||
) {
|
status: 'active',
|
||||||
const session = await Session.findOne({
|
|
||||||
user: user.id,
|
|
||||||
quest: questId,
|
|
||||||
status: 'active',
|
|
||||||
})
|
|
||||||
if (!session) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
const quest = await questManager.get(questId)
|
|
||||||
if (!quest) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
const gameSession = new GameSession(
|
|
||||||
session,
|
|
||||||
quest,
|
|
||||||
new VoidFileExistenceChecker()
|
|
||||||
)
|
|
||||||
return {
|
|
||||||
_id: session._id.toString(),
|
|
||||||
user: session.user,
|
|
||||||
quest: session.quest,
|
|
||||||
status: session.status,
|
|
||||||
createdAt: session.createdAt,
|
|
||||||
lastActivityAt: session.lastActivityAt,
|
|
||||||
completedEvents: session.completedEvents,
|
|
||||||
responses: gameSession.getResponses(),
|
|
||||||
tasks: gameSession.getTasks(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw new TRPCError({
|
|
||||||
code: 'BAD_REQUEST',
|
|
||||||
message: 'Quest is locked',
|
|
||||||
})
|
})
|
||||||
|
if (!session) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return sessionDetail(session)
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
Reference in new issue
Block a user