Синхронизация bindings в vps_domains с привязкой по IP, API приёма с Bearer-токеном, настройки и App Switcher из SQLite, UI доменов на VPS и уведомление CFDM при vps_down. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
@@ -1,10 +1,52 @@
|
||||
import { asc, eq } from 'drizzle-orm'
|
||||
import {
|
||||
appSwitcherConfigSchema,
|
||||
type AppSwitcherConfig,
|
||||
} from '@cfdm/shared/contracts/app-switcher'
|
||||
import { getDb, schema } from '../index.js'
|
||||
|
||||
type Row = typeof schema.settings.$inferSelect
|
||||
|
||||
export type SettingsDto = Omit<Row, 'telegramBotToken' | 'autoConvert' | 'syncEnabled' | 'notifyPaymentExpiryEnabled' | 'notifyNewTariffsEnabled' | 'notifyLowBalanceEnabled' | 'notifySyncDigestEnabled' | 'notifyVpsDownEnabled' | 'webhookEnabled' | 'customFields'> & {
|
||||
const DEFAULT_APP_SWITCHER: AppSwitcherConfig = {
|
||||
menuLabel: 'Приложения',
|
||||
apps: [
|
||||
{
|
||||
id: 'vps-tracker',
|
||||
name: 'VPS Tracker',
|
||||
subtitle: 'Учёт виртуальных серверов',
|
||||
url: 'http://192.168.100.67:3001',
|
||||
icon: 'server',
|
||||
shortcut: '⌘1',
|
||||
},
|
||||
{
|
||||
id: 'cfdm',
|
||||
name: 'CF Domain Manager',
|
||||
subtitle: 'Управление доменами',
|
||||
url: 'http://192.168.100.67:6363',
|
||||
icon: 'cloud',
|
||||
shortcut: '⌘2',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
export type SettingsDto = Omit<
|
||||
Row,
|
||||
| 'telegramBotToken'
|
||||
| 'integrationToken'
|
||||
| 'autoConvert'
|
||||
| 'syncEnabled'
|
||||
| 'notifyPaymentExpiryEnabled'
|
||||
| 'notifyNewTariffsEnabled'
|
||||
| 'notifyLowBalanceEnabled'
|
||||
| 'notifySyncDigestEnabled'
|
||||
| 'notifyVpsDownEnabled'
|
||||
| 'webhookEnabled'
|
||||
| 'integrationEnabled'
|
||||
| 'customFields'
|
||||
| 'appSwitcherJson'
|
||||
> & {
|
||||
telegramBotTokenSet: boolean
|
||||
integrationTokenSet: boolean
|
||||
autoConvert: boolean
|
||||
syncEnabled: boolean
|
||||
notifyPaymentExpiryEnabled: boolean
|
||||
@@ -13,9 +55,20 @@ export type SettingsDto = Omit<Row, 'telegramBotToken' | 'autoConvert' | 'syncEn
|
||||
notifySyncDigestEnabled: boolean
|
||||
notifyVpsDownEnabled: boolean
|
||||
webhookEnabled: boolean
|
||||
integrationEnabled: boolean
|
||||
notifyIntervalMinutes: number
|
||||
uptimeCheckIntervalMinutes: number
|
||||
customFields: unknown[]
|
||||
appSwitcher: AppSwitcherConfig
|
||||
}
|
||||
|
||||
function parseAppSwitcher(raw: string | null | undefined): AppSwitcherConfig {
|
||||
if (!raw?.trim()) return DEFAULT_APP_SWITCHER
|
||||
try {
|
||||
return appSwitcherConfigSchema.parse(JSON.parse(raw))
|
||||
} catch {
|
||||
return DEFAULT_APP_SWITCHER
|
||||
}
|
||||
}
|
||||
|
||||
function toDto(row: Row | undefined): SettingsDto | undefined {
|
||||
@@ -28,10 +81,11 @@ function toDto(row: Row | undefined): SettingsDto | undefined {
|
||||
customFields = []
|
||||
}
|
||||
}
|
||||
const { telegramBotToken, ...rest } = row
|
||||
const { telegramBotToken, integrationToken, appSwitcherJson, ...rest } = row
|
||||
return {
|
||||
...rest,
|
||||
telegramBotTokenSet: Boolean(telegramBotToken?.trim()),
|
||||
integrationTokenSet: Boolean(integrationToken?.trim()),
|
||||
autoConvert: Boolean(row.autoConvert),
|
||||
syncEnabled: Boolean(row.syncEnabled),
|
||||
notifyPaymentExpiryEnabled: Boolean(row.notifyPaymentExpiryEnabled),
|
||||
@@ -40,9 +94,11 @@ function toDto(row: Row | undefined): SettingsDto | undefined {
|
||||
notifySyncDigestEnabled: Boolean(row.notifySyncDigestEnabled),
|
||||
notifyVpsDownEnabled: Boolean(row.notifyVpsDownEnabled),
|
||||
webhookEnabled: Boolean(row.webhookEnabled),
|
||||
integrationEnabled: Boolean(row.integrationEnabled),
|
||||
notifyIntervalMinutes: Number(row.notifyIntervalMinutes) || 60,
|
||||
uptimeCheckIntervalMinutes: Number(row.uptimeCheckIntervalMinutes) || 5,
|
||||
customFields: Array.isArray(customFields) ? customFields : [],
|
||||
appSwitcher: parseAppSwitcher(appSwitcherJson),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,6 +130,11 @@ interface SettingsInput {
|
||||
notifyIntervalMinutes?: number
|
||||
uptimeCheckIntervalMinutes?: number
|
||||
customFields?: unknown
|
||||
appSwitcher?: AppSwitcherConfig
|
||||
integrationToken?: string
|
||||
integrationEnabled?: boolean
|
||||
integrationLastSyncAt?: string
|
||||
cfdmApiUrl?: string
|
||||
}
|
||||
|
||||
function buildValues(id: string, existing: Row | undefined, r: SettingsInput) {
|
||||
@@ -156,6 +217,27 @@ function buildValues(id: string, existing: Row | undefined, r: SettingsInput) {
|
||||
? Math.max(1, Number(r.uptimeCheckIntervalMinutes) || 5)
|
||||
: existing?.uptimeCheckIntervalMinutes ?? 5,
|
||||
customFields: serializeCustomFields(r.customFields ?? existing?.customFields),
|
||||
appSwitcherJson:
|
||||
r.appSwitcher !== undefined
|
||||
? JSON.stringify(r.appSwitcher)
|
||||
: existing?.appSwitcherJson ?? JSON.stringify(DEFAULT_APP_SWITCHER),
|
||||
integrationToken:
|
||||
r.integrationToken !== undefined && String(r.integrationToken || '').trim() !== ''
|
||||
? r.integrationToken
|
||||
: existing?.integrationToken ?? '',
|
||||
integrationEnabled:
|
||||
r.integrationEnabled !== undefined
|
||||
? r.integrationEnabled
|
||||
? 1
|
||||
: 0
|
||||
: existing?.integrationEnabled
|
||||
? 1
|
||||
: 0,
|
||||
integrationLastSyncAt:
|
||||
r.integrationLastSyncAt !== undefined
|
||||
? r.integrationLastSyncAt || ''
|
||||
: existing?.integrationLastSyncAt ?? '',
|
||||
cfdmApiUrl: r.cfdmApiUrl !== undefined ? r.cfdmApiUrl || '' : existing?.cfdmApiUrl ?? '',
|
||||
}
|
||||
}
|
||||
|
||||
@@ -170,6 +252,24 @@ export const settingsRepository = {
|
||||
getRow(id: string): Row | undefined {
|
||||
return getDb().select().from(schema.settings).where(eq(schema.settings.id, id)).get()
|
||||
},
|
||||
getIntegrationToken(id = 'settings-main'): string {
|
||||
return this.getRow(id)?.integrationToken?.trim() ?? ''
|
||||
},
|
||||
getAppSwitcher(id = 'settings-main'): AppSwitcherConfig {
|
||||
const row = this.getRow(id)
|
||||
return parseAppSwitcher(row?.appSwitcherJson)
|
||||
},
|
||||
touchIntegrationSync(id = 'settings-main'): void {
|
||||
const db = getDb()
|
||||
const at = new Date().toISOString()
|
||||
const existing = this.getRow(id)
|
||||
if (existing) {
|
||||
db.update(schema.settings)
|
||||
.set({ integrationLastSyncAt: at })
|
||||
.where(eq(schema.settings.id, id))
|
||||
.run()
|
||||
}
|
||||
},
|
||||
upsert(id: string, input: SettingsInput): SettingsDto {
|
||||
const db = getDb()
|
||||
const existing = this.getRow(id)
|
||||
|
||||
@@ -7,6 +7,7 @@ import { settingsRepository } from './settings.js'
|
||||
import { activeTariffsRepository, tariffSyncOptionsRepository } from './tariffs.js'
|
||||
import { projectsRepository } from './projects.js'
|
||||
import { syncLogRepository } from './sync-log.js'
|
||||
import { vpsDomainsRepository } from './vps-domains.js'
|
||||
|
||||
export interface Snapshot {
|
||||
vps: ReturnType<typeof vpsRepository.list>
|
||||
@@ -19,6 +20,7 @@ export interface Snapshot {
|
||||
activeTariffs: ReturnType<typeof activeTariffsRepository.list>
|
||||
tariffSyncOptions: ReturnType<typeof tariffSyncOptionsRepository.list>
|
||||
syncLog: ReturnType<typeof syncLogRepository.listRecent>
|
||||
vpsDomains: ReturnType<typeof vpsDomainsRepository.list>
|
||||
}
|
||||
|
||||
export function getSnapshot(): Snapshot {
|
||||
@@ -33,6 +35,7 @@ export function getSnapshot(): Snapshot {
|
||||
activeTariffs: activeTariffsRepository.list(),
|
||||
tariffSyncOptions: tariffSyncOptionsRepository.list(),
|
||||
syncLog: syncLogRepository.listRecent(50),
|
||||
vpsDomains: vpsDomainsRepository.list(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,4 +50,5 @@ export {
|
||||
tariffSyncOptionsRepository,
|
||||
projectsRepository,
|
||||
syncLogRepository,
|
||||
vpsDomainsRepository,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
||||
import { closeDb } from '../index.js'
|
||||
import { vpsRepository } from './vps.js'
|
||||
import { vpsDomainsRepository } from './vps-domains.js'
|
||||
import { settingsRepository } from './settings.js'
|
||||
import { resetTestDb, seedTestProvider, seedTestProviderAccount } from '../test-setup.js'
|
||||
|
||||
describe('vpsDomainsRepository', () => {
|
||||
beforeEach(() => {
|
||||
resetTestDb()
|
||||
seedTestProvider('p1')
|
||||
seedTestProviderAccount('a1', 'p1')
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
closeDb()
|
||||
})
|
||||
|
||||
it('привязывает домен к VPS по IP', () => {
|
||||
const vps = vpsRepository.create({
|
||||
ip: '203.0.113.10',
|
||||
providerId: 'p1',
|
||||
providerAccountId: 'a1',
|
||||
status: 'active',
|
||||
tariffType: 'monthly',
|
||||
currency: 'RUB',
|
||||
vcpu: 1,
|
||||
ramGb: 1,
|
||||
diskGb: 10,
|
||||
})
|
||||
const created = Array.isArray(vps) ? vps[0]! : vps
|
||||
|
||||
const result = vpsDomainsRepository.syncBindings([
|
||||
{
|
||||
bindingId: 1,
|
||||
serviceId: 10,
|
||||
serviceName: 'VPN Node',
|
||||
serviceSlug: 'vpn-node',
|
||||
fqdn: 'vpn.example.com',
|
||||
zoneName: 'example.com',
|
||||
hostname: 'vpn',
|
||||
ips: ['203.0.113.10'],
|
||||
},
|
||||
])
|
||||
|
||||
expect(result.upserted).toBe(1)
|
||||
expect(result.matched).toBe(1)
|
||||
const domains = vpsDomainsRepository.listByVpsId(created.id)
|
||||
expect(domains).toHaveLength(1)
|
||||
expect(domains[0]?.fqdn).toBe('vpn.example.com')
|
||||
expect(domains[0]?.matchStatus).toBe('matched')
|
||||
})
|
||||
|
||||
it('помечает unmatched без совпадения IP', () => {
|
||||
const result = vpsDomainsRepository.syncBindings([
|
||||
{
|
||||
bindingId: 2,
|
||||
serviceId: 11,
|
||||
serviceName: 'CDN',
|
||||
serviceSlug: 'cdn',
|
||||
fqdn: 'cdn.example.com',
|
||||
zoneName: 'example.com',
|
||||
hostname: 'cdn',
|
||||
ips: ['198.51.100.1'],
|
||||
},
|
||||
])
|
||||
expect(result.unmatched).toBe(1)
|
||||
expect(vpsDomainsRepository.listUnmatched()).toHaveLength(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('settingsRepository integration fields', () => {
|
||||
beforeEach(() => {
|
||||
resetTestDb()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
closeDb()
|
||||
})
|
||||
|
||||
it('маскирует integration token в DTO', () => {
|
||||
settingsRepository.upsert('settings-main', {
|
||||
integrationToken: 'secret-token-value',
|
||||
integrationEnabled: true,
|
||||
})
|
||||
const dto = settingsRepository.get('settings-main')
|
||||
expect(dto?.integrationTokenSet).toBe(true)
|
||||
expect(dto).not.toHaveProperty('integrationToken')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,196 @@
|
||||
import { asc, eq, isNull } from 'drizzle-orm'
|
||||
import type { CfdmBindingSyncItem } from '@cfdm/shared/contracts/integration-cfdm'
|
||||
import { getDb, schema } from '../index.js'
|
||||
import { generateId } from './utils.js'
|
||||
import { vpsRepository } from './vps.js'
|
||||
|
||||
type Row = typeof schema.vpsDomains.$inferSelect
|
||||
|
||||
export type VpsDomainDto = Row
|
||||
|
||||
function normalizeIp(ip: string): string {
|
||||
return ip.trim().toLowerCase()
|
||||
}
|
||||
|
||||
function collectVpsIps(vps: { ip?: string | null; additionalIps?: string[] }): string[] {
|
||||
const ips: string[] = []
|
||||
if (vps.ip?.trim()) ips.push(normalizeIp(vps.ip))
|
||||
for (const raw of vps.additionalIps ?? []) {
|
||||
if (raw?.trim()) ips.push(normalizeIp(raw))
|
||||
}
|
||||
return ips
|
||||
}
|
||||
|
||||
function findVpsIdByIps(
|
||||
allVps: ReturnType<typeof vpsRepository.list>,
|
||||
ips: string[],
|
||||
): string | null {
|
||||
const normalized = [...new Set(ips.map(normalizeIp).filter(Boolean))]
|
||||
if (normalized.length === 0) return null
|
||||
|
||||
const matches: string[] = []
|
||||
for (const v of allVps) {
|
||||
const vips = collectVpsIps(v)
|
||||
if (normalized.some((ip) => vips.includes(ip))) {
|
||||
matches.push(v.id)
|
||||
}
|
||||
}
|
||||
if (matches.length === 1) return matches[0]!
|
||||
return null
|
||||
}
|
||||
|
||||
function resolveMatchStatus(vpsId: string | null): 'matched' | 'unmatched' {
|
||||
return vpsId ? 'matched' : 'unmatched'
|
||||
}
|
||||
|
||||
export const vpsDomainsRepository = {
|
||||
list(): VpsDomainDto[] {
|
||||
return getDb()
|
||||
.select()
|
||||
.from(schema.vpsDomains)
|
||||
.orderBy(asc(schema.vpsDomains.fqdn))
|
||||
.all()
|
||||
},
|
||||
|
||||
listByVpsId(vpsId: string): VpsDomainDto[] {
|
||||
return getDb()
|
||||
.select()
|
||||
.from(schema.vpsDomains)
|
||||
.where(eq(schema.vpsDomains.vpsId, vpsId))
|
||||
.orderBy(asc(schema.vpsDomains.fqdn))
|
||||
.all()
|
||||
},
|
||||
|
||||
getByCfdmBindingId(bindingId: number): VpsDomainDto | undefined {
|
||||
return getDb()
|
||||
.select()
|
||||
.from(schema.vpsDomains)
|
||||
.where(eq(schema.vpsDomains.cfdmBindingId, bindingId))
|
||||
.get()
|
||||
},
|
||||
|
||||
deleteByCfdmBindingId(bindingId: number): boolean {
|
||||
const row = this.getByCfdmBindingId(bindingId)
|
||||
if (!row) return false
|
||||
getDb().delete(schema.vpsDomains).where(eq(schema.vpsDomains.id, row.id)).run()
|
||||
return true
|
||||
},
|
||||
|
||||
rematchAll(): { updated: number } {
|
||||
const db = getDb()
|
||||
const allVps = vpsRepository.list()
|
||||
const rows = db.select().from(schema.vpsDomains).all()
|
||||
let updated = 0
|
||||
const vpsIds = new Set(allVps.map((v) => v.id))
|
||||
|
||||
for (const row of rows) {
|
||||
let storedIps: string[] = []
|
||||
try {
|
||||
storedIps = row.targetIps ? JSON.parse(row.targetIps) : []
|
||||
} catch {
|
||||
storedIps = []
|
||||
}
|
||||
|
||||
let vpsId = row.vpsId
|
||||
if (vpsId && !vpsIds.has(vpsId)) {
|
||||
vpsId = null
|
||||
}
|
||||
if (!vpsId && storedIps.length > 0) {
|
||||
vpsId = findVpsIdByIps(allVps, storedIps)
|
||||
}
|
||||
const matchStatus =
|
||||
vpsId && vpsIds.has(vpsId)
|
||||
? 'matched'
|
||||
: row.vpsId && !vpsIds.has(row.vpsId)
|
||||
? 'orphaned'
|
||||
: resolveMatchStatus(vpsId)
|
||||
|
||||
if (vpsId !== row.vpsId || matchStatus !== row.matchStatus) {
|
||||
db.update(schema.vpsDomains)
|
||||
.set({ vpsId, matchStatus })
|
||||
.where(eq(schema.vpsDomains.id, row.id))
|
||||
.run()
|
||||
updated++
|
||||
}
|
||||
}
|
||||
return { updated }
|
||||
},
|
||||
|
||||
syncBindings(items: CfdmBindingSyncItem[]): {
|
||||
matched: number
|
||||
unmatched: number
|
||||
deleted: number
|
||||
upserted: number
|
||||
} {
|
||||
const db = getDb()
|
||||
const allVps = vpsRepository.list()
|
||||
const now = new Date().toISOString()
|
||||
let matched = 0
|
||||
let unmatched = 0
|
||||
let deleted = 0
|
||||
let upserted = 0
|
||||
|
||||
for (const item of items) {
|
||||
if (item.deleted) {
|
||||
if (this.deleteByCfdmBindingId(item.bindingId)) deleted++
|
||||
continue
|
||||
}
|
||||
|
||||
const vpsId = findVpsIdByIps(allVps, item.ips)
|
||||
const matchStatus = resolveMatchStatus(vpsId)
|
||||
if (matchStatus === 'matched') matched++
|
||||
else unmatched++
|
||||
|
||||
const existing = this.getByCfdmBindingId(item.bindingId)
|
||||
const values = {
|
||||
vpsId,
|
||||
fqdn: item.fqdn,
|
||||
zoneName: item.zoneName,
|
||||
hostname: item.hostname,
|
||||
serviceName: item.serviceName,
|
||||
serviceSlug: item.serviceSlug,
|
||||
cfdmServiceId: item.serviceId,
|
||||
cfdmBindingId: item.bindingId,
|
||||
source: 'cfdm' as const,
|
||||
matchStatus,
|
||||
targetIps: JSON.stringify(item.ips),
|
||||
syncedAt: now,
|
||||
}
|
||||
|
||||
if (existing) {
|
||||
db.update(schema.vpsDomains).set(values).where(eq(schema.vpsDomains.id, existing.id)).run()
|
||||
} else {
|
||||
db.insert(schema.vpsDomains).values({ id: generateId('vd'), ...values }).run()
|
||||
}
|
||||
upserted++
|
||||
}
|
||||
|
||||
return { matched, unmatched, deleted, upserted }
|
||||
},
|
||||
|
||||
markOrphanedForMissingBindings(serviceId: number, keptBindingIds: number[]): number {
|
||||
const db = getDb()
|
||||
const rows = db
|
||||
.select()
|
||||
.from(schema.vpsDomains)
|
||||
.where(eq(schema.vpsDomains.cfdmServiceId, serviceId))
|
||||
.all()
|
||||
let removed = 0
|
||||
for (const row of rows) {
|
||||
if (!keptBindingIds.includes(row.cfdmBindingId)) {
|
||||
db.delete(schema.vpsDomains).where(eq(schema.vpsDomains.id, row.id)).run()
|
||||
removed++
|
||||
}
|
||||
}
|
||||
return removed
|
||||
},
|
||||
|
||||
listUnmatched(): VpsDomainDto[] {
|
||||
return getDb()
|
||||
.select()
|
||||
.from(schema.vpsDomains)
|
||||
.where(isNull(schema.vpsDomains.vpsId))
|
||||
.orderBy(asc(schema.vpsDomains.fqdn))
|
||||
.all()
|
||||
},
|
||||
}
|
||||
@@ -9,6 +9,12 @@ const COLUMN_MIGRATIONS: string[] = [
|
||||
`ALTER TABLE settings ADD COLUMN webhookEnabled INTEGER`,
|
||||
`ALTER TABLE settings ADD COLUMN notifyIntervalMinutes INTEGER`,
|
||||
`ALTER TABLE settings ADD COLUMN uptimeCheckIntervalMinutes INTEGER`,
|
||||
`ALTER TABLE settings ADD COLUMN appSwitcherJson TEXT`,
|
||||
`ALTER TABLE settings ADD COLUMN integrationToken TEXT`,
|
||||
`ALTER TABLE settings ADD COLUMN integrationEnabled INTEGER`,
|
||||
`ALTER TABLE settings ADD COLUMN integrationLastSyncAt TEXT`,
|
||||
`ALTER TABLE settings ADD COLUMN cfdmApiUrl TEXT`,
|
||||
`ALTER TABLE vps_domains ADD COLUMN targetIps TEXT`,
|
||||
]
|
||||
|
||||
const TABLE_MIGRATIONS: string[] = [
|
||||
@@ -52,6 +58,21 @@ const TABLE_MIGRATIONS: string[] = [
|
||||
notes TEXT,
|
||||
createdAt TEXT
|
||||
)`,
|
||||
`CREATE TABLE IF NOT EXISTS vps_domains (
|
||||
id TEXT PRIMARY KEY,
|
||||
vpsId TEXT REFERENCES vps(id) ON DELETE SET NULL,
|
||||
fqdn TEXT NOT NULL,
|
||||
zoneName TEXT NOT NULL,
|
||||
hostname TEXT NOT NULL,
|
||||
serviceName TEXT NOT NULL,
|
||||
serviceSlug TEXT NOT NULL,
|
||||
cfdmServiceId INTEGER NOT NULL,
|
||||
cfdmBindingId INTEGER NOT NULL UNIQUE,
|
||||
source TEXT NOT NULL DEFAULT 'cfdm',
|
||||
matchStatus TEXT NOT NULL DEFAULT 'unmatched',
|
||||
targetIps TEXT,
|
||||
syncedAt TEXT NOT NULL
|
||||
)`,
|
||||
]
|
||||
|
||||
let migrated = false
|
||||
|
||||
@@ -128,6 +128,27 @@ export const settings = sqliteTable('settings', {
|
||||
webhookEnabled: integer('webhookEnabled'),
|
||||
notifyIntervalMinutes: integer('notifyIntervalMinutes'),
|
||||
uptimeCheckIntervalMinutes: integer('uptimeCheckIntervalMinutes'),
|
||||
appSwitcherJson: text('appSwitcherJson'),
|
||||
integrationToken: text('integrationToken'),
|
||||
integrationEnabled: integer('integrationEnabled'),
|
||||
integrationLastSyncAt: text('integrationLastSyncAt'),
|
||||
cfdmApiUrl: text('cfdmApiUrl'),
|
||||
})
|
||||
|
||||
export const vpsDomains = sqliteTable('vps_domains', {
|
||||
id: text('id').primaryKey(),
|
||||
vpsId: text('vpsId').references(() => vps.id, { onDelete: 'set null' }),
|
||||
fqdn: text('fqdn').notNull(),
|
||||
zoneName: text('zoneName').notNull(),
|
||||
hostname: text('hostname').notNull(),
|
||||
serviceName: text('serviceName').notNull(),
|
||||
serviceSlug: text('serviceSlug').notNull(),
|
||||
cfdmServiceId: integer('cfdmServiceId').notNull(),
|
||||
cfdmBindingId: integer('cfdmBindingId').notNull(),
|
||||
source: text('source').notNull().default('cfdm'),
|
||||
matchStatus: text('matchStatus').notNull().default('unmatched'),
|
||||
targetIps: text('targetIps'),
|
||||
syncedAt: text('syncedAt').notNull(),
|
||||
})
|
||||
|
||||
export const notificationLog = sqliteTable('notification_log', {
|
||||
|
||||
@@ -160,7 +160,29 @@ CREATE TABLE IF NOT EXISTS settings (
|
||||
webhookUrl TEXT,
|
||||
webhookEnabled INTEGER,
|
||||
notifyIntervalMinutes INTEGER,
|
||||
uptimeCheckIntervalMinutes INTEGER
|
||||
uptimeCheckIntervalMinutes INTEGER,
|
||||
appSwitcherJson TEXT,
|
||||
integrationToken TEXT,
|
||||
integrationEnabled INTEGER,
|
||||
integrationLastSyncAt TEXT,
|
||||
cfdmApiUrl TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS vps_domains (
|
||||
id TEXT PRIMARY KEY,
|
||||
vpsId TEXT,
|
||||
fqdn TEXT NOT NULL,
|
||||
zoneName TEXT NOT NULL,
|
||||
hostname TEXT NOT NULL,
|
||||
serviceName TEXT NOT NULL,
|
||||
serviceSlug TEXT NOT NULL,
|
||||
cfdmServiceId INTEGER NOT NULL,
|
||||
cfdmBindingId INTEGER NOT NULL UNIQUE,
|
||||
source TEXT NOT NULL DEFAULT 'cfdm',
|
||||
matchStatus TEXT NOT NULL DEFAULT 'unmatched',
|
||||
targetIps TEXT,
|
||||
syncedAt TEXT NOT NULL,
|
||||
FOREIGN KEY (vpsId) REFERENCES vps(id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS notification_log (
|
||||
|
||||
Reference in New Issue
Block a user