diff --git a/app/src/store/session.ts b/app/src/store/session.ts index aaa6512..6b28363 100644 --- a/app/src/store/session.ts +++ b/app/src/store/session.ts @@ -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') }, diff --git a/app/src/utils/api.ts b/app/src/utils/api.ts index 19c589e..94a987d 100644 --- a/app/src/utils/api.ts +++ b/app/src/utils/api.ts @@ -9,17 +9,3 @@ const api = axios.create({ withCredentials: true, }) export default api - -export async function createSession(questId: string): Promise { - const res = await api.post('/sessions', { questId }) - return res.data -} - -export async function getActiveSession( - questId: string -): Promise { - const res = await api.get('/sessions/active', { - params: { questId }, - }) - return res.data -} diff --git a/server/src/api/controllers/sessionController.ts b/server/src/api/controllers/sessionController.ts deleted file mode 100644 index 81dcf77..0000000 --- a/server/src/api/controllers/sessionController.ts +++ /dev/null @@ -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.' }) - } - } -) diff --git a/server/src/api/routes/index.ts b/server/src/api/routes/index.ts index a9aeba6..71cc14c 100644 --- a/server/src/api/routes/index.ts +++ b/server/src/api/routes/index.ts @@ -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) diff --git a/server/src/api/routes/sessionRouter.ts b/server/src/api/routes/sessionRouter.ts deleted file mode 100644 index 5e24fd2..0000000 --- a/server/src/api/routes/sessionRouter.ts +++ /dev/null @@ -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 diff --git a/server/src/models/sessionManager.ts b/server/src/models/sessionManager.ts index c4d0415..f12366a 100644 --- a/server/src/models/sessionManager.ts +++ b/server/src/models/sessionManager.ts @@ -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, + userId: string, questId: string ): Promise { 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, + userId: string, questId: string ): Promise { - 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) { diff --git a/server/src/routers/sessionRouter.ts b/server/src/routers/sessionRouter.ts index 7c4333b..0c7ba7d 100644 --- a/server/src/routers/sessionRouter.ts +++ b/server/src/routers/sessionRouter.ts @@ -1,13 +1,55 @@ 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' +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({ + 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 .input( z.object({ @@ -17,42 +59,14 @@ export const sessionRouter = router({ .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) - 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', + const session = await Session.findOne({ + user: user.id, + quest: questId, + status: 'active', }) + if (!session) { + return null + } + return sessionDetail(session) }), })