# Authenticated Text Sharing Plan ## Overview Create a dedicated text-sharing feature with: - A simple `/share` upload 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 `.txt` attachments. - PostgreSQL tables `share.text` and `share.comment`. - Duplicate-safe synchronization with existing NextAuth accounts. ## System Diagram ### Web upload ```mermaid 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
Paste or drop .txt
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 ```mermaid flowchart TD A["Discord member sends a .txt file"] --> B["External Discord bot"] B -->|"file + senderDiscordId
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
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 ```mermaid 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 ```mermaid 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
Whitespace-preserving monospace"] E["Copy text
Copies main text only"] F["Copy link
Copies page URL"] G["Comments
Oldest or newest · 50 per page"] H["Public to read
Discord login required to post"] A --> B --> C --> D D --> E D --> F E --> G F --> G G --> H end ``` ## Database and Identity - Add `share` to Drizzle's `schemaFilter`. - Define a dedicated `pgSchema("share")`. - Create `share.text` with: - Random eight-character lowercase alphanumeric primary key. - Exact main text content. - Optional plain-text description. - Optional CDN image reference. - Source value of `web` or `bot`. - Nullable `account.user` reference. - Discord ID, display-name, and avatar snapshots. - Creation timestamp. - Create `share.comment` with: - UUIDv7 primary key. - Parent `share.text` reference with cascade deletion. - Comment body. - Nullable `account.user` reference. - Discord identity snapshots. - Creation timestamp. - An index supporting stable sorting and pagination. - Repair the missing Drizzle snapshot for migration `0017` before generating the share migration. ## Duplicate-Safe Discord Accounts Bot uploads must not create multiple users for the same Discord account. - Backfill `account.user.discord_id` from existing Discord account links. - Add a unique index for non-null Discord IDs. - Implement a shared transactional `ensureDiscordUser` operation: 1. Look up `account.account` using provider `discord` and the Discord user ID. 2. If linked, update that existing user's latest profile information. 3. If not linked, upsert `account.user` by unique `discord_id`. 4. Insert the Discord `account.account` link using its existing composite conflict key. 5. Reload and return the database winner after a concurrent conflict. - 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 - `/share` checks 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 here` textarea. - Paste, Browse, and Drop support for `.txt` files. - 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: ```http Authorization: Bearer ``` 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: 1. Verify the bearer secret. 2. Reject files without a case-insensitive `.txt` extension. 3. Reject empty, invalid UTF-8, NUL-containing, or oversized files. 4. Confirm the sender belongs to the configured Discord guild. 5. Fetch the sender's global Discord profile server-side. 6. Use `global_name`, falling back to `username`. 7. Use the global Discord avatar rather than a guild-specific avatar. 8. Ignore and reject request-provided display-name or avatar fields. 9. Synchronize the sender with the existing NextAuth account tables. 10. Store the optional image and share in one transaction. Successful response: ```json { "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: - `201` share created. - `400` malformed or blank content. - `401` invalid bot secret or missing web session. - `413` text or image exceeds its limit. - `415` unsupported file or image type. - `422` invalid sender or sender is not a guild member. - `429` rate limit exceeded, including `Retry-After`. - `503` Discord 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-After` when 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|newest` and `page` query parameters. - Comment authors and configured administrators may delete comments. - Deleting a share cascades to all comments. ## Login Dialog Extraction - Move `LoginDialog` from `app/form/client.tsx` to `components/login-dialog.tsx`. - Preserve the form dialog's current behavior and Thai copy as defaults. - Add optional `title` and `description` props. - Update the form page to import it from `@/components/login-dialog`. - Use share-specific Thai copy on `/share` explaining 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 `/share` requests do not render upload controls. - Verify direct unauthenticated web upload requests return HTTP 401. - Verify OAuth returns to `/share` and 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.user` and one Discord `account.account` link. - 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 test` - `bunx tsc --noEmit` - `bun run lint` - `git 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.id` when forwarding a `.txt` attachment. - Shared pages remain until deleted. - No editing, automatic expiration, public share feed, search, anonymous publishing, or anonymous comments are included.