feat: add reminders page, BMad skills upgrade, MCP server refactor
- Add reminders page with navigation support - Upgrade BMad builder module to skills-based architecture - Refactor MCP server: extract tools and auth into separate modules - Add connections cache, custom AI provider support - Update prisma schema and generated client - Various UI/UX improvements and i18n updates - Add service worker for PWA support Made-with: Cursor
This commit is contained in:
219
mcp-server/auth.js
Normal file
219
mcp-server/auth.js
Normal file
@@ -0,0 +1,219 @@
|
||||
/**
|
||||
* Memento MCP Server - API Key Management
|
||||
*
|
||||
* Stores API keys in the SystemConfig table.
|
||||
* Each key is hashed with SHA-256. The raw key is only shown once at creation.
|
||||
*
|
||||
* SystemConfig entries:
|
||||
* key: "mcp_key_{shortId}"
|
||||
* value: JSON { shortId, name, userId, userName, keyHash, createdAt, lastUsedAt, active }
|
||||
*/
|
||||
|
||||
import { createHash, randomBytes } from 'crypto';
|
||||
|
||||
const KEY_PREFIX = 'mcp_key_';
|
||||
|
||||
/**
|
||||
* Generate a new API key.
|
||||
* @param {import('@prisma/client').PrismaClient} prisma
|
||||
* @param {object} opts
|
||||
* @param {string} opts.name - Human-readable name for this key
|
||||
* @param {string} opts.userId - User ID to link this key to
|
||||
* @returns {{ rawKey: string, info: object }} The raw key (show once!) and key info
|
||||
*/
|
||||
export async function generateApiKey(prisma, { name, userId }) {
|
||||
// Validate user exists
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
select: { id: true, name: true, email: true, role: true },
|
||||
});
|
||||
if (!user) throw new Error(`User not found: ${userId}`);
|
||||
|
||||
// Generate raw key: mcp_sk_{32 random hex chars}
|
||||
const rawBytes = randomBytes(24);
|
||||
const shortId = rawBytes.toString('hex').substring(0, 8);
|
||||
const rawKey = `mcp_sk_${rawBytes.toString('hex')}`;
|
||||
|
||||
// Hash for storage
|
||||
const keyHash = hashKey(rawKey);
|
||||
|
||||
const keyInfo = {
|
||||
shortId,
|
||||
name: name || `Key for ${user.name}`,
|
||||
userId: user.id,
|
||||
userName: user.name,
|
||||
userEmail: user.email,
|
||||
keyHash,
|
||||
createdAt: new Date().toISOString(),
|
||||
lastUsedAt: null,
|
||||
active: true,
|
||||
};
|
||||
|
||||
await prisma.systemConfig.create({
|
||||
data: {
|
||||
key: `${KEY_PREFIX}${shortId}`,
|
||||
value: JSON.stringify(keyInfo),
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
rawKey, // Only returned once!
|
||||
info: {
|
||||
shortId,
|
||||
name: keyInfo.name,
|
||||
userId: keyInfo.userId,
|
||||
userName: keyInfo.userName,
|
||||
createdAt: keyInfo.createdAt,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate an API key and return the associated user info.
|
||||
* @param {import('@prisma/client').PrismaClient} prisma
|
||||
* @param {string} rawKey - The raw API key from the request header
|
||||
* @returns {object|null} User info if valid, null if invalid/inactive
|
||||
*/
|
||||
export async function validateApiKey(prisma, rawKey) {
|
||||
if (!rawKey || !rawKey.startsWith('mcp_sk_')) return null;
|
||||
|
||||
const keyHash = hashKey(rawKey);
|
||||
|
||||
// Find matching key
|
||||
const allKeys = await prisma.systemConfig.findMany({
|
||||
where: { key: { startsWith: KEY_PREFIX } },
|
||||
});
|
||||
|
||||
for (const entry of allKeys) {
|
||||
try {
|
||||
const info = JSON.parse(entry.value);
|
||||
if (info.keyHash === keyHash && info.active) {
|
||||
// Update lastUsedAt
|
||||
info.lastUsedAt = new Date().toISOString();
|
||||
await prisma.systemConfig.update({
|
||||
where: { key: entry.key },
|
||||
data: { value: JSON.stringify(info) },
|
||||
});
|
||||
|
||||
// Return user context
|
||||
return {
|
||||
apiKeyId: info.shortId,
|
||||
apiKeyName: info.name,
|
||||
userId: info.userId,
|
||||
userName: info.userName,
|
||||
};
|
||||
}
|
||||
} catch {
|
||||
// Invalid JSON, skip
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* List all API keys (without revealing hashes).
|
||||
* @param {import('@prisma/client').PrismaClient} prisma
|
||||
* @param {object} [opts]
|
||||
* @param {string} [opts.userId] - Filter by user
|
||||
* @returns {array}
|
||||
*/
|
||||
export async function listApiKeys(prisma, { userId } = {}) {
|
||||
const allKeys = await prisma.systemConfig.findMany({
|
||||
where: { key: { startsWith: KEY_PREFIX } },
|
||||
});
|
||||
|
||||
const keys = [];
|
||||
for (const entry of allKeys) {
|
||||
try {
|
||||
const info = JSON.parse(entry.value);
|
||||
if (userId && info.userId !== userId) continue;
|
||||
keys.push({
|
||||
shortId: info.shortId,
|
||||
name: info.name,
|
||||
userId: info.userId,
|
||||
userName: info.userName,
|
||||
active: info.active,
|
||||
createdAt: info.createdAt,
|
||||
lastUsedAt: info.lastUsedAt,
|
||||
});
|
||||
} catch {
|
||||
// skip
|
||||
}
|
||||
}
|
||||
|
||||
return keys;
|
||||
}
|
||||
|
||||
/**
|
||||
* Revoke an API key by shortId.
|
||||
* @param {import('@prisma/client').PrismaClient} prisma
|
||||
* @param {string} shortId - The key's short identifier
|
||||
* @returns {boolean} Whether the key was found and deactivated
|
||||
*/
|
||||
export async function revokeApiKey(prisma, shortId) {
|
||||
const configKey = `${KEY_PREFIX}${shortId}`;
|
||||
const entry = await prisma.systemConfig.findUnique({ where: { key: configKey } });
|
||||
|
||||
if (!entry) return false;
|
||||
|
||||
const info = JSON.parse(entry.value);
|
||||
if (!info.active) return false;
|
||||
|
||||
info.active = false;
|
||||
info.revokedAt = new Date().toISOString();
|
||||
|
||||
await prisma.systemConfig.update({
|
||||
where: { key: configKey },
|
||||
data: { value: JSON.stringify(info) },
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an API key permanently from DB.
|
||||
* @param {import('@prisma/client').PrismaClient} prisma
|
||||
* @param {string} shortId
|
||||
*/
|
||||
export async function deleteApiKey(prisma, shortId) {
|
||||
const configKey = `${KEY_PREFIX}${shortId}`;
|
||||
try {
|
||||
await prisma.systemConfig.delete({ where: { key: configKey } });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a user by email or ID for auth purposes.
|
||||
* @param {import('@prisma/client').PrismaClient} prisma
|
||||
* @param {string} identifier - Email or user ID
|
||||
* @returns {object|null}
|
||||
*/
|
||||
export async function resolveUser(prisma, identifier) {
|
||||
if (!identifier) return null;
|
||||
|
||||
// Try by ID first
|
||||
let user = await prisma.user.findUnique({
|
||||
where: { id: identifier },
|
||||
select: { id: true, name: true, email: true, role: true },
|
||||
});
|
||||
|
||||
// Try by email
|
||||
if (!user) {
|
||||
user = await prisma.user.findUnique({
|
||||
where: { email: identifier },
|
||||
select: { id: true, name: true, email: true, role: true },
|
||||
});
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
// ── Internal ──────────────────────────────────────────────────────────────────
|
||||
|
||||
function hashKey(rawKey) {
|
||||
return createHash('sha256').update(rawKey).digest('hex');
|
||||
}
|
||||
Reference in New Issue
Block a user