general fixes

This commit is contained in:
gulimabr
2026-01-18 22:06:02 -03:00
parent 9f8315120b
commit 3be3c22be9
12 changed files with 87 additions and 178 deletions

View File

@@ -41,6 +41,14 @@ COOKIE_DOMAIN=
# Cookie max age in seconds (default: 3600 = 1 hour)
COOKIE_MAX_AGE=3600
# -------------------------------------------
# Reverse Proxy / TLS Termination
# -------------------------------------------
# Enable to trust X-Forwarded-* headers (set true behind ingress/nginx)
PROXY_HEADERS=false
# Comma-separated trusted proxy hosts/IPs (use "*" to trust all)
TRUSTED_PROXY_HOSTS=127.0.0.1,::1
# -------------------------------------------
# Database Configuration (PostgreSQL)
# -------------------------------------------

View File

@@ -25,6 +25,12 @@ class Settings(BaseSettings):
cookie_max_age: int = Field(default=28800, env="COOKIE_MAX_AGE") # 8 hours
cookie_name: str = Field(default="access_token", env="COOKIE_NAME")
# Proxy / TLS termination settings
# Enable to honor X-Forwarded-Proto when behind a reverse proxy (ingress/nginx)
proxy_headers: bool = Field(default=False, env="PROXY_HEADERS")
# Comma-separated list of trusted proxy hosts/IPs. Use "*" to trust all.
trusted_proxy_hosts: str = Field(default="127.0.0.1,::1", env="TRUSTED_PROXY_HOSTS")
# Database settings
database_host: str = Field(default="postgres", env="DATABASE_HOST")
database_port: int = Field(default=5432, env="DATABASE_PORT")

View File

