Separer le mode sysadmin instance
This commit is contained in:
parent
ea01dcec00
commit
8fafb8e000
6 changed files with 57 additions and 11 deletions
|
|
@ -21,10 +21,12 @@ def verify_pin(pin: str, hashed: str) -> bool:
|
|||
return bcrypt.checkpw(pin.encode("utf-8"), hashed.encode("utf-8"))
|
||||
|
||||
|
||||
def create_token(membre_id: str, groupe_id: str, expire_jours: int = None, admin_id: str | None = None) -> str:
|
||||
def create_token(membre_id: str, groupe_id: str | None, expire_jours: int = None, admin_id: str | None = None) -> str:
|
||||
jours = expire_jours or settings.JWT_EXPIRE_DAYS
|
||||
expire = datetime.now(timezone.utc) + timedelta(days=jours)
|
||||
payload = {"sub": str(membre_id), "grp": str(groupe_id), "exp": expire}
|
||||
payload = {"sub": str(membre_id), "exp": expire}
|
||||
if groupe_id:
|
||||
payload["grp"] = str(groupe_id)
|
||||
if admin_id:
|
||||
payload["adm"] = str(admin_id)
|
||||
return jwt.encode(payload, settings.JWT_SECRET, algorithm=settings.JWT_ALGORITHM)
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ from app.models.groupe import Groupe
|
|||
from app.models.membre import Membre
|
||||
from app.models.reunion import Reunion
|
||||
from app.models.historique_modification import HistoriqueModification
|
||||
from app.schemas.schemas import MembrePublic, TokenResponse
|
||||
|
||||
router = APIRouter(prefix="/admin", tags=["Administration"])
|
||||
|
||||
|
|
@ -44,13 +43,25 @@ async def require_sysadmin(membre=Depends(get_current_membre)):
|
|||
return membre
|
||||
|
||||
|
||||
@router.post("/sysadmin-login", response_model=TokenResponse)
|
||||
@router.post("/sysadmin-login")
|
||||
async def sysadmin_login(req: SysadminLoginRequest, db: AsyncSession = Depends(get_db)):
|
||||
result = await db.execute(select(Membre).where(Membre.courriel == "sysadmin", Membre.actif == True))
|
||||
for membre in result.scalars().all():
|
||||
if verify_pin(req.pin, membre.pin_hash):
|
||||
token = create_token(str(membre.id), str(membre.groupe_id), expire_jours=1)
|
||||
return TokenResponse(access_token=token, membre=MembrePublic.model_validate(membre))
|
||||
token = create_token(str(membre.id), None, expire_jours=1)
|
||||
return {
|
||||
"access_token": token,
|
||||
"token_type": "bearer",
|
||||
"sysadmin_instance": True,
|
||||
"membre": {
|
||||
"id": str(membre.id),
|
||||
"prenom": "Sysadmin",
|
||||
"courriel": "sysadmin",
|
||||
"groupe_id": None,
|
||||
"date_abstinence": None,
|
||||
"is_sysadmin": True,
|
||||
},
|
||||
}
|
||||
raise HTTPException(status_code=401, detail="PIN Sysadmin invalide")
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
|||
from app.core.decision_types import type_decision_depuis_proposition
|
||||
from app.core.financial_position import PositionTresorerie
|
||||
from app.core.sanitization import safe_query_params
|
||||
from app.core.security import create_token, decode_token
|
||||
from app.models.poste import Poste
|
||||
from app.models.affectation import Affectation
|
||||
from app.models.candidature import Candidature
|
||||
|
|
@ -60,6 +61,14 @@ class AuditTests(unittest.TestCase):
|
|||
self.assertEqual(safe_query_params(request.query_params), {"pin": "***", "membre_id": "abc"})
|
||||
|
||||
|
||||
class SecurityTokenTests(unittest.TestCase):
|
||||
def test_sysadmin_instance_token_has_no_group_context(self):
|
||||
token = create_token("member-id", None, expire_jours=1)
|
||||
payload = decode_token(token)
|
||||
self.assertEqual(payload["sub"], "member-id")
|
||||
self.assertNotIn("grp", payload)
|
||||
|
||||
|
||||
class MultiTenantTests(unittest.TestCase):
|
||||
def test_postes_are_group_scoped(self):
|
||||
self.assertIn("groupe_id", Poste.__table__.columns)
|
||||
|
|
|
|||
|
|
@ -36,14 +36,16 @@ import ModePapier from './pages/ModePapier';
|
|||
import BottomNav from './components/BottomNav';
|
||||
|
||||
export default function App() {
|
||||
const { token, page, navContext, loadAccueil, loadGroupes } = useStore();
|
||||
const { token, page, navContext, sysadminInstance, loadAccueil, loadGroupes } = useStore();
|
||||
const chemin = window.location.pathname;
|
||||
|
||||
useEffect(() => {
|
||||
const handleUnauthorized = () => useStore.getState().logout();
|
||||
window.addEventListener('gm:unauthorized', handleUnauthorized);
|
||||
loadGroupes();
|
||||
if (token) {
|
||||
if (token && sysadminInstance) {
|
||||
useStore.setState({ page: 'G14' });
|
||||
} else if (token) {
|
||||
loadAccueil().then(() => {
|
||||
// Restaurer la page sauvegardée ou aller à l'accueil
|
||||
const savedPage = localStorage.getItem('gm_page');
|
||||
|
|
@ -62,6 +64,9 @@ export default function App() {
|
|||
|
||||
// Pages protégées
|
||||
if (token) {
|
||||
if (sysadminInstance) {
|
||||
return <main><GestionGroupes /></main>;
|
||||
}
|
||||
let pageActive;
|
||||
switch (page) {
|
||||
case 'accueil':
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ const jourLabel = (jour) => {
|
|||
};
|
||||
|
||||
export default function Login() {
|
||||
const { login, loadAccueil, loading, error, groupes, loadGroupes, setPage } = useStore();
|
||||
const { login, loading, error, groupes, loadGroupes, setPage } = useStore();
|
||||
const [courriel, setCourriel] = useState(localStorage.getItem('gm_courriel') || '');
|
||||
const [pin, setPin] = useState('');
|
||||
const [groupeId, setGroupeId] = useState(localStorage.getItem('gm_groupe_id') || '');
|
||||
|
|
@ -54,13 +54,22 @@ export default function Login() {
|
|||
try {
|
||||
const data = await api.post('/admin/sysadmin-login', { pin });
|
||||
api.setToken(data.access_token, false);
|
||||
localStorage.setItem('gm_sysadmin_instance', '1');
|
||||
localStorage.removeItem('gm_groupe_id');
|
||||
useStore.setState({
|
||||
token: data.access_token,
|
||||
membre: data.membre,
|
||||
groupeId: data.membre.groupe_id,
|
||||
groupeId: null,
|
||||
groupe: null,
|
||||
postes: ['Sysadmin'],
|
||||
estExecutif: true,
|
||||
estSysadmin: true,
|
||||
modulesConsultation: [],
|
||||
modulesSaisie: [],
|
||||
modulesGestion: [],
|
||||
sysadminInstance: true,
|
||||
error: null,
|
||||
});
|
||||
await loadAccueil();
|
||||
setPage('G14');
|
||||
} catch (e) {
|
||||
useStore.setState({ error: e.message });
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ export const useStore = create((set, get) => ({
|
|||
token: api.getToken(),
|
||||
membre: null,
|
||||
groupeId: localStorage.getItem('gm_groupe_id'),
|
||||
sysadminInstance: localStorage.getItem('gm_sysadmin_instance') === '1',
|
||||
groupes: [],
|
||||
|
||||
// Accueil
|
||||
|
|
@ -49,6 +50,7 @@ export const useStore = create((set, get) => ({
|
|||
courriel, pin, groupe_id: groupeId, se_souvenir: seSouvenir,
|
||||
});
|
||||
api.setToken(data.access_token, seSouvenir);
|
||||
localStorage.removeItem('gm_sysadmin_instance');
|
||||
localStorage.setItem('gm_groupe_id', groupeId);
|
||||
if (seSouvenir) {
|
||||
localStorage.setItem('gm_courriel', courriel);
|
||||
|
|
@ -59,6 +61,7 @@ export const useStore = create((set, get) => ({
|
|||
token: data.access_token,
|
||||
membre: data.membre,
|
||||
groupeId,
|
||||
sysadminInstance: false,
|
||||
loading: false,
|
||||
});
|
||||
await get().loadAccueil();
|
||||
|
|
@ -79,10 +82,12 @@ export const useStore = create((set, get) => ({
|
|||
};
|
||||
const res = await api.post('/auth/inscription', payload);
|
||||
api.setToken(res.access_token, false);
|
||||
localStorage.removeItem('gm_sysadmin_instance');
|
||||
localStorage.setItem('gm_groupe_id', data.groupe_id || '');
|
||||
set({
|
||||
token: res.access_token,
|
||||
membre: res.membre,
|
||||
sysadminInstance: false,
|
||||
loading: false,
|
||||
});
|
||||
await get().loadAccueil();
|
||||
|
|
@ -98,9 +103,12 @@ export const useStore = create((set, get) => ({
|
|||
try {
|
||||
const data = await api.post(`/auth/impersoner/${membreId}`);
|
||||
api.setToken(data.access_token, false);
|
||||
localStorage.removeItem('gm_sysadmin_instance');
|
||||
set({
|
||||
token: data.access_token,
|
||||
membre: data.membre,
|
||||
groupeId: data.membre.groupe_id,
|
||||
sysadminInstance: false,
|
||||
loading: false,
|
||||
});
|
||||
await get().loadAccueil();
|
||||
|
|
@ -132,6 +140,7 @@ export const useStore = create((set, get) => ({
|
|||
logout: () => {
|
||||
api.setToken(null);
|
||||
localStorage.removeItem('gm_groupe_id');
|
||||
localStorage.removeItem('gm_sysadmin_instance');
|
||||
localStorage.removeItem('gm_page');
|
||||
localStorage.removeItem('gm_nav_context');
|
||||
set({
|
||||
|
|
@ -139,6 +148,7 @@ export const useStore = create((set, get) => ({
|
|||
membre: null,
|
||||
groupe: null,
|
||||
groupeId: null,
|
||||
sysadminInstance: false,
|
||||
postes: [],
|
||||
estExecutif: false,
|
||||
estSysadmin: false,
|
||||
|
|
|
|||
Loading…
Reference in a new issue