Skip to content

Latest commit

 

History

History
301 lines (248 loc) · 10.3 KB

File metadata and controls

301 lines (248 loc) · 10.3 KB

R2 File Storage Guide

better-auth-cloudflare provides seamless file upload, tracking, and management with Cloudflare R2 object storage. Files are automatically tracked in your database with rich metadata and custom fields.

Setup

Server Configuration

import { withCloudflare } from "better-auth-cloudflare";

export const auth = betterAuth(
    withCloudflare(
        {
            r2: {
                bucket: getCloudflareContext().env.R2_BUCKET,
                maxFileSize: 2 * 1024 * 1024, // 2MB
                allowedTypes: [".jpg", ".jpeg", ".png", ".gif"],
                additionalFields: {
                    category: { type: "string", required: false },
                    isPublic: { type: "boolean", required: false },
                    description: { type: "string", required: false },
                },
                hooks: {
                    upload: {
                        before: async (file, ctx) => {
                            // Only allow authenticated users to upload files
                            if (ctx.session === null) {
                                return null; // Blocks upload
                            }

                            // Only allow paid users to upload files (for example)
                            const isPaidUser = (userId: string) => true; // example
                            if (isPaidUser(ctx.session.user.id) === false) {
                                return null; // Blocks upload
                            }

                            // Allow upload
                        },
                        after: async (file, ctx) => {
                            // Track your analytics (for example)
                            console.log("File uploaded:", file);
                        },
                    },
                    download: {
                        before: async (file, ctx) => {
                            // Only allow user to access their own files (by default all files are public)
                            if (file.isPublic === false && file.userId !== ctx.session?.user.id) {
                                return null; // Blocks download
                            }
                            // Allow download
                        },
                    },
                },
            },
            // ... other config
        },
        {
            // ... your auth config
        }
    )
);

Client Configuration

import { createAuthClient } from "better-auth/client";
import { cloudflareClient } from "better-auth-cloudflare/client";

const authClient = createAuthClient({
    baseURL: "/api/auth", // Adjust if your auth routes are elsewhere
    plugins: [cloudflareClient()], // Enables uploadFile method and files endpoints
});

export default authClient;

Note: The cloudflareClient() plugin adds the uploadFile convenience method as well as inferred API client methods for file management (files.list, files.download, files.delete, files.get).

Adding Custom Fields

Track additional metadata with type-safe custom fields:

const r2Config = {
    bucket: env.R2_BUCKET,
    additionalFields: {
        category: { type: "string", required: false },
        isPublic: { type: "boolean", required: false },
        description: { type: "string", required: false },

        // Additional examples...
        priority: { type: "number", required: false },
        tags: { type: "string[]", required: false },
    },
} satisfies R2Config;

Upload Files

Client-side Upload

Use uploadFile method. Filename and content type are automatically inferred from the File object:

import authClient from "@/lib/authClient";

// Upload with custom fields
const file = fileInput.files[0];
const category = "documents";
const description = "Important contract document";
const isPublic = false;

const result = await authClient.uploadFile(file, {
    isPublic,
    ...(category.trim() && { category: category.trim() }),
    ...(description.trim() && { description: description.trim() }),
});

if (result.error) {
    console.error("Upload failed:", result.error.message || "Failed to upload file. Please try again.");
} else {
    console.log("File uploaded:", result.data);
    // Clear form or refresh file list as needed
}

Using Lifecycle Hooks

Hooks let you add business logic at key points in the file lifecycle. Only define the hooks you need - much cleaner than individual callbacks:

const r2Config = {
    bucket: env.R2_BUCKET,
    additionalFields: {
        category: { type: "string", required: false },
        isPublic: { type: "boolean", required: false },
        description: { type: "string", required: false },
    },

    hooks: {
        // Upload lifecycle
        upload: {
            before: async (file, ctx) => {
                // Only allow authenticated users to upload files
                if (ctx.session === null) {
                    return null; // Blocks upload
                }

                // Only allow paid users to upload files (for example)
                const isPaidUser = (userId: string) => true; // example
                if (isPaidUser(ctx.session.user.id) === false) {
                    return null; // Blocks upload
                }

                // Allow upload
            },
            after: async (file, ctx) => {
                // Track your analytics (for example)
                console.log("File uploaded:", file);
            },
        },

        // Download lifecycle
        download: {
            before: async (file, ctx) => {
                // Only allow user to access their own files (by default all files are public)
                if (file.isPublic === false && file.userId !== ctx.session?.user.id) {
                    return null; // Blocks download
                }
                // Allow download
            },
        },

        // Delete lifecycle
        delete: {
            before: async (file, ctx) => {
                // Remove from search index
                await removeFromSearchIndex(file.id);
            },
        },
    },
};

Hooks:

  • upload - Before and after upload hooks
  • download - Before and after download hooks
  • delete - Before and after delete hooks

File Management

List User Files

// Get files for current user
const result = await authClient.files.list();
if (result.data) {
    const { files, nextCursor, hasMore } = result.data;
    console.log("User files:", files);

    // Load next page if available
    if (hasMore && nextCursor) {
        const nextPage = await authClient.files.list({
            limit: 20,
            cursor: nextCursor,
        });
        console.log("Next page:", nextPage.data?.files);
    }
}

Download Files

const downloadFile = async (fileId: string, filename: string) => {
    try {
        const result = await authClient.files.download({ fileId });

        if (result.error) {
            console.error("Download failed:", result.error);
            return;
        }

        // Extract blob from Better Auth response structure
        const response = result.data;
        const blob = response instanceof Response ? await response.blob() : response;

        // Create and trigger download
        const url = window.URL.createObjectURL(blob);
        const a = document.createElement("a");
        a.href = url;
        a.download = filename;
        a.style.display = "none";
        document.body.appendChild(a);
        a.click();

        // Cleanup
        setTimeout(() => {
            window.URL.revokeObjectURL(url);
            document.body.removeChild(a);
        }, 100);
    } catch (error) {
        console.error("Failed to download file:", error);
    }
};

Delete Files

const deleteFile = async (fileId: string) => {
    try {
        const result = await authClient.files.delete({ fileId });
        if (!result.error) {
            console.log("File deleted successfully");
            // Refresh your file list here if needed
        } else {
            console.error("Delete failed:", result.error);
        }
    } catch (error) {
        console.error("Failed to delete file:", error);
    }
};

Get File Metadata

// Get file metadata by ID
const result = await authClient.files.get({ fileId });
if (result.data) {
    console.log("File metadata:", result.data);
}

Client Methods

Use these type-safe client methods:

Client Method Description
authClient.uploadFile(file, metadata) Upload files with custom fields
authClient.files.list({ limit?, cursor? }) List user's files with pagination
authClient.files.download({ fileId }) Download file
authClient.files.delete({ fileId }) Delete file
authClient.files.get({ fileId }) Get file metadata

API Endpoints (Reference)

These are the underlying endpoints (use client methods above instead):

Endpoint Method Description Parameters
/files/upload-raw POST Upload file Body: File (as binary), Headers: x-filename, x-file-metadata (JSON)
/files/list GET List user's files Query: limit?: number, cursor?: string
Response: { files: File[], nextCursor: string|null, hasMore: boolean }
/files/download POST Download file Body: { fileId: string }
Response: File as binary data
/files/delete POST Delete file Body: { fileId: string }
/files/get POST Get file metadata Body: { fileId: string }
Response: { data: FileMetadata }