El servidor Frameworks e inicio de sesión
Frameworks e inicio de sesión
createDriveHandler recibe un Request y da un Response, así que cada framework necesita unas pocas líneas: pasarle la petición tal como llegó, antes de que nada lea su cuerpo. Cada ejemplo aquí es un archivo de examples/, servido en un puerto y consultado por HTTP en sus tests: una carpeta creada, un archivo de dos partes subido y leído de vuelta, y un cambio avisado en el momento.En esta página
Un drive para todos
Los bytes en un bucket cuando S3_BUCKET lo indica, si no en el disco; las entradas en SQLite. Igual en cada ejemplo.
import { mkdirSync } from 'node:fs';
import { type DriveServerOptions, byRoles, createDriveHandler, diskStorage, s3OptionsFrom, s3Storage, sqliteMetadata } from '@rebofiles/server';
/** Long and random, the same in every process: `openssl rand -hex 32`. */
const secret = process.env.REBOFILES_SECRET ?? (process.env.NODE_ENV === 'production' ? undefined : 'a secret for this computer only');
if (!secret) throw new Error('Set REBOFILES_SECRET: long, random, the same in every process.');
export async function createDrive(identify: DriveServerOptions['identify'], extra: Partial<DriveServerOptions> = {}) {
const folder = process.env.REBOFILES_FOLDER ?? './storage';
mkdirSync(folder, { recursive: true });
return createDriveHandler({
storage: process.env.S3_BUCKET ? s3Storage(s3OptionsFrom()) : diskStorage({ folder: `${folder}/files`, secret: secret! }),
metadata: await sqliteMetadata(`${folder}/drive.sqlite`),
identify,
authorize: byRoles,
secret: secret!,
base: '/api/files',
...extra,
});
}Next.js
Un route handler para todas las rutas, en Node: firma con node:crypto.
import { createDrive } from '../../../../../drive';
import { identify } from '../../../../../auth/dev';
export const runtime = 'nodejs';
// Never cached: every answer is of the one asking, and the events go on as long as the page watches.
export const dynamic = 'force-dynamic';
const handle = await createDrive(identify);
export { handle as GET, handle as POST, handle as PUT, handle as PATCH, handle as DELETE, handle as OPTIONS };Express
Montado antes de express.json(): el drive lee sus propios cuerpos.
import express from 'express';
import { nodeHandler } from '@rebofiles/server';
import { identify } from '../auth/dev';
import { createDrive } from '../drive';
export async function createApp() {
const app = express();
app.use('/api/files', nodeHandler(await createDrive(identify)));
// The rest of your application, its parsers among it, after.
app.use(express.json());
return app;
}
if (import.meta.url === `file://${process.argv[1]}`) {
const app = await createApp();
app.listen(3000, () => console.log('The drive is at http://localhost:3000/api/files'));
}Hono
La petición de Hono es la que recibe el drive.
import { serve } from '@hono/node-server';
import { Hono } from 'hono';
import { identify } from '../auth/dev';
import { createDrive } from '../drive';
export async function createApp() {
const handle = await createDrive(identify);
const app = new Hono();
app.all('/api/files/*', (c) => handle(c.req.raw));
return app;
}
if (import.meta.url === `file://${process.argv[1]}`) {
serve({ fetch: (await createApp()).fetch, port: 3000 }, () => console.log('The drive is at http://localhost:3000/api/files'));
}Fastify
En un plugin propio, donde ningún parser de Fastify lee el cuerpo, y la respuesta escrita en la respuesta cruda.
import Fastify from 'fastify';
import { nodeHandler } from '@rebofiles/server';
import { identify } from '../auth/dev';
import { createDrive } from '../drive';
export async function createApp() {
const drive = nodeHandler(await createDrive(identify));
const app = Fastify();
await app.register(async (scope) => {
// Every body left as it came, in this plugin only: the drive reads it.
scope.removeAllContentTypeParsers();
scope.addContentTypeParser('*', (_request, _payload, done) => done(null));
scope.all('/api/files/*', (request, reply) => {
reply.hijack();
return drive(request.raw, reply.raw);
});
scope.all('/api/files', (request, reply) => {
reply.hijack();
return drive(request.raw, reply.raw);
});
});
return app;
}
if (import.meta.url === `file://${process.argv[1]}`) {
const app = await createApp();
await app.listen({ port: 3000 });
console.log('The drive is at http://localhost:3000/api/files');
}Quién pregunta, con Auth.js
import type { User } from '@rebofiles/server';
import { auth } from '@/auth';
export async function identify(): Promise<User | null> {
const session = await auth();
const id = session?.user?.id;
return id ? { id, admin: session.user?.email === process.env.DRIVE_ADMIN } : null;
}Pásalo como identify. La página envía la cookie de sesión con cada petición a su propio origen, así que en el navegador no hace falta nada más.
Quién pregunta, con Clerk
import { createClerkClient } from '@clerk/backend';
import type { User } from '@rebofiles/server';
const clerk = createClerkClient({ secretKey: process.env.CLERK_SECRET_KEY, publishableKey: process.env.CLERK_PUBLISHABLE_KEY });
export async function identify(request: Request): Promise<User | null> {
const state = await clerk.authenticateRequest(request, { authorizedParties: [process.env.APP_ORIGIN ?? 'http://localhost:3000'] });
const { userId, orgRole } = state.toAuth() ?? {};
return userId ? { id: userId, admin: orgRole === 'org:admin' } : null;
}Un token propio
Para una aplicación con su propio inicio de sesión, y para la aplicación de escritorio, que envía `Authorization: Bearer`.
import type { User } from '@rebofiles/server';
import { jwtVerify } from 'jose';
const key = new TextEncoder().encode(process.env.SESSION_SECRET);
export async function identify(request: Request): Promise<User | null> {
const bearer = /^Bearer (.+)$/.exec(request.headers.get('authorization') ?? '')?.[1];
const cookie = /(?:^|;\s*)session=([^;]+)/.exec(request.headers.get('cookie') ?? '')?.[1];
const token = bearer ?? cookie;
if (!token) return null;
try {
const { payload } = await jwtVerify(token, key, { algorithms: ['HS256'] });
return payload.sub ? { id: payload.sub, admin: payload.role === 'admin' } : null;
} catch {
return null;
}
}Probado, y no: el token propio lo comprueban los tests, y uno falsificado se rechaza. Auth.js y Clerk están escritos según su documentación y no se ejecutan aquí, porque necesitan cuentas suyas.
Cuando algo falla
The body is not JSON.
Un parser del framework leyó el cuerpo primero: monta el drive antes, o fuera de su alcance, como hacen los ejemplos.
No GET /api/files/…
La ruta que dio el framework no es la completa. nodeHandler toma originalUrl cuando lo hay (Express); si no, móntalo donde dice base.
Sign in first.
identify no encontró a nadie: no llegó la cookie (otro origen necesita cors y un token), o la sesión no tiene id.