mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-11 03:13:24 +00:00
- docs: device.pair.update and the `permanent` option on approve in docs/04-gateway-protocol.md, docs/19-websocket-rpc.md and websocket-protocol.md; the paired-device TTL row in docs/09-security.md now mentions the admin opt-out. - store.ErrPairedDeviceNotFound: SetPairingPermanent wraps it in both stores. device.pair.update maps it to NOT_FOUND and any other store error to INTERNAL, so a DB failure no longer reads as "not found". - web UI: approve and make-permanent/set-expiry now toast the server error and reload the list in `finally`. A partially applied approve (paired, but the permanent write failed) shows up in the table instead of leaving the dialog dead-ended. - SQLite ListPaired: a stored expiry that fails to parse stays 0 (expires, date unknown) rather than being mistaken for permanent; the UI renders it as "--" instead of a 1970 date. Tests: gateway handler error mapping (NOT_FOUND / INTERNAL / OK), sentinel checks in the PG and SQLite store tests, SQLite unreadable-expiry case. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
54 lines
2.6 KiB
Go
54 lines
2.6 KiB
Go
package store
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
)
|
|
|
|
// ErrPairedDeviceNotFound is returned by SetPairingPermanent when there is no
|
|
// live (non-expired) pairing for the sender/channel.
|
|
var ErrPairedDeviceNotFound = errors.New("paired device not found")
|
|
|
|
// PairingRequest represents a pending pairing code.
|
|
type PairingRequestData struct {
|
|
Code string `json:"code" db:"code"`
|
|
SenderID string `json:"sender_id" db:"sender_id"`
|
|
Channel string `json:"channel" db:"channel"`
|
|
ChatID string `json:"chat_id" db:"chat_id"`
|
|
AccountID string `json:"account_id" db:"account_id"`
|
|
CreatedAt int64 `json:"created_at" db:"created_at"`
|
|
ExpiresAt int64 `json:"expires_at" db:"expires_at"`
|
|
Metadata map[string]string `json:"metadata,omitempty" db:"metadata"`
|
|
}
|
|
|
|
// PairedDeviceData represents an approved pairing.
|
|
// ExpiresAt is Unix ms; nil means the pairing never expires, 0 means it
|
|
// expires but the stored date could not be read.
|
|
type PairedDeviceData struct {
|
|
SenderID string `json:"sender_id" db:"sender_id"`
|
|
Channel string `json:"channel" db:"channel"`
|
|
ChatID string `json:"chat_id" db:"chat_id"`
|
|
PairedAt int64 `json:"paired_at" db:"paired_at"`
|
|
PairedBy string `json:"paired_by" db:"paired_by"`
|
|
ExpiresAt *int64 `json:"expires_at" db:"expires_at"`
|
|
Metadata map[string]string `json:"metadata,omitempty" db:"metadata"`
|
|
}
|
|
|
|
// PairingStore manages device pairing.
|
|
type PairingStore interface {
|
|
RequestPairing(ctx context.Context, senderID, channel, chatID, accountID string, metadata map[string]string) (string, error)
|
|
ApprovePairing(ctx context.Context, code, approvedBy string) (*PairedDeviceData, error)
|
|
DenyPairing(ctx context.Context, code string) error
|
|
RevokePairing(ctx context.Context, senderID, channel string) error
|
|
IsPaired(ctx context.Context, senderID, channel string) (bool, error)
|
|
// SetPairingPermanent clears the expiry of an existing pairing (permanent=true)
|
|
// or restarts the default TTL from now (permanent=false).
|
|
SetPairingPermanent(ctx context.Context, senderID, channel string, permanent bool) error
|
|
ListPending(ctx context.Context) []PairingRequestData
|
|
ListPaired(ctx context.Context) []PairedDeviceData
|
|
// MigrateGroupChatID updates all references from oldChatID to newChatID
|
|
// across paired_devices, sessions, and channel_contacts within a transaction.
|
|
// Scoped by tenant_id and channel. Idempotent (safe to call multiple times).
|
|
MigrateGroupChatID(ctx context.Context, channel, oldChatID, newChatID string) error
|
|
}
|