feat: TTLock — add Учетная запись + Пароль fields for OAuth

Per Sciener docs: OAuth requires TTHotel account credentials (Учетная запись + Пароль)
separate from developer app client_id/client_secret.

- Migration 055: add ttlock_username, ttlock_password columns
- Backend: store and pass new fields to agent
- UI: add input fields matching TTHotel PMS integration page
- Agent: getAccessToken() uses ttlockUsername/ttlockPassword when provided

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-02 11:46:30 +03:00
parent a7b59c4881
commit 8c215f2dcd
4 changed files with 108 additions and 58 deletions

View File

@@ -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<SlugParam & {
Body: {
is_enabled?: boolean
client_id?: string
client_secret?: string
card_sectors?: string
api_server?: string
is_enabled?: boolean
client_id?: string
client_secret?: string
card_sectors?: string
api_server?: string
ttlock_username?: string
ttlock_password?: string
}
}>('/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 : 'Агент не ответил'