feat: v7.3.0 — cycle v6.8.0→v7.3.0 (Sites, Search, Automations, Calendar, SCIM, Wiki) + audit A9
- v6.8.0 Sites & Forms publics (migrations 24)
- v6.9.0 Recherche sémantique hybride + Ask AI (migration 25)
- v7.0.0 Automations v2 multi-étapes + Workers sandboxés (migration 26)
- v7.1.0 Calendar sync Google/CalDAV + Meeting Notes (migration 27)
- v7.2.0 Enterprise : SCIM 2.0, 2FA TOTP/passkeys, audit UI, agent approvals (migration 28)
- v7.3.0 Wiki/Teamspaces, verified pages, collab polish, charts, unfurl (migration 29)
- docs V68→V73, ROADMAP/CHANGELOG/WORKLOAD à jour, VERSION 7.3.0
- A9 : flowdeck.db, flowdeck_dev.db, test-commit.md, upload_test.txt et e2e/{node_modules,shots,test-results} désindexés + ignorés (.gitignore/.dockerignore)
This commit is contained in:
@@ -985,6 +985,459 @@ def _migration_row_content_pages(conn: sqlite3.Connection) -> None:
|
||||
)
|
||||
|
||||
|
||||
@register(24, "v6.8.0: Sites & public Forms")
|
||||
def _migration_sites_forms(conn: sqlite3.Connection) -> None:
|
||||
"""v6.8.0 — Notion Sites + Forms publics (voir docs/V68_Sites_Forms.md).
|
||||
|
||||
``sites`` — mini-site multi-pages (slug, root_page, thème,
|
||||
domaine custom, password hash, expiry, noindex).
|
||||
``site_pages`` — arbre public ordonné (site_id, page_id, position).
|
||||
``site_views`` — compteur de vues jour/site (upsert, pas d'IP brute).
|
||||
``form_responses`` — log des soumissions anonymes (ip_hash jour, pas d'IP).
|
||||
``collections.form_config_json`` — config du formulaire public par DB.
|
||||
"""
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS sites (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
slug TEXT NOT NULL UNIQUE,
|
||||
root_page_id INTEGER NOT NULL REFERENCES pages(id) ON DELETE CASCADE,
|
||||
title TEXT NOT NULL DEFAULT '',
|
||||
theme TEXT NOT NULL DEFAULT 'dark',
|
||||
custom_domain TEXT UNIQUE,
|
||||
password_hash TEXT DEFAULT '',
|
||||
expires_at TIMESTAMP,
|
||||
noindex INTEGER NOT NULL DEFAULT 0,
|
||||
analytics_id TEXT DEFAULT '',
|
||||
created_by INTEGER REFERENCES users(id),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
"""
|
||||
)
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS site_pages (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
site_id INTEGER NOT NULL REFERENCES sites(id) ON DELETE CASCADE,
|
||||
page_id INTEGER NOT NULL REFERENCES pages(id) ON DELETE CASCADE,
|
||||
position INTEGER NOT NULL DEFAULT 0,
|
||||
UNIQUE(site_id, page_id)
|
||||
)
|
||||
"""
|
||||
)
|
||||
conn.execute(
|
||||
"CREATE INDEX IF NOT EXISTS idx_site_pages_site ON site_pages(site_id, position)"
|
||||
)
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS site_views (
|
||||
site_id INTEGER NOT NULL REFERENCES sites(id) ON DELETE CASCADE,
|
||||
day TEXT NOT NULL,
|
||||
views INTEGER NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (site_id, day)
|
||||
)
|
||||
"""
|
||||
)
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS form_responses (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
collection_id INTEGER NOT NULL REFERENCES collections(id) ON DELETE CASCADE,
|
||||
row_id INTEGER REFERENCES collection_pages(id) ON DELETE SET NULL,
|
||||
ip_hash TEXT NOT NULL DEFAULT '',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
"""
|
||||
)
|
||||
conn.execute(
|
||||
"CREATE INDEX IF NOT EXISTS idx_form_responses_col ON form_responses(collection_id, created_at)"
|
||||
)
|
||||
cols = {r[1] for r in conn.execute("PRAGMA table_info(collections)").fetchall()}
|
||||
if "form_config_json" not in cols:
|
||||
conn.execute(
|
||||
"ALTER TABLE collections ADD COLUMN form_config_json TEXT NOT NULL DEFAULT '{}'"
|
||||
)
|
||||
|
||||
|
||||
@register(25, "v6.9.0: semantic search + Ask AI")
|
||||
def _migration_semantic_search(conn: sqlite3.Connection) -> None:
|
||||
"""v6.9.0 — hybrid lexical+vector search and RAG Ask AI (docs/V69_* md).
|
||||
|
||||
``semantic_embeddings`` — hashed-TF chunk vectors (no external dep):
|
||||
keyed by (resource_type, resource_id, chunk_id) so both ``page``
|
||||
and ``collection`` resources are indexed. (Design doc names a
|
||||
``page_embeddings`` table; the generic key covers collections too.)
|
||||
``semantic_index_state`` — last indexed timestamp per resource for the
|
||||
incremental background job.
|
||||
``pages.search_excluded`` — opt-out flag respected by indexer + search.
|
||||
"""
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS semantic_embeddings (
|
||||
resource_type TEXT NOT NULL,
|
||||
resource_id INTEGER NOT NULL,
|
||||
chunk_id INTEGER NOT NULL,
|
||||
chunk_text TEXT NOT NULL DEFAULT '',
|
||||
embedding BLOB NOT NULL,
|
||||
model TEXT NOT NULL DEFAULT 'hash-256',
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (resource_type, resource_id, chunk_id)
|
||||
)
|
||||
"""
|
||||
)
|
||||
conn.execute(
|
||||
"CREATE INDEX IF NOT EXISTS idx_sem_emb_res "
|
||||
"ON semantic_embeddings(resource_type, resource_id)"
|
||||
)
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS semantic_index_state (
|
||||
resource_type TEXT NOT NULL,
|
||||
resource_id INTEGER NOT NULL,
|
||||
indexed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (resource_type, resource_id)
|
||||
)
|
||||
"""
|
||||
)
|
||||
cols = {r[1] for r in conn.execute("PRAGMA table_info(pages)").fetchall()}
|
||||
if "search_excluded" not in cols:
|
||||
conn.execute(
|
||||
"ALTER TABLE pages ADD COLUMN search_excluded INTEGER NOT NULL DEFAULT 0"
|
||||
)
|
||||
|
||||
|
||||
@register(26, "v7.0.0: automations v2 (steps) + workers")
|
||||
def _migration_automations_v2_workers(conn: sqlite3.Connection) -> None:
|
||||
"""v7.0.0 — multi-step automations + sandboxed workers (docs/V70_* md).
|
||||
|
||||
``automation_steps`` — ordered trigger/condition/delay/action chain per
|
||||
automation. Legacy single trigger+actions columns keep working
|
||||
(engine falls back when an automation has no steps).
|
||||
``automations.trigger_mode`` — ``any`` (default) or ``all`` (every
|
||||
trigger event must arrive within a 5-minute window).
|
||||
``workers`` / ``worker_runs`` — custom Python snippets (cron/manual),
|
||||
shareable across the team, with execution logs + daily budget.
|
||||
``collection_properties.button_automation_id`` — native DB button cells.
|
||||
"""
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS automation_steps (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
automation_id INTEGER NOT NULL REFERENCES automations(id) ON DELETE CASCADE,
|
||||
kind TEXT NOT NULL,
|
||||
position INTEGER NOT NULL DEFAULT 0,
|
||||
config_json TEXT NOT NULL DEFAULT '{}',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
"""
|
||||
)
|
||||
conn.execute(
|
||||
"CREATE INDEX IF NOT EXISTS idx_asteps_auto "
|
||||
"ON automation_steps(automation_id, position)"
|
||||
)
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS workers (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
slug TEXT NOT NULL UNIQUE,
|
||||
workspace_id INTEGER REFERENCES workspaces(id) ON DELETE CASCADE,
|
||||
name TEXT NOT NULL DEFAULT '',
|
||||
code_py TEXT NOT NULL DEFAULT '',
|
||||
schedule_cron TEXT DEFAULT '',
|
||||
shared INTEGER NOT NULL DEFAULT 0,
|
||||
daily_budget_s INTEGER NOT NULL DEFAULT 60,
|
||||
created_by INTEGER REFERENCES users(id),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
"""
|
||||
)
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS worker_runs (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
worker_id INTEGER NOT NULL REFERENCES workers(id) ON DELETE CASCADE,
|
||||
status TEXT NOT NULL,
|
||||
logs TEXT NOT NULL DEFAULT '',
|
||||
duration_ms INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
"""
|
||||
)
|
||||
conn.execute(
|
||||
"CREATE INDEX IF NOT EXISTS idx_worker_runs_worker "
|
||||
"ON worker_runs(worker_id, created_at)"
|
||||
)
|
||||
auto_cols = {r[1] for r in conn.execute("PRAGMA table_info(automations)").fetchall()}
|
||||
if "trigger_mode" not in auto_cols:
|
||||
conn.execute(
|
||||
"ALTER TABLE automations ADD COLUMN trigger_mode TEXT NOT NULL DEFAULT 'any'"
|
||||
)
|
||||
prop_cols = {r[1] for r in conn.execute("PRAGMA table_info(collection_properties)").fetchall()}
|
||||
if "button_automation_id" not in prop_cols:
|
||||
conn.execute(
|
||||
"ALTER TABLE collection_properties ADD COLUMN button_automation_id "
|
||||
"INTEGER REFERENCES automations(id) ON DELETE SET NULL"
|
||||
)
|
||||
|
||||
|
||||
@register(27, "v7.1.0: calendar sync + meeting transcripts")
|
||||
def _migration_calendar_meetings(conn: sqlite3.Connection) -> None:
|
||||
"""v7.1.0 — external calendar sync + AI meeting notes (docs/V71_* md).
|
||||
|
||||
``calendar_links`` — per-user link between a collection and an external
|
||||
calendar (google REST / generic caldav), tokens Fernet-encrypted.
|
||||
``meeting_transcripts`` — uploaded audio + transcript + AI summary per page.
|
||||
``collection_pages.external_event_id`` — remote event id for push/pull
|
||||
matching and conflict detection.
|
||||
"""
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS calendar_links (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
provider TEXT NOT NULL,
|
||||
tokens_enc TEXT NOT NULL DEFAULT '',
|
||||
calendar_id TEXT NOT NULL DEFAULT 'primary',
|
||||
collection_id INTEGER REFERENCES collections(id) ON DELETE CASCADE,
|
||||
date_property TEXT DEFAULT '',
|
||||
sync_token TEXT DEFAULT '',
|
||||
last_sync TIMESTAMP,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(user_id, provider, calendar_id)
|
||||
)
|
||||
"""
|
||||
)
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS meeting_transcripts (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
page_id INTEGER NOT NULL REFERENCES pages(id) ON DELETE CASCADE,
|
||||
audio_path TEXT NOT NULL DEFAULT '',
|
||||
transcript TEXT NOT NULL DEFAULT '',
|
||||
summary TEXT NOT NULL DEFAULT '',
|
||||
language TEXT NOT NULL DEFAULT 'fr',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
"""
|
||||
)
|
||||
conn.execute(
|
||||
"CREATE INDEX IF NOT EXISTS idx_meeting_transcripts_page "
|
||||
"ON meeting_transcripts(page_id)"
|
||||
)
|
||||
cols = {r[1] for r in conn.execute("PRAGMA table_info(collection_pages)").fetchall()}
|
||||
if "external_event_id" not in cols:
|
||||
conn.execute(
|
||||
"ALTER TABLE collection_pages ADD COLUMN external_event_id TEXT DEFAULT ''"
|
||||
)
|
||||
conn.execute(
|
||||
"CREATE INDEX IF NOT EXISTS idx_cp_external "
|
||||
"ON collection_pages(collection_id, external_event_id)"
|
||||
)
|
||||
|
||||
|
||||
@register(28, "v7.2.0: SCIM + 2FA + audit UI + agent governance")
|
||||
def _migration_enterprise_admin(conn: sqlite3.Connection) -> None:
|
||||
"""v7.2.0 — enterprise admin (docs/V72_* md).
|
||||
|
||||
``scim_tokens`` — Bearer tokens for SCIM provisioning (admin-managed).
|
||||
``domain_claims`` — DNS/well-known verified domains + SSO enforcement.
|
||||
``webauthn_credentials`` — passkeys (credential_id, COSE public key).
|
||||
``agent_policies`` — per-workspace tool scope + approval gate.
|
||||
``agent_approvals`` — approval queue for gated write actions.
|
||||
``users.totp_secret_enc`` / ``totp_backup_hashes`` — TOTP 2FA.
|
||||
(``users.is_active`` already exists — used by SCIM suspend.)
|
||||
"""
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS scim_tokens (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
token_hash TEXT NOT NULL UNIQUE,
|
||||
name TEXT NOT NULL DEFAULT '',
|
||||
created_by INTEGER REFERENCES users(id),
|
||||
revoked INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
"""
|
||||
)
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS domain_claims (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
domain TEXT NOT NULL UNIQUE,
|
||||
txt_token TEXT NOT NULL DEFAULT '',
|
||||
verified INTEGER NOT NULL DEFAULT 0,
|
||||
auto_join_role TEXT NOT NULL DEFAULT 'viewer',
|
||||
enforce_sso INTEGER NOT NULL DEFAULT 0,
|
||||
workspace_id INTEGER REFERENCES workspaces(id) ON DELETE SET NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
"""
|
||||
)
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS webauthn_credentials (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
credential_id TEXT NOT NULL UNIQUE,
|
||||
public_key TEXT NOT NULL DEFAULT '',
|
||||
sign_count INTEGER NOT NULL DEFAULT 0,
|
||||
name TEXT NOT NULL DEFAULT '',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
"""
|
||||
)
|
||||
conn.execute(
|
||||
"CREATE INDEX IF NOT EXISTS idx_webauthn_user ON webauthn_credentials(user_id)"
|
||||
)
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS agent_policies (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
workspace_id INTEGER REFERENCES workspaces(id) ON DELETE CASCADE,
|
||||
allowed_tools_json TEXT,
|
||||
max_steps INTEGER NOT NULL DEFAULT 12,
|
||||
require_approval INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(workspace_id)
|
||||
)
|
||||
"""
|
||||
)
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS agent_approvals (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
conversation_id INTEGER NOT NULL DEFAULT 0,
|
||||
tool TEXT NOT NULL DEFAULT '',
|
||||
args_json TEXT NOT NULL DEFAULT '{}',
|
||||
status TEXT NOT NULL DEFAULT 'pending',
|
||||
requester_id INTEGER REFERENCES users(id),
|
||||
approver_id INTEGER REFERENCES users(id),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
"""
|
||||
)
|
||||
conn.execute(
|
||||
"CREATE INDEX IF NOT EXISTS idx_agent_approvals_status "
|
||||
"ON agent_approvals(status, created_at)"
|
||||
)
|
||||
user_cols = {r[1] for r in conn.execute("PRAGMA table_info(users)").fetchall()}
|
||||
if "totp_secret_enc" not in user_cols:
|
||||
conn.execute("ALTER TABLE users ADD COLUMN totp_secret_enc TEXT DEFAULT ''")
|
||||
if "totp_backup_hashes" not in user_cols:
|
||||
conn.execute("ALTER TABLE users ADD COLUMN totp_backup_hashes TEXT DEFAULT '[]'")
|
||||
|
||||
|
||||
@register(29, "v7.3.0: teamspaces + verified pages + collab polish")
|
||||
def _migration_wiki_teamspaces(conn: sqlite3.Connection) -> None:
|
||||
"""v7.3.0 — teamspaces, verified pages, collab polish (docs/V73_*.md).
|
||||
|
||||
``teamspaces`` / ``teamspace_members`` — namespaces for pages + databases;
|
||||
``private=1`` hides a teamspace from non-members (404, like restricted
|
||||
collections). ``page_verifications`` — ✅ badge with expiry.
|
||||
``comment_reactions`` / ``page_follows`` — collab polish.
|
||||
``guest_shares`` — account-less page access via ``/g/<token>``.
|
||||
``page_views`` — daily counters, same pattern as ``site_views``.
|
||||
"""
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS teamspaces (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
workspace_id INTEGER NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT DEFAULT '',
|
||||
private INTEGER NOT NULL DEFAULT 0,
|
||||
created_by INTEGER REFERENCES users(id),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(workspace_id, name)
|
||||
)
|
||||
"""
|
||||
)
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS teamspace_members (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
teamspace_id INTEGER NOT NULL REFERENCES teamspaces(id) ON DELETE CASCADE,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
role TEXT NOT NULL DEFAULT 'editor',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(teamspace_id, user_id)
|
||||
)
|
||||
"""
|
||||
)
|
||||
conn.execute(
|
||||
"CREATE INDEX IF NOT EXISTS idx_teamspace_members_user "
|
||||
"ON teamspace_members(user_id)"
|
||||
)
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS page_verifications (
|
||||
page_id INTEGER PRIMARY KEY REFERENCES pages(id) ON DELETE CASCADE,
|
||||
verified_by INTEGER REFERENCES users(id),
|
||||
note TEXT NOT NULL DEFAULT '',
|
||||
verified_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
expires_at TIMESTAMP
|
||||
)
|
||||
"""
|
||||
)
|
||||
conn.execute(
|
||||
"CREATE INDEX IF NOT EXISTS idx_page_verifications_expiry "
|
||||
"ON page_verifications(expires_at)"
|
||||
)
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS comment_reactions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
comment_id INTEGER NOT NULL REFERENCES comments(id) ON DELETE CASCADE,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
emoji TEXT NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(comment_id, user_id, emoji)
|
||||
)
|
||||
"""
|
||||
)
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS page_follows (
|
||||
page_id INTEGER NOT NULL REFERENCES pages(id) ON DELETE CASCADE,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (page_id, user_id)
|
||||
)
|
||||
"""
|
||||
)
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS guest_shares (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
page_id INTEGER NOT NULL REFERENCES pages(id) ON DELETE CASCADE,
|
||||
email TEXT NOT NULL DEFAULT '',
|
||||
token TEXT NOT NULL UNIQUE,
|
||||
role TEXT NOT NULL DEFAULT 'viewer',
|
||||
created_by INTEGER REFERENCES users(id),
|
||||
expires_at TIMESTAMP,
|
||||
revoked INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
"""
|
||||
)
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS page_views (
|
||||
page_id INTEGER NOT NULL REFERENCES pages(id) ON DELETE CASCADE,
|
||||
day TEXT NOT NULL,
|
||||
views INTEGER NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (page_id, day)
|
||||
)
|
||||
"""
|
||||
)
|
||||
for table in ("pages", "collections"):
|
||||
cols = {r[1] for r in conn.execute(f"PRAGMA table_info({table})").fetchall()}
|
||||
if "teamspace_id" not in cols:
|
||||
conn.execute(f"ALTER TABLE {table} ADD COLUMN teamspace_id INTEGER")
|
||||
|
||||
|
||||
@register(23, "v6.7.0: SSO/SAML enterprise auth")
|
||||
def _migration_sso_enterprise_auth(conn: sqlite3.Connection) -> None:
|
||||
"""v6.7.0 — SSO/SAML 2.0 + OIDC enterprise authentication.
|
||||
|
||||
Reference in New Issue
Block a user