Add embeddable chat widget for websites
Reuses the existing portal-token/SSE mechanism (customers.portalToken, /api/portal/events) for anonymous widget visitors instead of building a parallel auth system. Public API: session bootstrap + messages endpoint, both scoped by a non-secret siteKey with optional origin allowlist. Embeddable script (public/widget.js) is a small self-contained vanilla JS file: floating button + iframe kept mounted for a live SSE connection, with a postMessage bridge for the unread badge. Admin UI at Settings -> Виджет manages sites and shows the embed snippet. Also generalized recordTelegramInboundMessage -> recordChatInboundMessage (channel param) since Telegram and the widget need the identical find-or-create-open-ticket heuristic. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QcXH24ky6zjk2UyK5oZUPH
This commit is contained in:
1 parent
59e6c32d79
commit
4efce5f307
19 files changed
+1267
-14
No files matched your search
+14
-1
@@ -63,7 +63,7 @@ export const tickets = sqliteTable("tickets", {
|
||||
priority: text("priority", { enum: ["low", "normal", "high", "urgent"] })
|
||||
.notNull()
|
||||
.default("normal"),
|
||||
channel: text("channel", { enum: ["telegram", "portal", "manual", "email"] }).notNull(),
|
||||
channel: text("channel", { enum: ["telegram", "portal", "manual", "email", "widget"] }).notNull(),
|
||||
customerId: text("customer_id")
|
||||
.notNull()
|
||||
.references(() => customers.id, { onDelete: "cascade" }),
|
||||
@@ -120,3 +120,16 @@ export const mailboxConfig = sqliteTable("mailbox_config", {
|
||||
verifiedAt: integer("verified_at", { mode: "timestamp_ms" }),
|
||||
createdAt: timestamps.createdAt,
|
||||
});
|
||||
|
||||
/** A site the chat widget is embedded on. Public, non-secret identifier — labels ticket origin, not a trust boundary. */
|
||||
export const widgetSites = sqliteTable("widget_sites", {
|
||||
id: id(),
|
||||
siteKey: text("site_key")
|
||||
.notNull()
|
||||
.unique()
|
||||
.$defaultFn(() => crypto.randomBytes(12).toString("base64url")),
|
||||
name: text("name").notNull(),
|
||||
allowedOrigin: text("allowed_origin"),
|
||||
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true),
|
||||
createdAt: timestamps.createdAt,
|
||||
});
|
||||
Reference in new issue
Block a user