mirror of
https://github.com/linux-odyssey/linux-odyssey.git
synced 2026-11-08 03:20:12 +08:00
24 files changed
+391
-148
No files matched your search
@@ -13,7 +13,8 @@
|
|||||||
"@linux-odyssey/models": "*",
|
"@linux-odyssey/models": "*",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"express-basic-auth": "^1.2.1",
|
"express-basic-auth": "^1.2.1",
|
||||||
"express-handlebars": "^7.1.2"
|
"express-handlebars": "^7.1.2",
|
||||||
|
"mongoose": "^8.0.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"nodemon": "^3.0.1"
|
"nodemon": "^3.0.1"
|
||||||
|
|||||||
Whitespace-only changes.
@@ -1,9 +1,24 @@
|
|||||||
|
/* eslint-disable object-shorthand */
|
||||||
|
import Pagination from '../models/pagination.js'
|
||||||
import { errorCommands } from '../models/commands.js'
|
import { errorCommands } from '../models/commands.js'
|
||||||
|
|
||||||
// eslint-disable-next-line import/prefer-default-export
|
// eslint-disable-next-line import/prefer-default-export
|
||||||
export async function commandListController(req, res) {
|
export async function commandListController(req, res) {
|
||||||
const payload = {
|
const { nextKey, order } = req.query
|
||||||
errorCommands: await errorCommands(),
|
const itemsPerPage = 100
|
||||||
|
|
||||||
|
try {
|
||||||
|
const pagination = new Pagination(order, itemsPerPage, nextKey)
|
||||||
|
const payload = await errorCommands(pagination)
|
||||||
|
const newNextKey = payload[payload.length - 1]?._id
|
||||||
|
|
||||||
|
res.render('commands', {
|
||||||
|
errorCommands: payload,
|
||||||
|
nextKey: newNextKey,
|
||||||
|
order: pagination.getOrder(),
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
res.status(500).send('Error fetching data')
|
||||||
}
|
}
|
||||||
res.render('commands', payload)
|
|
||||||
}
|
}
|
||||||
@@ -1,15 +1,24 @@
|
|||||||
import { userCount } from '../models/users.js'
|
import { userCount } from '../models/users.js'
|
||||||
import { sessionCount, sessionCompleted } from '../models/sessions.js'
|
import { questList } from '../models/quests.js'
|
||||||
|
|
||||||
// eslint-disable-next-line import/prefer-default-export
|
// eslint-disable-next-line import/prefer-default-export
|
||||||
export async function homeController(req, res) {
|
export async function homeController(req, res) {
|
||||||
|
const quests = await questList()
|
||||||
|
const [totalQuests, totalCompleted] = quests.reduce(
|
||||||
|
([total, completed], quest) => [
|
||||||
|
total + quest.started,
|
||||||
|
completed + quest.completed,
|
||||||
|
],
|
||||||
|
[0, 0]
|
||||||
|
)
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
title: 'Linux Odyssey Admin',
|
title: 'Linux Odyssey Admin',
|
||||||
userCount: await userCount(),
|
userCount: await userCount(),
|
||||||
sessionCount: await sessionCount(),
|
totalQuests,
|
||||||
sessionCompleted: await sessionCompleted(),
|
totalCompleted,
|
||||||
}
|
}
|
||||||
payload.completedRate =
|
payload.completedRate =
|
||||||
Math.round((payload.sessionCompleted / payload.sessionCount) * 10000) / 100
|
Math.round((payload.totalCompleted / payload.totalQuests) * 10000) / 100
|
||||||
res.render('home', payload)
|
res.render('home', payload)
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { questList } from '../models/quests.js'
|
||||||
|
|
||||||
|
// eslint-disable-next-line import/prefer-default-export
|
||||||
|
export async function questListController(req, res) {
|
||||||
|
const quests = await questList()
|
||||||
|
quests.forEach((quest) => {
|
||||||
|
quest.failed = quest.started - quest.completed
|
||||||
|
})
|
||||||
|
res.render('quests', { quests })
|
||||||
|
}
|
||||||
@@ -1,8 +1,24 @@
|
|||||||
|
import Pagination from '../models/pagination.js'
|
||||||
import { sessionList, sessionDetail } from '../models/sessions.js'
|
import { sessionList, sessionDetail } from '../models/sessions.js'
|
||||||
|
|
||||||
export async function sessionListController(req, res) {
|
export async function sessionListController(req, res) {
|
||||||
const sessions = await sessionList()
|
const { nextKey, order } = req.query
|
||||||
res.render('sessions', { sessions })
|
const itemsPerPage = 50
|
||||||
|
|
||||||
|
try {
|
||||||
|
const pagination = new Pagination(order, itemsPerPage, nextKey)
|
||||||
|
const sessions = await sessionList(pagination)
|
||||||
|
const newNextKey = sessions[sessions.length - 1]?._id
|
||||||
|
|
||||||
|
res.render('sessions', {
|
||||||
|
sessions,
|
||||||
|
nextKey: newNextKey,
|
||||||
|
order: pagination.getOrder(),
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
res.status(500).send('Error fetching session data')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function sessionDetailController(req, res) {
|
export async function sessionDetailController(req, res) {
|
||||||
|
|||||||
@@ -1,6 +1,22 @@
|
|||||||
|
import Pagination from '../models/pagination.js'
|
||||||
import { userList } from '../models/users.js'
|
import { userList } from '../models/users.js'
|
||||||
|
|
||||||
// eslint-disable-next-line import/prefer-default-export
|
|
||||||
export async function userListController(req, res) {
|
export async function userListController(req, res) {
|
||||||
res.render('users', { users: await userList() })
|
const { nextKey, order } = req.query
|
||||||
|
const itemsPerPage = 50
|
||||||
|
|
||||||
|
try {
|
||||||
|
const pagination = new Pagination(order, itemsPerPage, nextKey)
|
||||||
|
const users = await userList(pagination)
|
||||||
|
const newNextKey = users[users.length - 1]?._id
|
||||||
|
|
||||||
|
res.render('users', {
|
||||||
|
users,
|
||||||
|
nextKey: newNextKey,
|
||||||
|
order: pagination.getOrder(),
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
res.status(500).send('Error fetching user data')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,22 +1,44 @@
|
|||||||
import { Command } from '@linux-odyssey/models'
|
import { Command } from '@linux-odyssey/models'
|
||||||
|
|
||||||
// eslint-disable-next-line import/prefer-default-export
|
// eslint-disable-next-line import/prefer-default-export
|
||||||
export async function errorCommands() {
|
export async function errorCommands(pagination) {
|
||||||
const commands = await Command.find({
|
const commands = await Command.aggregate([
|
||||||
$and: [
|
pagination.match('_id'),
|
||||||
{ error: { $exists: true } },
|
{
|
||||||
{ error: { $ne: null } },
|
$match: {
|
||||||
{ error: { $ne: '' } },
|
$and: [
|
||||||
],
|
{ error: { $exists: true } },
|
||||||
})
|
{ error: { $ne: null } },
|
||||||
.populate({
|
{ error: { $ne: '' } },
|
||||||
path: 'session',
|
],
|
||||||
populate: { path: 'user' },
|
},
|
||||||
})
|
},
|
||||||
.exec()
|
{
|
||||||
return commands.map(({ command, error, createdAt, session }) => {
|
$lookup: {
|
||||||
|
from: 'sessions', // 替換成 'session' 對應的集合名稱
|
||||||
|
localField: 'session', // 替換成 'session' 對應的字段名稱
|
||||||
|
foreignField: '_id',
|
||||||
|
as: 'session',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ $unwind: '$session' },
|
||||||
|
{
|
||||||
|
$lookup: {
|
||||||
|
from: 'users', // 替換成 'user' 對應的集合名稱
|
||||||
|
localField: 'session.user', // 替換成 'user' 對應的字段名稱
|
||||||
|
foreignField: '_id',
|
||||||
|
as: 'session.user',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ $unwind: '$session.user' },
|
||||||
|
pagination.sort('_id'),
|
||||||
|
pagination.limit(),
|
||||||
|
])
|
||||||
|
|
||||||
|
return commands.map(({ _id, command, error, createdAt, session }) => {
|
||||||
const { user, quest } = session
|
const { user, quest } = session
|
||||||
return {
|
return {
|
||||||
|
_id,
|
||||||
command: command.slice(0, 20),
|
command: command.slice(0, 20),
|
||||||
error: error.slice(0, 50),
|
error: error.slice(0, 50),
|
||||||
createdAt: createdAt?.toLocaleString(),
|
createdAt: createdAt?.toLocaleString(),
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
/* eslint-disable lines-between-class-members */
|
||||||
|
/* eslint-disable class-methods-use-this */
|
||||||
|
// eslint-disable-next-line max-classes-per-file
|
||||||
|
import mongoose from 'mongoose'
|
||||||
|
|
||||||
|
export const DESC = 'desc'
|
||||||
|
export const ASC = 'asc'
|
||||||
|
|
||||||
|
class Asc {
|
||||||
|
getOperator() {
|
||||||
|
return '$gt'
|
||||||
|
}
|
||||||
|
|
||||||
|
getOrder() {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
getName() {
|
||||||
|
return ASC
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Desc {
|
||||||
|
getOperator() {
|
||||||
|
return '$lt'
|
||||||
|
}
|
||||||
|
|
||||||
|
getOrder() {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
getName() {
|
||||||
|
return DESC
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class Pagination {
|
||||||
|
nextKey = null
|
||||||
|
itemsPerPage = 50
|
||||||
|
order = null
|
||||||
|
|
||||||
|
constructor(order, itemsPerPage = 50, nextKey = null) {
|
||||||
|
this.itemsPerPage = itemsPerPage
|
||||||
|
if (!order || order === DESC) {
|
||||||
|
this.order = new Desc()
|
||||||
|
} else if (order === ASC) {
|
||||||
|
this.order = new Asc()
|
||||||
|
} else {
|
||||||
|
throw new Error('order must be asc or desc')
|
||||||
|
}
|
||||||
|
if (nextKey) {
|
||||||
|
this.nextKey = new mongoose.Types.ObjectId(nextKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match(field) {
|
||||||
|
const stage = this.nextKey
|
||||||
|
? { [field]: { [this.order.getOperator()]: this.nextKey } }
|
||||||
|
: {}
|
||||||
|
return { $match: stage }
|
||||||
|
}
|
||||||
|
|
||||||
|
sort(field) {
|
||||||
|
return { $sort: { [field]: this.order.getOrder() } }
|
||||||
|
}
|
||||||
|
|
||||||
|
limit() {
|
||||||
|
return { $limit: this.itemsPerPage }
|
||||||
|
}
|
||||||
|
|
||||||
|
getOrder() {
|
||||||
|
return this.order.getName()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
/* eslint-disable object-shorthand */
|
||||||
|
/* eslint-disable func-names */
|
||||||
|
import { UserProfile } from '@linux-odyssey/models'
|
||||||
|
|
||||||
|
export function questList() {
|
||||||
|
return UserProfile.aggregate([
|
||||||
|
{
|
||||||
|
$project: {
|
||||||
|
progress: {
|
||||||
|
$objectToArray: '$progress',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$unwind: '$progress',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$group: {
|
||||||
|
_id: '$progress.v.quest',
|
||||||
|
started: {
|
||||||
|
$sum: 1,
|
||||||
|
},
|
||||||
|
completed: {
|
||||||
|
$sum: {
|
||||||
|
$cond: {
|
||||||
|
if: { $eq: ['$progress.v.completed', true] },
|
||||||
|
then: 1,
|
||||||
|
else: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$project: {
|
||||||
|
_id: 1,
|
||||||
|
started: 1,
|
||||||
|
completed: 1,
|
||||||
|
completedRate: {
|
||||||
|
$cond: {
|
||||||
|
if: { $eq: ['$started', 0] },
|
||||||
|
then: 0,
|
||||||
|
else: {
|
||||||
|
$round: [
|
||||||
|
{ $multiply: [{ $divide: ['$completed', '$started'] }, 100] },
|
||||||
|
0,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$sort: {
|
||||||
|
completed: -1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
|
}
|
||||||
@@ -1,28 +1,5 @@
|
|||||||
import { Command, Session } from '@linux-odyssey/models'
|
import { Command, Session } from '@linux-odyssey/models'
|
||||||
|
|
||||||
export function sessionCount() {
|
|
||||||
return Session.find().count()
|
|
||||||
}
|
|
||||||
|
|
||||||
export function sessionCompleted() {
|
|
||||||
return Session.find({ status: 'finished' }).count()
|
|
||||||
}
|
|
||||||
|
|
||||||
export function averageTimeUsage() {
|
|
||||||
return Session.aggregate([
|
|
||||||
{
|
|
||||||
$group: {
|
|
||||||
_id: '$quest',
|
|
||||||
average: {
|
|
||||||
$avg: {
|
|
||||||
$subtract: ['$finishedAt', '$createdAt'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatTime(time) {
|
function formatTime(time) {
|
||||||
const seconds = Math.floor(time / 1000)
|
const seconds = Math.floor(time / 1000)
|
||||||
const minutes = Math.floor(seconds / 60)
|
const minutes = Math.floor(seconds / 60)
|
||||||
@@ -30,8 +7,9 @@ function formatTime(time) {
|
|||||||
return `${hours}h ${minutes % 60}m ${seconds % 60}s`
|
return `${hours}h ${minutes % 60}m ${seconds % 60}s`
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function sessionList() {
|
export async function sessionList(pagination) {
|
||||||
const sessions = await Session.aggregate([
|
const sessions = await Session.aggregate([
|
||||||
|
pagination.match('_id'),
|
||||||
{
|
{
|
||||||
$lookup: {
|
$lookup: {
|
||||||
from: 'users',
|
from: 'users',
|
||||||
@@ -49,7 +27,10 @@ export async function sessionList() {
|
|||||||
as: 'commands',
|
as: 'commands',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
pagination.sort('_id'),
|
||||||
|
pagination.limit(),
|
||||||
])
|
])
|
||||||
|
|
||||||
return sessions.map(
|
return sessions.map(
|
||||||
({
|
({
|
||||||
_id,
|
_id,
|
||||||
|
|||||||
@@ -11,8 +11,9 @@ export function userCount() {
|
|||||||
return User.find().count()
|
return User.find().count()
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function userList() {
|
export async function userList(pagination) {
|
||||||
const users = await Session.aggregate([
|
const users = await Session.aggregate([
|
||||||
|
pagination.match('user'),
|
||||||
{
|
{
|
||||||
$group: {
|
$group: {
|
||||||
_id: '$user',
|
_id: '$user',
|
||||||
@@ -29,11 +30,14 @@ export async function userList() {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{ $unwind: '$userData' },
|
{ $unwind: '$userData' },
|
||||||
{ $sort: { 'userData.createdAt': 1 } },
|
pagination.sort('userData._id'),
|
||||||
|
pagination.limit(),
|
||||||
])
|
])
|
||||||
|
|
||||||
return users.map(({ userData, count, lastActivityAt }) => {
|
return users.map(({ userData, count, lastActivityAt }) => {
|
||||||
const { username, email, createdAt } = userData
|
const { _id, username, email, createdAt } = userData
|
||||||
return {
|
return {
|
||||||
|
_id,
|
||||||
username,
|
username,
|
||||||
email,
|
email,
|
||||||
createdAt: createdAt?.toLocaleString(),
|
createdAt: createdAt?.toLocaleString(),
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Router } from 'express'
|
|||||||
|
|
||||||
import { homeController } from './controllers/index.js'
|
import { homeController } from './controllers/index.js'
|
||||||
import { userListController } from './controllers/users.js'
|
import { userListController } from './controllers/users.js'
|
||||||
|
import { questListController } from './controllers/quests.js'
|
||||||
import { commandListController } from './controllers/commands.js'
|
import { commandListController } from './controllers/commands.js'
|
||||||
import {
|
import {
|
||||||
sessionDetailController,
|
sessionDetailController,
|
||||||
@@ -12,6 +13,7 @@ const router = Router()
|
|||||||
|
|
||||||
router.get('/', homeController)
|
router.get('/', homeController)
|
||||||
router.get('/users', userListController)
|
router.get('/users', userListController)
|
||||||
|
router.get('/quests', questListController)
|
||||||
router.get('/commands', commandListController)
|
router.get('/commands', commandListController)
|
||||||
router.get('/sessions', sessionListController)
|
router.get('/sessions', sessionListController)
|
||||||
router.get('/sessions/:id', sessionDetailController)
|
router.get('/sessions/:id', sessionDetailController)
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import mongoose from 'mongoose'
|
||||||
|
import Pagination, { ASC, DESC } from '../src/models/pagination.js'
|
||||||
|
|
||||||
|
describe('Pagination', () => {
|
||||||
|
it('should failed when not provide order', () => {
|
||||||
|
expect(new Pagination('').getOrder()).toBe(DESC)
|
||||||
|
expect(() => new Pagination('Hahaha')).toThrow()
|
||||||
|
})
|
||||||
|
it('should return correct limit', () => {
|
||||||
|
const pagination = new Pagination(DESC, 10)
|
||||||
|
|
||||||
|
expect(pagination.limit()).toMatchObject({ $limit: 10 })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('desc should return correct order', async () => {
|
||||||
|
const pagination = new Pagination(DESC, 10)
|
||||||
|
|
||||||
|
expect(pagination.sort('_id')).toMatchObject({ $sort: { _id: -1 } })
|
||||||
|
expect(pagination.sort('time')).toMatchObject({ $sort: { time: -1 } })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('asc should return correct order', async () => {
|
||||||
|
const pagination = new Pagination(ASC, 10)
|
||||||
|
|
||||||
|
expect(pagination.sort('_id')).toMatchObject({ $sort: { _id: 1 } })
|
||||||
|
expect(pagination.sort('time')).toMatchObject({ $sort: { time: 1 } })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return correct match stage for empty nextKey', async () => {
|
||||||
|
const pagination = new Pagination(DESC, 10)
|
||||||
|
|
||||||
|
expect(pagination.match('_id')).toMatchObject({ $match: {} })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return correct match stage for empty nextKey', async () => {
|
||||||
|
const nextKeyStr = '656c7fea28a4c087bfcefe12'
|
||||||
|
const pagination = new Pagination(DESC, 10, nextKeyStr)
|
||||||
|
const nextKey = new mongoose.Types.ObjectId(nextKeyStr)
|
||||||
|
|
||||||
|
expect(pagination.match('_id')).toMatchObject({
|
||||||
|
$match: {
|
||||||
|
_id: {
|
||||||
|
$lt: nextKey,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -1,6 +1,10 @@
|
|||||||
<h1>Commands</h1>
|
<h1>Commands</h1>
|
||||||
<div>
|
<div>
|
||||||
<h2>Error commands</h2>
|
<h2>Error commands</h2>
|
||||||
|
<div class='pagination'>
|
||||||
|
<a href='/commands?order=asc'>Ascending</a>
|
||||||
|
<a href='/commands?order=desc'>Descending</a>
|
||||||
|
</div>
|
||||||
<table>
|
<table>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Command</th>
|
<th>Command</th>
|
||||||
@@ -21,4 +25,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
</table>
|
</table>
|
||||||
|
<div class='pagination'>
|
||||||
|
<a href='/commands?nextKey={{nextKey}}&order={{order}}'>Next</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1,18 +1,18 @@
|
|||||||
<h1>Welcome to {{title}}</h1>
|
<h1>Welcome to {{title}}</h1>
|
||||||
<div>
|
<div>
|
||||||
<h2>Users</h2>
|
<h2>Overview</h2>
|
||||||
<table>
|
<table>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Registered Users</th>
|
<th>Registered Users</th>
|
||||||
<td>{{userCount}}</td>
|
<td>{{userCount}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Session Count</th>
|
<th>Total Started Quests</th>
|
||||||
<td>{{sessionCount}}</td>
|
<td>{{totalQuests}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Completed Session</th>
|
<th>Completed Quests</th>
|
||||||
<td>{{sessionCompleted}}</td>
|
<td>{{totalCompleted}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Completed Rate</th>
|
<th>Completed Rate</th>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
<ul>
|
<ul>
|
||||||
<li><a href='/'>Home</a></li>
|
<li><a href='/'>Home</a></li>
|
||||||
<li><a href='/users'>Users</a></li>
|
<li><a href='/users'>Users</a></li>
|
||||||
|
<li><a href='/quests'>Quests</a></li>
|
||||||
<li><a href='/commands'>Commands</a></li>
|
<li><a href='/commands'>Commands</a></li>
|
||||||
<li><a href='/sessions'>Sessions</a></li>
|
<li><a href='/sessions'>Sessions</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<h1>Quests</h1>
|
||||||
|
<div>
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<th>Id</th>
|
||||||
|
<th>Started</th>
|
||||||
|
<th>Completed</th>
|
||||||
|
<th>Failed</th>
|
||||||
|
<th>Completed Rate</th>
|
||||||
|
</tr>
|
||||||
|
{{#each quests}}
|
||||||
|
<tr>
|
||||||
|
<td>{{_id}}</td>
|
||||||
|
<td>{{started}}</td>
|
||||||
|
<td>{{completed}}</td>
|
||||||
|
<td>{{failed}}</td>
|
||||||
|
<td>{{completedRate}} %</td>
|
||||||
|
</tr>
|
||||||
|
{{/each}}
|
||||||
|
</table>
|
||||||
@@ -36,20 +36,20 @@
|
|||||||
|
|
||||||
<table>
|
<table>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Command</th>
|
<th>Stage</th>
|
||||||
<th>PWD</th>
|
<th>PWD</th>
|
||||||
|
<th>Input</th>
|
||||||
<th>Output</th>
|
<th>Output</th>
|
||||||
<th>Error</th>
|
<th>Error</th>
|
||||||
<th>Stage</th>
|
<th>Input Time</th>
|
||||||
<th>Created At</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
{{#each commands}}
|
{{#each commands}}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{command}}</td>
|
<td>{{stage}}</td>
|
||||||
<td>{{pwd}}</td>
|
<td>{{pwd}}</td>
|
||||||
|
<td>{{command}}</td>
|
||||||
<td>{{output}}</td>
|
<td>{{output}}</td>
|
||||||
<td>{{error}}</td>
|
<td>{{error}}</td>
|
||||||
<td>{{stage}}</td>
|
|
||||||
<td>{{createdAt}}</td>
|
<td>{{createdAt}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
|
|||||||
@@ -1,27 +1,34 @@
|
|||||||
<h1>Sessions</h1>
|
<h1>Sessions</h1>
|
||||||
|
<div class='pagination'>
|
||||||
|
<a href='/sessions?order=asc'>Ascending</a>
|
||||||
|
<a href='/sessions?order=desc'>Descending</a>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<table>
|
<table>
|
||||||
<tr>
|
<tr>
|
||||||
<th>User</th>
|
<th>User</th>
|
||||||
<th>Quest</th>
|
<th>Quest</th>
|
||||||
<th></th>
|
|
||||||
<th>Status</th>
|
<th>Status</th>
|
||||||
<th>Created At</th>
|
<th>Created At</th>
|
||||||
<th>Finished At</th>
|
<th>Finished At</th>
|
||||||
<th>Used Time</th>
|
<th>Used Time</th>
|
||||||
<th>Command Count</th>
|
<th>Command Count</th>
|
||||||
|
<th>Session</th>
|
||||||
</tr>
|
</tr>
|
||||||
{{#each sessions}}
|
{{#each sessions}}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{user}}</td>
|
<td>{{user}}</td>
|
||||||
<td>{{quest}}</td>
|
<td>{{quest}}</td>
|
||||||
<td><a href='/sessions/{{_id}}'>Detail</a></td>
|
|
||||||
<td>{{status}}</td>
|
<td>{{status}}</td>
|
||||||
<td>{{createdAt}}</td>
|
<td>{{createdAt}}</td>
|
||||||
<td>{{finishedAt}}</td>
|
<td>{{finishedAt}}</td>
|
||||||
<td>{{usedTime}}</td>
|
<td>{{usedTime}}</td>
|
||||||
<td>{{commandCount}}</td>
|
<td>{{commandCount}}</td>
|
||||||
|
<td><a href='/sessions/{{_id}}'>Detail</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
</table>
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class='pagination'>
|
||||||
|
<a href='/sessions?nextKey={{nextKey}}&order={{order}}'>Next</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -1,23 +1,30 @@
|
|||||||
<h1>Users List</h1>
|
<h1>Users List</h1>
|
||||||
|
<div class='pagination'>
|
||||||
|
<a href='/users?order=asc'>Ascending</a>
|
||||||
|
<a href='/users?order=desc'>Descending</a>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<table>
|
<table>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Username</th>
|
<th>Username</th>
|
||||||
<th>Email</th>
|
|
||||||
<th>Registration Method</th>
|
|
||||||
<th>Registration Time</th>
|
|
||||||
<th>Sessions Count</th>
|
|
||||||
<th>Last Activity</th>
|
<th>Last Activity</th>
|
||||||
|
<th>Sessions Count</th>
|
||||||
|
<th>Email</th>
|
||||||
|
<th>Registration Time</th>
|
||||||
|
<th>Registration Method</th>
|
||||||
</tr>
|
</tr>
|
||||||
{{#each users}}
|
{{#each users}}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{username}}</td>
|
<td href='/users/{{_id}}'>{{username}}</td>
|
||||||
<td>{{email}}</td>
|
|
||||||
<td>{{method}}</td>
|
|
||||||
<td>{{createdAt}}</td>
|
|
||||||
<td>{{sessionCount}}</td>
|
|
||||||
<td>{{lastActivityAt}}</td>
|
<td>{{lastActivityAt}}</td>
|
||||||
|
<td>{{sessionCount}}</td>
|
||||||
|
<td>{{email}}</td>
|
||||||
|
<td>{{createdAt}}</td>
|
||||||
|
<td>{{method}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
</table>
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class='pagination'>
|
||||||
|
<a href='/users?nextKey={{nextKey}}&order={{order}}'>Next</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -6,6 +6,6 @@
|
|||||||
"license": "GPL",
|
"license": "GPL",
|
||||||
"exports": "./index.js",
|
"exports": "./index.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"mongoose": "^7.6.0"
|
"mongoose": "^8.0.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+1
-1
@@ -26,7 +26,7 @@
|
|||||||
"express-validator": "^7.0.1",
|
"express-validator": "^7.0.1",
|
||||||
"jsonwebtoken": "^9.0.1",
|
"jsonwebtoken": "^9.0.1",
|
||||||
"minimist": "^1.2.8",
|
"minimist": "^1.2.8",
|
||||||
"mongoose": "^7.4.0",
|
"mongoose": "^8.0.3",
|
||||||
"node-schedule": "^2.1.1",
|
"node-schedule": "^2.1.1",
|
||||||
"passport": "^0.6.0",
|
"passport": "^0.6.0",
|
||||||
"passport-github2": "^0.1.12",
|
"passport-github2": "^0.1.12",
|
||||||
|
|||||||
@@ -1756,15 +1756,10 @@ bser@2.1.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
node-int64 "^0.4.0"
|
node-int64 "^0.4.0"
|
||||||
|
|
||||||
bson@^5.4.0:
|
bson@^6.2.0:
|
||||||
version "5.4.0"
|
version "6.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/bson/-/bson-5.4.0.tgz#0eea77276d490953ad8616b483298dbff07384c6"
|
resolved "https://registry.yarnpkg.com/bson/-/bson-6.2.0.tgz#4b6acafc266ba18eeee111373c2699304a9ba0a3"
|
||||||
integrity sha512-WRZ5SQI5GfUuKnPTNmAYPiKIof3ORXAF4IRU5UcgmivNIon01rWQlw5RUH954dpu8yGL8T59YShVddIPaU/gFA==
|
integrity sha512-ID1cI+7bazPDyL9wYy9GaQ8gEEohWvcUl/Yf0dIdutJxnmInEEyCsb4awy/OiBfall7zBA179Pahi3vCdFze3Q==
|
||||||
|
|
||||||
bson@^5.5.0:
|
|
||||||
version "5.5.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/bson/-/bson-5.5.0.tgz#a419cc69f368d2def3b8b22ea03ed1c9be40e53f"
|
|
||||||
integrity sha512-B+QB4YmDx9RStKv8LLSl/aVIEV3nYJc3cJNNTK2Cd1TL+7P+cNpw9mAPeCgc5K+j01Dv6sxUzcITXDx7ZU3F0w==
|
|
||||||
|
|
||||||
buffer-crc32@~0.2.3:
|
buffer-crc32@~0.2.3:
|
||||||
version "0.2.13"
|
version "0.2.13"
|
||||||
@@ -3668,11 +3663,6 @@ internal-slot@^1.0.5:
|
|||||||
has "^1.0.3"
|
has "^1.0.3"
|
||||||
side-channel "^1.0.4"
|
side-channel "^1.0.4"
|
||||||
|
|
||||||
ip@^2.0.0:
|
|
||||||
version "2.0.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/ip/-/ip-2.0.0.tgz#4cf4ab182fee2314c75ede1276f8c80b479936da"
|
|
||||||
integrity sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==
|
|
||||||
|
|
||||||
ipaddr.js@1.9.1:
|
ipaddr.js@1.9.1:
|
||||||
version "1.9.1"
|
version "1.9.1"
|
||||||
resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3"
|
resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3"
|
||||||
@@ -4833,49 +4823,23 @@ mongodb-connection-string-url@^2.6.0:
|
|||||||
"@types/whatwg-url" "^8.2.1"
|
"@types/whatwg-url" "^8.2.1"
|
||||||
whatwg-url "^11.0.0"
|
whatwg-url "^11.0.0"
|
||||||
|
|
||||||
mongodb@5.7.0:
|
mongodb@6.2.0:
|
||||||
version "5.7.0"
|
version "6.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/mongodb/-/mongodb-5.7.0.tgz#e16d2fcdfd9f8503ec2d88288392dc3235bb3ecc"
|
resolved "https://registry.yarnpkg.com/mongodb/-/mongodb-6.2.0.tgz#2c9dcb3eeaf528ed850e94b3df392de6c6b0d7ab"
|
||||||
integrity sha512-zm82Bq33QbqtxDf58fLWBwTjARK3NSvKYjyz997KSy6hpat0prjeX/kxjbPVyZY60XYPDNETaHkHJI2UCzSLuw==
|
integrity sha512-d7OSuGjGWDZ5usZPqfvb36laQ9CPhnWkAGHT61x5P95p/8nMVeH8asloMwW6GcYFeB0Vj4CB/1wOTDG2RA9BFA==
|
||||||
dependencies:
|
dependencies:
|
||||||
bson "^5.4.0"
|
|
||||||
mongodb-connection-string-url "^2.6.0"
|
|
||||||
socks "^2.7.1"
|
|
||||||
optionalDependencies:
|
|
||||||
saslprep "^1.0.3"
|
|
||||||
|
|
||||||
mongodb@5.9.0:
|
|
||||||
version "5.9.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/mongodb/-/mongodb-5.9.0.tgz#5a22065fa8cfaf1d58bf2e3c451cd2c4bfa983a2"
|
|
||||||
integrity sha512-g+GCMHN1CoRUA+wb1Agv0TI4YTSiWr42B5ulkiAfLLHitGK1R+PkSAf3Lr5rPZwi/3F04LiaZEW0Kxro9Fi2TA==
|
|
||||||
dependencies:
|
|
||||||
bson "^5.5.0"
|
|
||||||
mongodb-connection-string-url "^2.6.0"
|
|
||||||
socks "^2.7.1"
|
|
||||||
optionalDependencies:
|
|
||||||
"@mongodb-js/saslprep" "^1.1.0"
|
"@mongodb-js/saslprep" "^1.1.0"
|
||||||
|
bson "^6.2.0"
|
||||||
|
mongodb-connection-string-url "^2.6.0"
|
||||||
|
|
||||||
mongoose@^7.4.0:
|
mongoose@^8.0.3:
|
||||||
version "7.4.0"
|
version "8.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/mongoose/-/mongoose-7.4.0.tgz#3726a8dea0c9e2d54651845cc84c813a6507906a"
|
resolved "https://registry.yarnpkg.com/mongoose/-/mongoose-8.0.3.tgz#f3708ba0f40b6dec8f5f1a5d40abd69a80207114"
|
||||||
integrity sha512-oHE1eqodfKzugXRlQxpo+msIea7jPcRoayDuEMr50+bYwM/juA5f+1stjkWlXcg6vo1PdJFVA6DGaKOPLuG5mA==
|
integrity sha512-LJRT0yP4TW14HT4r2RkxqyvoTylMSzWpl5QOeVHTnRggCLQSpkoBdgbUtORFq/mSL2o9cLCPJz+6uzFj25qbHw==
|
||||||
dependencies:
|
dependencies:
|
||||||
bson "^5.4.0"
|
bson "^6.2.0"
|
||||||
kareem "2.5.1"
|
kareem "2.5.1"
|
||||||
mongodb "5.7.0"
|
mongodb "6.2.0"
|
||||||
mpath "0.9.0"
|
|
||||||
mquery "5.0.0"
|
|
||||||
ms "2.1.3"
|
|
||||||
sift "16.0.1"
|
|
||||||
|
|
||||||
mongoose@^7.6.0:
|
|
||||||
version "7.6.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/mongoose/-/mongoose-7.6.0.tgz#3037b754ccdf168385e52d4bcabeb7f5c3b17a5f"
|
|
||||||
integrity sha512-ztQ12rm0BQN5i7LB6xhWX4l9a9w2aa3jEwa/mM2vAutYJRyAwOzcusvKJBULMzFHyUDBOVW15grisexypgMIWA==
|
|
||||||
dependencies:
|
|
||||||
bson "^5.4.0"
|
|
||||||
kareem "2.5.1"
|
|
||||||
mongodb "5.9.0"
|
|
||||||
mpath "0.9.0"
|
mpath "0.9.0"
|
||||||
mquery "5.0.0"
|
mquery "5.0.0"
|
||||||
ms "2.1.3"
|
ms "2.1.3"
|
||||||
@@ -5758,13 +5722,6 @@ safe-stable-stringify@^2.3.1:
|
|||||||
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
|
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
|
||||||
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
|
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
|
||||||
|
|
||||||
saslprep@^1.0.3:
|
|
||||||
version "1.0.3"
|
|
||||||
resolved "https://registry.yarnpkg.com/saslprep/-/saslprep-1.0.3.tgz#4c02f946b56cf54297e347ba1093e7acac4cf226"
|
|
||||||
integrity sha512-/MY/PEMbk2SuY5sScONwhUDsV2p77Znkb/q3nSVstq/yQzYJOH/Azh29p9oJLsl3LnQwSvZDKagDGBsBwSooag==
|
|
||||||
dependencies:
|
|
||||||
sparse-bitfield "^3.0.3"
|
|
||||||
|
|
||||||
semver@^6.0.0, semver@^6.3.0, semver@^6.3.1:
|
semver@^6.0.0, semver@^6.3.0, semver@^6.3.1:
|
||||||
version "6.3.1"
|
version "6.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4"
|
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4"
|
||||||
@@ -5913,11 +5870,6 @@ slice-ansi@^4.0.0:
|
|||||||
astral-regex "^2.0.0"
|
astral-regex "^2.0.0"
|
||||||
is-fullwidth-code-point "^3.0.0"
|
is-fullwidth-code-point "^3.0.0"
|
||||||
|
|
||||||
smart-buffer@^4.2.0:
|
|
||||||
version "4.2.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.2.0.tgz#6e1d71fa4f18c05f7d0ff216dd16a481d0e8d9ae"
|
|
||||||
integrity sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==
|
|
||||||
|
|
||||||
socket.io-adapter@~2.5.2:
|
socket.io-adapter@~2.5.2:
|
||||||
version "2.5.2"
|
version "2.5.2"
|
||||||
resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-2.5.2.tgz#5de9477c9182fdc171cd8c8364b9a8894ec75d12"
|
resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-2.5.2.tgz#5de9477c9182fdc171cd8c8364b9a8894ec75d12"
|
||||||
@@ -5956,14 +5908,6 @@ socket.io@^4.7.1:
|
|||||||
socket.io-adapter "~2.5.2"
|
socket.io-adapter "~2.5.2"
|
||||||
socket.io-parser "~4.2.4"
|
socket.io-parser "~4.2.4"
|
||||||
|
|
||||||
socks@^2.7.1:
|
|
||||||
version "2.7.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/socks/-/socks-2.7.1.tgz#d8e651247178fde79c0663043e07240196857d55"
|
|
||||||
integrity sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ==
|
|
||||||
dependencies:
|
|
||||||
ip "^2.0.0"
|
|
||||||
smart-buffer "^4.2.0"
|
|
||||||
|
|
||||||
sorted-array-functions@^1.3.0:
|
sorted-array-functions@^1.3.0:
|
||||||
version "1.3.0"
|
version "1.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/sorted-array-functions/-/sorted-array-functions-1.3.0.tgz#8605695563294dffb2c9796d602bd8459f7a0dd5"
|
resolved "https://registry.yarnpkg.com/sorted-array-functions/-/sorted-array-functions-1.3.0.tgz#8605695563294dffb2c9796d602bd8459f7a0dd5"
|
||||||
|
|||||||
Reference in new issue
Block a user