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:
5
backend/migrations/055_ttlock_credentials.sql
Normal file
5
backend/migrations/055_ttlock_credentials.sql
Normal file
@@ -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)
|
||||
@@ -47,11 +47,11 @@ 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 {
|
||||
@@ -59,6 +59,7 @@ const ttlockRoutes: FastifyPluginAsync = async (fastify) => {
|
||||
clientId: r.client_id,
|
||||
cardSectors: r.card_sectors,
|
||||
apiServer: r.api_server ?? 'https://euapi.ttlock.com',
|
||||
ttlockUsername: r.ttlock_username ?? '',
|
||||
}
|
||||
})
|
||||
|
||||
@@ -70,6 +71,8 @@ const ttlockRoutes: FastifyPluginAsync = async (fastify) => {
|
||||
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,17 +81,23 @@ 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),
|
||||
ttlock_username = COALESCE(NULLIF($7, ''), hotel_ttlock_config.ttlock_username),
|
||||
ttlock_password = COALESCE(NULLIF($8, ''), hotel_ttlock_config.ttlock_password),
|
||||
updated_at = NOW()`,
|
||||
[
|
||||
hotelId,
|
||||
@@ -97,6 +106,8 @@ const ttlockRoutes: FastifyPluginAsync = async (fastify) => {
|
||||
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]
|
||||
@@ -187,6 +198,8 @@ const ttlockRoutes: FastifyPluginAsync = async (fastify) => {
|
||||
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 }
|
||||
|
||||
@@ -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]
|
||||
@@ -394,6 +407,8 @@ const ttlockRoutes: FastifyPluginAsync = async (fastify) => {
|
||||
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,
|
||||
|
||||
@@ -1098,12 +1098,15 @@ export interface TTLockConfig {
|
||||
clientId: string
|
||||
cardSectors: string
|
||||
apiServer: string
|
||||
ttlockUsername: string
|
||||
}
|
||||
|
||||
export interface TTLockConfigUpdate {
|
||||
isEnabled?: boolean
|
||||
clientId?: string
|
||||
clientSecret?: string
|
||||
ttlock_username?: string
|
||||
ttlock_password?: string
|
||||
cardSectors?: string
|
||||
apiServer?: string
|
||||
}
|
||||
|
||||
@@ -142,6 +142,8 @@ export function TTLockPage() {
|
||||
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')
|
||||
|
||||
@@ -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)
|
||||
@@ -265,11 +268,14 @@ export function TTLockPage() {
|
||||
isEnabled,
|
||||
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() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="form-label">Учетная запись <span className="font-normal text-slate-400">(поле «Учетная запись» в TTHotel)</span></label>
|
||||
<input
|
||||
type="text"
|
||||
value={ttlockUsername}
|
||||
onChange={e => setTtlockUsername(e.target.value)}
|
||||
placeholder="h_1754382851163"
|
||||
className="input w-full font-mono text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="form-label">Пароль <span className="font-normal text-slate-400">(поле «Пароль» в TTHotel)</span></label>
|
||||
<PasswordInput
|
||||
value={ttlockPassword}
|
||||
onChange={setTtlockPassword}
|
||||
placeholder={ttlockUsername && !ttlockPassword ? '••••••• (не изменится)' : 'Пароль от учетной записи TTHotel'}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Сервер TTLock API */}
|
||||
<div>
|
||||
<label className="form-label">Сервер TTLock API</label>
|
||||
|
||||
Reference in New Issue
Block a user