0.0.1-canary.60 • Published 9 days ago

@whop-apps/storage v0.0.1-canary.60

Weekly downloads
-
License
ISC
Repository
-
Last release
9 days ago

Storage App Component

The storage app component is a simple tool that you may attach to your app to make it super easy to handle uploading and storing files for your app.

Most apps will want to allow their users to upload files attached to a particular resource.

For example:

  • An app that allows sellers to sell access to files on a product. (Example 1)
  • An app that allows customers to open tickets with a seller and share files in a chat. (Example 2)
  • An app that allows sellers to attach files to posts in a feed that is shown to customers. (Example 3)

In all of these cases (and many more) you will need to handle

  • Allowing only certain users to upload files.
  • Preventing unauthorized users from viewing files.
  • Controlling who can delete files.
  • Linking files to a particular resource in your database.
  • Deleting files if this resource is deleted.

The storage component handles all of this for you, and provides a simple API to interact with your files.

Storage Model

The storage component is built around the concept of a "bucket". A bucket is a collection of files that are all related to a particular resource in your app.

For each bucket you can control who can view, upload and delete files within the bucket. Access control for buckets is done through "access strings".

You can also specify the ID of the bucket, allowing you to link the bucket to a particular resource in your database. (For example, a product ID)

A bucket can contain many files. Each file has a unique ID, and a name. The name is the original name of the file when it was uploaded. You can move files between buckets (as long as you have permission to "delete" from the old bucket and permission to "upload" to the new one).

Each bucket may also have an optional expiry field which is a UTC Timestamp (in seconds) after which the bucket will be deleted. This is useful for temporary buckets that you only need for a short period of time. Or a bucket to allow people to upload files to a form prior to the resource the form is responsible for creating being created. Eg: a post creation form for a feed app.

Example 1 - Selling access to files

When a seller attaches your app to a product, create a bucket with the product id: prod_XXXX.

  • Set the view access to or-prod_XXXXX-authorized-biz_XXXX_
    • This will allow either admins or users that have purchased the product to view the files in the bucket.
  • Set the upload and delete access to authorized-biz_XXXXX. This should be the company id of the seller.
    • This will only allow the seller team to upload and delete files in the bucket.
  • Set the bill_to field to biz_XXXXX. This will allow you to easily bill the company for the cost associated with the storage used by the bucket.

Now provide the bucket id to your frontend. Since the bucket is just the product id, the frontend can easily fetch it from the URL.

The pre-build client components will allow only the sellers to upload files to the bucket. All authenticated/authorization checks are handled automatically.

When a customer purchases the product, you can fetch the list of files uploaded to the bucket. The function will return a list of URLs that you can display to the client. Again, all authentication/authorization checks ensure that only a valid customer of the product can view the files.

When your app is removed from the product, simply send a DELETE request to the bucket endpoint. This will delete all files in the bucket.

Example 2 - Services chat

When a ticket is opened, create a new bucket with the ticket id: ticket_XXXXX.

Since the ticket is between a customer and the seller(s), set the view and upload access to or-user_XXXXX-authorized-biz_XXXX_. This will allow either the user or authorized users of the business to view and upload files to the bucket.

Set the delete access to authorized-biz_XXXXX. This will allow only the seller team to delete files from the bucket. By default a user can always delete files they themselves have uploaded. This will allow the seller to delete files uploaded by the customer. But the customer will not be able to delete files uploaded by the seller.

You can again set the bill_to field to the current company id.

Since the bucket id is just the ticket id, you can easily fetch and upload files directly on the frontend. No backend / authentication / authorization required.

When a ticket is closed permanently, simply send a DELETE request to the bucket endpoint. This will delete all files in the bucket.

Example 3 - Feed posts

When clicking on the upload file button in the post form, send a request to your server to generate a temporary bucket. There is a helper function to do this for you. createTemporaryBucket(userId)

