192 lines
7.4 KiB
Python
192 lines
7.4 KiB
Python
import logging
|
|
from contextlib import asynccontextmanager
|
|
|
|
from fastapi import FastAPI
|
|
from fastapi.staticfiles import StaticFiles
|
|
|
|
from app.core.config import get_settings
|
|
from app.db.pool import close_pool, get_pool, init_pool
|
|
from app.routers.auth import router as auth_router
|
|
from app.routers.domain import router as domain_router
|
|
from app.routers.ui import router as ui_router
|
|
from app.routers.ws import router as ws_router
|
|
from app.services.jsonlint_runtime import refresh_jsonlint_config_cache
|
|
from app.services.yamllint_runtime import refresh_yamllint_config_cache
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
|
|
@asynccontextmanager
|
|
async def lifespan(_: FastAPI):
|
|
await init_pool()
|
|
pool = get_pool()
|
|
async with pool.acquire() as conn:
|
|
await conn.execute("alter table users add column if not exists username text")
|
|
await conn.execute("alter table users add column if not exists role text not null default 'user'")
|
|
await conn.execute(
|
|
"""
|
|
update users
|
|
set username = split_part(email, '@', 1) || '_' || substr(id::text, 1, 8)
|
|
where username is null or username = ''
|
|
"""
|
|
)
|
|
await conn.execute("create unique index if not exists users_username_key on users(username)")
|
|
await conn.execute(
|
|
"""
|
|
with first_user as (
|
|
select id
|
|
from users
|
|
order by created_at asc
|
|
limit 1
|
|
)
|
|
update users
|
|
set role='admin'
|
|
where id in (select id from first_user)
|
|
"""
|
|
)
|
|
await conn.execute(
|
|
"""
|
|
create table if not exists role_categories (
|
|
id uuid primary key default gen_random_uuid(),
|
|
owner_id uuid references users(id) on delete cascade,
|
|
name text not null,
|
|
created_at timestamptz not null default now(),
|
|
unique (owner_id, name)
|
|
)
|
|
"""
|
|
)
|
|
await conn.execute("alter table role_categories alter column owner_id drop not null")
|
|
await conn.execute("create table if not exists app_config (key text primary key, value jsonb not null default '{}'::jsonb, updated_at timestamptz not null default now())")
|
|
await conn.execute(
|
|
"""
|
|
create table if not exists role_files (
|
|
id uuid primary key default gen_random_uuid(),
|
|
role_id uuid not null references ansible_roles(id) on delete cascade,
|
|
path text not null,
|
|
content text not null default '',
|
|
created_at timestamptz not null default now(),
|
|
updated_at timestamptz not null default now(),
|
|
unique (role_id, path)
|
|
)
|
|
"""
|
|
)
|
|
await conn.execute(
|
|
"""
|
|
do $$
|
|
begin
|
|
if exists (
|
|
select 1
|
|
from information_schema.tables
|
|
where table_schema='public' and table_name='ansible_roles'
|
|
) then
|
|
alter table ansible_roles add column if not exists category_id uuid;
|
|
end if;
|
|
end $$;
|
|
"""
|
|
)
|
|
await conn.execute(
|
|
"""
|
|
do $$
|
|
begin
|
|
if exists (
|
|
select 1 from information_schema.tables
|
|
where table_schema='public' and table_name='ansible_roles'
|
|
) then
|
|
-- Remove accidental starter-role duplicates before adding unique partial index.
|
|
delete from ansible_roles r
|
|
using (
|
|
select id
|
|
from (
|
|
select
|
|
id,
|
|
row_number() over (
|
|
partition by owner_id, name, source_ref
|
|
order by created_at asc, id asc
|
|
) as rn
|
|
from ansible_roles
|
|
where source_ref = 'starter'
|
|
) t
|
|
where t.rn > 1
|
|
) d
|
|
where r.id = d.id;
|
|
|
|
create unique index if not exists ux_ansible_roles_owner_name_starter
|
|
on ansible_roles (owner_id, name)
|
|
where source_ref = 'starter';
|
|
end if;
|
|
end $$;
|
|
"""
|
|
)
|
|
await conn.execute(
|
|
"""
|
|
do $$
|
|
begin
|
|
if not exists (
|
|
select 1
|
|
from pg_constraint
|
|
where conname = 'ansible_roles_category_id_fkey'
|
|
) then
|
|
alter table ansible_roles
|
|
add constraint ansible_roles_category_id_fkey
|
|
foreign key (category_id) references role_categories(id) on delete set null;
|
|
end if;
|
|
end $$;
|
|
"""
|
|
)
|
|
await conn.execute(
|
|
"""
|
|
do $$
|
|
begin
|
|
if exists (
|
|
select 1 from information_schema.tables
|
|
where table_schema='public' and table_name='ansible_roles'
|
|
) then
|
|
alter table ansible_roles add column if not exists visibility text not null default 'public';
|
|
alter table ansible_roles add column if not exists team_id uuid;
|
|
alter table ansible_roles add column if not exists forked_from_id uuid;
|
|
if not exists (
|
|
select 1 from pg_constraint where conname = 'ansible_roles_forked_from_id_fkey'
|
|
) then
|
|
alter table ansible_roles
|
|
add constraint ansible_roles_forked_from_id_fkey
|
|
foreign key (forked_from_id) references ansible_roles(id) on delete set null;
|
|
end if;
|
|
update ansible_roles set visibility = 'public' where visibility is null or visibility = '';
|
|
if not exists (
|
|
select 1 from pg_constraint where conname = 'ansible_roles_visibility_check'
|
|
) then
|
|
alter table ansible_roles
|
|
add constraint ansible_roles_visibility_check
|
|
check (visibility in ('public', 'team', 'personal'));
|
|
end if;
|
|
end if;
|
|
end $$;
|
|
"""
|
|
)
|
|
# Warm lint caches outside the migration connection so nested pool.acquire() cannot deadlock.
|
|
try:
|
|
await refresh_yamllint_config_cache()
|
|
await refresh_jsonlint_config_cache()
|
|
except Exception:
|
|
_logger.exception(
|
|
"Lint config cache warmup failed (app still starts; caches load on first use)."
|
|
)
|
|
yield
|
|
await close_pool()
|
|
|
|
|
|
settings = get_settings()
|
|
app = FastAPI(title=settings.app_name, lifespan=lifespan)
|
|
|
|
app.mount("/static", StaticFiles(directory="app/static"), name="static")
|
|
app.include_router(ui_router)
|
|
app.include_router(auth_router)
|
|
app.include_router(auth_router, prefix="/api/v1")
|
|
app.include_router(domain_router, prefix="/api/v1")
|
|
app.include_router(ws_router)
|
|
|
|
|
|
@app.get("/healthz", tags=["health"])
|
|
async def healthz() -> dict[str, str]:
|
|
return {"status": "ok"}
|