The server Frameworks and sign-in
Frameworks and sign-in
createDriveHandler takes a Request and gives a Response, so each framework needs a few lines: hand it the request as it came, before anything reads its body. Every example here is a file of examples/, served on a port and asked over HTTP by its tests: a folder made, a file of two parts uploaded and read back, and a change told as it happens.On this page
One drive for all of them
The bytes in a bucket when S3_BUCKET says one, else on the disk; the entries in SQLite. The same in each example.
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
One route handler for every route, in Node: it signs with 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
Mounted before express.json(): the drive reads its own bodies.
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
Hono’s request is the one the drive takes.
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
In a plugin of its own, where no parser of Fastify’s reads the body, and the answer written on the raw response.
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');
}Who asks, with 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;
}Pass it as identify. The page sends the session cookie with every request of its own origin, so nothing else is needed in the browser.
Who asks, with 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;
}A token of your own
For an application with its own sign-in, and for the desktop application, which sends `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;
}
}Tried, and not: the token of your own is checked by the tests, with a forged one refused. Auth.js and Clerk are written to their documentation and not run here, as they need accounts of theirs.
When something fails
The body is not JSON.
A parser of the framework read the body first: mount the drive before it, or out of its reach, as the examples do.
No GET /api/files/…
The path the framework gave is not the whole one. nodeHandler takes originalUrl when there is one (Express); otherwise mount it where base says.
Sign in first.
identify found no one: the cookie did not come (another origin needs cors and a token), or the session has no id.