mirror of
https://github.com/linux-odyssey/linux-odyssey.git
synced 2026-11-06 03:20:13 +08:00
convert analytics to TS
This commit is contained in:
1 parent
fb69cb4e10
commit
923de901c4
24 files changed
+11093
-211
No files matched your search
@@ -1,3 +1,6 @@
|
||||
{
|
||||
"ext": "js,handlebars"
|
||||
"watch": ["src"],
|
||||
"ext": "ts,handlebars,js",
|
||||
"ignore": ["src/**/*.spec.ts"],
|
||||
"exec": "node --loader ts-node/esm src/index.ts"
|
||||
}
|
||||
@@ -6,9 +6,10 @@
|
||||
"type": "module",
|
||||
"license": "GPL",
|
||||
"scripts": {
|
||||
"start": "node src/index.js",
|
||||
"start": "node --loader ts-node/esm src/index.ts",
|
||||
"build": "tsc",
|
||||
"dev": "nodemon src/index.js"
|
||||
"watch": "tsc -w",
|
||||
"dev": "nodemon --loader ts-node/esm src/index.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@linux-odyssey/models": "*",
|
||||
@@ -18,6 +19,9 @@
|
||||
"mongoose": "^8.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^3.0.1"
|
||||
"@types/node": "^20.11.20",
|
||||
"nodemon": "^3.1.0",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.3.3"
|
||||
}
|
||||
}
|
||||
@@ -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,30 @@
|
||||
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) : 3001,
|
||||
db: process.env.MONGO_URL || 'mongodb://localhost:27017/odyssey-test',
|
||||
//username: getOrFailed('ADMIN_USERNAME'),
|
||||
username: 'wancat',
|
||||
//password: getOrFailed('ADMIN_PASSWORD'),
|
||||
password: 'InMatrix',
|
||||
baseUrl:
|
||||
process.env.BASE_URL ||
|
||||
`http://${process.env.HOST || 'localhost'}:${process.env.PORT ? parseInt(process.env.PORT) : 3001}`,
|
||||
}
|
||||
|
||||
export default config
|
||||
File renamed without changes.
+3
-3
@@ -1,10 +1,10 @@
|
||||
/* eslint-disable object-shorthand */
|
||||
import Pagination from '../models/pagination.js'
|
||||
import Pagination from '../models/pagination.ts'
|
||||
import { errorCommands } from '../models/commands.js'
|
||||
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export async function commandListController(req, res) {
|
||||
const { nextKey, order } = req.query
|
||||
export async function commandListController(req: any, res: any): Promise<void> {
|
||||
const { nextKey, order } = req.query as { nextKey?: string; order: string }
|
||||
const itemsPerPage = 100
|
||||
|
||||
try {
|
||||
+3
-4
@@ -1,8 +1,8 @@
|
||||
import { userCount } from '../models/users.js'
|
||||
import { questList } from '../models/quests.js'
|
||||
import { questList } from '../models/quests.ts'
|
||||
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export async function homeController(req, res) {
|
||||
export async function homeController(req: any, res: any) {
|
||||
const quests = await questList()
|
||||
const [totalQuests, totalCompleted] = quests.reduce(
|
||||
([total, completed], quest) => [
|
||||
@@ -17,8 +17,7 @@ export async function homeController(req, res) {
|
||||
userCount: await userCount(),
|
||||
totalQuests,
|
||||
totalCompleted,
|
||||
completedRate: Math.round((totalCompleted / totalQuests) * 10000) / 100,
|
||||
}
|
||||
payload.completedRate =
|
||||
Math.round((payload.totalCompleted / payload.totalQuests) * 10000) / 100
|
||||
res.render('home', payload)
|
||||
}
|
||||
+2
-3
@@ -1,7 +1,6 @@
|
||||
import { questList } from '../models/quests.js'
|
||||
|
||||
import { questList } from '../models/quests.ts'
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export async function questListController(req, res) {
|
||||
export async function questListController(req: any, res: any) {
|
||||
const quests = await questList()
|
||||
quests.forEach((quest) => {
|
||||
quest.failed = quest.started - quest.completed
|
||||
+5
-5
@@ -1,7 +1,7 @@
|
||||
import Pagination from '../models/pagination.js'
|
||||
import { sessionList, sessionDetail } from '../models/sessions.js'
|
||||
|
||||
export async function sessionListController(req, res) {
|
||||
import Pagination from '../models/pagination.ts'
|
||||
import { sessionList, sessionDetail } from '../models/sessions.ts'
|
||||
import { Request, Response } from 'express'
|
||||
export async function sessionListController(req: any, res: any) {
|
||||
const { nextKey, order } = req.query
|
||||
const itemsPerPage = 50
|
||||
|
||||
@@ -21,7 +21,7 @@ export async function sessionListController(req, res) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function sessionDetailController(req, res) {
|
||||
export async function sessionDetailController(req: any, res: any) {
|
||||
const { id } = req.params
|
||||
const session = await sessionDetail(id)
|
||||
res.render('sessionDetail', { session })
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
import Pagination from '../models/pagination.js'
|
||||
import Pagination from '../models/pagination.ts'
|
||||
import { userList } from '../models/users.js'
|
||||
|
||||
export async function userListController(req, res) {
|
||||
export async function userListController(req: any, res: any) {
|
||||
const { nextKey, order } = req.query
|
||||
const itemsPerPage = 50
|
||||
|
||||
@@ -2,8 +2,8 @@ import connectDB from '@linux-odyssey/models'
|
||||
import express from 'express'
|
||||
import { engine } from 'express-handlebars'
|
||||
import basicAuth from 'express-basic-auth'
|
||||
import config from './config.js'
|
||||
import router from './router.js'
|
||||
import config from './config.ts'
|
||||
import router from './router.ts'
|
||||
|
||||
async function main() {
|
||||
await connectDB(config.db)
|
||||
+2
-1
@@ -1,7 +1,8 @@
|
||||
import { Command } from '@linux-odyssey/models'
|
||||
import Pagination from './pagination.ts'
|
||||
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export async function errorCommands(pagination) {
|
||||
export async function errorCommands(pagination: Pagination) {
|
||||
const commands = await Command.aggregate([
|
||||
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,103 @@
|
||||
import { Command, Session } from '@linux-odyssey/models'
|
||||
import Pagination from './pagination.ts'
|
||||
import { Types } from 'mongoose'
|
||||
|
||||
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<any[]> {
|
||||
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
|
||||
console.log(finishedAt)
|
||||
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,
|
||||
}
|
||||
})
|
||||
}
|
||||
interface User {
|
||||
username: string
|
||||
}
|
||||
interface SessionDocument {
|
||||
_id: Types.ObjectId
|
||||
user: User
|
||||
quest: string
|
||||
status: string
|
||||
createdAt: Date
|
||||
finishedAt: Date
|
||||
}
|
||||
|
||||
export async function sessionDetail(id: string): Promise<any> {
|
||||
const session = (await Session.findById(id)
|
||||
.populate('user')
|
||||
.exec()) as unknown as SessionDocument
|
||||
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.getTime() - createdAt.getTime())
|
||||
: '',
|
||||
commands,
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Session, User } from '@linux-odyssey/models'
|
||||
import Pagination from './pagination.ts'
|
||||
|
||||
function loginMethods(user) {
|
||||
function loginMethods(user: any) {
|
||||
const methods = []
|
||||
if (user.hashedPassword) methods.push('password')
|
||||
if (user.socialLogins) methods.push(...Object.keys(user.socialLogins))
|
||||
@@ -8,10 +9,10 @@ function loginMethods(user) {
|
||||
}
|
||||
|
||||
export function userCount() {
|
||||
return User.find().count()
|
||||
return User.countDocuments({})
|
||||
}
|
||||
|
||||
export async function userList(pagination) {
|
||||
export async function userList(pagination: Pagination) {
|
||||
const users = await Session.aggregate([
|
||||
pagination.match('user'),
|
||||
{
|
||||
@@ -2,12 +2,12 @@ import { Router } from 'express'
|
||||
|
||||
import { homeController } from './controllers/index.js'
|
||||
import { userListController } from './controllers/users.js'
|
||||
import { questListController } from './controllers/quests.js'
|
||||
import { commandListController } from './controllers/commands.js'
|
||||
import { questListController } from './controllers/quests.ts'
|
||||
import { commandListController } from './controllers/commands.ts'
|
||||
import {
|
||||
sessionDetailController,
|
||||
sessionListController,
|
||||
} from './controllers/sessions.js'
|
||||
} from './controllers/sessions.ts'
|
||||
|
||||
const router = Router()
|
||||
|
||||
@@ -3,7 +3,10 @@
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"rootDir": "src",
|
||||
"outDir": "./dist"
|
||||
"outDir": "./dist",
|
||||
"esModuleInterop": true,
|
||||
"noEmit": true,
|
||||
"allowImportingTsExtensions": true
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
|
||||
Reference in new issue
Block a user