mirror of
https://github.com/linux-odyssey/linux-odyssey.git
synced 2026-11-08 11:30:12 +08:00
Chris - convert analytics to TS (#106)
This commit is contained in:
37 files changed
+771
-333
No files matched your search
@@ -12,23 +12,6 @@ jobs:
|
|||||||
uses: ./.github/workflows/ci-checks.yml
|
uses: ./.github/workflows/ci-checks.yml
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|
||||||
code_review:
|
|
||||||
permissions:
|
|
||||||
pull-requests: write
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Code Review GPT
|
|
||||||
uses: mattzcarey/code-review-gpt@v0.1.4-alpha
|
|
||||||
with:
|
|
||||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
|
||||||
MODEL: 'gpt-3.5-turbo'
|
|
||||||
GITHUB_TOKEN: ${{ github.token }}
|
|
||||||
|
|
||||||
build:
|
build:
|
||||||
needs: ci-checks
|
|
||||||
uses: ./.github/workflows/build.yml
|
uses: ./.github/workflows/build.yml
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
ADMIN_USERNAME=alex
|
||||||
|
ADMIN_PASSWORD=Alex1234
|
||||||
@@ -4,8 +4,7 @@ WORKDIR /app
|
|||||||
COPY ./package.json yarn.lock tsconfig.json ./
|
COPY ./package.json yarn.lock tsconfig.json ./
|
||||||
COPY packages/analytics/package.json ./packages/analytics/package.json
|
COPY packages/analytics/package.json ./packages/analytics/package.json
|
||||||
COPY packages/models/package.json ./packages/models/package.json
|
COPY packages/models/package.json ./packages/models/package.json
|
||||||
RUN yarn set version berry
|
RUN yarn install --frozen-lockfile
|
||||||
RUN yarn install
|
|
||||||
ENV HOST=0.0.0.0
|
ENV HOST=0.0.0.0
|
||||||
ENV PORT=3001
|
ENV PORT=3001
|
||||||
EXPOSE 3001
|
EXPOSE 3001
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
{
|
{
|
||||||
"ext": "js,handlebars"
|
"watch": ["src", "views"],
|
||||||
|
"ext": "ts,handlebars,js",
|
||||||
|
"ignore": ["src/**/*.spec.ts"],
|
||||||
|
"exec": "node --loader ts-node/esm src/index.ts"
|
||||||
}
|
}
|
||||||
@@ -6,19 +6,27 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "GPL",
|
"license": "GPL",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node src/index.js",
|
"start": "node dist/index.js",
|
||||||
"build": "tsc",
|
"build": "tsc",
|
||||||
"dev": "nodemon src/index.js"
|
"watch": "tsc -w",
|
||||||
|
"dev": "nodemon"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@linux-odyssey/models": "*",
|
"@linux-odyssey/models": "*",
|
||||||
|
"dotenv": "^16.4.5",
|
||||||
"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"
|
"mongoose": "^8.0.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"nodemon": "^3.0.1",
|
"@swc/core": "^1.4.2",
|
||||||
|
"@swc/helpers": "^0.5.6",
|
||||||
|
"@types/express": "^4.17.21",
|
||||||
|
"@types/node": "^20.11.20",
|
||||||
|
"nodemon": "^3.1.0",
|
||||||
|
"regenerator-runtime": "^0.14.1",
|
||||||
|
"ts-node": "^10.9.2",
|
||||||
"typescript": "^5.3.3"
|
"typescript": "^5.3.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -9,7 +9,7 @@ th {
|
|||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
border-spacing: 0;
|
border-spacing: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border: 2px solid black;
|
border: 2px solid #dcdcdd;
|
||||||
}
|
}
|
||||||
|
|
||||||
th,
|
th,
|
||||||
@@ -17,3 +17,117 @@ td {
|
|||||||
width: 1px;
|
width: 1px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
background-color: #000000;
|
||||||
|
color: #333;
|
||||||
|
margin: 0;
|
||||||
|
padding: 20px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title h1 {
|
||||||
|
color: #00ff00;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 50px;
|
||||||
|
margin-top: 50px;
|
||||||
|
font-family:
|
||||||
|
ui-monospace,
|
||||||
|
SFMono-Regular,
|
||||||
|
Menlo,
|
||||||
|
Monaco,
|
||||||
|
Consolas,
|
||||||
|
Liberation Mono,
|
||||||
|
Courier New,
|
||||||
|
monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title img {
|
||||||
|
width: 135px;
|
||||||
|
}
|
||||||
|
.title {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 30px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
.heading {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav a {
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin-top: 20px;
|
||||||
|
color: #dcdcdd;
|
||||||
|
|
||||||
|
background-color: #24262e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-table td {
|
||||||
|
padding: 10px;
|
||||||
|
text-align: left;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-table a {
|
||||||
|
color: #00dd00;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-table th {
|
||||||
|
color: #dcdcdd;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid #dcdcdd;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
background-color: #24262e;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sort {
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: 25px;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sort a {
|
||||||
|
background-color: #24262e;
|
||||||
|
height: 50px;
|
||||||
|
padding: 0 5 0 5;
|
||||||
|
border-radius: 10px;
|
||||||
|
text-decoration: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sort .btn h3 {
|
||||||
|
margin: 15 3 0 3;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #dcdcdd;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<svg width="638" height="732" viewBox="0 0 638 732" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
|
<path d="M0 0H638V732H0V0Z" fill="url(#pattern0)"/>
|
||||||
|
<defs>
|
||||||
|
<pattern id="pattern0" patternContentUnits="objectBoundingBox" width="1" height="1">
|
||||||
|
<use xlink:href="#image0_379_14" transform="matrix(0.00126582 0 0 0.00110327 0 -0.000333748)"/>
|
||||||
|
</pattern>
|
||||||
|
<image id="image0_379_14" width="790" height="907" xlink:href="data:image/png;base64,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 truncated
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 630 KiB |
@@ -1,18 +0,0 @@
|
|||||||
function getOrFailed(key) {
|
|
||||||
if (!process.env[key]) {
|
|
||||||
throw new Error(`Missing required environment variable: ${key}`)
|
|
||||||
}
|
|
||||||
return process.env[key]
|
|
||||||
}
|
|
||||||
|
|
||||||
const config = {
|
|
||||||
host: process.env.HOST || 'localhost',
|
|
||||||
port: process.env.PORT || 3001,
|
|
||||||
db: process.env.MONGO_URL || 'mongodb://localhost:27017/odyssey-test',
|
|
||||||
username: getOrFailed('ADMIN_USERNAME'),
|
|
||||||
password: getOrFailed('ADMIN_PASSWORD'),
|
|
||||||
}
|
|
||||||
|
|
||||||
config.baseUrl = process.env.BASE_URL || `http://${config.host}:${config.port}`
|
|
||||||
|
|
||||||
export default config
|
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import dotenv from 'dotenv'
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV !== 'production') {
|
||||||
|
dotenv.config({ path: '.env.dev' })
|
||||||
|
}
|
||||||
|
|
||||||
|
function getOrFailed(key: string): string {
|
||||||
|
if (!process.env[key]) {
|
||||||
|
throw new Error(`Missing required environment variable: ${key}`)
|
||||||
|
}
|
||||||
|
return process.env[key] as string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Config {
|
||||||
|
host: string
|
||||||
|
port: number
|
||||||
|
db: string
|
||||||
|
username: string
|
||||||
|
password: string
|
||||||
|
baseUrl: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const config: Config = {
|
||||||
|
host: process.env.HOST || 'localhost',
|
||||||
|
port: process.env.PORT ? parseInt(process.env.PORT, 10) : 3001,
|
||||||
|
db: process.env.MONGO_URL || 'mongodb://localhost:27017/odyssey-test',
|
||||||
|
username: getOrFailed('ADMIN_USERNAME'),
|
||||||
|
password: getOrFailed('ADMIN_PASSWORD'),
|
||||||
|
baseUrl:
|
||||||
|
process.env.BASE_URL ||
|
||||||
|
`http://${process.env.HOST || 'localhost'}:${process.env.PORT ? parseInt(process.env.PORT, 10) : 3001}`,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default config
|
||||||
File renamed without changes.
+6
-4
@@ -1,10 +1,12 @@
|
|||||||
/* eslint-disable object-shorthand */
|
import type { Request, Response } from 'express'
|
||||||
import Pagination from '../models/pagination.js'
|
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
|
export async function commandListController(
|
||||||
export async function commandListController(req, res) {
|
req: Request,
|
||||||
const { nextKey, order } = req.query
|
res: Response
|
||||||
|
): Promise<void> {
|
||||||
|
const { nextKey, order } = req.query as { nextKey?: string; order: string }
|
||||||
const itemsPerPage = 100
|
const itemsPerPage = 100
|
||||||
|
|
||||||
try {
|
try {
|
||||||
+3
-4
@@ -1,8 +1,8 @@
|
|||||||
|
import { Request, Response } from 'express'
|
||||||
import { userCount } from '../models/users.js'
|
import { userCount } from '../models/users.js'
|
||||||
import { questList } from '../models/quests.js'
|
import { questList } from '../models/quests.js'
|
||||||
|
|
||||||
// eslint-disable-next-line import/prefer-default-export
|
export async function homeController(req: Request, res: Response) {
|
||||||
export async function homeController(req, res) {
|
|
||||||
const quests = await questList()
|
const quests = await questList()
|
||||||
const [totalQuests, totalCompleted] = quests.reduce(
|
const [totalQuests, totalCompleted] = quests.reduce(
|
||||||
([total, completed], quest) => [
|
([total, completed], quest) => [
|
||||||
@@ -17,8 +17,7 @@ export async function homeController(req, res) {
|
|||||||
userCount: await userCount(),
|
userCount: await userCount(),
|
||||||
totalQuests,
|
totalQuests,
|
||||||
totalCompleted,
|
totalCompleted,
|
||||||
|
completedRate: Math.round((totalCompleted / totalQuests) * 10000) / 100,
|
||||||
}
|
}
|
||||||
payload.completedRate =
|
|
||||||
Math.round((payload.totalCompleted / payload.totalQuests) * 10000) / 100
|
|
||||||
res.render('home', payload)
|
res.render('home', payload)
|
||||||
}
|
}
|
||||||
+2
-2
@@ -1,7 +1,7 @@
|
|||||||
|
import { Request, Response } from 'express'
|
||||||
import { questList } from '../models/quests.js'
|
import { questList } from '../models/quests.js'
|
||||||
|
|
||||||
// eslint-disable-next-line import/prefer-default-export
|
export async function questListController(req: Request, res: Response) {
|
||||||
export async function questListController(req, res) {
|
|
||||||
const quests = await questList()
|
const quests = await questList()
|
||||||
quests.forEach((quest) => {
|
quests.forEach((quest) => {
|
||||||
quest.failed = quest.started - quest.completed
|
quest.failed = quest.started - quest.completed
|
||||||
+4
-3
@@ -1,8 +1,9 @@
|
|||||||
|
import { Request, Response } from 'express'
|
||||||
import Pagination from '../models/pagination.js'
|
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: Request, res: Response) {
|
||||||
const { nextKey, order } = req.query
|
const { nextKey, order } = req.query as { nextKey: string; order: string }
|
||||||
const itemsPerPage = 50
|
const itemsPerPage = 50
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -21,7 +22,7 @@ export async function sessionListController(req, res) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function sessionDetailController(req, res) {
|
export async function sessionDetailController(req: Request, res: Response) {
|
||||||
const { id } = req.params
|
const { id } = req.params
|
||||||
const session = await sessionDetail(id)
|
const session = await sessionDetail(id)
|
||||||
res.render('sessionDetail', { session })
|
res.render('sessionDetail', { session })
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import Pagination from '../models/pagination.js'
|
|
||||||
import { userList } from '../models/users.js'
|
|
||||||
|
|
||||||
export async function userListController(req, res) {
|
|
||||||
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')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { Request, Response } from 'express'
|
||||||
|
import mongoose from 'mongoose'
|
||||||
|
import Pagination from '../models/pagination.js'
|
||||||
|
import { userList, userDetail, idToUser } from '../models/users.js'
|
||||||
|
|
||||||
|
export async function userListController(req: Request, res: Response) {
|
||||||
|
const { nextKey, order } = req.query as { nextKey: string; order: string }
|
||||||
|
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')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function userDetailController(req: Request, res: Response) {
|
||||||
|
const id = new mongoose.Types.ObjectId(req.params.id)
|
||||||
|
const sessions = await userDetail(id)
|
||||||
|
try {
|
||||||
|
const user = await idToUser(id)
|
||||||
|
res.render('usersDetail', { sessions, user })
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
res.status(500).send('Error fetching user data')
|
||||||
|
}
|
||||||
|
}
|
||||||
File renamed without changes.
@@ -0,0 +1,61 @@
|
|||||||
|
import { Types } from 'mongoose'
|
||||||
|
|
||||||
|
export interface User {
|
||||||
|
username: string
|
||||||
|
}
|
||||||
|
export interface SessionDocument {
|
||||||
|
_id: Types.ObjectId
|
||||||
|
user: User
|
||||||
|
quest: string
|
||||||
|
status: string
|
||||||
|
createdAt: Date
|
||||||
|
finishedAt: Date
|
||||||
|
}
|
||||||
|
export interface CommandObject {
|
||||||
|
command: string
|
||||||
|
pwd: string
|
||||||
|
stage?: string | null
|
||||||
|
output: string | undefined
|
||||||
|
error: string | undefined
|
||||||
|
createdAt: string
|
||||||
|
}
|
||||||
|
export interface SessionObject {
|
||||||
|
_id: Types.ObjectId
|
||||||
|
user: string
|
||||||
|
quest: string
|
||||||
|
status: string
|
||||||
|
createdAt: string
|
||||||
|
finishedAt: string
|
||||||
|
lastActivityAt: string
|
||||||
|
usedTime: string
|
||||||
|
commandCount: number
|
||||||
|
commands: CommandObject[]
|
||||||
|
}
|
||||||
|
export interface SessionDetail {
|
||||||
|
_id: Types.ObjectId
|
||||||
|
user: string
|
||||||
|
quest: string
|
||||||
|
status: string
|
||||||
|
createdAt: string
|
||||||
|
finishedAt: string
|
||||||
|
usedTime: string
|
||||||
|
commands: CommandObject[]
|
||||||
|
}
|
||||||
|
export interface IUser extends Document {
|
||||||
|
hashedPassword?: string
|
||||||
|
socialLogins?: Record<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
id: string
|
||||||
|
provider: string
|
||||||
|
displayName?: string | null | undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
|
}
|
||||||
|
export interface UserSessionDetail {
|
||||||
|
_id: Types.ObjectId
|
||||||
|
quest: string
|
||||||
|
status: string
|
||||||
|
createdAt: string
|
||||||
|
usedTime: string
|
||||||
|
}
|
||||||
+2
-1
@@ -1,7 +1,8 @@
|
|||||||
import { Command } from '@linux-odyssey/models'
|
import { Command } from '@linux-odyssey/models'
|
||||||
|
import Pagination from './pagination.js'
|
||||||
|
|
||||||
// eslint-disable-next-line import/prefer-default-export
|
// eslint-disable-next-line import/prefer-default-export
|
||||||
export async function errorCommands(pagination) {
|
export async function errorCommands(pagination: Pagination) {
|
||||||
const commands = await Command.aggregate([
|
const commands = await Command.aggregate([
|
||||||
pagination.match('_id'),
|
pagination.match('_id'),
|
||||||
{
|
{
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
/* 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,89 @@
|
|||||||
|
/* eslint-disable lines-between-class-members */
|
||||||
|
/* eslint-disable class-methods-use-this */
|
||||||
|
// eslint-disable-next-line max-classes-per-file
|
||||||
|
import mongoose, { Types } from 'mongoose'
|
||||||
|
|
||||||
|
export const DESC: string = 'desc'
|
||||||
|
export const ASC: string = 'asc'
|
||||||
|
|
||||||
|
interface Order {
|
||||||
|
getOperator(): string
|
||||||
|
getOrder(): number
|
||||||
|
getName(): string
|
||||||
|
}
|
||||||
|
|
||||||
|
class Asc implements Order {
|
||||||
|
getOperator(): string {
|
||||||
|
return '$gt'
|
||||||
|
}
|
||||||
|
|
||||||
|
getOrder(): number {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
getName(): string {
|
||||||
|
return ASC
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Desc implements Order {
|
||||||
|
getOperator(): string {
|
||||||
|
return '$lt'
|
||||||
|
}
|
||||||
|
|
||||||
|
getOrder(): 1 | -1 {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
getName(): string {
|
||||||
|
return DESC
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class Pagination {
|
||||||
|
nextKey: Types.ObjectId | null = null
|
||||||
|
itemsPerPage: number = 50
|
||||||
|
order: Order
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
order: string,
|
||||||
|
itemsPerPage: number = 50,
|
||||||
|
nextKey: string | null = 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: string) {
|
||||||
|
const stage: { [key: string]: { [key: string]: Types.ObjectId } } = this
|
||||||
|
.nextKey
|
||||||
|
? {
|
||||||
|
[field]: {
|
||||||
|
[this.order ? this.order.getOperator() : '$gt']: this.nextKey,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {}
|
||||||
|
return { $match: stage }
|
||||||
|
}
|
||||||
|
|
||||||
|
sort(field: string): { $sort: Record<string, 1 | -1> } {
|
||||||
|
return { $sort: { [field]: this.order.getOrder() as 1 | -1 } }
|
||||||
|
}
|
||||||
|
|
||||||
|
limit() {
|
||||||
|
return { $limit: this.itemsPerPage }
|
||||||
|
}
|
||||||
|
|
||||||
|
getOrder() {
|
||||||
|
return this.order.getName()
|
||||||
|
}
|
||||||
|
}
|
||||||
File renamed without changes.
@@ -1,83 +0,0 @@
|
|||||||
import { Command, Session } from '@linux-odyssey/models'
|
|
||||||
|
|
||||||
function formatTime(time) {
|
|
||||||
const seconds = Math.floor(time / 1000)
|
|
||||||
const minutes = Math.floor(seconds / 60)
|
|
||||||
const hours = Math.floor(minutes / 60)
|
|
||||||
return `${hours}h ${minutes % 60}m ${seconds % 60}s`
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function sessionList(pagination) {
|
|
||||||
const sessions = await Session.aggregate([
|
|
||||||
pagination.match('_id'),
|
|
||||||
{
|
|
||||||
$lookup: {
|
|
||||||
from: 'users',
|
|
||||||
localField: 'user',
|
|
||||||
foreignField: '_id',
|
|
||||||
as: 'user',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ $unwind: '$user' },
|
|
||||||
{
|
|
||||||
$lookup: {
|
|
||||||
from: 'commands',
|
|
||||||
localField: '_id',
|
|
||||||
foreignField: 'session',
|
|
||||||
as: 'commands',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
pagination.sort('_id'),
|
|
||||||
pagination.limit(),
|
|
||||||
])
|
|
||||||
|
|
||||||
return sessions.map(
|
|
||||||
({
|
|
||||||
_id,
|
|
||||||
user,
|
|
||||||
quest,
|
|
||||||
status,
|
|
||||||
createdAt,
|
|
||||||
finishedAt,
|
|
||||||
commands,
|
|
||||||
lastActivityAt,
|
|
||||||
}) => {
|
|
||||||
return {
|
|
||||||
_id,
|
|
||||||
user: user.username,
|
|
||||||
quest,
|
|
||||||
status,
|
|
||||||
createdAt: createdAt?.toLocaleString(),
|
|
||||||
finishedAt: finishedAt?.toLocaleString(),
|
|
||||||
lastActivityAt: lastActivityAt?.toLocaleString(),
|
|
||||||
usedTime: formatTime(lastActivityAt - createdAt),
|
|
||||||
commandCount: commands?.length,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function sessionDetail(id) {
|
|
||||||
const session = await Session.findById(id).populate('user').exec()
|
|
||||||
const commands = (await Command.find({ session: id })).map(
|
|
||||||
({ command, pwd, output, error, createdAt, stage }) => ({
|
|
||||||
command: command?.slice(0, 20),
|
|
||||||
pwd,
|
|
||||||
stage,
|
|
||||||
output: output?.slice(0, 50),
|
|
||||||
error: error?.slice(0, 50),
|
|
||||||
createdAt: createdAt?.toLocaleString(),
|
|
||||||
})
|
|
||||||
)
|
|
||||||
const { _id, user, quest, status, createdAt, finishedAt } = session
|
|
||||||
return {
|
|
||||||
_id,
|
|
||||||
user: user.username,
|
|
||||||
quest,
|
|
||||||
status,
|
|
||||||
createdAt: createdAt?.toLocaleString(),
|
|
||||||
finishedAt: finishedAt?.toLocaleString(),
|
|
||||||
usedTime: finishedAt ? formatTime(finishedAt - createdAt) : '',
|
|
||||||
commands,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
import { Command, Session } from '@linux-odyssey/models'
|
||||||
|
import Pagination from './pagination.js'
|
||||||
|
import {
|
||||||
|
CommandObject,
|
||||||
|
SessionDetail,
|
||||||
|
SessionDocument,
|
||||||
|
SessionObject,
|
||||||
|
} from '../interface.js'
|
||||||
|
|
||||||
|
function formatTime(time: number) {
|
||||||
|
const seconds = Math.floor(time / 1000)
|
||||||
|
const minutes = Math.floor(seconds / 60)
|
||||||
|
const hours = Math.floor(minutes / 60)
|
||||||
|
return `${hours}h ${minutes % 60}m ${seconds % 60}s`
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function sessionList(
|
||||||
|
pagination: Pagination
|
||||||
|
): Promise<SessionObject[]> {
|
||||||
|
const sessions = await Session.aggregate([
|
||||||
|
pagination.match('_id'),
|
||||||
|
{
|
||||||
|
$lookup: {
|
||||||
|
from: 'users',
|
||||||
|
localField: 'user',
|
||||||
|
foreignField: '_id',
|
||||||
|
as: 'user',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ $unwind: '$user' },
|
||||||
|
{
|
||||||
|
$lookup: {
|
||||||
|
from: 'commands',
|
||||||
|
localField: '_id',
|
||||||
|
foreignField: 'session',
|
||||||
|
as: 'commands',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
pagination.sort('_id'),
|
||||||
|
pagination.limit(),
|
||||||
|
])
|
||||||
|
|
||||||
|
return sessions.map((session) => {
|
||||||
|
const {
|
||||||
|
_id,
|
||||||
|
user,
|
||||||
|
quest,
|
||||||
|
status,
|
||||||
|
createdAt,
|
||||||
|
finishedAt,
|
||||||
|
commands,
|
||||||
|
lastActivityAt,
|
||||||
|
} = session
|
||||||
|
return {
|
||||||
|
_id,
|
||||||
|
user: user.username, // Assuming `user` is always populated.
|
||||||
|
quest,
|
||||||
|
status,
|
||||||
|
createdAt: createdAt?.toLocaleString(),
|
||||||
|
finishedAt: finishedAt?.toLocaleString(),
|
||||||
|
lastActivityAt: lastActivityAt?.toLocaleString(),
|
||||||
|
usedTime:
|
||||||
|
lastActivityAt && createdAt
|
||||||
|
? formatTime(lastActivityAt.getTime() - createdAt.getTime())
|
||||||
|
: 'N/A', // Ensure both dates are present
|
||||||
|
commandCount: commands?.length || 0,
|
||||||
|
commands,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function sessionDetail(id: string): Promise<SessionDetail> {
|
||||||
|
const session = (await Session.findById(id)
|
||||||
|
.populate('user')
|
||||||
|
.exec()) as unknown as SessionDocument
|
||||||
|
const commands: CommandObject[] = (await Command.find({ session: id })).map(
|
||||||
|
({ command, pwd, output, error, createdAt, stage }): CommandObject => ({
|
||||||
|
command: command?.slice(0, 20),
|
||||||
|
pwd,
|
||||||
|
stage,
|
||||||
|
output: output?.slice(0, 50),
|
||||||
|
error: error?.slice(0, 50),
|
||||||
|
createdAt: createdAt?.toLocaleString(),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
const { _id, user, quest, status, createdAt, finishedAt } = session
|
||||||
|
return {
|
||||||
|
_id,
|
||||||
|
user: user.username,
|
||||||
|
quest,
|
||||||
|
status,
|
||||||
|
createdAt: createdAt?.toLocaleString(),
|
||||||
|
finishedAt: finishedAt?.toLocaleString(),
|
||||||
|
usedTime: finishedAt
|
||||||
|
? formatTime(finishedAt.getTime() - createdAt.getTime())
|
||||||
|
: '',
|
||||||
|
commands,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
import { Session, User } from '@linux-odyssey/models'
|
|
||||||
|
|
||||||
function loginMethods(user) {
|
|
||||||
const methods = []
|
|
||||||
if (user.hashedPassword) methods.push('password')
|
|
||||||
if (user.socialLogins) methods.push(...Object.keys(user.socialLogins))
|
|
||||||
return methods
|
|
||||||
}
|
|
||||||
|
|
||||||
export function userCount() {
|
|
||||||
return User.find().count()
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function userList(pagination) {
|
|
||||||
const users = await Session.aggregate([
|
|
||||||
pagination.match('user'),
|
|
||||||
{
|
|
||||||
$group: {
|
|
||||||
_id: '$user',
|
|
||||||
count: { $sum: 1 },
|
|
||||||
lastActivityAt: { $max: '$lastActivityAt' },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
$lookup: {
|
|
||||||
from: 'users',
|
|
||||||
localField: '_id',
|
|
||||||
foreignField: '_id',
|
|
||||||
as: 'userData',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ $unwind: '$userData' },
|
|
||||||
pagination.sort('userData._id'),
|
|
||||||
pagination.limit(),
|
|
||||||
])
|
|
||||||
|
|
||||||
return users.map(({ userData, count, lastActivityAt }) => {
|
|
||||||
const { _id, username, email, createdAt } = userData
|
|
||||||
return {
|
|
||||||
_id,
|
|
||||||
username,
|
|
||||||
email,
|
|
||||||
createdAt: createdAt?.toLocaleString(),
|
|
||||||
method: loginMethods(userData),
|
|
||||||
sessionCount: count,
|
|
||||||
lastActivityAt: lastActivityAt?.toLocaleString(),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
import { Session, User } from '@linux-odyssey/models'
|
||||||
|
import { Types } from 'mongoose'
|
||||||
|
import Pagination from './pagination.js'
|
||||||
|
import { IUser, UserSessionDetail } from '../interface.js'
|
||||||
|
|
||||||
|
function loginMethods(user: IUser): string[] {
|
||||||
|
const methods: string[] = []
|
||||||
|
if (user.hashedPassword) methods.push('password')
|
||||||
|
if (user.socialLogins && typeof user.socialLogins === 'object') {
|
||||||
|
methods.push(...Object.keys(user.socialLogins))
|
||||||
|
}
|
||||||
|
return methods
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTime(time: number) {
|
||||||
|
const seconds = Math.floor(time / 1000)
|
||||||
|
const minutes = Math.floor(seconds / 60)
|
||||||
|
const hours = Math.floor(minutes / 60)
|
||||||
|
return `${hours}h ${minutes % 60}m ${seconds % 60}s`
|
||||||
|
}
|
||||||
|
export function userCount() {
|
||||||
|
return User.countDocuments({})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function userList(pagination: Pagination) {
|
||||||
|
const users = await Session.aggregate([
|
||||||
|
pagination.match('user'),
|
||||||
|
{
|
||||||
|
$group: {
|
||||||
|
_id: '$user',
|
||||||
|
count: { $sum: 1 },
|
||||||
|
lastActivityAt: { $max: '$lastActivityAt' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$lookup: {
|
||||||
|
from: 'users',
|
||||||
|
localField: '_id',
|
||||||
|
foreignField: '_id',
|
||||||
|
as: 'userData',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ $unwind: '$userData' },
|
||||||
|
pagination.sort('userData._id'),
|
||||||
|
pagination.limit(),
|
||||||
|
])
|
||||||
|
|
||||||
|
return users.map(({ userData, count, lastActivityAt }) => {
|
||||||
|
const { _id, username, email, createdAt } = userData
|
||||||
|
return {
|
||||||
|
_id,
|
||||||
|
username,
|
||||||
|
email,
|
||||||
|
createdAt: createdAt?.toLocaleString(),
|
||||||
|
method: loginMethods(userData),
|
||||||
|
sessionCount: count,
|
||||||
|
lastActivityAt: lastActivityAt?.toLocaleString(),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function userDetail(
|
||||||
|
id: Types.ObjectId
|
||||||
|
): Promise<UserSessionDetail[]> {
|
||||||
|
const sessions = await Session.find({
|
||||||
|
user: id,
|
||||||
|
})
|
||||||
|
return sessions.map((session): UserSessionDetail => {
|
||||||
|
const { _id, quest, status, createdAt, lastActivityAt } = session
|
||||||
|
return {
|
||||||
|
_id,
|
||||||
|
quest,
|
||||||
|
status,
|
||||||
|
createdAt: createdAt?.toLocaleString(),
|
||||||
|
usedTime:
|
||||||
|
lastActivityAt && createdAt
|
||||||
|
? formatTime(lastActivityAt.getTime() - createdAt.getTime())
|
||||||
|
: 'N/A', // Ensure both dates are present
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function idToUser(id: Types.ObjectId): Promise<String> {
|
||||||
|
const user = await User.findOne({
|
||||||
|
_id: id,
|
||||||
|
})
|
||||||
|
return user?.username || 'User not found'
|
||||||
|
}
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
import { Router } from 'express'
|
import { Router } from 'express'
|
||||||
|
|
||||||
import { homeController } from './controllers/index.js'
|
import { homeController } from './controllers/index.js'
|
||||||
import { userListController } from './controllers/users.js'
|
import {
|
||||||
|
userListController,
|
||||||
|
userDetailController,
|
||||||
|
} from './controllers/users.js'
|
||||||
import { questListController } from './controllers/quests.js'
|
import { questListController } from './controllers/quests.js'
|
||||||
import { commandListController } from './controllers/commands.js'
|
import { commandListController } from './controllers/commands.js'
|
||||||
import {
|
import {
|
||||||
@@ -13,6 +16,7 @@ const router = Router()
|
|||||||
|
|
||||||
router.get('/', homeController)
|
router.get('/', homeController)
|
||||||
router.get('/users', userListController)
|
router.get('/users', userListController)
|
||||||
|
router.get('/users/:id', userDetailController)
|
||||||
router.get('/quests', questListController)
|
router.get('/quests', questListController)
|
||||||
router.get('/commands', commandListController)
|
router.get('/commands', commandListController)
|
||||||
router.get('/sessions', sessionListController)
|
router.get('/sessions', sessionListController)
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
{
|
{
|
||||||
"extends": "../../tsconfig.json",
|
"extends": "../../tsconfig.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"allowJs": true,
|
|
||||||
"rootDir": "src",
|
"rootDir": "src",
|
||||||
"outDir": "./dist"
|
"outDir": "./dist"
|
||||||
},
|
},
|
||||||
"include": ["src"],
|
"include": ["src"],
|
||||||
"exclude": ["node_modules", "dist"]
|
"ts-node": {
|
||||||
|
"swc": true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,11 +1,15 @@
|
|||||||
<h1>Commands</h1>
|
<h1 class='heading'>Commands</h1>
|
||||||
<div>
|
<div>
|
||||||
<h2>Error commands</h2>
|
<h2 class='heading'>Error commands</h2>
|
||||||
<div class='pagination'>
|
<div class='sort'>
|
||||||
<a href='/commands?order=asc'>Ascending</a>
|
<a href='/commands?order=asc'><div class='btn'>
|
||||||
<a href='/commands?order=desc'>Descending</a>
|
<h3>Ascending</h3>
|
||||||
|
</div></a>
|
||||||
|
<a href='/commands?order=desc'><div class='btn'>
|
||||||
|
<h3>Descending</h3>
|
||||||
|
</div></a>
|
||||||
</div>
|
</div>
|
||||||
<table>
|
<table class='data-table'>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Command</th>
|
<th>Command</th>
|
||||||
<th>User</th>
|
<th>User</th>
|
||||||
@@ -25,7 +29,9 @@
|
|||||||
</tr>
|
</tr>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
</table>
|
</table>
|
||||||
<div class='pagination'>
|
<div class='pagination sort'>
|
||||||
<a href='/commands?nextKey={{nextKey}}&order={{order}}'>Next</a>
|
<a href='/commands?nextKey={{nextKey}}&order={{order}}'><div class='btn'>
|
||||||
|
<h3>Next</h3>
|
||||||
|
</div></a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1,7 +1,11 @@
|
|||||||
|
<body>
|
||||||
|
<div class='title'>
|
||||||
|
<img src='icon_totem.svg' class='styled-image' />
|
||||||
<h1>Welcome to {{title}}</h1>
|
<h1>Welcome to {{title}}</h1>
|
||||||
<div>
|
</div>
|
||||||
<h2>Overview</h2>
|
<div class='container'>
|
||||||
<table>
|
<h2 class='heading'>Overview</h2>
|
||||||
|
<table class='data-table'>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Registered Users</th>
|
<th>Registered Users</th>
|
||||||
<td>{{userCount}}</td>
|
<td>{{userCount}}</td>
|
||||||
@@ -20,3 +24,4 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
</body>
|
||||||
@@ -4,14 +4,12 @@
|
|||||||
<link rel='stylesheet' href='/css/style.css' />
|
<link rel='stylesheet' href='/css/style.css' />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<nav>
|
<nav class='nav'>
|
||||||
<ul>
|
<div><a href='/'>Home</a></div>
|
||||||
<li><a href='/'>Home</a></li>
|
<div><a href='/users'>Users</a></div>
|
||||||
<li><a href='/users'>Users</a></li>
|
<div><a href='/quests'>Quests</a></div>
|
||||||
<li><a href='/quests'>Quests</a></li>
|
<div><a href='/commands'>Commands</a></div>
|
||||||
<li><a href='/commands'>Commands</a></li>
|
<div><a href='/sessions'>Sessions</a></div>
|
||||||
<li><a href='/sessions'>Sessions</a></li>
|
|
||||||
</ul>
|
|
||||||
</nav>
|
</nav>
|
||||||
{{{body}}}
|
{{{body}}}
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<h1>Quests</h1>
|
<h1 class='heading'>Quests</h1>
|
||||||
<div>
|
<div>
|
||||||
<table>
|
<table class='data-table'>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Id</th>
|
<th>Id</th>
|
||||||
<th>Started</th>
|
<th>Started</th>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<h1>{{title}}</h1>
|
{{#with session}}<h1 class='heading'>Session {{_id}}</h1>
|
||||||
<div>
|
<div>
|
||||||
{{#with session}}
|
|
||||||
<table>
|
<table class='data-table'>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Quest</th>
|
<th>Quest</th>
|
||||||
<td>{{quest}}</td>
|
<td>{{quest}}</td>
|
||||||
@@ -32,9 +32,9 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<h2>Commands</h2>
|
<h2 class='heading'>Commands</h2>
|
||||||
|
|
||||||
<table>
|
<table class='data-table'>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Stage</th>
|
<th>Stage</th>
|
||||||
<th>PWD</th>
|
<th>PWD</th>
|
||||||
@@ -54,5 +54,5 @@
|
|||||||
</tr>
|
</tr>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
</table>
|
</table>
|
||||||
{{/with}}
|
|
||||||
</div>
|
</div>{{/with}}
|
||||||
@@ -1,10 +1,14 @@
|
|||||||
<h1>Sessions</h1>
|
<h1 class='heading'>Sessions</h1>
|
||||||
<div class='pagination'>
|
<div class='sort'>
|
||||||
<a href='/sessions?order=asc'>Ascending</a>
|
<a href='/sessions?order=asc'><div class='btn'>
|
||||||
<a href='/sessions?order=desc'>Descending</a>
|
<h3>Ascending</h3>
|
||||||
|
</div></a>
|
||||||
|
<a href='/sessions?order=desc'><div class='btn'>
|
||||||
|
<h3>Descending</h3>
|
||||||
|
</div></a>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<table>
|
<table class='data-table'>
|
||||||
<tr>
|
<tr>
|
||||||
<th>User</th>
|
<th>User</th>
|
||||||
<th>Quest</th>
|
<th>Quest</th>
|
||||||
@@ -29,6 +33,8 @@
|
|||||||
{{/each}}
|
{{/each}}
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<div class='pagination'>
|
<div class='pagination sort'>
|
||||||
<a href='/sessions?nextKey={{nextKey}}&order={{order}}'>Next</a>
|
<a href='/sessions?nextKey={{nextKey}}&order={{order}}'><div class='btn'>
|
||||||
|
<h3>Next</h3>
|
||||||
|
</div></a>
|
||||||
</div>
|
</div>
|
||||||
@@ -1,10 +1,14 @@
|
|||||||
<h1>Users List</h1>
|
<h1 class='heading'>Users List</h1>
|
||||||
<div class='pagination'>
|
<div class='sort'>
|
||||||
<a href='/users?order=asc'>Ascending</a>
|
<a href='/users?order=asc'><div class='btn'>
|
||||||
<a href='/users?order=desc'>Descending</a>
|
<h3>Ascending</h3>
|
||||||
|
</div></a>
|
||||||
|
<a href='/users?order=desc'><div class='btn'>
|
||||||
|
<h3>Descending</h3>
|
||||||
|
</div></a>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<table>
|
<table class='data-table'>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Username</th>
|
<th>Username</th>
|
||||||
<th>Last Activity</th>
|
<th>Last Activity</th>
|
||||||
@@ -15,7 +19,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
{{#each users}}
|
{{#each users}}
|
||||||
<tr>
|
<tr>
|
||||||
<td href='/users/{{_id}}'>{{username}}</td>
|
<td><a href='/users/{{_id}}'>{{username}}</a></td>
|
||||||
<td>{{lastActivityAt}}</td>
|
<td>{{lastActivityAt}}</td>
|
||||||
<td>{{sessionCount}}</td>
|
<td>{{sessionCount}}</td>
|
||||||
<td>{{email}}</td>
|
<td>{{email}}</td>
|
||||||
@@ -25,6 +29,8 @@
|
|||||||
{{/each}}
|
{{/each}}
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<div class='pagination'>
|
<div class='pagination sort'>
|
||||||
<a href='/users?nextKey={{nextKey}}&order={{order}}'>Next</a>
|
<a href='/users?nextKey={{nextKey}}&order={{order}}'><div class='btn'>
|
||||||
|
<h3>Next</h3>
|
||||||
|
</div></a>
|
||||||
</div>
|
</div>
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<h1 class='heading'>{{user}}'s Sessions History</h1>
|
||||||
|
<div>
|
||||||
|
<table class='data-table'>
|
||||||
|
<tr>
|
||||||
|
<th>Quest</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Created At</th>
|
||||||
|
<th>Used Time</th>
|
||||||
|
<th>Session</th>
|
||||||
|
</tr>
|
||||||
|
{{#each sessions}}
|
||||||
|
<tr>
|
||||||
|
<td>{{quest}}</td>
|
||||||
|
<td>{{status}}</td>
|
||||||
|
<td>{{createdAt}}</td>
|
||||||
|
<td>{{usedTime}}</td>
|
||||||
|
<td><a href='/sessions/{{_id}}'>Detail</a></td>
|
||||||
|
</tr>
|
||||||
|
{{/each}}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
@@ -913,6 +913,92 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz#96116f2a912e0c02817345b3c10751069920d553"
|
resolved "https://registry.yarnpkg.com/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz#96116f2a912e0c02817345b3c10751069920d553"
|
||||||
integrity sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==
|
integrity sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==
|
||||||
|
|
||||||
|
"@swc/core-darwin-arm64@1.4.2":
|
||||||
|
version "1.4.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@swc/core-darwin-arm64/-/core-darwin-arm64-1.4.2.tgz#3b5677c5b9c5a7a91d953b96cd603c94064e2835"
|
||||||
|
integrity sha512-1uSdAn1MRK5C1m/TvLZ2RDvr0zLvochgrZ2xL+lRzugLlCTlSA+Q4TWtrZaOz+vnnFVliCpw7c7qu0JouhgQIw==
|
||||||
|
|
||||||
|
"@swc/core-darwin-x64@1.4.2":
|
||||||
|
version "1.4.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@swc/core-darwin-x64/-/core-darwin-x64-1.4.2.tgz#bbc8bbf420389b12541151255a50f319cc17ef96"
|
||||||
|
integrity sha512-TYD28+dCQKeuxxcy7gLJUCFLqrwDZnHtC2z7cdeGfZpbI2mbfppfTf2wUPzqZk3gEC96zHd4Yr37V3Tvzar+lQ==
|
||||||
|
|
||||||
|
"@swc/core-linux-arm-gnueabihf@1.4.2":
|
||||||
|
version "1.4.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.4.2.tgz#aa9a18f130820717df08c9dd882043fc47e8d35a"
|
||||||
|
integrity sha512-Eyqipf7ZPGj0vplKHo8JUOoU1un2sg5PjJMpEesX0k+6HKE2T8pdyeyXODN0YTFqzndSa/J43EEPXm+rHAsLFQ==
|
||||||
|
|
||||||
|
"@swc/core-linux-arm64-gnu@1.4.2":
|
||||||
|
version "1.4.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.4.2.tgz#5ef1de0ca7cc3a034aa3a1c3c1794b78e6ca207e"
|
||||||
|
integrity sha512-wZn02DH8VYPv3FC0ub4my52Rttsus/rFw+UUfzdb3tHMHXB66LqN+rR0ssIOZrH6K+VLN6qpTw9VizjyoH0BxA==
|
||||||
|
|
||||||
|
"@swc/core-linux-arm64-musl@1.4.2":
|
||||||
|
version "1.4.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.4.2.tgz#5dfd2a8c0483770a307de0ccb6019a082ff0d902"
|
||||||
|
integrity sha512-3G0D5z9hUj9bXNcwmA1eGiFTwe5rWkuL3DsoviTj73TKLpk7u64ND0XjEfO0huVv4vVu9H1jodrKb7nvln/dlw==
|
||||||
|
|
||||||
|
"@swc/core-linux-x64-gnu@1.4.2":
|
||||||
|
version "1.4.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.4.2.tgz#314aa76b7c1208e315e3156ab57b7188fb605bc2"
|
||||||
|
integrity sha512-LFxn9U8cjmYHw3jrdPNqPAkBGglKE3tCZ8rA7hYyp0BFxuo7L2ZcEnPm4RFpmSCCsExFH+LEJWuMGgWERoktvg==
|
||||||
|
|
||||||
|
"@swc/core-linux-x64-musl@1.4.2":
|
||||||
|
version "1.4.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.4.2.tgz#b2b226657f6a8d48f561cb3dbe2d414cfbafe467"
|
||||||
|
integrity sha512-dp0fAmreeVVYTUcb4u9njTPrYzKnbIH0EhH2qvC9GOYNNREUu2GezSIDgonjOXkHiTCvopG4xU7y56XtXj4VrQ==
|
||||||
|
|
||||||
|
"@swc/core-win32-arm64-msvc@1.4.2":
|
||||||
|
version "1.4.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.4.2.tgz#582f79fa328ce0f426ab8313b3d881e7315fab2f"
|
||||||
|
integrity sha512-HlVIiLMQkzthAdqMslQhDkoXJ5+AOLUSTV6fm6shFKZKqc/9cJvr4S8UveNERL9zUficA36yM3bbfo36McwnvQ==
|
||||||
|
|
||||||
|
"@swc/core-win32-ia32-msvc@1.4.2":
|
||||||
|
version "1.4.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.4.2.tgz#15c8289e1c18857f79b9b888100ab1f871bf58f6"
|
||||||
|
integrity sha512-WCF8faPGjCl4oIgugkp+kL9nl3nUATlzKXCEGFowMEmVVCFM0GsqlmGdPp1pjZoWc9tpYanoXQDnp5IvlDSLhA==
|
||||||
|
|
||||||
|
"@swc/core-win32-x64-msvc@1.4.2":
|
||||||
|
version "1.4.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.4.2.tgz#c999ca7b68124d058b40a1431cdd6f56779670d5"
|
||||||
|
integrity sha512-oV71rwiSpA5xre2C5570BhCsg1HF97SNLsZ/12xv7zayGzqr3yvFALFJN8tHKpqUdCB4FGPjoP3JFdV3i+1wUw==
|
||||||
|
|
||||||
|
"@swc/core@^1.4.2":
|
||||||
|
version "1.4.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@swc/core/-/core-1.4.2.tgz#310b0d5e93e47ca72f54150c8f9efcb434c39b17"
|
||||||
|
integrity sha512-vWgY07R/eqj1/a0vsRKLI9o9klGZfpLNOVEnrv4nrccxBgYPjcf22IWwAoaBJ+wpA7Q4fVjCUM8lP0m01dpxcg==
|
||||||
|
dependencies:
|
||||||
|
"@swc/counter" "^0.1.2"
|
||||||
|
"@swc/types" "^0.1.5"
|
||||||
|
optionalDependencies:
|
||||||
|
"@swc/core-darwin-arm64" "1.4.2"
|
||||||
|
"@swc/core-darwin-x64" "1.4.2"
|
||||||
|
"@swc/core-linux-arm-gnueabihf" "1.4.2"
|
||||||
|
"@swc/core-linux-arm64-gnu" "1.4.2"
|
||||||
|
"@swc/core-linux-arm64-musl" "1.4.2"
|
||||||
|
"@swc/core-linux-x64-gnu" "1.4.2"
|
||||||
|
"@swc/core-linux-x64-musl" "1.4.2"
|
||||||
|
"@swc/core-win32-arm64-msvc" "1.4.2"
|
||||||
|
"@swc/core-win32-ia32-msvc" "1.4.2"
|
||||||
|
"@swc/core-win32-x64-msvc" "1.4.2"
|
||||||
|
|
||||||
|
"@swc/counter@^0.1.2":
|
||||||
|
version "0.1.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/@swc/counter/-/counter-0.1.3.tgz#cc7463bd02949611c6329596fccd2b0ec782b0e9"
|
||||||
|
integrity sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==
|
||||||
|
|
||||||
|
"@swc/helpers@^0.5.6":
|
||||||
|
version "0.5.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.5.6.tgz#d16d8566b7aea2bef90d059757e2d77f48224160"
|
||||||
|
integrity sha512-aYX01Ke9hunpoCexYAgQucEpARGQ5w/cqHFrIR+e9gdKb1QWTsVJuTJ2ozQzIAxLyRQe/m+2RqzkyOOGiMKRQA==
|
||||||
|
dependencies:
|
||||||
|
tslib "^2.4.0"
|
||||||
|
|
||||||
|
"@swc/types@^0.1.5":
|
||||||
|
version "0.1.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@swc/types/-/types-0.1.5.tgz#043b731d4f56a79b4897a3de1af35e75d56bc63a"
|
||||||
|
integrity sha512-myfUej5naTBWnqOCc/MdVOLVjXUXtIA+NpDrDBKJtLLg2shUjBu3cZmB/85RyitKc55+lUUyl7oRfLOvkr2hsw==
|
||||||
|
|
||||||
"@testing-library/cypress@^10.0.1":
|
"@testing-library/cypress@^10.0.1":
|
||||||
version "10.0.1"
|
version "10.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/@testing-library/cypress/-/cypress-10.0.1.tgz#15abae0edb83237316ec6d07e152b71a50b38387"
|
resolved "https://registry.yarnpkg.com/@testing-library/cypress/-/cypress-10.0.1.tgz#15abae0edb83237316ec6d07e152b71a50b38387"
|
||||||
@@ -1183,6 +1269,13 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
undici-types "~5.26.4"
|
undici-types "~5.26.4"
|
||||||
|
|
||||||
|
"@types/node@^20.11.20":
|
||||||
|
version "20.11.21"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.21.tgz#ad67e65652f7be15686e2df87a38076a81c5e9c5"
|
||||||
|
integrity sha512-/ySDLGscFPNasfqStUuWWPfL78jompfIoVzLJPVVAHBh6rpG68+pI2Gk+fNLeI8/f1yPYL4s46EleVIc20F1Ow==
|
||||||
|
dependencies:
|
||||||
|
undici-types "~5.26.4"
|
||||||
|
|
||||||
"@types/oauth@*":
|
"@types/oauth@*":
|
||||||
version "0.9.4"
|
version "0.9.4"
|
||||||
resolved "https://registry.yarnpkg.com/@types/oauth/-/oauth-0.9.4.tgz#dcbab5efa2f34f312b915f80685760ccc8111e0a"
|
resolved "https://registry.yarnpkg.com/@types/oauth/-/oauth-0.9.4.tgz#dcbab5efa2f34f312b915f80685760ccc8111e0a"
|
||||||
@@ -2882,7 +2975,7 @@ dompurify@^3.0.6:
|
|||||||
resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.0.9.tgz#b3f362f24b99f53498c75d43ecbd784b0b3ad65e"
|
resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.0.9.tgz#b3f362f24b99f53498c75d43ecbd784b0b3ad65e"
|
||||||
integrity sha512-uyb4NDIvQ3hRn6NiC+SIFaP4mJ/MdXlvtunaqK9Bn6dD3RuB/1S/gasEjDHD8eiaqdSael2vBv+hOs7Y+jhYOQ==
|
integrity sha512-uyb4NDIvQ3hRn6NiC+SIFaP4mJ/MdXlvtunaqK9Bn6dD3RuB/1S/gasEjDHD8eiaqdSael2vBv+hOs7Y+jhYOQ==
|
||||||
|
|
||||||
dotenv@*, dotenv@^16.3.1, dotenv@^16.4.1:
|
dotenv@*, dotenv@^16.3.1, dotenv@^16.4.1, dotenv@^16.4.5:
|
||||||
version "16.4.5"
|
version "16.4.5"
|
||||||
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.5.tgz#cdd3b3b604cb327e286b4762e13502f717cb099f"
|
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.5.tgz#cdd3b3b604cb327e286b4762e13502f717cb099f"
|
||||||
integrity sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==
|
integrity sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==
|
||||||
@@ -5471,6 +5564,22 @@ nodemon@^3.0.1:
|
|||||||
touch "^3.1.0"
|
touch "^3.1.0"
|
||||||
undefsafe "^2.0.5"
|
undefsafe "^2.0.5"
|
||||||
|
|
||||||
|
nodemon@^3.1.0:
|
||||||
|
version "3.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-3.1.0.tgz#ff7394f2450eb6a5e96fe4180acd5176b29799c9"
|
||||||
|
integrity sha512-xqlktYlDMCepBJd43ZQhjWwMw2obW/JRvkrLxq5RCNcuDDX1DbcPT+qT1IlIIdf+DhnWs90JpTMe+Y5KxOchvA==
|
||||||
|
dependencies:
|
||||||
|
chokidar "^3.5.2"
|
||||||
|
debug "^4"
|
||||||
|
ignore-by-default "^1.0.1"
|
||||||
|
minimatch "^3.1.2"
|
||||||
|
pstree.remy "^1.1.8"
|
||||||
|
semver "^7.5.3"
|
||||||
|
simple-update-notifier "^2.0.0"
|
||||||
|
supports-color "^5.5.0"
|
||||||
|
touch "^3.1.0"
|
||||||
|
undefsafe "^2.0.5"
|
||||||
|
|
||||||
nopt@^5.0.0:
|
nopt@^5.0.0:
|
||||||
version "5.0.0"
|
version "5.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/nopt/-/nopt-5.0.0.tgz#530942bb58a512fccafe53fe210f13a25355dc88"
|
resolved "https://registry.yarnpkg.com/nopt/-/nopt-5.0.0.tgz#530942bb58a512fccafe53fe210f13a25355dc88"
|
||||||
@@ -6093,7 +6202,7 @@ readdirp@~3.6.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
picomatch "^2.2.1"
|
picomatch "^2.2.1"
|
||||||
|
|
||||||
regenerator-runtime@^0.14.0:
|
regenerator-runtime@^0.14.0, regenerator-runtime@^0.14.1:
|
||||||
version "0.14.1"
|
version "0.14.1"
|
||||||
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz#356ade10263f685dda125100cd862c1db895327f"
|
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz#356ade10263f685dda125100cd862c1db895327f"
|
||||||
integrity sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==
|
integrity sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==
|
||||||
@@ -6938,7 +7047,7 @@ tslib@2.3.0:
|
|||||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.0.tgz#803b8cdab3e12ba581a4ca41c8839bbb0dacb09e"
|
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.0.tgz#803b8cdab3e12ba581a4ca41c8839bbb0dacb09e"
|
||||||
integrity sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==
|
integrity sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==
|
||||||
|
|
||||||
tslib@^2.1.0, tslib@^2.6.2:
|
tslib@^2.1.0, tslib@^2.4.0, tslib@^2.6.2:
|
||||||
version "2.6.2"
|
version "2.6.2"
|
||||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae"
|
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae"
|
||||||
integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==
|
integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==
|
||||||
|
|||||||
Reference in new issue
Block a user