Files
fourge-portal/api/fourge-passwords-crypto.js
Krao Hasanee eee0885811 Fix file sharing load speed and move error; misc updates
- 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>
2026-05-13 14:20:38 -04:00

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' });
}
}