feat: rebrand Hemmelig to paste.es for cloudhost.es
- Set Spanish as default language with ephemeral/encrypted privacy focus - Translate all user-facing strings and legal pages to Spanish - Replace Norwegian flag with Spanish flag in footer - Remove Hemmelig/terces.cloud links, add cloudhost.es sponsorship - Rewrite PrivacyPage: zero data collection, ephemeral design emphasis - Rewrite TermsPage: Spanish law, RGPD, paste.es/CloudHost.es references - Update PWA manifest, HTML meta tags, package.json branding - Rename webhook headers to X-Paste-Event / X-Paste-Signature - Update API docs title and contact to paste.es / cloudhost.es Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
91
api/middlewares/auth.ts
Normal file
91
api/middlewares/auth.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import { createHash } from 'crypto';
|
||||
import { createMiddleware } from 'hono/factory';
|
||||
import { auth } from '../auth';
|
||||
import prisma from '../lib/db';
|
||||
|
||||
type Env = {
|
||||
Variables: {
|
||||
user: typeof auth.$Infer.Session.user | null;
|
||||
session: typeof auth.$Infer.Session.session | null;
|
||||
};
|
||||
};
|
||||
|
||||
export const authMiddleware = createMiddleware<Env>(async (c, next) => {
|
||||
const user = c.get('user');
|
||||
if (!user) {
|
||||
return c.json({ error: 'Unauthorized' }, 401);
|
||||
}
|
||||
await next();
|
||||
});
|
||||
|
||||
export const checkAdmin = createMiddleware<Env>(async (c, next) => {
|
||||
const sessionUser = c.get('user');
|
||||
if (!sessionUser) {
|
||||
return c.json({ error: 'Forbidden' }, 403);
|
||||
}
|
||||
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: sessionUser.id },
|
||||
select: { role: true },
|
||||
});
|
||||
|
||||
if (!user || user.role !== 'admin') {
|
||||
return c.json({ error: 'Forbidden' }, 403);
|
||||
}
|
||||
await next();
|
||||
});
|
||||
|
||||
// Middleware that accepts either session auth OR API key auth
|
||||
export const apiKeyOrAuthMiddleware = createMiddleware<Env>(async (c, next) => {
|
||||
// First check if user is already authenticated via session
|
||||
const sessionUser = c.get('user');
|
||||
if (sessionUser) {
|
||||
return next();
|
||||
}
|
||||
|
||||
// Check for API key in Authorization header
|
||||
const authHeader = c.req.header('Authorization');
|
||||
if (!authHeader?.startsWith('Bearer ')) {
|
||||
return c.json({ error: 'Unauthorized' }, 401);
|
||||
}
|
||||
|
||||
const apiKey = authHeader.substring(7);
|
||||
if (!apiKey.startsWith('hemmelig_')) {
|
||||
return c.json({ error: 'Invalid API key format' }, 401);
|
||||
}
|
||||
|
||||
try {
|
||||
const keyHash = createHash('sha256').update(apiKey).digest('hex');
|
||||
|
||||
const apiKeyRecord = await prisma.apiKey.findUnique({
|
||||
where: { keyHash },
|
||||
include: { user: true },
|
||||
});
|
||||
|
||||
if (!apiKeyRecord) {
|
||||
return c.json({ error: 'Invalid API key' }, 401);
|
||||
}
|
||||
|
||||
// Check if key is expired
|
||||
if (apiKeyRecord.expiresAt && new Date() > apiKeyRecord.expiresAt) {
|
||||
return c.json({ error: 'API key has expired' }, 401);
|
||||
}
|
||||
|
||||
// Update last used timestamp (fire and forget)
|
||||
prisma.apiKey
|
||||
.update({
|
||||
where: { id: apiKeyRecord.id },
|
||||
data: { lastUsedAt: new Date() },
|
||||
})
|
||||
.catch(() => {});
|
||||
|
||||
// Set user from API key
|
||||
c.set('user', apiKeyRecord.user as typeof auth.$Infer.Session.user);
|
||||
c.set('session', null);
|
||||
|
||||
return next();
|
||||
} catch (error) {
|
||||
console.error('API key auth error:', error);
|
||||
return c.json({ error: 'Authentication failed' }, 401);
|
||||
}
|
||||
});
|
||||
33
api/middlewares/ip-restriction.ts
Normal file
33
api/middlewares/ip-restriction.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { Context, Next } from 'hono';
|
||||
import ipRangeCheck from 'ip-range-check';
|
||||
import prisma from '../lib/db';
|
||||
import { getClientIp } from '../lib/utils';
|
||||
|
||||
export const ipRestriction = async (c: Context, next: Next) => {
|
||||
const { id } = c.req.param();
|
||||
|
||||
const item = await prisma.secrets.findUnique({
|
||||
where: { id },
|
||||
select: {
|
||||
ipRange: true,
|
||||
},
|
||||
});
|
||||
|
||||
// If no restriction is configured, move on
|
||||
if (!item?.ipRange) {
|
||||
return next();
|
||||
}
|
||||
|
||||
const ip = getClientIp(c);
|
||||
|
||||
if (!ip) {
|
||||
return c.json({ error: 'Could not identify client IP' }, 400);
|
||||
}
|
||||
|
||||
// The core logic is now a single, clean line
|
||||
if (!ipRangeCheck(ip, item.ipRange)) {
|
||||
return c.json({ error: 'Access restricted by IP' }, 403);
|
||||
}
|
||||
|
||||
await next();
|
||||
};
|
||||
32
api/middlewares/ratelimit.ts
Normal file
32
api/middlewares/ratelimit.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { Context, Next } from 'hono';
|
||||
import { rateLimiter } from 'hono-rate-limiter';
|
||||
import settingsCache from '../lib/settings';
|
||||
import { getClientIp } from '../lib/utils';
|
||||
|
||||
let rateLimitInstance: ReturnType<typeof rateLimiter> | null = null;
|
||||
|
||||
const ratelimit = async (c: Context, next: Next) => {
|
||||
const instanceSettings = settingsCache.get('instanceSettings');
|
||||
|
||||
if (instanceSettings?.enableRateLimiting) {
|
||||
if (rateLimitInstance === null) {
|
||||
rateLimitInstance = rateLimiter({
|
||||
windowMs: instanceSettings.rateLimitWindow * 1000, // Convert seconds to milliseconds
|
||||
limit: instanceSettings.rateLimitRequests,
|
||||
standardHeaders: true,
|
||||
keyGenerator: (c) => getClientIp(c) || 'anonymous',
|
||||
});
|
||||
}
|
||||
|
||||
return rateLimitInstance(c, next);
|
||||
}
|
||||
|
||||
// If rate limiting is disabled, ensure the limiter is cleared
|
||||
if (rateLimitInstance !== null) {
|
||||
rateLimitInstance = null;
|
||||
}
|
||||
|
||||
await next();
|
||||
};
|
||||
|
||||
export default ratelimit;
|
||||
Reference in New Issue
Block a user