eee0885811
- Remove recursive directory size calculations (single Seafile API call per list) - Remove 'Used in this location' usage display - Fix move using v2 per-type endpoints instead of broken batch endpoint - Send entry type from frontend for correct move routing Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
99 lines
3.3 KiB
JavaScript
99 lines
3.3 KiB
JavaScript
import crypto from 'crypto';
|
|
import { createClient } from '@supabase/supabase-js';
|
|
|
|
function json(res, status, body) {
|
|
res.status(status).setHeader('Content-Type', 'application/json');
|
|
res.setHeader('Cache-Control', 'no-store');
|
|
res.send(JSON.stringify(body));
|
|
}
|
|
|
|
async function requireTeam(authHeader) {
|
|
const supabaseUrl = process.env.VITE_SUPABASE_URL || process.env.SUPABASE_URL;
|
|
const supabaseAnonKey = process.env.VITE_SUPABASE_ANON_KEY || process.env.SUPABASE_ANON_KEY;
|
|
|
|
if (!supabaseUrl || !supabaseAnonKey) {
|
|
throw new Error('Supabase auth env is not configured on Vercel.');
|
|
}
|
|
|
|
const callerClient = createClient(supabaseUrl, supabaseAnonKey, {
|
|
auth: { persistSession: false, autoRefreshToken: false },
|
|
global: { headers: { Authorization: authHeader } },
|
|
});
|
|
|
|
const { data: userData, error: userError } = await callerClient.auth.getUser();
|
|
if (userError || !userData?.user) {
|
|
return { ok: false, status: 401, message: 'Unauthorized' };
|
|
}
|
|
|
|
const { data: profile, error: profileError } = await callerClient
|
|
.from('profiles')
|
|
.select('role')
|
|
.eq('id', userData.user.id)
|
|
.single();
|
|
|
|
if (profileError) {
|
|
return { ok: false, status: 500, message: profileError.message };
|
|
}
|
|
|
|
if (profile?.role !== 'team') {
|
|
return { ok: false, status: 403, message: 'Forbidden' };
|
|
}
|
|
|
|
return { ok: true };
|
|
}
|
|
|
|
function getKey() {
|
|
const secret = process.env.PASSWORD_VAULT_KEY || '';
|
|
if (!secret) throw new Error('PASSWORD_VAULT_KEY is not configured on Vercel.');
|
|
return Buffer.from(secret, 'base64');
|
|
}
|
|
|
|
export default async function handler(req, res) {
|
|
if (req.method !== 'POST') return json(res, 405, { error: 'Method not allowed' });
|
|
|
|
try {
|
|
const authHeader = req.headers.authorization || '';
|
|
if (!authHeader) return json(res, 401, { error: 'No authorization header' });
|
|
|
|
const auth = await requireTeam(authHeader);
|
|
if (!auth.ok) return json(res, auth.status, { error: auth.message });
|
|
|
|
const { action, plaintext, ciphertext, iv } = req.body || {};
|
|
const key = getKey();
|
|
|
|
if (action === 'encrypt') {
|
|
if (!plaintext) return json(res, 400, { error: 'plaintext required' });
|
|
|
|
const ivBuffer = crypto.randomBytes(12);
|
|
const cipher = crypto.createCipheriv('aes-256-gcm', key, ivBuffer);
|
|
const encrypted = Buffer.concat([cipher.update(String(plaintext), 'utf8'), cipher.final()]);
|
|
const tag = cipher.getAuthTag();
|
|
const packed = Buffer.concat([encrypted, tag]);
|
|
|
|
return json(res, 200, {
|
|
ciphertext: packed.toString('base64'),
|
|
iv: ivBuffer.toString('base64'),
|
|
});
|
|
}
|
|
|
|
if (action === 'decrypt') {
|
|
if (!ciphertext || !iv) return json(res, 400, { error: 'ciphertext and iv required' });
|
|
|
|
const raw = Buffer.from(String(ciphertext), 'base64');
|
|
const ivBuffer = Buffer.from(String(iv), 'base64');
|
|
const encrypted = raw.subarray(0, raw.length - 16);
|
|
const tag = raw.subarray(raw.length - 16);
|
|
|
|
const decipher = crypto.createDecipheriv('aes-256-gcm', key, ivBuffer);
|
|
decipher.setAuthTag(tag);
|
|
const decrypted = Buffer.concat([decipher.update(encrypted), decipher.final()]);
|
|
|
|
return json(res, 200, { plaintext: decrypted.toString('utf8') });
|
|
}
|
|
|
|
return json(res, 400, { error: 'Invalid action' });
|
|
} catch (error) {
|
|
return json(res, 500, { error: error.message || 'Unexpected server error' });
|
|
}
|
|
}
|