mirror of
https://github.com/linux-odyssey/linux-odyssey.git
synced 2026-11-08 03:20:12 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
99871fa299 | ||
|
|
248271355f | ||
|
|
988cb65b51 | ||
|
|
4324932fc9 | ||
|
|
e9b49502ab | ||
|
|
d5519e7157 | ||
|
|
ed596c245d | ||
|
|
57c080d88b | ||
|
|
d795dbd788 | ||
|
|
218d433c14 | ||
|
|
48835f8830 | ||
|
|
dcd8f441f8 | ||
|
|
94ef3749f5 | ||
|
|
3398d18055 | ||
|
|
c64043316e | ||
|
|
7cdc542c19 | ||
|
|
78b9529699 | ||
|
|
8a31a58c56 | ||
|
|
103ce4774c | ||
|
|
4e6602d82f | ||
|
|
2eaf3c8f24 | ||
|
|
15465feda9 | ||
|
|
ea2ba2c7a0 | ||
|
|
0b136a830a | ||
|
|
3848ca6941 | ||
|
|
443e810471 | ||
|
|
198dcae3f8 | ||
|
|
7347c37a9e | ||
|
|
1d725d82ae | ||
|
|
b5c601f4a9 |
No files matched your search
@@ -35,23 +35,3 @@ jobs:
|
|||||||
|
|
||||||
- name: Test
|
- name: Test
|
||||||
run: yarn test
|
run: yarn test
|
||||||
|
|
||||||
e2e:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout Code
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Build Test Environment, Stage 1
|
|
||||||
run: docker-compose -f docker-compose.testing.yml build base backend app-test cypress
|
|
||||||
|
|
||||||
- name: Build Test Quests
|
|
||||||
run: docker-compose -f docker-compose.testing.yml run --rm backend yarn build:quests
|
|
||||||
|
|
||||||
- name: Run Cypress Tests
|
|
||||||
env:
|
|
||||||
GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID }}
|
|
||||||
GOOGLE_CLIENT_SECRET: ${{ secrets.GOOGLE_CLIENT_SECRET }}
|
|
||||||
GITHUB_CLIENT_ID: ${{ secrets.GH_CLIENT_ID }}
|
|
||||||
GITHUB_CLIENT_SECRET: ${{ secrets.GH_CLIENT_SECRET }}
|
|
||||||
run: docker-compose -f docker-compose.testing.yml run cypress
|
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
name: E2E Tests
|
||||||
|
|
||||||
|
on: [push, pull_request]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
e2e:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout Code
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Build Test Environment, Stage 1
|
||||||
|
run: docker-compose -f docker-compose.testing.yml build base backend app-test cypress
|
||||||
|
|
||||||
|
- name: Build Test Quests
|
||||||
|
run: docker-compose -f docker-compose.testing.yml run --rm backend yarn build:quests
|
||||||
|
|
||||||
|
- name: Run Cypress Tests
|
||||||
|
env:
|
||||||
|
GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID }}
|
||||||
|
GOOGLE_CLIENT_SECRET: ${{ secrets.GOOGLE_CLIENT_SECRET }}
|
||||||
|
GITHUB_CLIENT_ID: ${{ secrets.GH_CLIENT_ID }}
|
||||||
|
GITHUB_CLIENT_SECRET: ${{ secrets.GH_CLIENT_SECRET }}
|
||||||
|
run: docker-compose -f docker-compose.testing.yml run cypress
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
|
|
||||||
Run mongoDB first:
|
Run mongoDB first:
|
||||||
|
|
||||||
docker compose -f docker-compose.dev.yml up -d db
|
docker compose up -d
|
||||||
|
|
||||||
The db will automatically restarts by Docker.
|
The db will automatically restarts by Docker.
|
||||||
|
|
||||||
|
|||||||
+3
-1
@@ -6,6 +6,7 @@
|
|||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"dev:docker": "API_TARGET=http://backend:3000 vite",
|
"dev:docker": "API_TARGET=http://backend:3000 vite",
|
||||||
"build": "vue-tsc -b && vite build",
|
"build": "vue-tsc -b && vite build",
|
||||||
|
"type-check": "vue-tsc -b",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -16,13 +17,14 @@
|
|||||||
"@fortawesome/vue-fontawesome": "^3.0.3",
|
"@fortawesome/vue-fontawesome": "^3.0.3",
|
||||||
"@linux-odyssey/constants": "*",
|
"@linux-odyssey/constants": "*",
|
||||||
"@linux-odyssey/file-graph": "*",
|
"@linux-odyssey/file-graph": "*",
|
||||||
"@linux-odyssey/utils": "*",
|
|
||||||
"@linux-odyssey/models": "*",
|
"@linux-odyssey/models": "*",
|
||||||
|
"@linux-odyssey/utils": "*",
|
||||||
"@types/dompurify": "^3.0.5",
|
"@types/dompurify": "^3.0.5",
|
||||||
"axios": "^1.6.0",
|
"axios": "^1.6.0",
|
||||||
"dompurify": "^3.0.6",
|
"dompurify": "^3.0.6",
|
||||||
"echarts": "^5.4.3",
|
"echarts": "^5.4.3",
|
||||||
"marked": "^10.0.0",
|
"marked": "^10.0.0",
|
||||||
|
"pinia": "^2.1.7",
|
||||||
"socket.io-client": "^4.7.1",
|
"socket.io-client": "^4.7.1",
|
||||||
"validator": "^13.11.0",
|
"validator": "^13.11.0",
|
||||||
"vue": "^3.3.4",
|
"vue": "^3.3.4",
|
||||||
|
|||||||
@@ -83,6 +83,16 @@ const handleChange = () => {
|
|||||||
errorMessage.value = 'Something went wrong'
|
errorMessage.value = 'Something went wrong'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const registerGuest = async () => {
|
||||||
|
try {
|
||||||
|
await api.post('/auth/register-guest')
|
||||||
|
router.push({ name: 'map' })
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
toast.error('Something went wrong.')
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<!-- login page -->
|
<!-- login page -->
|
||||||
@@ -219,6 +229,13 @@ const handleChange = () => {
|
|||||||
</RouterLink></span
|
</RouterLink></span
|
||||||
>
|
>
|
||||||
</p>
|
</p>
|
||||||
|
<button
|
||||||
|
id="register-guest"
|
||||||
|
class="rounded-lg py-2 mt-3 bg-bg-primary text-text-secondary w-full border-text-secondary border-2"
|
||||||
|
@click="registerGuest()"
|
||||||
|
>
|
||||||
|
以訪客身分繼續
|
||||||
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -1,142 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { defineProps, onMounted } from 'vue'
|
|
||||||
import { useRouter } from 'vue-router'
|
|
||||||
import { useToast } from 'vue-toastification'
|
|
||||||
import { logout } from '../utils/auth'
|
|
||||||
import { bugReportUrl, surveyUrl } from '../config'
|
|
||||||
import { reset } from '../store/session'
|
|
||||||
import userProfileStore, {
|
|
||||||
loadUserProfile,
|
|
||||||
resetUserProfile,
|
|
||||||
} from '../store/userProfile'
|
|
||||||
|
|
||||||
const router = useRouter()
|
|
||||||
|
|
||||||
const toast = useToast()
|
|
||||||
|
|
||||||
const handleLogout = async () => {
|
|
||||||
try {
|
|
||||||
await logout()
|
|
||||||
resetUserProfile()
|
|
||||||
reset()
|
|
||||||
router.push({ name: 'login' })
|
|
||||||
} catch (err) {
|
|
||||||
toast.error('登出失敗')
|
|
||||||
console.error(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
defineProps({
|
|
||||||
title: {
|
|
||||||
type: String,
|
|
||||||
required: false,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
try {
|
|
||||||
await loadUserProfile()
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err)
|
|
||||||
toast.error('無法讀取使用者資料')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div id="header" class="bg-bg-secondary content-center h-full w-full p-1">
|
|
||||||
<div class="h-full flex items-center justify-center mx-3">
|
|
||||||
<div class="flex items-center gap-3">
|
|
||||||
<!-- <button id="stream" class="flex items-center h-5 w-5">
|
|
||||||
<font-awesome-icon
|
|
||||||
:icon="['fas', 'bars']"
|
|
||||||
class="text-text-primary h-full w-full"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
<button id="backward" class="flex items-center h-5 w-5">
|
|
||||||
<font-awesome-icon
|
|
||||||
:icon="['fas', 'arrow-left']"
|
|
||||||
class="text-border h-full w-full"
|
|
||||||
/>
|
|
||||||
</button> -->
|
|
||||||
<img src="../img/icon_totem.svg" class="h-5" />
|
|
||||||
<button
|
|
||||||
id="GameTitle"
|
|
||||||
class="text-text-primary inline-block font-bold whitespace-nowrap"
|
|
||||||
style="font-size: 2vh"
|
|
||||||
>
|
|
||||||
Linux Odyssey
|
|
||||||
</button>
|
|
||||||
<p
|
|
||||||
id="QuestTitle"
|
|
||||||
class="text-text inline-block font-bold whitespace-nowrap"
|
|
||||||
style="font-size: 2vh"
|
|
||||||
>
|
|
||||||
{{ 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">
|
|
||||||
<font-awesome-icon
|
|
||||||
:icon="['fas', 'triangle-exclamation']"
|
|
||||||
class="text-text-primary h-full w-full"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
<button id="setting" class="flex items-center h-5 w-5">
|
|
||||||
<font-awesome-icon
|
|
||||||
:icon="['fas', 'gear']"
|
|
||||||
class="text-text-primary h-full w-full"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
<button id="fullscreen" class="flex items-center h-5 w-5">
|
|
||||||
<font-awesome-icon
|
|
||||||
:icon="['fas', 'expand']"
|
|
||||||
class="text-text-primary h-full w-full"
|
|
||||||
/>
|
|
||||||
</button> -->
|
|
||||||
<p
|
|
||||||
id="UsernameText"
|
|
||||||
class="text-text inline-block whitespace-nowrap"
|
|
||||||
style="font-size: 2vh"
|
|
||||||
>
|
|
||||||
{{ userProfileStore.username }}
|
|
||||||
</p>
|
|
||||||
<a title="Survey" :href="surveyUrl" target="_blank" class="h-5 w-5">
|
|
||||||
<font-awesome-icon
|
|
||||||
:icon="['fas', 'file-invoice']"
|
|
||||||
class="text-text-primary h-full w-full"
|
|
||||||
/>
|
|
||||||
</a>
|
|
||||||
<RouterLink title="LeaderBoard" to="/leaderboard" class="h-5 w-5">
|
|
||||||
<font-awesome-icon
|
|
||||||
:icon="['fas', 'trophy']"
|
|
||||||
class="text-text-primary h-full w-full"
|
|
||||||
/>
|
|
||||||
</RouterLink>
|
|
||||||
<RouterLink title="Map" to="/map" class="h-5 w-5">
|
|
||||||
<font-awesome-icon
|
|
||||||
:icon="['fas', 'map']"
|
|
||||||
class="text-text-primary h-full w-full"
|
|
||||||
/>
|
|
||||||
</RouterLink>
|
|
||||||
<a
|
|
||||||
title="Bug Report"
|
|
||||||
:href="bugReportUrl"
|
|
||||||
target="_blank"
|
|
||||||
class="h-5 w-5"
|
|
||||||
>
|
|
||||||
<font-awesome-icon
|
|
||||||
:icon="['fas', 'bug']"
|
|
||||||
class="text-text-primary h-full w-full"
|
|
||||||
/>
|
|
||||||
</a>
|
|
||||||
<button title="Sign Out" @click="handleLogout" class="h-5 w-5">
|
|
||||||
<font-awesome-icon
|
|
||||||
:icon="['fas', 'arrow-right-from-bracket']"
|
|
||||||
class="text-text-primary h-full w-full"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@@ -14,7 +14,9 @@ import {
|
|||||||
import { DAG } from '@linux-odyssey/utils'
|
import { DAG } from '@linux-odyssey/utils'
|
||||||
import api from '../utils/api'
|
import api from '../utils/api'
|
||||||
import { NodeImage } from '../img/svg.js'
|
import { NodeImage } from '../img/svg.js'
|
||||||
|
import useUserProfile from '../store/userProfile'
|
||||||
|
|
||||||
|
const store = useUserProfile()
|
||||||
const marginX = 500
|
const marginX = 500
|
||||||
const marginY = 300
|
const marginY = 300
|
||||||
const chartContainer = ref(null)
|
const chartContainer = ref(null)
|
||||||
@@ -40,16 +42,6 @@ async function getQuests() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getProgress() {
|
|
||||||
try {
|
|
||||||
const res = await api.get('/users/me')
|
|
||||||
return res.data.progress
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err)
|
|
||||||
throw err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const genOption = (nodes: any, edges: any) => ({
|
const genOption = (nodes: any, edges: any) => ({
|
||||||
renderer: 'svg',
|
renderer: 'svg',
|
||||||
tooltip: {},
|
tooltip: {},
|
||||||
@@ -162,9 +154,9 @@ function initChart(option: any) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
await store.loadUserProfile()
|
||||||
const quests = await getQuests()
|
const quests = await getQuests()
|
||||||
const progress = await getProgress()
|
const option = getOption(quests, store.progress)
|
||||||
const option = getOption(quests, progress)
|
|
||||||
initChart(option)
|
initChart(option)
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { useToast } from 'vue-toastification'
|
import { useToast } from 'vue-toastification'
|
||||||
import { createSession } from '../../store/session'
|
import useSession from '../../store/session'
|
||||||
import { surveyUrl } from '../../config'
|
import { surveyUrl } from '../../config'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const sessionStore = useSession()
|
||||||
const reset = async () => {
|
const reset = async () => {
|
||||||
console.log('resetting...')
|
console.log('resetting...')
|
||||||
await createSession()
|
await sessionStore.createSession()
|
||||||
}
|
}
|
||||||
const continuePlay = async () => {
|
const continuePlay = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -29,7 +30,7 @@ const continuePlay = async () => {
|
|||||||
:icon="['fas', 'file-invoice']"
|
:icon="['fas', 'file-invoice']"
|
||||||
class="text-text-primary px-1"
|
class="text-text-primary px-1"
|
||||||
/>
|
/>
|
||||||
<p class="text-text-primary inline ml-1 hidden md:flex">問卷</p>
|
<p class="text-text-primary inline ml-1 md:flex">問卷</p>
|
||||||
</a>
|
</a>
|
||||||
</button>
|
</button>
|
||||||
<button disabled id="solution" class="bg-bg-disabled rounded-lg p-2">
|
<button disabled id="solution" class="bg-bg-disabled rounded-lg p-2">
|
||||||
@@ -37,7 +38,7 @@ const continuePlay = async () => {
|
|||||||
:icon="['far', 'circle-question']"
|
:icon="['far', 'circle-question']"
|
||||||
class="text-text-disabled px-1"
|
class="text-text-disabled px-1"
|
||||||
/>
|
/>
|
||||||
<p class="text-text-disabled inline ml-1 hidden md:flex">解答</p>
|
<p class="text-text-disabled inline ml-1 md:flex">解答</p>
|
||||||
</button>
|
</button>
|
||||||
<button id="reset" class="bg-bg-secondary rounded-lg p-2" @click="reset">
|
<button id="reset" class="bg-bg-secondary rounded-lg p-2" @click="reset">
|
||||||
<font-awesome-icon
|
<font-awesome-icon
|
||||||
|
|||||||
@@ -1,16 +1,21 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, watch } from 'vue'
|
import { computed, ref, watch } from 'vue'
|
||||||
import sessionStore from '../../store/session'
|
import useSession from '../../store/session'
|
||||||
import MarkdownText from '../MarkdownText.vue'
|
import MarkdownText from '../MarkdownText.vue'
|
||||||
|
|
||||||
|
const sessionStore = useSession()
|
||||||
const current = ref(-1)
|
const current = ref(-1)
|
||||||
watch(
|
const length = computed(() => {
|
||||||
() => sessionStore.session.hints,
|
return sessionStore.session?.responses.length ?? 0
|
||||||
(hints) => {
|
})
|
||||||
current.value = hints.length - 1
|
|
||||||
},
|
const disabled = computed(() => {
|
||||||
{ deep: true }
|
return current.value === length.value - 1
|
||||||
)
|
})
|
||||||
|
|
||||||
|
watch(length, () => {
|
||||||
|
current.value = length.value - 1
|
||||||
|
})
|
||||||
|
|
||||||
const left = () => {
|
const left = () => {
|
||||||
if (current.value > 0) {
|
if (current.value > 0) {
|
||||||
@@ -19,7 +24,7 @@ const left = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const right = () => {
|
const right = () => {
|
||||||
if (current.value < sessionStore.session.hints.length - 1) {
|
if (current.value < length.value - 1) {
|
||||||
current.value += 1
|
current.value += 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -32,7 +37,7 @@ const right = () => {
|
|||||||
:icon="['far', 'lightbulb']"
|
:icon="['far', 'lightbulb']"
|
||||||
class="text-yellow-200 p-2 content-center"
|
class="text-yellow-200 p-2 content-center"
|
||||||
/>
|
/>
|
||||||
<h1 class="inline text-text w-1/2 font-xl p-2 m-1">提示</h1>
|
<h1 class="inline text-text w-1/2 font-xl p-2 m-1">提示 {{ length }}</h1>
|
||||||
<div v-if="current !== -1" class="flex w-full justify-end items-end">
|
<div v-if="current !== -1" class="flex w-full justify-end items-end">
|
||||||
<button
|
<button
|
||||||
class="p-2 m-1 w-1/8"
|
class="p-2 m-1 w-1/8"
|
||||||
@@ -49,13 +54,13 @@ const right = () => {
|
|||||||
<p class="inline text-text font-xl w-1/14 p-2 m-1">{{ current + 1 }}</p>
|
<p class="inline text-text font-xl w-1/14 p-2 m-1">{{ current + 1 }}</p>
|
||||||
<p class="inline text-text font-xl w-1/14 p-2 m-1">/</p>
|
<p class="inline text-text font-xl w-1/14 p-2 m-1">/</p>
|
||||||
<p class="inline text-text font-xl w-1/14 p-2 m-1">
|
<p class="inline text-text font-xl w-1/14 p-2 m-1">
|
||||||
{{ sessionStore.session.hints.length }}
|
{{ length }}
|
||||||
</p>
|
</p>
|
||||||
<button
|
<button
|
||||||
class="p-2 m-1 w-1/8"
|
class="p-2 m-1 w-1/8"
|
||||||
id="Rbutton"
|
id="Rbutton"
|
||||||
@click="right"
|
@click="right"
|
||||||
:disabled="current === sessionStore.session.hints.length - 1"
|
:disabled="disabled"
|
||||||
>
|
>
|
||||||
<font-awesome-icon :icon="['fas', 'arrow-right']" class="text-text" />
|
<font-awesome-icon :icon="['fas', 'arrow-right']" class="text-text" />
|
||||||
</button>
|
</button>
|
||||||
@@ -64,7 +69,7 @@ const right = () => {
|
|||||||
<div id="hint" class="bg-bg p-8 overflow-y-auto">
|
<div id="hint" class="bg-bg p-8 overflow-y-auto">
|
||||||
<ul>
|
<ul>
|
||||||
<li
|
<li
|
||||||
v-for="response in sessionStore.session.responses[current]"
|
v-for="response in sessionStore.session?.responses[current]"
|
||||||
:key="response.type"
|
:key="response.type"
|
||||||
class="text-text font-xl whitespace-pre-wrap"
|
class="text-text font-xl whitespace-pre-wrap"
|
||||||
>
|
>
|
||||||
@@ -73,7 +78,7 @@ const right = () => {
|
|||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
<li
|
<li
|
||||||
v-for="hint in sessionStore.session.hints[current]"
|
v-for="hint in sessionStore.session?.hints[current]"
|
||||||
:key="hint"
|
:key="hint"
|
||||||
class="text-text-primary font-xl whitespace-pre-wrap"
|
class="text-text-primary font-xl whitespace-pre-wrap"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import MarkdownText from '../MarkdownText.vue'
|
import MarkdownText from '../MarkdownText.vue'
|
||||||
import sessionStore from '../../store/session'
|
import useSession from '../../store/session'
|
||||||
import { Task } from '../../types'
|
import { Task } from '../../types'
|
||||||
|
|
||||||
|
const sessionStore = useSession()
|
||||||
const tasks = computed(() => {
|
const tasks = computed(() => {
|
||||||
return sessionStore.session.tasks.map((task: Task) => {
|
return sessionStore.session?.tasks.map((task: Task) => {
|
||||||
const prefix = task.completed ? '✓' : '➤'
|
const prefix = task.completed ? '✓' : '➤'
|
||||||
const color = task.completed ? 'text-text-primary' : 'text-text'
|
const color = task.completed ? 'text-text-primary' : 'text-text'
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useToast } from 'vue-toastification'
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import useSession from '../../store/session'
|
||||||
|
|
||||||
|
const toast = useToast()
|
||||||
|
const sessionStore = useSession()
|
||||||
|
|
||||||
|
const showCover = computed(() => sessionStore.session === null)
|
||||||
|
|
||||||
|
const startSession = async () => {
|
||||||
|
try {
|
||||||
|
await sessionStore.createSession()
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error(err)
|
||||||
|
toast.error(err.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-if="showCover"
|
||||||
|
class="bg-bg-secondary h-full w-1/2 fixed bg-opacity-50 flex justify-center items-center"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
id="start"
|
||||||
|
class="bg-bg-secondary rounded-lg p-3"
|
||||||
|
@click="startSession"
|
||||||
|
>
|
||||||
|
<font-awesome-icon
|
||||||
|
:icon="['fas', 'play']"
|
||||||
|
class="text-text-primary h-full px-3"
|
||||||
|
/>
|
||||||
|
<p class="text-text-primary inline ml-1 md:flex">開始</p>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import FileNode from '../FileNode.vue'
|
import FileNode from '../FileNode.vue'
|
||||||
import sessionStore from '../../store/session'
|
import useSession from '../../store/session'
|
||||||
|
|
||||||
|
const sessionStore = useSession()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -8,6 +10,7 @@ import sessionStore from '../../store/session'
|
|||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
<FileNode
|
<FileNode
|
||||||
|
v-if="sessionStore.session"
|
||||||
:node="sessionStore.session.graph"
|
:node="sessionStore.session.graph"
|
||||||
:pwd="sessionStore.session.pwd"
|
:pwd="sessionStore.session.pwd"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -4,12 +4,11 @@ import { useRouter } from 'vue-router'
|
|||||||
import { useToast } from 'vue-toastification'
|
import { useToast } from 'vue-toastification'
|
||||||
import { logout } from '../../utils/auth'
|
import { logout } from '../../utils/auth'
|
||||||
import { bugReportUrl, surveyUrl } from '../../config'
|
import { bugReportUrl, surveyUrl } from '../../config'
|
||||||
import { reset } from '../../store/session'
|
import useSession from '../../store/session'
|
||||||
import userProfileStore, {
|
import useUserProfile from '../../store/userProfile'
|
||||||
loadUserProfile,
|
|
||||||
resetUserProfile,
|
|
||||||
} from '../../store/userProfile'
|
|
||||||
|
|
||||||
|
const userStore = useUserProfile()
|
||||||
|
const sessionStore = useSession()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
@@ -17,8 +16,8 @@ const toast = useToast()
|
|||||||
const handleLogout = async () => {
|
const handleLogout = async () => {
|
||||||
try {
|
try {
|
||||||
await logout()
|
await logout()
|
||||||
resetUserProfile()
|
userStore.resetUserProfile()
|
||||||
reset()
|
sessionStore.reset()
|
||||||
router.push({ name: 'login' })
|
router.push({ name: 'login' })
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast.error('登出失敗')
|
toast.error('登出失敗')
|
||||||
@@ -28,7 +27,7 @@ const handleLogout = async () => {
|
|||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
try {
|
try {
|
||||||
await loadUserProfile()
|
await userStore.loadUserProfile()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast.error('無法讀取使用者資料')
|
toast.error('無法讀取使用者資料')
|
||||||
console.error(err)
|
console.error(err)
|
||||||
@@ -59,7 +58,7 @@ onMounted(async () => {
|
|||||||
class="text-text inline-block whitespace-nowrap px-1.5"
|
class="text-text inline-block whitespace-nowrap px-1.5"
|
||||||
style="font-size: 2vh"
|
style="font-size: 2vh"
|
||||||
>
|
>
|
||||||
{{ userProfileStore.username }}
|
{{ userStore.username }}
|
||||||
</p>
|
</p>
|
||||||
<a
|
<a
|
||||||
title="Survey"
|
title="Survey"
|
||||||
|
|||||||
+3
-1
@@ -19,6 +19,7 @@ import {
|
|||||||
faTrophy,
|
faTrophy,
|
||||||
faFileInvoice,
|
faFileInvoice,
|
||||||
faEnvelope,
|
faEnvelope,
|
||||||
|
faPlay,
|
||||||
} from '@fortawesome/free-solid-svg-icons'
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
import {
|
import {
|
||||||
faLightbulb,
|
faLightbulb,
|
||||||
@@ -54,5 +55,6 @@ library.add(
|
|||||||
faGoogle,
|
faGoogle,
|
||||||
faFileInvoice,
|
faFileInvoice,
|
||||||
faFacebook,
|
faFacebook,
|
||||||
faEnvelope
|
faEnvelope,
|
||||||
|
faPlay
|
||||||
)
|
)
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
import { createApp } from 'vue'
|
import { createApp } from 'vue'
|
||||||
|
import { createPinia } from 'pinia'
|
||||||
|
|
||||||
import './style.css'
|
import './style.css'
|
||||||
/* import font awesome icon component */
|
/* import font awesome icon component */
|
||||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
||||||
@@ -13,9 +15,11 @@ import App from './App.vue'
|
|||||||
import router from './router'
|
import router from './router'
|
||||||
|
|
||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
|
const pinia = createPinia()
|
||||||
|
|
||||||
app.component('font-awesome-icon', FontAwesomeIcon)
|
app.component('font-awesome-icon', FontAwesomeIcon)
|
||||||
|
|
||||||
|
app.use(pinia)
|
||||||
app.use(router)
|
app.use(router)
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
|
|||||||
+61
-119
@@ -1,142 +1,75 @@
|
|||||||
import { reactive } from 'vue'
|
import { defineStore } from 'pinia'
|
||||||
import { useToast } from 'vue-toastification'
|
|
||||||
import { FileGraph, FileObject } from '@linux-odyssey/file-graph'
|
import { FileGraph, FileObject } from '@linux-odyssey/file-graph'
|
||||||
import { IQuest } from '@linux-odyssey/models'
|
import { IQuest } from '@linux-odyssey/models'
|
||||||
import api from '../utils/api'
|
import { createSession, getActiveSession, getQuest } from '../utils/api'
|
||||||
import Socket from '../utils/socket'
|
import Socket from '../utils/socket'
|
||||||
import SocketTerminal from '../utils/terminal'
|
import SocketTerminal from '../utils/terminal'
|
||||||
import { LoadQuestError, LoadSessionError } from '../utils/errors'
|
|
||||||
import { Session, StageResponse } from '../types'
|
import { Session, StageResponse } from '../types'
|
||||||
|
|
||||||
const toast = useToast()
|
const socket = new Socket()
|
||||||
|
const term = new SocketTerminal(40, 80)
|
||||||
function newSession() {
|
let hasSetup = false
|
||||||
const graph = new FileGraph({
|
|
||||||
path: '/',
|
|
||||||
type: 'folder',
|
|
||||||
discovered: false,
|
|
||||||
})
|
|
||||||
return {
|
|
||||||
_id: '',
|
|
||||||
status: 'inactive',
|
|
||||||
graph,
|
|
||||||
pwd: '',
|
|
||||||
hints: [],
|
|
||||||
tasks: [],
|
|
||||||
responses: [],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Store {
|
interface Store {
|
||||||
session: Session
|
session: Session | null
|
||||||
questId: string
|
questId: string
|
||||||
quest: IQuest | null
|
quest: IQuest | null
|
||||||
}
|
}
|
||||||
|
|
||||||
const store = reactive<Store>({
|
const useSession = defineStore('session', {
|
||||||
session: newSession(),
|
state: (): Store => ({
|
||||||
|
session: null,
|
||||||
questId: '',
|
questId: '',
|
||||||
quest: null,
|
quest: null,
|
||||||
})
|
}),
|
||||||
|
actions: {
|
||||||
const socket = new Socket()
|
async setQuest(questId: string) {
|
||||||
const term = new SocketTerminal(40, 80)
|
this.quest = await getQuest(questId)
|
||||||
|
this.questId = questId
|
||||||
function setQuest(questId: string) {
|
},
|
||||||
return api.get(`/quests/${questId}`).then((res) => {
|
async setSession(session: Session) {
|
||||||
store.questId = res.data._id
|
this.session = session
|
||||||
store.quest = res.data
|
this.session.graph = new FileGraph(session.graph)
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async function setSession(session: Session) {
|
|
||||||
try {
|
|
||||||
console.log('Setting session...', session)
|
|
||||||
store.session = session
|
|
||||||
store.session.graph = new FileGraph(session.graph)
|
|
||||||
term.reset()
|
term.reset()
|
||||||
await socket.connect(session)
|
await socket.connect(session)
|
||||||
term.focus()
|
term.focus()
|
||||||
} catch (err: any) {
|
},
|
||||||
console.error(err)
|
async createSession() {
|
||||||
if (err instanceof Error) {
|
const session = await createSession(this.questId)
|
||||||
toast.error(err.message)
|
await this.setSession(session)
|
||||||
} else {
|
socket.emit('terminal', 'echo start\n')
|
||||||
toast.error('Failed to connect to the session. Please try again later.')
|
},
|
||||||
|
async getActiveSession() {
|
||||||
|
const session = await getActiveSession(this.questId)
|
||||||
|
if (session) {
|
||||||
|
this.setSession(session)
|
||||||
}
|
}
|
||||||
throw err
|
},
|
||||||
}
|
updateGraph(event: { discover?: FileObject[]; pwd?: string }) {
|
||||||
}
|
if (!this.session) return
|
||||||
|
|
||||||
export async function createSession() {
|
|
||||||
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: string) {
|
|
||||||
try {
|
|
||||||
const res = await api.post('/sessions/active', { questId })
|
|
||||||
await setSession(res.data)
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err)
|
|
||||||
toast.warning('Failed to connect previous session. Creating a new one...')
|
|
||||||
await createSession()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateGraph(event: { discover: FileObject[]; pwd: string }) {
|
|
||||||
if (event.discover) {
|
if (event.discover) {
|
||||||
store.session.graph.discover(event.discover)
|
this.session.graph.discover(event.discover)
|
||||||
}
|
}
|
||||||
if (event.pwd) {
|
if (event.pwd) {
|
||||||
store.session.pwd = event.pwd
|
this.session.pwd = event.pwd
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
newResponse(response: StageResponse) {
|
||||||
|
if (!this.session) return
|
||||||
|
this.session.responses.push(response.responses)
|
||||||
|
this.session.hints.push(response.hints)
|
||||||
|
if (response.tasks) {
|
||||||
|
this.session.tasks = response.tasks
|
||||||
}
|
}
|
||||||
|
this.session.status = response.status
|
||||||
function newResponse(response: StageResponse) {
|
},
|
||||||
console.log('newResponse', response, 'session', store.session)
|
reset() {
|
||||||
store.session.responses.push(response.responses)
|
|
||||||
store.session.hints.push(response.hints)
|
|
||||||
store.session.tasks = response.tasks
|
|
||||||
store.session.status = response.status
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useTerminal() {
|
|
||||||
return term
|
|
||||||
}
|
|
||||||
|
|
||||||
export function reset() {
|
|
||||||
socket.reset()
|
socket.reset()
|
||||||
term.reset()
|
term.reset()
|
||||||
store.session = newSession()
|
this.$reset()
|
||||||
store.quest = null
|
},
|
||||||
}
|
setup() {
|
||||||
|
if (hasSetup) return
|
||||||
export async function init(questId: string) {
|
|
||||||
if (!questId) throw new Error('No quest ID provided')
|
|
||||||
reset()
|
|
||||||
try {
|
|
||||||
await setQuest(questId)
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err)
|
|
||||||
throw new LoadQuestError('Failed to load quest', questId)
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
await getActiveSession(questId)
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err)
|
|
||||||
throw new LoadSessionError('Failed to load session', questId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function setup() {
|
|
||||||
socket.on('terminal', (data: string) => {
|
socket.on('terminal', (data: string) => {
|
||||||
term.write(data)
|
term.write(data)
|
||||||
})
|
})
|
||||||
@@ -144,13 +77,22 @@ function setup() {
|
|||||||
socket.emit('terminal', data)
|
socket.emit('terminal', data)
|
||||||
})
|
})
|
||||||
socket.on('graph', (event: { discover: FileObject[]; pwd: string }) => {
|
socket.on('graph', (event: { discover: FileObject[]; pwd: string }) => {
|
||||||
updateGraph(event)
|
this.updateGraph(event)
|
||||||
})
|
})
|
||||||
socket.on('response', (response: StageResponse) => {
|
socket.on('response', (response: StageResponse) => {
|
||||||
newResponse(response)
|
this.newResponse(response)
|
||||||
})
|
})
|
||||||
|
hasSetup = true
|
||||||
|
},
|
||||||
|
setStatus(status: string) {
|
||||||
|
if (!this.session) return
|
||||||
|
this.session.status = status
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export function useTerminal() {
|
||||||
|
return term
|
||||||
}
|
}
|
||||||
|
|
||||||
setup()
|
export default useSession
|
||||||
|
|
||||||
export default store
|
|
||||||
@@ -1,30 +1,35 @@
|
|||||||
import { reactive } from 'vue'
|
import { defineStore } from 'pinia'
|
||||||
import api from '../utils/api'
|
import { getUserProfile } from '../utils/api'
|
||||||
|
|
||||||
const store = reactive({
|
interface IProgress {
|
||||||
|
quest: string
|
||||||
|
sessions: string[]
|
||||||
|
completed: boolean
|
||||||
|
startedAt: Date
|
||||||
|
finishedAt?: Date
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserProfile {
|
||||||
|
username: string
|
||||||
|
email: string
|
||||||
|
progress: Map<string, IProgress>
|
||||||
|
}
|
||||||
|
|
||||||
|
const useUserProfile = defineStore('userProfile', {
|
||||||
|
state: (): UserProfile => ({
|
||||||
username: '',
|
username: '',
|
||||||
email: '',
|
email: '',
|
||||||
progress: {},
|
progress: new Map<string, IProgress>(),
|
||||||
|
}),
|
||||||
|
actions: {
|
||||||
|
async loadUserProfile() {
|
||||||
|
const res = await getUserProfile()
|
||||||
|
this.$patch(res)
|
||||||
|
},
|
||||||
|
async resetUserProfile() {
|
||||||
|
this.$reset()
|
||||||
|
},
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
export async function loadUserProfile() {
|
export default useUserProfile
|
||||||
try {
|
|
||||||
const res = await api.get('/users/me')
|
|
||||||
const { username, email, progress } = res.data
|
|
||||||
store.username = username
|
|
||||||
store.email = email
|
|
||||||
store.progress = progress
|
|
||||||
return store
|
|
||||||
} catch (err: any) {
|
|
||||||
console.error(err)
|
|
||||||
throw new Error(`Failed to load user profile: ${err.message}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function resetUserProfile() {
|
|
||||||
store.username = ''
|
|
||||||
store.email = ''
|
|
||||||
store.progress = {}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default store
|
|
||||||
@@ -1,4 +1,8 @@
|
|||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
import { IQuest } from '@linux-odyssey/models'
|
||||||
|
import { GetSessionsRequest } from '@linux-odyssey/constants'
|
||||||
|
import { Session } from '../types'
|
||||||
|
import { UserProfile } from '../store/userProfile'
|
||||||
|
|
||||||
const api = axios.create({
|
const api = axios.create({
|
||||||
baseURL: '/api/v1',
|
baseURL: '/api/v1',
|
||||||
@@ -8,3 +12,34 @@ const api = axios.create({
|
|||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
})
|
})
|
||||||
export default api
|
export default api
|
||||||
|
|
||||||
|
export async function getUserProfile(): Promise<UserProfile> {
|
||||||
|
const res = await api.get<UserProfile>('/users/me')
|
||||||
|
return res.data
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getQuest(id: string): Promise<IQuest> {
|
||||||
|
const res = await api.get<IQuest>(`/quests/${id}`)
|
||||||
|
return res.data
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createSession(questId: string): Promise<Session> {
|
||||||
|
const res = await api.post<Session>('/sessions', { questId })
|
||||||
|
return res.data
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getSessions(
|
||||||
|
request: GetSessionsRequest
|
||||||
|
): Promise<Session[]> {
|
||||||
|
const res = await api.get<Session[]>('/sessions', { params: request })
|
||||||
|
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
|
||||||
|
}
|
||||||
@@ -46,7 +46,6 @@ class SocketTerminal {
|
|||||||
|
|
||||||
mount(reference: HTMLElement): void {
|
mount(reference: HTMLElement): void {
|
||||||
this.term.open(reference)
|
this.term.open(reference)
|
||||||
this.term.focus()
|
|
||||||
this.resizeScreen()
|
this.resizeScreen()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+11
-22
@@ -1,20 +1,20 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted } from 'vue'
|
import { computed, onMounted } from 'vue'
|
||||||
import { useToast } from 'vue-toastification'
|
import useSession from '../store/session'
|
||||||
import sessionStore, { init } from '../store/session'
|
|
||||||
import HeaderPart from '../components/header/HeaderPart.vue'
|
import HeaderPart from '../components/header/HeaderPart.vue'
|
||||||
import GameHeaderComponents from '../components/header/GameHeaderComponents.vue'
|
import GameHeaderComponents from '../components/header/GameHeaderComponents.vue'
|
||||||
// import CommandlistPart from '../components/CommandlistPart.vue'
|
|
||||||
import HintPart from '../components/game/HintPart.vue'
|
import HintPart from '../components/game/HintPart.vue'
|
||||||
import QuestPart from '../components/game/QuestPart.vue'
|
import QuestPart from '../components/game/QuestPart.vue'
|
||||||
import TerminalPart from '../components/game/TerminalPart.vue'
|
import TerminalPart from '../components/game/TerminalPart.vue'
|
||||||
import VisualizationPart from '../components/game/VisualizationPart.vue'
|
import VisualizationPart from '../components/game/VisualizationPart.vue'
|
||||||
import ControlPalette from '../components/game/ControlPalette.vue'
|
import ControlPalette from '../components/game/ControlPalette.vue'
|
||||||
import CompleteModal from '../components/game/CompleteModal.vue'
|
import CompleteModal from '../components/game/CompleteModal.vue'
|
||||||
import { LoadQuestError, LoadSessionError } from '../utils/errors'
|
import StartButton from '../components/game/StartButton.vue'
|
||||||
|
|
||||||
|
const sessionStore = useSession()
|
||||||
|
|
||||||
const completed = computed(() => {
|
const completed = computed(() => {
|
||||||
return sessionStore.session.status === 'finished'
|
return sessionStore.session?.status === 'finished'
|
||||||
})
|
})
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -24,23 +24,11 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const toast = useToast()
|
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
try {
|
sessionStore.reset()
|
||||||
await init(props.questId)
|
sessionStore.setup()
|
||||||
} catch (err: any) {
|
await sessionStore.setQuest(props.questId)
|
||||||
if (err instanceof LoadQuestError) {
|
await sessionStore.getActiveSession()
|
||||||
toast.error(`無法讀取關卡: ${err.questId},請確認網頁連結。`)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (err instanceof LoadSessionError) {
|
|
||||||
toast.error(`無法建立工作階段: ${err.questId},請重新登入再試一次。`)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
console.error(err)
|
|
||||||
toast.error(err.message)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -56,7 +44,7 @@ onMounted(async () => {
|
|||||||
<input
|
<input
|
||||||
type="hidden"
|
type="hidden"
|
||||||
id="currentStatus"
|
id="currentStatus"
|
||||||
:value="sessionStore.session.status"
|
:value="sessionStore.session?.status"
|
||||||
/>
|
/>
|
||||||
<!-- main -->
|
<!-- main -->
|
||||||
<div id="main" class="h-full pt-10 w-full flex p-3 space-x-3">
|
<div id="main" class="h-full pt-10 w-full flex p-3 space-x-3">
|
||||||
@@ -72,6 +60,7 @@ onMounted(async () => {
|
|||||||
</div>
|
</div>
|
||||||
<!-- Terminal and Hint -->
|
<!-- Terminal and Hint -->
|
||||||
<div class="bg-bg h-full rounded-lg w-1/2">
|
<div class="bg-bg h-full rounded-lg w-1/2">
|
||||||
|
<StartButton :questId="$props.questId" />
|
||||||
<section id="hint" class="h-3/5">
|
<section id="hint" class="h-3/5">
|
||||||
<HintPart />
|
<HintPart />
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -1,95 +0,0 @@
|
|||||||
version: '3'
|
|
||||||
services:
|
|
||||||
app:
|
|
||||||
depends_on:
|
|
||||||
- backend
|
|
||||||
build:
|
|
||||||
context: .
|
|
||||||
dockerfile: app/Dockerfile
|
|
||||||
target: dev
|
|
||||||
ports:
|
|
||||||
- 5173:5173
|
|
||||||
networks:
|
|
||||||
- internal
|
|
||||||
volumes:
|
|
||||||
- ./app:/app/app
|
|
||||||
- ./packages:/app/packages
|
|
||||||
|
|
||||||
backend:
|
|
||||||
depends_on:
|
|
||||||
- db
|
|
||||||
build:
|
|
||||||
context: .
|
|
||||||
dockerfile: server/Dockerfile
|
|
||||||
target: dev
|
|
||||||
networks:
|
|
||||||
- internal
|
|
||||||
- players
|
|
||||||
ports:
|
|
||||||
- '3000:3000'
|
|
||||||
volumes:
|
|
||||||
- /var/run/docker.sock:/var/run/docker.sock
|
|
||||||
- ./server/src:/app/server/src
|
|
||||||
- ./server/dist:/app/server/dist
|
|
||||||
- ./server/swagger.yaml:/app/server/swagger.yaml
|
|
||||||
- ./packages:/app/packages
|
|
||||||
- ./quests:/app/quests
|
|
||||||
- ./logs:/app/logs
|
|
||||||
environment:
|
|
||||||
- BASE_URL=http://localhost:5173
|
|
||||||
- MONGO_URL=mongodb://db:27017/odyssey-test
|
|
||||||
- DOCKER_NETWORK=linux-odyssey-players
|
|
||||||
- SECRET_KEY=b1d2e917-17b9-4fce-9055-082dee6ec5d6 # Only for testing
|
|
||||||
- TRUSTED_PROXIES=${TRUSTED_PROXIES}
|
|
||||||
- SURVEY_URL=${SURVEY_URL}
|
|
||||||
- BUG_REPORT_URL=${BUG_REPORT_URL}
|
|
||||||
- GOOGLE_CLIENT_ID=${GOOGLE_CLIENT_ID}
|
|
||||||
- GOOGLE_CLIENT_SECRET=${GOOGLE_CLIENT_SECRET}
|
|
||||||
- GITHUB_CLIENT_ID=${GITHUB_CLIENT_ID}
|
|
||||||
- GITHUB_CLIENT_SECRET=${GITHUB_CLIENT_SECRET}
|
|
||||||
- TESTING=true
|
|
||||||
- TESTING_USERNAME=alex
|
|
||||||
- TESTING_PASSWORD=Alex1234
|
|
||||||
- HOST_PWD=${PWD}
|
|
||||||
- MOUNT_QUEST=${MOUNT_QUEST}
|
|
||||||
|
|
||||||
analytics:
|
|
||||||
build:
|
|
||||||
context: .
|
|
||||||
dockerfile: packages/analytics/Dockerfile
|
|
||||||
target: dev
|
|
||||||
depends_on:
|
|
||||||
- db
|
|
||||||
networks:
|
|
||||||
- internal
|
|
||||||
ports:
|
|
||||||
- '3001:3001'
|
|
||||||
environment:
|
|
||||||
- MONGO_URL=mongodb://db:27017/odyssey-test
|
|
||||||
- ADMIN_USERNAME=alex
|
|
||||||
- ADMIN_PASSWORD=Alex1234
|
|
||||||
volumes:
|
|
||||||
- ./packages/analytics:/app/packages/analytics
|
|
||||||
- ./packages/models:/app/packages/models
|
|
||||||
|
|
||||||
db:
|
|
||||||
image: mongo
|
|
||||||
restart: always
|
|
||||||
networks:
|
|
||||||
- internal
|
|
||||||
ports:
|
|
||||||
- '27017:27017'
|
|
||||||
volumes:
|
|
||||||
- ./data:/data/db
|
|
||||||
|
|
||||||
base:
|
|
||||||
image: linuxodyssey/quest-base
|
|
||||||
build:
|
|
||||||
context: .
|
|
||||||
dockerfile: ./quests/Dockerfile
|
|
||||||
|
|
||||||
networks:
|
|
||||||
internal:
|
|
||||||
players:
|
|
||||||
name: linux-odyssey-players
|
|
||||||
internal: true
|
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
version: '3'
|
||||||
|
services:
|
||||||
|
db:
|
||||||
|
image: mongo
|
||||||
|
restart: always
|
||||||
|
networks:
|
||||||
|
- internal
|
||||||
|
ports:
|
||||||
|
- '27017:27017'
|
||||||
|
volumes:
|
||||||
|
- ./data:/data/db
|
||||||
|
|
||||||
|
base:
|
||||||
|
image: linuxodyssey/quest-base
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: ./quests/Dockerfile
|
||||||
|
|
||||||
|
networks:
|
||||||
|
internal:
|
||||||
|
players:
|
||||||
|
name: linux-odyssey-players
|
||||||
|
internal: true
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { z } from 'zod'
|
||||||
|
|
||||||
export const passwordPolicy = {
|
export const passwordPolicy = {
|
||||||
minLength: 8,
|
minLength: 8,
|
||||||
minNumbers: 1,
|
minNumbers: 1,
|
||||||
@@ -6,6 +8,11 @@ export const passwordPolicy = {
|
|||||||
minSymbols: 0,
|
minSymbols: 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export const getSessionsRequestSchema = z.object({
|
||||||
passwordPolicy,
|
questId: z.string().optional(),
|
||||||
}
|
status: z.enum(['active', 'inactive', 'finished']).optional(),
|
||||||
|
limit: z.coerce.number().optional(),
|
||||||
|
order: z.enum(['asc', 'desc']).optional(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type GetSessionsRequest = z.infer<typeof getSessionsRequestSchema>
|
||||||
@@ -10,5 +10,8 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc",
|
"build": "tsc",
|
||||||
"dev": "tsc -w"
|
"dev": "tsc -w"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"zod": "^3.23.8"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -28,6 +28,7 @@ export interface IUser {
|
|||||||
socialLogins: Map<string, ISocialLogin>
|
socialLogins: Map<string, ISocialLogin>
|
||||||
createdAt: Date
|
createdAt: Date
|
||||||
updatedAt: Date
|
updatedAt: Date
|
||||||
|
isGuest: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const User = model<IUser>(
|
export const User = model<IUser>(
|
||||||
@@ -51,6 +52,11 @@ export const User = model<IUser>(
|
|||||||
default: {},
|
default: {},
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
isGuest: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
timestamps: true,
|
timestamps: true,
|
||||||
|
|||||||
@@ -95,10 +95,13 @@ stages:
|
|||||||
not:
|
not:
|
||||||
pwd:
|
pwd:
|
||||||
- ^/home/commander$
|
- ^/home/commander$
|
||||||
|
responses:
|
||||||
|
- type: narrative
|
||||||
content:
|
content:
|
||||||
- 請留在 `/home/commander` 資料夾中。
|
- 請留在 `/home/commander` 資料夾中。
|
||||||
- 請輸入 `cd ~` 回到 `/home/commander` 資料夾。
|
- 請輸入 `cd ~` 回到 `/home/commander` 資料夾。
|
||||||
responses:
|
responses:
|
||||||
- content:
|
- type: narrative
|
||||||
|
content:
|
||||||
- 你成功地查看到 `file.txt` 的內容了。
|
- 你成功地查看到 `file.txt` 的內容了。
|
||||||
- 你已經會使用 `cat` 指令查看檔案的內容了。
|
- 你已經會使用 `cat` 指令查看檔案的內容了。
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import connectDB, { User } from '@linux-odyssey/models'
|
||||||
|
|
||||||
|
async function migrate() {
|
||||||
|
await connectDB(
|
||||||
|
process.env.MONGO_URL || 'mongodb://localhost:27017/odyssey-test'
|
||||||
|
)
|
||||||
|
await User.updateMany(
|
||||||
|
{
|
||||||
|
isGuest: { $exists: false },
|
||||||
|
},
|
||||||
|
{ $set: { isGuest: false } }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
migrate()
|
||||||
|
.then(() => {
|
||||||
|
console.log('Migration completed')
|
||||||
|
process.exit(0)
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error(err)
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
+3
-1
@@ -9,7 +9,8 @@
|
|||||||
"clean": "node dist/cli/clean.js",
|
"clean": "node dist/cli/clean.js",
|
||||||
"build": "tsc -b",
|
"build": "tsc -b",
|
||||||
"dev": "nodemon",
|
"dev": "nodemon",
|
||||||
"build:quests": "node dist/cli/build.js"
|
"build:quests": "node dist/cli/build.js",
|
||||||
|
"ts-node": "node --loader ts-node/esm"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@linux-odyssey/constants": "*",
|
"@linux-odyssey/constants": "*",
|
||||||
@@ -20,6 +21,7 @@
|
|||||||
"commander": "^11.0.0",
|
"commander": "^11.0.0",
|
||||||
"connect-mongo": "^5.0.0",
|
"connect-mongo": "^5.0.0",
|
||||||
"cookie-parser": "^1.4.6",
|
"cookie-parser": "^1.4.6",
|
||||||
|
"date-fns": "^3.6.0",
|
||||||
"dockerode": "^3.3.5",
|
"dockerode": "^3.3.5",
|
||||||
"dotenv": "^16.4.1",
|
"dotenv": "^16.4.1",
|
||||||
"express": "^4.19.2",
|
"express": "^4.19.2",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { Request, Response, NextFunction, Express } from 'express'
|
import type { Request, Response, NextFunction, Express } from 'express'
|
||||||
import { matchedData } from 'express-validator'
|
import { matchedData } from 'express-validator'
|
||||||
import { genJWT } from '../../utils/auth.js'
|
import { genJWT } from '../../utils/auth.js'
|
||||||
import { createUser } from '../../models/userManager.js'
|
import { createUser, createGuestUser } from '../../models/userManager.js'
|
||||||
import { asyncHandler } from '../../middleware/error.js'
|
import { asyncHandler } from '../../middleware/error.js'
|
||||||
import logger from '../../utils/logger.js'
|
import logger from '../../utils/logger.js'
|
||||||
|
|
||||||
@@ -39,6 +39,30 @@ export const register = asyncHandler(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
export const registerGuest = asyncHandler(
|
||||||
|
async (req: Request, res: Response) => {
|
||||||
|
const user = await createGuestUser()
|
||||||
|
req.login(
|
||||||
|
{
|
||||||
|
id: user.id,
|
||||||
|
username: user.username,
|
||||||
|
email: user.email,
|
||||||
|
},
|
||||||
|
(err) => {
|
||||||
|
if (err) {
|
||||||
|
res.status(500).json({
|
||||||
|
message: 'error logging in',
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
res.status(201).json({
|
||||||
|
message: 'user created',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
export function logout(req: Request, res: Response) {
|
export function logout(req: Request, res: Response) {
|
||||||
req.logout((err) => {
|
req.logout((err) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
|
|||||||
@@ -49,8 +49,6 @@ export const newCommand = asyncHandler(async (req: Request, res: Response) => {
|
|||||||
c.stage = response.stage
|
c.stage = response.stage
|
||||||
await c.save()
|
await c.save()
|
||||||
|
|
||||||
session.responses.push(response.responses)
|
|
||||||
|
|
||||||
if ((session.status as string) === 'finished') {
|
if ((session.status as string) === 'finished') {
|
||||||
await finishSession(session)
|
await finishSession(session)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import type { Express, Request, Response } from 'express'
|
|||||||
import { User, Session, ISession } from '@linux-odyssey/models'
|
import { User, Session, ISession } from '@linux-odyssey/models'
|
||||||
import {
|
import {
|
||||||
createNewSession,
|
createNewSession,
|
||||||
getOrCreateActiveSession,
|
|
||||||
isQuestUnlocked,
|
isQuestUnlocked,
|
||||||
} from '../../models/sessionManager.js'
|
} from '../../models/sessionManager.js'
|
||||||
import { asyncHandler } from '../../middleware/error.js'
|
import { asyncHandler } from '../../middleware/error.js'
|
||||||
@@ -56,7 +55,6 @@ export const createSession = asyncHandler(
|
|||||||
export const getSessionById = asyncHandler(
|
export const getSessionById = asyncHandler(
|
||||||
async (req: Request, res: Response) => {
|
async (req: Request, res: Response) => {
|
||||||
const { sessionId } = matchedData(req)
|
const { sessionId } = matchedData(req)
|
||||||
console.log(sessionId)
|
|
||||||
const session = await Session.findOne({
|
const session = await Session.findOne({
|
||||||
_id: sessionId,
|
_id: sessionId,
|
||||||
user: (req.user as Express.ExistingUser).id,
|
user: (req.user as Express.ExistingUser).id,
|
||||||
@@ -71,7 +69,7 @@ export const getSessionById = asyncHandler(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
export const getOrCreateSession = asyncHandler(
|
export const getActiveSessionHandler = asyncHandler(
|
||||||
async (req: Request, res: Response) => {
|
async (req: Request, res: Response) => {
|
||||||
const { questId } = matchedData(req)
|
const { questId } = matchedData(req)
|
||||||
const user = await User.findById((req.user as Express.ExistingUser).id)
|
const user = await User.findById((req.user as Express.ExistingUser).id)
|
||||||
@@ -79,8 +77,12 @@ export const getOrCreateSession = asyncHandler(
|
|||||||
user &&
|
user &&
|
||||||
((await isQuestUnlocked(user, questId)) || config.testing.enabled)
|
((await isQuestUnlocked(user, questId)) || config.testing.enabled)
|
||||||
) {
|
) {
|
||||||
const session = await getOrCreateActiveSession(user, questId)
|
const session = await Session.findOne({
|
||||||
res.json(sessionDetail(session))
|
user: user.id,
|
||||||
|
quest: questId,
|
||||||
|
status: 'active',
|
||||||
|
})
|
||||||
|
res.json(session ? sessionDetail(session) : null)
|
||||||
} else {
|
} else {
|
||||||
res.status(403).json({ message: 'Quest is locked.' })
|
res.status(403).json({ message: 'Quest is locked.' })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
logout,
|
logout,
|
||||||
registerFromSession,
|
registerFromSession,
|
||||||
socialLoginHandler,
|
socialLoginHandler,
|
||||||
|
registerGuest,
|
||||||
} from '../controllers/authController.js'
|
} from '../controllers/authController.js'
|
||||||
import {
|
import {
|
||||||
checkUsernameValidators,
|
checkUsernameValidators,
|
||||||
@@ -32,6 +33,8 @@ router.post(
|
|||||||
|
|
||||||
router.post('/register', authenticateRateLimit, registerValidators, register)
|
router.post('/register', authenticateRateLimit, registerValidators, register)
|
||||||
|
|
||||||
|
router.post('/register-guest', authenticateRateLimit, registerGuest)
|
||||||
|
|
||||||
router.post('/logout', logout)
|
router.post('/logout', logout)
|
||||||
|
|
||||||
router.get(
|
router.get(
|
||||||
|
|||||||
@@ -17,10 +17,10 @@ router.post(
|
|||||||
sessionRateLimit,
|
sessionRateLimit,
|
||||||
sessionController.createSession
|
sessionController.createSession
|
||||||
)
|
)
|
||||||
router.post(
|
router.get(
|
||||||
'/active',
|
'/active',
|
||||||
activeSessionValidator,
|
activeSessionValidator,
|
||||||
sessionController.getOrCreateSession
|
sessionController.getActiveSessionHandler
|
||||||
)
|
)
|
||||||
router.get(
|
router.get(
|
||||||
'/:sessionId',
|
'/:sessionId',
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import SessionMiddleware from '../middleware/session.js'
|
|||||||
import { genJWT } from '../utils/auth.js'
|
import { genJWT } from '../utils/auth.js'
|
||||||
import logger from '../utils/logger.js'
|
import logger from '../utils/logger.js'
|
||||||
|
|
||||||
// eslint-disable-next-line no-unused-vars
|
|
||||||
type SocketCallback = (event: string, ...args: any[]) => void
|
type SocketCallback = (event: string, ...args: any[]) => void
|
||||||
|
|
||||||
const sessions = new Map<string, SocketCallback[]>()
|
const sessions = new Map<string, SocketCallback[]>()
|
||||||
@@ -44,7 +43,6 @@ async function connectContainer(socket: Socket, next: (err?: Error) => void) {
|
|||||||
next(new Error('User not found.'))
|
next(new Error('User not found.'))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// console.log(socket.handshake)
|
|
||||||
const { sessionId } = socket.handshake.query
|
const { sessionId } = socket.handshake.query
|
||||||
if (
|
if (
|
||||||
!sessionId ||
|
!sessionId ||
|
||||||
@@ -79,7 +77,7 @@ async function connectContainer(socket: Socket, next: (err?: Error) => void) {
|
|||||||
stream,
|
stream,
|
||||||
}
|
}
|
||||||
next()
|
next()
|
||||||
} catch (err) {
|
} catch {
|
||||||
next(new Error('Failed to start container.'))
|
next(new Error('Failed to start container.'))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ export async function deleteContainer(id: string) {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await container.stop()
|
await container.stop()
|
||||||
} catch (error) {
|
} catch {
|
||||||
logger.warn('Failed to stop container', id)
|
logger.warn('Failed to stop container', id)
|
||||||
}
|
}
|
||||||
await container.remove()
|
await container.remove()
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ export default class CommandHandler extends SessionHandler {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
return checks.every((c) => c === true)
|
return checks.every((c) => c === true)
|
||||||
} catch (e) {
|
} catch {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,21 +79,6 @@ export async function createNewSession(
|
|||||||
return session
|
return session
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getOrCreateActiveSession(
|
|
||||||
user: HydratedDocument<IUser>,
|
|
||||||
questId: string
|
|
||||||
): Promise<ISession> {
|
|
||||||
const session = await Session.findOne({
|
|
||||||
user,
|
|
||||||
quest: questId,
|
|
||||||
status: 'active',
|
|
||||||
})
|
|
||||||
if (session) {
|
|
||||||
return session
|
|
||||||
}
|
|
||||||
return createNewSession(user, questId)
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function finishSession(
|
export async function finishSession(
|
||||||
session: HydratedDocument<ISession>
|
session: HydratedDocument<ISession>
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Express } from 'express'
|
import { Express } from 'express'
|
||||||
|
import { format } from 'date-fns'
|
||||||
import { User, UserProfile } from '@linux-odyssey/models'
|
import { User, UserProfile } from '@linux-odyssey/models'
|
||||||
import { hashPassword } from '../utils/auth.js'
|
import { hashPassword } from '../utils/auth.js'
|
||||||
|
|
||||||
@@ -31,6 +32,23 @@ export async function createUser(
|
|||||||
return user
|
return user
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function createGuestUser() {
|
||||||
|
const username = `guest_${format(new Date(), 'yyyyMMddHHmmss')}`
|
||||||
|
const user = new User({
|
||||||
|
username,
|
||||||
|
email: `${username}@example.com`,
|
||||||
|
isGuest: true,
|
||||||
|
})
|
||||||
|
await user.save()
|
||||||
|
|
||||||
|
const profile = new UserProfile({
|
||||||
|
user,
|
||||||
|
})
|
||||||
|
await profile.save()
|
||||||
|
|
||||||
|
return user
|
||||||
|
}
|
||||||
|
|
||||||
export function leaderboard() {
|
export function leaderboard() {
|
||||||
return UserProfile.aggregate([
|
return UserProfile.aggregate([
|
||||||
{
|
{
|
||||||
|
|||||||
+31
-2
@@ -1,14 +1,14 @@
|
|||||||
openapi: 3.0.0
|
openapi: 3.0.0
|
||||||
servers:
|
servers:
|
||||||
- url: /api/v1
|
- url: /api/v1
|
||||||
- url: https://odyssey.wancat.cc/api/v1
|
- url: https://linuxodyssey.xyz/api/v1
|
||||||
- url: http://localhost:3000/api/v1
|
- url: http://localhost:3000/api/v1
|
||||||
info:
|
info:
|
||||||
description: Linux Odyssey API
|
description: Linux Odyssey API
|
||||||
version: '1.0.0'
|
version: '1.0.0'
|
||||||
title: Session API
|
title: Session API
|
||||||
contact:
|
contact:
|
||||||
email: wancat@wancat.cc
|
email: contact@linuxodyssey.xyz
|
||||||
license:
|
license:
|
||||||
name: Apache 2.0
|
name: Apache 2.0
|
||||||
url: 'http://www.apache.org/licenses/LICENSE-2.0.html'
|
url: 'http://www.apache.org/licenses/LICENSE-2.0.html'
|
||||||
@@ -37,6 +37,19 @@ paths:
|
|||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
enum: [active, finished, inactive]
|
enum: [active, finished, inactive]
|
||||||
|
- in: query
|
||||||
|
name: limit
|
||||||
|
description: limit the number of sessions to return
|
||||||
|
schema:
|
||||||
|
type: number
|
||||||
|
example: 10
|
||||||
|
- in: query
|
||||||
|
name: order
|
||||||
|
description: order the sessions by createdAt
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
enum: [asc, desc]
|
||||||
|
example: desc
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: list of existing session
|
description: list of existing session
|
||||||
@@ -233,6 +246,22 @@ paths:
|
|||||||
type: string
|
type: string
|
||||||
example: connect.sid=s%3Aq1Z; Path=/; HttpOnly; SameSite=Strict; Secure
|
example: connect.sid=s%3Aq1Z; Path=/; HttpOnly; SameSite=Strict; Secure
|
||||||
|
|
||||||
|
/auth/register-guest:
|
||||||
|
post:
|
||||||
|
summary: Register a new guest user
|
||||||
|
responses:
|
||||||
|
201:
|
||||||
|
description: User created
|
||||||
|
headers:
|
||||||
|
Set-Cookie:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
example: connect.sid=s%3Aq1Z; Path=/; HttpOnly; SameSite=Strict; Secure
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/User'
|
||||||
|
|
||||||
/auth/login:
|
/auth/login:
|
||||||
post:
|
post:
|
||||||
summary: Log in a user
|
summary: Log in a user
|
||||||
|
|||||||
@@ -2479,6 +2479,11 @@
|
|||||||
"@vue/compiler-dom" "3.4.19"
|
"@vue/compiler-dom" "3.4.19"
|
||||||
"@vue/shared" "3.4.19"
|
"@vue/shared" "3.4.19"
|
||||||
|
|
||||||
|
"@vue/devtools-api@^6.5.0":
|
||||||
|
version "6.6.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/@vue/devtools-api/-/devtools-api-6.6.3.tgz#b23a588154cba8986bba82b6e1d0248bde3fd1a0"
|
||||||
|
integrity sha512-0MiMsFma/HqA6g3KLKn+AGpL1kgKhFWszC9U29NfpWK5LE7bjeXxySWJrOJ77hBz+TBrBQ7o4QJqbPbqbs8rJw==
|
||||||
|
|
||||||
"@vue/devtools-api@^6.5.1":
|
"@vue/devtools-api@^6.5.1":
|
||||||
version "6.6.1"
|
version "6.6.1"
|
||||||
resolved "https://registry.yarnpkg.com/@vue/devtools-api/-/devtools-api-6.6.1.tgz#7c14346383751d9f6ad4bea0963245b30220ef83"
|
resolved "https://registry.yarnpkg.com/@vue/devtools-api/-/devtools-api-6.6.1.tgz#7c14346383751d9f6ad4bea0963245b30220ef83"
|
||||||
@@ -3594,6 +3599,11 @@ date-fns@^2.30.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.21.0"
|
"@babel/runtime" "^7.21.0"
|
||||||
|
|
||||||
|
date-fns@^3.6.0:
|
||||||
|
version "3.6.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-3.6.0.tgz#f20ca4fe94f8b754951b24240676e8618c0206bf"
|
||||||
|
integrity sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==
|
||||||
|
|
||||||
dayjs@^1.10.4:
|
dayjs@^1.10.4:
|
||||||
version "1.11.10"
|
version "1.11.10"
|
||||||
resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.10.tgz#68acea85317a6e164457d6d6947564029a6a16a0"
|
resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.10.tgz#68acea85317a6e164457d6d6947564029a6a16a0"
|
||||||
@@ -6768,6 +6778,14 @@ pify@^2.2.0, pify@^2.3.0:
|
|||||||
resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
|
resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
|
||||||
integrity sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==
|
integrity sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==
|
||||||
|
|
||||||
|
pinia@^2.1.7:
|
||||||
|
version "2.1.7"
|
||||||
|
resolved "https://registry.yarnpkg.com/pinia/-/pinia-2.1.7.tgz#4cf5420d9324ca00b7b4984d3fbf693222115bbc"
|
||||||
|
integrity sha512-+C2AHFtcFqjPih0zpYuvof37SFxMQ7OEG2zV9jRI12i9BOy3YQVAHwdKtyyc8pDcDyIc33WCIsZaCFWU7WWxGQ==
|
||||||
|
dependencies:
|
||||||
|
"@vue/devtools-api" "^6.5.0"
|
||||||
|
vue-demi ">=0.14.5"
|
||||||
|
|
||||||
pirates@^4.0.1, pirates@^4.0.4:
|
pirates@^4.0.1, pirates@^4.0.4:
|
||||||
version "4.0.6"
|
version "4.0.6"
|
||||||
resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.6.tgz#3018ae32ecfcff6c29ba2267cbf21166ac1f36b9"
|
resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.6.tgz#3018ae32ecfcff6c29ba2267cbf21166ac1f36b9"
|
||||||
@@ -8155,6 +8173,11 @@ vite@^4.5.3:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
fsevents "~2.3.2"
|
fsevents "~2.3.2"
|
||||||
|
|
||||||
|
vue-demi@>=0.14.5:
|
||||||
|
version "0.14.8"
|
||||||
|
resolved "https://registry.yarnpkg.com/vue-demi/-/vue-demi-0.14.8.tgz#00335e9317b45e4a68d3528aaf58e0cec3d5640a"
|
||||||
|
integrity sha512-Uuqnk9YE9SsWeReYqK2alDI5YzciATE0r2SkA6iMAtuXvNTMNACJLJEXNXaEy94ECuBe4Sk6RzRU80kjdbIo1Q==
|
||||||
|
|
||||||
vue-eslint-parser@^9.4.2:
|
vue-eslint-parser@^9.4.2:
|
||||||
version "9.4.2"
|
version "9.4.2"
|
||||||
resolved "https://registry.yarnpkg.com/vue-eslint-parser/-/vue-eslint-parser-9.4.2.tgz#02ffcce82042b082292f2d1672514615f0d95b6d"
|
resolved "https://registry.yarnpkg.com/vue-eslint-parser/-/vue-eslint-parser-9.4.2.tgz#02ffcce82042b082292f2d1672514615f0d95b6d"
|
||||||
@@ -8452,6 +8475,11 @@ yocto-queue@^0.1.0:
|
|||||||
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
|
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
|
||||||
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
|
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
|
||||||
|
|
||||||
|
zod@^3.23.8:
|
||||||
|
version "3.23.8"
|
||||||
|
resolved "https://registry.yarnpkg.com/zod/-/zod-3.23.8.tgz#e37b957b5d52079769fb8097099b592f0ef4067d"
|
||||||
|
integrity sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==
|
||||||
|
|
||||||
zrender@5.5.0:
|
zrender@5.5.0:
|
||||||
version "5.5.0"
|
version "5.5.0"
|
||||||
resolved "https://registry.yarnpkg.com/zrender/-/zrender-5.5.0.tgz#54d0d6c4eda81a96d9f60a9cd74dc48ea026bc1e"
|
resolved "https://registry.yarnpkg.com/zrender/-/zrender-5.5.0.tgz#54d0d6c4eda81a96d9f60a9cd74dc48ea026bc1e"
|
||||||
|
|||||||
Reference in new issue
Block a user