Add Valkey/Redis cache visualization and API autoloading for graphical inspection
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
# Universal Auth Module
|
||||
|
||||
## Overview
|
||||
This module provides a standardized, immutable authentication and token management system for any web project using PocketBase and Microsoft Graph. It supports user and agent tokens, delegated and app-only flows, and a consistent popup UI for login prompts.
|
||||
|
||||
## Features
|
||||
- Pattern-based: Choose 'pb-user', 'pb-agent', 'graph-user', 'graph-agent', or any combination (e.g., 'pb-user+graph-agent') for your project needs
|
||||
- Supports both user and agent tokens for PocketBase and Microsoft Graph
|
||||
- Special handling for Valkey/Redis agent login and token storage (for backend/automation flows)
|
||||
- Single source of truth for token storage, retrieval, and refresh
|
||||
- Consistent, hidden popup for all user login prompts
|
||||
- Extracts user info (display name, email) from PocketBase
|
||||
- Works in any frontend project with PocketBase
|
||||
|
||||
## Usage
|
||||
|
||||
### 1. Include in your project
|
||||
```html
|
||||
<script src="https://cdn.jsdelivr.net/npm/pocketbase@0.26.5/dist/pocketbase.umd.js"></script>
|
||||
<script src="auth/auth-universal.js"></script>
|
||||
```
|
||||
|
||||
### 2. Initialize the pattern
|
||||
```js
|
||||
// Choose your pattern:
|
||||
// 'pb-user' - PocketBase user login
|
||||
// 'pb-agent' - PocketBase agent/service login
|
||||
// 'graph-user' - Microsoft Graph delegated (user) login
|
||||
// 'graph-agent' - Microsoft Graph agent/app-only (client credentials)
|
||||
// Combine as needed: 'pb-user+graph-agent', 'pb-agent+graph-user', etc.
|
||||
Auth.use('pb-user+graph-agent');
|
||||
```
|
||||
|
||||
### 3. Get and use tokens
|
||||
```js
|
||||
const pbToken = await Auth.ensureToken('pb');
|
||||
const graphToken = await Auth.ensureToken('graph');
|
||||
const userInfo = Auth.getUserInfo();
|
||||
```
|
||||
|
||||
### 4. Handle reauthentication
|
||||
- If a token is missing or expired, Auth will automatically show a popup for login.
|
||||
- The popup is hidden at all other times.
|
||||
|
||||
### 5. Clear tokens (for testing or logout)
|
||||
```js
|
||||
Auth.clearToken('pb');
|
||||
Auth.clearToken('graph');
|
||||
```
|
||||
|
||||
## Secrets and Environment
|
||||
- For frontend, all secrets are handled by PocketBase OAuth (no client secrets exposed).
|
||||
- For backend/agent flows, use environment variables and server-side code to store secrets securely.
|
||||
- The module does not expose or require secrets in the frontend.
|
||||
|
||||
## Example Test Page
|
||||
See `auth-test.html` for a safe way to test all flows without affecting your main app.
|
||||
|
||||
## Rules for Copilot Instructions
|
||||
- Always use Auth.use() to set the pattern before requesting tokens. Supported types: 'pb-user', 'pb-agent', 'graph-user', 'graph-agent', or any combination.
|
||||
- Always use Auth.ensureToken(type) to get a valid token; it will handle refresh and prompt if needed.
|
||||
- Never store secrets in frontend code; use PocketBase OAuth for user login.
|
||||
- All login prompts must use the provided popup, never custom dialogs.
|
||||
- Token keys are immutable: 'pbUserToken', 'pbAgentToken', 'graphUserToken', 'graphAgentToken'.
|
||||
- User info extraction must use Auth.getUserInfo().
|
||||
- For agent/app-only tokens, use backend code, environment variables, and optionally Valkey/Redis for secure storage and retrieval.
|
||||
|
||||
## FAQ
|
||||
- **Can I use this in any project?** Yes, as long as you include PocketBase and this module.
|
||||
- **Does it work for both user and agent tokens?** Yes, with the correct pattern and backend support for agent tokens (including Valkey/Redis for automation).
|
||||
- **Is the popup customizable?** The style can be changed, but the flow must remain consistent for all projects.
|
||||
- **How are secrets handled?** Only via backend environment variables for agent tokens; never exposed in frontend.
|
||||
|
||||
---
|
||||
For further integration, see the comments in `auth-universal.js` and the example test page.
|
||||
@@ -0,0 +1,85 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Universal Auth Test</title>
|
||||
<script src="https://cdn.jsdelivr.net/npm/pocketbase@0.26.5/dist/pocketbase.umd.js"></script>
|
||||
<script src="auth-universal.js"></script>
|
||||
<style>
|
||||
body { font-family: sans-serif; background: #f3f4f6; margin: 0; padding: 2em; }
|
||||
.test-block { background: #fff; border-radius: 1em; box-shadow: 0 2px 12px #0001; padding: 2em; margin-bottom: 2em; }
|
||||
.pattern-btn { margin: 0.2em 0.5em 0.2em 0; padding: 0.5em 1.2em; border-radius: 0.5em; border: none; background: #2563eb; color: #fff; cursor: pointer; font-size: 1em; }
|
||||
.pattern-btn.active { background: #059669; }
|
||||
#output { font-family: monospace; background: #f9fafb; border-radius: 0.5em; padding: 1em; margin-top: 1em; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="test-block">
|
||||
<h2>Choose Auth Pattern</h2>
|
||||
<div id="patterns"></div>
|
||||
<div style="margin-top:1em;">
|
||||
<button onclick="clearAll()" class="pattern-btn" style="background:#b91c1c;">Clear All Tokens</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="test-block">
|
||||
<h2>Test Actions</h2>
|
||||
<button onclick="testToken('pb-user')" class="pattern-btn">Test PB User</button>
|
||||
<button onclick="testToken('pb-agent')" class="pattern-btn">Test PB Agent</button>
|
||||
<button onclick="testToken('graph-user')" class="pattern-btn">Test Graph User</button>
|
||||
<button onclick="testToken('graph-agent')" class="pattern-btn">Test Graph Agent</button>
|
||||
<button onclick="showUserInfo()" class="pattern-btn" style="background:#6d28d9;">Show User Info</button>
|
||||
<div id="output"></div>
|
||||
</div>
|
||||
<script>
|
||||
const patterns = [
|
||||
'pb-user',
|
||||
'pb-agent',
|
||||
'graph-user',
|
||||
'graph-agent',
|
||||
'pb-user+graph-user',
|
||||
'pb-user+graph-agent',
|
||||
'pb-agent+graph-user',
|
||||
'pb-agent+graph-agent',
|
||||
'pb-user+pb-agent+graph-user+graph-agent'
|
||||
];
|
||||
let currentPattern = patterns[0];
|
||||
function setPattern(p) {
|
||||
currentPattern = p;
|
||||
Auth.use(p);
|
||||
document.querySelectorAll('.pattern-btn').forEach(btn => btn.classList.remove('active'));
|
||||
document.querySelectorAll('.pattern-btn[data-pattern="' + p + '"]').forEach(btn => btn.classList.add('active'));
|
||||
document.getElementById('output').textContent = `Pattern set: ${p}`;
|
||||
}
|
||||
function renderPatterns() {
|
||||
const el = document.getElementById('patterns');
|
||||
el.innerHTML = '';
|
||||
patterns.forEach(p => {
|
||||
const btn = document.createElement('button');
|
||||
btn.textContent = p;
|
||||
btn.className = 'pattern-btn' + (p === currentPattern ? ' active' : '');
|
||||
btn.setAttribute('data-pattern', p);
|
||||
btn.onclick = () => setPattern(p);
|
||||
el.appendChild(btn);
|
||||
});
|
||||
}
|
||||
renderPatterns();
|
||||
setPattern(currentPattern);
|
||||
async function testToken(type) {
|
||||
try {
|
||||
const token = await Auth.ensureToken(type);
|
||||
document.getElementById('output').textContent = `${type} token: ${token ? token.substring(0, 40) + '...' : 'NONE'}`;
|
||||
} catch (err) {
|
||||
document.getElementById('output').textContent = `Error: ${err.message}`;
|
||||
}
|
||||
}
|
||||
function clearAll() {
|
||||
['pb-user','pb-agent','graph-user','graph-agent'].forEach(Auth.clearToken);
|
||||
document.getElementById('output').textContent = 'All tokens cleared.';
|
||||
}
|
||||
function showUserInfo() {
|
||||
const info = Auth.getUserInfo();
|
||||
document.getElementById('output').textContent = 'User Info: ' + JSON.stringify(info, null, 2);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,220 @@
|
||||
// Universal Auth Module: Immutable, Pattern-Based, Project-Agnostic
|
||||
// Usage: import and call Auth.use('pb-graph') or Auth.use('pb') or Auth.use('graph')
|
||||
// Provides: getToken(type), ensureToken(type), refreshToken(type), getUserInfo(), promptReauth()
|
||||
// Token types: 'pb' (PocketBase user/agent), 'graph' (Graph delegated), 'graphAgent' (Graph app-only)
|
||||
// All storage, retrieval, refresh, and UI are standardized and immutable
|
||||
|
||||
const TOKEN_KEYS = {
|
||||
'pb-user': 'pbUserToken',
|
||||
'pb-agent': 'pbAgentToken',
|
||||
'graph-user': 'graphUserToken',
|
||||
'graph-agent': 'graphAgentToken'
|
||||
};
|
||||
|
||||
function parsePattern(pattern) {
|
||||
return pattern.split('+').map(s => s.trim()).filter(Boolean);
|
||||
}
|
||||
|
||||
window.Auth = (() => {
|
||||
let pattern = 'pb-user'; // default
|
||||
let enabledTypes = ['pb-user'];
|
||||
let pb = null;
|
||||
let popup = null;
|
||||
let agentCreds = { email: '', password: '' };
|
||||
let superuserCreds = { email: '', password: '' };
|
||||
|
||||
// --- Setup ---
|
||||
function use(type) {
|
||||
pattern = type;
|
||||
enabledTypes = parsePattern(type);
|
||||
if (enabledTypes.some(t => t.startsWith('pb'))) {
|
||||
pb = window.PocketBase ? new PocketBase('https://pocketbase.ccllc.pro') : null;
|
||||
}
|
||||
// Only one-time setup
|
||||
if (!document.getElementById('authPopup')) {
|
||||
createPopup();
|
||||
}
|
||||
}
|
||||
|
||||
// --- Token Storage ---
|
||||
function getToken(type) {
|
||||
return localStorage.getItem(TOKEN_KEYS[type]) || '';
|
||||
}
|
||||
function setToken(type, value) {
|
||||
if (value) localStorage.setItem(TOKEN_KEYS[type], value);
|
||||
}
|
||||
function clearToken(type) {
|
||||
localStorage.removeItem(TOKEN_KEYS[type]);
|
||||
}
|
||||
|
||||
// --- Info Extraction ---
|
||||
function getUserInfo() {
|
||||
if (pb && pb.authStore.model) {
|
||||
return {
|
||||
displayName: pb.authStore.model.display_name || pb.authStore.model.name || pb.authStore.model.email || pb.authStore.model.username || 'Unknown',
|
||||
email: pb.authStore.model.email || pb.authStore.model.username || null
|
||||
};
|
||||
}
|
||||
return { displayName: '', email: '' };
|
||||
}
|
||||
|
||||
// --- Token Ensure/Refresh ---
|
||||
async function ensureToken(type) {
|
||||
let token = getToken(type);
|
||||
if (token) return token;
|
||||
if (type === 'pb-user' && pb) {
|
||||
if (pb.authStore.isValid) {
|
||||
setToken('pb-user', pb.authStore.token);
|
||||
return pb.authStore.token;
|
||||
}
|
||||
// Choose login method: email/password or OAuth
|
||||
if (pattern.includes('pb-user-oauth')) {
|
||||
return await promptReauth('pb-user-oauth');
|
||||
} else {
|
||||
return await promptReauth('pb-user');
|
||||
}
|
||||
}
|
||||
if (type === 'pb-superuser' && pb) {
|
||||
// Prompt for superuser credentials
|
||||
await promptReauth('pb-superuser');
|
||||
// Use superuser credentials to login
|
||||
const authData = await pb.collection('_superuser').authWithPassword(superuserCreds.email, superuserCreds.password);
|
||||
setToken('pb-superuser', pb.authStore.token);
|
||||
return pb.authStore.token;
|
||||
}
|
||||
if (type === 'pb-agent' && pb) {
|
||||
// Prompt for agent credentials if not set
|
||||
if (!agentCreds.email || !agentCreds.password) {
|
||||
await promptReauth('pb-agent');
|
||||
}
|
||||
// Use agent credentials to login
|
||||
const authData = await pb.collection('users').authWithPassword(agentCreds.email, agentCreds.password);
|
||||
setToken('pb-agent', pb.authStore.token);
|
||||
return pb.authStore.token;
|
||||
}
|
||||
if (type === 'graph-user' && pb) {
|
||||
// OAuth only
|
||||
return await promptReauth('graph-user');
|
||||
}
|
||||
if (type === 'graph-agent') {
|
||||
// Prompt for agent credentials if not set
|
||||
if (!agentCreds.email || !agentCreds.password) {
|
||||
await promptReauth('graph-agent');
|
||||
}
|
||||
// Use agent credentials to get token (simulate, real flow is backend)
|
||||
setToken('graph-agent', 'AGENT_TOKEN_' + agentCreds.email);
|
||||
return getToken('graph-agent');
|
||||
}
|
||||
throw new Error('Unknown token type');
|
||||
}
|
||||
|
||||
async function refreshToken(type) {
|
||||
if (type === 'graph-user' && pb) {
|
||||
try {
|
||||
const authData = await pb.collection('users').authRefresh();
|
||||
const meta = authData?.meta || pb?.authStore?.model?.meta || {};
|
||||
const token = meta.graphAccessToken || meta.graph_token || meta.graphToken || meta.accessToken || meta.access_token || meta.token || meta.rawToken || meta?.authData?.access_token || '';
|
||||
setToken('graph-user', token);
|
||||
return token || '';
|
||||
} catch (err) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
// Add refresh logic for other types as needed
|
||||
return '';
|
||||
}
|
||||
|
||||
// --- Reauth Popup ---
|
||||
function createPopup() {
|
||||
popup = document.createElement('div');
|
||||
popup.id = 'authPopup';
|
||||
popup.style = 'display:none;position:fixed;top:0;left:0;width:100vw;height:100vh;background:rgba(0,0,0,0.4);z-index:9999;align-items:center;justify-content:center;';
|
||||
popup.innerHTML = `
|
||||
<div style="background:#fff;padding:2em 2.5em;border-radius:1em;box-shadow:0 2px 24px #0002;text-align:center;max-width:350px;margin:auto;">
|
||||
<h2 style="font-size:1.3em;margin-bottom:1em;">Sign In Required</h2>
|
||||
<div id="authPopupForms"></div>
|
||||
<div id="authPopupError" style="color:#b91c1c;margin-top:1em;display:none;"></div>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(popup);
|
||||
}
|
||||
|
||||
function showForm(type) {
|
||||
const forms = {
|
||||
'pb-user': `<input id="pbUserEmail" type="email" placeholder="Email" style="width:90%;margin-bottom:0.5em;"><br><input id="pbUserPassword" type="password" placeholder="Password" style="width:90%;margin-bottom:0.5em;"><br><button id="authPopupBtn" style="background:#2563eb;color:#fff;padding:0.7em 2em;border:none;border-radius:0.5em;font-size:1em;cursor:pointer;">Sign in</button>`,
|
||||
'pb-user-oauth': `<button id="authPopupBtn" style="background:#2563eb;color:#fff;padding:0.7em 2em;border:none;border-radius:0.5em;font-size:1em;cursor:pointer;">Sign in with Microsoft</button>`,
|
||||
'pb-superuser': `<input id="pbSuperuserEmail" type="email" placeholder="Superuser Email" style="width:90%;margin-bottom:0.5em;"><br><input id="pbSuperuserPassword" type="password" placeholder="Superuser Password" style="width:90%;margin-bottom:0.5em;"><br><button id="authPopupBtn" style="background:#d97706;color:#fff;padding:0.7em 2em;border:none;border-radius:0.5em;font-size:1em;cursor:pointer;">Sign in as Superuser</button>`,
|
||||
'pb-agent': `<input id="pbAgentEmail" type="email" placeholder="Agent Email" style="width:90%;margin-bottom:0.5em;"><br><input id="pbAgentPassword" type="password" placeholder="Agent Password" style="width:90%;margin-bottom:0.5em;"><br><button id="authPopupBtn" style="background:#059669;color:#fff;padding:0.7em 2em;border:none;border-radius:0.5em;font-size:1em;cursor:pointer;">Sign in as Agent</button>`,
|
||||
'graph-user': `<button id="authPopupBtn" style="background:#2563eb;color:#fff;padding:0.7em 2em;border:none;border-radius:0.5em;font-size:1em;cursor:pointer;">Sign in with Microsoft</button>`,
|
||||
'graph-agent': `<input id="graphAgentEmail" type="email" placeholder="Agent Email" style="width:90%;margin-bottom:0.5em;"><br><input id="graphAgentPassword" type="password" placeholder="Agent Password" style="width:90%;margin-bottom:0.5em;"><br><button id="authPopupBtn" style="background:#059669;color:#fff;padding:0.7em 2em;border:none;border-radius:0.5em;font-size:1em;cursor:pointer;">Sign in as Graph Agent</button>`
|
||||
};
|
||||
document.getElementById('authPopupForms').innerHTML = forms[type] || '';
|
||||
}
|
||||
|
||||
async function promptReauth(type) {
|
||||
showForm(type);
|
||||
popup.style.display = 'flex';
|
||||
return new Promise((resolve, reject) => {
|
||||
document.getElementById('authPopupBtn').onclick = async () => {
|
||||
try {
|
||||
if (type === 'pb-user') {
|
||||
const email = document.getElementById('pbUserEmail').value;
|
||||
const password = document.getElementById('pbUserPassword').value;
|
||||
const authData = await pb.collection('users').authWithPassword(email, password);
|
||||
setToken('pb-user', pb.authStore.token);
|
||||
popup.style.display = 'none';
|
||||
resolve(pb.authStore.token);
|
||||
} else if (type === 'pb-user-oauth') {
|
||||
const authData = await pb.collection('users').authWithOAuth2({ provider: 'microsoft' });
|
||||
setToken('pb-user', authData?.meta?.graphAccessToken || '');
|
||||
popup.style.display = 'none';
|
||||
resolve(authData?.meta?.graphAccessToken || '');
|
||||
} else if (type === 'pb-superuser') {
|
||||
superuserCreds.email = document.getElementById('pbSuperuserEmail').value;
|
||||
superuserCreds.password = document.getElementById('pbSuperuserPassword').value;
|
||||
popup.style.display = 'none';
|
||||
resolve();
|
||||
} else if (type === 'pb-agent') {
|
||||
agentCreds.email = document.getElementById('pbAgentEmail').value;
|
||||
agentCreds.password = document.getElementById('pbAgentPassword').value;
|
||||
popup.style.display = 'none';
|
||||
resolve();
|
||||
} else if (type === 'graph-user') {
|
||||
const authData = await pb.collection('users').authWithOAuth2({ provider: 'microsoft' });
|
||||
setToken('graph-user', authData?.meta?.graphAccessToken || '');
|
||||
popup.style.display = 'none';
|
||||
resolve(authData?.meta?.graphAccessToken || '');
|
||||
} else if (type === 'graph-agent') {
|
||||
agentCreds.email = document.getElementById('graphAgentEmail').value;
|
||||
agentCreds.password = document.getElementById('graphAgentPassword').value;
|
||||
popup.style.display = 'none';
|
||||
resolve();
|
||||
}
|
||||
} catch (err) {
|
||||
document.getElementById('authPopupError').textContent = err.message || 'Authentication failed.';
|
||||
document.getElementById('authPopupError').style.display = '';
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// --- API ---
|
||||
return {
|
||||
use,
|
||||
getToken,
|
||||
setToken,
|
||||
clearToken,
|
||||
getUserInfo,
|
||||
ensureToken,
|
||||
refreshToken,
|
||||
promptReauth
|
||||
};
|
||||
})();
|
||||
|
||||
window.Auth = Auth;
|
||||
|
||||
// Example usage:
|
||||
// Auth.use('pb-graph');
|
||||
// const token = await Auth.ensureToken('graph');
|
||||
// const user = Auth.getUserInfo();
|
||||
// All login prompts are handled via the popup, which is hidden unless needed.
|
||||
@@ -0,0 +1,29 @@
|
||||
// Simple Express API to serve all Redis/Valkey cache keys and values as JSON (CommonJS)
|
||||
const express = require('express');
|
||||
const Redis = require('ioredis');
|
||||
const app = express();
|
||||
const redis = new Redis({
|
||||
host: process.env.REDIS_HOST || '127.0.0.1',
|
||||
port: Number(process.env.REDIS_PORT || 6379),
|
||||
password: process.env.REDIS_PASSWORD,
|
||||
});
|
||||
|
||||
app.get('/api/cache', async (req, res) => {
|
||||
try {
|
||||
const keys = await redis.keys('*');
|
||||
const result = [];
|
||||
for (const key of keys) {
|
||||
let value = await redis.get(key);
|
||||
try { value = JSON.parse(value); } catch {}
|
||||
result.push({ key, value });
|
||||
}
|
||||
res.json(result);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
const port = process.env.CACHE_API_PORT || 3006;
|
||||
app.listen(port, () => {
|
||||
console.log(`Cache API running on http://localhost:${port}/api/cache`);
|
||||
});
|
||||
@@ -0,0 +1,29 @@
|
||||
// Simple Express API to serve all Redis/Valkey cache keys and values as JSON
|
||||
const express = require('express');
|
||||
const Redis = require('ioredis');
|
||||
const app = express();
|
||||
const redis = new Redis({
|
||||
host: process.env.REDIS_HOST || '127.0.0.1',
|
||||
port: Number(process.env.REDIS_PORT || 6379),
|
||||
password: process.env.REDIS_PASSWORD,
|
||||
});
|
||||
|
||||
app.get('/api/cache', async (req, res) => {
|
||||
try {
|
||||
const keys = await redis.keys('*');
|
||||
const result = [];
|
||||
for (const key of keys) {
|
||||
let value = await redis.get(key);
|
||||
try { value = JSON.parse(value); } catch {}
|
||||
result.push({ key, value });
|
||||
}
|
||||
res.json(result);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
const port = process.env.CACHE_API_PORT || 3030;
|
||||
app.listen(port, () => {
|
||||
console.log(`Cache API running on http://localhost:${port}/api/cache`);
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
// Script to list all Redis/Valkey keys and their values for visualization (CommonJS)
|
||||
const Redis = require('ioredis');
|
||||
|
||||
const redis = new Redis({
|
||||
host: process.env.REDIS_HOST || '127.0.0.1',
|
||||
port: Number(process.env.REDIS_PORT || 6379),
|
||||
password: process.env.REDIS_PASSWORD,
|
||||
});
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
const keys = await redis.keys('*');
|
||||
const result = [];
|
||||
for (const key of keys) {
|
||||
let value = await redis.get(key);
|
||||
try { value = JSON.parse(value); } catch {}
|
||||
result.push({ key, value });
|
||||
}
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
process.exit(0);
|
||||
} catch (err) {
|
||||
console.error('Error listing Redis keys:', err);
|
||||
process.exit(1);
|
||||
}
|
||||
})();
|
||||
@@ -0,0 +1,25 @@
|
||||
// Script to list all Redis/Valkey keys and their values for visualization
|
||||
const Redis = require('ioredis');
|
||||
|
||||
const redis = new Redis({
|
||||
host: process.env.REDIS_HOST || '127.0.0.1',
|
||||
port: Number(process.env.REDIS_PORT || 6379),
|
||||
password: process.env.REDIS_PASSWORD,
|
||||
});
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
const keys = await redis.keys('*');
|
||||
const result = [];
|
||||
for (const key of keys) {
|
||||
let value = await redis.get(key);
|
||||
try { value = JSON.parse(value); } catch {}
|
||||
result.push({ key, value });
|
||||
}
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
process.exit(0);
|
||||
} catch (err) {
|
||||
console.error('Error listing Redis keys:', err);
|
||||
process.exit(1);
|
||||
}
|
||||
})();
|
||||
@@ -0,0 +1,77 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Valkey/Redis Cache Visualization</title>
|
||||
<style>
|
||||
body { font-family: sans-serif; background: #f8fafc; margin: 0; padding: 2em; }
|
||||
h1 { color: #2563eb; }
|
||||
table { border-collapse: collapse; width: 100%; background: #fff; }
|
||||
th, td { border: 1px solid #e5e7eb; padding: 0.5em 1em; }
|
||||
th { background: #f1f5f9; }
|
||||
tr:nth-child(even) { background: #f9fafb; }
|
||||
.json { font-family: monospace; font-size: 0.95em; white-space: pre; }
|
||||
.expand { cursor: pointer; color: #2563eb; text-decoration: underline; }
|
||||
.search { margin-bottom: 1em; padding: 0.5em; width: 300px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Valkey/Redis Cache Visualization</h1>
|
||||
<input class="search" type="text" id="search" placeholder="Search by key or value..." />
|
||||
<table id="cacheTable">
|
||||
<thead>
|
||||
<tr><th>Key</th><th>Value</th></tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
<script>
|
||||
let cacheData = [];
|
||||
function renderTable(data) {
|
||||
const tbody = document.querySelector('#cacheTable tbody');
|
||||
tbody.innerHTML = '';
|
||||
for (const { key, value } of data) {
|
||||
const tr = document.createElement('tr');
|
||||
const tdKey = document.createElement('td');
|
||||
tdKey.textContent = key;
|
||||
const tdValue = document.createElement('td');
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
const btn = document.createElement('span');
|
||||
btn.textContent = '[expand]';
|
||||
btn.className = 'expand';
|
||||
btn.onclick = () => {
|
||||
btn.outerHTML = `<div class='json'>${JSON.stringify(value, null, 2)}</div>`;
|
||||
};
|
||||
tdValue.appendChild(btn);
|
||||
} else {
|
||||
tdValue.textContent = String(value);
|
||||
}
|
||||
tr.appendChild(tdKey);
|
||||
tr.appendChild(tdValue);
|
||||
tbody.appendChild(tr);
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch cache data from API
|
||||
async function loadCache() {
|
||||
try {
|
||||
const res = await fetch('http://localhost:3006/api/cache');
|
||||
cacheData = await res.json();
|
||||
renderTable(cacheData);
|
||||
} catch (err) {
|
||||
document.querySelector('#cacheTable tbody').innerHTML = `<tr><td colspan='2' style='color:#b91c1c'>Failed to load cache data: ${err}</td></tr>`;
|
||||
}
|
||||
}
|
||||
|
||||
// Search functionality
|
||||
document.getElementById('search').addEventListener('input', function() {
|
||||
const q = this.value.toLowerCase();
|
||||
renderTable(cacheData.filter(({key, value}) =>
|
||||
key.toLowerCase().includes(q) || JSON.stringify(value).toLowerCase().includes(q)
|
||||
));
|
||||
});
|
||||
|
||||
// Initial load
|
||||
loadCache();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user