Files
RoleForge/app/main.py
Sergey Antropoff b2d3b6b803 Профиль и аккаунт
- API и страницы профиля (редактирование, смена пароля, аватар), публичные карточки.
- Сайдбар: блок пользователя, пункт Users для admin/root, исправлен порядок
  инициализации (показ admin-only после initAuthSession, currentUser).
- GET /auth/me: ответ через ProfileMeResponse, исправлена валидация (is_founder bool).

Команды и роли
- Маршруты и UI команд; при редактировании роли: видимость Team, выбор команды
  в модалке, только команды с активным членством; API team_id в details/ update.
- GET /api/v1/teams?membership=active для списка «своих» команд.
- Форма роли: сегмент Team, панель выбора команды только при Team и не при
  с
2026-05-05 08:15:21 +03:00

382 lines
16 KiB
Python

import logging
from contextlib import asynccontextmanager
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
from pathlib import Path
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.admin_users import router as admin_users_router
from app.routers.domain import router as domain_router
from app.routers.profile import router as profile_router
from app.routers.teams import router as teams_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 'personal';
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;
-- Remove auto-generated starter roles (no longer created for new users).
delete from ansible_roles where source_ref = 'starter';
alter table ansible_roles alter column visibility set default 'personal';
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 role_tags text[] not null default array[]::text[];
alter table ansible_roles
add column if not exists os_families text[] not null default array[]::text[];
end if;
end $$;
"""
)
await conn.execute(
"""
do $$
begin
if exists (
select 1 from information_schema.tables
where table_schema = 'public' and table_name = 'role_categories'
) then
-- Duplicate rows with the same normalized name were possible because UNIQUE(owner_id, name)
-- treats NULL owner_id as distinct per row. Point roles at the canonical category
-- (earliest created_at, then smallest id) and remove extras.
update ansible_roles ar
set category_id = canon.canon_id
from (
select c.id as dup_id,
(
select c2.id
from role_categories c2
where lower(trim(c2.name)) = lower(trim(c.name))
order by c2.created_at asc nulls last, c2.id asc
limit 1
) as canon_id
from role_categories c
) canon
where ar.category_id = canon.dup_id
and canon.canon_id is not null
and canon.dup_id <> canon.canon_id;
delete from role_categories d
where d.id in (
select c.id
from role_categories c
where c.id <> (
select c2.id
from role_categories c2
where lower(trim(c2.name)) = lower(trim(c.name))
order by c2.created_at asc nulls last, c2.id asc
limit 1
)
);
create unique index if not exists ux_role_categories_name_normalized
on role_categories (lower(trim(name)));
end if;
end $$;
"""
)
await conn.execute(
"""
alter table users add column if not exists profile_bio text not null default '';
alter table users add column if not exists avatar_ext text;
alter table users add column if not exists avatar_updated_at timestamptz;
"""
)
await conn.execute(
"""
alter table users add column if not exists is_active boolean not null default true;
alter table users add column if not exists deleted_at timestamptz;
alter table users add column if not exists updated_at timestamptz;
update users set updated_at = created_at where updated_at is null;
alter table users alter column updated_at set default now();
alter table users alter column updated_at set not null;
"""
)
await conn.execute(
"""
alter table users add column if not exists ban_until timestamptz;
alter table users add column if not exists ban_reason text;
alter table users add column if not exists banned_by_id uuid;
"""
)
await conn.execute(
"""
do $$
begin
if not exists (
select 1 from pg_constraint where conname = 'users_banned_by_id_fkey'
) then
alter table users
add constraint users_banned_by_id_fkey
foreign key (banned_by_id) references users(id) on delete set null;
end if;
end $$;
"""
)
await conn.execute(
"""
alter table users add column if not exists is_founder boolean not null default false;
"""
)
await conn.execute(
"""
update users set is_founder = true
where id = (select id from users order by created_at asc limit 1);
"""
)
await conn.execute(
"""
update users set role = 'root' where role = 'super_admin';
"""
)
await conn.execute(
"""
update users u
set role = 'root'
where u.id = (select id from users order by created_at asc limit 1)
and not exists (select 1 from users u2 where u2.role = 'root');
"""
)
await conn.execute(
"""
create table if not exists teams (
id uuid primary key default gen_random_uuid(),
name text not null,
description text not null default '',
created_by uuid not null references users(id) on delete restrict,
created_at timestamptz not null default now()
);
"""
)
await conn.execute(
"""
create table if not exists team_memberships (
team_id uuid not null references teams(id) on delete cascade,
user_id uuid not null references users(id) on delete cascade,
team_role text not null default 'member',
status text not null default 'active',
invited_by uuid references users(id) on delete set null,
created_at timestamptz not null default now(),
primary key (team_id, user_id),
constraint team_memberships_team_role_check check (team_role in ('owner', 'member')),
constraint team_memberships_status_check check (status in ('active', 'pending_join', 'invited'))
);
"""
)
await conn.execute(
"""
create index if not exists ix_team_memberships_user_id on team_memberships(user_id);
"""
)
await conn.execute(
"""
do $$
begin
if exists (
select 1 from information_schema.columns
where table_schema = 'public' and table_name = 'ansible_roles' and column_name = 'team_id'
) and not exists (
select 1 from pg_constraint where conname = 'ansible_roles_team_id_fkey'
) then
alter table ansible_roles
add constraint ansible_roles_team_id_fkey
foreign key (team_id) references teams(id) on delete set null;
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)
_path_static = Path(__file__).resolve().parent / "static"
_app_data = Path(settings.app_data_dir)
_app_data.mkdir(parents=True, exist_ok=True)
_avatar_dir = settings.avatar_storage_path
_avatar_dir.mkdir(parents=True, exist_ok=True)
app.mount("/static", StaticFiles(directory=str(_path_static)), name="static")
app.mount("/media/avatars", StaticFiles(directory=str(_avatar_dir)), name="avatars_media")
app.include_router(ui_router)
app.include_router(auth_router)
app.include_router(auth_router, prefix="/api/v1")
app.include_router(profile_router, prefix="/api/v1")
app.include_router(admin_users_router, prefix="/api/v1")
app.include_router(teams_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"}