Merge branch 'main' into quest-file

This commit is contained in:
Lin, Hung-Shin committed 2023-11-22 11:39:24 +08:00
commit 21ca083628
46 files changed
+653 -220

No files matched your search

+2 -1
View File
@@ -2,6 +2,7 @@ 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
@@ -31,7 +32,7 @@ export const register = asyncHandler(async (req, res, next) => {
export function logout(req, res) {
req.logout((err) => {
if (err) {
console.error(err)
logger.error(err)
res.status(500).json({
message: 'error logging out',
})
+3 -4
View File
@@ -4,6 +4,7 @@ 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()
@@ -37,7 +38,7 @@ async function connectContainer(socket, next) {
// console.log(socket.handshake)
const { sessionId } = socket.handshake.query
if (!(sessionId && validator.isMongoId(sessionId))) {
console.warn('Invalid Session ID.', sessionId)
logger.warn('Invalid Session ID.', user.username, sessionId)
next(new Error('Invalid Session ID.'))
return
}
@@ -71,8 +72,6 @@ function onConnect(socket) {
socket.emit('terminal', chunk.toString())
})
socket.on('message', console.log)
socket.on('terminal', function incoming(message) {
stream.socket.write(message)
})
@@ -98,6 +97,6 @@ export default (server) => {
io.on('connection', onConnect)
io.on('connect_error', (err) => {
console.error(err)
logger.error('Socket connect error: ', err)
})
}
+2 -1
View File
@@ -4,6 +4,7 @@ import passwordStrategy from './passwordStrategy.js'
import jwtStrategy from './jwtStrategy.js'
import googleStrategy from './googleStrategy.js'
import githubStrategy from './githubStrategy.js'
import logger from '../utils/logger.js'
passport.use(passwordStrategy)
passport.use(jwtStrategy)
@@ -14,7 +15,7 @@ const enabledMethods = {
github: githubStrategy !== null,
}
console.log('Enabled login methods', enabledMethods)
logger.info('Enabled login methods', enabledMethods)
if (enabledMethods.google) {
passport.use(googleStrategy)
+6 -5
View File
@@ -1,12 +1,13 @@
import { getQuestDockerfiles } from '../utils/quest.js'
import { buildQuestImage } from '../containers/docker.js'
import logger from '../utils/logger.js'
async function buildImages() {
const quests = await getQuestDockerfiles()
console.log(`Building ${quests.length} images`)
logger.info(`Building ${quests.length} images`)
return Promise.all(
quests.map(({ id, questPath }) => {
console.log(`Building image for ${id}`)
logger.info(`Building image for ${id}`)
return buildQuestImage(questPath, id)
})
)
@@ -14,11 +15,11 @@ async function buildImages() {
buildImages()
.then((result) => {
console.log(result)
console.log('Done!')
logger.info(result)
logger.info('Done!')
process.exit(0)
})
.catch((err) => {
console.error(err)
logger.error(err)
process.exit(1)
})
+2 -1
View File
@@ -1,6 +1,7 @@
import connectDB from '@linux-odyssey/models'
import { removeExpired } from '../containers/expiryChecker.js'
import config from '../config.js'
import logger from '../utils/logger.js'
connectDB(config.db)
.then(removeExpired)
@@ -8,6 +9,6 @@ connectDB(config.db)
process.exit(0)
})
.catch((err) => {
console.error(err)
logger.error(err)
process.exit(1)
})
+1
View File
@@ -1,3 +1,4 @@
/* eslint-disable no-console */
import { genSecret } from '../utils/env.js'
function setup() {
+4
View File
@@ -46,6 +46,10 @@ const config = {
mountQuest: get('MOUNT_QUEST', ''),
hostPwd: get('HOST_PWD', ''),
},
log: {
path: get('LOG_PATH', '../logs'),
},
}
config.baseUrl = get('BASE_URL', `http://${config.host}:${config.port}`)
+26 -11
View File
@@ -1,5 +1,6 @@
import Docker from 'dockerode'
import config, { getQuestImage } from '../config.js'
import logger from '../utils/logger.js'
const engine = new Docker()
@@ -25,7 +26,7 @@ export function createContainer(name, questId) {
}
const { hostPwd, mountQuest } = config.docker
if (!config.isProduction && hostPwd && mountQuest) {
console.log('Mounting quest folder', mountQuest)
logger.debug('Mounting quest folder', mountQuest)
option.HostConfig.Binds = [
`${hostPwd}/quests/${mountQuest}/home:/home/commander`,
`${hostPwd}/packages/container:/usr/local/lib/container`,
@@ -35,7 +36,7 @@ export function createContainer(name, questId) {
}
export async function getAndStartContainer(id) {
console.log(`Getting container: ${id}`)
logger.debug(`Getting container: ${id}`)
const container = engine.getContainer(id)
if (!container) {
throw new Error(`Container ${id} not found`)
@@ -76,11 +77,21 @@ export async function deleteContainer(id) {
try {
await container.stop()
} catch (error) {
console.warn('Failed to stop container', id)
logger.warn('Failed to stop container', id)
}
await container.remove()
}
function parseJSONOutput(data) {
const { stream, error } = JSON.parse(data.toString())
if (stream) {
logger.info(stream)
}
if (error) {
throw error
}
}
export function buildQuestImage(questPath, questId) {
return new Promise((resolve, reject) => {
engine.buildImage(
@@ -99,23 +110,27 @@ export function buildQuestImage(questPath, questId) {
}
response.on('data', (data) => {
// Process the data (this could be Docker build output)
const { stream, error } = JSON.parse(data.toString())
if (stream) {
console.log(stream)
}
if (error) {
console.error(error)
try {
data
.toString()
.split('\n')
.map((line) => line.trim())
.filter((line) => line !== '')
.forEach((line) => {
parseJSONOutput(line)
})
} catch (error) {
reject(error)
}
})
response.on('end', () => {
console.log(`Build completed for ${questId}`)
logger.info(`Build completed for ${questId}`)
resolve(questId)
})
response.on('error', (error) => {
console.error(`Build failed for ${questId}:`, error)
logger.error(`Build failed for ${questId}:`, error)
reject(error)
})
}
+2 -2
View File
@@ -2,6 +2,7 @@ import schedule from 'node-schedule'
import { Session } from '@linux-odyssey/models'
import config from '../config.js'
import { deleteContainer } from './docker.js'
import logger from '../utils/logger.js'
async function disableSession(session) {
try {
@@ -29,7 +30,6 @@ export async function removeExpired() {
// Find all sessions that are containerId is not null and lastActivityAt is
// less than the current time minus the expiry time
try {
console.log('Checking for expired sessions')
const sessions = await Session.find({
containerId: { $ne: null },
lastActivityAt: { $lt: new Date(Date.now() - config.containerExpiry) },
@@ -42,7 +42,7 @@ export async function removeExpired() {
)
)
} catch (err) {
console.error('Remove expiring failed:', err)
logger.error('Remove expiring failed:', err)
}
}
+2 -1
View File
@@ -4,6 +4,7 @@ import { Quest } from '@linux-odyssey/models'
import { pushToSession } from '../api/socket.js'
import SessionHandler from './sessionHandler.js'
import { checkFile } from '../containers/cli.js'
import logger from '../utils/logger.js'
// 檢查 pattern 是否符合 input
const checkMatch = (pattern, input) => {
@@ -114,7 +115,7 @@ export default class CommandHandler extends SessionHandler {
this.quest = await Quest.findById(this.session.quest)
const stages = this.getStages()
if (stages.length === 0) {
console.error('stage not found', this.session.tasks)
logger.warn('stage not found', this.session.tasks)
return {}
}
+8 -8
View File
@@ -16,21 +16,21 @@ import { globalRateLimit } from './middleware/rateLimiter.js'
import sessionMiddleware from './middleware/session.js'
import expiryRemovalScheduler from './containers/expiryChecker.js'
import setupTest from './utils/setupTest.js'
import logger from './utils/logger.js'
async function main() {
if (!config.secret) {
console.warn(
'No SECRET_KEY found in .env! To set up a persistent key, please run the setup script:'
logger.error(
'No SECRET_KEY found in .env! To set up a persistent key, please run `yarn setup`:'
)
console.warn('yarn setup')
process.exit(1)
}
try {
await connectDB(config.db)
console.log('Connected to MongoDB')
logger.info('Connected to MongoDB')
} catch (err) {
console.error(err)
logger.error(err)
process.exit(1)
}
@@ -53,7 +53,7 @@ async function main() {
'uniquelocal',
...config.trustedProxies,
])
console.log('Trusted proxies:', config.trustedProxies)
logger.info('Trusted proxies:', config.trustedProxies)
app.use(globalRateLimit)
app.use(sessionMiddleware)
app.use(passport.session())
@@ -74,8 +74,8 @@ async function main() {
app.use(errorHandler)
server.listen(config.port, config.host, () => {
console.log(`Server listening at ${config.baseUrl}`)
logger.info(`Server listening at ${config.baseUrl}`)
})
}
main().catch((err) => console.log(err))
main().catch((err) => logger.error(err))
+3 -1
View File
@@ -1,3 +1,5 @@
import logger from '../utils/logger.js'
export function asyncHandler(fn) {
return (req, res, next) => {
fn(req, res, next).catch(next)
@@ -6,6 +8,6 @@ export function asyncHandler(fn) {
// eslint-disable-next-line no-unused-vars
export default function errorHandler(err, req, res, next) {
console.error(err.stack) // Log error stack trace to the console
logger.error(err) // Log error stack trace to the console
res.status(500).send({ message: 'Something broke!' }) // Send error response
}
+3 -2
View File
@@ -1,6 +1,7 @@
import { Session, Quest, UserProfile } 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) {
const sessions = await Session.find({
@@ -11,7 +12,7 @@ async function deactivateSessions(user, quest) {
return Promise.all(
sessions.map((session) => {
return deleteContainer(session.containerId)
.catch(console.error)
.catch((err) => logger.error('Failed to delete container', err))
.finally(() => {
session.status = 'inactive'
session.containerId = null
@@ -29,7 +30,7 @@ export async function createNewSession(user, questId) {
// deactivate all active sessions
deactivateSessions(user._id, quest._id).catch((err) => {
console.error(err)
logger.error(err)
})
const container = await createContainer(
+37
View File
@@ -0,0 +1,37 @@
import winston from 'winston'
import config from '../config.js'
const logger = winston.createLogger({
level: 'info',
format: winston.format.json(),
defaultMeta: { service: 'backend', timestamp: new Date().toISOString() },
transports: [
//
// - Write all logs with importance level of `error` or less to `error.log`
// - Write all logs with importance level of `info` or less to `combined.log`
//
new winston.transports.File({
dirname: config.log.path,
filename: 'error.log',
level: 'error',
}),
new winston.transports.File({
dirname: config.log.path,
filename: 'combined.log',
}),
],
})
//
// If we're not in production then log to the `console` with the format:
// `${info.level}: ${info.message} JSON.stringify({ ...rest }) `
//
if (process.env.NODE_ENV !== 'production') {
logger.add(
new winston.transports.Console({
format: winston.format.simple(),
})
)
}
export default logger
+2 -1
View File
@@ -3,6 +3,7 @@ import path from 'path'
import yaml from 'yaml'
import { Quest } from '@linux-odyssey/models'
import logger from './logger.js'
const questDirectory = path.join(process.cwd(), '..', 'quests')
@@ -45,7 +46,7 @@ export async function loadAndUpdateQuests() {
})
await quest.save()
} catch (error) {
console.error(`Error parsing quest ${id}:`, error.message)
logger.error(`Error parsing quest ${id}:`, error)
throw error
}
})
+4 -3
View File
@@ -1,9 +1,10 @@
import { User } from '@linux-odyssey/models'
import config from '../config.js'
import { createUser } from '../models/userManager.js'
import logger from './logger.js'
export default async function setupTest() {
console.log('Setting up test account')
logger.info('Setting up test account')
if (!(config.testing.username && config.testing.password)) {
throw new Error(
'No testing account found! Please set TESTING_USERNAME and TESTING_PASSWORD in .env'
@@ -13,10 +14,10 @@ export default async function setupTest() {
const email = `${username}@example.com`
if (await User.exists({ username })) {
console.log('Testing account already exists')
logger.info('Testing account already exists')
return
}
await createUser(username, email, { password })
console.log('Created testing account')
logger.info('Created testing account')
}