The temporary bucket will only allow the specified user to view, upload and delete files in the bucket. The bucket will also be deleted after 24 hours. Upon creating the temporary bucket, a random bucket id is returned. Send this to the frontend.

When the user uploads their files and submits the form, you can create the post resource. Use the post id to transform the temporary bucket into a permanent bucket. There is a helper function to do this for you. makeBucketPermanent(temporaryBucketId, postId, {view_access, upload_access, delete_access, bill_to})

In this call, set the view access to authenticated. This will allow any authenticated whop user to view the files in the bucket. Leave the upload and delete access as is. This will allow only the user to upload and delete files in the bucket. Set the bill_to field to the current company id.

Now you can fetch the list of files in the bucket and display them to the user. When the post is deleted, simply send a DELETE request to the bucket endpoint with the post id. This will delete all files in the bucket.

API Endpoints

The storage api is designed to be used from 2 environments:

  1. Server side. Here you can make requests from your backend server to manage files. Requests must be authenticated with your whop api key. The base URL is: https://storage.api.whop.com/api.
  2. Client side. Requests made are automatically authenticated using the user's whop_user_token. You must make these requests using relative URLs from your frontend client, using the automatic api proxy capability of the app proxy. The base URL is: /_whop/storage/api

A OpenAPI v3.1 schema is available at /api/schema that provides the exact required return types of the api.

Common Server Operations

NOTE: If using the TS SDK, you must import the ServerStorageApi from @whop-apps/storage/dist/api.server

  • POST /buckets => Create a bucket, provide a bucket id.
  • GET /buckets => List all your buckets, paginated.
  • GET /buckets/{bucket_id} => Get info about a bucket
  • DELETE /buckets/{bucket_id} => Delete a bucket
  • PATCH /buckets/{bucket_id} => Update properties of a bucket. You can even update the bucket ID if you wish.
  • GET /buckets/{bucket_id}/files => List files in a bucket, paginated. Useful for server side rendering.
  • GET /buckets/{bucket_id}/files/{file_id} => Fetch / Download the uploaded file.
  • DELETE /buckets/{bucket_id}/files/{file_id} => Delete the file.
  • GET /buckets/{bucket_id}/files/{file_id}/metadata => Get metadata associated with the file.
  • PUT /buckets/{bucket_id}/upload => Upload a file to the server.

In addition to the rest endpoints above, the Typescript SDK exposes two helper functions that wrap the above endpoints.

  • createTempBucket({ forUser: string }) => creates a temporary (24hr) bucket that only the specified user id can manage.
  • convertToPermanentBucket({...}) => converts a previously created temporary bucket into a permanent bucket by reassigning the id.

Common Client Operations

The client api uses the same endpoints, however, only a subset work using the user's authentication cookie.

  • GET /buckets/{bucket_id}/files => List files in a bucket, paginated. Only works if the user has view_access on the bucket.
  • GET /buckets/{bucket_id}/files/{file_id} => Fetch / Download the uploaded file. Only if the user has permissions
  • DELETE /buckets/{bucket_id}/files/{file_id} => Delete the file. Only if the user has delete_access permission.
  • GET /buckets/{bucket_id}/files/{file_id}/metadata => Get metadata associated with the file.
  • PUT /buckets/{bucket_id}/upload => Upload a file to the server. Only if the user has upload_access permission.

Client JS SDK

uploadFile(options)

The client side JS SDK exposes a wrapper around XMLHttpRequest that simplifies uploading files and listening to upload "progress" events.

It takes the following configuration:

  • bucketId => the id of the bucket to upload the file to.
  • fileId? => (optional) if you want to specify the file ID, you may do so here.
  • file => A Javascript File object for the data to actually upload.
  • onProgress => A callback of type: (percentage: number) => unknown that is called throughout the upload to indicate progress.

The function either returns the FileMetadata of the uploaded file if successful, or null if an error occurred.

React

This library exposes 2 react hooks to ease in uploading files.

useUploader

