Edoruin commited on
Commit
1ac174c
·
1 Parent(s): 33ea8ba

password recuperation scripts

Browse files
Files changed (3) hide show
  1. README.md +32 -8
  2. app/main.py +141 -40
  3. app/requirements.txt +2 -0
README.md CHANGED
@@ -19,13 +19,31 @@ Aplicación web para gestionar préstamos de herramientas y proyectos en un Make
19
  - 🛠️ **Sistema de préstamos** de herramientas y dispositivos
20
  - 📱 **Integración con Telegram** para notificaciones y aprobaciones en tiempo real
21
  - 📊 **Gestión de proyectos GitLab**
22
- - 💾 **Persistencia de datos** en archivos JSON
23
 
24
  ## 🚀 Configuración
25
 
26
- ### Variables de Entorno (Repository Secrets)
27
 
28
- Configura las siguientes variables en **Settings → Repository secrets**:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
 
30
  | Variable | Descripción |
31
  |----------|-------------|
@@ -36,13 +54,19 @@ Configura las siguientes variables en **Settings → Repository secrets**:
36
  | `GITLAB_GROUP_ID` | ID del grupo de GitLab |
37
  | `RESEND_API_KEY` | API key de Resend para emails |
38
  | `FROM_EMAIL` | Email remitente verificado en Resend |
39
- | `SECRET_KEY` | Clave secreta de Flask (genera con: `python3 -c "import secrets; print(secrets.token_hex(32))"`) |
40
 
41
- ## ⚠️ Persistencia de Datos
 
 
 
 
 
 
42
 
43
- > **Importante:** Hugging Face Spaces NO soporta volúmenes persistentes. Los datos se guardan temporalmente en `/app/data` pero se perderán al reiniciar el Space.
 
44
 
45
- Para producción, considera implementar una solución de persistencia externa. Ver [HUGGINGFACE_PERSISTENCE.md](HUGGINGFACE_PERSISTENCE.md) para opciones.
46
 
47
  ## 🔧 Tecnologías
48
 
@@ -51,7 +75,7 @@ Para producción, considera implementar una solución de persistencia externa. V
51
  - **Notificaciones:** Telegram Bot API
52
  - **Email:** Resend API
53
  - **Integración:** GitLab API
54
- - **Persistencia:** JSON (temporal en HF Spaces)
55
  - **Despliegue:** Docker + Gunicorn + Eventlet
56
 
57
  ## 📝 Licencia
 
19
  - 🛠️ **Sistema de préstamos** de herramientas y dispositivos
20
  - 📱 **Integración con Telegram** para notificaciones y aprobaciones en tiempo real
21
  - 📊 **Gestión de proyectos GitLab**
22
+ - 💾 **Persistencia de datos** con Hugging Face Datasets
23
 
24
  ## 🚀 Configuración
25
 
26
+ ### 1. Configurar Variables de Entorno
27
 
