12 KiB
12 KiB
Authenticated Text Sharing Plan
Overview
Create a dedicated text-sharing feature with:
- A simple
/shareupload page inspired by mclo.gs. - Publicly readable pages at
/share/[id]. - Discord login required before web uploads or comments.
- A private bot endpoint for forwarding Discord
.txtattachments. - PostgreSQL tables
share.textandshare.comment. - Duplicate-safe synchronization with existing NextAuth accounts.
System Diagram
Web upload
flowchart TD
A["Visitor opens /share"] --> B{"Discord session?"}
B -- No --> C["LoginDialog"]
C --> D["Discord OAuth"]
D --> E["Return to /share with session"]
E --> F
B -- Yes --> F["Upload form<br/>Paste or drop .txt<br/>Description and optional image"]
F --> G["Session-protected web handler"]
G --> H["Validate text, description, and image"]
H --> I["Check 10 shares/hour"]
I --> J["Resolve Discord account"]
J --> K["Store image and share atomically"]
K --> L["Redirect to /share/a1b2c3d4"]
Bot upload
flowchart TD
A["Discord member sends a .txt file"] --> B["External Discord bot"]
B -->|"file + senderDiscordId<br/>Bearer SHARE_BOT_SECRET"| C["POST /api/share"]
C --> D["Verify private bot secret"]
D --> E["Validate .txt and optional image"]
E --> F["Check bot and sender rate limits"]
F --> G["Verify sender is a guild member"]
G --> H["Fetch global name and avatar from Discord"]
H --> I["Ensure one Discord account"]
I --> J{"Discord account link exists?"}
J -- Yes --> K["Update existing account.user"]
J -- No --> L["Create one account.user<br/>and Discord account.account link"]
K --> M["Create share.text atomically"]
L --> M
M --> N["Return /share/id"]
N --> O["Bot sends link to Discord"]
Database relationships
erDiagram
ACCOUNT_USER ||--o{ ACCOUNT_ACCOUNT : "has OAuth links"
ACCOUNT_USER o|--o{ SHARE_TEXT : "authors"
ACCOUNT_USER o|--o{ SHARE_COMMENT : "writes"
SHARE_TEXT ||--o{ SHARE_COMMENT : "contains"
PUBLIC_CDN o|--o| SHARE_TEXT : "optional image"
ACCOUNT_USER {
text id PK
text discord_id UK
text name
text image
}
ACCOUNT_ACCOUNT {
text userId FK
text provider PK
text providerAccountId PK
}
SHARE_TEXT {
varchar id PK "8 lowercase alphanumeric characters"
text content
text description "optional"
text image_cdn_id FK "optional"
text source "web or bot"
text author_user_id FK "optional"
text author_discord_id
text author_name
text author_avatar_url "optional"
timestamp created_at
}
SHARE_COMMENT {
text id PK "UUIDv7"
varchar text_id FK
text body
text author_user_id FK "optional"
text author_discord_id
text author_name
text author_avatar_url "optional"
timestamp created_at
}
PUBLIC_CDN {
text id PK
bytea data
text type
integer size
}
Public shared page
flowchart TB
subgraph PAGE["/share/id"]
direction TB
A["Author avatar · Global display name · Bangkok time"]
B["Optional description"]
C["Optional image"]
D["Main shared text<br/>Whitespace-preserving monospace"]
E["Copy text<br/>Copies main text only"]
F["Copy link<br/>Copies page URL"]
G["Comments<br/>Oldest or newest · 50 per page"]
H["Public to read<br/>Discord login required to post"]
A --> B --> C --> D
D --> E
D --> F
E --> G
F --> G
G --> H
end
Database and Identity
- Add
shareto Drizzle'sschemaFilter. - Define a dedicated
pgSchema("share"). - Create
share.textwith:- Random eight-character lowercase alphanumeric primary key.
- Exact main text content.
- Optional plain-text description.
- Optional CDN image reference.
- Source value of
weborbot. - Nullable
account.userreference. - Discord ID, display-name, and avatar snapshots.
- Creation timestamp.
- Create
share.commentwith:- UUIDv7 primary key.
- Parent
share.textreference with cascade deletion. - Comment body.
- Nullable
account.userreference. - Discord identity snapshots.
- Creation timestamp.
- An index supporting stable sorting and pagination.
- Repair the missing Drizzle snapshot for migration
0017before generating the share migration.
Duplicate-Safe Discord Accounts
Bot uploads must not create multiple users for the same Discord account.
- Backfill
account.user.discord_idfrom existing Discord account links. - Add a unique index for non-null Discord IDs.
- Implement a shared transactional
ensureDiscordUseroperation:- Look up
account.accountusing providerdiscordand the Discord user ID. - If linked, update that existing user's latest profile information.
- If not linked, upsert
account.userby uniquediscord_id. - Insert the Discord
account.accountlink using its existing composite conflict key. - Reload and return the database winner after a concurrent conflict.
- Look up
- Invoke the same reconciliation during Discord OAuth before NextAuth completes account handling.
- Bot profile updates must not erase an existing email address.
- Verified OAuth login may refresh the email, global display name, avatar, and account token fields.
Authentication
/sharechecks the NextAuth session on the server before rendering upload controls.- Unauthenticated visitors receive a non-dismissible Discord login dialog.
- Do not render paste, description, image, or upload controls before login.
- The web upload handler repeats authentication and returns HTTP 401 without a valid session.
/share/[id]remains publicly readable.- Reading comments remains public, but posting comments requires Discord login.
Web Upload
The authenticated /share page should remain visually simple:
- One large
Paste your text heretextarea. - Paste, Browse, and Drop support for
.txtfiles. - A byte counter against the 5 MiB limit.
- A smaller optional description field.
- One optional image picker with preview and remove actions.
- One clear Thai upload button:
อัปโหลดและสร้างลิงก์. - Navigate directly to
/share/[id]after success.
Validation:
- Main text is required and must contain non-whitespace content.
- Main text may be at most 5 MiB of UTF-8 data.
- Description may be at most 5,000 characters.
- Image is optional and may be PNG, JPEG, WebP, or GIF.
- Image contents must match their declared type.
- Image size may be at most 50 MiB.
Private Bot API
POST /api/share is private and reserved for the trusted Discord bot.
Authentication:
Authorization: Bearer <SHARE_BOT_SECRET>
Multipart fields:
| Field | Required | Description |
|---|---|---|
file |
Yes | UTF-8 .txt attachment, maximum 5 MiB |
senderDiscordId |
Yes | ID of the Discord message author |
description |
No | Plain text, maximum 5,000 characters |
image |
No | Validated image, maximum 50 MiB |
The endpoint must:
- Verify the bearer secret.
- Reject files without a case-insensitive
.txtextension. - Reject empty, invalid UTF-8, NUL-containing, or oversized files.
- Confirm the sender belongs to the configured Discord guild.
- Fetch the sender's global Discord profile server-side.
- Use
global_name, falling back tousername. - Use the global Discord avatar rather than a guild-specific avatar.
- Ignore and reject request-provided display-name or avatar fields.
- Synchronize the sender with the existing NextAuth account tables.
- Store the optional image and share in one transaction.
Successful response:
{
"success": true,
"share": {
"id": "a1b2c3d4",
"url": "https://example.com/share/a1b2c3d4",
"author": {
"displayName": "Example User",
"avatarUrl": "https://cdn.discordapp.com/..."
},
"imageUrl": null,
"createdAt": "2026-08-16T00:00:00.000Z"
}
}
Expected statuses:
201share created.400malformed or blank content.401invalid bot secret or missing web session.413text or image exceeds its limit.415unsupported file or image type.422invalid sender or sender is not a guild member.429rate limit exceeded, includingRetry-After.503Discord profile lookup or rate-limit storage is unavailable.
Rate Limits
- Combined web and bot uploads: 10 shares/hour per attributed Discord sender.
- Private bot endpoint: 60 requests/hour globally.
- Comments: 30 comments/hour per Discord account.
- Store fixed-window counters in Redis.
- Return HTTP 429 with
Retry-Afterwhen exceeded. - Return HTTP 503 if Redis is unavailable instead of accepting unlimited uploads.
Shared Page
/share/[id] displays:
- Author global display name and avatar snapshot.
- Bangkok-localized creation timestamp.
- Optional description in its own plain-text section.
- Optional image beneath the description.
- Main content in a whitespace-preserving monospaced panel.
คัดลอกข้อความto copy the exact main text.คัดลอกลิงก์to copy the page URL.- Delete control for the author or configured administrators.
The text copy action must use only share.text.content. It must preserve whitespace and must never append or prepend the description.
Comments
- Everyone may read comments.
- Discord login is required to post.
- Comment body maximum is 2,000 characters.
- Default order is oldest first.
- Allow switching between oldest and newest.
- Use 50 comments per page.
- Use
comments=oldest|newestandpagequery parameters. - Comment authors and configured administrators may delete comments.
- Deleting a share cascades to all comments.
Login Dialog Extraction
- Move
LoginDialogfromapp/form/client.tsxtocomponents/login-dialog.tsx. - Preserve the form dialog's current behavior and Thai copy as defaults.
- Add optional
titleanddescriptionprops. - Update the form page to import it from
@/components/login-dialog. - Use share-specific Thai copy on
/shareexplaining that login is required before uploading. - Keep the dialog non-dismissible and return users to the original URL after OAuth.
- Remove only imports made unused by the extraction.
Verification
- Verify unauthenticated
/sharerequests do not render upload controls. - Verify direct unauthenticated web upload requests return HTTP 401.
- Verify OAuth returns to
/shareand reveals the form. - Test existing-user updates, new bot sender creation, concurrent first uploads, and OAuth login after a bot-created account.
- Assert that each Discord ID resolves to exactly one
account.userand one Discordaccount.accountlink. - Test
.txt, UTF-8, NUL, text, description, and image boundaries. - Test bot authorization, sender verification, atomic rollback, and response shapes.
- Test rate limits at 10/11 user shares, 60/61 bot requests, and 30/31 comments.
- Test that Copy text preserves the content exactly and excludes the description.
- Regression-test the extracted form login dialog.
- Run:
bun testbunx tsc --noEmitbun run lintgit diff --check
- Confirm Drizzle reports no pending schema difference after migration generation.
Scope Boundaries
- The Discord bot implementation is outside this repository.
- The bot supplies
message.author.idwhen forwarding a.txtattachment. - Shared pages remain until deleted.
- No editing, automatic expiration, public share feed, search, anonymous publishing, or anonymous comments are included.