Добавлены event_id/actorUserId и async push source_app=vps без блокировки CRUD. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
@@ -5,6 +5,8 @@ AUTH_REQUIRED=false
|
|||||||
AUTH_JWT_SECRET=dev-secret-change-me
|
AUTH_JWT_SECRET=dev-secret-change-me
|
||||||
AUTH_ISSUER=https://auth.shnt.top
|
AUTH_ISSUER=https://auth.shnt.top
|
||||||
AUTH_PORTAL_URL=http://localhost:5175
|
AUTH_PORTAL_URL=http://localhost:5175
|
||||||
|
# Shared with auth-portal AUDIT_INGEST_SECRET (Bearer for POST /api/v1/ingest/audit)
|
||||||
|
AUTH_AUDIT_INGEST_SECRET=dev-audit-ingest-secret
|
||||||
# Owner of space-main after migration (portal user id)
|
# Owner of space-main after migration (portal user id)
|
||||||
# VPS_MAIN_SPACE_OWNER_USER_ID=
|
# VPS_MAIN_SPACE_OWNER_USER_ID=
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import type { FastifyRequest } from 'fastify'
|
||||||
|
import type { AuthUser } from './permissions.js'
|
||||||
|
|
||||||
|
export type AuditActorContext = {
|
||||||
|
actorUserId: string | null
|
||||||
|
actorEmail: string | null
|
||||||
|
actorName: string | null
|
||||||
|
ip: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function actorFromAuthUser(user?: AuthUser | null): AuditActorContext {
|
||||||
|
if (!user) {
|
||||||
|
return {
|
||||||
|
actorUserId: null,
|
||||||
|
actorEmail: null,
|
||||||
|
actorName: null,
|
||||||
|
ip: null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
actorUserId: user.id,
|
||||||
|
actorEmail: user.email || null,
|
||||||
|
actorName: user.name || null,
|
||||||
|
ip: null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function actorFromRequest(request: FastifyRequest): AuditActorContext {
|
||||||
|
const base = actorFromAuthUser(request.authUser)
|
||||||
|
const forwarded = request.headers['x-forwarded-for']
|
||||||
|
let ip: string | null = request.ip ?? null
|
||||||
|
if (typeof forwarded === 'string' && forwarded.trim()) {
|
||||||
|
ip = forwarded.split(',')[0]?.trim() ?? ip
|
||||||
|
}
|
||||||
|
return { ...base, ip }
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import { vpsGrantsRepository } from '@cfdm/db/repositories/spaces'
|
|||||||
import { getCurrentSpaceId } from '@cfdm/db'
|
import { getCurrentSpaceId } from '@cfdm/db'
|
||||||
import { vpsSchema } from '@cfdm/shared/contracts/vps'
|
import { vpsSchema } from '@cfdm/shared/contracts/vps'
|
||||||
import { auditCreate, auditDelete, auditUpdate } from '../services/audit.js'
|
import { auditCreate, auditDelete, auditUpdate } from '../services/audit.js'
|
||||||
|
import { actorFromRequest } from '../lib/audit-actor.js'
|
||||||
import { canWriteInSpace, requireSpaceRole } from '../plugins/space.js'
|
import { canWriteInSpace, requireSpaceRole } from '../plugins/space.js'
|
||||||
|
|
||||||
export const vpsRoutes: FastifyPluginAsync = async (app) => {
|
export const vpsRoutes: FastifyPluginAsync = async (app) => {
|
||||||
@@ -43,7 +44,9 @@ export const vpsRoutes: FastifyPluginAsync = async (app) => {
|
|||||||
const created = vpsRepository.create(parsed.data)
|
const created = vpsRepository.create(parsed.data)
|
||||||
const list = Array.isArray(created) ? created : [created]
|
const list = Array.isArray(created) ? created : [created]
|
||||||
const last = list[list.length - 1] as { id?: string } | undefined
|
const last = list[list.length - 1] as { id?: string } | undefined
|
||||||
if (last?.id) auditCreate('vps', last.id, parsed.data as Record<string, unknown>)
|
if (last?.id) {
|
||||||
|
auditCreate('vps', last.id, parsed.data as Record<string, unknown>, actorFromRequest(req))
|
||||||
|
}
|
||||||
return reply.code(201).send(created)
|
return reply.code(201).send(created)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -61,7 +64,7 @@ export const vpsRoutes: FastifyPluginAsync = async (app) => {
|
|||||||
return reply.code(404).send({ error: { code: 'NOT_FOUND', message: 'Not found' } })
|
return reply.code(404).send({ error: { code: 'NOT_FOUND', message: 'Not found' } })
|
||||||
}
|
}
|
||||||
vpsDomainsRepository.rematchAll()
|
vpsDomainsRepository.rematchAll()
|
||||||
auditUpdate('vps', req.params.id, parsed.data as Record<string, unknown>)
|
auditUpdate('vps', req.params.id, parsed.data as Record<string, unknown>, actorFromRequest(req))
|
||||||
return updated
|
return updated
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,7 +76,7 @@ export const vpsRoutes: FastifyPluginAsync = async (app) => {
|
|||||||
if (!updated) {
|
if (!updated) {
|
||||||
return reply.code(404).send({ error: { code: 'NOT_FOUND', message: 'Not found' } })
|
return reply.code(404).send({ error: { code: 'NOT_FOUND', message: 'Not found' } })
|
||||||
}
|
}
|
||||||
auditUpdate('vps', req.params.id, parsed.data as Record<string, unknown>)
|
auditUpdate('vps', req.params.id, parsed.data as Record<string, unknown>, actorFromRequest(req))
|
||||||
return { ...updated, access: 'shared', grantPermission: 'write' }
|
return { ...updated, access: 'shared', grantPermission: 'write' }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,7 +99,7 @@ export const vpsRoutes: FastifyPluginAsync = async (app) => {
|
|||||||
}
|
}
|
||||||
return reply.code(404).send({ error: { code: 'NOT_FOUND', message: 'Not found' } })
|
return reply.code(404).send({ error: { code: 'NOT_FOUND', message: 'Not found' } })
|
||||||
}
|
}
|
||||||
auditDelete('vps', req.params.id)
|
auditDelete('vps', req.params.id, actorFromRequest(req))
|
||||||
return reply.code(204).send()
|
return reply.code(204).send()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,138 @@
|
|||||||
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
import { closeDb } from '@cfdm/db'
|
||||||
|
import { auditLogRepository } from '@cfdm/db/repositories/audit-log'
|
||||||
|
import { resetTestDb } from '@cfdm/db/test-setup'
|
||||||
|
import {
|
||||||
|
auditCreate,
|
||||||
|
loadAuditIngestConfig,
|
||||||
|
pushAuditToPortal,
|
||||||
|
} from './audit.js'
|
||||||
|
|
||||||
|
describe('audit dual-write', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
resetTestDb()
|
||||||
|
process.env.AUTH_PORTAL_URL = 'http://portal.test'
|
||||||
|
process.env.AUTH_AUDIT_INGEST_SECRET = 'test-ingest-secret'
|
||||||
|
process.env.NODE_ENV = 'test'
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.unstubAllGlobals()
|
||||||
|
closeDb()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('writes eventId and actorUserId locally', () => {
|
||||||
|
auditCreate(
|
||||||
|
'vps',
|
||||||
|
'vps-1',
|
||||||
|
{ ip: '1.2.3.4' },
|
||||||
|
{
|
||||||
|
actorUserId: 'user-42',
|
||||||
|
actorEmail: '[email protected]',
|
||||||
|
actorName: 'User',
|
||||||
|
ip: '10.0.0.1',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
const rows = auditLogRepository.list(10)
|
||||||
|
expect(rows).toHaveLength(1)
|
||||||
|
expect(rows[0]?.entity).toBe('vps')
|
||||||
|
expect(rows[0]?.entityId).toBe('vps-1')
|
||||||
|
expect(rows[0]?.action).toBe('create')
|
||||||
|
expect(rows[0]?.actorUserId).toBe('user-42')
|
||||||
|
expect(rows[0]?.eventId).toMatch(
|
||||||
|
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('fire-and-forgets portal ingest with vps action key', async () => {
|
||||||
|
const fetchMock = vi.fn(async () => new Response(JSON.stringify({ accepted: 1, duplicates: 0 })))
|
||||||
|
vi.stubGlobal('fetch', fetchMock)
|
||||||
|
|
||||||
|
auditCreate(
|
||||||
|
'vps',
|
||||||
|
'vps-2',
|
||||||
|
{ status: 'active' },
|
||||||
|
{
|
||||||
|
actorUserId: 'user-7',
|
||||||
|
actorEmail: '[email protected]',
|
||||||
|
actorName: 'Actor',
|
||||||
|
ip: null,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
await vi.waitFor(() => expect(fetchMock).toHaveBeenCalledTimes(1))
|
||||||
|
|
||||||
|
const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit]
|
||||||
|
expect(url).toBe('http://portal.test/api/v1/ingest/audit')
|
||||||
|
expect(init.method).toBe('POST')
|
||||||
|
expect(init.headers).toMatchObject({
|
||||||
|
Authorization: 'Bearer test-ingest-secret',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
})
|
||||||
|
|
||||||
|
const body = JSON.parse(String(init.body)) as {
|
||||||
|
events: Array<{
|
||||||
|
event_id: string
|
||||||
|
source_app: string
|
||||||
|
action: string
|
||||||
|
actor_user_id: string
|
||||||
|
target_type: string
|
||||||
|
target_id: string
|
||||||
|
summary: string
|
||||||
|
}>
|
||||||
|
}
|
||||||
|
expect(body.events).toHaveLength(1)
|
||||||
|
expect(body.events[0]?.source_app).toBe('vps')
|
||||||
|
expect(body.events[0]?.action).toBe('vps.vps.create')
|
||||||
|
expect(body.events[0]?.actor_user_id).toBe('user-7')
|
||||||
|
expect(body.events[0]?.target_type).toBe('app_resource')
|
||||||
|
expect(body.events[0]?.target_id).toBe('vps-2')
|
||||||
|
expect(body.events[0]?.summary).toContain('VPS')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not throw when portal ingest fails', () => {
|
||||||
|
vi.stubGlobal(
|
||||||
|
'fetch',
|
||||||
|
vi.fn(async () => {
|
||||||
|
throw new Error('network down')
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(() =>
|
||||||
|
pushAuditToPortal({
|
||||||
|
eventId: 'evt-1',
|
||||||
|
entity: 'vps',
|
||||||
|
entityId: 'vps-3',
|
||||||
|
op: 'delete',
|
||||||
|
ctx: {
|
||||||
|
actorUserId: null,
|
||||||
|
actorEmail: null,
|
||||||
|
actorName: null,
|
||||||
|
ip: null,
|
||||||
|
},
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
}),
|
||||||
|
).not.toThrow()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('skips portal push when ingest secret is unset in production', () => {
|
||||||
|
process.env.NODE_ENV = 'production'
|
||||||
|
delete process.env.AUTH_AUDIT_INGEST_SECRET
|
||||||
|
|
||||||
|
const fetchMock = vi.fn()
|
||||||
|
vi.stubGlobal('fetch', fetchMock)
|
||||||
|
|
||||||
|
pushAuditToPortal({
|
||||||
|
eventId: 'evt-2',
|
||||||
|
entity: 'vps',
|
||||||
|
entityId: 'vps-4',
|
||||||
|
op: 'update',
|
||||||
|
diff: { ip: '9.9.9.9' },
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(fetchMock).not.toHaveBeenCalled()
|
||||||
|
expect(loadAuditIngestConfig().ingestSecret).toBeUndefined()
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -1,13 +1,160 @@
|
|||||||
|
import { randomUUID } from 'node:crypto'
|
||||||
import { auditLogRepository } from '@cfdm/db/repositories/audit-log'
|
import { auditLogRepository } from '@cfdm/db/repositories/audit-log'
|
||||||
|
import type { AuditActorContext } from '../lib/audit-actor.js'
|
||||||
|
|
||||||
export function auditCreate(entity: string, entityId: string, data?: Record<string, unknown>): void {
|
type AuditOp = 'create' | 'update' | 'delete'
|
||||||
auditLogRepository.append({ entity, entityId, action: 'create', diff: data })
|
|
||||||
|
export type AuditIngestConfig = {
|
||||||
|
portalUrl: string
|
||||||
|
ingestSecret: string | undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
export function auditUpdate(entity: string, entityId: string, patch: Record<string, unknown>): void {
|
export function loadAuditIngestConfig(
|
||||||
auditLogRepository.append({ entity, entityId, action: 'update', diff: patch })
|
env: NodeJS.ProcessEnv = process.env,
|
||||||
|
): AuditIngestConfig {
|
||||||
|
const isProd = env.NODE_ENV === 'production'
|
||||||
|
const portalUrl = (
|
||||||
|
env.AUTH_PORTAL_URL ??
|
||||||
|
env.VITE_AUTH_PORTAL_URL ??
|
||||||
|
'http://localhost:5175'
|
||||||
|
).replace(/\/$/, '')
|
||||||
|
const ingestSecret =
|
||||||
|
env.AUTH_AUDIT_INGEST_SECRET ??
|
||||||
|
(isProd ? undefined : 'dev-audit-ingest-secret')
|
||||||
|
return { portalUrl, ingestSecret }
|
||||||
}
|
}
|
||||||
|
|
||||||
export function auditDelete(entity: string, entityId: string): void {
|
const ENTITY_LABELS: Record<string, string> = {
|
||||||
auditLogRepository.append({ entity, entityId, action: 'delete' })
|
vps: 'VPS',
|
||||||
|
payment: 'Платёж',
|
||||||
|
providerAccount: 'Аккаунт',
|
||||||
|
provider: 'Хостер',
|
||||||
|
settings: 'Настройки',
|
||||||
|
balanceLedger: 'Баланс',
|
||||||
|
serverProject: 'Проект',
|
||||||
|
}
|
||||||
|
|
||||||
|
const ACTION_LABELS: Record<AuditOp, string> = {
|
||||||
|
create: 'создание',
|
||||||
|
update: 'изменение',
|
||||||
|
delete: 'удаление',
|
||||||
|
}
|
||||||
|
|
||||||
|
function auditSummary(entity: string, op: AuditOp, entityId: string): string {
|
||||||
|
const label = ENTITY_LABELS[entity] ?? entity
|
||||||
|
const verb = ACTION_LABELS[op]
|
||||||
|
return `${label}: ${verb} (${entityId})`
|
||||||
|
}
|
||||||
|
|
||||||
|
function portalAction(entity: string, op: AuditOp): string {
|
||||||
|
return `vps.${entity}.${op}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function recordAudit(
|
||||||
|
entity: string,
|
||||||
|
entityId: string,
|
||||||
|
op: AuditOp,
|
||||||
|
diff: Record<string, unknown> | undefined,
|
||||||
|
ctx?: AuditActorContext,
|
||||||
|
): void {
|
||||||
|
const eventId = randomUUID()
|
||||||
|
const createdAt = new Date().toISOString()
|
||||||
|
const actorUserId = ctx?.actorUserId ?? null
|
||||||
|
|
||||||
|
try {
|
||||||
|
auditLogRepository.append({
|
||||||
|
eventId,
|
||||||
|
entity,
|
||||||
|
entityId,
|
||||||
|
action: op,
|
||||||
|
diff,
|
||||||
|
actorUserId,
|
||||||
|
createdAt,
|
||||||
|
})
|
||||||
|
} catch {
|
||||||
|
// Local audit must not break CRUD; swallow repository errors too.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
pushAuditToPortal({
|
||||||
|
eventId,
|
||||||
|
entity,
|
||||||
|
entityId,
|
||||||
|
op,
|
||||||
|
diff,
|
||||||
|
ctx,
|
||||||
|
createdAt,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type PortalPushInput = {
|
||||||
|
eventId: string
|
||||||
|
entity: string
|
||||||
|
entityId: string
|
||||||
|
op: AuditOp
|
||||||
|
diff?: Record<string, unknown>
|
||||||
|
ctx?: AuditActorContext
|
||||||
|
createdAt: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function pushAuditToPortal(input: PortalPushInput): void {
|
||||||
|
const config = loadAuditIngestConfig()
|
||||||
|
if (!config.ingestSecret) return
|
||||||
|
|
||||||
|
const body = {
|
||||||
|
events: [
|
||||||
|
{
|
||||||
|
event_id: input.eventId,
|
||||||
|
source_app: 'vps' as const,
|
||||||
|
action: portalAction(input.entity, input.op),
|
||||||
|
severity: 'info' as const,
|
||||||
|
actor_user_id: input.ctx?.actorUserId ?? null,
|
||||||
|
actor_email: input.ctx?.actorEmail ?? null,
|
||||||
|
actor_name: input.ctx?.actorName ?? null,
|
||||||
|
target_type: 'app_resource' as const,
|
||||||
|
target_id: input.entityId,
|
||||||
|
summary: auditSummary(input.entity, input.op, input.entityId),
|
||||||
|
details: input.diff ?? null,
|
||||||
|
ip: input.ctx?.ip ?? null,
|
||||||
|
created_at: input.createdAt,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
void fetch(`${config.portalUrl}/api/v1/ingest/audit`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${config.ingestSecret}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
}).catch(() => {
|
||||||
|
// Fire-and-forget: portal ingest failures must not affect CRUD.
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function auditCreate(
|
||||||
|
entity: string,
|
||||||
|
entityId: string,
|
||||||
|
data?: Record<string, unknown>,
|
||||||
|
ctx?: AuditActorContext,
|
||||||
|
): void {
|
||||||
|
recordAudit(entity, entityId, 'create', data, ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function auditUpdate(
|
||||||
|
entity: string,
|
||||||
|
entityId: string,
|
||||||
|
patch: Record<string, unknown>,
|
||||||
|
ctx?: AuditActorContext,
|
||||||
|
): void {
|
||||||
|
recordAudit(entity, entityId, 'update', patch, ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function auditDelete(
|
||||||
|
entity: string,
|
||||||
|
entityId: string,
|
||||||
|
ctx?: AuditActorContext,
|
||||||
|
): void {
|
||||||
|
recordAudit(entity, entityId, 'delete', undefined, ctx)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,11 +4,13 @@ import { getDb, schema } from '../index.js'
|
|||||||
import { getCurrentSpaceId } from '../space-context.js'
|
import { getCurrentSpaceId } from '../space-context.js'
|
||||||
|
|
||||||
export interface AuditEntryInput {
|
export interface AuditEntryInput {
|
||||||
|
eventId: string
|
||||||
entity: string
|
entity: string
|
||||||
entityId: string
|
entityId: string
|
||||||
action: 'create' | 'update' | 'delete'
|
action: 'create' | 'update' | 'delete'
|
||||||
diff?: Record<string, unknown>
|
diff?: Record<string, unknown>
|
||||||
actorUserId?: string | null
|
actorUserId?: string | null
|
||||||
|
createdAt?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseDiff(row: { diff: string | null }) {
|
function parseDiff(row: { diff: string | null }) {
|
||||||
@@ -26,13 +28,14 @@ export const auditLogRepository = {
|
|||||||
.insert(schema.auditLog)
|
.insert(schema.auditLog)
|
||||||
.values({
|
.values({
|
||||||
id: `audit-${randomUUID()}`,
|
id: `audit-${randomUUID()}`,
|
||||||
|
eventId: input.eventId,
|
||||||
spaceId: getCurrentSpaceId(),
|
spaceId: getCurrentSpaceId(),
|
||||||
entity: input.entity,
|
entity: input.entity,
|
||||||
entityId: input.entityId,
|
entityId: input.entityId,
|
||||||
action: input.action,
|
action: input.action,
|
||||||
diff: input.diff ? JSON.stringify(input.diff) : null,
|
diff: input.diff ? JSON.stringify(input.diff) : null,
|
||||||
actorUserId: input.actorUserId ?? null,
|
actorUserId: input.actorUserId ?? null,
|
||||||
createdAt: new Date().toISOString(),
|
createdAt: input.createdAt ?? new Date().toISOString(),
|
||||||
})
|
})
|
||||||
.run()
|
.run()
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -220,6 +220,7 @@ const CORE_TABLE_MIGRATIONS: string[] = [
|
|||||||
)`,
|
)`,
|
||||||
`CREATE TABLE IF NOT EXISTS audit_log (
|
`CREATE TABLE IF NOT EXISTS audit_log (
|
||||||
id TEXT PRIMARY KEY,
|
id TEXT PRIMARY KEY,
|
||||||
|
eventId TEXT,
|
||||||
spaceId TEXT NOT NULL DEFAULT 'space-main',
|
spaceId TEXT NOT NULL DEFAULT 'space-main',
|
||||||
entity TEXT NOT NULL,
|
entity TEXT NOT NULL,
|
||||||
entityId TEXT NOT NULL,
|
entityId TEXT NOT NULL,
|
||||||
@@ -325,6 +326,7 @@ const COLUMN_MIGRATIONS: string[] = [
|
|||||||
`ALTER TABLE vps_health_checks ADD COLUMN spaceId TEXT`,
|
`ALTER TABLE vps_health_checks ADD COLUMN spaceId TEXT`,
|
||||||
`ALTER TABLE audit_log ADD COLUMN spaceId TEXT`,
|
`ALTER TABLE audit_log ADD COLUMN spaceId TEXT`,
|
||||||
`ALTER TABLE audit_log ADD COLUMN actorUserId TEXT`,
|
`ALTER TABLE audit_log ADD COLUMN actorUserId TEXT`,
|
||||||
|
`ALTER TABLE audit_log ADD COLUMN eventId TEXT`,
|
||||||
`ALTER TABLE spaces ADD COLUMN deletedAt TEXT`,
|
`ALTER TABLE spaces ADD COLUMN deletedAt TEXT`,
|
||||||
`ALTER TABLE sync_log ADD COLUMN spaceId TEXT`,
|
`ALTER TABLE sync_log ADD COLUMN spaceId TEXT`,
|
||||||
`ALTER TABLE sync_log ADD COLUMN summary TEXT`,
|
`ALTER TABLE sync_log ADD COLUMN summary TEXT`,
|
||||||
|
|||||||
@@ -275,6 +275,7 @@ export const vpsHealthChecks = sqliteTable('vps_health_checks', {
|
|||||||
|
|
||||||
export const auditLog = sqliteTable('audit_log', {
|
export const auditLog = sqliteTable('audit_log', {
|
||||||
id: text('id').primaryKey(),
|
id: text('id').primaryKey(),
|
||||||
|
eventId: text('eventId'),
|
||||||
spaceId: text('spaceId')
|
spaceId: text('spaceId')
|
||||||
.notNull()
|
.notNull()
|
||||||
.default('space-main')
|
.default('space-main')
|
||||||
|
|||||||
@@ -264,6 +264,7 @@ CREATE TABLE IF NOT EXISTS server_projects (
|
|||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS audit_log (
|
CREATE TABLE IF NOT EXISTS audit_log (
|
||||||
id TEXT PRIMARY KEY,
|
id TEXT PRIMARY KEY,
|
||||||
|
eventId TEXT,
|
||||||
spaceId TEXT NOT NULL DEFAULT 'space-main',
|
spaceId TEXT NOT NULL DEFAULT 'space-main',
|
||||||
entity TEXT NOT NULL,
|
entity TEXT NOT NULL,
|
||||||
entityId TEXT NOT NULL,
|
entityId TEXT NOT NULL,
|
||||||
|
|||||||
Reference in New Issue
Block a user