From fac91e0f1014f8569f8c2f22ba4c2c0ad2534edb Mon Sep 17 00:00:00 2001 From: "Lin, Hung-Shin" Date: Wed, 8 Nov 2023 14:23:52 +0800 Subject: [PATCH 01/16] feat: update quest and user schema * add requirements in quest * add progress in user --- packages/models/quest.js | 36 ++++++++++++++++++++++++++++++------ packages/models/user.js | 20 ++++++++++++++++++++ quests/discover/game.yml | 6 ++++++ quests/read/game.yml | 6 ++++++ quests/spell/game.yml | 4 ++++ server/src/utils/quest.js | 2 +- 6 files changed, 67 insertions(+), 7 deletions(-) create mode 100644 quests/discover/game.yml create mode 100644 quests/read/game.yml diff --git a/packages/models/quest.js b/packages/models/quest.js index 1eb2b94..3634183 100644 --- a/packages/models/quest.js +++ b/packages/models/quest.js @@ -54,7 +54,10 @@ const stageSchema = new Schema({ required: true, default: false, }, - condition: conditionSchema, + condition: { + type: conditionSchema, + required: true, + }, responses: { type: [responseSchema], required: true, @@ -68,11 +71,32 @@ const stageSchema = new Schema({ const Quest = model( 'Quest', new Schema({ - _id: String, - title: String, - order: Number, - instruction: String, - stages: [stageSchema], + _id: { + type: String, + unique: true, + required: true, + }, + title: { + type: String, + required: true, + }, + requirements: { + type: [ + { + type: String, + ref: 'Quest', + }, + ], + required: true, + }, + instruction: { + type: String, + required: true, + }, + stages: { + type: [stageSchema], + required: true, + }, }) ) diff --git a/packages/models/user.js b/packages/models/user.js index 6c3bc44..7bdb2f3 100644 --- a/packages/models/user.js +++ b/packages/models/user.js @@ -1,5 +1,24 @@ import { model, Schema } from 'mongoose' +const progressSchema = new Schema({ + quest: { + type: String, + ref: 'Quest', + }, + sessions: [ + { + type: Schema.ObjectId, + ref: 'Session', + }, + ], + completionStatus: { + type: String, + enum: ['incomplete', 'completed'], + }, + startedAt: Date, + finishedAt: Date, +}) + const User = model( 'User', new Schema( @@ -23,6 +42,7 @@ const User = model( id: String, displayName: String, }, + progress: [progressSchema], }, { timestamps: true, diff --git a/quests/discover/game.yml b/quests/discover/game.yml new file mode 100644 index 0000000..dbfde46 --- /dev/null +++ b/quests/discover/game.yml @@ -0,0 +1,6 @@ +title: Discover the World! +instruction: | + 試著探索這個世界吧! + +requirements: + - spell diff --git a/quests/read/game.yml b/quests/read/game.yml new file mode 100644 index 0000000..9940a5a --- /dev/null +++ b/quests/read/game.yml @@ -0,0 +1,6 @@ +title: Read the File +instruction: | + 閱讀檔案 + +requirements: + - spell diff --git a/quests/spell/game.yml b/quests/spell/game.yml index 83c0d30..e56c5f2 100644 --- a/quests/spell/game.yml +++ b/quests/spell/game.yml @@ -4,6 +4,10 @@ instruction: | 「輸入 echo Hello World!」 +requirements: + - helloworld + - noexists + conditions: pwd_home: &pwd_home pwd: diff --git a/server/src/utils/quest.js b/server/src/utils/quest.js index 61866b0..ffb4041 100644 --- a/server/src/utils/quest.js +++ b/server/src/utils/quest.js @@ -31,7 +31,7 @@ export default async function loadAndUpdateQuests() { return Quest.findByIdAndUpdate( id, { - id, + _id: id, ...quest, }, { From e981bff3bb8692c23cf02259df95b184c6d9bd3a Mon Sep 17 00:00:00 2001 From: "Lin, Hung-Shin" Date: Wed, 8 Nov 2023 14:58:57 +0800 Subject: [PATCH 02/16] feat: change base image to alpine --- quests/Dockerfile | 24 ++++-------------------- 1 file changed, 4 insertions(+), 20 deletions(-) diff --git a/quests/Dockerfile b/quests/Dockerfile index 9eeb5ae..8044c7f 100644 --- a/quests/Dockerfile +++ b/quests/Dockerfile @@ -1,31 +1,15 @@ -FROM ubuntu:22.04 +FROM alpine:3.18 LABEL maintainer="wancat@wancat.cc" -RUN apt-get update && apt-get install -y \ - locales \ - curl \ +RUN apk add --no-cache \ vim \ zsh \ - ca-certificates \ - gnupg - -RUN sed -i -e 's/# zh_TW.UTF-8 UTF-8/zh_TW.UTF-8 UTF-8/' /etc/locale.gen && \ - dpkg-reconfigure --frontend=noninteractive locales && \ - update-locale LANG=zh_TW.UTF-8 - -ENV LANG zh_TW.UTF-8 -ENV LANGUAGE zh_TW:zh -ENV LC_ALL zh_TW.UTF-8 - -ENV NODE_MAJOR=20 -RUN curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg -RUN echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list -RUN apt-get update && apt-get install nodejs -y + nodejs \ + yarn WORKDIR /usr/local/lib/container -RUN npm install -g yarn COPY ./packages/container/package.json ./yarn.lock ./ RUN yarn install --modules-folder /usr/src/node_modules ENV NODE_PATH=/usr/src/node_modules From 45f12acbd97d68aac8c9cceaf0ae0ee8de2dcc9e Mon Sep 17 00:00:00 2001 From: wancat Date: Sat, 11 Nov 2023 15:49:06 +0800 Subject: [PATCH 03/16] feat: add UserProfile collection, create on login * add progress --- packages/models/index.js | 1 + packages/models/user.js | 20 ------------ packages/models/userProfile.js | 51 +++++++++++++++++++++++++++++++ server/src/auth/passport.js | 6 +++- server/src/game/sessionManager.js | 19 +++++++++++- 5 files changed, 75 insertions(+), 22 deletions(-) create mode 100644 packages/models/userProfile.js diff --git a/packages/models/index.js b/packages/models/index.js index db6b269..c3b835c 100644 --- a/packages/models/index.js +++ b/packages/models/index.js @@ -4,6 +4,7 @@ export { default as User } from './user.js' export { default as Quest } from './quest.js' export { default as Session } from './session.js' export { default as Command } from './command.js' +export { default as UserProfile } from './userProfile.js' export default async function connectDB(uri) { const db = await mongoose.connect(uri, { diff --git a/packages/models/user.js b/packages/models/user.js index 7bdb2f3..6c3bc44 100644 --- a/packages/models/user.js +++ b/packages/models/user.js @@ -1,24 +1,5 @@ import { model, Schema } from 'mongoose' -const progressSchema = new Schema({ - quest: { - type: String, - ref: 'Quest', - }, - sessions: [ - { - type: Schema.ObjectId, - ref: 'Session', - }, - ], - completionStatus: { - type: String, - enum: ['incomplete', 'completed'], - }, - startedAt: Date, - finishedAt: Date, -}) - const User = model( 'User', new Schema( @@ -42,7 +23,6 @@ const User = model( id: String, displayName: String, }, - progress: [progressSchema], }, { timestamps: true, diff --git a/packages/models/userProfile.js b/packages/models/userProfile.js new file mode 100644 index 0000000..61fe45d --- /dev/null +++ b/packages/models/userProfile.js @@ -0,0 +1,51 @@ +import { model, Schema } from 'mongoose' + +const progressSchema = new Schema({ + quest: { + type: String, + ref: 'Quest', + required: true, + }, + sessions: [ + { + type: Schema.ObjectId, + ref: 'Session', + required: true, + }, + ], + completed: { + type: Boolean, + default: false, + required: true, + }, + startedAt: { + type: Date, + required: true, + }, + finishedAt: Date, +}) + +const UserProfile = model( + 'UserProfile', + new Schema( + { + user: { + type: Schema.ObjectId, + ref: 'User', + unique: true, + required: true, + }, + progress: { + type: Map, + of: progressSchema, + default: {}, + required: true, + }, + }, + { + timestamps: true, + } + ) +) + +export default UserProfile diff --git a/server/src/auth/passport.js b/server/src/auth/passport.js index 9cd9011..47f9776 100644 --- a/server/src/auth/passport.js +++ b/server/src/auth/passport.js @@ -1,4 +1,5 @@ import passport from 'passport' +import { UserProfile } from '@linux-odyssey/models' import passwordStrategy from './passwordStrategy.js' import jwtStrategy from './jwtStrategy.js' import googleStrategy from './googleStrategy.js' @@ -24,7 +25,10 @@ if (enabledMethods.github) { } passport.serializeUser((user, done) => { - process.nextTick(() => { + process.nextTick(async () => { + if (!(await UserProfile.exists({ user: user._id }))) { + UserProfile.create({ user: user._id }) + } done(null, { _id: user._id, username: user.username, diff --git a/server/src/game/sessionManager.js b/server/src/game/sessionManager.js index 409f58e..6ab8c7c 100644 --- a/server/src/game/sessionManager.js +++ b/server/src/game/sessionManager.js @@ -1,4 +1,4 @@ -import { Session, Quest } from '@linux-odyssey/models' +import { Session, Quest, UserProfile } from '@linux-odyssey/models' import { createContainer, deleteContainer } from '../containers/docker.js' import SessionHandler from './sessionHandler.js' @@ -48,6 +48,23 @@ export async function createNewSession(user, questId) { const session = sessionHandler.getSession() await session.save() + + 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, { + quest: quest._id, + sessions: [session._id], + startedAt: new Date(), + }) + } else { + progress.sessions.push(session._id) + } + await userProfile.save() return session } From f49fb0e4dab8c967656f0e9e1fa9e55ce515aaf1 Mon Sep 17 00:00:00 2001 From: wancat Date: Sat, 11 Nov 2023 16:04:04 +0800 Subject: [PATCH 04/16] feat: finishSession and allow user command after finished --- .../src/api/controllers/commandController.js | 10 ++++++++++ server/src/game/sessionHandler.js | 5 ----- server/src/game/sessionManager.js | 20 +++++++++++++++++++ 3 files changed, 30 insertions(+), 5 deletions(-) diff --git a/server/src/api/controllers/commandController.js b/server/src/api/controllers/commandController.js index 2435176..6e7aa4d 100644 --- a/server/src/api/controllers/commandController.js +++ b/server/src/api/controllers/commandController.js @@ -1,6 +1,7 @@ import { Command, Session } from '@linux-odyssey/models' import CommandHandler from '../../game/commandHandler.js' import { pushToSession } from '../socket.js' +import { finishSession } from '../../game/sessionManager.js' const commandCompleteCallbacks = new Map() @@ -21,6 +22,11 @@ export async function newCommand(req, res) { return } + if (session.status === 'finished') { + // Allow commands to be sent to finished sessions + res.status(200).end() + return + } if (session.status !== 'active') { res.status(400).json({ message: 'session is not active' }) return @@ -45,6 +51,10 @@ export async function newCommand(req, res) { if (response) { c.stage = response.stage await c.save() + + if (response.stage === 'END') { + await finishSession(session) + } commandCompleteCallbacks.set(session.id, response.callback) pushToSession(session.id, 'status', 'pending') } diff --git a/server/src/game/sessionHandler.js b/server/src/game/sessionHandler.js index 7b11e1e..053126c 100644 --- a/server/src/game/sessionHandler.js +++ b/server/src/game/sessionHandler.js @@ -49,11 +49,6 @@ export default class SessionHandler { this.session.hints.push(...stage.hints) - if (stage.id === 'END') { - this.session.status = 'finished' - this.session.finishedAt = new Date() - } - this.addNewTasks() return { diff --git a/server/src/game/sessionManager.js b/server/src/game/sessionManager.js index 6ab8c7c..6fe800e 100644 --- a/server/src/game/sessionManager.js +++ b/server/src/game/sessionManager.js @@ -79,3 +79,23 @@ export async function getOrCreateActiveSession(user, questId) { } return createNewSession(user, questId) } + +export async function finishSession(session) { + session.status = 'finished' + await session.save() + + const userProfile = await UserProfile.findOne({ user: session.user }) + if (!userProfile) { + throw new Error(`UserProfile ${session.user} not found`) + } + + const progress = userProfile.progress.get(session.quest) + if (!progress) { + throw new Error(`Progress ${session.quest} not found`) + } + if (!progress.completed) { + progress.finishedAt = new Date() + progress.completed = true + } + await userProfile.save() +} From ae221a696c60a4c18196ed1ea6125e84a3bf147f Mon Sep 17 00:00:00 2001 From: wancat Date: Sat, 11 Nov 2023 16:05:29 +0800 Subject: [PATCH 05/16] fix: update repo url in package.json --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 601f97b..e7724c5 100644 --- a/package.json +++ b/package.json @@ -2,8 +2,8 @@ "name": "linux-odyssey", "version": "0.0.0", "main": "index.js", - "repository": "https://github.com/lancatlin/linux-odyssey", - "license": "MIT", + "repository": "https://github.com/linux-odyssey/linux-odyssey", + "license": "GPL", "private": true, "workspaces": [ "app", From a7422853fac4bf51af0232e3d5e19ad1dffbee28 Mon Sep 17 00:00:00 2001 From: wancat Date: Sat, 11 Nov 2023 16:33:05 +0800 Subject: [PATCH 06/16] feat: add /users/me endpoint --- server/src/api/controllers/userController.js | 25 ++++++ .../src/api/routes/{auth.js => authRouter.js} | 0 .../routes/{commands.js => commandRouter.js} | 0 server/src/api/routes/index.js | 10 ++- .../api/routes/{quests.js => questRouter.js} | 0 .../routes/{sessions.js => sessionRouter.js} | 0 server/src/api/routes/userRouter.js | 9 ++ server/swagger.yaml | 87 +++++++++++++++---- 8 files changed, 112 insertions(+), 19 deletions(-) create mode 100644 server/src/api/controllers/userController.js rename server/src/api/routes/{auth.js => authRouter.js} (100%) rename server/src/api/routes/{commands.js => commandRouter.js} (100%) rename server/src/api/routes/{quests.js => questRouter.js} (100%) rename server/src/api/routes/{sessions.js => sessionRouter.js} (100%) create mode 100644 server/src/api/routes/userRouter.js diff --git a/server/src/api/controllers/userController.js b/server/src/api/controllers/userController.js new file mode 100644 index 0000000..e6a3160 --- /dev/null +++ b/server/src/api/controllers/userController.js @@ -0,0 +1,25 @@ +import { UserProfile } from '@linux-odyssey/models' + +export async function me(req, res) { + const userProfile = await UserProfile.findOne({ + user: req.user._id, + }).populate('user') + console.log(userProfile) + if (!userProfile) { + res.status(404).json({ message: 'User not found' }) + return + } + const { + user: { username, email }, + progress, + } = userProfile + res.json({ + username, + email, + progress, + }) +} + +export default { + me, +} diff --git a/server/src/api/routes/auth.js b/server/src/api/routes/authRouter.js similarity index 100% rename from server/src/api/routes/auth.js rename to server/src/api/routes/authRouter.js diff --git a/server/src/api/routes/commands.js b/server/src/api/routes/commandRouter.js similarity index 100% rename from server/src/api/routes/commands.js rename to server/src/api/routes/commandRouter.js diff --git a/server/src/api/routes/index.js b/server/src/api/routes/index.js index aea4d31..427cf91 100644 --- a/server/src/api/routes/index.js +++ b/server/src/api/routes/index.js @@ -1,10 +1,11 @@ import { Router } from 'express' import passport from 'passport' -import sessions from './sessions.js' -import quests from './quests.js' -import commands from './commands.js' -import authRouter from './auth.js' +import sessions from './sessionRouter.js' +import quests from './questRouter.js' +import commands from './commandRouter.js' +import authRouter from './authRouter.js' import authRequired from '../../middleware/authRequired.js' +import userRouter from './userRouter.js' import config from '../../config.js' const router = Router() @@ -21,6 +22,7 @@ if (!config.isProduction) { router.use('/auth', authRouter) router.use('/quests', quests) +router.use('/users', userRouter) router.use('/sessions', authRequired, sessions) router.use( diff --git a/server/src/api/routes/quests.js b/server/src/api/routes/questRouter.js similarity index 100% rename from server/src/api/routes/quests.js rename to server/src/api/routes/questRouter.js diff --git a/server/src/api/routes/sessions.js b/server/src/api/routes/sessionRouter.js similarity index 100% rename from server/src/api/routes/sessions.js rename to server/src/api/routes/sessionRouter.js diff --git a/server/src/api/routes/userRouter.js b/server/src/api/routes/userRouter.js new file mode 100644 index 0000000..2bd7fe8 --- /dev/null +++ b/server/src/api/routes/userRouter.js @@ -0,0 +1,9 @@ +import express from 'express' +import userController from '../controllers/userController.js' +import authRequired from '../../middleware/authRequired.js' + +const router = express.Router() + +router.get('/me', authRequired, userController.me) + +export default router diff --git a/server/swagger.yaml b/server/swagger.yaml index b5cc28a..fab89f7 100644 --- a/server/swagger.yaml +++ b/server/swagger.yaml @@ -27,7 +27,7 @@ paths: - cookieAuth: [] parameters: - in: query - name: quest_id + name: questId description: find sessions by quest id schema: type: string @@ -60,7 +60,7 @@ paths: schema: type: object properties: - quest_id: + questId: type: string example: helloworld responses: @@ -93,9 +93,9 @@ paths: schema: type: 'object' required: - - quest_id + - questId properties: - quest_id: + questId: type: 'string' description: 'The ID of the quest.' example: 'helloworld' @@ -111,7 +111,7 @@ paths: 500: description: 'Internal Server Error.' - /sessions/{session_id}: + /sessions/{sessionId}: get: tags: - signed-in @@ -120,7 +120,7 @@ paths: - cookieAuth: [] parameters: - in: path - name: session_id + name: sessionId description: ID of the session to retrieve required: true schema: @@ -151,7 +151,7 @@ paths: - cookieAuth: [] parameters: - in: path - name: session_id + name: sessionId required: true schema: type: string @@ -468,12 +468,12 @@ paths: items: $ref: '#/components/schemas/QuestSummary' - /quests/{quest_id}: + /quests/{questId}: get: summary: get a quest's detail by name parameters: - in: path - name: quest_id + name: questId description: name of the quest to retrieve required: true schema: @@ -534,6 +534,23 @@ paths: type: string example: '127.0.0.1' + /users/me: + get: + summary: Get current user's profile + tags: + - signed-in + security: + - cookieAuth: [] + responses: + '200': + description: User profile + content: + application/json: + schema: + $ref: '#/components/schemas/UserProfile' + '401': + description: Unauthorized + components: schemas: User: @@ -549,6 +566,46 @@ components: email: type: string format: email + + UserProfile: + type: object + properties: + username: + type: string + example: 'alex' + email: + type: string + format: email + example: alex@example.com + progress: + type: object + additionalProperties: + $ref: '#/components/schemas/Progress' + + Progress: + type: object + properties: + questId: + type: string + example: helloworld + sessions: + type: array + items: + type: string + format: objectId + example: 64b7a3a3c182b4826dd1aaa1 + completed: + type: boolean + example: true + startedAt: + type: string + format: date-time + example: 2020-01-01T00:00:00.000Z + finishedAt: + type: string + format: date-time + example: 2020-01-01T00:00:00.000Z + Quest: type: object properties: @@ -587,13 +644,13 @@ components: $ref: '#/components/schemas/User' quest: $ref: '#/components/schemas/Quest' - created_at: + createdAt: type: string format: date-time - updated_at: + updatedAt: type: string format: date-time - finished_at: + finishedAt: type: string format: date-time hints: @@ -621,13 +678,13 @@ components: example: 64b7a3a3c182b4826dd1aaa1 quest: $ref: '#/components/schemas/Quest' - created_at: + createdAt: type: string format: date-time - updated_at: + updatedAt: type: string format: date-time - finished_at: + finishedAt: type: string format: date-time From 5df4967a85d4507767e21aa55a146b58657d0a30 Mon Sep 17 00:00:00 2001 From: wancat Date: Sat, 11 Nov 2023 17:11:25 +0800 Subject: [PATCH 07/16] feat: delete "deleteSession" method, validate questId and sessionId --- app/src/store/session.js | 6 +-- app/src/utils/socket.js | 4 +- packages/client/index.js | 4 +- .../src/api/controllers/commandController.js | 4 +- server/src/api/controllers/questController.js | 5 ++- .../src/api/controllers/sessionController.js | 36 +++++------------- server/src/api/routes/questRouter.js | 3 +- server/src/api/routes/sessionRouter.js | 7 ++-- server/src/api/socket.js | 32 ++++++++-------- server/src/api/validators/questValidator.js | 11 ++++++ server/src/api/validators/sessionValidator.js | 16 +++++++- server/swagger.yaml | 38 ------------------- 12 files changed, 67 insertions(+), 99 deletions(-) create mode 100644 server/src/api/validators/questValidator.js diff --git a/app/src/store/session.js b/app/src/store/session.js index eae3b08..f904f51 100644 --- a/app/src/store/session.js +++ b/app/src/store/session.js @@ -45,15 +45,13 @@ async function setSession(session) { export async function createSession() { console.log('Creating a new session...') - const res = await api.post('/sessions', { quest_id: store.questId }) + const res = await api.post('/sessions', { questId: store.questId }) const { data } = res await setSession(data) } async function getActiveSession(questId) { - const res = await api.post('/sessions/active', { - quest_id: questId, - }) + const res = await api.post('/sessions/active', { questId }) const session = res.data try { await setSession(session) diff --git a/app/src/utils/socket.js b/app/src/utils/socket.js index f242485..5411ab1 100644 --- a/app/src/utils/socket.js +++ b/app/src/utils/socket.js @@ -12,7 +12,7 @@ class SocketWrapper { this.socket = io('', { query: { - session_id: session._id, + sessionId: session._id, }, }) @@ -23,7 +23,7 @@ class SocketWrapper { this.socket.once('terminal', () => { resolve() }) - this.socket.once('error', reject) + this.socket.once('connect_error', reject) this.bindListeners() }) } diff --git a/packages/client/index.js b/packages/client/index.js index a732579..4505774 100644 --- a/packages/client/index.js +++ b/packages/client/index.js @@ -27,7 +27,7 @@ function debug(...args) { async function createSession() { console.log('Creating a new session...') const res = await api.post('/sessions', { - quest_id: 'helloworld', + questId: 'helloworld', }) const { data } = await res if (!data._id) { @@ -68,7 +68,7 @@ async function connect(sessionId, cookie) { Cookie: cookie, }, query: { - session_id: sessionId, + sessionId, }, }) diff --git a/server/src/api/controllers/commandController.js b/server/src/api/controllers/commandController.js index 6e7aa4d..ca19ac3 100644 --- a/server/src/api/controllers/commandController.js +++ b/server/src/api/controllers/commandController.js @@ -13,7 +13,7 @@ export async function newCommand(req, res) { return } - const sessionId = req.user.session_id + const { sessionId } = req.user const session = await Session.findById(sessionId) @@ -64,7 +64,7 @@ export async function newCommand(req, res) { } export async function completedCommand(req, res) { - const sessionId = req.user.session_id + const { sessionId } = req.user const session = await Session.findById(sessionId) diff --git a/server/src/api/controllers/questController.js b/server/src/api/controllers/questController.js index 745756d..098a77c 100644 --- a/server/src/api/controllers/questController.js +++ b/server/src/api/controllers/questController.js @@ -1,4 +1,5 @@ import { Quest } from '@linux-odyssey/models' +import { matchedData } from 'express-validator' export async function getQuests(req, res) { const quests = await Quest.find({}).sort({ order: 1 }) @@ -12,8 +13,8 @@ export async function getQuests(req, res) { } export async function getQuestDetail(req, res) { - const { id } = req.params - const quest = await Quest.findById(id) + const { questId } = matchedData(req) + const quest = await Quest.findById(questId) if (!quest) { res.status(404).json({ message: 'Quest not found.' }) return diff --git a/server/src/api/controllers/sessionController.js b/server/src/api/controllers/sessionController.js index 1fdbf32..69ce2af 100644 --- a/server/src/api/controllers/sessionController.js +++ b/server/src/api/controllers/sessionController.js @@ -28,10 +28,11 @@ function sessionDetail(session) { export async function getSessionList(req, res) { try { const query = { user: req.user._id } - if (req.query.quest_id) { - query.quest = req.query.quest_id + const { questId, status } = matchedData(req) + if (questId) { + query.quest = questId } - query.status = req.query.status || 'active' + query.status = status || 'active' const sessions = await Session.find(query) res.json(sessions.map(sessionSummary)) } catch (err) { @@ -41,9 +42,9 @@ export async function getSessionList(req, res) { } export async function createSession(req, res) { - const { quest_id } = matchedData(req) + const { questId } = matchedData(req) try { - const session = await createNewSession(req.user, quest_id) + const session = await createNewSession(req.user, questId) res.status(201).json(sessionDetail(session)) } catch (err) { console.error(err) @@ -53,8 +54,9 @@ export async function createSession(req, res) { export async function getSessionById(req, res) { try { + const { sessionId } = matchedData(req) const session = await Session.findOne({ - _id: req.params.id, + _id: sessionId, user: req.user._id, }) @@ -69,28 +71,10 @@ export async function getSessionById(req, res) { } } -export async function deleteSessionById(req, res) { - try { - const session = await Session.findOneAndDelete({ - _id: req.params.id, - user: req.user._id, - }) - - if (!session) { - res.status(404).json({ message: 'Session not found.' }) - return - } - - res.status(204).end() - } catch (err) { - res.status(500).json({ message: err.message }) - } -} - export async function getOrCreateSession(req, res) { - const { quest_id } = matchedData(req) + const { questId } = matchedData(req) try { - const session = await getOrCreateActiveSession(req.user, quest_id) + const session = await getOrCreateActiveSession(req.user, questId) res.json(sessionDetail(session)) } catch (err) { console.error(err) diff --git a/server/src/api/routes/questRouter.js b/server/src/api/routes/questRouter.js index 00fda55..4749f54 100644 --- a/server/src/api/routes/questRouter.js +++ b/server/src/api/routes/questRouter.js @@ -1,9 +1,10 @@ import { Router } from 'express' import * as questController from '../controllers/questController.js' +import { questIdValidator } from '../validators/questValidator.js' const router = Router() router.get('/', questController.getQuests) -router.get('/:id', questController.getQuestDetail) +router.get('/:questId', questIdValidator, questController.getQuestDetail) export default router diff --git a/server/src/api/routes/sessionRouter.js b/server/src/api/routes/sessionRouter.js index aad0db8..ddc7cdb 100644 --- a/server/src/api/routes/sessionRouter.js +++ b/server/src/api/routes/sessionRouter.js @@ -4,11 +4,13 @@ import { sessionRateLimit } from '../../middleware/rateLimiter.js' import { createSessionValidator, activeSessionValidator, + sessionListValidator, + sessionDetailValidator, } from '../validators/sessionValidator.js' const router = Router() -router.get('/', sessionController.getSessionList) +router.get('/', sessionListValidator, sessionController.getSessionList) router.post( '/', createSessionValidator, @@ -20,7 +22,6 @@ router.post( activeSessionValidator, sessionController.getOrCreateSession ) -router.get('/:id', sessionController.getSessionById) -router.delete('/:id', sessionController.deleteSessionById) +router.get('/:id', sessionDetailValidator, sessionController.getSessionById) export default router diff --git a/server/src/api/socket.js b/server/src/api/socket.js index 276970e..cd482c9 100644 --- a/server/src/api/socket.js +++ b/server/src/api/socket.js @@ -1,8 +1,10 @@ import { Server } from 'socket.io' +import validator from 'validator' import { Session } from '@linux-odyssey/models' import { getAndStartContainer, attachContainer } from '../containers/docker.js' import SessionMiddleware from '../middleware/session.js' import { genJWT } from '../utils/auth.js' +import { checkSessionId } from './validators/sessionValidator.js' const sessions = new Map() @@ -26,33 +28,26 @@ export function pushToSession(sessionId, event, ...args) { } } -async function connectClient(socket) { +async function connectClient(socket, next) { const user = socket.request.session?.passport?.user if (!user) { - socket.send('User not found.') - socket.disconnect() + next(new Error('User not found.')) return } - const sessionId = socket.handshake.query.session_id - if (!sessionId) { - socket.emit('error', 'Session ID not found.') - socket.disconnect() + const { sessionId } = socket.handshake.query + if (!validator.isMongoId(sessionId)) { + next(new Error('Invalid Session ID.')) return } - const session = await Session.findOne({ - _id: sessionId, - user, - status: 'active', - }) + const session = await Session.findById(sessionId) if (!session) { - socket.emit('error', 'Session not found.') - socket.disconnect() + next(new Error('Session not found.')) return } const token = await genJWT({ - session_id: session.id, + sessionId: session.id, }) let container @@ -61,8 +56,7 @@ async function connectClient(socket) { container = await getAndStartContainer(session.containerId) stream = await attachContainer(container, { token }) } catch (err) { - socket.emit('error', 'Failed to start container.') - socket.disconnect() + next(new Error('Failed to start container.')) return } @@ -95,6 +89,10 @@ export default (server) => { const io = new Server(server) io.engine.use(SessionMiddleware) + io.engine.use(checkSessionId()) io.on('connection', connectClient) + io.on('connect_error', (err) => { + console.error(err) + }) } diff --git a/server/src/api/validators/questValidator.js b/server/src/api/validators/questValidator.js new file mode 100644 index 0000000..be610a8 --- /dev/null +++ b/server/src/api/validators/questValidator.js @@ -0,0 +1,11 @@ +import { check } from 'express-validator' +import { noError } from '../../middleware/validator.js' + +export const checkQuestId = () => + check('questId').notEmpty().isString().isAlphanumeric().isLength({ max: 64 }) + +export const questIdValidator = [checkQuestId(), noError] + +export default { + questIdValidator, +} diff --git a/server/src/api/validators/sessionValidator.js b/server/src/api/validators/sessionValidator.js index 73a7bd2..fc80dde 100644 --- a/server/src/api/validators/sessionValidator.js +++ b/server/src/api/validators/sessionValidator.js @@ -1,8 +1,20 @@ import { check } from 'express-validator' +import { checkQuestId } from './questValidator.js' import { noError } from '../../middleware/validator.js' -const checkQuestId = () => - check('quest_id').notEmpty().isString().isLowercase().isLength({ max: 64 }) +const checkSessionStatus = () => + check('status') + .isString() + .custom((value) => ['active', 'inactive', 'finished'].includes(value)) +export const checkSessionId = () => + check('sessionId').notEmpty().isString().isMongoId() + +export const sessionListValidator = [ + checkQuestId(), + checkSessionStatus(), + noError, +] +export const sessionDetailValidator = [checkSessionId(), noError] export const createSessionValidator = [checkQuestId(), noError] export const activeSessionValidator = [checkQuestId(), noError] diff --git a/server/swagger.yaml b/server/swagger.yaml index fab89f7..bae7705 100644 --- a/server/swagger.yaml +++ b/server/swagger.yaml @@ -143,44 +143,6 @@ paths: properties: message: type: string - delete: - tags: - - signed-in - summary: delete a specific session by ID - security: - - cookieAuth: [] - parameters: - - in: path - name: sessionId - required: true - schema: - type: string - format: objectId - example: 64b7a3a3c182b4826dd1aaa1 - responses: - '204': - description: Session successfully deleted - - '404': - description: Session not found - content: - application/json: - schema: - type: object - properties: - message: - type: string - example: 'Session not found.' - '401': - description: Unauthorized - content: - application/json: - schema: - type: object - properties: - message: - type: string - example: 'Unauthorized.' /auth/register: post: From 1e728b413bafcb1fd9e07d2f24abecc54faf5788 Mon Sep 17 00:00:00 2001 From: wancat Date: Sat, 11 Nov 2023 17:25:01 +0800 Subject: [PATCH 08/16] fix: not reexport in default --- server/src/api/controllers/userController.js | 5 ----- server/src/api/routes/userRouter.js | 4 ++-- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/server/src/api/controllers/userController.js b/server/src/api/controllers/userController.js index e6a3160..1374634 100644 --- a/server/src/api/controllers/userController.js +++ b/server/src/api/controllers/userController.js @@ -4,7 +4,6 @@ export async function me(req, res) { const userProfile = await UserProfile.findOne({ user: req.user._id, }).populate('user') - console.log(userProfile) if (!userProfile) { res.status(404).json({ message: 'User not found' }) return @@ -19,7 +18,3 @@ export async function me(req, res) { progress, }) } - -export default { - me, -} diff --git a/server/src/api/routes/userRouter.js b/server/src/api/routes/userRouter.js index 2bd7fe8..235127f 100644 --- a/server/src/api/routes/userRouter.js +++ b/server/src/api/routes/userRouter.js @@ -1,9 +1,9 @@ import express from 'express' -import userController from '../controllers/userController.js' +import { me } from '../controllers/userController.js' import authRequired from '../../middleware/authRequired.js' const router = express.Router() -router.get('/me', authRequired, userController.me) +router.get('/me', authRequired, me) export default router From cce6127d51d30107e554acbf3f45176ef93f85bb Mon Sep 17 00:00:00 2001 From: wancat Date: Sat, 11 Nov 2023 17:29:22 +0800 Subject: [PATCH 09/16] fix: allow optional value by removing noError in SessionList --- server/src/api/validators/sessionValidator.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/server/src/api/validators/sessionValidator.js b/server/src/api/validators/sessionValidator.js index fc80dde..ccc441a 100644 --- a/server/src/api/validators/sessionValidator.js +++ b/server/src/api/validators/sessionValidator.js @@ -10,11 +10,7 @@ const checkSessionStatus = () => export const checkSessionId = () => check('sessionId').notEmpty().isString().isMongoId() -export const sessionListValidator = [ - checkQuestId(), - checkSessionStatus(), - noError, -] +export const sessionListValidator = [checkQuestId(), checkSessionStatus()] export const sessionDetailValidator = [checkSessionId(), noError] export const createSessionValidator = [checkQuestId(), noError] export const activeSessionValidator = [checkQuestId(), noError] From f973982ec48238b74c5ba18949a7aaaf94b79f21 Mon Sep 17 00:00:00 2001 From: wancat Date: Sat, 11 Nov 2023 17:44:41 +0800 Subject: [PATCH 10/16] feat: use dynamic route to select quest (no error handling yet) --- app/src/components/GameHeaderPart.vue | 20 +++----------------- app/src/router.js | 3 ++- app/src/views/GamePage.vue | 9 ++++++++- 3 files changed, 13 insertions(+), 19 deletions(-) diff --git a/app/src/components/GameHeaderPart.vue b/app/src/components/GameHeaderPart.vue index 7fb256b..43c589a 100644 --- a/app/src/components/GameHeaderPart.vue +++ b/app/src/components/GameHeaderPart.vue @@ -1,10 +1,8 @@