mirror of
https://github.com/linux-odyssey/linux-odyssey.git
synced 2026-11-05 19:20:12 +08:00
Merge branch 'main' into feat-automation-test
This commit is contained in:
commit
cb376caff0
41 files changed
+536
-301
No files matched your search
+2
-1
@@ -16,14 +16,15 @@
|
||||
"@fortawesome/free-regular-svg-icons": "^6.4.0",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.4.0",
|
||||
"@fortawesome/vue-fontawesome": "^3.0.3",
|
||||
"@linux-odyssey/constants": "*",
|
||||
"@linux-odyssey/file-graph": "*",
|
||||
"@linux-odyssey/utils": "*",
|
||||
"@linux-odyssey/constants": "*",
|
||||
"axios": "^1.6.0",
|
||||
"socket.io-client": "^4.7.1",
|
||||
"validator": "^13.11.0",
|
||||
"vue": "^3.3.4",
|
||||
"vue-router": "^4.2.4",
|
||||
"vue-toastification": "^2.0.0-rc.5",
|
||||
"xterm": "^5.2.1",
|
||||
"xterm-addon-attach": "^0.8.0",
|
||||
"xterm-addon-fit": "^0.7.0"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup>
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { RouterLink, useRouter } from 'vue-router'
|
||||
import { useToast } from 'vue-toastification'
|
||||
import api from '../utils/api'
|
||||
|
||||
const emit = defineEmits(['onSubmit', 'onChange'])
|
||||
@@ -24,6 +25,7 @@ const username = ref('')
|
||||
const email = ref('')
|
||||
const password = ref('')
|
||||
const errorMessage = ref('')
|
||||
const toast = useToast()
|
||||
|
||||
const availableMethods = ref({})
|
||||
onMounted(async () => {
|
||||
@@ -39,11 +41,10 @@ const hasSocialLogins = computed(() => {
|
||||
})
|
||||
|
||||
const success = () => {
|
||||
router.push({ name: 'game' })
|
||||
router.push({ name: 'game', params: { questId: 'helloworld' } })
|
||||
}
|
||||
|
||||
const error = (err) => {
|
||||
console.error(err)
|
||||
errorMessage.value = err
|
||||
}
|
||||
|
||||
@@ -62,8 +63,8 @@ const handleSubmit = () => {
|
||||
error,
|
||||
})
|
||||
} catch (err) {
|
||||
toast.error('Something went wrong.')
|
||||
console.error(err)
|
||||
errorMessage.value = 'Something went wrong'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,34 +1,24 @@
|
||||
<script setup>
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useToast } from 'vue-toastification'
|
||||
import { logout } from '../utils/auth'
|
||||
import { bugReportUrl } from '../config'
|
||||
import api from '../utils/api'
|
||||
import { reset } from '../store/session'
|
||||
import sessionStore, { reset } from '../store/session'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const toast = useToast()
|
||||
|
||||
const handleLogout = async () => {
|
||||
try {
|
||||
await logout()
|
||||
reset()
|
||||
router.push({ name: 'login' })
|
||||
} catch (err) {
|
||||
toast.error('Failed to logout')
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
|
||||
const questData = ref(null)
|
||||
const questErr = ref(null)
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const res = await api.get('/quests/helloworld')
|
||||
questData.value = res.data
|
||||
} catch (err) {
|
||||
questErr.value = err
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -47,14 +37,12 @@ onMounted(async () => {
|
||||
class="text-text-tertiary h-full w-full"
|
||||
/>
|
||||
</button> -->
|
||||
<button>
|
||||
<p
|
||||
class="text-text-primary inline-block font-bold whitespace-nowrap"
|
||||
v-if="questData"
|
||||
>
|
||||
{{ questData.title }}
|
||||
</p>
|
||||
</button>
|
||||
<p
|
||||
class="text-text-primary inline-block font-bold whitespace-nowrap"
|
||||
v-if="sessionStore.quest"
|
||||
>
|
||||
{{ sessionStore.quest.title }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="h-full w-full flex gap-3 items-center justify-end">
|
||||
<!-- <button id="warning" class="flex items-center h-5 w-5">
|
||||
|
||||
@@ -3,6 +3,9 @@ import './style.css'
|
||||
/* import font awesome icon component */
|
||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
||||
import './library'
|
||||
/* import Vue Toastification */
|
||||
import Toast, { POSITION } from 'vue-toastification'
|
||||
import 'vue-toastification/dist/index.css'
|
||||
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
@@ -13,4 +16,10 @@ app.component('font-awesome-icon', FontAwesomeIcon)
|
||||
|
||||
app.use(router)
|
||||
|
||||
const options = {
|
||||
position: POSITION.TOP_RIGHT,
|
||||
}
|
||||
|
||||
app.use(Toast, options)
|
||||
|
||||
app.mount('#app')
|
||||
+11
-2
@@ -8,7 +8,12 @@ import { isLoggedIn } from './utils/auth'
|
||||
const routes = [
|
||||
{
|
||||
path: '/',
|
||||
redirect: '/game',
|
||||
redirect: {
|
||||
name: 'game',
|
||||
params: {
|
||||
questId: 'helloworld',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/login',
|
||||
@@ -23,9 +28,10 @@ const routes = [
|
||||
meta: { requiresGuest: true },
|
||||
},
|
||||
{
|
||||
path: '/game',
|
||||
path: '/game/:questId',
|
||||
name: 'game',
|
||||
component: GamePage,
|
||||
props: true,
|
||||
meta: { requiresAuth: true },
|
||||
},
|
||||
{
|
||||
@@ -54,6 +60,9 @@ router.beforeEach(async (to, from, next) => {
|
||||
} else if (to.meta.requiresGuest && loggedIn) {
|
||||
next({
|
||||
name: 'game', // Or any default authenticated user route
|
||||
params: {
|
||||
questId: 'helloworld',
|
||||
},
|
||||
})
|
||||
} else {
|
||||
next() // Always call next() at the end
|
||||
|
||||
+28
-16
@@ -1,9 +1,12 @@
|
||||
import { reactive } from 'vue'
|
||||
import { useToast } from 'vue-toastification'
|
||||
import { FileGraph } from '@linux-odyssey/file-graph'
|
||||
import api from '../utils/api'
|
||||
import Socket from '../utils/socket'
|
||||
import SocketTerminal from '../utils/terminal'
|
||||
|
||||
const toast = useToast()
|
||||
|
||||
function newSession() {
|
||||
const graph = new FileGraph({
|
||||
path: '/',
|
||||
@@ -36,30 +39,39 @@ function setQuest(questId) {
|
||||
}
|
||||
|
||||
async function setSession(session) {
|
||||
store.session = session
|
||||
store.session.graph = new FileGraph(session.graph)
|
||||
term.reset()
|
||||
await socket.connect(session)
|
||||
term.focus()
|
||||
try {
|
||||
store.session = session
|
||||
store.session.graph = new FileGraph(session.graph)
|
||||
term.reset()
|
||||
await socket.connect(session)
|
||||
term.focus()
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
toast.error(err.message)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
export async function createSession() {
|
||||
console.log('Creating a new session...')
|
||||
const res = await api.post('/sessions', { quest_id: store.questId })
|
||||
const { data } = res
|
||||
await setSession(data)
|
||||
try {
|
||||
console.log('Creating a new session...')
|
||||
const res = await api.post('/sessions', { questId: store.questId })
|
||||
const { data } = res
|
||||
await setSession(data)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
toast.error('Failed to create a new session. Please try again later.')
|
||||
}
|
||||
}
|
||||
|
||||
async function getActiveSession(questId) {
|
||||
const res = await api.post('/sessions/active', {
|
||||
quest_id: questId,
|
||||
})
|
||||
const session = res.data
|
||||
try {
|
||||
await setSession(session)
|
||||
const res = await api.post('/sessions/active', { questId })
|
||||
await setSession(res.data)
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
await createSession().catch(console.error)
|
||||
console.error(err)
|
||||
toast.warning('Failed to connect previous session. Creating a new one...')
|
||||
await createSession()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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.on('connect_error', reject)
|
||||
this.bindListeners()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup>
|
||||
import { computed, onMounted } from 'vue'
|
||||
import { useToast } from 'vue-toastification'
|
||||
import sessionStore, { init } from '../store/session'
|
||||
import GameHeaderPart from '../components/GameHeaderPart.vue'
|
||||
import CommandlistPart from '../components/CommandlistPart.vue'
|
||||
@@ -14,8 +15,22 @@ const completed = computed(() => {
|
||||
return sessionStore.session.status === 'finished'
|
||||
})
|
||||
|
||||
const props = defineProps({
|
||||
questId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const toast = useToast()
|
||||
|
||||
onMounted(async () => {
|
||||
await init('helloworld')
|
||||
try {
|
||||
await init(props.questId)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
toast.error('Failed to initialize game session')
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
+2
-2
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ async function main() {
|
||||
},
|
||||
})
|
||||
|
||||
const additionalData = await handleCommand(CMD_NAME)
|
||||
const params = await handleCommand(CMD_NAME)
|
||||
|
||||
const output = await readOrNone(CMD_OUTPUT_FILE)
|
||||
const error = await readOrNone(CMD_ERROR_FILE)
|
||||
@@ -58,7 +58,7 @@ async function main() {
|
||||
error,
|
||||
exit_code: CMD_EXIT_CODE,
|
||||
pwd: PWD,
|
||||
...additionalData,
|
||||
params,
|
||||
}
|
||||
try {
|
||||
const res = await api.post('/commands', payload)
|
||||
@@ -77,7 +77,11 @@ async function main() {
|
||||
console.log(colorize('Quest completed!', 'blue'))
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
if (err.response) {
|
||||
console.error(err.response.data)
|
||||
return
|
||||
}
|
||||
console.error(err.message)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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,31 @@ const stageSchema = new Schema({
|
||||
const Quest = model(
|
||||
'Quest',
|
||||
new Schema({
|
||||
_id: String,
|
||||
title: String,
|
||||
order: Number,
|
||||
instruction: String,
|
||||
stages: [stageSchema],
|
||||
_id: {
|
||||
type: String,
|
||||
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,
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
+4
-20
@@ -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
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
title: Discover the World!
|
||||
instruction: |
|
||||
試著探索這個世界吧!
|
||||
|
||||
requirements:
|
||||
- spell
|
||||
@@ -0,0 +1,6 @@
|
||||
title: Read the File
|
||||
instruction: |
|
||||
閱讀檔案
|
||||
|
||||
requirements:
|
||||
- spell
|
||||
@@ -4,6 +4,9 @@ instruction: |
|
||||
|
||||
「輸入 echo Hello World!」
|
||||
|
||||
requirements:
|
||||
- helloworld
|
||||
|
||||
conditions:
|
||||
pwd_home: &pwd_home
|
||||
pwd:
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { matchedData } from 'express-validator'
|
||||
import { User } from '@linux-odyssey/models'
|
||||
import { genJWT, hashPassword } from '../../utils/auth.js'
|
||||
import { asyncHandler } from '../../middleware/error.js'
|
||||
|
||||
export async function issueToken(req, res) {
|
||||
export const issueToken = asyncHandler(async (req, res) => {
|
||||
const { user } = req
|
||||
const token = await genJWT({
|
||||
_id: user.id,
|
||||
@@ -10,34 +11,27 @@ export async function issueToken(req, res) {
|
||||
email: user.email,
|
||||
})
|
||||
res.json({ token })
|
||||
}
|
||||
})
|
||||
|
||||
export async function register(req, res, next) {
|
||||
export const register = asyncHandler(async (req, res, next) => {
|
||||
const { username, password, email } = matchedData(req)
|
||||
|
||||
const user = new User({
|
||||
username,
|
||||
email,
|
||||
})
|
||||
try {
|
||||
user.hashedPassword = await hashPassword(password)
|
||||
await user.save()
|
||||
req.login(user, (err) => {
|
||||
if (err) {
|
||||
next(err)
|
||||
return
|
||||
}
|
||||
res.status(201).json({
|
||||
message: 'user created',
|
||||
})
|
||||
user.hashedPassword = await hashPassword(password)
|
||||
await user.save()
|
||||
req.login(user, (err) => {
|
||||
if (err) {
|
||||
next(err)
|
||||
return
|
||||
}
|
||||
res.status(201).json({
|
||||
message: 'user created',
|
||||
})
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
res.status(500).json({
|
||||
message: 'error registering user',
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
export function logout(req, res) {
|
||||
req.logout((err) => {
|
||||
@@ -60,7 +54,7 @@ export function checkSession(req, res) {
|
||||
})
|
||||
}
|
||||
|
||||
export async function socialLogin(req, res) {
|
||||
export function socialLogin(req, res) {
|
||||
const { newUser } = req.user
|
||||
if (newUser) {
|
||||
req.session.newUser = newUser
|
||||
@@ -70,7 +64,7 @@ export async function socialLogin(req, res) {
|
||||
res.redirect('/')
|
||||
}
|
||||
|
||||
export async function registerFromSession(req, res) {
|
||||
export const registerFromSession = asyncHandler(async (req, res, next) => {
|
||||
const { username } = matchedData(req)
|
||||
const { newUser } = req.session
|
||||
|
||||
@@ -88,10 +82,7 @@ export async function registerFromSession(req, res) {
|
||||
|
||||
req.login(user, (err) => {
|
||||
if (err) {
|
||||
console.error(err)
|
||||
res.status(500).json({
|
||||
message: 'error logging in',
|
||||
})
|
||||
next(err)
|
||||
return
|
||||
}
|
||||
delete req.session.newUser
|
||||
@@ -99,4 +90,4 @@ export async function registerFromSession(req, res) {
|
||||
message: 'user created',
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -1,26 +1,26 @@
|
||||
import { Command, Session } from '@linux-odyssey/models'
|
||||
import { matchedData } from 'express-validator'
|
||||
import CommandHandler from '../../game/commandHandler.js'
|
||||
import { pushToSession } from '../socket.js'
|
||||
import { finishSession } from '../../game/sessionManager.js'
|
||||
import { asyncHandler } from '../../middleware/error.js'
|
||||
|
||||
const commandCompleteCallbacks = new Map()
|
||||
|
||||
export async function newCommand(req, res) {
|
||||
const { command, pwd, output, error, ...additionalData } = req.body
|
||||
|
||||
if (!command) {
|
||||
res.status(400).json({ message: 'command is required' })
|
||||
return
|
||||
}
|
||||
|
||||
const sessionId = req.user.session_id
|
||||
export const newCommand = asyncHandler(async (req, res) => {
|
||||
const { command, pwd, output, error, params } = matchedData(req)
|
||||
|
||||
const { sessionId } = req.user
|
||||
const session = await Session.findById(sessionId)
|
||||
|
||||
if (!session) {
|
||||
res.status(404).json({ message: 'session not found' })
|
||||
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
|
||||
@@ -38,23 +38,25 @@ export async function newCommand(req, res) {
|
||||
|
||||
await c.save()
|
||||
|
||||
const commandHandler = new CommandHandler(session, c, additionalData)
|
||||
|
||||
const commandHandler = new CommandHandler(session, c, params)
|
||||
const response = await commandHandler.run()
|
||||
|
||||
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')
|
||||
}
|
||||
|
||||
res.status(201).json(response)
|
||||
await session.save()
|
||||
}
|
||||
})
|
||||
|
||||
export async function completedCommand(req, res) {
|
||||
const sessionId = req.user.session_id
|
||||
export const completedCommand = asyncHandler(async (req, res) => {
|
||||
const { sessionId } = req.user
|
||||
|
||||
const session = await Session.findById(sessionId)
|
||||
|
||||
@@ -76,4 +78,4 @@ export async function completedCommand(req, res) {
|
||||
callback()
|
||||
commandCompleteCallbacks.delete(session.id)
|
||||
res.json({ message: 'ok' })
|
||||
}
|
||||
})
|
||||
@@ -1,23 +1,25 @@
|
||||
import { Quest } from '@linux-odyssey/models'
|
||||
import { matchedData } from 'express-validator'
|
||||
import { asyncHandler } from '../../middleware/error.js'
|
||||
|
||||
export async function getQuests(req, res) {
|
||||
const quests = await Quest.find({}).sort({ order: 1 })
|
||||
export const getQuests = asyncHandler(async (req, res) => {
|
||||
const quests = await Quest.find({})
|
||||
res.json(
|
||||
quests.map(({ title, order, _id }) => ({
|
||||
quests.map(({ title, _id, requirements }) => ({
|
||||
_id,
|
||||
title,
|
||||
order,
|
||||
requirements,
|
||||
}))
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
export async function getQuestDetail(req, res) {
|
||||
const { id } = req.params
|
||||
const quest = await Quest.findById(id)
|
||||
export const getQuestDetail = asyncHandler(async (req, res) => {
|
||||
const { questId } = matchedData(req)
|
||||
const quest = await Quest.findById(questId)
|
||||
if (!quest) {
|
||||
res.status(404).json({ message: 'Quest not found.' })
|
||||
return
|
||||
}
|
||||
const { _id, title, order, instruction } = quest
|
||||
res.json({ _id, title, order, instruction })
|
||||
}
|
||||
})
|
||||
@@ -3,7 +3,9 @@ import { Session } from '@linux-odyssey/models'
|
||||
import {
|
||||
createNewSession,
|
||||
getOrCreateActiveSession,
|
||||
isQuestUnlocked,
|
||||
} from '../../game/sessionManager.js'
|
||||
import { asyncHandler } from '../../middleware/error.js'
|
||||
|
||||
function sessionSummary(session) {
|
||||
return {
|
||||
@@ -25,75 +27,48 @@ 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
|
||||
}
|
||||
query.status = req.query.status || 'active'
|
||||
const sessions = await Session.find(query)
|
||||
res.json(sessions.map(sessionSummary))
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
res.status(500).json({ message: err.message })
|
||||
export const getSessionList = asyncHandler(async (req, res) => {
|
||||
const query = { user: req.user._id }
|
||||
const { questId, status } = matchedData(req)
|
||||
if (questId) {
|
||||
query.quest = questId
|
||||
}
|
||||
}
|
||||
query.status = status || 'active'
|
||||
const sessions = await Session.find(query)
|
||||
res.json(sessions.map(sessionSummary))
|
||||
})
|
||||
|
||||
export async function createSession(req, res) {
|
||||
const { quest_id } = matchedData(req)
|
||||
try {
|
||||
const session = await createNewSession(req.user, quest_id)
|
||||
export const createSession = asyncHandler(async (req, res) => {
|
||||
const { questId } = matchedData(req)
|
||||
if (await isQuestUnlocked(req.user, questId)) {
|
||||
const session = await createNewSession(req.user, questId)
|
||||
res.status(201).json(sessionDetail(session))
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
res.status(500).json({ message: err.message })
|
||||
} else {
|
||||
res.status(403).json({ message: 'Quest is locked.' })
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
export async function getSessionById(req, res) {
|
||||
try {
|
||||
const session = await Session.findOne({
|
||||
_id: req.params.id,
|
||||
user: req.user._id,
|
||||
})
|
||||
export const getSessionById = asyncHandler(async (req, res) => {
|
||||
const { sessionId } = matchedData(req)
|
||||
const session = await Session.findOne({
|
||||
_id: sessionId,
|
||||
user: req.user._id,
|
||||
})
|
||||
|
||||
if (!session) {
|
||||
res.status(404).json({ message: 'Session not found.' })
|
||||
return
|
||||
}
|
||||
if (!session) {
|
||||
res.status(404).json({ message: 'Session not found.' })
|
||||
return
|
||||
}
|
||||
|
||||
res.json(sessionDetail(session))
|
||||
})
|
||||
|
||||
export const getOrCreateSession = asyncHandler(async (req, res) => {
|
||||
const { questId } = matchedData(req)
|
||||
if (await isQuestUnlocked(req.user, questId)) {
|
||||
const session = await getOrCreateActiveSession(req.user, questId)
|
||||
res.json(sessionDetail(session))
|
||||
} catch (err) {
|
||||
res.status(500).json({ message: err.message })
|
||||
} else {
|
||||
res.status(403).json({ message: 'Quest is locked.' })
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
try {
|
||||
const session = await getOrCreateActiveSession(req.user, quest_id)
|
||||
res.json(sessionDetail(session))
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
res.status(500).json({ message: err.message })
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,20 @@
|
||||
import { UserProfile } from '@linux-odyssey/models'
|
||||
import { asyncHandler } from '../../middleware/error.js'
|
||||
|
||||
export const me = asyncHandler(async (req, res) => {
|
||||
const userProfile = await UserProfile.findOne({
|
||||
user: req.user._id,
|
||||
})
|
||||
if (!userProfile) {
|
||||
throw new Error(
|
||||
`UserProfile not found for user ${req.user.username} ${req.user._id}!`
|
||||
)
|
||||
}
|
||||
const { username, email } = req.user
|
||||
const { progress } = userProfile
|
||||
res.json({
|
||||
username,
|
||||
email,
|
||||
progress,
|
||||
})
|
||||
})
|
||||
@@ -32,7 +32,7 @@ router.post(
|
||||
|
||||
router.post('/register', authenticateRateLimit, registerValidators, register)
|
||||
|
||||
router.post('/logout', authenticateRateLimit, logout)
|
||||
router.post('/logout', logout)
|
||||
|
||||
router.get('/check-username', checkUsernameValidators, (req, res) =>
|
||||
res.json({ available: true })
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Router } from 'express'
|
||||
import * as commandController from '../controllers/commandController.js'
|
||||
import { newCommandValidator } from '../validators/commandValidator.js'
|
||||
|
||||
const router = Router()
|
||||
|
||||
router.post('/', commandController.newCommand)
|
||||
router.post('/', newCommandValidator, commandController.newCommand)
|
||||
router.post('/completed', commandController.completedCommand)
|
||||
|
||||
export default router
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,9 @@
|
||||
import express from 'express'
|
||||
import { me } from '../controllers/userController.js'
|
||||
import authRequired from '../../middleware/authRequired.js'
|
||||
|
||||
const router = express.Router()
|
||||
|
||||
router.get('/me', authRequired, me)
|
||||
|
||||
export default router
|
||||
+28
-25
@@ -1,4 +1,5 @@
|
||||
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'
|
||||
@@ -26,46 +27,46 @@ export function pushToSession(sessionId, event, ...args) {
|
||||
}
|
||||
}
|
||||
|
||||
async function connectClient(socket) {
|
||||
async function connectContainer(socket, next) {
|
||||
const user = socket.request.session?.passport?.user
|
||||
// console.log(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()
|
||||
// console.log(socket.handshake)
|
||||
const { sessionId } = socket.handshake.query
|
||||
if (!(sessionId && validator.isMongoId(sessionId))) {
|
||||
console.warn('Invalid Session ID.', 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
|
||||
let stream
|
||||
try {
|
||||
container = await getAndStartContainer(session.containerId)
|
||||
stream = await attachContainer(container, { token })
|
||||
const container = await getAndStartContainer(session.containerId)
|
||||
const stream = await attachContainer(container, { token })
|
||||
socket.context = {
|
||||
session,
|
||||
stream,
|
||||
}
|
||||
next()
|
||||
} catch (err) {
|
||||
socket.emit('error', 'Failed to start container.')
|
||||
socket.disconnect()
|
||||
return
|
||||
next(new Error('Failed to start container.'))
|
||||
}
|
||||
}
|
||||
|
||||
function onConnect(socket) {
|
||||
const { session, stream } = socket.context
|
||||
stream.socket.on('data', (chunk) => {
|
||||
socket.emit('terminal', chunk.toString())
|
||||
})
|
||||
@@ -87,14 +88,16 @@ async function connectClient(socket) {
|
||||
removeFromSession(session.id, socketCallback)
|
||||
stream.destroy()
|
||||
})
|
||||
|
||||
socket.send(`${container.id}\n`)
|
||||
}
|
||||
|
||||
export default (server) => {
|
||||
const io = new Server(server)
|
||||
|
||||
io.engine.use(SessionMiddleware)
|
||||
io.use(connectContainer)
|
||||
|
||||
io.on('connection', connectClient)
|
||||
io.on('connection', onConnect)
|
||||
io.on('connect_error', (err) => {
|
||||
console.error(err)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { body } from 'express-validator'
|
||||
import { noError } from '../../middleware/validator.js'
|
||||
|
||||
const checkCommand = () => body('command').isString().notEmpty().trim()
|
||||
const checkPwd = () => body('pwd').isString().notEmpty().trim()
|
||||
const checkOutput = () => body('output').trim().optional()
|
||||
const checkError = () => body('error').trim().optional()
|
||||
const checkParams = () => body('params').isObject().optional()
|
||||
|
||||
export const newCommandValidator = [
|
||||
checkCommand(),
|
||||
checkPwd(),
|
||||
checkOutput(),
|
||||
checkError(),
|
||||
checkParams(),
|
||||
noError,
|
||||
]
|
||||
@@ -0,0 +1,7 @@
|
||||
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]
|
||||
@@ -1,8 +1,14 @@
|
||||
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().isIn(['active', 'inactive', 'finished'])
|
||||
|
||||
export const checkSessionId = () =>
|
||||
check('sessionId').notEmpty().isString().isMongoId()
|
||||
|
||||
export const sessionListValidator = [checkQuestId(), checkSessionStatus()]
|
||||
export const sessionDetailValidator = [checkSessionId(), noError]
|
||||
export const createSessionValidator = [checkQuestId(), noError]
|
||||
export const activeSessionValidator = [checkQuestId(), noError]
|
||||
@@ -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,
|
||||
|
||||
@@ -15,16 +15,16 @@ const checkMatch = (pattern, input) => {
|
||||
}
|
||||
|
||||
export default class CommandHandler extends SessionHandler {
|
||||
constructor(session, commandInput, additionalData) {
|
||||
constructor(session, commandInput, params) {
|
||||
super(session)
|
||||
this.commandInput = commandInput
|
||||
this.argv = minimist(this.commandInput.command.split(' '))
|
||||
|
||||
this.additionalData = additionalData
|
||||
this.params = params
|
||||
}
|
||||
|
||||
handleEvent() {
|
||||
if (this.additionalData.discover) this.discoverHandler()
|
||||
if (this.params.discover) this.discoverHandler()
|
||||
}
|
||||
|
||||
handleCommand() {
|
||||
@@ -39,7 +39,7 @@ export default class CommandHandler extends SessionHandler {
|
||||
case 'ls':
|
||||
pushToSession(this.session.id, 'graph', {
|
||||
pwd: this.commandInput.pwd,
|
||||
discover: this.additionalData.discover,
|
||||
discover: this.params.discover,
|
||||
})
|
||||
break
|
||||
|
||||
@@ -50,7 +50,7 @@ export default class CommandHandler extends SessionHandler {
|
||||
|
||||
async discoverHandler() {
|
||||
const graph = new FileGraph(this.session.graph)
|
||||
graph.discover(this.additionalData.discover)
|
||||
graph.discover(this.params.discover)
|
||||
this.session.graph = graph
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -62,3 +79,38 @@ 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()
|
||||
}
|
||||
|
||||
export async function isQuestUnlocked(user, questId) {
|
||||
const userProfile = await UserProfile.findOne({ user: user._id })
|
||||
if (!userProfile) {
|
||||
throw new Error(`UserProfile ${user._id} not found`)
|
||||
}
|
||||
const quest = await Quest.findById(questId)
|
||||
if (!quest) {
|
||||
throw new Error(`Quest ${questId} not found`)
|
||||
}
|
||||
return quest.requirements.every((requiredQuestId) => {
|
||||
const progress = userProfile.progress.get(requiredQuestId)
|
||||
return progress && progress.completed
|
||||
})
|
||||
}
|
||||
@@ -1,3 +1,9 @@
|
||||
export function asyncHandler(fn) {
|
||||
return (req, res, next) => {
|
||||
fn(req, res, next).catch(next)
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
export default function errorHandler(err, req, res, next) {
|
||||
console.error(err.stack) // Log error stack trace to the console
|
||||
|
||||
@@ -31,7 +31,7 @@ export default async function loadAndUpdateQuests() {
|
||||
return Quest.findByIdAndUpdate(
|
||||
id,
|
||||
{
|
||||
id,
|
||||
_id: id,
|
||||
...quest,
|
||||
},
|
||||
{
|
||||
|
||||
+75
-52
@@ -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:
|
||||
@@ -79,6 +79,8 @@ paths:
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
'403':
|
||||
description: This quest is locked.
|
||||
|
||||
/sessions/active:
|
||||
post:
|
||||
@@ -93,9 +95,9 @@ paths:
|
||||
schema:
|
||||
type: 'object'
|
||||
required:
|
||||
- quest_id
|
||||
- questId
|
||||
properties:
|
||||
quest_id:
|
||||
questId:
|
||||
type: 'string'
|
||||
description: 'The ID of the quest.'
|
||||
example: 'helloworld'
|
||||
@@ -108,10 +110,12 @@ paths:
|
||||
$ref: '#/components/schemas/Session'
|
||||
400:
|
||||
description: 'Bad Request. The request is invalid or malformed.'
|
||||
403:
|
||||
description: This quest is locked.
|
||||
500:
|
||||
description: 'Internal Server Error.'
|
||||
|
||||
/sessions/{session_id}:
|
||||
/sessions/{sessionId}:
|
||||
get:
|
||||
tags:
|
||||
- signed-in
|
||||
@@ -120,7 +124,7 @@ paths:
|
||||
- cookieAuth: []
|
||||
parameters:
|
||||
- in: path
|
||||
name: session_id
|
||||
name: sessionId
|
||||
description: ID of the session to retrieve
|
||||
required: true
|
||||
schema:
|
||||
@@ -143,44 +147,6 @@ paths:
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
delete:
|
||||
tags:
|
||||
- signed-in
|
||||
summary: delete a specific session by ID
|
||||
security:
|
||||
- cookieAuth: []
|
||||
parameters:
|
||||
- in: path
|
||||
name: session_id
|
||||
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:
|
||||
@@ -468,12 +434,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 +500,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 +532,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 +610,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 +644,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
|
||||
|
||||
|
||||
@@ -6515,6 +6515,11 @@ vue-router@^4.2.4:
|
||||
dependencies:
|
||||
"@vue/devtools-api" "^6.5.0"
|
||||
|
||||
vue-toastification@^2.0.0-rc.5:
|
||||
version "2.0.0-rc.5"
|
||||
resolved "https://registry.yarnpkg.com/vue-toastification/-/vue-toastification-2.0.0-rc.5.tgz#92798604d806ae473cfb76ed776fae294280f8f8"
|
||||
integrity sha512-q73e5jy6gucEO/U+P48hqX+/qyXDozAGmaGgLFm5tXX4wJBcVsnGp4e/iJqlm9xzHETYOilUuwOUje2Qg1JdwA==
|
||||
|
||||
vue@^3.3.4:
|
||||
version "3.3.4"
|
||||
resolved "https://registry.yarnpkg.com/vue/-/vue-3.3.4.tgz#8ed945d3873667df1d0fcf3b2463ada028f88bd6"
|
||||
|
||||
Reference in new issue
Block a user