Files
RoleForge/app/main.py

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"}