diff --git a/backend/migrations/055_ttlock_credentials.sql b/backend/migrations/055_ttlock_credentials.sql new file mode 100644 index 0000000..4fbd215 --- /dev/null +++ b/backend/migrations/055_ttlock_credentials.sql @@ -0,0 +1,5 @@ +-- TTHotel account credentials for OAuth (separate from developer app client_id/secret) +-- "Учетная запись" + "Пароль" from TTHotel → Settings → PMS Integration +ALTER TABLE hotel_ttlock_config + ADD COLUMN IF NOT EXISTS ttlock_username TEXT, -- "Учетная запись", e.g. h_1754382851163 + ADD COLUMN IF NOT EXISTS ttlock_password TEXT; -- "Пароль" (stored as-is, md5'd before OAuth call) diff --git a/backend/src/routes/ttlock.ts b/backend/src/routes/ttlock.ts index a9f2e92..6c46cab 100644 --- a/backend/src/routes/ttlock.ts +++ b/backend/src/routes/ttlock.ts @@ -47,29 +47,32 @@ const ttlockRoutes: FastifyPluginAsync = async (fastify) => { if (!hotelId) return reply.code(404).send({ error: 'Hotel not found' }) const { rows } = await db.query( - `SELECT is_enabled, client_id, card_sectors, api_server FROM hotel_ttlock_config WHERE hotel_id = $1`, + `SELECT is_enabled, client_id, card_sectors, api_server, ttlock_username FROM hotel_ttlock_config WHERE hotel_id = $1`, [hotelId], ) if (!rows[0]) { - return { isEnabled: false, clientId: '', cardSectors: '1,2,3,4,5,6,7,8,9,10', apiServer: 'https://euapi.ttlock.com' } + return { isEnabled: false, clientId: '', cardSectors: '1,2,3,4,5,6,7,8,9,10', apiServer: 'https://euapi.ttlock.com', ttlockUsername: '' } } const r = rows[0] return { - isEnabled: r.is_enabled, - clientId: r.client_id, - cardSectors: r.card_sectors, - apiServer: r.api_server ?? 'https://euapi.ttlock.com', + isEnabled: r.is_enabled, + clientId: r.client_id, + cardSectors: r.card_sectors, + apiServer: r.api_server ?? 'https://euapi.ttlock.com', + ttlockUsername: r.ttlock_username ?? '', } }) // ── PATCH /api/hotels/:slug/ttlock/config ─────────────────────────────────── fastify.patch('/api/hotels/:slug/ttlock/config', { onRequest: [fastify.authenticate] }, async (req, reply) => { const { slug } = req.params @@ -78,25 +81,33 @@ const ttlockRoutes: FastifyPluginAsync = async (fastify) => { const hotelId = await getHotelId(slug) if (!hotelId) return reply.code(404).send({ error: 'Hotel not found' }) - const { is_enabled: isEnabled, client_id: clientId, client_secret: clientSecret, card_sectors: cardSectors, api_server: apiServer } = req.body + const { + is_enabled: isEnabled, client_id: clientId, client_secret: clientSecret, + card_sectors: cardSectors, api_server: apiServer, + ttlock_username: ttlockUsername, ttlock_password: ttlockPassword, + } = req.body await db.query( - `INSERT INTO hotel_ttlock_config (hotel_id, is_enabled, client_id, client_secret, card_sectors, api_server) - VALUES ($1, $2, $3, $4, $5, $6) + `INSERT INTO hotel_ttlock_config (hotel_id, is_enabled, client_id, client_secret, card_sectors, api_server, ttlock_username, ttlock_password) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8) ON CONFLICT (hotel_id) DO UPDATE SET - is_enabled = COALESCE($2, hotel_ttlock_config.is_enabled), - client_id = COALESCE($3, hotel_ttlock_config.client_id), - client_secret = COALESCE(NULLIF($4, ''), hotel_ttlock_config.client_secret), - card_sectors = COALESCE($5, hotel_ttlock_config.card_sectors), - api_server = COALESCE($6, hotel_ttlock_config.api_server), - updated_at = NOW()`, + is_enabled = COALESCE($2, hotel_ttlock_config.is_enabled), + client_id = COALESCE($3, hotel_ttlock_config.client_id), + client_secret = COALESCE(NULLIF($4, ''), hotel_ttlock_config.client_secret), + card_sectors = COALESCE($5, hotel_ttlock_config.card_sectors), + api_server = COALESCE($6, hotel_ttlock_config.api_server), + ttlock_username = COALESCE(NULLIF($7, ''), hotel_ttlock_config.ttlock_username), + ttlock_password = COALESCE(NULLIF($8, ''), hotel_ttlock_config.ttlock_password), + updated_at = NOW()`, [ hotelId, - isEnabled ?? null, - clientId ?? null, - clientSecret ?? '', - cardSectors ?? null, - apiServer ?? null, + isEnabled ?? null, + clientId ?? null, + clientSecret ?? '', + cardSectors ?? null, + apiServer ?? null, + ttlockUsername ?? '', + ttlockPassword ?? '', ], ) return { ok: true } @@ -173,7 +184,7 @@ const ttlockRoutes: FastifyPluginAsync = async (fastify) => { if (!hotelId) return reply.code(404).send({ error: 'Hotel not found' }) const { rows: cfgRows } = await db.query( - 'SELECT client_id, client_secret, api_server FROM hotel_ttlock_config WHERE hotel_id = $1', + 'SELECT client_id, client_secret, api_server, ttlock_username, ttlock_password FROM hotel_ttlock_config WHERE hotel_id = $1', [hotelId], ) const cfg = cfgRows[0] @@ -184,10 +195,12 @@ const ttlockRoutes: FastifyPluginAsync = async (fastify) => { try { const result = await sendCommandAndWait(workstationId, { - type: 'ttlock:test_api', - clientId: cfg.client_id, - clientSecret: cfg.client_secret, - apiServer: cfg.api_server ?? 'https://euapi.ttlock.com', + type: 'ttlock:test_api', + clientId: cfg.client_id, + clientSecret: cfg.client_secret, + ttlockUsername: cfg.ttlock_username ?? '', + ttlockPassword: cfg.ttlock_password ?? '', + apiServer: cfg.api_server ?? 'https://euapi.ttlock.com', }, 15_000) as { ok?: boolean; lockCount?: number; error?: string } if (!result.ok) return reply.code(502).send({ error: result.error ?? 'Ошибка API' }) @@ -340,7 +353,7 @@ const ttlockRoutes: FastifyPluginAsync = async (fastify) => { // Конфигурация TTHotel (client_id, card_sectors) const { rows: cfgRows } = await db.query( - 'SELECT client_id, client_secret, card_sectors, is_enabled, api_server FROM hotel_ttlock_config WHERE hotel_id = $1', + 'SELECT client_id, client_secret, card_sectors, is_enabled, api_server, ttlock_username, ttlock_password FROM hotel_ttlock_config WHERE hotel_id = $1', [hotelId], ) const cfg = cfgRows[0] @@ -388,15 +401,17 @@ const ttlockRoutes: FastifyPluginAsync = async (fastify) => { let result: unknown try { result = await sendCommandAndWait(workstationId, { - type: 'ttlock:write_card', - lockMac: lock.lock_mac, - comPort: comPort, - cardSectors: cfg.card_sectors, - clientId: cfg.client_id, - clientSecret: cfg.client_secret, - apiServer: cfg.api_server ?? 'https://euapi.ttlock.com', - startDate: checkIn, - endDate: checkOut, + type: 'ttlock:write_card', + lockMac: lock.lock_mac, + comPort: comPort, + cardSectors: cfg.card_sectors, + clientId: cfg.client_id, + clientSecret: cfg.client_secret, + ttlockUsername: cfg.ttlock_username ?? '', + ttlockPassword: cfg.ttlock_password ?? '', + apiServer: cfg.api_server ?? 'https://euapi.ttlock.com', + startDate: checkIn, + endDate: checkOut, }, 30_000) } catch (err) { const msg = err instanceof Error ? err.message : 'Агент не ответил' diff --git a/src/lib/api.ts b/src/lib/api.ts index cda68cb..d5df984 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -1094,18 +1094,21 @@ export interface Workstation { } export interface TTLockConfig { - isEnabled: boolean - clientId: string - cardSectors: string - apiServer: string + isEnabled: boolean + clientId: string + cardSectors: string + apiServer: string + ttlockUsername: string } export interface TTLockConfigUpdate { - isEnabled?: boolean - clientId?: string - clientSecret?: string - cardSectors?: string - apiServer?: string + isEnabled?: boolean + clientId?: string + clientSecret?: string + ttlock_username?: string + ttlock_password?: string + cardSectors?: string + apiServer?: string } export interface RoomLockMapping { diff --git a/src/pages/TTLockPage.tsx b/src/pages/TTLockPage.tsx index 12828a2..c30cfb4 100644 --- a/src/pages/TTLockPage.tsx +++ b/src/pages/TTLockPage.tsx @@ -139,11 +139,13 @@ export function TTLockPage() { const [success, setSuccess] = useState(null) // Форма настроек - const [isEnabled, setIsEnabled] = useState(false) - const [clientId, setClientId] = useState('') - const [clientSecret, setClientSecret] = useState('') - const [cardSectors, setCardSectors] = useState('1,2,3,4,5,6,7,8,9,10') - const [apiServer, setApiServer] = useState('https://euapi.ttlock.com') + const [isEnabled, setIsEnabled] = useState(false) + const [clientId, setClientId] = useState('') + const [clientSecret, setClientSecret] = useState('') + const [ttlockUsername, setTtlockUsername] = useState('') + const [ttlockPassword, setTtlockPassword] = useState('') + const [cardSectors, setCardSectors] = useState('1,2,3,4,5,6,7,8,9,10') + const [apiServer, setApiServer] = useState('https://euapi.ttlock.com') // Per-workstation COM port state const [wsComPorts, setWsComPorts] = useState>({}) @@ -242,6 +244,7 @@ export function TTLockPage() { setConfig(cfg) setIsEnabled(cfg.isEnabled) setClientId(cfg.clientId) + setTtlockUsername(cfg.ttlockUsername || '') setCardSectors(cfg.cardSectors || '1,2,3,4,5,6,7,8,9,10') setApiServer(cfg.apiServer || 'https://euapi.ttlock.com') setMappings(maps) @@ -263,13 +266,16 @@ export function TTLockPage() { try { await api.ttlock.updateConfig(slug, { isEnabled, - clientId: clientId.trim(), - clientSecret: clientSecret.trim() || undefined, - cardSectors: cardSectors.trim(), - apiServer: apiServer, + clientId: clientId.trim(), + clientSecret: clientSecret.trim() || undefined, + ttlock_username: ttlockUsername.trim() || undefined, + ttlock_password: ttlockPassword.trim() || undefined, + cardSectors: cardSectors.trim(), + apiServer: apiServer, }) showSuccess('Настройки сохранены') setClientSecret('') + setTtlockPassword('') await load() } catch { setError('Ошибка сохранения') @@ -410,6 +416,27 @@ export function TTLockPage() { +
+
+ + setTtlockUsername(e.target.value)} + placeholder="h_1754382851163" + className="input w-full font-mono text-sm" + /> +
+
+ + +
+
+ {/* Сервер TTLock API */}