mirror of
https://github.com/linux-odyssey/linux-odyssey.git
synced 2026-11-05 19:20:12 +08:00
Merge branch 'main' into feat-automation-test
This commit is contained in:
commit
025553f42a
123 files changed
+8922
-11964
No files matched your search
+6
-3
@@ -1,6 +1,5 @@
|
||||
FROM node:20-alpine AS build
|
||||
|
||||
RUN yarn set version berry
|
||||
WORKDIR /app
|
||||
COPY ./package.json yarn.lock tsconfig.json ./
|
||||
COPY packages/file-graph/package.json ./packages/file-graph/package.json
|
||||
@@ -27,7 +26,11 @@ COPY server ./server
|
||||
COPY quests ./quests
|
||||
|
||||
ENV NODE_ENV=production
|
||||
RUN yarn build
|
||||
RUN yarn workspace @linux-odyssey/file-graph build
|
||||
RUN yarn workspace @linux-odyssey/utils build
|
||||
RUN yarn workspace @linux-odyssey/models build
|
||||
RUN yarn workspace @linux-odyssey/constants build
|
||||
RUN yarn workspace server build
|
||||
WORKDIR /app/server
|
||||
CMD [ "yarn", "start" ]
|
||||
|
||||
@@ -39,4 +42,4 @@ COPY server/nodemon.json ./server/nodemon.json
|
||||
COPY tsconfig.json ./tsconfig.json
|
||||
COPY server/tsconfig.json ./server/tsconfig.json
|
||||
ENV NODE_ENV=development
|
||||
CMD [ "yarn", "server" ]
|
||||
CMD [ "yarn", "server" ]
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"watch": ["dist", "swagger.yaml", "../quests", "../packages"],
|
||||
"ext": "js,ts,yaml,yml",
|
||||
"exec": "node dist/index.js",
|
||||
"exec": "node -r source-map-support/register dist/index.js",
|
||||
"delay": 200
|
||||
}
|
||||
+17
-2
@@ -5,11 +5,10 @@
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "node dist/index.js",
|
||||
"dev": "nodemon",
|
||||
"setup": "node dist/cli/setup.js",
|
||||
"clean": "node dist/cli/clean.js",
|
||||
"build": "tsc",
|
||||
"watch": "concurrently \"nodemon\" \"tsc -w\"",
|
||||
"dev": "concurrently \"tsc -w\" \"sleep 5 && nodemon\"",
|
||||
"build:quests": "node dist/cli/build.js"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -20,6 +19,7 @@
|
||||
"bcrypt": "^5.1.1",
|
||||
"commander": "^11.0.0",
|
||||
"connect-mongo": "^5.0.0",
|
||||
"cookie-parser": "^1.4.6",
|
||||
"dockerode": "^3.3.5",
|
||||
"dotenv": "^16.4.1",
|
||||
"express": "^4.18.2",
|
||||
@@ -27,6 +27,7 @@
|
||||
"express-session": "^1.17.3",
|
||||
"express-validator": "^7.0.1",
|
||||
"jsonwebtoken": "^9.0.1",
|
||||
"lusca": "^1.7.0",
|
||||
"minimist": "^1.2.8",
|
||||
"mongodb": "^6.3.0",
|
||||
"mongoose": "^8.0.3",
|
||||
@@ -43,11 +44,25 @@
|
||||
"yaml": "^2.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bcrypt": "^5.0.2",
|
||||
"@types/dockerode": "^3.3.23",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/express-session": "^1.17.10",
|
||||
"@types/jsonwebtoken": "^9.0.5",
|
||||
"@types/minimist": "^1.2.5",
|
||||
"@types/node-schedule": "^2.1.6",
|
||||
"@types/passport": "^1.0.16",
|
||||
"@types/passport-github2": "^1.2.9",
|
||||
"@types/passport-jwt": "^4.0.1",
|
||||
"@types/passport-local": "^1.0.38",
|
||||
"@types/passport-oauth2": "^1.4.15",
|
||||
"@types/source-map-support": "^0",
|
||||
"@types/swagger-ui-express": "^4.1.6",
|
||||
"@types/validator": "^13.11.9",
|
||||
"concurrently": "^8.2.2",
|
||||
"jest": "^29.6.1",
|
||||
"nodemon": "^3.0.1",
|
||||
"source-map-support": "^0.5.21",
|
||||
"typescript": "^5.3.3"
|
||||
}
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
import { matchedData } from 'express-validator'
|
||||
import { genJWT } from '../../utils/auth.js'
|
||||
import { createUser } from '../../models/userManager.js'
|
||||
import { asyncHandler } from '../../middleware/error.js'
|
||||
import logger from '../../utils/logger.js'
|
||||
|
||||
export const issueToken = asyncHandler(async (req, res) => {
|
||||
const { user } = req
|
||||
const token = await genJWT({
|
||||
_id: user.id,
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
})
|
||||
res.json({ token })
|
||||
})
|
||||
|
||||
export const register = asyncHandler(async (req, res, next) => {
|
||||
const { username, password, email } = matchedData(req)
|
||||
|
||||
const user = await createUser(username, email, { password })
|
||||
req.login(user, (err) => {
|
||||
if (err) {
|
||||
next(err)
|
||||
return
|
||||
}
|
||||
res.status(201).json({
|
||||
message: 'user created',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
export function logout(req, res) {
|
||||
req.logout((err) => {
|
||||
if (err) {
|
||||
logger.error(err)
|
||||
res.status(500).json({
|
||||
message: 'error logging out',
|
||||
})
|
||||
return
|
||||
}
|
||||
res.status(200).json({
|
||||
message: 'logged out',
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export function checkSession(req, res) {
|
||||
res.json({
|
||||
loggedIn: req.isAuthenticated() && !req.session.newUser,
|
||||
})
|
||||
}
|
||||
|
||||
export function socialLoginHandler(req, res) {
|
||||
const { newUser } = req.user
|
||||
if (newUser) {
|
||||
req.session.newUser = newUser
|
||||
res.redirect('/choose-username')
|
||||
return
|
||||
}
|
||||
res.redirect('/')
|
||||
}
|
||||
|
||||
export const registerFromSession = asyncHandler(async (req, res, next) => {
|
||||
const { username } = matchedData(req)
|
||||
const { newUser } = req.session
|
||||
|
||||
if (!newUser) {
|
||||
res.status(401).json({
|
||||
message: 'no new user found',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const { email, socialLogin } = newUser
|
||||
|
||||
if (!socialLogin) {
|
||||
throw new Error('Socail Login not found')
|
||||
}
|
||||
|
||||
const user = await createUser(username, email, { socialLogin })
|
||||
|
||||
req.login(user, (err) => {
|
||||
if (err) {
|
||||
next(err)
|
||||
return
|
||||
}
|
||||
delete req.session.newUser
|
||||
res.status(201).json({
|
||||
message: 'user created',
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,111 @@
|
||||
import type { Request, Response, NextFunction, Express } from 'express'
|
||||
import { matchedData } from 'express-validator'
|
||||
import { genJWT } from '../../utils/auth.js'
|
||||
import { createUser } from '../../models/userManager.js'
|
||||
import { asyncHandler } from '../../middleware/error.js'
|
||||
import logger from '../../utils/logger.js'
|
||||
|
||||
export const issueToken = asyncHandler(async (req: Request, res: Response) => {
|
||||
const user = req.user as Express.ExistingUser
|
||||
const token = await genJWT({
|
||||
_id: user.id,
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
})
|
||||
res.json({ token })
|
||||
})
|
||||
|
||||
export const register = asyncHandler(
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const { username, password, email } = matchedData(req)
|
||||
|
||||
const user = await createUser(username, email, { password })
|
||||
req.login(
|
||||
{
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
},
|
||||
(err) => {
|
||||
if (err) {
|
||||
next(err)
|
||||
return
|
||||
}
|
||||
res.status(201).json({
|
||||
message: 'user created',
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
export function logout(req: Request, res: Response) {
|
||||
req.logout((err) => {
|
||||
if (err) {
|
||||
logger.error(err)
|
||||
res.status(500).json({
|
||||
message: 'error logging out',
|
||||
})
|
||||
return
|
||||
}
|
||||
res.status(200).json({
|
||||
message: 'logged out',
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export function checkSession(req: Request, res: Response) {
|
||||
res.json({
|
||||
loggedIn: req.isAuthenticated() && !req.session.newUser,
|
||||
})
|
||||
}
|
||||
|
||||
export function socialLoginHandler(req: Request, res: Response) {
|
||||
const { newUser } = req.user as Express.NewUser
|
||||
if (newUser) {
|
||||
req.session.newUser = newUser
|
||||
res.redirect('/choose-username')
|
||||
return
|
||||
}
|
||||
res.redirect('/')
|
||||
}
|
||||
|
||||
export const registerFromSession = asyncHandler(
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const { username } = matchedData(req)
|
||||
const { newUser } = req.session
|
||||
|
||||
if (!newUser) {
|
||||
res.status(401).json({
|
||||
message: 'no new user found',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const { email, socialLogin } = newUser
|
||||
|
||||
if (!socialLogin) {
|
||||
throw new Error('Socail Login not found')
|
||||
}
|
||||
|
||||
const user = await createUser(username, email, { socialLogin })
|
||||
|
||||
req.login(
|
||||
{
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
},
|
||||
(err) => {
|
||||
if (err) {
|
||||
next(err)
|
||||
return
|
||||
}
|
||||
delete req.session.newUser
|
||||
res.status(201).json({
|
||||
message: 'user created',
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
+37
-26
@@ -1,5 +1,6 @@
|
||||
import { Command, Session } from '@linux-odyssey/models'
|
||||
import { Command, Quest, Session } from '@linux-odyssey/models'
|
||||
import { matchedData } from 'express-validator'
|
||||
import type { Request, Response } from 'express'
|
||||
import CommandHandler from '../../game/commandHandler.js'
|
||||
import { pushToSession } from '../socket.js'
|
||||
import { finishSession } from '../../models/sessionManager.js'
|
||||
@@ -7,10 +8,10 @@ import { asyncHandler } from '../../middleware/error.js'
|
||||
|
||||
const commandCompleteCallbacks = new Map()
|
||||
|
||||
export const newCommand = asyncHandler(async (req, res) => {
|
||||
export const newCommand = asyncHandler(async (req: Request, res: Response) => {
|
||||
const { command, pwd, output, error, params } = matchedData(req)
|
||||
|
||||
const { sessionId } = req.user
|
||||
const { sessionId } = req.user as any
|
||||
const session = await Session.findById(sessionId)
|
||||
if (!session) {
|
||||
res.status(404).json({ message: 'session not found' })
|
||||
@@ -38,7 +39,13 @@ export const newCommand = asyncHandler(async (req, res) => {
|
||||
|
||||
await c.save()
|
||||
|
||||
const commandHandler = new CommandHandler(session, c, params)
|
||||
const quest = await Quest.findById(session.quest)
|
||||
if (!quest) {
|
||||
res.status(404).json({ message: 'quest not found' })
|
||||
return
|
||||
}
|
||||
|
||||
const commandHandler = new CommandHandler(session, quest, c, params)
|
||||
const response = await commandHandler.run()
|
||||
if (response) {
|
||||
c.stage = response.stage
|
||||
@@ -49,33 +56,37 @@ export const newCommand = asyncHandler(async (req, res) => {
|
||||
}
|
||||
commandCompleteCallbacks.set(session.id, response.callback)
|
||||
pushToSession(session.id, 'status', 'pending')
|
||||
res.status(201).json(response)
|
||||
} else {
|
||||
res.status(200).end()
|
||||
}
|
||||
|
||||
res.status(201).json(response)
|
||||
await session.save()
|
||||
})
|
||||
|
||||
export const completedCommand = asyncHandler(async (req, res) => {
|
||||
const { sessionId } = req.user
|
||||
export const completedCommand = asyncHandler(
|
||||
async (req: Request, res: Response) => {
|
||||
const { sessionId } = req.user as any
|
||||
|
||||
const session = await Session.findById(sessionId)
|
||||
const session = await Session.findById(sessionId)
|
||||
|
||||
if (!session) {
|
||||
res.status(404).json({ message: 'session not found' })
|
||||
return
|
||||
if (!session) {
|
||||
res.status(404).json({ message: 'session not found' })
|
||||
return
|
||||
}
|
||||
|
||||
if (session.status === 'inactive') {
|
||||
res.status(400).json({ message: 'session is not active' })
|
||||
return
|
||||
}
|
||||
|
||||
const callback = commandCompleteCallbacks.get(session.id)
|
||||
if (!callback) {
|
||||
res.status(200).json({ message: 'no callback found' })
|
||||
return
|
||||
}
|
||||
callback()
|
||||
commandCompleteCallbacks.delete(session.id)
|
||||
res.json({ message: 'ok' })
|
||||
}
|
||||
|
||||
if (session.status === 'inactive') {
|
||||
res.status(400).json({ message: 'session is not active' })
|
||||
return
|
||||
}
|
||||
|
||||
const callback = commandCompleteCallbacks.get(session.id)
|
||||
if (!callback) {
|
||||
res.status(200).json({ message: 'no callback found' })
|
||||
return
|
||||
}
|
||||
callback()
|
||||
commandCompleteCallbacks.delete(session.id)
|
||||
res.json({ message: 'ok' })
|
||||
})
|
||||
)
|
||||
+2
-1
@@ -1,7 +1,8 @@
|
||||
import type { Request, Response } from 'express'
|
||||
import { asyncHandler } from '../../middleware/error.js'
|
||||
import { leaderboard } from '../../models/userManager.js'
|
||||
|
||||
export default asyncHandler(async (req, res) => {
|
||||
export default asyncHandler(async (req: Request, res: Response) => {
|
||||
const users = await leaderboard()
|
||||
const response = users.map((user) => ({
|
||||
username: user.user.username,
|
||||
@@ -1,25 +0,0 @@
|
||||
import { Quest } from '@linux-odyssey/models'
|
||||
import { matchedData } from 'express-validator'
|
||||
import { asyncHandler } from '../../middleware/error.js'
|
||||
|
||||
export const getQuests = asyncHandler(async (req, res) => {
|
||||
const quests = await Quest.find({})
|
||||
res.json(
|
||||
quests.map(({ title, _id, requirements }) => ({
|
||||
_id,
|
||||
title,
|
||||
requirements,
|
||||
}))
|
||||
)
|
||||
})
|
||||
|
||||
export const getQuestDetail = asyncHandler(async (req, res) => {
|
||||
const { questId } = matchedData(req)
|
||||
const quest = await Quest.findById(questId)
|
||||
if (!quest) {
|
||||
res.status(404).json({ message: 'Quest not found.' })
|
||||
return
|
||||
}
|
||||
const { _id, title, order, instruction } = quest
|
||||
res.json({ _id, title, order, instruction })
|
||||
})
|
||||
@@ -0,0 +1,28 @@
|
||||
import { Quest } from '@linux-odyssey/models'
|
||||
import { matchedData } from 'express-validator'
|
||||
import type { Request, Response } from 'express'
|
||||
import { asyncHandler } from '../../middleware/error.js'
|
||||
|
||||
export const getQuests = asyncHandler(async (req: Request, res: Response) => {
|
||||
const quests = await Quest.find({})
|
||||
res.json(
|
||||
quests.map(({ title, _id, requirements }) => ({
|
||||
_id,
|
||||
title,
|
||||
requirements,
|
||||
}))
|
||||
)
|
||||
})
|
||||
|
||||
export const getQuestDetail = asyncHandler(
|
||||
async (req: Request, res: Response) => {
|
||||
const { questId } = matchedData(req)
|
||||
const quest = await Quest.findById(questId)
|
||||
if (!quest) {
|
||||
res.status(404).json({ message: 'Quest not found.' })
|
||||
return
|
||||
}
|
||||
const { _id, title, instruction, requirements } = quest
|
||||
res.json({ _id, title, instruction, requirements })
|
||||
}
|
||||
)
|
||||
@@ -1,75 +0,0 @@
|
||||
import { matchedData } from 'express-validator'
|
||||
import { Session } from '@linux-odyssey/models'
|
||||
import {
|
||||
createNewSession,
|
||||
getOrCreateActiveSession,
|
||||
isQuestUnlocked,
|
||||
} from '../../models/sessionManager.js'
|
||||
import { asyncHandler } from '../../middleware/error.js'
|
||||
import config from '../../config.js'
|
||||
|
||||
function sessionSummary(session) {
|
||||
return {
|
||||
_id: session._id,
|
||||
user: session.user._id || session.user,
|
||||
quest: session.quest._id || session.quest,
|
||||
status: session.status,
|
||||
createdAt: session.createdAt,
|
||||
lastActivityAt: session.lastActivityAt,
|
||||
}
|
||||
}
|
||||
|
||||
function sessionDetail(session) {
|
||||
return {
|
||||
...sessionSummary(session),
|
||||
hints: session.hints,
|
||||
tasks: session.tasks,
|
||||
graph: session.graph,
|
||||
}
|
||||
}
|
||||
|
||||
export const getSessionList = asyncHandler(async (req, res) => {
|
||||
const query = { user: req.user._id }
|
||||
const { questId, status } = matchedData(req)
|
||||
if (questId) {
|
||||
query.quest = questId
|
||||
}
|
||||
query.status = status || 'active'
|
||||
const sessions = await Session.find(query)
|
||||
res.json(sessions.map(sessionSummary))
|
||||
})
|
||||
|
||||
export const createSession = asyncHandler(async (req, res) => {
|
||||
const { questId } = matchedData(req)
|
||||
if (await isQuestUnlocked(req.user, questId)) {
|
||||
const session = await createNewSession(req.user, questId)
|
||||
res.status(201).json(sessionDetail(session))
|
||||
} else {
|
||||
res.status(403).json({ message: 'Quest is locked.' })
|
||||
}
|
||||
})
|
||||
|
||||
export const getSessionById = asyncHandler(async (req, res) => {
|
||||
const { sessionId } = matchedData(req)
|
||||
const session = await Session.findOne({
|
||||
_id: sessionId,
|
||||
user: req.user._id,
|
||||
})
|
||||
|
||||
if (!session) {
|
||||
res.status(404).json({ message: 'Session not found.' })
|
||||
return
|
||||
}
|
||||
|
||||
res.json(sessionDetail(session))
|
||||
})
|
||||
|
||||
export const getOrCreateSession = asyncHandler(async (req, res) => {
|
||||
const { questId } = matchedData(req)
|
||||
if ((await isQuestUnlocked(req.user, questId)) || config.testing.enabled) {
|
||||
const session = await getOrCreateActiveSession(req.user, questId)
|
||||
res.json(sessionDetail(session))
|
||||
} else {
|
||||
res.status(403).json({ message: 'Quest is locked.' })
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,86 @@
|
||||
import { matchedData } from 'express-validator'
|
||||
import type { Express, Request, Response } from 'express'
|
||||
import { User, Session, ISession } from '@linux-odyssey/models'
|
||||
import {
|
||||
createNewSession,
|
||||
getOrCreateActiveSession,
|
||||
isQuestUnlocked,
|
||||
} from '../../models/sessionManager.js'
|
||||
import { asyncHandler } from '../../middleware/error.js'
|
||||
import config from '../../config.js'
|
||||
|
||||
function sessionSummary(session: ISession) {
|
||||
return {
|
||||
_id: session._id,
|
||||
user: session.user,
|
||||
quest: session.quest,
|
||||
status: session.status,
|
||||
createdAt: session.createdAt,
|
||||
lastActivityAt: session.lastActivityAt,
|
||||
}
|
||||
}
|
||||
|
||||
function sessionDetail(session: ISession) {
|
||||
return {
|
||||
...sessionSummary(session),
|
||||
hints: session.hints,
|
||||
tasks: session.tasks,
|
||||
graph: session.graph,
|
||||
}
|
||||
}
|
||||
|
||||
export const getSessionList = asyncHandler(
|
||||
async (req: Request, res: Response) => {
|
||||
const user = req.user as Express.ExistingUser
|
||||
const { questId, status } = matchedData(req)
|
||||
const query = { user: user.id, quest: questId, status }
|
||||
const sessions = await Session.find(query)
|
||||
res.json(sessions.map(sessionSummary))
|
||||
}
|
||||
)
|
||||
|
||||
export const createSession = asyncHandler(
|
||||
async (req: Request, res: Response) => {
|
||||
const { questId } = matchedData(req)
|
||||
const user = await User.findById((req.user as Express.ExistingUser).id)
|
||||
if (user && (await isQuestUnlocked(user, questId))) {
|
||||
const session = await createNewSession(user, questId)
|
||||
res.status(201).json(sessionDetail(session))
|
||||
} else {
|
||||
res.status(403).json({ message: 'Quest is locked.' })
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
export const getSessionById = asyncHandler(
|
||||
async (req: Request, res: Response) => {
|
||||
const { sessionId } = matchedData(req)
|
||||
const session = await Session.findOne({
|
||||
_id: sessionId,
|
||||
user: (req.user as Express.ExistingUser).id,
|
||||
})
|
||||
|
||||
if (!session) {
|
||||
res.status(404).json({ message: 'Session not found.' })
|
||||
return
|
||||
}
|
||||
|
||||
res.json(sessionDetail(session))
|
||||
}
|
||||
)
|
||||
|
||||
export const getOrCreateSession = asyncHandler(
|
||||
async (req: Request, res: Response) => {
|
||||
const { questId } = matchedData(req)
|
||||
const user = await User.findById((req.user as Express.ExistingUser).id)
|
||||
if (
|
||||
user &&
|
||||
((await isQuestUnlocked(user, questId)) || config.testing.enabled)
|
||||
) {
|
||||
const session = await getOrCreateActiveSession(user, questId)
|
||||
res.json(sessionDetail(session))
|
||||
} else {
|
||||
res.status(403).json({ message: 'Quest is locked.' })
|
||||
}
|
||||
}
|
||||
)
|
||||
+6
-6
@@ -1,16 +1,16 @@
|
||||
import type { Express, Request, Response } from 'express'
|
||||
import { UserProfile } from '@linux-odyssey/models'
|
||||
import { asyncHandler } from '../../middleware/error.js'
|
||||
|
||||
export const me = asyncHandler(async (req, res) => {
|
||||
export const me = asyncHandler(async (req: Request, res: Response) => {
|
||||
const { id, username, email } = req.user as Express.ExistingUser
|
||||
|
||||
const userProfile = await UserProfile.findOne({
|
||||
user: req.user._id,
|
||||
user: id,
|
||||
})
|
||||
if (!userProfile) {
|
||||
throw new Error(
|
||||
`UserProfile not found for user ${req.user.username} ${req.user._id}!`
|
||||
)
|
||||
throw new Error(`UserProfile not found for user ${username} ${id}!`)
|
||||
}
|
||||
const { username, email } = req.user
|
||||
const { progress } = userProfile
|
||||
res.json({
|
||||
username,
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Router } from 'express'
|
||||
import { Router, Request, Response } from 'express'
|
||||
import passport from 'passport'
|
||||
|
||||
import enabledMethods from '../../auth/passport.js'
|
||||
@@ -25,7 +25,7 @@ router.post(
|
||||
authenticateRateLimit,
|
||||
loginValidators,
|
||||
passport.authenticate('local', { failureMessage: true }),
|
||||
(req, res) => {
|
||||
(req: Request, res: Response) => {
|
||||
res.json({ message: 'success' })
|
||||
}
|
||||
)
|
||||
@@ -34,8 +34,10 @@ router.post('/register', authenticateRateLimit, registerValidators, register)
|
||||
|
||||
router.post('/logout', logout)
|
||||
|
||||
router.get('/check-username', checkUsernameValidators, (req, res) =>
|
||||
res.json({ available: true })
|
||||
router.get(
|
||||
'/check-username',
|
||||
checkUsernameValidators,
|
||||
(req: Request, res: Response) => res.json({ available: true })
|
||||
)
|
||||
router.get('/check-session', checkSession)
|
||||
|
||||
File renamed without changes.
@@ -10,7 +10,6 @@ import config from '../../config.js'
|
||||
import leaderboardController from '../controllers/leaderboardController.js'
|
||||
|
||||
const router = Router()
|
||||
|
||||
if (!config.isProduction) {
|
||||
router.get('/', (req, res) => {
|
||||
res.send('Hello API!')
|
||||
@@ -21,17 +20,17 @@ if (!config.isProduction) {
|
||||
})
|
||||
}
|
||||
|
||||
router.use('/auth', authRouter)
|
||||
router.use('/quests', quests)
|
||||
router.use('/users', userRouter)
|
||||
|
||||
router.use('/sessions', authRequired, sessions)
|
||||
router.use(
|
||||
'/commands',
|
||||
passport.authenticate('jwt', { session: false }),
|
||||
authRequired,
|
||||
commands
|
||||
)
|
||||
router.use('/auth', authRouter)
|
||||
router.use('/sessions', authRequired, sessions)
|
||||
|
||||
router.use('/quests', quests)
|
||||
router.use('/users', userRouter)
|
||||
|
||||
router.get('/survey', (req, res) => {
|
||||
if (!config.surveyUrl) {
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -1,43 +1,56 @@
|
||||
import { Server } from 'socket.io'
|
||||
import type { Socket } from 'socket.io'
|
||||
import validator from 'validator'
|
||||
import type { Server as HttpServer } from 'http'
|
||||
import { Session } from '@linux-odyssey/models'
|
||||
import { getAndStartContainer, attachContainer } from '../containers/docker.js'
|
||||
import SessionMiddleware from '../middleware/session.js'
|
||||
import { genJWT } from '../utils/auth.js'
|
||||
import logger from '../utils/logger.js'
|
||||
|
||||
const sessions = new Map()
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
type SocketCallback = (event: string, ...args: any[]) => void
|
||||
|
||||
function listenToSession(sessionId, callback) {
|
||||
const sessions = new Map<string, SocketCallback[]>()
|
||||
|
||||
function listenToSession(sessionId: string, callback: SocketCallback) {
|
||||
const callbacks = sessions.get(sessionId) || []
|
||||
sessions.set(sessionId, callbacks.concat(callback))
|
||||
}
|
||||
|
||||
function removeFromSession(sessionId, event, callback) {
|
||||
const callbacks = sessions.get((sessionId, event)) || []
|
||||
function removeFromSession(sessionId: string, callback: SocketCallback) {
|
||||
const callbacks = sessions.get(sessionId) || []
|
||||
sessions.set(
|
||||
(sessionId, event),
|
||||
sessionId,
|
||||
callbacks.filter((cb) => cb !== callback)
|
||||
)
|
||||
}
|
||||
|
||||
export function pushToSession(sessionId, event, ...args) {
|
||||
export function pushToSession(
|
||||
sessionId: string,
|
||||
event: string,
|
||||
...args: any[]
|
||||
) {
|
||||
const callbacks = sessions.get(sessionId)
|
||||
if (callbacks) {
|
||||
callbacks.forEach((callback) => callback(event, ...args))
|
||||
}
|
||||
}
|
||||
|
||||
async function connectContainer(socket, next) {
|
||||
const user = socket.request.session?.passport?.user
|
||||
// console.log(user)
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
async function connectContainer(socket: Socket, next: (err?: Error) => void) {
|
||||
const user = (socket.request as any).session?.passport?.user
|
||||
if (!user) {
|
||||
next(new Error('User not found.'))
|
||||
return
|
||||
}
|
||||
// console.log(socket.handshake)
|
||||
const { sessionId } = socket.handshake.query
|
||||
if (!(sessionId && validator.isMongoId(sessionId))) {
|
||||
if (
|
||||
!sessionId ||
|
||||
typeof sessionId !== 'string' ||
|
||||
!validator.isMongoId(sessionId)
|
||||
) {
|
||||
logger.warn('Invalid Session ID.', user.username, sessionId)
|
||||
next(new Error('Invalid Session ID.'))
|
||||
return
|
||||
@@ -53,10 +66,15 @@ async function connectContainer(socket, next) {
|
||||
sessionId: session.id,
|
||||
})
|
||||
|
||||
if (!session.containerId) {
|
||||
next(new Error('Session has no container.'))
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const container = await getAndStartContainer(session.containerId)
|
||||
const stream = await attachContainer(container, { token })
|
||||
socket.context = {
|
||||
socket.data.context = {
|
||||
session,
|
||||
stream,
|
||||
}
|
||||
@@ -66,9 +84,9 @@ async function connectContainer(socket, next) {
|
||||
}
|
||||
}
|
||||
|
||||
function onConnect(socket) {
|
||||
const { session, stream } = socket.context
|
||||
stream.socket.on('data', (chunk) => {
|
||||
function onConnect(socket: Socket) {
|
||||
const { session, stream } = socket.data.context
|
||||
stream.socket.on('data', (chunk: Buffer) => {
|
||||
socket.emit('terminal', chunk.toString())
|
||||
})
|
||||
|
||||
@@ -76,7 +94,7 @@ function onConnect(socket) {
|
||||
stream.socket.write(message)
|
||||
})
|
||||
|
||||
const socketCallback = (event, ...data) => {
|
||||
const socketCallback = (event: string, ...data: any[]) => {
|
||||
socket.emit(event, ...data)
|
||||
}
|
||||
|
||||
@@ -89,7 +107,7 @@ function onConnect(socket) {
|
||||
})
|
||||
}
|
||||
|
||||
export default (server) => {
|
||||
export default (server: HttpServer) => {
|
||||
const io = new Server(server)
|
||||
|
||||
io.engine.use(SessionMiddleware)
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -1,9 +1,16 @@
|
||||
import GitHubStrategy from 'passport-github2'
|
||||
import { Strategy as GitHubStrategy } from 'passport-github2'
|
||||
import type { Profile } from 'passport'
|
||||
import type { VerifyCallback } from 'passport-oauth2'
|
||||
|
||||
import config from '../config.js'
|
||||
import oauthVerify from './oauthVerify.js'
|
||||
|
||||
function verify(accessToken, refreshToken, profile, cb) {
|
||||
function verify(
|
||||
accessToken: string,
|
||||
refreshToken: string,
|
||||
profile: Profile,
|
||||
cb: VerifyCallback
|
||||
) {
|
||||
oauthVerify('github', profile, { 'socialLogins.github.id': profile.id }, cb)
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import GoogleStrategy from 'passport-google-oidc'
|
||||
import { Strategy as GoogleStrategy } from 'passport-google-oidc'
|
||||
|
||||
import config from '../config.js'
|
||||
import oauthVerify from './oauthVerify.js'
|
||||
|
||||
function verify(issuer, profile, cb) {
|
||||
function verify(issuer: string, profile: any, cb: any) {
|
||||
oauthVerify('google', profile, { 'socialLogins.google.id': profile.id }, cb)
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@ export default enabled
|
||||
clientID: config.google.clientID,
|
||||
clientSecret: config.google.clientSecret,
|
||||
callbackURL: `${config.baseUrl}/api/v1/auth/google/callback`,
|
||||
scope: ['profile', 'email'],
|
||||
},
|
||||
verify
|
||||
)
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Strategy as JwtStrategy, ExtractJwt } from 'passport-jwt'
|
||||
import type { VerifyCallback } from 'passport-jwt'
|
||||
import config from '../config.js'
|
||||
|
||||
const jwtOptions = {
|
||||
@@ -6,7 +7,7 @@ const jwtOptions = {
|
||||
secretOrKey: config.secret,
|
||||
}
|
||||
|
||||
function verifyJwt(jwtPayload, done) {
|
||||
function verifyJwt(jwtPayload: any, done: VerifyCallback) {
|
||||
return done(null, jwtPayload)
|
||||
}
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
import { User } from '@linux-odyssey/models'
|
||||
import type { Profile } from 'passport'
|
||||
import type { VerifyCallback } from 'passport-oauth2'
|
||||
|
||||
export default async function oauthVerify(provider, profile, query, cb) {
|
||||
export default async function oauthVerify(
|
||||
provider: string,
|
||||
profile: Profile,
|
||||
query: Record<string, any>,
|
||||
cb: VerifyCallback
|
||||
) {
|
||||
process.nextTick(async () => {
|
||||
try {
|
||||
const email = profile.emails[0]?.value
|
||||
const email = profile.emails ? profile.emails[0]?.value : undefined
|
||||
if (!email) {
|
||||
return cb(new Error('No email found'))
|
||||
}
|
||||
@@ -24,9 +31,15 @@ export default async function oauthVerify(provider, profile, query, cb) {
|
||||
await user.save()
|
||||
}
|
||||
|
||||
return cb(null, user)
|
||||
return cb(null, {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
})
|
||||
} catch (err) {
|
||||
return cb(err)
|
||||
return cb(
|
||||
err instanceof Error ? err : new Error('An unknown error occurred')
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import passport from 'passport'
|
||||
import { UserProfile } from '@linux-odyssey/models'
|
||||
import type { Express } from 'express'
|
||||
import passwordStrategy from './passwordStrategy.js'
|
||||
import jwtStrategy from './jwtStrategy.js'
|
||||
import googleStrategy from './googleStrategy.js'
|
||||
@@ -17,32 +18,33 @@ const enabledMethods = {
|
||||
|
||||
logger.info('Enabled login methods', enabledMethods)
|
||||
|
||||
if (enabledMethods.google) {
|
||||
if (googleStrategy !== null) {
|
||||
passport.use(googleStrategy)
|
||||
}
|
||||
|
||||
if (enabledMethods.github) {
|
||||
if (githubStrategy !== null) {
|
||||
passport.use(githubStrategy)
|
||||
}
|
||||
|
||||
passport.serializeUser((user, done) => {
|
||||
passport.serializeUser((user: Express.User, done) => {
|
||||
process.nextTick(async () => {
|
||||
try {
|
||||
if (user._id && !(await UserProfile.exists({ user: user._id }))) {
|
||||
await UserProfile.create({ user: user._id })
|
||||
if (!('id' in user)) {
|
||||
done(new Error('User has no _id'))
|
||||
return
|
||||
}
|
||||
done(null, {
|
||||
_id: user._id,
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
})
|
||||
const { id } = user
|
||||
if (!(await UserProfile.exists({ user: id }))) {
|
||||
await UserProfile.create({ user: id })
|
||||
}
|
||||
done(null, user)
|
||||
} catch (err) {
|
||||
done(err)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
passport.deserializeUser((user, done) => {
|
||||
passport.deserializeUser((user: Express.User, done) => {
|
||||
process.nextTick(() => {
|
||||
done(null, user)
|
||||
})
|
||||
@@ -1,24 +0,0 @@
|
||||
import bcrypt from 'bcrypt'
|
||||
import LocalStrategy from 'passport-local'
|
||||
import { User } from '@linux-odyssey/models'
|
||||
|
||||
async function verifyPassword(username, password, done) {
|
||||
try {
|
||||
const user = await User.findOne({
|
||||
$or: [{ username }, { email: username }],
|
||||
})
|
||||
if (!user) {
|
||||
return done(null, false, { message: 'Incorrect username or password.' })
|
||||
}
|
||||
|
||||
const isPasswordValid = await bcrypt.compare(password, user.hashedPassword)
|
||||
if (!isPasswordValid) {
|
||||
return done(null, false, { message: 'Incorrect username or password.' })
|
||||
}
|
||||
return done(null, user)
|
||||
} catch (err) {
|
||||
return done(err)
|
||||
}
|
||||
}
|
||||
|
||||
export default new LocalStrategy(verifyPassword)
|
||||
@@ -0,0 +1,42 @@
|
||||
/* eslint-disable no-unused-vars */
|
||||
import bcrypt from 'bcrypt'
|
||||
import { Strategy as LocalStrategy } from 'passport-local'
|
||||
import type { IVerifyOptions } from 'passport-local'
|
||||
import type { Express } from 'express'
|
||||
import { User } from '@linux-odyssey/models'
|
||||
|
||||
async function verifyPassword(
|
||||
username: string,
|
||||
password: string,
|
||||
done: (
|
||||
error: any,
|
||||
user?: Express.User | false,
|
||||
options?: IVerifyOptions
|
||||
) => void
|
||||
) {
|
||||
try {
|
||||
const user = await User.findOne({
|
||||
$or: [{ username }, { email: username }],
|
||||
})
|
||||
if (!user) {
|
||||
return done(null, false, { message: 'Incorrect username or password.' })
|
||||
}
|
||||
|
||||
const isPasswordValid = await bcrypt.compare(
|
||||
password,
|
||||
user.hashedPassword || ''
|
||||
)
|
||||
if (!isPasswordValid) {
|
||||
return done(null, false, { message: 'Incorrect username or password.' })
|
||||
}
|
||||
return done(null, {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
})
|
||||
} catch (err) {
|
||||
return done(err)
|
||||
}
|
||||
}
|
||||
|
||||
export default new LocalStrategy(verifyPassword)
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -1,76 +0,0 @@
|
||||
import validator from 'validator'
|
||||
import dotenv from 'dotenv'
|
||||
import { get } from './utils/env.js'
|
||||
|
||||
// Import dotenv and load ../.env
|
||||
dotenv.config({ path: '../.env' })
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
dotenv.config({ path: '../.env.dev' })
|
||||
}
|
||||
|
||||
function getTrustProxies(key) {
|
||||
if (!process.env[key]) {
|
||||
return []
|
||||
}
|
||||
return process.env[key]
|
||||
.split(',')
|
||||
.map((p) => validator.trim(p))
|
||||
.filter((p) => validator.isIPRange(p) || validator.isIP(p))
|
||||
}
|
||||
|
||||
function getUrl(key) {
|
||||
const url = get(key, '')
|
||||
if (validator.isURL(url)) {
|
||||
return validator.trim(url)
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
const config = {
|
||||
host: get('HOST', 'localhost'),
|
||||
port: get('PORT', 3000),
|
||||
db: get('MONGO_URL', 'mongodb://localhost:27017/odyssey-test'),
|
||||
secret: get('SECRET_KEY', ''),
|
||||
isProduction: process.env.NODE_ENV === 'production',
|
||||
containerExpiry: get('EXPIRY', 1000 * 60 * 60),
|
||||
sessionMaxAge: get('SESSION_MAX_AGE', 1000 * 60 * 60 * 24 * 7),
|
||||
google: {
|
||||
clientID: get('GOOGLE_CLIENT_ID', ''),
|
||||
clientSecret: get('GOOGLE_CLIENT_SECRET', ''),
|
||||
},
|
||||
github: {
|
||||
clientID: get('GITHUB_CLIENT_ID', ''),
|
||||
clientSecret: get('GITHUB_CLIENT_SECRET', ''),
|
||||
},
|
||||
trustedProxies: getTrustProxies('TRUSTED_PROXIES'),
|
||||
surveyUrl: getUrl('SURVEY_URL', ''),
|
||||
bugReportUrl: getUrl('BUG_REPORT_URL', ''),
|
||||
|
||||
docker: {
|
||||
network: get('DOCKER_NETWORK', 'host'),
|
||||
defaultImage: get('QUEST_IMAGE', 'linuxodyssey/quest-base'),
|
||||
imagePrefix: get('DOCKER_PREFIX', 'linuxodyssey/quest-'),
|
||||
mountQuest: get('MOUNT_QUEST', ''),
|
||||
hostPwd: get('HOST_PWD', ''),
|
||||
},
|
||||
|
||||
log: {
|
||||
path: get('LOG_PATH', '../logs'),
|
||||
},
|
||||
}
|
||||
|
||||
config.baseUrl = get('BASE_URL', `http://${config.host}:${config.port}`)
|
||||
config.backendUrl = get('BACKEND_URL', config.baseUrl)
|
||||
config.testing = {
|
||||
enabled: !config.isProduction && process.env.TESTING === 'true',
|
||||
username: get('TESTING_USERNAME', ''),
|
||||
password: get('TESTING_PASSWORD', ''),
|
||||
}
|
||||
|
||||
export function getQuestImage(id) {
|
||||
return `${config.docker.imagePrefix}${id}`
|
||||
}
|
||||
|
||||
console.log(config)
|
||||
|
||||
export default config
|
||||
@@ -0,0 +1,85 @@
|
||||
import validator from 'validator'
|
||||
import dotenv from 'dotenv'
|
||||
import { get } from './utils/env.js'
|
||||
|
||||
// Import dotenv and load ../.env
|
||||
dotenv.config({ path: '../.env' })
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
dotenv.config({ path: '../.env.dev' })
|
||||
}
|
||||
|
||||
function getTrustProxies(key: string): string[] {
|
||||
const value = process.env[key]
|
||||
if (!value) {
|
||||
return []
|
||||
}
|
||||
return value
|
||||
.split(',')
|
||||
.map((p) => validator.trim(p))
|
||||
.filter((p) => validator.isIPRange(p) || validator.isIP(p))
|
||||
}
|
||||
|
||||
function getUrl(key: string): string {
|
||||
const url = get(key, '')
|
||||
if (validator.isURL(url)) {
|
||||
return validator.trim(url)
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
function createConfig() {
|
||||
const isProduction = process.env.NODE_ENV === 'production'
|
||||
const host = get('HOST', 'localhost')
|
||||
const port = Number(get('PORT', 3000))
|
||||
const baseUrl = get('BASE_URL', `http://${host}:${port}`)
|
||||
const url = new URL(baseUrl)
|
||||
return {
|
||||
host,
|
||||
port,
|
||||
baseUrl,
|
||||
domain: url.hostname,
|
||||
protocol: url.protocol,
|
||||
backendUrl: get('BACKEND_URL', baseUrl),
|
||||
db: get('MONGO_URL', 'mongodb://localhost:27017/odyssey-test'),
|
||||
secret: get('SECRET_KEY', ''),
|
||||
isProduction: process.env.NODE_ENV === 'production',
|
||||
containerExpiry: get('EXPIRY', 1000 * 60 * 60),
|
||||
sessionMaxAge: get('SESSION_MAX_AGE', 1000 * 60 * 60 * 24 * 7),
|
||||
google: {
|
||||
clientID: get('GOOGLE_CLIENT_ID', ''),
|
||||
clientSecret: get('GOOGLE_CLIENT_SECRET', ''),
|
||||
},
|
||||
github: {
|
||||
clientID: get('GITHUB_CLIENT_ID', ''),
|
||||
clientSecret: get('GITHUB_CLIENT_SECRET', ''),
|
||||
},
|
||||
trustedProxies: getTrustProxies('TRUSTED_PROXIES'),
|
||||
surveyUrl: getUrl('SURVEY_URL'),
|
||||
bugReportUrl: getUrl('BUG_REPORT_URL'),
|
||||
|
||||
docker: {
|
||||
network: get('DOCKER_NETWORK', 'host'),
|
||||
defaultImage: get('QUEST_IMAGE', 'linuxodyssey/quest-base'),
|
||||
imagePrefix: get('DOCKER_PREFIX', 'linuxodyssey/quest-'),
|
||||
mountQuest: get('MOUNT_QUEST', ''),
|
||||
hostPwd: get('HOST_PWD', ''),
|
||||
},
|
||||
|
||||
log: {
|
||||
path: get('LOG_PATH', '../logs'),
|
||||
},
|
||||
testing: {
|
||||
enabled: !isProduction && process.env.TESTING === 'true',
|
||||
username: get('TESTING_USERNAME', ''),
|
||||
password: get('TESTING_PASSWORD', ''),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const config = createConfig()
|
||||
|
||||
export function getQuestImage(id: string): string {
|
||||
return `${config.docker.imagePrefix}${id}`
|
||||
}
|
||||
|
||||
export default config
|
||||
@@ -1,13 +1,14 @@
|
||||
import { execFile } from 'child_process'
|
||||
|
||||
import { buildFileCheckCmd } from '@linux-odyssey/utils'
|
||||
import type { IFileCondition } from '@linux-odyssey/models'
|
||||
|
||||
function sanitizeId(id) {
|
||||
function sanitizeId(id: string) {
|
||||
return id.replace(/[^a-zA-Z0-9]/g, '')
|
||||
}
|
||||
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export function checkFile(id, file) {
|
||||
export function checkFile(id: string, file: IFileCondition) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const cmd = buildFileCheckCmd(file)
|
||||
// WARNING: This is vulnerable to command injection
|
||||
@@ -14,9 +14,12 @@ const containerOptions = {
|
||||
HostConfig: {
|
||||
NetworkMode: config.docker.network,
|
||||
},
|
||||
}
|
||||
} as Docker.ContainerCreateOptions
|
||||
|
||||
export function createContainer(name, questId) {
|
||||
export function createContainer(
|
||||
name: string,
|
||||
questId: string
|
||||
): Promise<Docker.Container> {
|
||||
const option = {
|
||||
...containerOptions,
|
||||
name,
|
||||
@@ -25,7 +28,7 @@ export function createContainer(name, questId) {
|
||||
const { hostPwd, mountQuest } = config.docker
|
||||
if (!config.isProduction && hostPwd && mountQuest === questId) {
|
||||
logger.info('Mounting quest folder', mountQuest)
|
||||
option.HostConfig.Binds = [
|
||||
option.HostConfig!.Binds = [
|
||||
`${hostPwd}/quests/${mountQuest}/home:/home/commander`,
|
||||
`${hostPwd}/packages/container:/usr/local/lib/container`,
|
||||
]
|
||||
@@ -33,7 +36,9 @@ export function createContainer(name, questId) {
|
||||
return engine.createContainer(option)
|
||||
}
|
||||
|
||||
export async function getAndStartContainer(id) {
|
||||
export async function getAndStartContainer(
|
||||
id: string
|
||||
): Promise<Docker.Container> {
|
||||
logger.debug(`Getting container: ${id}`)
|
||||
const container = engine.getContainer(id)
|
||||
if (!container) {
|
||||
@@ -45,7 +50,10 @@ export async function getAndStartContainer(id) {
|
||||
return container
|
||||
}
|
||||
|
||||
export async function attachContainer(container, { token }) {
|
||||
export async function attachContainer(
|
||||
container: Docker.Container,
|
||||
{ token }: { token: string }
|
||||
) {
|
||||
const exec = await container.exec({
|
||||
AttachStdin: true,
|
||||
AttachStdout: true,
|
||||
@@ -67,7 +75,7 @@ export async function attachContainer(container, { token }) {
|
||||
return execOutput
|
||||
}
|
||||
|
||||
export async function deleteContainer(id) {
|
||||
export async function deleteContainer(id: string) {
|
||||
const container = engine.getContainer(id)
|
||||
if (!container) {
|
||||
return
|
||||
@@ -80,8 +88,8 @@ export async function deleteContainer(id) {
|
||||
await container.remove()
|
||||
}
|
||||
|
||||
function parseJSONOutput(data) {
|
||||
const { stream, error } = JSON.parse(data.toString())
|
||||
function parseJSONOutput(data: string) {
|
||||
const { stream, error } = JSON.parse(data)
|
||||
if (stream) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(stream)
|
||||
@@ -91,7 +99,7 @@ function parseJSONOutput(data) {
|
||||
}
|
||||
}
|
||||
|
||||
export function buildQuestImage(questPath, questId) {
|
||||
export function buildQuestImage(questPath: string, questId: string) {
|
||||
return new Promise((resolve, reject) => {
|
||||
engine.buildImage(
|
||||
{
|
||||
@@ -114,9 +122,9 @@ export function buildQuestImage(questPath, questId) {
|
||||
data
|
||||
.toString()
|
||||
.split('\n')
|
||||
.map((line) => line.trim())
|
||||
.filter((line) => line !== '')
|
||||
.forEach((line) => {
|
||||
.map((line: string) => line.trim())
|
||||
.filter((line: string) => line !== '')
|
||||
.forEach((line: string) => {
|
||||
parseJSONOutput(line)
|
||||
})
|
||||
} catch (error) {
|
||||
@@ -1,18 +1,30 @@
|
||||
import schedule from 'node-schedule'
|
||||
import { Session } from '@linux-odyssey/models'
|
||||
import type { ISession } from '@linux-odyssey/models'
|
||||
import type { HydratedDocument } from 'mongoose'
|
||||
import config from '../config.js'
|
||||
import { deleteContainer } from './docker.js'
|
||||
import logger from '../utils/logger.js'
|
||||
|
||||
async function disableSession(session) {
|
||||
async function disableSession(session: HydratedDocument<ISession>) {
|
||||
if (!session.containerId) {
|
||||
logger.warn(`Deleting session ${session._id} has no containerId`)
|
||||
return
|
||||
}
|
||||
try {
|
||||
await deleteContainer(session.containerId)
|
||||
} catch (err) {
|
||||
// If the container doesn't exist, we can ignore the error
|
||||
if (err.statusCode !== 404) {
|
||||
throw new Error(
|
||||
`Failed to delete container ${session.containerId} for session ${session._id}`
|
||||
)
|
||||
// Perform runtime check to safely access statusCode
|
||||
if (typeof err === 'object' && err !== null && 'statusCode' in err) {
|
||||
const error = err as { statusCode: number }
|
||||
if (error.statusCode !== 404) {
|
||||
throw new Error(
|
||||
`Failed to delete container ${session.containerId} for session ${session._id}`
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// Handle the case where err is not the expected type
|
||||
logger.error(`An unexpected error occurred: ${err}`)
|
||||
}
|
||||
}
|
||||
if (session.status === 'active') {
|
||||
@@ -1,23 +1,41 @@
|
||||
import minimist from 'minimist'
|
||||
import { HydratedDocument } from 'mongoose'
|
||||
import { FileGraph } from '@linux-odyssey/file-graph'
|
||||
import { Quest } from '@linux-odyssey/models'
|
||||
import {
|
||||
ICommand,
|
||||
ICondition,
|
||||
IFileCondition,
|
||||
IQuest,
|
||||
ISession,
|
||||
IStage,
|
||||
IException,
|
||||
} from '@linux-odyssey/models'
|
||||
import { pushToSession } from '../api/socket.js'
|
||||
import SessionHandler from './sessionHandler.js'
|
||||
import SessionHandler, { ExecuteResult } from './sessionHandler.js'
|
||||
import { checkFile } from '../containers/cli.js'
|
||||
import logger from '../utils/logger.js'
|
||||
|
||||
// 檢查 pattern 是否符合 input
|
||||
const checkMatch = (pattern, input) => {
|
||||
const checkMatch = (pattern?: string[], input?: string) => {
|
||||
if (!pattern || pattern.length === 0) return true
|
||||
return pattern.some((p) => {
|
||||
const regex = new RegExp(p)
|
||||
return regex.test(input)
|
||||
return input && regex.test(input)
|
||||
})
|
||||
}
|
||||
|
||||
export default class CommandHandler extends SessionHandler {
|
||||
constructor(session, commandInput, params) {
|
||||
super(session)
|
||||
commandInput: ICommand
|
||||
argv: minimist.ParsedArgs
|
||||
params: Record<string, any>
|
||||
|
||||
constructor(
|
||||
session: HydratedDocument<ISession>,
|
||||
quest: HydratedDocument<IQuest>,
|
||||
commandInput: ICommand,
|
||||
params: Record<string, any>
|
||||
) {
|
||||
super(session, quest)
|
||||
this.commandInput = commandInput
|
||||
this.argv = minimist(this.commandInput.command.split(' '))
|
||||
|
||||
@@ -56,7 +74,7 @@ export default class CommandHandler extends SessionHandler {
|
||||
this.session.graph = graph
|
||||
} catch (e) {
|
||||
logger.error('Update file graph failed', {
|
||||
error: e.message,
|
||||
error: (e as Error).message,
|
||||
session: this.session.id,
|
||||
graph: this.session.graph,
|
||||
discover: this.params.discover,
|
||||
@@ -64,7 +82,7 @@ export default class CommandHandler extends SessionHandler {
|
||||
}
|
||||
}
|
||||
|
||||
async isMatch(condition) {
|
||||
async isMatch(condition: ICondition): Promise<boolean> {
|
||||
return (
|
||||
this.checkKeys(condition) &&
|
||||
(await this.checkFiles(condition.files)) &&
|
||||
@@ -73,18 +91,25 @@ export default class CommandHandler extends SessionHandler {
|
||||
)
|
||||
}
|
||||
|
||||
checkKeys(condition) {
|
||||
const keys = ['command', 'output', 'error', 'pwd']
|
||||
return keys.every((k) => checkMatch(condition[k], this.commandInput[k]))
|
||||
checkKeys(condition: ICondition) {
|
||||
return (
|
||||
checkMatch(condition.command, this.commandInput.command) &&
|
||||
checkMatch(condition.output, this.commandInput.output) &&
|
||||
checkMatch(condition.error, this.commandInput.error) &&
|
||||
checkMatch(condition.pwd, this.commandInput.pwd)
|
||||
)
|
||||
}
|
||||
|
||||
async checkFiles(files) {
|
||||
if (files.length === 0) {
|
||||
async checkFiles(files?: IFileCondition[]): Promise<boolean> {
|
||||
if (!files || files.length === 0) {
|
||||
return true
|
||||
}
|
||||
try {
|
||||
const checks = await Promise.all(
|
||||
files.map((f) => checkFile(this.session.containerId, f))
|
||||
files.map(
|
||||
(f) =>
|
||||
this.session.containerId && checkFile(this.session.containerId, f)
|
||||
)
|
||||
)
|
||||
return checks.every((c) => c === true)
|
||||
} catch (e) {
|
||||
@@ -92,22 +117,22 @@ export default class CommandHandler extends SessionHandler {
|
||||
}
|
||||
}
|
||||
|
||||
async checkNot(condition) {
|
||||
async checkNot(condition?: ICondition): Promise<boolean> {
|
||||
if (!condition) {
|
||||
return true
|
||||
}
|
||||
return !(await this.isMatch(condition))
|
||||
}
|
||||
|
||||
async checkOr(conditions) {
|
||||
if (conditions.length === 0) {
|
||||
async checkOr(conditions?: ICondition[]): Promise<boolean> {
|
||||
if (!conditions || conditions.length === 0) {
|
||||
return true
|
||||
}
|
||||
const matches = await Promise.all(conditions.map((c) => this.isMatch(c)))
|
||||
return matches.some((m) => m === true)
|
||||
}
|
||||
|
||||
async checkException(stage) {
|
||||
async checkException(stage: IStage): Promise<IException | null> {
|
||||
const exceptions = stage.exceptions || []
|
||||
for (const exception of exceptions) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
@@ -119,20 +144,19 @@ export default class CommandHandler extends SessionHandler {
|
||||
return null
|
||||
}
|
||||
|
||||
async run() {
|
||||
async run(): Promise<ExecuteResult | null> {
|
||||
try {
|
||||
this.quest = await Quest.findById(this.session.quest)
|
||||
const stages = this.getStages()
|
||||
if (stages.length === 0) {
|
||||
logger.warn('stage not found', { session: this.session.id })
|
||||
return {}
|
||||
return null
|
||||
}
|
||||
|
||||
this.handleEvent()
|
||||
this.handleCommand()
|
||||
|
||||
const matches = await Promise.all(
|
||||
stages.map((stage) => this.isMatch(stage.condition))
|
||||
stages.map((stage) => stage.condition && this.isMatch(stage.condition))
|
||||
)
|
||||
|
||||
const stage = stages.find((_, i) => matches[i])
|
||||
@@ -161,22 +185,25 @@ export default class CommandHandler extends SessionHandler {
|
||||
this.quest.exceptions
|
||||
)
|
||||
for (const exception of globalExceptions) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
if (await this.isMatch(exception.condition)) {
|
||||
if (
|
||||
exception.condition &&
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
(await this.isMatch(exception.condition))
|
||||
) {
|
||||
return this.executeException(exception)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {}
|
||||
return null
|
||||
} catch (err) {
|
||||
logger.error('CommandHandler error', {
|
||||
error: err.message,
|
||||
error: (err as Error).message,
|
||||
session: this.session._id,
|
||||
quest: this.quest._id,
|
||||
command: this.commandInput,
|
||||
})
|
||||
return {}
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,27 +1,51 @@
|
||||
import { HydratedDocument } from 'mongoose'
|
||||
import type {
|
||||
IException,
|
||||
IQuest,
|
||||
IRequirement,
|
||||
IResponse,
|
||||
ISession,
|
||||
IStage,
|
||||
ITask,
|
||||
} from '@linux-odyssey/models'
|
||||
import { pushToSession } from '../api/socket.js'
|
||||
|
||||
export interface ExecuteResult {
|
||||
responses: IResponse[]
|
||||
hints: string[]
|
||||
callback: () => void
|
||||
stage?: string
|
||||
end?: boolean
|
||||
}
|
||||
|
||||
export default class SessionHandler {
|
||||
constructor(session) {
|
||||
session: HydratedDocument<ISession>
|
||||
quest: HydratedDocument<IQuest>
|
||||
|
||||
constructor(
|
||||
session: HydratedDocument<ISession>,
|
||||
quest: HydratedDocument<IQuest>
|
||||
) {
|
||||
this.session = session
|
||||
this.quest = session.quest
|
||||
this.quest = quest
|
||||
}
|
||||
|
||||
getSession() {
|
||||
getSession(): HydratedDocument<ISession> {
|
||||
return this.session
|
||||
}
|
||||
|
||||
getTask(stage) {
|
||||
getTask(stage: Partial<IStage>): ITask | undefined {
|
||||
return this.session.tasks.find((t) => t.id === stage.id)
|
||||
}
|
||||
|
||||
isCompleted(stage) {
|
||||
isCompleted(stage: Partial<IStage>): boolean {
|
||||
const task = this.getTask(stage)
|
||||
return task ? task.completed : false
|
||||
}
|
||||
|
||||
getUnlockedComponents(components) {
|
||||
getUnlockedComponents<T extends IRequirement>(components: T[]): T[] {
|
||||
return components.filter((component) =>
|
||||
component.requirements.every((r) => this.isCompleted({ id: r }))
|
||||
component.requirements.every((r: string) => this.isCompleted({ id: r }))
|
||||
)
|
||||
}
|
||||
|
||||
@@ -43,7 +67,7 @@ export default class SessionHandler {
|
||||
this.session.tasks.push(...newTasks)
|
||||
}
|
||||
|
||||
execute(stage) {
|
||||
execute(stage: IStage): ExecuteResult {
|
||||
this.session.tasks
|
||||
.filter((t) => t.id === stage.id)
|
||||
.forEach((t) => {
|
||||
@@ -67,7 +91,7 @@ export default class SessionHandler {
|
||||
}
|
||||
}
|
||||
|
||||
executeException(exception) {
|
||||
executeException(exception: IException): ExecuteResult {
|
||||
const { hints, responses } = exception
|
||||
if (hints && hints.length > 0) this.session.hints.push(hints)
|
||||
|
||||
@@ -62,13 +62,7 @@ async function main() {
|
||||
app.use(globalRateLimit)
|
||||
app.use(sessionMiddleware)
|
||||
app.use(passport.session())
|
||||
app.use(
|
||||
'/api-docs',
|
||||
swaggerUI.serve,
|
||||
swaggerUI.setup(swaggerDocument, {
|
||||
withCredentials: true,
|
||||
})
|
||||
)
|
||||
app.use('/api-docs', swaggerUI.serve, swaggerUI.setup(swaggerDocument))
|
||||
app.use(express.json())
|
||||
|
||||
app.get('/', (req, res) => {
|
||||
@@ -1,9 +0,0 @@
|
||||
export default function authRequired(req, res, next) {
|
||||
if (req.isAuthenticated() && !req.session.newUser) {
|
||||
next()
|
||||
return
|
||||
}
|
||||
res.status(401).json({
|
||||
message: 'not authenticated',
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import type { Request, Response, NextFunction } from 'express'
|
||||
|
||||
export default function authRequired(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
if (req.isAuthenticated() && !('newUser' in req.user)) {
|
||||
next()
|
||||
return
|
||||
}
|
||||
res.status(401).json({
|
||||
message: 'not authenticated',
|
||||
})
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import logger from '../utils/logger.js'
|
||||
|
||||
export function asyncHandler(fn) {
|
||||
return (req, res, next) => {
|
||||
fn(req, res, next).catch(next)
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
export default function errorHandler(err, req, res, next) {
|
||||
logger.error(err) // Log error stack trace to the console
|
||||
res.status(500).send({ message: 'Something broke!' }) // Send error response
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import type { Request, Response, NextFunction } from 'express'
|
||||
import logger from '../utils/logger.js'
|
||||
|
||||
export function asyncHandler(fn: any) {
|
||||
return (req: Request, res: Response, next: NextFunction) => {
|
||||
fn(req, res, next).catch(next)
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
export default function errorHandler(
|
||||
err: any,
|
||||
req: Request,
|
||||
res: Response,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
next: NextFunction
|
||||
) {
|
||||
logger.error(err) // Log error stack trace to the console
|
||||
res.status(500).send({ message: 'Something broke!' }) // Send error response
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
import rateLimit from 'express-rate-limit'
|
||||
import type { Options } from 'express-rate-limit'
|
||||
import type { Request, Response, NextFunction } from 'express'
|
||||
import config from '../config.js'
|
||||
|
||||
function createRateLimiter(options) {
|
||||
function createRateLimiter(options: Partial<Options>) {
|
||||
if (config.testing.enabled) {
|
||||
return (req, res, next) => next()
|
||||
return (req: Request, res: Response, next: NextFunction) => next()
|
||||
}
|
||||
return rateLimit(options)
|
||||
}
|
||||
@@ -21,7 +23,7 @@ export const authenticateRateLimit = createRateLimiter({
|
||||
export const sessionRateLimit = createRateLimiter({
|
||||
windowMs: 10 * 1000,
|
||||
max: 1,
|
||||
keyGenerator: (req) => {
|
||||
return req.user ? req.user._id : req.ip
|
||||
keyGenerator: (req: Request) => {
|
||||
return req.user ? (req.user as any)._id : req.ip || ''
|
||||
},
|
||||
})
|
||||
@@ -9,9 +9,10 @@ export default session({
|
||||
saveUninitialized: false,
|
||||
rolling: true,
|
||||
cookie: {
|
||||
domain: config.domain,
|
||||
httpOnly: true,
|
||||
secure: false,
|
||||
sameSite: 'lax',
|
||||
secure: config.protocol === 'https:',
|
||||
sameSite: 'strict',
|
||||
maxAge: config.sessionMaxAge,
|
||||
},
|
||||
store: MongoStore.create({
|
||||
@@ -1,6 +1,7 @@
|
||||
import { validationResult } from 'express-validator'
|
||||
import type { Request, Response, NextFunction } from 'express'
|
||||
|
||||
export function noError(req, res, next) {
|
||||
export function noError(req: Request, res: Response, next: NextFunction) {
|
||||
const result = validationResult(req)
|
||||
if (result.isEmpty()) {
|
||||
next()
|
||||
@@ -1,28 +1,36 @@
|
||||
import { HydratedDocument, Types } from 'mongoose'
|
||||
import { Session, Quest, UserProfile } from '@linux-odyssey/models'
|
||||
import type { ISession, IUser } from '@linux-odyssey/models'
|
||||
import { createContainer, deleteContainer } from '../containers/docker.js'
|
||||
import SessionHandler from '../game/sessionHandler.js'
|
||||
import logger from '../utils/logger.js'
|
||||
|
||||
async function deactivateSessions(user, quest) {
|
||||
async function deactivateSessions(userId: Types.ObjectId, quest: string) {
|
||||
const sessions = await Session.find({
|
||||
user,
|
||||
user: userId,
|
||||
quest,
|
||||
status: 'active',
|
||||
})
|
||||
return Promise.all(
|
||||
sessions.map((session) => {
|
||||
return deleteContainer(session.containerId)
|
||||
.catch((err) => logger.error('Failed to delete container', err))
|
||||
.finally(() => {
|
||||
session.status = 'inactive'
|
||||
session.containerId = null
|
||||
return session.save()
|
||||
})
|
||||
return (
|
||||
session.containerId &&
|
||||
deleteContainer(session.containerId)
|
||||
.catch((err) => logger.error('Failed to delete container', err))
|
||||
.finally(() => {
|
||||
return Session.findByIdAndUpdate(session._id, {
|
||||
$set: { status: 'inactive', containerId: null },
|
||||
}).catch((err) => logger.error('Failed to update session', err))
|
||||
})
|
||||
)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
export async function createNewSession(user, questId) {
|
||||
export async function createNewSession(
|
||||
user: HydratedDocument<IUser>,
|
||||
questId: string
|
||||
): Promise<ISession> {
|
||||
const quest = await Quest.findById(questId)
|
||||
if (!quest) {
|
||||
throw new Error(`Quest ${questId} not found`)
|
||||
@@ -38,13 +46,13 @@ export async function createNewSession(user, questId) {
|
||||
quest.image
|
||||
)
|
||||
|
||||
const sessionHandler = new SessionHandler(
|
||||
new Session({
|
||||
user,
|
||||
quest,
|
||||
containerId: container.id,
|
||||
})
|
||||
)
|
||||
const newSession = new Session({
|
||||
user,
|
||||
quest,
|
||||
containerId: container.id,
|
||||
})
|
||||
|
||||
const sessionHandler = new SessionHandler(newSession, quest)
|
||||
|
||||
sessionHandler.addNewTasks()
|
||||
|
||||
@@ -62,6 +70,7 @@ export async function createNewSession(user, questId) {
|
||||
quest: quest._id,
|
||||
sessions: [session._id],
|
||||
startedAt: new Date(),
|
||||
completed: false,
|
||||
})
|
||||
} else {
|
||||
progress.sessions.push(session._id)
|
||||
@@ -70,7 +79,10 @@ export async function createNewSession(user, questId) {
|
||||
return session
|
||||
}
|
||||
|
||||
export async function getOrCreateActiveSession(user, questId) {
|
||||
export async function getOrCreateActiveSession(
|
||||
user: HydratedDocument<IUser>,
|
||||
questId: string
|
||||
): Promise<ISession> {
|
||||
const session = await Session.findOne({
|
||||
user,
|
||||
quest: questId,
|
||||
@@ -82,7 +94,9 @@ export async function getOrCreateActiveSession(user, questId) {
|
||||
return createNewSession(user, questId)
|
||||
}
|
||||
|
||||
export async function finishSession(session) {
|
||||
export async function finishSession(
|
||||
session: HydratedDocument<ISession>
|
||||
): Promise<void> {
|
||||
session.status = 'finished'
|
||||
await session.save()
|
||||
|
||||
@@ -102,10 +116,13 @@ export async function finishSession(session) {
|
||||
await userProfile.save()
|
||||
}
|
||||
|
||||
export async function isQuestUnlocked(user, questId) {
|
||||
const userProfile = await UserProfile.findOne({ user: user._id })
|
||||
export async function isQuestUnlocked(
|
||||
user: HydratedDocument<IUser>,
|
||||
questId: string
|
||||
): Promise<boolean> {
|
||||
const userProfile = await UserProfile.findOne({ user: user.id })
|
||||
if (!userProfile) {
|
||||
throw new Error(`UserProfile ${user._id} not found`)
|
||||
throw new Error(`UserProfile ${user.id} not found`)
|
||||
}
|
||||
const quest = await Quest.findById(questId)
|
||||
if (!quest) {
|
||||
@@ -1,7 +1,15 @@
|
||||
import { Express } from 'express'
|
||||
import { User, UserProfile } from '@linux-odyssey/models'
|
||||
import { hashPassword } from '../utils/auth.js'
|
||||
|
||||
export async function createUser(username, email, { password, socialLogin }) {
|
||||
export async function createUser(
|
||||
username: string,
|
||||
email: string,
|
||||
{
|
||||
password,
|
||||
socialLogin,
|
||||
}: { password?: string; socialLogin?: Express.SocialLogin }
|
||||
) {
|
||||
const user = new User({
|
||||
username,
|
||||
email,
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
/* eslint-disable no-undef */
|
||||
/* eslint-disable no-unused-vars */
|
||||
import 'express-session'
|
||||
|
||||
declare module 'express-session' {
|
||||
interface SessionData {
|
||||
newUser?: Express.PendingUser // Adjust the type according to your newUser structure
|
||||
}
|
||||
}
|
||||
Vendored
+25
@@ -0,0 +1,25 @@
|
||||
/* eslint-disable no-unused-vars */
|
||||
declare namespace Express {
|
||||
export type SocialLogin = {
|
||||
provider: string
|
||||
id: string
|
||||
displayName?: string
|
||||
}
|
||||
|
||||
export type PendingUser = {
|
||||
email: string
|
||||
socialLogin: SocialLogin
|
||||
}
|
||||
|
||||
export interface ExistingUser {
|
||||
id: string
|
||||
username: string
|
||||
email: string
|
||||
}
|
||||
|
||||
export interface NewUser {
|
||||
newUser?: PendingUser
|
||||
}
|
||||
|
||||
export type User = ExistingUser | NewUser
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
/* eslint-disable no-unused-vars */
|
||||
declare module 'passport-google-oidc' {
|
||||
// Add specific type declarations here as needed.
|
||||
// For example, if you're only using it as a middleware in Express:
|
||||
import { Strategy as PassportStrategy } from 'passport'
|
||||
import { Request } from 'express'
|
||||
|
||||
interface StrategyOptions {
|
||||
clientID: string
|
||||
clientSecret: string
|
||||
callbackURL: string
|
||||
}
|
||||
|
||||
interface VerifyFunction {
|
||||
(
|
||||
issuer: string,
|
||||
profile: any,
|
||||
done: (error: any, user?: any, info?: any) => void
|
||||
): void
|
||||
}
|
||||
|
||||
export class Strategy extends PassportStrategy {
|
||||
constructor(options: StrategyOptions, verify: VerifyFunction)
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,11 @@ import jwt from 'jsonwebtoken'
|
||||
import bcrypt from 'bcrypt'
|
||||
import config from '../config.js'
|
||||
|
||||
export function hashPassword(password) {
|
||||
export function hashPassword(password: string): Promise<string> {
|
||||
return bcrypt.hash(password, 10)
|
||||
}
|
||||
|
||||
export function genJWT(payload) {
|
||||
export function genJWT(payload: any): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
jwt.sign(
|
||||
payload,
|
||||
@@ -15,6 +15,8 @@ export function genJWT(payload) {
|
||||
(err, token) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
} else if (token === undefined) {
|
||||
reject(new Error('Token is undefined'))
|
||||
} else {
|
||||
resolve(token)
|
||||
}
|
||||
@@ -23,7 +25,7 @@ export function genJWT(payload) {
|
||||
})
|
||||
}
|
||||
|
||||
export function verifyJWT(token) {
|
||||
export function verifyJWT(token: string): Promise<any> {
|
||||
return new Promise((resolve, reject) => {
|
||||
jwt.verify(token, config.secret, (err, decoded) => {
|
||||
if (err) {
|
||||
@@ -1,13 +0,0 @@
|
||||
import crypto from 'crypto'
|
||||
|
||||
export function get(key, defaultValue) {
|
||||
const value = process.env[key]
|
||||
if (value != null) {
|
||||
return value
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
export function genSecret() {
|
||||
return crypto.randomUUID()
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import crypto from 'crypto'
|
||||
|
||||
export function get<T>(key: string, defaultValue: T): T {
|
||||
const value = process.env[key]
|
||||
return value !== undefined ? (value as unknown as T) : defaultValue
|
||||
}
|
||||
|
||||
export function genSecret() {
|
||||
return crypto.randomUUID()
|
||||
}
|
||||
File renamed without changes.
@@ -8,7 +8,10 @@ import logger from './logger.js'
|
||||
const questDirectory = path.join(process.cwd(), '..', 'quests')
|
||||
|
||||
class QuestValidationError extends Error {
|
||||
constructor(message, questId, error) {
|
||||
questId: string
|
||||
error: any
|
||||
|
||||
constructor(message: string, questId: string, error: any) {
|
||||
super(message)
|
||||
this.name = 'QuestValidationError'
|
||||
this.questId = questId
|
||||
@@ -16,7 +19,10 @@ class QuestValidationError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
async function mapQuests(callback) {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
type QuestCallback = (id: string, questPath: string) => Promise<any>
|
||||
|
||||
async function mapQuests(callback: QuestCallback) {
|
||||
const questNames = await fs.readdir(questDirectory, {
|
||||
withFileTypes: true,
|
||||
})
|
||||
@@ -44,7 +50,7 @@ export async function loadAndUpdateQuests() {
|
||||
return mapQuests(async (id, questPath) => {
|
||||
const files = await fs.readdir(questPath)
|
||||
if (!files.includes('game.yml')) {
|
||||
throw new QuestValidationError(`Missing game.yml`, id)
|
||||
throw new QuestValidationError(`Missing game.yml`, id, null)
|
||||
}
|
||||
|
||||
try {
|
||||
File renamed without changes.
Reference in new issue
Block a user