general fixes
This commit is contained in:
@@ -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)
|
||||
# -------------------------------------------
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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')}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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('')
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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
|
||||
---
|
||||
|
||||
Reference in New Issue
Block a user