This hook is a react wrapper around the uploadFile function. It takes in 2 parameters, the bucketId and a onUpload callback that is called when a file is successfully uploaded.

The bucketId may either be a string, or an async callback that returns a string. This is to allow passing a server action, or api call that creates the bucket on the server, after checking necessary permissions (according to your business logic), and returns an ID.

The hook returns the following:

  • uploadFile => A function to upload a File object.
  • state => Either "idle", number, "done" or "error" depending on the internal state of the uploader.
  • reset => A function to reset the uploader from the done or error state back to idle
  • uploadOnFileInputChange => A callback you can directly pass to a html <input /> element to upload the selected file on change.
  • mostRecentlyUploaded => The metadata of the last file uploaded using the uploader
  • bucketId => The bucket id that was passed in. (This may be null, if a function was passed, as the function may not yet have been evaluated.)
  • resetBucketId => Reset the internal bucket id.

useBucket

This hook provides full management and list ability of files. You can easily render all the files in a bucket by just passing the ID, and also upload / delete files from the bucket.

It uses @tanstack/react-query under the hook, however to easily integrated with your existing react query provider, you must pass in the useQuery, and useQueryClient function from your installed version of react query.

You must also pass the bucketId.

It returns a list of FileMetadata object in the files parameter. Each of these have an attached async function that deletes the file.

It also returns the full return of the above useUploader() hook in the uploader parameter.


Types

For complete accuracy refer to the types defined in the OpenAPI spec.

Bucket

type Bucket = {
  id: string; // Must be unique to your app. Must match: `^[a-zA-Z0-9-_]+$`
  view_access: string;
  upload_access: string | undefined;
  delete_access: string | undefined;
  bill_to: string | undefined;
  expiry: number | undefined; // in seconds
  created_at: number; // in seconds
  uploader_cannot_delete: boolean;
};

If upload_access is undefined, and require_signed_upload is false, then only the app can upload to the bucket from the server (using the WHOP_API_KEY). Same for delete_access.

If uploader_cannot_delete is true, then the uploader of the file cannot delete the file. Only the app can delete the file.

FileMetadata

type FileMetadata = {
  url: string;
  file_id: string;
  app_id: string;
  bucket_id: string;
  uploaded_at: number;
  name?: string | null | undefined;
  size: number;
};
0.0.1-canary.60

9 days ago

0.0.1-canary.59

25 days ago

0.0.1-canary.58

25 days ago

0.0.1-canary.57

26 days ago

0.0.1-canary.56

26 days ago

0.0.1-canary.55

1 month ago

0.0.1-canary.54

1 month ago

0.0.1-canary.52

1 month ago

0.0.1-canary.51

1 month ago

0.0.1-canary.53

1 month ago

0.0.1-canary.50

2 months ago

0.0.1-canary.49

2 months ago

0.0.1-canary.48

2 months ago

0.0.1-canary.47

2 months ago

0.0.1-canary.46

2 months ago

0.0.1-canary.41

3 months ago

0.0.1-canary.45

3 months ago

0.0.1-canary.44

3 months ago

0.0.1-canary.43

3 months ago

0.0.1-canary.42

3 months ago

0.0.1-canary.40

3 months ago

0.0.1-canary.39

3 months ago

0.0.1-canary.38

4 months ago

0.0.1-canary.37

5 months ago

0.0.1-canary.34

5 months ago

0.0.1-canary.33

5 months ago

0.0.1-canary.36

5 months ago

0.0.1-canary.35

5 months ago

0.0.1-canary.32

5 months ago

0.0.1-canary.31

6 months ago

0.0.1-canary.30

6 months ago

0.0.1-canary.29

6 months ago

0.0.1-canary.28

6 months ago

0.0.1-canary.27

6 months ago

0.0.1-canary.26

7 months ago

0.0.1-canary.25

7 months ago

0.0.1-canary.24

7 months ago

0.0.1-canary.23

7 months ago

0.0.1-canary.22

7 months ago

0.0.1-canary.21

7 months ago