Files
miaojingAI/audit_db_storage.js

68 lines
5.7 KiB
JavaScript

const { Client } = require('pg');
const fs = require('fs');
const path = require('path');
require('dotenv').config({ path: '/root/miaojingAI/.env.local' });
const c = new Client({ connectionString: process.env.LOCAL_DB_URL });
const root = '/root/miaojingAI';
function storageKeyFromUrl(url) {
if (!url || typeof url !== 'string') return null;
const marker = '/api/local-storage/';
const idx = url.indexOf(marker);
if (idx < 0) return null;
try { return decodeURIComponent(url.slice(idx + marker.length).split('?')[0]); } catch { return null; }
}
async function query(name, sql, params=[]) {
try { const r = await c.query(sql, params); return { name, rows: r.rows }; }
catch (e) { return { name, error: e.message }; }
}
(async () => {
await c.connect();
const out = {};
out.db = await query('db', `select current_database() as database, current_user as user, now() as checked_at`);
out.tables = await query('tables', `select schemaname, tablename from pg_tables where schemaname in ('public','auth') order by schemaname, tablename`);
const tableNames = out.tables.rows || [];
out.counts = [];
for (const t of tableNames) {
const full = t.schemaname === 'public' ? t.tablename : `${t.schemaname}.${t.tablename}`;
const r = await query(full, `select count(*)::int as count from ${full}`);
out.counts.push({ table: full, count: r.rows?.[0]?.count ?? null, error: r.error });
}
out.admins = await query('admins', `select id,email,nickname,role,membership_tier,is_active,created_at,updated_at from profiles where role='admin' or email in ('admin@example.com','admin@miaojing.ai') order by created_at`);
out.authAdmins = await query('auth_admins', `select id,email,created_at, password_hash is not null as has_password from auth.users where email in ('admin@example.com','admin@miaojing.ai') order by created_at`);
out.profileWithoutAuth = await query('profile_without_auth', `select p.id,p.email,p.role from profiles p left join auth.users u on u.id=p.id where u.id is null order by p.created_at limit 50`);
out.authWithoutProfile = await query('auth_without_profile', `select u.id,u.email from auth.users u left join profiles p on p.id=u.id where p.id is null order by u.created_at limit 50`);
out.worksMissingUser = await query('works_missing_user', `select w.id,w.user_id,w.title,w.type,w.status,w.created_at from works w left join profiles p on p.id=w.user_id where w.user_id is not null and p.id is null order by w.created_at desc limit 50`);
out.worksNullUser = await query('works_null_user', `select id,title,type,status,created_at from works where user_id is null order by created_at desc limit 50`);
out.worksByStatus = await query('works_by_status', `select status, count(*)::int from works group by status order by status`);
out.worksByUser = await query('works_by_user', `select p.email,p.nickname,w.user_id,count(*)::int as works from works w left join profiles p on p.id=w.user_id group by p.email,p.nickname,w.user_id order by works desc nulls last limit 20`);
out.workLikesMissing = await query('work_likes_missing_refs', `select wl.id,wl.user_id,wl.work_id from work_likes wl left join profiles p on p.id=wl.user_id left join works w on w.id=wl.work_id where p.id is null or w.id is null limit 50`);
out.creditMissingUser = await query('credit_missing_user', `select ct.id,ct.user_id,ct.amount,ct.type,ct.created_at from credit_transactions ct left join profiles p on p.id=ct.user_id where p.id is null limit 50`);
out.ordersMissingUser = await query('orders_missing_user', `select o.id,o.user_id,o.order_no,o.status,o.created_at from orders o left join profiles p on p.id=o.user_id where o.user_id is not null and p.id is null limit 50`);
out.apiKeysMissingUser = await query('api_keys_missing_user', `select k.id,k.user_id,k.provider,k.type from user_api_keys k left join profiles p on p.id=k.user_id where p.id is null limit 50`);
out.jobsMissingUser = await query('jobs_missing_user', `select j.id,j.user_id,j.type,j.status,j.created_at from generation_jobs j left join profiles p on p.id=j.user_id where j.user_id is not null and p.id is null order by j.created_at desc limit 50`);
out.jobsNullUser = await query('jobs_null_user', `select id,type,status,created_at from generation_jobs where user_id is null order by created_at desc limit 50`);
out.jobsByUser = await query('jobs_by_user', `select p.email,p.nickname,j.user_id,count(*)::int as jobs from generation_jobs j left join profiles p on p.id=j.user_id group by p.email,p.nickname,j.user_id order by jobs desc nulls last limit 20`);
out.worksFileCheck = { totalLocalUrls: 0, missing: [] };
const works = await query('works_urls', `select id,result_url,thumbnail_url from works order by created_at desc`);
for (const w of works.rows || []) {
for (const field of ['result_url','thumbnail_url']) {
const key = storageKeyFromUrl(w[field]);
if (!key) continue;
out.worksFileCheck.totalLocalUrls++;
const abs = path.join(root, 'local-storage', key);
if (!fs.existsSync(abs)) out.worksFileCheck.missing.push({ workId: w.id, field, key });
}
}
out.storage = {
localStorageExists: fs.existsSync(path.join(root,'local-storage')),
backupsExists: fs.existsSync(path.join(root,'backups')),
localFiles: 0,
galleryFiles: 0,
};
function walk(dir) { if (!fs.existsSync(dir)) return 0; let n=0; for (const e of fs.readdirSync(dir,{withFileTypes:true})) { const p=path.join(dir,e.name); n += e.isDirectory()?walk(p):1; } return n; }
out.storage.localFiles = walk(path.join(root,'local-storage'));
out.storage.galleryFiles = walk(path.join(root,'local-storage','gallery'));
console.log(JSON.stringify(out, null, 2));
await c.end();
})().catch(e => { console.error(e); process.exit(1); });