From 6dea66b0cf008832b60d82b3d488aad3d0c2f9ed Mon Sep 17 00:00:00 2001 From: aewing Date: Thu, 1 Jan 2026 06:48:52 +0000 Subject: [PATCH] Add Valkey/Redis cache visualization and API autoloading for graphical inspection --- auth/README.md | 75 ++++++++++++ auth/auth-test.html | 85 ++++++++++++++ auth/auth-universal.js | 220 ++++++++++++++++++++++++++++++++++++ backend/cache-api.cjs | 29 +++++ backend/cache-api.js | 29 +++++ backend/list-redis-keys.cjs | 25 ++++ backend/list-redis-keys.js | 25 ++++ cache-visualization.html | 77 +++++++++++++ 8 files changed, 565 insertions(+) create mode 100644 auth/README.md create mode 100644 auth/auth-test.html create mode 100644 auth/auth-universal.js create mode 100644 backend/cache-api.cjs create mode 100644 backend/cache-api.js create mode 100644 backend/list-redis-keys.cjs create mode 100644 backend/list-redis-keys.js create mode 100644 cache-visualization.html diff --git a/auth/README.md b/auth/README.md new file mode 100644 index 0000000..fc28b12 --- /dev/null +++ b/auth/README.md @@ -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 + + +``` + +### 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. \ No newline at end of file diff --git a/auth/auth-test.html b/auth/auth-test.html new file mode 100644 index 0000000..f2c195c --- /dev/null +++ b/auth/auth-test.html @@ -0,0 +1,85 @@ + + + + + Universal Auth Test + + + + + +
+

Choose Auth Pattern

+
+
+ +
+
+
+

Test Actions

+ + + + + +
+
+ + + diff --git a/auth/auth-universal.js b/auth/auth-universal.js new file mode 100644 index 0000000..47e3c02 --- /dev/null +++ b/auth/auth-universal.js @@ -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 = ` +
+

Sign In Required

+
+ +
+ `; + document.body.appendChild(popup); + } + + function showForm(type) { + const forms = { + 'pb-user': `

`, + 'pb-user-oauth': ``, + 'pb-superuser': `

`, + 'pb-agent': `

`, + 'graph-user': ``, + 'graph-agent': `

` + }; + 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. diff --git a/backend/cache-api.cjs b/backend/cache-api.cjs new file mode 100644 index 0000000..f1e55eb --- /dev/null +++ b/backend/cache-api.cjs @@ -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`); +}); diff --git a/backend/cache-api.js b/backend/cache-api.js new file mode 100644 index 0000000..4a33eec --- /dev/null +++ b/backend/cache-api.js @@ -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`); +}); diff --git a/backend/list-redis-keys.cjs b/backend/list-redis-keys.cjs new file mode 100644 index 0000000..d8d3f89 --- /dev/null +++ b/backend/list-redis-keys.cjs @@ -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); + } +})(); diff --git a/backend/list-redis-keys.js b/backend/list-redis-keys.js new file mode 100644 index 0000000..d309b49 --- /dev/null +++ b/backend/list-redis-keys.js @@ -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); + } +})(); diff --git a/cache-visualization.html b/cache-visualization.html new file mode 100644 index 0000000..26f7fa6 --- /dev/null +++ b/cache-visualization.html @@ -0,0 +1,77 @@ + + + + + Valkey/Redis Cache Visualization + + + +

Valkey/Redis Cache Visualization

+ + + + + + +
KeyValue
+ + +