diff --git a/ansible/playbook-fail2ban.yml b/ansible/playbook-fail2ban.yml new file mode 100644 index 00000000..0c6e3bf0 --- /dev/null +++ b/ansible/playbook-fail2ban.yml @@ -0,0 +1,142 @@ +--- +# ============================================ +# Fail2Ban Installation et configuration +# ============================================ +- name: Installation et configuration de Fail2Ban + hosts: uber-stup + become: true + gather_facts: true + + vars: + fail2ban_bantime: "1h" + fail2ban_findtime: "10m" + fail2ban_maxretry: 5 + fail2ban_ignoreip: "127.0.0.1/8 ::1" + + tasks: + # ============================================================ + # Installation + # ============================================================ + - name: Installer Fail2Ban + ansible.builtin.apt: + name: fail2ban + state: present + update_cache: yes + + # ============================================================ + # Configuration globale (jail.local) + # ============================================================ + - name: Créer jail.local avec la configuration globale + ansible.builtin.copy: + dest: /etc/fail2ban/jail.local + owner: root + group: root + mode: "0644" + content: | + [DEFAULT] + ignoreip = {{ fail2ban_ignoreip }} + bantime = {{ fail2ban_bantime }} + findtime = {{ fail2ban_findtime }} + maxretry = {{ fail2ban_maxretry }} + banaction = iptables-multiport + backend = systemd + loglevel = INFO + + # ============================================================ + # SSH + # ============================================================ + [sshd] + enabled = true + port = ssh + filter = sshd + logpath = %(sshd_log)s + maxretry = 3 + bantime = 24h + + # ============================================================ + # Nginx - requêtes trop fréquentes (rate limit) + # ============================================================ + [nginx-limit-req] + enabled = true + port = http,https + filter = nginx-limit-req + logpath = /var/log/nginx/error.log + maxretry = 10 + bantime = 1h + + # ============================================================ + # Nginx - 4xx répétés (scan, bot) + # ============================================================ + [nginx-http-auth] + enabled = true + port = http,https + filter = nginx-http-auth + logpath = /var/log/nginx/error.log + maxretry = 5 + bantime = 1h + + # ============================================================ + # Nginx - bad bots / scans (404 répétés) + # ============================================================ + [nginx-botsearch] + enabled = true + port = http,https + filter = nginx-botsearch + logpath = /var/log/nginx/access.log + maxretry = 10 + bantime = 2h + + # ============================================================ + # API Backend - brute force login (POST /api/v2/login) + # ============================================================ + [backend-login] + enabled = true + port = http,https + filter = backend-login + logpath = /var/log/nginx/access.log + maxretry = 10 + findtime = 5m + bantime = 2h + notify: Restart fail2ban + + # ============================================================ + # Filtre personnalisé - brute force login API + # ============================================================ + - name: Créer le filtre fail2ban pour le login API + ansible.builtin.copy: + dest: /etc/fail2ban/filter.d/backend-login.conf + owner: root + group: root + mode: "0644" + content: | + [Definition] + failregex = ^ .* "POST /api/v2/login HTTP/.*" (401|403) .*$ + ignoreregex = + notify: Restart fail2ban + + # ============================================================ + # Démarrage et activation + # ============================================================ + - name: Démarrer et activer Fail2Ban + ansible.builtin.systemd: + name: fail2ban + state: started + enabled: yes + + # ============================================================ + # Vérification + # ============================================================ + - name: Vérifier le statut de Fail2Ban + ansible.builtin.command: fail2ban-client status + register: fail2ban_status + changed_when: false + + - name: Afficher le statut des jails + ansible.builtin.debug: + msg: "{{ fail2ban_status.stdout_lines }}" + + handlers: + - name: Restart fail2ban + ansible.builtin.systemd: + name: fail2ban + state: restarted diff --git a/ansible/playbook.yml b/ansible/playbook.yml index a2795223..7b6e0961 100644 --- a/ansible/playbook.yml +++ b/ansible/playbook.yml @@ -120,6 +120,80 @@ privs: ALL state: present + # ============================================================ + # Données initiales - corrections d'adresses + # ============================================================ + - name: Insérer les corrections d'adresses + become_user: postgres + ansible.builtin.postgresql_query: + db: "{{ db_name }}" + query: | + INSERT INTO adresse_correction (invalid_address, correct_address) VALUES + ('6 Place Alfred Lallié 44000 Nantes', '13 Rue Saint-Rogatien 44000 Nantes'), + ('26 boulevard Vincent gâche 44200', '62 Rue des Français Libres 44200'), + ('41 quai Malakoff 44000', '41 rue du pré Gauchet 44000'), + ('au 6 rue du Rhône 44100', '106 rue du bois Hardy 44100'), + ('10 allée de l''île gloriette 44000', '8 rue Deurbroucq 44000'), + ('1 Ter Boulevard de Launay 44100', '5 Rue Lavoisier 44100'), + ('6 Boulevard de la prairie au duc 44200', '10 Rue Alain Barbe Torte 44200'), + ('4 rue de mauves , 44470 thouaré sur Loire', '4 Rue de la Halbarderie 44470 Thouaré-sur-Loire'), + ('26 rue Louis blanc, 44200 Nantes', '6 Rue de Hercé 44200 Nantes'), + ('4 rue la tour d''Auvergne 44200 nantes', '6 Rue de Hercé 44200 Nantes'), + ('12 rue d''Orléans 44000', '10 rue de l''industrie 44000'), + ('3 rue Bertrand Geslin 44000 Nantes', '10 Rue de Sévigné 44000 Nantes'), + ('33 boulevard de doulon 44300', '3 Avenue du Forgeron 44300 Nantes'), + ('2 rue Stanislas Baudry 44000 Nantes', '9 Rue de Richebourg 44000 Nantes'), + ('8 Place Saint-Pierre, 44000 Nantes', '2 Rue Saint-Denis 44000 Nantes'), + ('20 boulevard du petit port 44300', '3 Rue du Transvaal 44300 Nantes'), + ('5 cours Sully 44000 Nantes', '75 Rue Préfet Bonnefoy 44000'), + ('50 rue Paul Bellamy 44000', '2 rue du docteur Heurteaux 44000'), + ('33 rue de la petite sensive 44300', '8 Rue du Docteur Chenantais 44300'), + ('45 rue Louis guiotton 44300 Nantes', '59 Rue de Nancy 44300 Nantes'), + ('6 rue Meuris 44000 Nantes', '3 Rue Daubenton 44100 Nantes'), + ('26 rue Félix Faure 44000', '12 rue bonaparte 44000'), + ('1 rue Charles Dullin 44100 nantes', '12 Avenue de l''Églantine 44800 Saint-Herblain'), + ('16 rue Anatole de monzie 44200', '1 Rue Georges Leygues 44200 Nantes'), + ('52 boulevard de l''Estuaire 44200', '1 Rue Ruth First 44200 Nantes'), + ('186 Route de Saint-Joseph 44300 Nantes', 'Rue de Pontecorvo 44300 Nantes'), + ('16 Rue Jules Launey 44100 Nantes', '5 Chemin du Pressoir Chênaie 44100 Nantes'), + ('1 rue maréchal Lyautey 44000', '9 rue Louis Mékarski 44000'), + ('74 boulevard de la prairie au duc 44200', '7 Rue René Siegfried 44200'), + ('1 impasse du baut 44300 Nantes', '8 Avenue d''Assise 44300 Nantes'), + ('5 rue moquechien, 44000 Nantes', '4 Rue Adolphe Moitié 44000 Nantes'), + ('21 chaussée de la madeleine 44000', '4 bis Rue Perrault 44000 Nantes'), + ('31 boulevard Michelet 44000', '26 Rue de la Haute Forêt 44000 Nantes'), + ('95 Boulevard longchamp 44300', '7 Petite Avenue de Longchamp 44300'), + ('7 Quai Marcel Boissard, 44400 Rezé', '6 Rue Jean Jouneau 44400 Rezé'), + ('43 avenue de la libération 44400', '10 rue de l''Erdronnière 44400'), + ('165 route de rennes 44700 orvault', '5 Rue de l''Ascension 44700 Orvault'), + ('17rue yves bodiguel 44000', '8 Rue Frédureau 44000 Nantes'), + ('176 boulevard Jules verne 44300', '44 Rue des Platanes 44300 Nantes'), + ('133 Boulevard Jules Verne 44300', '28 Rue Moriceau Thébault 44300'), + ('53 Boulevard Jules Verne 44300', '66 Rue de la Moisdonnière 44300'), + ('7 chaussé de la madeleine, 44000', '3 Rue Perrault 44000 Nantes'), + ('LA CHEVROLIÈRE', '12 Rue des Ajoncs 44400 Rezé'), + ('9 rue Delattre de tassigny 44000 nantes', '1 Rue des Cadeniers 44000 Nantes'), + ('14 place de l''Oratoire 44000 Nantes', '2 Rue Élie Delaunay 44000 Nantes'), + ('19 allée Baco, 44000 Nantes', '23 Rue Crucy 44000 Nantes'), + ('21bd babin chevaye 44200', '7 Rue de Hercé 44200 Nantes'), + ('49 boulevard dalby 44000', '51 Rue de la Ville en Pierre 44000'), + ('96 route de Vannes 44100', '14 Avenue des Coucous 44300'), + ('1 rue du Luxembourg 44000 Nantes', '5 Rue de l''Indre 44000 Nantes'), + ('3 rue de bretagne 44880 Sautron', '9 Rue de Flore 44880 Sautron'), + ('42 rue paul bellamy 44000 nantes', '5 Rue de Bouillé 44000 Nantes'), + ('125 Rue Paul Bellamy 44000 Nantes', '14 Rue Anatole le Braz 44000 Nantes'), + ('214 Rue Paul Bellamy 44000 Nantes', '17 Rue de la Saulzinière 44000 Nantes'), + ('49 Boulevard Robert Schuman 44300 Nantes', '47 Rue Georges Bizot 44300 Nantes'), + ('142 Boulevard Robert Schuman 44300', '10 Rue St-Mihiel 44300 Nantes'), + ('23 route de la chapelle sur erdre 44300', '14 Rue de la Boulonnerie 44300 Nantes'), + ('27 Rue Pierre Blard 44800 Saint-Herblain', '2 Rue Louis Boutin 44800 Saint-Herblain'), + ('109 Boulevard Ernest Dalby 44000 Nantes', '53 bis rue de la ville en pierre 44000'), + ('13 rue pitre chevalier 44000', '10 Rue Jean-Émile Laboureur 44000'), + ('Rue lesage - 44000 - Nantes', '4 rue Maurice Sibille 44000'), + ('86 Rue de la Bourgeonnière 44300', '6 Avenue de l''Alverne 44300'), + ('8 boulevard Émile romanet 44100', '33 Route de St-Herblain 44100 Nantes') + ON CONFLICT (invalid_address) DO NOTHING; + # ============================================================ # Authentification pg_hba.conf # ============================================================ diff --git a/backend/gestion/db/db_address.go b/backend/gestion/db/db_address.go index e83a63eb..c8b0ddcb 100644 --- a/backend/gestion/db/db_address.go +++ b/backend/gestion/db/db_address.go @@ -47,3 +47,21 @@ func (d *Database) DeleteAddress(InvalidAddressByAdmin string, CorrectAddressByA } return nil } + +func (d *Database) AllAddress() ([]models.Address, error) { + rows, err := d.DB.Query("SELECT invalid_address, correct_address FROM adresse_correction") + if err != nil { + return nil, fmt.Errorf("getAllAddress: %w", err) + } + defer rows.Close() + + var addresses []models.Address + for rows.Next() { + var a models.Address + if err := rows.Scan(&a.InvalidAddress, &a.CorrectAddress); err != nil { + return nil, fmt.Errorf("getAllAddress scan: %w", err) + } + addresses = append(addresses, a) + } + return addresses, rows.Err() +} diff --git a/backend/gestion/db/db_init.go b/backend/gestion/db/db_init.go index 90e50a0e..0b7e6dd6 100644 --- a/backend/gestion/db/db_init.go +++ b/backend/gestion/db/db_init.go @@ -255,7 +255,7 @@ func (db *Database) createTables() error { `CREATE TABLE IF NOT EXISTS adresse_correction ( id SERIAL PRIMARY KEY, invalid_address VARCHAR(255) NOT NULL UNIQUE, - correct_address VARCHAR(255) NOT NULL UNIQUE, + correct_address VARCHAR(255) NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP );`, diff --git a/backend/gestion/handlers/address.go b/backend/gestion/handlers/address.go index 349eda8f..b40fff63 100644 --- a/backend/gestion/handlers/address.go +++ b/backend/gestion/handlers/address.go @@ -57,3 +57,20 @@ func DeleteAddress(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"message": "Adresses supprimées avec succès"}) } + +func GetAllAddress(c *gin.Context) { + database := c.MustGet("database").(*db.Database) + userRole := c.GetString("role") + if userRole != "admin" { + c.JSON(http.StatusForbidden, gin.H{"error": "Accès réservé aux administrateurs"}) + return + } + + getAddress, err := database.AllAddress() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{"addresses": getAddress}) +} diff --git a/backend/gestion/routes/routes.go b/backend/gestion/routes/routes.go index 2f366b08..35e62021 100644 --- a/backend/gestion/routes/routes.go +++ b/backend/gestion/routes/routes.go @@ -135,6 +135,7 @@ func SetupRoutes(router *gin.Engine, database *db.Database, geoService *services // ============================================ adminGroupV2.POST("/add/address", handlers.AddAddress) adminGroupV2.DELETE("/delete/address", handlers.DeleteAddress) + adminGroupV2.GET("/addresses", handlers.GetAllAddress) // ============================================ // PRODUITS - GESTION // ============================================ diff --git a/frontend-admin/src/api/api_admin.ts b/frontend-admin/src/api/api_admin.ts index 469e78fb..f162445d 100644 --- a/frontend-admin/src/api/api_admin.ts +++ b/frontend-admin/src/api/api_admin.ts @@ -503,6 +503,13 @@ export const deleteAddress = async ( return { success: true, message: data.message }; }; +export const getAllAddresses = async (): Promise< + { invalid_address: string; correct_address: string }[] +> => { + const { data } = await apiClient.get(`${V2}/admin/protected/addresses`); + return data.addresses ?? []; +}; + // ============================================ // STATS HELPERS // ============================================ diff --git a/frontend-admin/src/screens/admin/AddressScreen.tsx b/frontend-admin/src/screens/admin/AddressScreen.tsx index 533d3abb..9b663a8e 100644 --- a/frontend-admin/src/screens/admin/AddressScreen.tsx +++ b/frontend-admin/src/screens/admin/AddressScreen.tsx @@ -1,15 +1,16 @@ -import React, { useState, useCallback, useMemo } from "react"; +import React, { useState, useEffect, useCallback, useMemo } from "react"; import { View, Text, StyleSheet, FlatList, TouchableOpacity, + RefreshControl, } from "react-native"; import { Ionicons } from "@expo/vector-icons"; import { spacing, fontSize } from "../../theme"; import { useTheme } from "../../context/ThemeContext"; -import { addAddress, deleteAddress } from "../../api/api_admin"; +import { addAddress, deleteAddress, getAllAddresses } from "../../api/api_admin"; import { useAlert } from "../../hooks/useAlert"; import Card from "../../components/ui/Card"; import Modal from "../../components/ui/Modal"; @@ -27,11 +28,34 @@ export default function AddressScreen() { const { alert, showError, showSuccess, showConfirm, hideAlert } = useAlert(); const [addresses, setAddresses] = useState([]); + const [loading, setLoading] = useState(true); + const [refreshing, setRefreshing] = useState(false); const [addModal, setAddModal] = useState(false); const [invalidInput, setInvalidInput] = useState(""); const [correctInput, setCorrectInput] = useState(""); const [saving, setSaving] = useState(false); + const loadAddresses = useCallback(async () => { + try { + const result = await getAllAddresses(); + setAddresses(result); + } catch (e: any) { + showError("Erreur", e.response?.data?.error ?? e.message); + } finally { + setLoading(false); + } + }, [showError]); + + useEffect(() => { + loadAddresses(); + }, [loadAddresses]); + + const onRefresh = async () => { + setRefreshing(true); + await loadAddresses(); + setRefreshing(false); + }; + const openAddModal = () => { setInvalidInput(""); setCorrectInput(""); @@ -167,9 +191,14 @@ export default function AddressScreen() { keyExtractor={(item, i) => `${item.invalid_address}-${i}`} renderItem={renderItem} contentContainerStyle={{ padding: spacing.l, paddingTop: spacing.s }} + refreshControl={ + + } ListEmptyComponent={ - Aucune correction.{"\n"}Appuyez sur + pour en ajouter une. + {loading + ? "Chargement..." + : "Aucune correction.\nAppuyez sur + pour en ajouter une."} } />