mirror of
https://github.com/linux-odyssey/linux-odyssey.git
synced 2026-11-05 19:20:12 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0467bf8d3f | ||
|
|
7caadd6695 | ||
|
|
7ab6a828ca | ||
|
|
acbe310a4c | ||
|
|
045941a05c | ||
|
|
676c12de77 | ||
|
|
b510d18758 | ||
|
|
9ffcd9fa90 | ||
|
|
3106ae00d0 | ||
|
|
9ce161e4f3 | ||
|
|
e449f9987b | ||
|
|
419440a14f | ||
|
|
54225528b3 | ||
|
|
634dc8f987 | ||
|
|
77577d9b8d | ||
|
|
2feedc329c | ||
|
|
fb7dc0eb73 | ||
|
|
f515228db6 |
No files matched your search
@@ -3,3 +3,4 @@
|
|||||||
**/dist
|
**/dist
|
||||||
**/tsconfig.tsbuildinfo
|
**/tsconfig.tsbuildinfo
|
||||||
data
|
data
|
||||||
|
postgres-data
|
||||||
@@ -3,3 +3,4 @@ vite.config.js
|
|||||||
data
|
data
|
||||||
mongo-init.js
|
mongo-init.js
|
||||||
dist
|
dist
|
||||||
|
postgres-data
|
||||||
+3
-1
@@ -2,7 +2,9 @@ module.exports = {
|
|||||||
root: true,
|
root: true,
|
||||||
env: {
|
env: {
|
||||||
node: true,
|
node: true,
|
||||||
jest: true,
|
},
|
||||||
|
globals: {
|
||||||
|
Bun: 'readonly',
|
||||||
},
|
},
|
||||||
plugins: ['@typescript-eslint', 'cypress', 'vue'],
|
plugins: ['@typescript-eslint', 'cypress', 'vue'],
|
||||||
extends: [
|
extends: [
|
||||||
|
|||||||
@@ -11,18 +11,21 @@ jobs:
|
|||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- uses: actions/setup-node@v3
|
- uses: oven-sh/setup-bun@v2
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
bun-version: 1.3.14
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: yarn install
|
run: bun install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Type check
|
||||||
|
run: bun run type-check
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: yarn build
|
run: bun run build
|
||||||
|
|
||||||
- name: Lint
|
- name: Lint
|
||||||
run: yarn lint
|
run: bun run lint
|
||||||
|
|
||||||
- name: Test
|
- name: Test
|
||||||
run: yarn test
|
run: bun run test
|
||||||
@@ -14,7 +14,7 @@ jobs:
|
|||||||
run: docker-compose -f docker-compose.testing.yml build base backend app-test cypress
|
run: docker-compose -f docker-compose.testing.yml build base backend app-test cypress
|
||||||
|
|
||||||
- name: Build Test Quests
|
- name: Build Test Quests
|
||||||
run: docker-compose -f docker-compose.testing.yml run --rm backend yarn build:quests
|
run: docker-compose -f docker-compose.testing.yml run --rm backend bun run build:quests
|
||||||
|
|
||||||
- name: Run Cypress Tests
|
- name: Run Cypress Tests
|
||||||
env:
|
env:
|
||||||
|
|||||||
Vendored
+4
-4
@@ -4,10 +4,10 @@
|
|||||||
{
|
{
|
||||||
"type": "node",
|
"type": "node",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"name": "File Graph Yarn Jest",
|
"name": "File Graph Tests",
|
||||||
"runtimeExecutable": "yarn",
|
"runtimeExecutable": "bun",
|
||||||
"args": ["test", "--runInBand"],
|
"args": ["test", "packages/file-graph"],
|
||||||
"cwd": "${workspaceFolder}/packages/file-graph",
|
"cwd": "${workspaceFolder}",
|
||||||
"console": "integratedTerminal",
|
"console": "integratedTerminal",
|
||||||
"internalConsoleOptions": "neverOpen"
|
"internalConsoleOptions": "neverOpen"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,16 +5,15 @@
|
|||||||
|
|
||||||
## TLDR
|
## TLDR
|
||||||
|
|
||||||
yarn install
|
bun install
|
||||||
cp .env.sample .env
|
cp .env.sample .env
|
||||||
docker compose up -d
|
docker compose up -d
|
||||||
yarn dev
|
bun run dev
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
- NodeJS v20+
|
- Bun: v1.3+
|
||||||
- Docker
|
- Docker
|
||||||
- Yarn: v1.22
|
|
||||||
|
|
||||||
## Known Issues
|
## Known Issues
|
||||||
|
|
||||||
@@ -26,7 +25,7 @@ Windows user please refer to [docs/windows-setup.md](docs/windows-setup.md) for
|
|||||||
|
|
||||||
git clone https://github.com/lancatlin/linux-odyssey.git
|
git clone https://github.com/lancatlin/linux-odyssey.git
|
||||||
cd linux-odyssey
|
cd linux-odyssey
|
||||||
yarn install
|
bun install
|
||||||
|
|
||||||
## Project Structures
|
## Project Structures
|
||||||
|
|
||||||
@@ -104,7 +103,7 @@ Customized `entrypoint.sh` for each quest is not yet supported.
|
|||||||
|
|
||||||
To build all quests:
|
To build all quests:
|
||||||
|
|
||||||
yarn build:quests
|
bun run build:quests
|
||||||
|
|
||||||
The base image is built with docker-compose, so you can use `docker compose build base` to rebuild it. Once you rebuild the base image, you have to rebuild all quests.
|
The base image is built with docker-compose, so you can use `docker compose build base` to rebuild it. Once you rebuild the base image, you have to rebuild all quests.
|
||||||
|
|
||||||
@@ -134,11 +133,11 @@ Copy `.env.sample` to `.env` and customize it for your needs.
|
|||||||
|
|
||||||
Run dev server (frontend, backend):
|
Run dev server (frontend, backend):
|
||||||
|
|
||||||
yarn dev
|
bun run dev
|
||||||
|
|
||||||
To develop analytics dashboard:
|
To develop analytics dashboard:
|
||||||
|
|
||||||
yarn analytics
|
bun run analytics
|
||||||
|
|
||||||
The default username and password is `alex` and `Alex1234`.
|
The default username and password is `alex` and `Alex1234`.
|
||||||
|
|
||||||
@@ -146,12 +145,12 @@ The default username and password is `alex` and `Alex1234`.
|
|||||||
|
|
||||||
Unit tests:
|
Unit tests:
|
||||||
|
|
||||||
yarn test:unit
|
bun run test
|
||||||
|
|
||||||
Run Cypress locally, using the current development containers:
|
Run Cypress locally, using the current development containers:
|
||||||
|
|
||||||
docker compose up -d
|
docker compose up -d
|
||||||
yarn test:e2e
|
bun run test:e2e
|
||||||
|
|
||||||
Run Cypress in container, create brand-new containers along with it:
|
Run Cypress in container, create brand-new containers along with it:
|
||||||
|
|
||||||
@@ -183,7 +182,7 @@ Start the service
|
|||||||
|
|
||||||
# Server side
|
# Server side
|
||||||
docker compose -f docker-compose.prod.yml pull
|
docker compose -f docker-compose.prod.yml pull
|
||||||
yarn build:quests
|
bun run build:quests
|
||||||
docker compose -f docker-compose.prod.yml up -d
|
docker compose -f docker-compose.prod.yml up -d
|
||||||
|
|
||||||
### Necessary Configuration
|
### Necessary Configuration
|
||||||
@@ -197,4 +196,4 @@ You can copy from `.env.sample`
|
|||||||
|
|
||||||
You can generate a random key by running
|
You can generate a random key by running
|
||||||
|
|
||||||
yarn setup
|
bun run setup
|
||||||
+11
-4
@@ -1,17 +1,24 @@
|
|||||||
# Base stage
|
# Base stage
|
||||||
FROM node:20-alpine AS build
|
FROM oven/bun:1.3.14-alpine AS build
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY package.json yarn.lock ./
|
COPY package.json bun.lock ./
|
||||||
COPY app/package.json ./app/package.json
|
COPY app/package.json ./app/package.json
|
||||||
COPY server/package.json ./server/package.json
|
COPY server/package.json ./server/package.json
|
||||||
RUN yarn install --frozen-lockfile
|
COPY services/analytics/package.json ./services/analytics/package.json
|
||||||
|
COPY tests/package.json ./tests/package.json
|
||||||
|
COPY packages/constants/package.json ./packages/constants/package.json
|
||||||
|
COPY packages/file-graph/package.json ./packages/file-graph/package.json
|
||||||
|
COPY packages/game/package.json ./packages/game/package.json
|
||||||
|
COPY packages/models/package.json ./packages/models/package.json
|
||||||
|
COPY packages/utils/package.json ./packages/utils/package.json
|
||||||
|
RUN bun install --frozen-lockfile --filter app
|
||||||
|
|
||||||
COPY tsconfig.json ./tsconfig.json
|
COPY tsconfig.json ./tsconfig.json
|
||||||
COPY server ./server
|
COPY server ./server
|
||||||
COPY packages ./packages
|
COPY packages ./packages
|
||||||
COPY app ./app
|
COPY app ./app
|
||||||
RUN yarn workspace app build
|
RUN bun run --filter app build
|
||||||
|
|
||||||
# Production stage
|
# Production stage
|
||||||
FROM nginx:stable-alpine AS prod
|
FROM nginx:stable-alpine AS prod
|
||||||
|
|||||||
+5
-1
@@ -16,6 +16,10 @@
|
|||||||
"@fortawesome/free-regular-svg-icons": "^6.4.0",
|
"@fortawesome/free-regular-svg-icons": "^6.4.0",
|
||||||
"@fortawesome/free-solid-svg-icons": "^6.4.0",
|
"@fortawesome/free-solid-svg-icons": "^6.4.0",
|
||||||
"@fortawesome/vue-fontawesome": "^3.0.3",
|
"@fortawesome/vue-fontawesome": "^3.0.3",
|
||||||
|
"@odyssey/constants": "workspace:*",
|
||||||
|
"@odyssey/file-graph": "workspace:*",
|
||||||
|
"@odyssey/game": "workspace:*",
|
||||||
|
"@odyssey/utils": "workspace:*",
|
||||||
"@trpc/client": "^11.1.0",
|
"@trpc/client": "^11.1.0",
|
||||||
"@types/dompurify": "^3.0.5",
|
"@types/dompurify": "^3.0.5",
|
||||||
"axios": "^1.20.0",
|
"axios": "^1.20.0",
|
||||||
@@ -26,7 +30,7 @@
|
|||||||
"socket.io-client": "^4.7.1",
|
"socket.io-client": "^4.7.1",
|
||||||
"validator": "^13.15.22",
|
"validator": "^13.15.22",
|
||||||
"vue": "^3.3.4",
|
"vue": "^3.3.4",
|
||||||
"vue-i18n": "11.3.0",
|
"vue-i18n": "11.4.14",
|
||||||
"vue-router": "^4.2.4",
|
"vue-router": "^4.2.4",
|
||||||
"vue-toastification": "^2.0.0-rc.5",
|
"vue-toastification": "^2.0.0-rc.5",
|
||||||
"vue3-lottie": "^3.2.0",
|
"vue3-lottie": "^3.2.0",
|
||||||
|
|||||||
@@ -87,15 +87,16 @@ const handleChange = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const registerGuest = async () => {
|
// Guest account currently disabled
|
||||||
try {
|
// const registerGuest = async () => {
|
||||||
await api.post('/auth/register-guest')
|
// try {
|
||||||
router.push({ name: 'map' })
|
// await api.post('/auth/register-guest')
|
||||||
} catch (err) {
|
// router.push({ name: 'map' })
|
||||||
console.error(err)
|
// } catch (err) {
|
||||||
toast.error('Something went wrong.')
|
// console.error(err)
|
||||||
}
|
// toast.error('Something went wrong.')
|
||||||
}
|
// }
|
||||||
|
// }
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<!-- login page -->
|
<!-- login page -->
|
||||||
@@ -241,13 +242,13 @@ const registerGuest = async () => {
|
|||||||
</RouterLink></span
|
</RouterLink></span
|
||||||
>
|
>
|
||||||
</p>
|
</p>
|
||||||
<button
|
<!-- <button
|
||||||
id="register-guest"
|
id="register-guest"
|
||||||
class="rounded-lg py-2 mt-3 bg-bg-primary text-text-secondary w-full border-text-secondary border-2"
|
class="rounded-lg py-2 mt-3 bg-bg-primary text-text-secondary w-full border-text-secondary border-2"
|
||||||
@click="registerGuest()"
|
@click="registerGuest()"
|
||||||
>
|
>
|
||||||
{{ t('authform.continue_as_guest') }}
|
{{ t('authform.continue_as_guest') }}
|
||||||
</button>
|
</button> -->
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { ref, onMounted, watch, computed } from 'vue'
|
import { ref, onMounted, watch, computed } from 'vue'
|
||||||
import { useToast } from 'vue-toastification'
|
import { useToast } from 'vue-toastification'
|
||||||
import { DAG } from '../../../packages/utils'
|
import { DAG } from '@odyssey/utils'
|
||||||
import { trpc } from '../utils/trpc'
|
import { trpc } from '../utils/trpc'
|
||||||
import useUserProfile from '../store/userProfile'
|
import useUserProfile from '../store/userProfile'
|
||||||
import QuestIntro from './QuestIntro.vue'
|
import QuestIntro from './QuestIntro.vue'
|
||||||
|
|||||||
@@ -2,10 +2,10 @@
|
|||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { useToast } from 'vue-toastification'
|
import { useToast } from 'vue-toastification'
|
||||||
|
import type { QuestLocale } from '@odyssey/constants'
|
||||||
import { useSession } from '../../store/session'
|
import { useSession } from '../../store/session'
|
||||||
import { openBugReport, openQuestSurvey } from '../../utils/formbricks'
|
import { openBugReport, openQuestSurvey } from '../../utils/formbricks'
|
||||||
import { trpc } from '../../utils/trpc'
|
import { trpc } from '../../utils/trpc'
|
||||||
import type { QuestLocale } from '../../../../packages/constants'
|
|
||||||
|
|
||||||
const { t, locale } = useI18n()
|
const { t, locale } = useI18n()
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import type { QuestLocale } from '@odyssey/constants'
|
||||||
|
import { SUPPORTED_LOCALES, LOCALE_NAME_MAP } from '@odyssey/constants'
|
||||||
import { setLocale } from '../../i18n'
|
import { setLocale } from '../../i18n'
|
||||||
import type { QuestLocale } from '../../../../packages/constants'
|
|
||||||
import {
|
|
||||||
SUPPORTED_LOCALES,
|
|
||||||
LOCALE_NAME_MAP,
|
|
||||||
} from '../../../../packages/constants'
|
|
||||||
|
|
||||||
const { t, locale } = useI18n({ useScope: 'global' })
|
const { t, locale } = useI18n({ useScope: 'global' })
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { createI18n } from 'vue-i18n'
|
import { createI18n } from 'vue-i18n'
|
||||||
import type { QuestLocale } from '../../../packages/constants'
|
import type { QuestLocale } from '@odyssey/constants'
|
||||||
import { SUPPORTED_LOCALES } from '../../../packages/constants'
|
import { SUPPORTED_LOCALES } from '@odyssey/constants'
|
||||||
import en from './locales/en.json'
|
import en from './locales/en.json'
|
||||||
import zhTW from './locales/zh-TW.json'
|
import zhTW from './locales/zh-TW.json'
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import type { QuestLocale } from '../../../packages/constants'
|
import type { QuestLocale } from '@odyssey/constants'
|
||||||
import {
|
import { FileGraph, type FileGraphUpdateEvent } from '@odyssey/file-graph'
|
||||||
FileGraph,
|
import type { IResponse, ITask } from '@odyssey/game'
|
||||||
type FileGraphUpdateEvent,
|
|
||||||
} from '../../../packages/file-graph'
|
|
||||||
import type { IResponse, ITask } from '../../../packages/game'
|
|
||||||
import type { SessionDetail } from '../../../server/src/routers/sessionRouter'
|
import type { SessionDetail } from '../../../server/src/routers/sessionRouter'
|
||||||
import Socket from '../utils/socket'
|
import Socket from '../utils/socket'
|
||||||
import SocketTerminal from '../utils/terminal'
|
import SocketTerminal from '../utils/terminal'
|
||||||
|
|||||||
+2
-2
@@ -1,5 +1,5 @@
|
|||||||
import type { FileGraph } from '../../packages/file-graph'
|
import type { FileGraph } from '@odyssey/file-graph'
|
||||||
import type { IResponse, ITask } from '../../packages/game'
|
import type { IResponse, ITask } from '@odyssey/game'
|
||||||
|
|
||||||
export interface Task {
|
export interface Task {
|
||||||
id: number
|
id: number
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { isValidUsername } from '../../../packages/utils'
|
import { isValidUsername } from '@odyssey/utils'
|
||||||
import AuthForm from '../components/AuthForm.vue'
|
import AuthForm from '../components/AuthForm.vue'
|
||||||
import DynamicBackground from '../components/DynamicBackground.vue'
|
import DynamicBackground from '../components/DynamicBackground.vue'
|
||||||
import HeaderPart from '../components/header/HeaderPart.vue'
|
import HeaderPart from '../components/header/HeaderPart.vue'
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import validator from 'validator'
|
import validator from 'validator'
|
||||||
import { isValidUsername } from '../../../packages/utils'
|
import { isValidUsername } from '@odyssey/utils'
|
||||||
import { passwordPolicy } from '../../../packages/constants'
|
import { passwordPolicy } from '@odyssey/constants'
|
||||||
import AuthForm from '../components/AuthForm.vue'
|
import AuthForm from '../components/AuthForm.vue'
|
||||||
import DynamicBackground from '../components/DynamicBackground.vue'
|
import DynamicBackground from '../components/DynamicBackground.vue'
|
||||||
import HeaderPart from '../components/header/HeaderPart.vue'
|
import HeaderPart from '../components/header/HeaderPart.vue'
|
||||||
|
|||||||
@@ -1,19 +1,9 @@
|
|||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite'
|
||||||
import vue from '@vitejs/plugin-vue'
|
import vue from '@vitejs/plugin-vue'
|
||||||
import path from 'path'
|
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [vue()],
|
plugins: [vue()],
|
||||||
resolve: {
|
|
||||||
alias: {
|
|
||||||
'@game': path.resolve(__dirname, '../packages/game'),
|
|
||||||
'@models': path.resolve(__dirname, '../packages/models'),
|
|
||||||
'@utils': path.resolve(__dirname, '../packages/utils'),
|
|
||||||
'@constants': path.resolve(__dirname, '../packages/constants'),
|
|
||||||
'@file-graph': path.resolve(__dirname, '../packages/file-graph'),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
server: {
|
server: {
|
||||||
host: '0.0.0.0',
|
host: '0.0.0.0',
|
||||||
proxy: {
|
proxy: {
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
presets: [
|
|
||||||
['@babel/preset-env', { targets: { node: 'current' } }],
|
|
||||||
'@babel/preset-typescript',
|
|
||||||
],
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
[test]
|
||||||
|
coverage = true
|
||||||
|
coverageDir = "coverage"
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
services:
|
services:
|
||||||
traefik:
|
traefik:
|
||||||
image: traefik:v3.6.9
|
image: traefik:v3.6.9
|
||||||
|
restart: always
|
||||||
ports:
|
ports:
|
||||||
- 8000:8000
|
- 8000:8000
|
||||||
- 8080:8080
|
- 8080:8080
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
version: '3'
|
|
||||||
services:
|
services:
|
||||||
traefik:
|
traefik:
|
||||||
image: traefik:v3.6.9
|
image: traefik:v3.6.9
|
||||||
@@ -22,7 +21,6 @@ services:
|
|||||||
|
|
||||||
db:
|
db:
|
||||||
image: mongo:6
|
image: mongo:6
|
||||||
restart: always
|
|
||||||
networks:
|
networks:
|
||||||
- internal
|
- internal
|
||||||
ports:
|
ports:
|
||||||
|
|||||||
+9
-15
@@ -74,29 +74,23 @@ sudo apt update
|
|||||||
sudo apt install -y git curl ca-certificates build-essential
|
sudo apt install -y git curl ca-certificates build-essential
|
||||||
```
|
```
|
||||||
|
|
||||||
### Install Node.js 20
|
### Install Bun
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
|
curl -fsSL https://bun.sh/install | bash
|
||||||
sudo apt install -y nodejs
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Install Yarn 1.22
|
Restart your terminal (or run `source ~/.bashrc`) so `bun` is on your `PATH`.
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo npm install -g yarn@1.22.22
|
|
||||||
```
|
|
||||||
|
|
||||||
Verify the installations:
|
Verify the installations:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git --version
|
git --version
|
||||||
node --version
|
node --version
|
||||||
npm --version
|
bun --version
|
||||||
yarn --version
|
|
||||||
```
|
```
|
||||||
|
|
||||||
The Node.js version should be `20.x` or later, and the Yarn version should be `1.22.x`.
|
The Node.js version should be `20.x` or later, and the Bun version should be `1.3` or later.
|
||||||
|
|
||||||
## 4. Clone the Project
|
## 4. Clone the Project
|
||||||
|
|
||||||
@@ -130,10 +124,10 @@ code .
|
|||||||
From the project root, run:
|
From the project root, run:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
yarn install
|
bun install
|
||||||
```
|
```
|
||||||
|
|
||||||
Warnings during installation may be harmless. The installation succeeded if Yarn finishes with a `Done` message and does not display `error Command failed`.
|
Warnings during installation may be harmless. The installation succeeded if Bun finishes with an `installed` message and does not display an `error:` message.
|
||||||
|
|
||||||
If a `gyp` error appears, confirm that it belongs to an optional dependency before ignoring it.
|
If a `gyp` error appears, confirm that it belongs to an optional dependency before ignoring it.
|
||||||
|
|
||||||
@@ -180,7 +174,7 @@ docker compose down
|
|||||||
Start the frontend and backend development servers:
|
Start the frontend and backend development servers:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
yarn dev
|
bun run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
Keep the terminal running and open the following address in your browser:
|
Keep the terminal running and open the following address in your browser:
|
||||||
@@ -194,5 +188,5 @@ To stop the development servers, press `Ctrl+C`.
|
|||||||
You can verify that the project builds successfully by running:
|
You can verify that the project builds successfully by running:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
yarn build
|
bun run build
|
||||||
```
|
```
|
||||||
-201
@@ -1,201 +0,0 @@
|
|||||||
/**
|
|
||||||
* For a detailed explanation regarding each configuration property, visit:
|
|
||||||
* https://jestjs.io/docs/configuration
|
|
||||||
*/
|
|
||||||
|
|
||||||
import type { Config } from 'jest'
|
|
||||||
|
|
||||||
const config: Config = {
|
|
||||||
// All imported modules in your tests should be mocked automatically
|
|
||||||
// automock: false,
|
|
||||||
|
|
||||||
// Stop running tests after `n` failures
|
|
||||||
// bail: 0,
|
|
||||||
|
|
||||||
// The directory where Jest should store its cached dependency information
|
|
||||||
// cacheDirectory: "/tmp/jest_rs",
|
|
||||||
|
|
||||||
// Automatically clear mock calls, instances, contexts and results before every test
|
|
||||||
// clearMocks: false,
|
|
||||||
|
|
||||||
// Indicates whether the coverage information should be collected while executing the test
|
|
||||||
collectCoverage: true,
|
|
||||||
|
|
||||||
// An array of glob patterns indicating a set of files for which coverage information should be collected
|
|
||||||
// collectCoverageFrom: undefined,
|
|
||||||
|
|
||||||
// The directory where Jest should output its coverage files
|
|
||||||
coverageDirectory: 'coverage',
|
|
||||||
|
|
||||||
// An array of regexp pattern strings used to skip coverage collection
|
|
||||||
// coveragePathIgnorePatterns: [
|
|
||||||
// "/node_modules/"
|
|
||||||
// ],
|
|
||||||
|
|
||||||
// Indicates which provider should be used to instrument code for coverage
|
|
||||||
coverageProvider: 'v8',
|
|
||||||
|
|
||||||
// A list of reporter names that Jest uses when writing coverage reports
|
|
||||||
// coverageReporters: [
|
|
||||||
// "json",
|
|
||||||
// "text",
|
|
||||||
// "lcov",
|
|
||||||
// "clover"
|
|
||||||
// ],
|
|
||||||
|
|
||||||
// An object that configures minimum threshold enforcement for coverage results
|
|
||||||
// coverageThreshold: undefined,
|
|
||||||
|
|
||||||
// A path to a custom dependency extractor
|
|
||||||
// dependencyExtractor: undefined,
|
|
||||||
|
|
||||||
// Make calling deprecated APIs throw helpful error messages
|
|
||||||
// errorOnDeprecated: false,
|
|
||||||
|
|
||||||
// The default configuration for fake timers
|
|
||||||
// fakeTimers: {
|
|
||||||
// "enableGlobally": false
|
|
||||||
// },
|
|
||||||
|
|
||||||
// Force coverage collection from ignored files using an array of glob patterns
|
|
||||||
// forceCoverageMatch: [],
|
|
||||||
|
|
||||||
// A path to a module which exports an async function that is triggered once before all test suites
|
|
||||||
// globalSetup: undefined,
|
|
||||||
|
|
||||||
// A path to a module which exports an async function that is triggered once after all test suites
|
|
||||||
// globalTeardown: undefined,
|
|
||||||
|
|
||||||
// A set of global variables that need to be available in all test environments
|
|
||||||
// globals: {},
|
|
||||||
|
|
||||||
// The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers.
|
|
||||||
// maxWorkers: "50%",
|
|
||||||
|
|
||||||
// An array of directory names to be searched recursively up from the requiring module's location
|
|
||||||
// moduleDirectories: [
|
|
||||||
// "node_modules"
|
|
||||||
// ],
|
|
||||||
|
|
||||||
// An array of file extensions your modules use
|
|
||||||
// moduleFileExtensions: [
|
|
||||||
// "js",
|
|
||||||
// "mjs",
|
|
||||||
// "cjs",
|
|
||||||
// "jsx",
|
|
||||||
// "ts",
|
|
||||||
// "tsx",
|
|
||||||
// "json",
|
|
||||||
// "node"
|
|
||||||
// ],
|
|
||||||
|
|
||||||
// A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
|
|
||||||
moduleNameMapper: {
|
|
||||||
'^(\\.{1,2}/.*)\\.js$': '$1', // Redirect .js imports to .ts files
|
|
||||||
},
|
|
||||||
|
|
||||||
// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
|
|
||||||
// modulePathIgnorePatterns: [],
|
|
||||||
|
|
||||||
// Activates notifications for test results
|
|
||||||
// notify: false,
|
|
||||||
|
|
||||||
// An enum that specifies notification mode. Requires { notify: true }
|
|
||||||
// notifyMode: "failure-change",
|
|
||||||
|
|
||||||
// A preset that is used as a base for Jest's configuration
|
|
||||||
// preset: undefined,
|
|
||||||
|
|
||||||
// Run tests from one or more projects
|
|
||||||
// projects: undefined,
|
|
||||||
|
|
||||||
// Use this configuration option to add custom reporters to Jest
|
|
||||||
// reporters: undefined,
|
|
||||||
|
|
||||||
// Automatically reset mock state before every test
|
|
||||||
// resetMocks: false,
|
|
||||||
|
|
||||||
// Reset the module registry before running each individual test
|
|
||||||
// resetModules: false,
|
|
||||||
|
|
||||||
// A path to a custom resolver
|
|
||||||
// resolver: undefined,
|
|
||||||
|
|
||||||
// Automatically restore mock state and implementation before every test
|
|
||||||
// restoreMocks: false,
|
|
||||||
|
|
||||||
// The root directory that Jest should scan for tests and modules within
|
|
||||||
// rootDir: undefined,
|
|
||||||
|
|
||||||
// A list of paths to directories that Jest should use to search for files in
|
|
||||||
// roots: [
|
|
||||||
// "<rootDir>"
|
|
||||||
// ],
|
|
||||||
|
|
||||||
// Allows you to use a custom runner instead of Jest's default test runner
|
|
||||||
// runner: "jest-runner",
|
|
||||||
|
|
||||||
// The paths to modules that run some code to configure or set up the testing environment before each test
|
|
||||||
// setupFiles: [],
|
|
||||||
|
|
||||||
// A list of paths to modules that run some code to configure or set up the testing framework before each test
|
|
||||||
// setupFilesAfterEnv: [],
|
|
||||||
|
|
||||||
// The number of seconds after which a test is considered as slow and reported as such in the results.
|
|
||||||
// slowTestThreshold: 5,
|
|
||||||
|
|
||||||
// A list of paths to snapshot serializer modules Jest should use for snapshot testing
|
|
||||||
// snapshotSerializers: [],
|
|
||||||
|
|
||||||
// The test environment that will be used for testing
|
|
||||||
// testEnvironment: "jest-environment-node",
|
|
||||||
|
|
||||||
// Options that will be passed to the testEnvironment
|
|
||||||
// testEnvironmentOptions: {},
|
|
||||||
|
|
||||||
// Adds a location field to test results
|
|
||||||
// testLocationInResults: false,
|
|
||||||
|
|
||||||
// The glob patterns Jest uses to detect test files
|
|
||||||
// testMatch: [
|
|
||||||
// "**/__tests__/**/*.[jt]s?(x)",
|
|
||||||
// "**/?(*.)+(spec|test).[tj]s?(x)"
|
|
||||||
// ],
|
|
||||||
|
|
||||||
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
|
|
||||||
// testPathIgnorePatterns: [
|
|
||||||
// "/node_modules/"
|
|
||||||
// ],
|
|
||||||
|
|
||||||
// The regexp pattern or array of patterns that Jest uses to detect test files
|
|
||||||
// testRegex: [],
|
|
||||||
|
|
||||||
// This option allows the use of a custom results processor
|
|
||||||
// testResultsProcessor: undefined,
|
|
||||||
|
|
||||||
// This option allows use of a custom test runner
|
|
||||||
// testRunner: "jest-circus/runner",
|
|
||||||
|
|
||||||
// A map from regular expressions to paths to transformers
|
|
||||||
// transform: undefined,
|
|
||||||
|
|
||||||
// An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
|
|
||||||
// transformIgnorePatterns: [
|
|
||||||
// "/node_modules/",
|
|
||||||
// "\\.pnp\\.[^\\/]+$"
|
|
||||||
// ],
|
|
||||||
|
|
||||||
// An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
|
|
||||||
// unmockedModulePathPatterns: undefined,
|
|
||||||
|
|
||||||
// Indicates whether each individual test should be reported during the run
|
|
||||||
// verbose: undefined,
|
|
||||||
|
|
||||||
// An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
|
|
||||||
// watchPathIgnorePatterns: [],
|
|
||||||
|
|
||||||
// Whether to use watchman for file crawling
|
|
||||||
// watchman: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
export default config
|
|
||||||
+18
-25
@@ -13,31 +13,27 @@
|
|||||||
"tests"
|
"tests"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"app": "yarn workspace app dev",
|
"app": "bun run --filter app dev",
|
||||||
"build": "yarn workspace server build && yarn workspace app build && yarn workspace analytics build",
|
"build": "bun run --filter app build",
|
||||||
"dev": "concurrently -n \"App,Server\" -c \"bgBlue.bold,bgGreen.bold\" \"yarn workspace app dev\" \"yarn workspace server dev\"",
|
"dev": "concurrently -n \"App,Server\" -c \"bgBlue.bold,bgGreen.bold\" \"bun run --filter app dev\" \"bun run --filter server dev\"",
|
||||||
"server": "yarn workspace server dev",
|
"server": "bun run --filter server dev",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"format": "prettier --ignore-path .prettierignore --write app server",
|
"format": "prettier --ignore-path .prettierignore --write app server",
|
||||||
"test": "yarn node --experimental-vm-modules --no-warnings $(yarn bin jest)",
|
"test": "bun test",
|
||||||
"test:e2e": "yarn workspace tests test",
|
"test:e2e": "bun run --filter tests test",
|
||||||
"test:open": "yarn workspace tests test:open",
|
"test:open": "bun run --filter tests test:open",
|
||||||
"analytics": "yarn workspace analytics dev",
|
"analytics": "bun run --filter analytics dev",
|
||||||
"setup": "yarn workspace server setup",
|
"setup": "bun run --filter server setup",
|
||||||
"clean": "yarn workspace server clean",
|
"clean": "bun run --filter server clean",
|
||||||
"build:quests": "yarn workspace server build:quests"
|
"build:quests": "bun run --filter server build:quests",
|
||||||
|
"type-check": "bun run --filter server --filter analytics --filter app type-check"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.29.6",
|
"@types/bun": "^1.4.2",
|
||||||
"@babel/preset-env": "^7.24.0",
|
|
||||||
"@babel/preset-typescript": "^7.23.3",
|
|
||||||
"@jest/globals": "^29.7.0",
|
|
||||||
"@types/jest": "^29.5.12",
|
|
||||||
"@types/node": "^20.10.4",
|
"@types/node": "^20.10.4",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
||||||
"@typescript-eslint/parser": "^6.21.0",
|
"@typescript-eslint/parser": "^6.21.0",
|
||||||
"@vue/eslint-config-prettier": "^7.1.0",
|
"@vue/eslint-config-prettier": "^7.1.0",
|
||||||
"babel-jest": "^29.7.0",
|
|
||||||
"concurrently": "^8.2.2",
|
"concurrently": "^8.2.2",
|
||||||
"eslint": "^8.56.0",
|
"eslint": "^8.56.0",
|
||||||
"eslint-config-airbnb-base": "^15.0.0",
|
"eslint-config-airbnb-base": "^15.0.0",
|
||||||
@@ -48,16 +44,13 @@
|
|||||||
"eslint-plugin-prettier": "^5.1.3",
|
"eslint-plugin-prettier": "^5.1.3",
|
||||||
"eslint-plugin-vue": "^9.15.1",
|
"eslint-plugin-vue": "^9.15.1",
|
||||||
"globals": "^16.0.0",
|
"globals": "^16.0.0",
|
||||||
"jest": "^29.7.0",
|
|
||||||
"prettier": "^3.0.0",
|
"prettier": "^3.0.0",
|
||||||
"ts-node": "^10.9.2",
|
|
||||||
"typescript": "^5.3.3",
|
"typescript": "^5.3.3",
|
||||||
"vue-eslint-parser": "^9.4.2",
|
"vue-eslint-parser": "^9.4.2"
|
||||||
"zod-to-json-schema": "^3.24.5"
|
|
||||||
},
|
},
|
||||||
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e",
|
"packageManager": "bun@1.3.14",
|
||||||
"resolutions": {
|
"overrides": {
|
||||||
"**/@mapbox/node-pre-gyp/tar": "^7.5.19",
|
"@mapbox/node-pre-gyp/tar": "^7.5.19",
|
||||||
"**/@cypress/request/form-data": "^2.5.4"
|
"@cypress/request/form-data": "^2.5.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"name": "@odyssey/constants",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"main": "index.ts",
|
||||||
|
"types": "index.ts",
|
||||||
|
"exports": {
|
||||||
|
".": "./index.ts"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"zod": "^4.6.7"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"name": "@odyssey/file-graph",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"main": "index.ts",
|
||||||
|
"types": "index.ts",
|
||||||
|
"exports": {
|
||||||
|
".": "./index.ts"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { describe, test, expect } from '@jest/globals'
|
import { describe, test, expect, beforeEach } from 'bun:test'
|
||||||
import File from '../file.js'
|
import File from '../file.js'
|
||||||
|
|
||||||
describe('File', () => {
|
describe('File', () => {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { describe, test, expect } from '@jest/globals'
|
import { describe, test, expect, beforeEach } from 'bun:test'
|
||||||
import FileGraph from '../fileGraph.js'
|
import FileGraph from '../fileGraph.js'
|
||||||
import FileNode from '../fileNode.js'
|
import FileNode from '../fileNode.js'
|
||||||
import { FileObject } from '../file.js'
|
import { FileObject } from '../file.js'
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { describe, test, expect } from '@jest/globals'
|
import { describe, test, expect, beforeEach } from 'bun:test'
|
||||||
import { ParentNotExistsError } from '../errors.js'
|
import { ParentNotExistsError } from '../errors.js'
|
||||||
import FileNode, { IFileNode } from '../fileNode.js'
|
import FileNode, { IFileNode } from '../fileNode.js'
|
||||||
import { FileObject } from '../file.js'
|
import { FileObject } from '../file.js'
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { FileGraph, IFileNode } from '../file-graph'
|
import { FileGraph, IFileNode } from '@odyssey/file-graph'
|
||||||
import { Quest } from './Quest'
|
import { Quest } from './Quest'
|
||||||
import {
|
import {
|
||||||
IQuest,
|
IQuest,
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"name": "@odyssey/game",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"main": "index.ts",
|
||||||
|
"types": "index.ts",
|
||||||
|
"exports": {
|
||||||
|
".": "./index.ts"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@odyssey/file-graph": "workspace:*",
|
||||||
|
"zod": "^4.6.7",
|
||||||
|
"zod-to-json-schema": "^3.24.5"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { IFileNode } from '../../file-graph'
|
import { IFileNode } from '@odyssey/file-graph'
|
||||||
|
|
||||||
export * from './condition.js'
|
export * from './condition.js'
|
||||||
export * from './stage.js'
|
export * from './stage.js'
|
||||||
|
|||||||
@@ -1,28 +1,7 @@
|
|||||||
import { test, expect } from '@jest/globals'
|
import { test, expect } from 'bun:test'
|
||||||
import { IFileExistenceChecker, IFileInput } from '../schema'
|
|
||||||
import { checkFiles } from '../condition/FileMatcher.js'
|
import { checkFiles } from '../condition/FileMatcher.js'
|
||||||
import { Condition } from '../condition/Condition.js'
|
import { Condition } from '../condition/Condition.js'
|
||||||
|
import { MockFileChecker } from './mockFileChecker'
|
||||||
export class MockFileChecker implements IFileExistenceChecker {
|
|
||||||
private files: IFileInput[] = [
|
|
||||||
{
|
|
||||||
path: '/home/user/hello.txt',
|
|
||||||
type: 'file',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/home/user/world.txt',
|
|
||||||
type: 'file',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/home/user/Downloads',
|
|
||||||
type: 'directory',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
async exists(file: IFileInput): Promise<boolean> {
|
|
||||||
return this.files.some((f) => f.path === file.path && f.type === file.type)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
test('file matcher', async () => {
|
test('file matcher', async () => {
|
||||||
const checker = new MockFileChecker()
|
const checker = new MockFileChecker()
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { test, expect } from '@jest/globals'
|
import { it, test, expect } from 'bun:test'
|
||||||
import { Condition } from '../condition/Condition'
|
import { Condition } from '../condition/Condition'
|
||||||
import { OutputMatcher } from '../condition/OutputMatcher'
|
import { OutputMatcher } from '../condition/OutputMatcher'
|
||||||
import { PwdMatcher } from '../condition/PwdMatcher'
|
import { PwdMatcher } from '../condition/PwdMatcher'
|
||||||
import { CommandMatcher } from '../condition/CommandMatcher'
|
import { CommandMatcher } from '../condition/CommandMatcher'
|
||||||
import { ErrorMatcher } from '../condition/ErrorMatcher'
|
import { ErrorMatcher } from '../condition/ErrorMatcher'
|
||||||
import { conditionSchema } from '../schema'
|
import { conditionSchema } from '../schema'
|
||||||
import { MockFileChecker } from './asyncCondition.test'
|
import { MockFileChecker } from './mockFileChecker'
|
||||||
|
|
||||||
test('command match', () => {
|
test('command match', () => {
|
||||||
const matcher = new CommandMatcher('^echo start$')
|
const matcher = new CommandMatcher('^echo start$')
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { expect, it, describe } from '@jest/globals'
|
import { describe, it, expect } from 'bun:test'
|
||||||
import { IQuest } from '../schema'
|
import { IQuest } from '../schema'
|
||||||
import { GameSession } from '../Session'
|
import { GameSession } from '../Session'
|
||||||
import { MockFileChecker } from './asyncCondition.test'
|
import { MockFileChecker } from './mockFileChecker'
|
||||||
|
|
||||||
const quest: IQuest = {
|
const quest: IQuest = {
|
||||||
id: 'quest1',
|
id: 'quest1',
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import type { IFileExistenceChecker, IFileInput } from '../schema'
|
||||||
|
|
||||||
|
export class MockFileChecker implements IFileExistenceChecker {
|
||||||
|
private files: IFileInput[] = [
|
||||||
|
{
|
||||||
|
path: '/home/user/hello.txt',
|
||||||
|
type: 'file',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/home/user/world.txt',
|
||||||
|
type: 'file',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/home/user/Downloads',
|
||||||
|
type: 'directory',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
async exists(file: IFileInput): Promise<boolean> {
|
||||||
|
return this.files.some((f) => f.path === file.path && f.type === file.type)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { expect, describe, it } from '@jest/globals'
|
import { describe, it, expect } from 'bun:test'
|
||||||
import { IQuest } from '../schema'
|
import { IQuest } from '../schema'
|
||||||
import { GameSession } from '../Session'
|
import { GameSession } from '../Session'
|
||||||
import { MockFileChecker } from './asyncCondition.test'
|
import { MockFileChecker } from './mockFileChecker'
|
||||||
|
|
||||||
const quest: IQuest = {
|
const quest: IQuest = {
|
||||||
id: 'quest1',
|
id: 'quest1',
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { test, expect } from '@jest/globals'
|
import { test, expect } from 'bun:test'
|
||||||
import { Stage } from '../Stage'
|
import { Stage } from '../Stage'
|
||||||
import { MockFileChecker } from './asyncCondition.test'
|
import { MockFileChecker } from './mockFileChecker'
|
||||||
|
|
||||||
const checker = new MockFileChecker()
|
const checker = new MockFileChecker()
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
import mongoose from 'mongoose'
|
import mongoose from 'mongoose'
|
||||||
|
|
||||||
export { User, IUser } from './user.js'
|
export { User, type IUser } from './user.js'
|
||||||
export { Session, ISession } from './session.js'
|
export { Session, type ISession } from './session.js'
|
||||||
export { Command, ICommand } from './command.js'
|
export { Command, type ICommand } from './command.js'
|
||||||
export { UserProfile, IUserProfile, IProgress } from './userProfile.js'
|
export {
|
||||||
export { ILoginAttempt, LoginAttempt } from './loginAttempt.js'
|
UserProfile,
|
||||||
|
type IUserProfile,
|
||||||
|
type IProgress,
|
||||||
|
} from './userProfile.js'
|
||||||
|
export { type ILoginAttempt, LoginAttempt } from './loginAttempt.js'
|
||||||
|
|
||||||
export async function connectDB(uri: string) {
|
export async function connectDB(uri: string) {
|
||||||
const db = await mongoose.connect(uri)
|
const db = await mongoose.connect(uri)
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"name": "@odyssey/models",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"main": "index.ts",
|
||||||
|
"types": "index.ts",
|
||||||
|
"exports": {
|
||||||
|
".": "./index.ts"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@odyssey/constants": "workspace:*",
|
||||||
|
"mongoose": "^8.24.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { model, Schema, Types } from 'mongoose'
|
import { model, Schema, Types } from 'mongoose'
|
||||||
import type { QuestLocale } from '../constants'
|
import type { QuestLocale } from '@odyssey/constants'
|
||||||
import { SUPPORTED_LOCALES } from '../constants'
|
import { SUPPORTED_LOCALES } from '@odyssey/constants'
|
||||||
|
|
||||||
export interface INode {
|
export interface INode {
|
||||||
path: string
|
path: string
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"name": "@odyssey/utils",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"main": "index.ts",
|
||||||
|
"types": "index.ts",
|
||||||
|
"exports": {
|
||||||
|
".": "./index.ts"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { test, expect } from '@jest/globals'
|
import { test, expect } from 'bun:test'
|
||||||
import { isValidEmail, isValidUsername } from '../auth'
|
import { isValidEmail, isValidUsername } from '../auth'
|
||||||
|
|
||||||
test('check valid username', () => {
|
test('check valid username', () => {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { describe, it, expect } from '@jest/globals'
|
import { describe, it, expect } from 'bun:test'
|
||||||
import { buildFileCheckCmd, File } from '../commands'
|
import { buildFileCheckCmd, File } from '../commands'
|
||||||
|
|
||||||
describe('buildFileCheckCmd function', () => {
|
describe('buildFileCheckCmd function', () => {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { describe, it, expect } from '@jest/globals'
|
import { describe, it, expect } from 'bun:test'
|
||||||
import DAG from '../dag'
|
import DAG from '../dag'
|
||||||
|
|
||||||
describe('dag layers', () => {
|
describe('dag layers', () => {
|
||||||
|
|||||||
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
+16
-15
@@ -1,27 +1,28 @@
|
|||||||
FROM node:20-alpine AS build
|
FROM oven/bun:1.3.14-alpine AS prod
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY ./package.json yarn.lock ./
|
COPY package.json bun.lock ./
|
||||||
|
COPY app/package.json ./app/package.json
|
||||||
COPY server/package.json ./server/package.json
|
COPY server/package.json ./server/package.json
|
||||||
RUN yarn install
|
COPY services/analytics/package.json ./services/analytics/package.json
|
||||||
|
COPY tests/package.json ./tests/package.json
|
||||||
|
COPY packages/constants/package.json ./packages/constants/package.json
|
||||||
|
COPY packages/file-graph/package.json ./packages/file-graph/package.json
|
||||||
|
COPY packages/game/package.json ./packages/game/package.json
|
||||||
|
COPY packages/models/package.json ./packages/models/package.json
|
||||||
|
COPY packages/utils/package.json ./packages/utils/package.json
|
||||||
|
RUN bun install --frozen-lockfile --production --filter server
|
||||||
|
|
||||||
# Install docker cli
|
# Install docker cli
|
||||||
COPY server ./server
|
|
||||||
COPY packages ./packages
|
|
||||||
COPY tsconfig.json ./tsconfig.json
|
|
||||||
RUN yarn workspace server build
|
|
||||||
|
|
||||||
# Production
|
|
||||||
FROM node:20-alpine AS prod
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
COPY --from=docker:dind /usr/local/bin/docker /usr/local/bin/
|
COPY --from=docker:dind /usr/local/bin/docker /usr/local/bin/
|
||||||
COPY --from=build /app/node_modules ./node_modules
|
COPY tsconfig.json ./tsconfig.json
|
||||||
COPY --from=build /app/server ./server
|
COPY packages ./packages
|
||||||
|
COPY server ./server
|
||||||
COPY ./quests ./quests
|
COPY ./quests ./quests
|
||||||
|
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
WORKDIR /app/server
|
WORKDIR /app/server
|
||||||
CMD [ "yarn", "start" ]
|
CMD [ "bun", "src/index.ts" ]
|
||||||
|
|
||||||
ENV HOST=0.0.0.0
|
ENV HOST=0.0.0.0
|
||||||
ENV PORT=3000
|
ENV PORT=3000
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { connectDB, User } from '../../packages/models'
|
import { connectDB, User } from '@odyssey/models'
|
||||||
|
|
||||||
async function migrate() {
|
async function migrate() {
|
||||||
await connectDB(
|
await connectDB(
|
||||||
|
|||||||
+12
-17
@@ -3,15 +3,18 @@
|
|||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"description": "Linux Odyssey Server",
|
"description": "Linux Odyssey Server",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node dist/server/src/index.js",
|
"start": "bun src/index.ts",
|
||||||
"setup": "tsx src/cli/setup.ts",
|
"setup": "bun src/cli/setup.ts",
|
||||||
"clean": "tsx src/cli/clean.ts",
|
"clean": "bun src/cli/clean.ts",
|
||||||
"build": "tsc",
|
|
||||||
"type-check": "tsc --noEmit",
|
"type-check": "tsc --noEmit",
|
||||||
"dev": "tsx watch --include \"../quests/**/*.yml\" src/index.ts",
|
"dev": "bun scripts/dev.ts",
|
||||||
"build:quests": "tsx src/cli/build.ts"
|
"build:quests": "bun src/cli/build.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@odyssey/constants": "workspace:*",
|
||||||
|
"@odyssey/game": "workspace:*",
|
||||||
|
"@odyssey/models": "workspace:*",
|
||||||
|
"@odyssey/utils": "workspace:*",
|
||||||
"@trpc/server": "^11.8.0",
|
"@trpc/server": "^11.8.0",
|
||||||
"bcrypt": "^5.1.1",
|
"bcrypt": "^5.1.1",
|
||||||
"commander": "^11.0.0",
|
"commander": "^11.0.0",
|
||||||
@@ -35,21 +38,19 @@
|
|||||||
"passport-google-oidc": "^0.1.0",
|
"passport-google-oidc": "^0.1.0",
|
||||||
"passport-jwt": "^4.0.1",
|
"passport-jwt": "^4.0.1",
|
||||||
"passport-local": "^1.0.0",
|
"passport-local": "^1.0.0",
|
||||||
|
"rate-limiter-flexible": "^11.2.3",
|
||||||
"socket.io": "^4.7.1",
|
"socket.io": "^4.7.1",
|
||||||
"ssh2": "^1.16.0",
|
|
||||||
"swagger-ui-express": "^5.0.0",
|
"swagger-ui-express": "^5.0.0",
|
||||||
"tsx": "^4.19.3",
|
|
||||||
"validator": "^13.15.22",
|
"validator": "^13.15.22",
|
||||||
"winston": "^3.11.0",
|
"winston": "^3.11.0",
|
||||||
"yaml": "^2.8.3",
|
"yaml": "^2.8.3",
|
||||||
"zod": "^3.24.2"
|
"zod": "^4.6.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@swc/core": "^1.4.2",
|
|
||||||
"@swc/helpers": "^0.5.6",
|
|
||||||
"@types/bcrypt": "^5.0.2",
|
"@types/bcrypt": "^5.0.2",
|
||||||
"@types/dockerode": "^3.3.23",
|
"@types/dockerode": "^3.3.23",
|
||||||
"@types/express": "^4.17.21",
|
"@types/express": "^4.17.21",
|
||||||
|
"@types/express-serve-static-core": "^5.1.3",
|
||||||
"@types/express-session": "^1.17.10",
|
"@types/express-session": "^1.17.10",
|
||||||
"@types/jsonwebtoken": "^9.0.5",
|
"@types/jsonwebtoken": "^9.0.5",
|
||||||
"@types/minimist": "^1.2.5",
|
"@types/minimist": "^1.2.5",
|
||||||
@@ -59,14 +60,8 @@
|
|||||||
"@types/passport-jwt": "^4.0.1",
|
"@types/passport-jwt": "^4.0.1",
|
||||||
"@types/passport-local": "^1.0.38",
|
"@types/passport-local": "^1.0.38",
|
||||||
"@types/passport-oauth2": "^1.4.15",
|
"@types/passport-oauth2": "^1.4.15",
|
||||||
"@types/source-map-support": "^0",
|
|
||||||
"@types/ssh2": "^1.15.1",
|
|
||||||
"@types/swagger-ui-express": "^4.1.6",
|
"@types/swagger-ui-express": "^4.1.6",
|
||||||
"@types/validator": "^13.11.9",
|
"@types/validator": "^13.11.9",
|
||||||
"concurrently": "^8.2.2",
|
|
||||||
"jest": "^29.6.1",
|
|
||||||
"regenerator-runtime": "^0.14.1",
|
|
||||||
"source-map-support": "^0.5.21",
|
|
||||||
"typescript": "^5.3.3"
|
"typescript": "^5.3.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
// Dev server with auto-restart. `bun --watch` only tracks imported modules,
|
||||||
|
// but quests are read from YAML at startup, so rewrite the entry file (same
|
||||||
|
// contents) whenever a quest file changes to trigger a restart. Bun ignores
|
||||||
|
// timestamp-only changes, so `touch` would not work.
|
||||||
|
import { watch, readFileSync, writeFileSync } from 'node:fs'
|
||||||
|
import path from 'node:path'
|
||||||
|
|
||||||
|
const entry = path.join(import.meta.dir, '../src/index.ts')
|
||||||
|
const questsDir = path.join(import.meta.dir, '../../quests')
|
||||||
|
|
||||||
|
const server = Bun.spawn(['bun', '--watch', entry], {
|
||||||
|
stdio: ['inherit', 'inherit', 'inherit'],
|
||||||
|
})
|
||||||
|
|
||||||
|
let timer: ReturnType<typeof setTimeout> | undefined
|
||||||
|
watch(questsDir, { recursive: true }, (_event, filename) => {
|
||||||
|
if (!filename?.endsWith('.yml')) return
|
||||||
|
clearTimeout(timer)
|
||||||
|
timer = setTimeout(() => {
|
||||||
|
writeFileSync(entry, readFileSync(entry))
|
||||||
|
}, 100)
|
||||||
|
})
|
||||||
|
|
||||||
|
const stop = () => server.kill()
|
||||||
|
process.on('SIGINT', stop)
|
||||||
|
process.on('SIGTERM', stop)
|
||||||
|
process.exit(await server.exited)
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { Request, Response } from 'express'
|
import type { Request, Response } from 'express'
|
||||||
import { GameSession, commandSchema } from '../../../../packages/game'
|
import { GameSession, commandSchema } from '@odyssey/game'
|
||||||
import { Command, Session } from '../../../../packages/models'
|
import { Command, Session } from '@odyssey/models'
|
||||||
import { pushToSession } from '../socket'
|
import { pushToSession } from '../socket'
|
||||||
import { finishSession } from '../../models/sessionManager'
|
import { finishSession } from '../../models/sessionManager'
|
||||||
import { asyncHandler } from '../../middleware/error'
|
import { asyncHandler } from '../../middleware/error'
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Router, Request, Response, Express } from 'express'
|
import { Router, Request, Response, Express } from 'express'
|
||||||
import passport from 'passport'
|
import passport from 'passport'
|
||||||
import { NextFunction } from 'express-serve-static-core'
|
import { NextFunction } from 'express-serve-static-core'
|
||||||
import { LoginAttempt } from '../../../../packages/models'
|
import { LoginAttempt } from '@odyssey/models'
|
||||||
import enabledMethods from '../../auth/passport.js'
|
import enabledMethods from '../../auth/passport.js'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -73,7 +73,7 @@ async function recordLogin(req: Request, success: boolean, message: string) {
|
|||||||
|
|
||||||
router.post('/register', authenticateRateLimit, registerValidators, register)
|
router.post('/register', authenticateRateLimit, registerValidators, register)
|
||||||
|
|
||||||
router.post('/register-guest', authenticateRateLimit, registerGuest)
|
// router.post('/register-guest', authenticateRateLimit, registerGuest)
|
||||||
|
|
||||||
router.post('/logout', logout)
|
router.post('/logout', logout)
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import { Router } from 'express'
|
import { Router } from 'express'
|
||||||
import passport from 'passport'
|
|
||||||
import commands from './commandRouter.js'
|
import commands from './commandRouter.js'
|
||||||
import authRouter from './authRouter.js'
|
import authRouter from './authRouter.js'
|
||||||
import authRequired from '../../middleware/authRequired.js'
|
|
||||||
import config from '../../config.js'
|
import config from '../../config.js'
|
||||||
import leaderboardController from '../controllers/leaderboardController.js'
|
import leaderboardController from '../controllers/leaderboardController.js'
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Server } from 'socket.io'
|
|||||||
import type { Socket } from 'socket.io'
|
import type { Socket } from 'socket.io'
|
||||||
import validator from 'validator'
|
import validator from 'validator'
|
||||||
import type { Server as HttpServer } from 'http'
|
import type { Server as HttpServer } from 'http'
|
||||||
import { Session } from '../../../packages/models'
|
import { Session } from '@odyssey/models'
|
||||||
import SessionMiddleware from '../middleware/session.js'
|
import SessionMiddleware from '../middleware/session.js'
|
||||||
import logger from '../utils/logger.js'
|
import logger from '../utils/logger.js'
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { check } from 'express-validator'
|
import { check } from 'express-validator'
|
||||||
|
|
||||||
import { isValidUsername, isValidEmail } from '../../../../packages/utils'
|
import { isValidUsername, isValidEmail } from '@odyssey/utils'
|
||||||
import { User } from '../../../../packages/models'
|
import { User } from '@odyssey/models'
|
||||||
import { passwordPolicy } from '../../../../packages/constants'
|
import { passwordPolicy } from '@odyssey/constants'
|
||||||
import { noError } from '../../middleware/validator.js'
|
import { noError } from '../../middleware/validator.js'
|
||||||
|
|
||||||
const checkUsername = () =>
|
const checkUsername = () =>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { Profile } from 'passport'
|
import type { Profile } from 'passport'
|
||||||
import type { VerifyCallback } from 'passport-oauth2'
|
import type { VerifyCallback } from 'passport-oauth2'
|
||||||
import { User } from '../../../packages/models'
|
import { User } from '@odyssey/models'
|
||||||
import logger from '../utils/logger.js'
|
import logger from '../utils/logger.js'
|
||||||
|
|
||||||
export default async function oauthVerify(
|
export default async function oauthVerify(
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import passport from 'passport'
|
import passport from 'passport'
|
||||||
import type { Express } from 'express'
|
import type { Express } from 'express'
|
||||||
import { UserProfile } from '../../../packages/models'
|
import { UserProfile } from '@odyssey/models'
|
||||||
import passwordStrategy from './passwordStrategy.js'
|
import passwordStrategy from './passwordStrategy.js'
|
||||||
import jwtStrategy from './jwtStrategy.js'
|
import jwtStrategy from './jwtStrategy.js'
|
||||||
import googleStrategy from './googleStrategy.js'
|
import googleStrategy from './googleStrategy.js'
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import bcrypt from 'bcrypt'
|
|||||||
import { Strategy as LocalStrategy } from 'passport-local'
|
import { Strategy as LocalStrategy } from 'passport-local'
|
||||||
import type { IVerifyOptions } from 'passport-local'
|
import type { IVerifyOptions } from 'passport-local'
|
||||||
import type { Express } from 'express'
|
import type { Express } from 'express'
|
||||||
import { User } from '../../../packages/models'
|
import { User } from '@odyssey/models'
|
||||||
|
|
||||||
async function verifyPassword(
|
async function verifyPassword(
|
||||||
username: string,
|
username: string,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { connectDB } from '../../../packages/models'
|
import { connectDB } from '@odyssey/models'
|
||||||
import { removeExpired } from '../containers/expiryChecker'
|
import { removeExpired } from '../containers/expiryChecker'
|
||||||
import config from '../config'
|
import config from '../config'
|
||||||
import logger from '../utils/logger'
|
import logger from '../utils/logger'
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ function createConfig() {
|
|||||||
|
|
||||||
function getProjectRoot(): string {
|
function getProjectRoot(): string {
|
||||||
// Start from the current file's directory
|
// Start from the current file's directory
|
||||||
let currentDir = __dirname
|
let currentDir = import.meta.dir
|
||||||
|
|
||||||
// Keep going up until we find the root package.json with workspaces
|
// Keep going up until we find the root package.json with workspaces
|
||||||
while (currentDir !== path.parse(currentDir).root) {
|
while (currentDir !== path.parse(currentDir).root) {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { execFile } from 'child_process'
|
import { execFile } from 'child_process'
|
||||||
|
|
||||||
import { buildFileCheckCmd } from '../../../packages/utils'
|
import { buildFileCheckCmd } from '@odyssey/utils'
|
||||||
import type { IFileExistenceChecker, IFileInput } from '../../../packages/game'
|
import type { IFileExistenceChecker, IFileInput } from '@odyssey/game'
|
||||||
|
|
||||||
function sanitizeId(id: string) {
|
function sanitizeId(id: string) {
|
||||||
return id.replace(/[^a-zA-Z0-9]/g, '')
|
return id.replace(/[^a-zA-Z0-9]/g, '')
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import fs from 'fs/promises'
|
import fs from 'fs/promises'
|
||||||
import Docker from 'dockerode'
|
import Docker from 'dockerode'
|
||||||
import type { QuestLocale } from '../../../packages/constants'
|
import type { QuestLocale } from '@odyssey/constants'
|
||||||
import config, { getQuestImage } from '../config.js'
|
import config, { getQuestImage } from '../config.js'
|
||||||
import logger from '../utils/logger.js'
|
import logger from '../utils/logger.js'
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import schedule from 'node-schedule'
|
import schedule from 'node-schedule'
|
||||||
import type { HydratedDocument } from 'mongoose'
|
import type { HydratedDocument } from 'mongoose'
|
||||||
import { Session } from '../../../packages/models'
|
import { Session } from '@odyssey/models'
|
||||||
import type { ISession } from '../../../packages/models'
|
import type { ISession } from '@odyssey/models'
|
||||||
import config from '../config.js'
|
import config from '../config.js'
|
||||||
import { deleteContainer } from './docker.js'
|
import { deleteContainer } from './docker.js'
|
||||||
import logger from '../utils/logger.js'
|
import logger from '../utils/logger.js'
|
||||||
|
|||||||
+2
-2
@@ -5,7 +5,7 @@ import YAML from 'yaml'
|
|||||||
import swaggerUI from 'swagger-ui-express'
|
import swaggerUI from 'swagger-ui-express'
|
||||||
import passport from 'passport'
|
import passport from 'passport'
|
||||||
import * as trpcExpress from '@trpc/server/adapters/express'
|
import * as trpcExpress from '@trpc/server/adapters/express'
|
||||||
import { connectDB } from '../../packages/models'
|
import { connectDB } from '@odyssey/models'
|
||||||
|
|
||||||
import './auth/passport.js'
|
import './auth/passport.js'
|
||||||
import socketServer from './api/socket.js'
|
import socketServer from './api/socket.js'
|
||||||
@@ -24,7 +24,7 @@ import { createContext } from './trpc.js'
|
|||||||
async function main() {
|
async function main() {
|
||||||
if (!config.secret) {
|
if (!config.secret) {
|
||||||
logger.error(
|
logger.error(
|
||||||
'No SECRET_KEY found in .env! To set up a persistent key, please run `yarn setup`:'
|
'No SECRET_KEY found in .env! To set up a persistent key, please run `bun run setup`:'
|
||||||
)
|
)
|
||||||
process.exit(1)
|
process.exit(1)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
import rateLimit from 'express-rate-limit'
|
import rateLimit from 'express-rate-limit'
|
||||||
import type { Options } from 'express-rate-limit'
|
import type { Options } from 'express-rate-limit'
|
||||||
import type { Request, Response, NextFunction } from 'express'
|
import type { Request, Response, NextFunction } from 'express'
|
||||||
|
import { RateLimiterMemory } from 'rate-limiter-flexible'
|
||||||
|
import { TRPCError } from '@trpc/server'
|
||||||
|
import { LoginAttempt } from '@odyssey/models'
|
||||||
import config from '../config.js'
|
import config from '../config.js'
|
||||||
import { LoginAttempt } from '../../../packages/models'
|
|
||||||
import logger from '../utils/logger.js'
|
import logger from '../utils/logger.js'
|
||||||
|
import { middleware } from '../trpc.js'
|
||||||
|
|
||||||
function createRateLimiter(options: Partial<Options>) {
|
function createRateLimiter(options: Partial<Options>) {
|
||||||
if (config.testing.enabled) {
|
if (config.testing.enabled) {
|
||||||
@@ -22,13 +25,32 @@ export const authenticateRateLimit = createRateLimiter({
|
|||||||
max: 5,
|
max: 5,
|
||||||
})
|
})
|
||||||
|
|
||||||
export const sessionRateLimit = createRateLimiter({
|
export function trpcRateLimit({
|
||||||
windowMs: 10 * 1000,
|
points,
|
||||||
max: 1,
|
duration,
|
||||||
keyGenerator: (req: Request) => {
|
}: {
|
||||||
return req.user ? (req.user as any)._id : req.ip || ''
|
points: number
|
||||||
},
|
duration: number
|
||||||
})
|
}) {
|
||||||
|
const limiter = new RateLimiterMemory({ points, duration })
|
||||||
|
return middleware(async (opts) => {
|
||||||
|
if (config.testing.enabled) {
|
||||||
|
return opts.next()
|
||||||
|
}
|
||||||
|
const key = opts.ctx.user?.id ?? opts.ctx.ip ?? ''
|
||||||
|
try {
|
||||||
|
await limiter.consume(key)
|
||||||
|
} catch {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: 'TOO_MANY_REQUESTS',
|
||||||
|
message: 'Too many requests. Please try again later.',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return opts.next()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const sessionRateLimit = trpcRateLimit({ points: 3, duration: 60 })
|
||||||
|
|
||||||
export async function rateLimitByAccount(
|
export async function rateLimitByAccount(
|
||||||
req: Request,
|
req: Request,
|
||||||
|
|||||||
@@ -3,13 +3,9 @@ import fs from 'fs/promises'
|
|||||||
import path from 'path'
|
import path from 'path'
|
||||||
import yaml from 'yaml'
|
import yaml from 'yaml'
|
||||||
|
|
||||||
import type { QuestLocale } from '../../../packages/constants'
|
import type { QuestLocale } from '@odyssey/constants'
|
||||||
import { SUPPORTED_LOCALES } from '../../../packages/constants'
|
import { SUPPORTED_LOCALES } from '@odyssey/constants'
|
||||||
import {
|
import { questSchema, globalExceptionSchema, IQuest } from '@odyssey/game'
|
||||||
questSchema,
|
|
||||||
globalExceptionSchema,
|
|
||||||
IQuest,
|
|
||||||
} from '../../../packages/game'
|
|
||||||
import logger from '../utils/logger.js'
|
import logger from '../utils/logger.js'
|
||||||
import config from '../config.js'
|
import config from '../config.js'
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { HydratedDocument } from 'mongoose'
|
import { HydratedDocument } from 'mongoose'
|
||||||
import type { QuestLocale } from '../../../packages/constants'
|
import type { QuestLocale } from '@odyssey/constants'
|
||||||
import { LOCALE_NAME_MAP } from '../../../packages/constants'
|
import { LOCALE_NAME_MAP } from '@odyssey/constants'
|
||||||
import { Session, UserProfile } from '../../../packages/models'
|
import { Session, UserProfile } from '@odyssey/models'
|
||||||
import type { ISession, IUser } from '../../../packages/models'
|
import type { ISession, IUser } from '@odyssey/models'
|
||||||
import {
|
import {
|
||||||
createContainer,
|
createContainer,
|
||||||
deleteContainer,
|
deleteContainer,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Express } from 'express'
|
import { Express } from 'express'
|
||||||
import { format } from 'date-fns'
|
import { format } from 'date-fns'
|
||||||
import { User, UserProfile } from '../../../packages/models'
|
import { User, UserProfile } from '@odyssey/models'
|
||||||
import { hashPassword } from '../utils/auth.js'
|
import { hashPassword } from '../utils/auth.js'
|
||||||
|
|
||||||
export async function createUser(
|
export async function createUser(
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { TRPCError } from '@trpc/server'
|
import { TRPCError } from '@trpc/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
|
import { SUPPORTED_LOCALES } from '@odyssey/constants'
|
||||||
import { router, publicProcedure } from '../trpc.js'
|
import { router, publicProcedure } from '../trpc.js'
|
||||||
import { questManager } from '../models/quest.js'
|
import { questManager } from '../models/quest.js'
|
||||||
import { SUPPORTED_LOCALES } from '../../../packages/constants'
|
|
||||||
|
|
||||||
export const questRouter = router({
|
export const questRouter = router({
|
||||||
getQuests: publicProcedure
|
getQuests: publicProcedure
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import { TRPCError } from '@trpc/server'
|
import { TRPCError } from '@trpc/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { SUPPORTED_LOCALES } from '../../../packages/constants'
|
import { SUPPORTED_LOCALES } from '@odyssey/constants'
|
||||||
import { ISession, Session } from '../../../packages/models'
|
import { ISession, Session } from '@odyssey/models'
|
||||||
import { GameSession, VoidFileExistenceChecker } from '../../../packages/game'
|
import { GameSession, VoidFileExistenceChecker } from '@odyssey/game'
|
||||||
import { createNewSession, isQuestUnlocked } from '../models/sessionManager.js'
|
import { createNewSession, isQuestUnlocked } from '../models/sessionManager.js'
|
||||||
import config from '../config.js'
|
import config from '../config.js'
|
||||||
import { protectedProcedure, router } from '../trpc.js'
|
import { protectedProcedure, router } from '../trpc.js'
|
||||||
import { questManager } from '../models/quest.js'
|
import { questManager } from '../models/quest.js'
|
||||||
import { genJWT } from '../utils/auth'
|
import { genJWT } from '../utils/auth'
|
||||||
|
import { sessionRateLimit } from '../middleware/rateLimiter.js'
|
||||||
|
|
||||||
export type SessionDetail = Awaited<ReturnType<typeof sessionDetail>>
|
export type SessionDetail = Awaited<ReturnType<typeof sessionDetail>>
|
||||||
|
|
||||||
@@ -47,6 +48,7 @@ async function sessionDetail(session: ISession) {
|
|||||||
|
|
||||||
export const sessionRouter = router({
|
export const sessionRouter = router({
|
||||||
createSession: protectedProcedure
|
createSession: protectedProcedure
|
||||||
|
.use(sessionRateLimit)
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
questId: z.string(),
|
questId: z.string(),
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { TRPCError } from '@trpc/server'
|
import { TRPCError } from '@trpc/server'
|
||||||
import { UserProfile } from '../../../packages/models'
|
import { UserProfile } from '@odyssey/models'
|
||||||
import { protectedProcedure, router } from '../trpc.js'
|
import { protectedProcedure, router } from '../trpc.js'
|
||||||
|
|
||||||
export const userRouter = router({
|
export const userRouter = router({
|
||||||
|
|||||||
+1
-1
@@ -18,7 +18,7 @@ const t = initTRPC.context<AppContext>().create()
|
|||||||
* Export reusable router and procedure helpers
|
* Export reusable router and procedure helpers
|
||||||
* that can be used throughout the router
|
* that can be used throughout the router
|
||||||
*/
|
*/
|
||||||
export const { router, procedure: publicProcedure } = t
|
export const { router, procedure: publicProcedure, middleware } = t
|
||||||
|
|
||||||
export const protectedProcedure = publicProcedure.use(async (opts) => {
|
export const protectedProcedure = publicProcedure.use(async (opts) => {
|
||||||
if (opts.ctx.isAuthenticated() && !('newUser' in opts.ctx.user)) {
|
if (opts.ctx.isAuthenticated() && !('newUser' in opts.ctx.user)) {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { User } from '../../../packages/models'
|
import { User } from '@odyssey/models'
|
||||||
import config from '../config.js'
|
import config from '../config.js'
|
||||||
import { createUser } from '../models/userManager.js'
|
import { createUser } from '../models/userManager.js'
|
||||||
import logger from './logger.js'
|
import logger from './logger.js'
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
{
|
{
|
||||||
"extends": "../tsconfig.json",
|
"extends": "../tsconfig.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"module": "NodeNext",
|
"module": "Preserve",
|
||||||
"moduleResolution": "nodenext",
|
"moduleResolution": "bundler",
|
||||||
"esModuleInterop": true,
|
"target": "ESNext",
|
||||||
"outDir": "./dist"
|
"types": ["bun", "node"],
|
||||||
|
"noEmit": true
|
||||||
},
|
},
|
||||||
"include": ["src"],
|
"include": ["src", "scripts", "migrations"],
|
||||||
"exclude": ["node_modules", "src/*.test.js", "dist"],
|
"exclude": ["node_modules", "src/*.test.js"]
|
||||||
"ts-node": {
|
|
||||||
"swc": true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,27 +1,26 @@
|
|||||||
FROM node:20-alpine AS build
|
FROM oven/bun:1.3.14-alpine AS prod
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY ./package.json yarn.lock ./
|
COPY package.json bun.lock ./
|
||||||
|
COPY app/package.json ./app/package.json
|
||||||
|
COPY server/package.json ./server/package.json
|
||||||
COPY services/analytics/package.json ./services/analytics/package.json
|
COPY services/analytics/package.json ./services/analytics/package.json
|
||||||
RUN yarn install --frozen-lockfile
|
COPY tests/package.json ./tests/package.json
|
||||||
|
COPY packages/constants/package.json ./packages/constants/package.json
|
||||||
|
COPY packages/file-graph/package.json ./packages/file-graph/package.json
|
||||||
|
COPY packages/game/package.json ./packages/game/package.json
|
||||||
|
COPY packages/models/package.json ./packages/models/package.json
|
||||||
|
COPY packages/utils/package.json ./packages/utils/package.json
|
||||||
|
RUN bun install --frozen-lockfile --production --filter analytics
|
||||||
|
|
||||||
COPY tsconfig.json ./tsconfig.json
|
COPY tsconfig.json ./tsconfig.json
|
||||||
COPY packages ./packages
|
COPY packages ./packages
|
||||||
COPY services/analytics ./services/analytics
|
COPY services/analytics ./services/analytics
|
||||||
|
|
||||||
RUN yarn workspace analytics build
|
|
||||||
|
|
||||||
# Production stage
|
|
||||||
FROM node:20-alpine AS prod
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
COPY --from=build /app/node_modules ./node_modules
|
|
||||||
COPY --from=build /app/services/analytics ./services/analytics
|
|
||||||
|
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
ENV HOST=0.0.0.0
|
ENV HOST=0.0.0.0
|
||||||
ENV PORT=3001
|
ENV PORT=3001
|
||||||
EXPOSE 3001
|
EXPOSE 3001
|
||||||
|
|
||||||
WORKDIR /app/services/analytics
|
WORKDIR /app/services/analytics
|
||||||
CMD [ "yarn", "start" ]
|
CMD [ "bun", "src/index.ts" ]
|
||||||
@@ -1,30 +1,25 @@
|
|||||||
{
|
{
|
||||||
"name": "analytics",
|
"name": "analytics",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"main": "src/index.js",
|
"main": "src/index.ts",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "GPL",
|
"license": "GPL",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node dist/services/analytics/src/index.js",
|
"start": "bun src/index.ts",
|
||||||
"build": "tsc",
|
|
||||||
"type-check": "tsc --noEmit",
|
"type-check": "tsc --noEmit",
|
||||||
"dev": "tsx watch src/index.ts"
|
"dev": "bun --watch src/index.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@odyssey/models": "workspace:*",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"express": "^4.20.0",
|
"express": "^4.20.0",
|
||||||
"express-basic-auth": "^1.2.1",
|
"express-basic-auth": "^1.2.1",
|
||||||
"express-handlebars": "^7.1.2",
|
"express-handlebars": "^7.1.2",
|
||||||
"mongoose": "^8.24.1",
|
"mongoose": "^8.24.1",
|
||||||
"zod": "^3.24.2"
|
"zod": "^4.6.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@swc/core": "^1.4.2",
|
|
||||||
"@swc/helpers": "^0.5.6",
|
|
||||||
"@types/express": "^4.17.21",
|
"@types/express": "^4.17.21",
|
||||||
"@types/node": "^20.11.20",
|
|
||||||
"regenerator-runtime": "^0.14.1",
|
|
||||||
"tsx": "^4.19.3",
|
|
||||||
"typescript": "^5.3.3"
|
"typescript": "^5.3.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import express from 'express'
|
import express from 'express'
|
||||||
import { engine } from 'express-handlebars'
|
import { engine } from 'express-handlebars'
|
||||||
import basicAuth from 'express-basic-auth'
|
import basicAuth from 'express-basic-auth'
|
||||||
import { connectDB } from '../../../packages/models'
|
import { connectDB } from '@odyssey/models'
|
||||||
import config from './config'
|
import config from './config'
|
||||||
import router from './router'
|
import router from './router'
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Command } from '../../../../packages/models'
|
import { Command } from '@odyssey/models'
|
||||||
import Pagination from './pagination'
|
import Pagination from './pagination'
|
||||||
|
|
||||||
// eslint-disable-next-line import/prefer-default-export
|
// eslint-disable-next-line import/prefer-default-export
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { UserProfile } from '../../../../packages/models'
|
import { UserProfile } from '@odyssey/models'
|
||||||
|
|
||||||
export interface QuestProgress {
|
export interface QuestProgress {
|
||||||
_id: string
|
_id: string
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Command, Session } from '../../../../packages/models'
|
import { Command, Session } from '@odyssey/models'
|
||||||
import Pagination from './pagination'
|
import Pagination from './pagination'
|
||||||
import {
|
import {
|
||||||
CommandObject,
|
CommandObject,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { HydratedDocument, Types } from 'mongoose'
|
import { HydratedDocument, Types } from 'mongoose'
|
||||||
import { Session, User, IUser, ISession } from '../../../../packages/models'
|
import { Session, User, IUser, ISession } from '@odyssey/models'
|
||||||
import Pagination from './pagination'
|
import Pagination from './pagination'
|
||||||
import { UserSessionDetail } from '../interface'
|
import { UserSessionDetail } from '../interface'
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { describe, it, expect } from 'bun:test'
|
||||||
import mongoose from 'mongoose'
|
import mongoose from 'mongoose'
|
||||||
import Pagination, { ASC, DESC } from '../src/models/pagination.js'
|
import Pagination, { ASC, DESC } from '../src/models/pagination.js'
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
{
|
{
|
||||||
"extends": "../../tsconfig.json",
|
"extends": "../../tsconfig.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"outDir": "./dist"
|
"module": "Preserve",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"target": "ESNext",
|
||||||
|
"types": ["bun", "node"],
|
||||||
|
"noEmit": true
|
||||||
},
|
},
|
||||||
"include": ["src", "../models"],
|
"include": ["src"]
|
||||||
"ts-node": {
|
|
||||||
"swc": true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
+13
-3
@@ -1,12 +1,22 @@
|
|||||||
FROM cypress/browsers
|
FROM cypress/browsers
|
||||||
|
|
||||||
|
COPY --from=oven/bun:1.3.14 /usr/local/bin/bun /usr/local/bin/bun
|
||||||
|
|
||||||
WORKDIR /e2e
|
WORKDIR /e2e
|
||||||
COPY package.json yarn.lock ./
|
COPY package.json bun.lock ./
|
||||||
|
COPY app/package.json ./app/package.json
|
||||||
|
COPY server/package.json ./server/package.json
|
||||||
|
COPY services/analytics/package.json ./services/analytics/package.json
|
||||||
COPY tests/package.json ./tests/package.json
|
COPY tests/package.json ./tests/package.json
|
||||||
RUN yarn install --frozen-lockfile
|
COPY packages/constants/package.json ./packages/constants/package.json
|
||||||
|
COPY packages/file-graph/package.json ./packages/file-graph/package.json
|
||||||
|
COPY packages/game/package.json ./packages/game/package.json
|
||||||
|
COPY packages/models/package.json ./packages/models/package.json
|
||||||
|
COPY packages/utils/package.json ./packages/utils/package.json
|
||||||
|
RUN bun install --frozen-lockfile
|
||||||
|
|
||||||
COPY quests ./quests
|
COPY quests ./quests
|
||||||
COPY tests ./tests
|
COPY tests ./tests
|
||||||
WORKDIR /e2e/tests
|
WORKDIR /e2e/tests
|
||||||
|
|
||||||
CMD [ "yarn", "test" ]
|
CMD [ "bun", "run", "test" ]
|
||||||
+2
-2
@@ -6,8 +6,8 @@
|
|||||||
"type": "commonjs",
|
"type": "commonjs",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "yarn cypress run",
|
"test": "cypress run",
|
||||||
"test:open": "yarn cypress open"
|
"test:open": "cypress open"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@testing-library/cypress": "^10.0.1",
|
"@testing-library/cypress": "^10.0.1",
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
|
"isolatedModules": true,
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"baseUrl": "."
|
"baseUrl": "."
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in new issue
Block a user