mirror of
https://github.com/hanzoai/sign.git
synced 2026-10-21 01:21:51 +00:00
Three unrelated defects, one image. The profile toast fired only after a session refresh. That refresh is a second read: it can stall, and when it answers unauthenticated it clears the session, which throws in useSession and takes down the subtree holding the toaster. A save that had already persisted could therefore go unconfirmed. The save is confirmed on the save now and the refresh follows it, so the confirmation depends only on the thing it reports. Document bytes are routed by NEXT_PUBLIC_UPLOAD_TRANSPORT. Three places read it with three slightly different rules, and the two that chose a store fell through to the database for any value they did not recognise -- so a transport named under a variable nothing reads stored every PDF inline while the configuration read as object storage, with nothing logged either way. One function resolves it now: unset means the database, and an unrecognised value throws rather than quietly picking a store. Reads still dispatch on each row's own type, so rows written under either transport keep working. entry.client and entry.server built different root trees -- StrictMode on the client only, and a second child beside the router. useId numbers a component by its position, and a parent's child count is part of that position, so every id below the root diverged and hydration failed on the first component to ask for one. Measured on the documents page: the header's menu trigger came back radix-:R26j5: from the server and radix-:R8qcl: from the client. The trees are identical now and all seven server-rendered ids survive hydration unchanged. Also: .gitignore still named the license artifact by its pre-rebrand filename, so the file the app actually writes was landing in git status untracked.
161 lines
4.4 KiB
TypeScript
161 lines
4.4 KiB
TypeScript
import {
|
|
DeleteObjectCommand,
|
|
GetObjectCommand,
|
|
PutObjectCommand,
|
|
S3Client,
|
|
} from '@aws-sdk/client-s3';
|
|
import slugify from '@sindresorhus/slugify';
|
|
import path from 'node:path';
|
|
|
|
import { env } from '@hanzo/esign-lib/utils/env';
|
|
|
|
import { ONE_HOUR, ONE_SECOND } from '../../constants/time';
|
|
import { alphaid } from '../id';
|
|
import { uploadTransport } from './transport';
|
|
|
|
export const getPresignPostUrl = async (fileName: string, contentType: string, userId?: number) => {
|
|
const client = getS3Client();
|
|
|
|
const { getSignedUrl } = await import('@aws-sdk/s3-request-presigner');
|
|
|
|
// Get the basename and extension for the file
|
|
const { name, ext } = path.parse(fileName);
|
|
|
|
let slugified = slugify(name);
|
|
|
|
// If the slugified name is empty or too long, generate a random string instead
|
|
//
|
|
// This is fine since we don't really need the filename in s3 since we store it
|
|
// in the database and can always get the original filename from there.
|
|
//
|
|
// The slugified name can be empty when a string contains only CJK or other
|
|
// special characters.
|
|
if (slugified.length === 0 || slugified.length > 100) {
|
|
slugified = alphaid(8);
|
|
}
|
|
|
|
let key = `${alphaid(12)}/${slugified}${ext}`;
|
|
|
|
if (userId) {
|
|
key = `${userId}/${key}`;
|
|
}
|
|
|
|
const putObjectCommand = new PutObjectCommand({
|
|
Bucket: env('NEXT_PRIVATE_UPLOAD_BUCKET'),
|
|
Key: key,
|
|
ContentType: contentType,
|
|
});
|
|
|
|
const url = await getSignedUrl(client, putObjectCommand, {
|
|
expiresIn: ONE_HOUR / ONE_SECOND,
|
|
});
|
|
|
|
return { key, url };
|
|
};
|
|
|
|
export const getAbsolutePresignPostUrl = async (key: string) => {
|
|
const client = getS3Client();
|
|
|
|
const { getSignedUrl: getS3SignedUrl } = await import('@aws-sdk/s3-request-presigner');
|
|
|
|
const putObjectCommand = new PutObjectCommand({
|
|
Bucket: env('NEXT_PRIVATE_UPLOAD_BUCKET'),
|
|
Key: key,
|
|
});
|
|
|
|
const url = await getS3SignedUrl(client, putObjectCommand, {
|
|
expiresIn: ONE_HOUR / ONE_SECOND,
|
|
});
|
|
|
|
return { key, url };
|
|
};
|
|
|
|
export const getPresignGetUrl = async (key: string) => {
|
|
if (env('NEXT_PRIVATE_UPLOAD_DISTRIBUTION_DOMAIN')) {
|
|
const distributionUrl = new URL(key, `${env('NEXT_PRIVATE_UPLOAD_DISTRIBUTION_DOMAIN')}`);
|
|
|
|
const { getSignedUrl: getCloudfrontSignedUrl } = await import('@aws-sdk/cloudfront-signer');
|
|
|
|
const url = getCloudfrontSignedUrl({
|
|
url: distributionUrl.toString(),
|
|
keyPairId: `${env('NEXT_PRIVATE_UPLOAD_DISTRIBUTION_KEY_ID')}`,
|
|
privateKey: `${env('NEXT_PRIVATE_UPLOAD_DISTRIBUTION_KEY_CONTENTS')}`,
|
|
dateLessThan: new Date(Date.now() + ONE_HOUR).toISOString(),
|
|
});
|
|
|
|
return { key, url };
|
|
}
|
|
|
|
const client = getS3Client();
|
|
|
|
const { getSignedUrl: getS3SignedUrl } = await import('@aws-sdk/s3-request-presigner');
|
|
|
|
const getObjectCommand = new GetObjectCommand({
|
|
Bucket: env('NEXT_PRIVATE_UPLOAD_BUCKET'),
|
|
Key: key,
|
|
});
|
|
|
|
const url = await getS3SignedUrl(client, getObjectCommand, {
|
|
expiresIn: ONE_HOUR / ONE_SECOND,
|
|
});
|
|
|
|
return { key, url };
|
|
};
|
|
|
|
/**
|
|
* Uploads a file to S3.
|
|
*/
|
|
export const uploadS3File = async (file: File) => {
|
|
const client = getS3Client();
|
|
|
|
// Get the basename and extension for the file
|
|
const { name, ext } = path.parse(file.name);
|
|
|
|
const key = `${alphaid(12)}/${slugify(name)}${ext}`;
|
|
|
|
const fileBuffer = await file.arrayBuffer();
|
|
|
|
const response = await client.send(
|
|
new PutObjectCommand({
|
|
Bucket: env('NEXT_PRIVATE_UPLOAD_BUCKET'),
|
|
Key: key,
|
|
Body: Buffer.from(fileBuffer),
|
|
ContentType: file.type,
|
|
}),
|
|
);
|
|
|
|
return { key, response };
|
|
};
|
|
|
|
export const deleteS3File = async (key: string) => {
|
|
const client = getS3Client();
|
|
|
|
await client.send(
|
|
new DeleteObjectCommand({
|
|
Bucket: env('NEXT_PRIVATE_UPLOAD_BUCKET'),
|
|
Key: key,
|
|
}),
|
|
);
|
|
};
|
|
|
|
const getS3Client = () => {
|
|
if (uploadTransport() !== 's3') {
|
|
throw new Error('Invalid upload transport');
|
|
}
|
|
|
|
const hasCredentials =
|
|
env('NEXT_PRIVATE_UPLOAD_ACCESS_KEY_ID') && env('NEXT_PRIVATE_UPLOAD_SECRET_ACCESS_KEY');
|
|
|
|
return new S3Client({
|
|
endpoint: env('NEXT_PRIVATE_UPLOAD_ENDPOINT') || undefined,
|
|
forcePathStyle: env('NEXT_PRIVATE_UPLOAD_FORCE_PATH_STYLE') === 'true',
|
|
region: env('NEXT_PRIVATE_UPLOAD_REGION') || 'us-east-1',
|
|
credentials: hasCredentials
|
|
? {
|
|
accessKeyId: String(env('NEXT_PRIVATE_UPLOAD_ACCESS_KEY_ID')),
|
|
secretAccessKey: String(env('NEXT_PRIVATE_UPLOAD_SECRET_ACCESS_KEY')),
|
|
}
|
|
: undefined,
|
|
});
|
|
};
|