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"))
|
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
|
jours = expire_jours or settings.JWT_EXPIRE_DAYS
|
||||||
expire = datetime.now(timezone.utc) + timedelta(days=jours)
|
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:
|
if admin_id:
|
||||||
payload["adm"] = str(admin_id)
|
payload["adm"] = str(admin_id)
|
||||||
return jwt.encode(payload, settings.JWT_SECRET, algorithm=settings.JWT_ALGORITHM)
|
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.membre import Membre
|
||||||
from app.models.reunion import Reunion
|
from app.models.reunion import Reunion
|
||||||
from app.models.historique_modification import HistoriqueModification
|
from app.models.historique_modification import HistoriqueModification
|
||||||
from app.schemas.schemas import MembrePublic, TokenResponse
|
|
||||||
|
|
||||||
router = APIRouter(prefix="/admin", tags=["Administration"])
|
router = APIRouter(prefix="/admin", tags=["Administration"])
|
||||||
|
|
||||||
|
|
@ -44,13 +43,25 @@ async def require_sysadmin(membre=Depends(get_current_membre)):
|
||||||
return 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)):
|
async def sysadmin_login(req: SysadminLoginRequest, db: AsyncSession = Depends(get_db)):
|
||||||
result = await db.execute(select(Membre).where(Membre.courriel == "sysadmin", Membre.actif == True))
|
result = await db.execute(select(Membre).where(Membre.courriel == "sysadmin", Membre.actif == True))
|
||||||
for membre in result.scalars().all():
|
for membre in result.scalars().all():
|
||||||
if verify_pin(req.pin, membre.pin_hash):
|
if verify_pin(req.pin, membre.pin_hash):
|
||||||
token = create_token(str(membre.id), str(membre.groupe_id), expire_jours=1)
|
token = create_token(str(membre.id), None, expire_jours=1)
|
||||||
return TokenResponse(access_token=token, membre=MembrePublic.model_validate(membre))
|
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")
|
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.decision_types import type_decision_depuis_proposition
|
||||||
from app.core.financial_position import PositionTresorerie
|
from app.core.financial_position import PositionTresorerie
|
||||||
from app.core.sanitization import safe_query_params
|
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.poste import Poste
|
||||||
from app.models.affectation import Affectation
|
from app.models.affectation import Affectation
|
||||||
from app.models.candidature import Candidature
|
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"})
|
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):
|
class MultiTenantTests(unittest.TestCase):
|
||||||
def test_postes_are_group_scoped(self):
|
def test_postes_are_group_scoped(self):
|
||||||
self.assertIn("groupe_id", Poste.__table__.columns)
|
self.assertIn("groupe_id", Poste.__table__.columns)
|
||||||
|
|
|
||||||
|
|
@ -36,14 +36,16 @@ import ModePapier from './pages/ModePapier';
|
||||||
import BottomNav from './components/BottomNav';
|
import BottomNav from './components/BottomNav';
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const { token, page, navContext, loadAccueil, loadGroupes } = useStore();
|
const { token, page, navContext, sysadminInstance, loadAccueil, loadGroupes } = useStore();
|
||||||
const chemin = window.location.pathname;
|
const chemin = window.location.pathname;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleUnauthorized = () => useStore.getState().logout();
|
const handleUnauthorized = () => useStore.getState().logout();
|
||||||
window.addEventListener('gm:unauthorized', handleUnauthorized);
|
window.addEventListener('gm:unauthorized', handleUnauthorized);
|
||||||
loadGroupes();
|
loadGroupes();
|
||||||
if (token) {
|
if (token && sysadminInstance) {
|
||||||
|
useStore.setState({ page: 'G14' });
|
||||||
|
} else if (token) {
|
||||||
loadAccueil().then(() => {
|
loadAccueil().then(() => {
|
||||||
// Restaurer la page sauvegardée ou aller à l'accueil
|
// Restaurer la page sauvegardée ou aller à l'accueil
|
||||||
const savedPage = localStorage.getItem('gm_page');
|
const savedPage = localStorage.getItem('gm_page');
|
||||||
|
|
@ -62,6 +64,9 @@ export default function App() {
|
||||||
|
|
||||||
// Pages protégées
|
// Pages protégées
|
||||||
if (token) {
|
if (token) {
|
||||||
|
if (sysadminInstance) {
|
||||||
|
return <main><GestionGroupes /></main>;
|
||||||
|
}
|
||||||
let pageActive;
|
let pageActive;
|
||||||
switch (page) {
|
switch (page) {
|
||||||
case 'accueil':
|
case 'accueil':
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ const jourLabel = (jour) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function Login() {
|
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 [courriel, setCourriel] = useState(localStorage.getItem('gm_courriel') || '');
|
||||||
const [pin, setPin] = useState('');
|
const [pin, setPin] = useState('');
|
||||||
const [groupeId, setGroupeId] = useState(localStorage.getItem('gm_groupe_id') || '');
|
const [groupeId, setGroupeId] = useState(localStorage.getItem('gm_groupe_id') || '');
|
||||||
|
|
@ -54,13 +54,22 @@ export default function Login() {
|
||||||
try {
|
try {
|
||||||
const data = await api.post('/admin/sysadmin-login', { pin });
|
const data = await api.post('/admin/sysadmin-login', { pin });
|
||||||
api.setToken(data.access_token, false);
|
api.setToken(data.access_token, false);
|
||||||
|
localStorage.setItem('gm_sysadmin_instance', '1');
|
||||||
|
localStorage.removeItem('gm_groupe_id');
|
||||||
useStore.setState({
|
useStore.setState({
|
||||||
token: data.access_token,
|
token: data.access_token,
|
||||||
membre: data.membre,
|
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,
|
error: null,
|
||||||
});
|
});
|
||||||
await loadAccueil();
|
|
||||||
setPage('G14');
|
setPage('G14');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
useStore.setState({ error: e.message });
|
useStore.setState({ error: e.message });
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ export const useStore = create((set, get) => ({
|
||||||
token: api.getToken(),
|
token: api.getToken(),
|
||||||
membre: null,
|
membre: null,
|
||||||
groupeId: localStorage.getItem('gm_groupe_id'),
|
groupeId: localStorage.getItem('gm_groupe_id'),
|
||||||
|
sysadminInstance: localStorage.getItem('gm_sysadmin_instance') === '1',
|
||||||
groupes: [],
|
groupes: [],
|
||||||
|
|
||||||
// Accueil
|
// Accueil
|
||||||
|
|
@ -49,6 +50,7 @@ export const useStore = create((set, get) => ({
|
||||||
courriel, pin, groupe_id: groupeId, se_souvenir: seSouvenir,
|
courriel, pin, groupe_id: groupeId, se_souvenir: seSouvenir,
|
||||||
});
|
});
|
||||||
api.setToken(data.access_token, seSouvenir);
|
api.setToken(data.access_token, seSouvenir);
|
||||||
|
localStorage.removeItem('gm_sysadmin_instance');
|
||||||
localStorage.setItem('gm_groupe_id', groupeId);
|
localStorage.setItem('gm_groupe_id', groupeId);
|
||||||
if (seSouvenir) {
|
if (seSouvenir) {
|
||||||
localStorage.setItem('gm_courriel', courriel);
|
localStorage.setItem('gm_courriel', courriel);
|
||||||
|
|
@ -59,6 +61,7 @@ export const useStore = create((set, get) => ({
|
||||||
token: data.access_token,
|
token: data.access_token,
|
||||||
membre: data.membre,
|
membre: data.membre,
|
||||||
groupeId,
|
groupeId,
|
||||||
|
sysadminInstance: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
});
|
});
|
||||||
await get().loadAccueil();
|
await get().loadAccueil();
|
||||||
|
|
@ -79,10 +82,12 @@ export const useStore = create((set, get) => ({
|
||||||
};
|
};
|
||||||
const res = await api.post('/auth/inscription', payload);
|
const res = await api.post('/auth/inscription', payload);
|
||||||
api.setToken(res.access_token, false);
|
api.setToken(res.access_token, false);
|
||||||
|
localStorage.removeItem('gm_sysadmin_instance');
|
||||||
localStorage.setItem('gm_groupe_id', data.groupe_id || '');
|
localStorage.setItem('gm_groupe_id', data.groupe_id || '');
|
||||||
set({
|
set({
|
||||||
token: res.access_token,
|
token: res.access_token,
|
||||||
membre: res.membre,
|
membre: res.membre,
|
||||||
|
sysadminInstance: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
});
|
});
|
||||||
await get().loadAccueil();
|
await get().loadAccueil();
|
||||||
|
|
@ -98,9 +103,12 @@ export const useStore = create((set, get) => ({
|
||||||
try {
|
try {
|
||||||
const data = await api.post(`/auth/impersoner/${membreId}`);
|
const data = await api.post(`/auth/impersoner/${membreId}`);
|
||||||
api.setToken(data.access_token, false);
|
api.setToken(data.access_token, false);
|
||||||
|
localStorage.removeItem('gm_sysadmin_instance');
|
||||||
set({
|
set({
|
||||||
token: data.access_token,
|
token: data.access_token,
|
||||||
membre: data.membre,
|
membre: data.membre,
|
||||||
|
groupeId: data.membre.groupe_id,
|
||||||
|
sysadminInstance: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
});
|
});
|
||||||
await get().loadAccueil();
|
await get().loadAccueil();
|
||||||
|
|
@ -132,6 +140,7 @@ export const useStore = create((set, get) => ({
|
||||||
logout: () => {
|
logout: () => {
|
||||||
api.setToken(null);
|
api.setToken(null);
|
||||||
localStorage.removeItem('gm_groupe_id');
|
localStorage.removeItem('gm_groupe_id');
|
||||||
|
localStorage.removeItem('gm_sysadmin_instance');
|
||||||
localStorage.removeItem('gm_page');
|
localStorage.removeItem('gm_page');
|
||||||
localStorage.removeItem('gm_nav_context');
|
localStorage.removeItem('gm_nav_context');
|
||||||
set({
|
set({
|
||||||
|
|
@ -139,6 +148,7 @@ export const useStore = create((set, get) => ({
|
||||||
membre: null,
|
membre: null,
|
||||||
groupe: null,
|
groupe: null,
|
||||||
groupeId: null,
|
groupeId: null,
|
||||||
|
sysadminInstance: false,
|
||||||
postes: [],
|
postes: [],
|
||||||
estExecutif: false,
|
estExecutif: false,
|
||||||
estSysadmin: false,
|
estSysadmin: false,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue