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 in seconds (default: 3600 = 1 hour)
COOKIE_MAX_AGE=3600 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) # 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_max_age: int = Field(default=28800, env="COOKIE_MAX_AGE") # 8 hours
cookie_name: str = Field(default="access_token", env="COOKIE_NAME") 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 settings
database_host: str = Field(default="postgres", env="DATABASE_HOST") database_host: str = Field(default="postgres", env="DATABASE_HOST")
database_port: int = Field(default=5432, env="DATABASE_PORT") 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 typing import List, Optional
from fastapi import FastAPI, Depends, Request, HTTPException, status from fastapi import FastAPI, Depends, Request, HTTPException, status
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from uvicorn.middleware.proxy_headers import ProxyHeadersMiddleware
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from fastapi.responses import RedirectResponse from fastapi.responses import RedirectResponse
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
@@ -139,6 +140,15 @@ app = FastAPI(
lifespan=lifespan 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 # Configure CORS
app.add_middleware( app.add_middleware(
CORSMiddleware, CORSMiddleware,

View File

@@ -15,6 +15,12 @@ interface LanguageSelectorProps {
export default function LanguageSelector({ className = '', compact = false }: LanguageSelectorProps) { export default function LanguageSelector({ className = '', compact = false }: LanguageSelectorProps) {
const { i18n, t } = useTranslation('common') 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 handleLanguageChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const newLang = e.target.value as LanguageCode const newLang = e.target.value as LanguageCode
i18n.changeLanguage(newLang) i18n.changeLanguage(newLang)
@@ -23,7 +29,7 @@ export default function LanguageSelector({ className = '', compact = false }: La
return ( return (
<div className={`relative ${className}`}> <div className={`relative ${className}`}>
<select <select
value={i18n.language} value={selectedLanguage}
onChange={handleLanguageChange} 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" 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')} title={t('selectLanguage')}

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import { useNavigate } from 'react-router-dom' import { useNavigate, useLocation } from 'react-router-dom'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useAuth, useProject } from '@/hooks' import { useAuth, useProject } from '@/hooks'
import { import {
@@ -19,10 +19,22 @@ export default function AdminPage() {
const { user } = useAuth() const { user } = useAuth()
const { currentProject, setCurrentProject } = useProject() const { currentProject, setCurrentProject } = useProject()
const navigate = useNavigate() const navigate = useNavigate()
const location = useLocation()
// Tab state // Tab state
const [activeTab, setActiveTab] = useState<TabType>('project') 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 // Project settings state
const [projectName, setProjectName] = useState('') const [projectName, setProjectName] = useState('')
const [projectDesc, setProjectDesc] = useState('') const [projectDesc, setProjectDesc] = useState('')

View File

@@ -73,7 +73,7 @@ interface GroupStats {
export default function DashboardPage() { export default function DashboardPage() {
const { user, logout } = useAuth() const { user, logout } = useAuth()
const { projects, currentProject, setCurrentProject, isLoading: projectsLoading, createProject } = useProject() const { projects, currentProject, setCurrentProject, isLoading: projectsLoading } = useProject()
const navigate = useNavigate() const navigate = useNavigate()
const { t } = useTranslation('dashboard') const { t } = useTranslation('dashboard')
const { t: tCommon } = useTranslation('common') const { t: tCommon } = useTranslation('common')
@@ -88,11 +88,7 @@ export default function DashboardPage() {
// Project dropdown state // Project dropdown state
const [showProjectDropdown, setShowProjectDropdown] = useState(false) 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 // Calculate stats for each group
const getGroupStats = (groupId: number): GroupStats => { const getGroupStats = (groupId: number): GroupStats => {
@@ -212,38 +208,6 @@ export default function DashboardPage() {
setShowProjectDropdown(false) 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 ( return (
<div className="min-h-screen bg-gray-50 flex"> <div className="min-h-screen bg-gray-50 flex">
{/* Sidebar */} {/* Sidebar */}
@@ -373,16 +337,6 @@ export default function DashboardPage() {
</button> </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>
</div> </div>
)} )}
@@ -473,14 +427,7 @@ export default function DashboardPage() {
<div> <div>
<h3 className="font-semibold text-amber-800">{t('noProjectWarning.title')}</h3> <h3 className="font-semibold text-amber-800">{t('noProjectWarning.title')}</h3>
<p className="text-sm text-amber-700"> <p className="text-sm text-amber-700">
{t('noProjectWarning.messagePart1')}{' '} {t('noProjectWarning.messagePart1')} {t('noProjectWarning.messagePart2')}
<button
onClick={() => setShowCreateProjectModal(true)}
className="underline font-medium hover:text-amber-900"
>
{t('noProjectWarning.createProject')}
</button>
{' '}{t('noProjectWarning.messagePart2')}
</p> </p>
</div> </div>
</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> </div>
) )
} }

View File

@@ -718,6 +718,7 @@ export default function RequirementDetailPage() {
// Check if requirement is in draft status // Check if requirement is in draft status
const isDraftStatus = requirement?.status?.status_code === 'DRAFT' const isDraftStatus = requirement?.status?.status_code === 'DRAFT'
const isAdmin = user?.role_id === 3
if (loading) { if (loading) {
return ( return (
@@ -852,6 +853,15 @@ export default function RequirementDetailPage() {
<div> <div>
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
<h3 className="text-xl font-bold text-gray-800">{t('relationships.title')}</h3> <h3 className="text-xl font-bold text-gray-800">{t('relationships.title')}</h3>
<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 && ( {!isAuditor && (
<button <button
onClick={openAddRelationshipModal} onClick={openAddRelationshipModal}
@@ -863,6 +873,7 @@ export default function RequirementDetailPage() {
</button> </button>
)} )}
</div> </div>
</div>
{relationshipTypes.length === 0 && !relationshipsLoading && ( {relationshipTypes.length === 0 && !relationshipsLoading && (
<div className="mb-4 p-3 bg-yellow-50 border border-yellow-200 rounded text-sm text-yellow-800"> <div className="mb-4 p-3 bg-yellow-50 border border-yellow-200 rounded text-sm text-yellow-800">

View File

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