update
This commit is contained in:
@@ -12,10 +12,10 @@ from sqlalchemy.orm import Session
|
||||
from backend.db import get_db
|
||||
from backend.models import User
|
||||
|
||||
pwd_context = CryptContext(schemes=["argon2"], deprecated="auto")
|
||||
pwd_context = CryptContext(schemes=["argon2", "bcrypt"], deprecated="auto")
|
||||
bearer = HTTPBearer(auto_error=False)
|
||||
|
||||
AUTH_VERSION = "argon2-2026-04-07"
|
||||
AUTH_VERSION = "argon2-bcrypt-compat-2026-04-07"
|
||||
|
||||
|
||||
def _env(name: str, default: str) -> str:
|
||||
@@ -48,6 +48,10 @@ def verify_password(password: str, password_hash: str) -> bool:
|
||||
) from e
|
||||
|
||||
|
||||
def password_needs_update(password_hash: str) -> bool:
|
||||
return pwd_context.needs_update(password_hash)
|
||||
|
||||
|
||||
def create_access_token(*, user_id: str) -> str:
|
||||
now = datetime.now(timezone.utc)
|
||||
payload = {
|
||||
|
||||
@@ -27,6 +27,7 @@ from backend.auth import ( # noqa: E402
|
||||
create_access_token,
|
||||
get_current_user,
|
||||
hash_password,
|
||||
password_needs_update,
|
||||
verify_password,
|
||||
)
|
||||
from backend.db import engine, get_db # noqa: E402
|
||||
@@ -130,6 +131,11 @@ def login(req: LoginRequest, db: Session = Depends(get_db)):
|
||||
if not user.is_active:
|
||||
raise HTTPException(status_code=403, detail="User is inactive")
|
||||
|
||||
if password_needs_update(user.password_hash):
|
||||
user.password_hash = hash_password(req.password)
|
||||
db.add(user)
|
||||
db.commit()
|
||||
|
||||
token = create_access_token(user_id=str(user.id))
|
||||
return {"access_token": token, "token_type": "bearer"}
|
||||
except HTTPException:
|
||||
|
||||
@@ -6,5 +6,7 @@ sqlalchemy
|
||||
psycopg[binary]
|
||||
passlib[argon2]
|
||||
argon2-cffi
|
||||
passlib[bcrypt]
|
||||
bcrypt
|
||||
python-jose[cryptography]
|
||||
pydantic[email]
|
||||
|
||||
104
frontend/ui.py
104
frontend/ui.py
@@ -4,10 +4,21 @@ from datetime import datetime, timezone
|
||||
import requests
|
||||
import streamlit as st
|
||||
|
||||
st.set_page_config(page_title="DevOps AI Assistant", page_icon="🚀")
|
||||
# В духе https://github.com/streamlit/demo-ai-assistant — крупный герой, заголовок в ряд с действием
|
||||
st.set_page_config(
|
||||
page_title="DevOps AI Assistant",
|
||||
page_icon="✨",
|
||||
layout="wide",
|
||||
initial_sidebar_state="expanded",
|
||||
)
|
||||
|
||||
st.title("DevOps AI Assistant")
|
||||
st.markdown("Портал с данными и инструментами для АС/ФП.")
|
||||
_HERO_HTML = """
|
||||
<style>
|
||||
.portal-hero-wrap { margin: 0 0 0.25rem 0; }
|
||||
.portal-hero { font-size: clamp(2.5rem, 6vw, 3.5rem); line-height: 1; margin: 0; font-weight: 400; }
|
||||
</style>
|
||||
<div class="portal-hero-wrap"><div class="portal-hero">❉</div></div>
|
||||
"""
|
||||
|
||||
|
||||
def _env(name: str, default: str) -> str:
|
||||
@@ -24,14 +35,36 @@ def _normalize_base_url(value: str) -> str:
|
||||
v = v[: -len("/generate")]
|
||||
return v.rstrip("/")
|
||||
|
||||
|
||||
def _title_bar(*, show_logout: bool):
|
||||
st.html(_HERO_HTML)
|
||||
row = st.container(horizontal=True, vertical_alignment="bottom")
|
||||
with row:
|
||||
st.title("DevOps AI Assistant", anchor=False)
|
||||
if show_logout:
|
||||
|
||||
def _logout():
|
||||
st.session_state.token = None
|
||||
st.rerun()
|
||||
|
||||
st.button(
|
||||
"Выйти",
|
||||
icon=":material/logout:",
|
||||
on_click=_logout,
|
||||
help="Выйти из портала",
|
||||
)
|
||||
|
||||
|
||||
with st.sidebar:
|
||||
st.header("Настройки")
|
||||
st.markdown("#### Настройки")
|
||||
backend_base_url = _normalize_base_url(st.text_input("Backend URL", default_backend_base_url))
|
||||
model = st.selectbox(
|
||||
"LLM Модель",
|
||||
"LLM модель",
|
||||
["deepseek-r1:1.5b", "qwen2.5-coder:1.5b", "llama3.1:8b", "Arena Model"],
|
||||
)
|
||||
platform = st.radio("Платформа", ["PV DOT CI", "PV DOT CD", "DPM CI", "DPM CD"])
|
||||
st.divider()
|
||||
st.caption("Портал АС/ФП · конфигурации в разделе «Инструменты»")
|
||||
|
||||
if "history" not in st.session_state:
|
||||
st.session_state.history = []
|
||||
@@ -67,6 +100,7 @@ def _auth_headers():
|
||||
return {}
|
||||
return {"Authorization": f"Bearer {st.session_state.token}"}
|
||||
|
||||
|
||||
def _detail_or_text(resp: requests.Response) -> str:
|
||||
try:
|
||||
payload = resp.json()
|
||||
@@ -78,15 +112,18 @@ def _detail_or_text(resp: requests.Response) -> str:
|
||||
|
||||
|
||||
def _show_account():
|
||||
st.subheader("Аутентификация")
|
||||
_title_bar(show_logout=bool(st.session_state.token))
|
||||
st.caption("Войдите или создайте учётную запись, чтобы открыть разделы портала.")
|
||||
|
||||
c1, c2 = st.columns(2)
|
||||
# Два блока с рамкой — ближе к «карточному» виду демо
|
||||
c1, c2 = st.columns(2, gap="large")
|
||||
with c1:
|
||||
with st.container(border=True):
|
||||
st.markdown("**Регистрация**")
|
||||
reg_email = st.text_input("Email", key="reg_email")
|
||||
reg_full_name = st.text_input("Имя (опционально)", key="reg_full_name")
|
||||
reg_password = st.text_input("Пароль", type="password", key="reg_password")
|
||||
if st.button("Создать аккаунт", key="btn_register"):
|
||||
if st.button("Создать аккаунт", key="btn_register", use_container_width=True):
|
||||
try:
|
||||
r = requests.post(
|
||||
f"{backend_base_url}/auth/register",
|
||||
@@ -101,12 +138,11 @@ def _show_account():
|
||||
st.error(f"Ошибка подключения к бекенду: {e}")
|
||||
|
||||
with c2:
|
||||
with st.container(border=True):
|
||||
st.markdown("**Вход**")
|
||||
login_email = st.text_input("Email ", key="login_email")
|
||||
login_password = st.text_input("Пароль ", type="password", key="login_password")
|
||||
login_col_a, login_col_b = st.columns([1, 1])
|
||||
with login_col_a:
|
||||
if st.button("Войти", type="primary", key="btn_login"):
|
||||
if st.button("Войти", type="primary", key="btn_login", use_container_width=True):
|
||||
try:
|
||||
r = requests.post(
|
||||
f"{backend_base_url}/auth/login",
|
||||
@@ -121,10 +157,6 @@ def _show_account():
|
||||
st.rerun()
|
||||
except Exception as e:
|
||||
st.error(f"Ошибка подключения к бекенду: {e}")
|
||||
with login_col_b:
|
||||
if st.button("Выйти", disabled=not bool(st.session_state.token), key="btn_logout"):
|
||||
st.session_state.token = None
|
||||
st.rerun()
|
||||
|
||||
st.divider()
|
||||
if st.session_state.token:
|
||||
@@ -135,16 +167,20 @@ def _show_account():
|
||||
st.warning("Сессия истекла или токен неверный. Войдите заново.")
|
||||
st.rerun()
|
||||
else:
|
||||
with st.expander("Профиль", expanded=False):
|
||||
st.json(r.json())
|
||||
except Exception as e:
|
||||
st.error(f"Ошибка подключения к бекенду: {e}")
|
||||
|
||||
|
||||
# Auth wall: пока пользователь не вошёл — показываем только экран входа/регистрации.
|
||||
# Auth wall
|
||||
if not st.session_state.token:
|
||||
_show_account()
|
||||
st.stop()
|
||||
|
||||
# После входа — тот же заголовок + вкладки
|
||||
_title_bar(show_logout=True)
|
||||
st.caption("Портал с данными и инструментами для АС/ФП.")
|
||||
|
||||
tab_info, tab_autonomous, tab_infra, tab_tools, tab_news = st.tabs(
|
||||
[
|
||||
@@ -158,29 +194,37 @@ tab_info, tab_autonomous, tab_infra, tab_tools, tab_news = st.tabs(
|
||||
|
||||
with tab_info:
|
||||
st.subheader("Сведения об АС/ФП")
|
||||
st.caption("Черновик формы. Дальше можно привязать к Postgres и хранить как сущность.")
|
||||
st.caption("Черновик формы. Дальше можно привязать к Postgres.")
|
||||
with st.container(border=True):
|
||||
st.session_state.asfp_info["name"] = st.text_input("Название", st.session_state.asfp_info["name"])
|
||||
st.session_state.asfp_info["owner"] = st.text_input("Владелец", st.session_state.asfp_info["owner"])
|
||||
st.session_state.asfp_info["contacts"] = st.text_input("Контакты", st.session_state.asfp_info["contacts"])
|
||||
st.session_state.asfp_info["description"] = st.text_area(
|
||||
"Описание", st.session_state.asfp_info["description"], height=120
|
||||
)
|
||||
st.divider()
|
||||
with st.expander("JSON", expanded=False):
|
||||
st.json(st.session_state.asfp_info)
|
||||
|
||||
with tab_autonomous:
|
||||
st.subheader("Автономные внедрения")
|
||||
st.info("Сюда можно добавить список внедрений, статус, даты, ответственных и ссылки на релизы/тикеты.")
|
||||
with st.container(border=True):
|
||||
st.info(
|
||||
"Сюда можно добавить список внедрений, статус, даты, ответственных и ссылки на релизы/тикеты."
|
||||
)
|
||||
|
||||
with tab_infra:
|
||||
st.subheader("Инфраструктура АС/ФП")
|
||||
st.info("Сюда можно добавить окружения, кластера, сервисы, ресурсы, сетевые зависимости, секреты и т.д.")
|
||||
with st.container(border=True):
|
||||
st.info(
|
||||
"Сюда можно добавить окружения, кластера, сервисы, ресурсы, сетевые зависимости и т.д."
|
||||
)
|
||||
|
||||
with tab_tools:
|
||||
st.subheader("Инструменты")
|
||||
st.caption(f"Backend URL: {backend_base_url}")
|
||||
st.caption(f"Backend: `{backend_base_url}`")
|
||||
|
||||
st.markdown("### Pipeline generator")
|
||||
with st.container(border=True):
|
||||
st.markdown("#### Pipeline generator")
|
||||
user_input = st.text_area(
|
||||
"Опишите пайплайн (например: 'Python app with pytest and docker build')",
|
||||
height=150,
|
||||
@@ -189,9 +233,9 @@ with tab_tools:
|
||||
|
||||
col_a, col_b = st.columns([1, 3])
|
||||
with col_a:
|
||||
generate_clicked = st.button("Сгенерировать конфигурацию", type="primary")
|
||||
generate_clicked = st.button("Сгенерировать", type="primary", use_container_width=True)
|
||||
with col_b:
|
||||
st.caption("Подсказка: чем конкретнее требования, тем валиднее YAML.")
|
||||
st.caption("Чем конкретнее требования, тем стабильнее YAML.")
|
||||
|
||||
if generate_clicked:
|
||||
if user_input.strip():
|
||||
@@ -233,4 +277,14 @@ with tab_tools:
|
||||
|
||||
with tab_news:
|
||||
st.subheader("Новости")
|
||||
st.info("Сюда можно вывести новости/изменения: релизы, инциденты, регламентные работы, обновления инструментов.")
|
||||
with st.container(border=True):
|
||||
st.info(
|
||||
"Релизы, инциденты, регламентные работы, обновления инструментов — сюда можно вывести ленту позже."
|
||||
)
|
||||
|
||||
st.divider()
|
||||
st.button(
|
||||
" :small[:gray[:material/balance: О портале]]",
|
||||
type="tertiary",
|
||||
help="DevOps AI Assistant — внутренний портал.",
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user