2e688c8e52
The application source was untracked, so this commit brings it under version control together with fixes for the issues found while auditing it. Notable fixes: Authorization - Require a session and scope to senderEmail on /api/transfers/[id] (GET, DELETE) and .../resend. These were unauthenticated over an autoincrement id, so the ids could be walked to soft-delete any transfer, read sender/recipient metadata, or make the app mail arbitrary recipients. - Require a session on the legacy /api/send and /api/chunk-upload endpoints, and take the sender from the session rather than a request field so transfers cannot be posted as another user. Encryption - Replace the chunk encryption scheme. Every chunk was encrypted under one shared session IV with its auth tag discarded, which reuses the AES-GCM keystream (XORing two ciphertexts recovers plaintext without the key) and left the stored file undecryptable, surfacing to users as a wrong-password error. Chunks are now self-contained frames carrying their own random IV and auth tag, behind a magic+salt header. - Files written by the previous format now report UNSUPPORTED_FORMAT instead of a misleading password error. Download - Verify the password against the stored bcrypt hash before serving a file, and enforce expiresAt and DELETED/EXPIRED status. - Move the password from the query string into a POST body so it stays out of access logs and Referer headers. - Record a download only after successful authentication. - Decrypt frame by frame through a stream instead of buffering the whole file, and encode the Content-Disposition filename per RFC 5987. Data exposure - /api/download ran before the password prompt and returned the full transfer row, including absolute server file paths. It now returns only what the pre-password screen renders; filenames, message and recipient are withheld until /api/verify succeeds. Correctness - Fix BigInt handling that made /api/transfers and /api/transfers/[id] fail unconditionally (JSON.stringify cannot serialize BigInt, and seeding a BigInt reduce with 0 throws). - Fail loudly on a missing chunk during reassembly rather than silently writing a corrupt file. - Meter plan usage in plaintext bytes rather than on-disk encrypted size. Ignore /uploads: it holds runtime transfer payloads, not source. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
111 lines
3.0 KiB
Plaintext
111 lines
3.0 KiB
Plaintext
// This is your Prisma schema file,
|
|
// learn more about it in the docs: https://pris.ly/d/prisma-schema
|
|
|
|
// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?
|
|
// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init
|
|
|
|
generator client {
|
|
provider = "prisma-client-js"
|
|
}
|
|
|
|
datasource db {
|
|
provider = "postgresql"
|
|
url = env("DATABASE_URL")
|
|
}
|
|
|
|
enum TransferStatus {
|
|
ACTIVE
|
|
EXPIRED
|
|
DELETED
|
|
}
|
|
|
|
model User {
|
|
id String @id @default(cuid())
|
|
name String?
|
|
email String? @unique
|
|
emailVerified DateTime?
|
|
image String?
|
|
plan String @default("free") // "free", "rookie", or "pro"
|
|
|
|
// Optional
|
|
transfers Transfer[] @relation("UserTransfers")
|
|
accounts Account[]
|
|
sessions Session[]
|
|
}
|
|
|
|
model Account {
|
|
id String @id @default(cuid())
|
|
userId String
|
|
type String
|
|
provider String
|
|
providerAccountId String
|
|
refresh_token String?
|
|
access_token String?
|
|
expires_at Int?
|
|
token_type String?
|
|
scope String?
|
|
id_token String?
|
|
session_state String?
|
|
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
|
|
@@unique([provider, providerAccountId])
|
|
}
|
|
|
|
model Session {
|
|
id String @id @default(cuid())
|
|
sessionToken String @unique
|
|
userId String
|
|
expires DateTime
|
|
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
}
|
|
|
|
model VerificationToken {
|
|
identifier String
|
|
token String @unique
|
|
expires DateTime
|
|
|
|
@@unique([identifier, token])
|
|
}
|
|
|
|
model Transfer {
|
|
id Int @id @default(autoincrement())
|
|
senderEmail String
|
|
recipientEmail String
|
|
passwordHash String?
|
|
downloadUrl String @unique
|
|
createdAt DateTime @default(now())
|
|
expiresAt DateTime
|
|
files File[]
|
|
filenames String? // JSON stringified array of filenames
|
|
totalFiles Int @default(0) // new field to track number of files in transfer
|
|
totalSize BigInt @default(0) // Use BigInt to support files > 2GB
|
|
message String? // optional message
|
|
status TransferStatus @default(ACTIVE) // Added
|
|
|
|
userId String? // Change from Int? to String?
|
|
user User? @relation("UserTransfers", fields: [userId], references: [id])
|
|
}
|
|
|
|
|
|
model File {
|
|
id Int @id @default(autoincrement())
|
|
name String
|
|
path String
|
|
size BigInt // Use BigInt to support files > 2GB
|
|
encryptionIv String? // 12-byte IV as base64 string for AES-GCM decryption
|
|
transferId Int
|
|
transfer Transfer @relation(fields: [transferId], references: [id])
|
|
downloads FileDownload[] // New
|
|
}
|
|
|
|
model FileDownload {
|
|
id Int @id @default(autoincrement())
|
|
fileId Int
|
|
file File @relation(fields: [fileId], references: [id], onDelete: Cascade)
|
|
timestamp DateTime @default(now())
|
|
ip String?
|
|
userAgent String?
|
|
}
|