feat(plugins): expose plugin routes and load FurryPlace SDK

Add setupPluginRoutes to the server and register it in index.ts.
Include /furryplace-sdk.js on admin, index and moderation backup pages
to enable custom button extensions.
This commit is contained in:
2025-10-04 16:40:55 -07:00
parent c291b68285
commit a86cd87964
9 changed files with 751 additions and 0 deletions
+3
View File
@@ -88,6 +88,9 @@
href="./img/apple-touch-icon.png" href="./img/apple-touch-icon.png"
/> />
<link rel="manifest" href="./site.webmanifest" /> <link rel="manifest" href="./site.webmanifest" />
<!-- FurryPlace SDK for custom button extensions -->
<script src="/furryplace-sdk.js"></script>
</head> </head>
<body data-sveltekit-preload-data="hover"> <body data-sveltekit-preload-data="hover">
+345
View File
@@ -0,0 +1,345 @@
/**
* FurryPlace SDK - Button Extension API
*
* This SDK allows external scripts to add custom buttons to the FurryPlace UI.
*
* @example
* // Register a custom button
* window.FurryPlaceSDK.registerButton({
* id: 'my-custom-button',
* title: 'My Button',
* icon: '<svg>...</svg>',
* position: 'before-leaderboard', // or 'after-leaderboard', 'top', 'bottom'
* onClick: (context) => {
* console.log('Button clicked!', context);
* },
* condition: (context) => {
* // Optional: only show when certain conditions are met
* return context.user?.isLoggedIn;
* }
* });
*/
(function() {
'use strict';
const SDK_VERSION = '1.0.0';
const registeredButtons = [];
let isInitialized = false;
let injectionPoint = null;
// Find the button container in the DOM
function findButtonContainer() {
// Look for the container with class "flex flex-col items-center gap-3"
const containers = document.querySelectorAll('.flex.flex-col.items-center.gap-3');
for (const container of containers) {
// Verify it contains the expected buttons by checking for specific classes
const hasExpectedButtons = container.querySelector('.btn.btn-square.shadow-md');
if (hasExpectedButtons) {
return container;
}
}
return null;
}
// Wait for the DOM to contain the button container
function waitForButtonContainer(callback, maxAttempts = 50) {
let attempts = 0;
const interval = setInterval(() => {
const container = findButtonContainer();
if (container) {
clearInterval(interval);
callback(container);
} else if (++attempts >= maxAttempts) {
clearInterval(interval);
console.error('[FurryPlace SDK] Could not find button container after', maxAttempts, 'attempts');
}
}, 100);
}
// Create a button element
function createButton(config) {
const wrapper = document.createElement('div');
wrapper.setAttribute('data-furryplace-button', config.id);
wrapper.className = config.wrapperClass || '';
const button = document.createElement('button');
button.className = config.className || 'btn btn-square shadow-md';
button.title = config.title || '';
if (config.disabled) {
button.disabled = true;
}
// Add icon
if (config.icon) {
if (typeof config.icon === 'string') {
button.innerHTML = config.icon;
} else if (config.icon instanceof HTMLElement) {
button.appendChild(config.icon);
}
}
// Add click handler
if (config.onClick) {
button.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
const context = getContext();
config.onClick(context, e);
});
}
wrapper.appendChild(button);
return wrapper;
}
// Get context information for button callbacks
function getContext() {
return {
sdk: {
version: SDK_VERSION,
},
user: {
// This would need to be populated from the app's state
// For now, return null - plugins can access app state directly if needed
isLoggedIn: false,
},
map: {
// Map context if available
}
};
}
// Inject buttons into the DOM
function injectButtons(container) {
if (!container) return;
registeredButtons.forEach(config => {
// Check condition
if (config.condition) {
const context = getContext();
if (!config.condition(context)) {
return; // Skip this button
}
}
const button = createButton(config);
// Determine insertion position
switch (config.position) {
case 'top':
container.insertBefore(button, container.firstChild);
break;
case 'bottom':
container.appendChild(button);
break;
case 'before-leaderboard': {
// Find leaderboard button (has specific SVG viewBox)
const leaderboardBtn = Array.from(container.querySelectorAll('button')).find(btn => {
const svg = btn.querySelector('svg[viewBox="0 -960 960 960"]');
return svg && svg.querySelector('path[d*="160-200h160v-320"]');
});
if (leaderboardBtn && leaderboardBtn.parentElement) {
container.insertBefore(button, leaderboardBtn.parentElement);
} else {
container.appendChild(button);
}
break;
}
case 'after-leaderboard': {
const leaderboardBtn = Array.from(container.querySelectorAll('button')).find(btn => {
const svg = btn.querySelector('svg[viewBox="0 -960 960 960"]');
return svg && svg.querySelector('path[d*="160-200h160v-320"]');
});
if (leaderboardBtn && leaderboardBtn.parentElement && leaderboardBtn.parentElement.nextSibling) {
container.insertBefore(button, leaderboardBtn.parentElement.nextSibling);
} else {
container.appendChild(button);
}
break;
}
default:
container.appendChild(button);
}
});
injectionPoint = container;
}
// Re-inject buttons (useful when the app re-renders)
function reinjectButtons() {
if (!injectionPoint) return;
// Remove old injected buttons
const oldButtons = injectionPoint.querySelectorAll('[data-furryplace-button]');
oldButtons.forEach(btn => btn.remove());
// Re-inject
injectButtons(injectionPoint);
}
// Initialize the SDK
function initialize() {
if (isInitialized) {
console.warn('[FurryPlace SDK] Already initialized');
return;
}
console.log('[FurryPlace SDK] Initializing v' + SDK_VERSION);
waitForButtonContainer((container) => {
console.log('[FurryPlace SDK] Found button container, injecting buttons...');
injectButtons(container);
isInitialized = true;
// Set up mutation observer to re-inject on DOM changes
const observer = new MutationObserver(() => {
const currentContainer = findButtonContainer();
if (currentContainer && currentContainer !== injectionPoint) {
// Container changed, re-inject
injectButtons(currentContainer);
} else if (currentContainer && injectionPoint) {
// Check if our buttons are still there
const ourButtons = currentContainer.querySelectorAll('[data-furryplace-button]');
if (ourButtons.length !== registeredButtons.length) {
reinjectButtons();
}
}
});
observer.observe(document.body, {
childList: true,
subtree: true,
});
});
}
// Public API
window.FurryPlaceSDK = {
version: SDK_VERSION,
/**
* Register a custom button
* @param {Object} config - Button configuration
* @param {string} config.id - Unique identifier for the button
* @param {string} config.title - Tooltip text
* @param {string|HTMLElement} config.icon - SVG string or element
* @param {Function} config.onClick - Click handler (context, event) => {}
* @param {string} [config.position='bottom'] - Where to place the button
* @param {string} [config.className] - Custom CSS classes
* @param {string} [config.wrapperClass] - Wrapper div CSS classes
* @param {Function} [config.condition] - Optional condition (context) => boolean
* @param {boolean} [config.disabled=false] - Whether button is disabled
*/
registerButton(config) {
if (!config.id) {
console.error('[FurryPlace SDK] Button must have an id');
return;
}
if (registeredButtons.find(b => b.id === config.id)) {
console.error('[FurryPlace SDK] Button with id "' + config.id + '" already registered');
return;
}
registeredButtons.push(config);
console.log('[FurryPlace SDK] Registered button:', config.id);
// If already initialized, inject immediately
if (isInitialized && injectionPoint) {
const button = createButton(config);
injectionPoint.appendChild(button);
}
},
/**
* Unregister a button
* @param {string} id - Button ID to remove
*/
unregisterButton(id) {
const index = registeredButtons.findIndex(b => b.id === id);
if (index !== -1) {
registeredButtons.splice(index, 1);
// Remove from DOM
if (injectionPoint) {
const element = injectionPoint.querySelector('[data-furryplace-button="' + id + '"]');
if (element) {
element.remove();
}
}
console.log('[FurryPlace SDK] Unregistered button:', id);
}
},
/**
* Get list of registered buttons
*/
getButtons() {
return registeredButtons.map(b => ({ id: b.id, title: b.title }));
},
/**
* Manually trigger SDK initialization
*/
init() {
initialize();
},
/**
* Get current context
*/
getContext() {
return getContext();
}
};
// Auto-load plugins from server
function autoLoadPlugins() {
console.log('[FurryPlace SDK] Auto-discovering plugins...');
fetch('/api/plugins')
.then(response => response.json())
.then(data => {
if (!data.plugins || data.plugins.length === 0) {
console.log('[FurryPlace SDK] No plugins found');
return;
}
console.log('[FurryPlace SDK] Found ' + data.plugins.length + ' plugin(s):', data.plugins.map(p => p.name));
// Load each plugin dynamically
data.plugins.forEach(plugin => {
const script = document.createElement('script');
script.src = plugin.path;
script.async = true;
script.onerror = () => {
console.error('[FurryPlace SDK] Failed to load plugin:', plugin.name);
};
script.onload = () => {
console.log('[FurryPlace SDK] Loaded plugin:', plugin.name);
};
document.head.appendChild(script);
});
})
.catch(error => {
console.warn('[FurryPlace SDK] Failed to auto-discover plugins:', error.message);
});
}
// Auto-initialize when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
initialize();
autoLoadPlugins();
});
} else {
initialize();
autoLoadPlugins();
}
console.log('[FurryPlace SDK] Loaded v' + SDK_VERSION);
})();
+3
View File
@@ -108,6 +108,9 @@
href="./img/apple-touch-icon.png" href="./img/apple-touch-icon.png"
/> />
<link rel="manifest" href="./site.webmanifest" /> <link rel="manifest" href="./site.webmanifest" />
<!-- FurryPlace SDK for custom button extensions -->
<script src="/furryplace-sdk.js"></script>
</head> </head>
<body data-sveltekit-preload-data="hover"> <body data-sveltekit-preload-data="hover">
+3
View File
@@ -95,6 +95,9 @@
href="./img/apple-touch-icon.png" href="./img/apple-touch-icon.png"
/> />
<link rel="manifest" href="./site.webmanifest" /> <link rel="manifest" href="./site.webmanifest" />
<!-- FurryPlace SDK for custom button extensions -->
<script src="/furryplace-sdk.js"></script>
</head> </head>
<body data-sveltekit-preload-data="hover"> <body data-sveltekit-preload-data="hover">
+155
View File
@@ -0,0 +1,155 @@
# FurryPlace Plugins
This directory contains plugins that extend the FurryPlace UI using the FurryPlace SDK.
## Quick Start
1. **Create a plugin file** in this directory (e.g., `my-plugin.js`)
2. **Refresh the page** - Plugins are automatically discovered and loaded!
3. **Use the SDK** to register buttons or add other functionality
> **✨ Auto-Loading:** Plugins are automatically discovered from the `frontend-backup/plugins/` directory. Any `.js` file you add here will be automatically loaded when the page loads. No need to manually edit HTML files!
## Example Plugin
See [example-button.js](./example-button.js) for a complete example showing how to:
- Add custom buttons with icons
- Handle click events
- Use conditional rendering
- Apply custom styling
- Open external links
## FurryPlace SDK API
### `window.FurryPlaceSDK.registerButton(config)`
Register a custom button in the UI.
**Configuration Object:**
```javascript
{
id: string, // Required: Unique identifier
title: string, // Required: Tooltip text
icon: string|HTMLElement, // Required: SVG string or DOM element
onClick: Function, // Required: (context, event) => {}
position: string, // Optional: 'top', 'bottom', 'before-leaderboard', 'after-leaderboard'
className: string, // Optional: Custom CSS classes (default: 'btn btn-square shadow-md')
wrapperClass: string, // Optional: Wrapper div CSS classes
condition: Function, // Optional: (context) => boolean
disabled: boolean // Optional: Whether button is disabled
}
```
**Positions:**
- `'top'` - First button in the container
- `'bottom'` - Last button in the container
- `'before-leaderboard'` - Before the leaderboard button
- `'after-leaderboard'` - After the leaderboard button (default)
**Context Object** (passed to `onClick` and `condition`):
```javascript
{
sdk: {
version: string // SDK version
},
user: {
isLoggedIn: boolean // User login status (not yet implemented)
},
map: {} // Map context (not yet implemented)
}
```
### Other SDK Methods
- `FurryPlaceSDK.unregisterButton(id)` - Remove a registered button
- `FurryPlaceSDK.getButtons()` - Get list of all registered buttons
- `FurryPlaceSDK.getContext()` - Get current context object
- `FurryPlaceSDK.init()` - Manually initialize SDK (usually automatic)
## Example Usage
```javascript
(function() {
'use strict';
// Wait for SDK to load
function waitForSDK(callback) {
if (window.FurryPlaceSDK) {
callback();
} else {
setTimeout(() => waitForSDK(callback), 100);
}
}
waitForSDK(() => {
// Register a button
window.FurryPlaceSDK.registerButton({
id: 'my-custom-button',
title: 'My Button',
position: 'bottom',
icon: `
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="currentColor" class="size-5">
<path d="M480-80q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Z"/>
</svg>
`,
onClick: (context) => {
alert('Button clicked!');
console.log('Context:', context);
}
});
console.log('[My Plugin] Loaded!');
});
})();
```
## Finding SVG Icons
You can find Material Design icons at:
- https://fonts.google.com/icons
Copy the SVG code and use it as the `icon` parameter.
## Tips
1. **Wrap your plugin in an IIFE** to avoid polluting the global scope
2. **Wait for the SDK** to be available before registering buttons
3. **Use unique IDs** to avoid conflicts with other plugins
4. **Test button positions** to find the best placement for your use case
5. **Check the console** for SDK loading messages and errors
## How Auto-Loading Works
The SDK automatically:
1. Calls `/api/plugins` to get a list of all `.js` files in the `plugins/` directory
2. Dynamically loads each plugin by creating `<script>` tags
3. Reports loading progress in the browser console
You can check which plugins are loaded by looking at the console:
```
[FurryPlace SDK] Auto-discovering plugins...
[FurryPlace SDK] Found 2 plugin(s): ['example-button.js', 'my-plugin.js']
[FurryPlace SDK] Loaded plugin: example-button.js
[FurryPlace SDK] Loaded plugin: my-plugin.js
```
> **Note:** If you need to disable a plugin, either delete it or rename it to something other than `.js` (like `.js.disabled`)
## Troubleshooting
**Button not appearing?**
- Check the browser console for errors
- Verify the SDK is loaded: `console.log(window.FurryPlaceSDK)`
- Ensure your button ID is unique
- Try different positions
**Button appearing in wrong place?**
- Try different `position` values
- Check if the button container has changed in the UI
**Icon not showing?**
- Verify your SVG has `viewBox` and `fill="currentColor"`
- Add `class="size-5"` to the SVG element
- Check for syntax errors in the SVG string
+122
View File
@@ -0,0 +1,122 @@
/**
* Example FurryPlace Plugin - Custom Button
*
* This demonstrates how to use the FurryPlace SDK to add custom buttons
* to the UI. This plugin adds a "Help" button that opens a dialog.
*
* To use this plugin, add it to your HTML:
* <script src="/plugins/example-button.js"></script>
*/
(function() {
'use strict';
// Wait for SDK to be available
function waitForSDK(callback) {
if (window.FurryPlaceSDK) {
callback();
} else {
setTimeout(() => waitForSDK(callback), 100);
}
}
// Example 1: Simple button with SVG icon
function registerHelpButton() {
window.FurryPlaceSDK.registerButton({
id: 'example-help',
title: 'Help & Documentation',
position: 'after-leaderboard',
icon: `
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="currentColor" class="size-5">
<path d="M478-240q21 0 35.5-14.5T528-290q0-21-14.5-35.5T478-340q-21 0-35.5 14.5T428-290q0 21 14.5 35.5T478-240Zm-36-154h74q0-33 7.5-52t42.5-52q26-26 41-49.5t15-56.5q0-56-41-86t-97-30q-57 0-92.5 30T342-618l66 26q5-18 22.5-39t53.5-21q32 0 48 17.5t16 38.5q0 20-12 37.5T506-526q-44 39-54 59t-10 73Zm38 314q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-80q134 0 227-93t93-227q0-134-93-227t-227-93q-134 0-227 93t-93 227q0 134 93 227t227 93Zm0-320Z"/>
</svg>
`,
onClick: (context, event) => {
alert('Help button clicked!\n\nSDK Version: ' + context.sdk.version);
console.log('Context:', context);
}
});
}
// Example 2: Button with custom styling
function registerDebugButton() {
window.FurryPlaceSDK.registerButton({
id: 'example-debug',
title: 'Debug Info',
position: 'bottom',
className: 'btn btn-square btn-accent shadow-md', // Custom styling
icon: `
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="currentColor" class="size-5">
<path d="M440-280h80v-160h-80v160Zm40-240q17 0 28.5-11.5T520-560q0-17-11.5-28.5T480-600q-17 0-28.5 11.5T440-560q0 17 11.5 28.5T480-520Zm0 440q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-80q134 0 227-93t93-227q0-134-93-227t-227-93q-134 0-227 93t-93 227q0 134 93 227t227 93Zm0-320Z"/>
</svg>
`,
onClick: () => {
const info = {
buttons: window.FurryPlaceSDK.getButtons(),
userAgent: navigator.userAgent,
viewport: {
width: window.innerWidth,
height: window.innerHeight
},
timestamp: new Date().toISOString()
};
console.log('Debug Info:', info);
alert('Debug info logged to console');
}
});
}
// Example 3: Conditional button (only shows when user is logged in)
function registerConditionalButton() {
window.FurryPlaceSDK.registerButton({
id: 'example-conditional',
title: 'Premium Feature',
position: 'before-leaderboard',
icon: `
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="currentColor" class="size-5">
<path d="m233-80 65-281L80-550l288-25 112-265 112 265 288 25-218 189 65 281-247-149L233-80Z"/>
</svg>
`,
onClick: () => {
alert('This is a premium feature!');
},
// This condition is just an example - in a real plugin you'd check actual user state
condition: (context) => {
// For now, always show it (change this to check real user state)
return true;
}
});
}
// Example 4: Button that opens an external link
function registerDiscordButton() {
window.FurryPlaceSDK.registerButton({
id: 'example-discord',
title: 'Join our Discord',
position: 'top',
icon: `
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="size-5">
<path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515a.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0a12.64 12.64 0 0 0-.617-1.25a.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057a19.9 19.9 0 0 0 5.993 3.03a.078.078 0 0 0 .084-.028a14.09 14.09 0 0 0 1.226-1.994a.076.076 0 0 0-.041-.106a13.107 13.107 0 0 1-1.872-.892a.077.077 0 0 1-.008-.128a10.2 10.2 0 0 0 .372-.292a.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127a12.299 12.299 0 0 1-1.873.892a.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028a19.839 19.839 0 0 0 6.002-3.03a.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.956-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.955-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.946 2.418-2.157 2.418z"/>
</svg>
`,
onClick: () => {
window.open('https://discord.gg/ZRC4DnP9Z2', '_blank');
}
});
}
// Initialize plugin
waitForSDK(() => {
console.log('[Example Plugin] Initializing...');
// Register all example buttons
registerHelpButton();
registerDebugButton();
registerConditionalButton();
registerDiscordButton();
console.log('[Example Plugin] Loaded successfully!');
console.log('[Example Plugin] Registered buttons:', window.FurryPlaceSDK.getButtons());
});
})();
+80
View File
@@ -0,0 +1,80 @@
#!/usr/bin/env node
/**
* Script to inject the FurryPlace SDK into the frontend HTML files
*
* This modifies the HTML files to include the SDK script tag
*/
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const FRONTEND_DIR = path.join(__dirname, '..', 'frontend-backup');
const SDK_PATH = '/furryplace-sdk.js';
const HTML_FILES = ['index.html', 'admin.html', 'moderation.html'];
function injectSDK(htmlPath) {
console.log(`Processing ${htmlPath}...`);
let content = fs.readFileSync(htmlPath, 'utf8');
// Check if SDK is already injected
if (content.includes('furryplace-sdk.js')) {
console.log(` ✓ SDK already injected in ${path.basename(htmlPath)}`);
return;
}
// Find the closing </head> tag and inject before it
const headCloseTag = '</head>';
const headCloseIndex = content.indexOf(headCloseTag);
if (headCloseIndex === -1) {
console.error(` ✗ Could not find </head> tag in ${path.basename(htmlPath)}`);
return;
}
// Create the SDK script tag
const sdkScript = `\n\t\t<!-- FurryPlace SDK for custom button extensions -->\n\t\t<script src="${SDK_PATH}"></script>\n\t`;
// Inject the script
content = content.slice(0, headCloseIndex) + sdkScript + content.slice(headCloseIndex);
// Write back
fs.writeFileSync(htmlPath, content, 'utf8');
console.log(` ✓ SDK injected into ${path.basename(htmlPath)}`);
}
function main() {
console.log('FurryPlace SDK Injection Script\n');
// Check if SDK file exists
const sdkFilePath = path.join(FRONTEND_DIR, 'furryplace-sdk.js');
if (!fs.existsSync(sdkFilePath)) {
console.error(`Error: SDK file not found at ${sdkFilePath}`);
process.exit(1);
}
console.log(`SDK file found: ${sdkFilePath}\n`);
// Process each HTML file
HTML_FILES.forEach(file => {
const htmlPath = path.join(FRONTEND_DIR, file);
if (!fs.existsSync(htmlPath)) {
console.warn(`Warning: ${file} not found, skipping...`);
return;
}
injectSDK(htmlPath);
});
console.log('\n✓ SDK injection complete!');
console.log('\nYou can now create plugins by adding a script tag to your HTML:');
console.log(' <script src="/plugins/my-plugin.js"></script>');
}
main();
+2
View File
@@ -15,6 +15,7 @@ import pixel from "./routes/pixel.js";
import store from "./routes/store.js"; import store from "./routes/store.js";
import { setupSiteContentRoutes } from "./routes/site-content.js"; import { setupSiteContentRoutes } from "./routes/site-content.js";
import { setupRulesRoutes } from "./routes/rules.js"; import { setupRulesRoutes } from "./routes/rules.js";
import { setupPluginRoutes } from "./routes/plugins.js";
import { addPrismaToRequest } from "./config/database.js"; import { addPrismaToRequest } from "./config/database.js";
import fs from "fs/promises"; import fs from "fs/promises";
import path from "path"; import path from "path";
@@ -275,6 +276,7 @@ pixel(app);
store(app); store(app);
setupSiteContentRoutes(app); setupSiteContentRoutes(app);
setupRulesRoutes(app); setupRulesRoutes(app);
setupPluginRoutes(app);
const PORT = Number(process.env["PORT"]) || 3000; const PORT = Number(process.env["PORT"]) || 3000;
+38
View File
@@ -0,0 +1,38 @@
import type { App } from '@tinyhttp/app';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
export function setupPluginRoutes(app: App) {
// Public endpoint - Get list of available plugins
app.get('/api/plugins', async (_req, res) => {
try {
const pluginsDir = path.join(__dirname, '..', '..', 'frontend-backup', 'plugins');
// Check if plugins directory exists
if (!fs.existsSync(pluginsDir)) {
return res.json({ plugins: [] });
}
// Read all .js files in plugins directory
const files = fs.readdirSync(pluginsDir);
const plugins = files
.filter(file => file.endsWith('.js') && file !== 'README.md')
.map(file => ({
name: file,
path: `/plugins/${file}`,
size: fs.statSync(path.join(pluginsDir, file)).size,
modified: fs.statSync(path.join(pluginsDir, file)).mtime
}))
.sort((a, b) => a.name.localeCompare(b.name));
return res.json({ plugins });
} catch (error) {
console.error('Error listing plugins:', error);
return res.status(500).json({ error: 'Failed to list plugins', status: 500 });
}
});
}