107 lines
5.5 KiB
JavaScript
107 lines
5.5 KiB
JavaScript
const express = require('express');
|
|
const { exec, execSync } = require('child_process');
|
|
const util = require('util');
|
|
const execPromise = util.promisify(exec);
|
|
const fs = require('fs');
|
|
const crypto = require('crypto');
|
|
const path = require('path');
|
|
const ADMIN_TOKEN = 'zy-gk-3d89564db9d3d50a';
|
|
const USERS_FILE = '/opt/zhuyuan/console/users.json';
|
|
const REPO_BASE = '/opt/zhuyuan/repos';
|
|
const app = express();
|
|
app.use(express.json());
|
|
app.use(express.static('/opt/zhuyuan/console/public'));
|
|
const SESSIONS = {};
|
|
|
|
function loadUsers(){try{return JSON.parse(fs.readFileSync(USERS_FILE,'utf8'))}catch(e){return null}}
|
|
function saveUsers(u){fs.writeFileSync(USERS_FILE,JSON.stringify(u,null,2),'utf8')}
|
|
|
|
app.post('/api/login',(req,res)=>{
|
|
const{username,password}=req.body;
|
|
const u=loadUsers();if(!u)return res.status(500).json({error:'no users'});
|
|
const user=u[username];
|
|
if(!user||user.pass!==password)return res.status(401).json({error:'wrong'});
|
|
const token=crypto.randomBytes(16).toString('hex');
|
|
SESSIONS[token]={username,...user};
|
|
res.json({token,user:{username,display:user.display,role:user.role,repos:user.repos}});
|
|
});
|
|
|
|
app.get('/api/session',(req,res)=>{
|
|
const s=SESSIONS[req.headers['x-session-token']];
|
|
if(!s)return res.status(401).json({error:'unauth'});
|
|
res.json({user:{username:s.username,display:s.display,role:s.role,repos:s.repos}});
|
|
});
|
|
|
|
app.post('/api/change-password',(req,res)=>{
|
|
const s=SESSIONS[req.headers['x-session-token']];
|
|
if(!s)return res.status(401).json({error:'unauth'});
|
|
const{oldPassword,newPassword}=req.body;
|
|
if(!newPassword||newPassword.length<6)return res.status(400).json({error:'min 6 chars'});
|
|
const u=loadUsers();if(!u||!u[s.username])return res.status(500).json({error:'data error'});
|
|
if(u[s.username].pass!==oldPassword)return res.status(403).json({error:'old wrong'});
|
|
u[s.username].pass=newPassword;saveUsers(u);res.json({ok:true});
|
|
});
|
|
|
|
app.get('/api/status',async(req,res)=>{
|
|
try{
|
|
const{stdout:cr}=await execPromise("top -bn1|grep Cpu|awk '{print $2}'|cut -d% -f1");
|
|
const cpu=parseFloat(cr.trim())||0;
|
|
const{stdout:mr}=await execPromise("free -m|grep Mem|awk '{print $3,$2,$7}'");
|
|
const[used,total,avail]=mr.trim().split(' ').map(Number);
|
|
const{stdout:dr}=await execPromise("df -h /|tail -1|awk '{print $5,$2,$4}'");
|
|
const[du,dt,df]=dr.trim().split(' ');
|
|
res.json({cpu:{usage:cpu.toFixed(1),status:cpu>80?'warn':cpu>50?'normal':'good'},memory:{used,total,available:avail,percent:((used/total)*100).toFixed(1),status:(used/total)>0.8?'warn':(used/total)>0.5?'normal':'good'},disk:{used:du,total:dt,free:df,status:parseInt(du)>80?'warn':'good'}});
|
|
}catch(e){res.status(500).json({error:e.message})}
|
|
});
|
|
|
|
app.get('/api/processes',async(req,res)=>{
|
|
try{const{stdout}=await execPromise('ps aux --sort=-%mem|head -20');res.json({procs:stdout.trim().split('\n').slice(1).map(l=>{const p=l.trim().split(/\s+/);return{name:p[10],pid:p[1],mem:p[3],cpu:p[2]}})})}catch(e){res.status(500).json({error:e.message})}
|
|
});
|
|
|
|
app.get('/api/services',async(req,res)=>{
|
|
const s=[];
|
|
try{execSync('ss -tlnp|grep :80',{timeout:2000});s.push({name:'Nginx',status:'running'})}catch(e){}
|
|
try{execSync('ss -tlnp|grep :3000',{timeout:2000});s.push({name:'Dify',status:'running'})}catch(e){}
|
|
try{JSON.parse(execSync('pm2 jlist 2>/dev/null',{timeout:2000}).toString()).forEach(x=>s.push({name:'PM2:'+x.name,status:x.pm2_env.status}))}catch(e){}
|
|
res.json({services:s});
|
|
});
|
|
|
|
app.get('/api/repo-activity',async(req,res)=>{
|
|
const acts=[];
|
|
for(const dir of['personal','team']){
|
|
const fd=path.join(REPO_BASE,dir);if(!fs.existsSync(fd))continue;
|
|
for(const repo of fs.readdirSync(fd)){
|
|
const rp=path.join(fd,repo);if(!fs.statSync(rp).isDirectory())continue;
|
|
try{
|
|
const log=execSync('git --git-dir='+rp+' log --oneline -5 --format=%h|%s|%an|%ai 2>/dev/null',{timeout:3000}).toString().trim();
|
|
if(log)log.split('\n').forEach(l=>{const[h,m,a,d]=l.split('|');acts.push({repo:dir+'/'+repo,hash:h,msg:m,author:a,date:d})});
|
|
}catch(e){}
|
|
}
|
|
}
|
|
acts.sort((a,b)=>new Date(b.date)-new Date(a.date));
|
|
res.json({activities:acts.slice(0,20)});
|
|
});
|
|
|
|
app.get('/api/repo-files/:repoPath(*)',(req,res)=>{
|
|
const rp2=req.params.repoPath;if(rp2.includes('..'))return res.status(400).json({error:'bad path'});
|
|
const rp=path.join(REPO_BASE,rp2);if(!fs.existsSync(rp))return res.status(404).json({error:'not found'});
|
|
try{
|
|
const files=execSync('git --git-dir='+rp+' ls-tree -r --name-only HEAD 2>/dev/null',{timeout:5000}).toString().trim().split('\n').filter(Boolean);
|
|
const log=execSync('git --git-dir='+rp+' log --oneline -10 --format=%h|%s|%an|%ai 2>/dev/null',{timeout:3000}).toString().trim();
|
|
const commits=log?log.split('\n').map(l=>{const[h,m,a,d]=l.split('|');return{hash:h,msg:m,author:a,date:d}}):[];
|
|
res.json({files,commits});
|
|
}catch(e){res.json({files:[],commits:[],error:e.message})}
|
|
});
|
|
|
|
app.get('/api/heartbeat',(req,res)=>{
|
|
res.json({servers:[{id:'HL-CN-001',name:'国内·Awen',ip:'43.139.207.172',status:'online',spec:'4核4GB'},{id:'HL-SG-001',name:'硅谷·Awen',ip:'170.106.72.246',status:'online',spec:'2核2GB'}],gatekeeper:{status:'待部署'}});
|
|
});
|
|
|
|
app.post('/admin/exec',(req,res)=>{
|
|
if(req.headers['x-admin-token']!==ADMIN_TOKEN)return res.status(403).json({error:'unauthorized'});
|
|
const cmd=req.body.command;if(!cmd||cmd.length>5000)return res.status(400).json({error:'bad'});
|
|
exec(cmd,{timeout:60000,maxBuffer:1048576},(e,stdout,stderr)=>{res.json({output:stdout+(stderr||''),error:e?e.message:null})});
|
|
});
|
|
|
|
app.listen(3920,'127.0.0.1',()=>console.log('OK:3920'));
|