28
+ En **Settings → Repository secrets**, configura las siguientes variables:
29
+
30
+ #### Variables Obligatorias
31
+
32
+ | Variable | Descripción |
33
+ |----------|-------------|
34
+ | `SECRET_KEY` | Clave secreta de Flask (genera con: `python3 -c "import secrets; print(secrets.token_hex(32))"`) |
35
+
36
+ #### Variables para Persistencia (Hugging Face Datasets)
37
+
38
+ | Variable | Descripción |
39
+ |----------|-------------|
40
+ | `HF_TOKEN` | Token de Hugging Face con permisos de escritura ([crear aquí](https://huggingface.co/settings/tokens)) |
41
+ | `HF_DATASET_USERS` | Nombre del dataset para usuarios (ej: `tu-usuario/makerpage-users`) |
42
+ | `HF_DATASET_LOANS` | Nombre del dataset para préstamos (ej: `tu-usuario/makerpage-loans`) |
43
+
44
+ > **Nota:** Los datasets deben ser **privados** (contienen contraseñas hasheadas). Se crearán automáticamente al guardar el primer registro.
45
+
46
+ #### Variables Opcionales (Funcionalidades Adicionales)
47
 
48
  | Variable | Descripción |
49
  |----------|-------------|
 
54
  | `GITLAB_GROUP_ID` | ID del grupo de GitLab |
55
  | `RESEND_API_KEY` | API key de Resend para emails |
56
  | `FROM_EMAIL` | Email remitente verificado en Resend |
 
57
 
58
+ ### 2. Desplegar
59
+
60
+ El Space se desplegará automáticamente al hacer push al repositorio.
61
+
62
+ ## 💾 Persistencia de Datos
63
+
64
+ La aplicación usa **Hugging Face Datasets** para almacenamiento persistente:
65
 
66
+ - **Con HF_TOKEN configurado:** Los datos se guardan en datasets privados de HF y persisten permanentemente
67
+ - ⚠️ **Sin HF_TOKEN:** Los datos se guardan localmente en `/app/data` (temporales, se pierden al reiniciar)
68
 
69
+ **Recomendación:** Configura `HF_TOKEN`, `HF_DATASET_USERS` y `HF_DATASET_LOANS` para garantizar persistencia en producción.
70
 
71
  ## 🔧 Tecnologías
72
 
 
75
  - **Notificaciones:** Telegram Bot API
76
  - **Email:** Resend API
77
  - **Integración:** GitLab API
78
+ - **Persistencia:** Hugging Face Datasets (con fallback a JSON local)
79
  - **Despliegue:** Docker + Gunicorn + Eventlet
80
 
81
  ## 📝 Licencia
app/main.py CHANGED
@@ -20,6 +20,15 @@ import markdown
20
  from dotenv import load_dotenv
21
  import resend
22
 
 
 
 
 
 
 
 
 
 
23
  # Cargar variables de entorno desde .env
24
  load_dotenv()
25
 
@@ -35,36 +44,115 @@ login_manager.login_view = 'login'
35
  login_manager.login_message = "Por favor, inicia sesión para acceder."
36
  login_manager.login_message_category = "red"
37
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
  class User(UserMixin):
39
  """Clase de usuario compatible con Flask-Login."""
40
  def __init__(self, id, username):
41
  self.id = id
42
  self.username = username
43
 
44
- # --- GESTOR DE USUARIOS (Persistencia JSON) ---
45
- class UserManager:
46
- """Clase para manejar los usuarios en un archivo JSON."""
47
- def __init__(self, filename="users.json"):
48
- # Usar directorio de datos configurable (para volúmenes Docker)
49
- data_dir = os.getenv("DATA_DIR", "/app/data")
50
- os.makedirs(data_dir, exist_ok=True)
51
- self.filename = os.path.join(data_dir, filename)
 
 
52
  self.users = self._load()
53
-
54
  def _load(self):
55
- """Carga los usuarios desde el archivo JSON."""
56
- if os.path.exists(self.filename):
57
- try:
58
- with open(self.filename, "r") as f:
59
- return json.load(f)
60
- except:
61
- return []
62
- return []
 
 
 
63
 
64
  def save(self):
65
- """Guarda la lista de usuarios en el archivo JSON."""
66
- with open(self.filename, "w") as f:
67
- json.dump(self.users, f, indent=4)
 
 
 
 
 
 
 
 
68
 
69
  def add_user(self, username, password, email, status="PENDING"):
70
  """Registra un nuevo usuario con email, contraseña hasheada y estado pendiente."""
@@ -165,30 +253,43 @@ def load_user(user_id):
165
  return User(user_data['id'], user_data['username'])
166
  return None
167
 
168
- # --- GESTOR DE PRÉSTAMOS (Persistencia JSON) ---
169
- class LoanManager:
170
- """Clase para manejar la persistencia de préstamos en un archivo JSON."""
171
- def __init__(self, filename="prestamos.json"):
172
- # Usar directorio de datos configurable (para volúmenes Docker)
173
- data_dir = os.getenv("DATA_DIR", "/app/data")
174
- os.makedirs(data_dir, exist_ok=True)
175
- self.filename = os.path.join(data_dir, filename)
 
 
176
  self.loans = self._load()
177
-
178
  def _load(self):
179
- """Carga los préstamos desde el archivo JSON."""
180
- if os.path.exists(self.filename):
181
- try:
182
- with open(self.filename, "r") as f:
183
- return json.load(f)
184
- except:
185
- return []
186
- return []
 
 
 
187
 
188
  def save(self):
189
- """Guarda la lista actual de préstamos en el archivo JSON."""
190
- with open(self.filename, "w") as f:
191
- json.dump(self.loans, f, indent=4)
 
 
 
 
 
 
 
 
192
 
193
  def add_loan(self, loan):
194
  """Añade un nuevo préstamo a la lista y guarda en disco."""
 
20
  from dotenv import load_dotenv
21
  import resend
22
 
23
+ # Importar Hugging Face Datasets para persistencia
24
+ try:
25
+ from datasets import Dataset, load_dataset
26
+ from huggingface_hub import HfApi
27
+ HF_AVAILABLE = True
28
+ except ImportError:
29
+ HF_AVAILABLE = False
30
+ print("[WARNING] huggingface_hub/datasets not available. Using local JSON storage only.")
31
+
32
  # Cargar variables de entorno desde .env
33
  load_dotenv()
34
 
 
44
  login_manager.login_message = "Por favor, inicia sesión para acceder."
45
  login_manager.login_message_category = "red"
46
 
47
+ # --- GESTOR DE DATASETS DE HUGGING FACE ---
48
+ class HFDatasetManager:
49
+ """Clase base para manejar persistencia con Hugging Face Datasets."""
50
+
51
+ def __init__(self, dataset_name, data_key, local_filename):
52
+ self.dataset_name = os.getenv(dataset_name)
53
+ self.data_key = data_key
54
+ self.hf_token = os.getenv("HF_TOKEN")
55
+ self.use_hf = HF_AVAILABLE and self.dataset_name and self.hf_token
56
+
57
+ # Configurar almacenamiento local como fallback
58
+ data_dir = os.getenv("DATA_DIR", "/app/data")
59
+ os.makedirs(data_dir, exist_ok=True)
60
+ self.local_file = os.path.join(data_dir, local_filename)
61
+
62
+ print(f"[{self.__class__.__name__}] HF Datasets: {'ENABLED' if self.use_hf else 'DISABLED (using local JSON)'}")
63
+ if self.use_hf:
64
+ print(f"[{self.__class__.__name__}] Dataset: {self.dataset_name}")
65
+
66
+ def _load_from_hf(self):
67
+ """Carga datos desde Hugging Face Dataset."""
68
+ try:
69
+ dataset = load_dataset(self.dataset_name, split="train", token=self.hf_token)
70
+ if len(dataset) > 0:
71
+ return dataset[0][self.data_key]
72
+ return []
73
+ except Exception as e:
74
+ print(f"[HF ERROR] Error loading from dataset: {e}")
75
+ return None
76
+
77
+ def _load_from_local(self):
78
+ """Carga datos desde archivo JSON local."""
79
+ if os.path.exists(self.local_file):
80
+ try:
81
+ with open(self.local_file, "r") as f:
82
+ return json.load(f)
83
+ except:
84
+ return []
85
+ return []
86
+
87
+ def _save_to_hf(self, data):
88
+ """Guarda datos en Hugging Face Dataset."""
89
+ try:
90
+ dataset_dict = {self.data_key: [data]}
91
+ dataset = Dataset.from_dict(dataset_dict)
92
+ dataset.push_to_hub(
93
+ self.dataset_name,
94
+ token=self.hf_token,
95
+ private=True
96
+ )
97
+ print(f"[HF SUCCESS] Data saved to {self.dataset_name}")
98
+ return True
99
+ except Exception as e:
100
+ print(f"[HF ERROR] Error saving to dataset: {e}")
101
+ return False
102
+
103
+ def _save_to_local(self, data):
104
+ """Guarda datos en archivo JSON local."""
105
+ try:
106
+ with open(self.local_file, "w") as f:
107
+ json.dump(data, f, indent=4)
108
+ return True
109
+ except Exception as e:
110
+ print(f"[LOCAL ERROR] Error saving to local file: {e}")
111
+ return False
112
+
113
  class User(UserMixin):
114
  """Clase de usuario compatible con Flask-Login."""
115
  def __init__(self, id, username):
116
  self.id = id
117
  self.username = username
118
 
119
+ # --- GESTOR DE USUARIOS (Persistencia con HF Datasets) ---
120
+ class UserManager(HFDatasetManager):
121
+ """Clase para manejar los usuarios con persistencia en HF Datasets."""
122
+
123
+ def __init__(self):
124
+ super().__init__(
125
+ dataset_name="HF_DATASET_USERS",
126
+ data_key="users",
127
+ local_filename="users.json"
128
+ )
129
  self.users = self._load()
130
+
131
  def _load(self):
132
+ """Carga los usuarios desde HF Dataset o archivo JSON local."""
133
+ if self.use_hf:
134
+ data = self._load_from_hf()
135
+ if data is not None:
136
+ print(f"[UserManager] Loaded {len(data)} users from HF Dataset")
137
+ return data
138
+ print("[UserManager] Failed to load from HF, trying local...")
139
+
140
+ data = self._load_from_local()
141
+ print(f"[UserManager] Loaded {len(data)} users from local JSON")
142
+ return data
143
 
144
  def save(self):
145
+ """Guarda la lista de usuarios en HF Dataset y/o archivo JSON."""
146
+ # Siempre guardar localmente como backup
147
+ self._save_to_local(self.users)
148
+
149
+ # Intentar guardar en HF si está habilitado
150
+ if self.use_hf:
151
+ success = self._save_to_hf(self.users)
152
+ if success:
153
+ print(f"[UserManager] Saved {len(self.users)} users to HF Dataset")
154
+ else:
155
+ print("[UserManager] Failed to save to HF, data saved locally only")
156
 
157
  def add_user(self, username, password, email, status="PENDING"):
158
  """Registra un nuevo usuario con email, contraseña hasheada y estado pendiente."""
 
253
  return User(user_data['id'], user_data['username'])
254
  return None
255
 
256
+ # --- GESTOR DE PRÉSTAMOS (Persistencia con HF Datasets) ---
257
+ class LoanManager(HFDatasetManager):
258
+ """Clase para manejar la persistencia de préstamos en HF Datasets."""
259
+
260
+ def __init__(self):
261
+ super().__init__(
262
+ dataset_name="HF_DATASET_LOANS",
263
+ data_key="loans",
264
+ local_filename="prestamos.json"
265
+ )
266
  self.loans = self._load()
267
+
268
  def _load(self):
269
+ """Carga los préstamos desde HF Dataset o archivo JSON local."""
270
+ if self.use_hf:
271
+ data = self._load_from_hf()
272
+ if data is not None:
273
+ print(f"[LoanManager] Loaded {len(data)} loans from HF Dataset")
274
+ return data
275
+ print("[LoanManager] Failed to load from HF, trying local...")
276
+
277
+ data = self._load_from_local()
278
+ print(f"[LoanManager] Loaded {len(data)} loans from local JSON")
279
+ return data
280
 
281
  def save(self):
282
+ """Guarda la lista actual de préstamos en HF Dataset y/o archivo JSON."""
283
+ # Siempre guardar localmente como backup
284
+ self._save_to_local(self.loans)
285
+
286
+ # Intentar guardar en HF si está habilitado
287
+ if self.use_hf:
288
+ success = self._save_to_hf(self.loans)
289
+ if success:
290
+ print(f"[LoanManager] Saved {len(self.loans)} loans to HF Dataset")
291
+ else:
292
+ print("[LoanManager] Failed to save to HF, data saved locally only")
293
 
294
  def add_loan(self, loan):
295
  """Añade un nuevo préstamo a la lista y guarda en disco."""
app/requirements.txt CHANGED
@@ -9,3 +9,5 @@ gunicorn
9
  eventlet
10
  markdown
11
  resend
 
 
 
9
  eventlet
10
  markdown
11
  resend
12
+ datasets
13
+ huggingface_hub