@@ -2,6 +2,7 @@ from contextlib import asynccontextmanager
from typing import List, Optional
from fastapi import FastAPI, Depends, Request, HTTPException, status
from fastapi.middleware.cors import CORSMiddleware
from uvicorn.middleware.proxy_headers import ProxyHeadersMiddleware
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from fastapi.responses import RedirectResponse
from sqlalchemy.ext.asyncio import AsyncSession
@@ -139,6 +140,15 @@ app = FastAPI(
lifespan=lifespan
)
# Respect X-Forwarded-Proto/For headers when behind a reverse proxy
if settings.proxy_headers:
trusted_hosts = [
host.strip()
for host in settings.trusted_proxy_hosts.split(",")
if host.strip()
]
app.add_middleware(ProxyHeadersMiddleware, trusted_hosts=trusted_hosts)
# Configure CORS
app.add_middleware(
CORSMiddleware,

View File

@@ -15,6 +15,12 @@ interface LanguageSelectorProps {
export default function LanguageSelector({ className = '', compact = false }: LanguageSelectorProps) {
const { i18n, t } = useTranslation('common')
const resolvedLanguage = i18n.resolvedLanguage || i18n.language
const normalizedLanguage = (resolvedLanguage?.split('-')[0] || 'en') as LanguageCode
const selectedLanguage = LANGUAGES.some((lang) => lang.code === normalizedLanguage)
? normalizedLanguage
: 'en'
const handleLanguageChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const newLang = e.target.value as LanguageCode
i18n.changeLanguage(newLang)
@@ -23,7 +29,7 @@ export default function LanguageSelector({ className = '', compact = false }: La
return (
<div className={`relative ${className}`}>
<select
value={i18n.language}
value={selectedLanguage}
onChange={handleLanguageChange}
className="appearance-none bg-white border border-gray-300 rounded-md pl-3 pr-8 py-1.5 text-sm text-gray-700 hover:border-gray-400 focus:outline-none focus:ring-2 focus:ring-teal-500 focus:border-transparent cursor-pointer"
title={t('selectLanguage')}

View File

@@ -24,9 +24,8 @@
},
"noProjectWarning": {
"title": "No Project Selected",
"messagePart1": "Please select a project from the dropdown above or",
"createProject": "create a new project",
"messagePart2": "to get started."
"messagePart1": "Please select a project from the dropdown above.",
"messagePart2": "New projects can be created in the Super Admin panel."
},
"quickFilters": {
"title": "Quick Search Filters",

View File

@@ -37,6 +37,7 @@
"relationships": {
"title": "Relationships",
"addButton": "Add Relationship",
"manageTypesButton": "Manage Relationship Types",
"noTypesWarning": "No relationship types have been defined for this project. Contact an administrator to set up relationship types.",
"loadingRelationships": "Loading relationships...",
"noRelationships": "No relationships defined yet.",

View File

@@ -24,9 +24,8 @@
},
"noProjectWarning": {
"title": "Nenhum Projeto Selecionado",
"messagePart1": "Por favor, selecione um projeto no menu acima ou",
"createProject": "crie um novo projeto",
"messagePart2": "para começar."
"messagePart1": "Por favor, selecione um projeto no menu acima.",
"messagePart2": "Novos projetos podem ser criados no painel de Super Admin."
},
"quickFilters": {
"title": "Filtros Rápidos de Busca",

View File

@@ -37,6 +37,7 @@
"relationships": {
"title": "Relacionamentos",
"addButton": "Adicionar Relacionamento",
"manageTypesButton": "Gerenciar Tipos de Relacionamento",
"noTypesWarning": "Nenhum tipo de relacionamento foi definido para este projeto. Contate um administrador para configurar os tipos de relacionamento.",
"loadingRelationships": "Carregando relacionamentos...",
"noRelationships": "Nenhum relacionamento definido ainda.",

View File

@@ -1,5 +1,5 @@
import { useState, useEffect } from 'react'
import { useNavigate } from 'react-router-dom'
import { useNavigate, useLocation } from 'react-router-dom'
import { useTranslation } from 'react-i18next'
import { useAuth, useProject } from '@/hooks'
import {
@@ -19,10 +19,22 @@ export default function AdminPage() {
const { user } = useAuth()
const { currentProject, setCurrentProject } = useProject()
const navigate = useNavigate()
const location = useLocation()
// Tab state
const [activeTab, setActiveTab] = useState<TabType>('project')
// Allow navigation to a specific tab (e.g., relationships)
useEffect(() => {
const tabFromState = (location.state as { tab?: TabType } | null)?.tab
const tabFromQuery = new URLSearchParams(location.search).get('tab') as TabType | null
const nextTab = tabFromState || tabFromQuery
if (nextTab && ['project', 'members', 'relationships'].includes(nextTab) && nextTab !== activeTab) {
setActiveTab(nextTab)
}
}, [location.state, location.search, activeTab])
// Project settings state
const [projectName, setProjectName] = useState('')
const [projectDesc, setProjectDesc] = useState('')

View File

@@ -73,7 +73,7 @@ interface GroupStats {
export default function DashboardPage() {
const { user, logout } = useAuth()
const { projects, currentProject, setCurrentProject, isLoading: projectsLoading, createProject } = useProject()
const { projects, currentProject, setCurrentProject, isLoading: projectsLoading } = useProject()
const navigate = useNavigate()
const { t } = useTranslation('dashboard')
const { t: tCommon } = useTranslation('common')
@@ -88,11 +88,7 @@ export default function DashboardPage() {
// Project dropdown state
const [showProjectDropdown, setShowProjectDropdown] = useState(false)
const [showCreateProjectModal, setShowCreateProjectModal] = useState(false)
const [newProjectName, setNewProjectName] = useState('')
const [newProjectDesc, setNewProjectDesc] = useState('')
const [createProjectLoading, setCreateProjectLoading] = useState(false)
const [createProjectError, setCreateProjectError] = useState<string | null>(null)
// Calculate stats for each group
const getGroupStats = (groupId: number): GroupStats => {
@@ -212,38 +208,6 @@ export default function DashboardPage() {
setShowProjectDropdown(false)
}
const handleCreateProject = async (e: React.FormEvent) => {
e.preventDefault()
if (!newProjectName.trim()) {
setCreateProjectError('Project name is required')
return
}
try {
setCreateProjectLoading(true)
setCreateProjectError(null)
const newProject = await createProject(
newProjectName.trim(),
newProjectDesc.trim() || undefined
)
// Select the newly created project
setCurrentProject(newProject)
// Close modal and reset form
setShowCreateProjectModal(false)
setNewProjectName('')
setNewProjectDesc('')
} catch (err) {
console.error('Failed to create project:', err)
setCreateProjectError('Failed to create project. Please try again.')
} finally {
setCreateProjectLoading(false)
}
}
return (
<div className="min-h-screen bg-gray-50 flex">
{/* Sidebar */}
@@ -373,16 +337,6 @@ export default function DashboardPage() {
</button>
))
)}
<hr className="my-1 border-gray-200" />
<button
onClick={() => {
setShowProjectDropdown(false)
setShowCreateProjectModal(true)
}}
className="w-full text-left px-4 py-2 text-sm text-teal-600 hover:bg-gray-100 font-medium"
>
{t('projectDropdown.createNewProject')}
</button>
</div>
</div>
)}
@@ -473,14 +427,7 @@ export default function DashboardPage() {
<div>
<h3 className="font-semibold text-amber-800">{t('noProjectWarning.title')}</h3>
<p className="text-sm text-amber-700">
{t('noProjectWarning.messagePart1')}{' '}
<button
onClick={() => setShowCreateProjectModal(true)}
className="underline font-medium hover:text-amber-900"
>
{t('noProjectWarning.createProject')}
</button>
{' '}{t('noProjectWarning.messagePart2')}
{t('noProjectWarning.messagePart1')} {t('noProjectWarning.messagePart2')}
</p>
</div>
</div>
@@ -790,95 +737,6 @@ export default function DashboardPage() {
/>
)}
{/* Create Project Modal */}
{showCreateProjectModal && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-lg shadow-xl w-full max-w-md mx-4">
{/* Modal Header */}
<div className="flex items-center justify-between px-6 py-4 border-b border-gray-200">
<h2 className="text-xl font-semibold text-gray-800">{t('createProject.title')}</h2>
<button
onClick={() => {
setShowCreateProjectModal(false)
setCreateProjectError(null)
setNewProjectName('')
setNewProjectDesc('')
}}
className="text-gray-400 hover:text-gray-600"
>
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
{/* Modal Body */}
<form onSubmit={handleCreateProject}>
<div className="px-6 py-4 space-y-4">
{/* Error message */}
{createProjectError && (
<div className="p-3 bg-red-100 border border-red-400 text-red-700 rounded text-sm">
{createProjectError}
</div>
)}
{/* Project Name */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
{t('createProject.projectName')} <span className="text-red-500">*</span>
</label>
<input
type="text"
value={newProjectName}
onChange={(e) => setNewProjectName(e.target.value)}
placeholder={t('createProject.projectNamePlaceholder')}
className="w-full px-3 py-2 border border-gray-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-teal-500 focus:border-transparent"
required
/>
</div>
{/* Project Description */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
{t('createProject.description')}
</label>
<textarea
value={newProjectDesc}
onChange={(e) => setNewProjectDesc(e.target.value)}
placeholder={t('createProject.descriptionPlaceholder')}
rows={3}
className="w-full px-3 py-2 border border-gray-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-teal-500 focus:border-transparent resize-none"
/>
</div>
</div>
{/* Modal Footer */}
<div className="flex justify-end gap-3 px-6 py-4 border-t border-gray-200 bg-gray-50 rounded-b-lg">
<button
type="button"
onClick={() => {
setShowCreateProjectModal(false)
setCreateProjectError(null)
setNewProjectName('')
setNewProjectDesc('')
}}
className="px-4 py-2 border border-gray-400 rounded text-sm font-medium text-gray-700 hover:bg-gray-100"
disabled={createProjectLoading}
>
{tCommon('cancel')}
</button>
<button
type="submit"
className="px-4 py-2 bg-teal-600 text-white rounded text-sm font-medium hover:bg-teal-700 disabled:opacity-50 disabled:cursor-not-allowed"
disabled={createProjectLoading}
>
{createProjectLoading ? t('createProject.creating') : t('createProject.createButton')}
</button>
</div>
</form>
</div>
</div>
)}
</div>
)
}

View File

@@ -718,6 +718,7 @@ export default function RequirementDetailPage() {
// Check if requirement is in draft status
const isDraftStatus = requirement?.status?.status_code === 'DRAFT'
const isAdmin = user?.role_id === 3
if (loading) {
return (
@@ -852,16 +853,26 @@ export default function RequirementDetailPage() {
<div>
<div className="flex items-center justify-between mb-4">
<h3 className="text-xl font-bold text-gray-800">{t('relationships.title')}</h3>
{!isAuditor && (
<button
onClick={openAddRelationshipModal}
disabled={relationshipTypes.length === 0}
className="px-4 py-1.5 bg-teal-600 text-white rounded text-sm font-medium hover:bg-teal-700 disabled:opacity-50 disabled:cursor-not-allowed"
title={relationshipTypes.length === 0 ? t('relationships.noTypesWarning') : ''}
>
{t('relationships.addButton')}
</button>
)}
<div className="flex items-center gap-2">
{isAdmin && (
<button
onClick={() => navigate('/admin', { state: { tab: 'relationships' } })}
className="px-4 py-1.5 border border-gray-400 rounded text-sm text-gray-700 hover:bg-gray-50"
>
{t('relationships.manageTypesButton')}
</button>
)}
{!isAuditor && (
<button
onClick={openAddRelationshipModal}
disabled={relationshipTypes.length === 0}
className="px-4 py-1.5 bg-teal-600 text-white rounded text-sm font-medium hover:bg-teal-700 disabled:opacity-50 disabled:cursor-not-allowed"
title={relationshipTypes.length === 0 ? t('relationships.noTypesWarning') : ''}
>
{t('relationships.addButton')}
</button>
)}
</div>
</div>
{relationshipTypes.length === 0 && !relationshipsLoading && (

View File

@@ -9,7 +9,7 @@ metadata:
name: fastapi-app
namespace: requirements-periodic-table
spec:
replicas: 2
replicas: 1
selector:
matchLabels:
app: fastapi-app
@@ -18,20 +18,20 @@ spec:
labels:
app: fastapi-app
spec:
imagePullSecrets:
- name: regcred
containers:
- name: fastapi-app
image: docker.io/your-dockerhub-username/periodic-table-backend:latest
imagePullPolicy: IfNotPresent
image: docker.io/gulimabr/requirements-periodic-table-backend:latest
imagePullPolicy: Always
ports:
- containerPort: 8080
env:
- name: DATABASE_HOST
value: postgresql
envFrom:
- secretRef:
name: periodic-table-env # Provide secrets for APP settings
- configMapRef:
name: periodic-table-env # Provide config for APP settings
env:
- name: PROXY_HEADERS
value: "true"
- name: TRUSTED_PROXY_HOSTS
value: "*"
---
apiVersion: v1
kind: Service
@@ -53,7 +53,7 @@ metadata:
name: frontend
namespace: requirements-periodic-table
spec:
replicas: 2
replicas: 1
selector:
matchLabels:
app: frontend
@@ -62,12 +62,10 @@ spec:
labels:
app: frontend
spec:
imagePullSecrets:
- name: regcred
containers:
- name: frontend
image: docker.io/your-dockerhub-username/periodic-table-frontend:latest
imagePullPolicy: IfNotPresent
image: docker.io/gulimabr/requirements-periodic-table-frontend:latest
imagePullPolicy: Always
ports:
- containerPort: 80
---