1919along with this program. If not, see <https://www.gnu.org/licenses/>.
2020
2121'''
22-
2322from flask import Flask , request , jsonify , redirect , url_for , send_from_directory , render_template
2423from flask_socketio import SocketIO , join_room , leave_room , send
2524from flask_login import LoginManager , UserMixin , login_user , logout_user , login_required , current_user
2625from werkzeug .utils import secure_filename
2726from flask_argon2 import Argon2
28- import os ,json
27+ import os , json
2928from argon2 import PasswordHasher
3029
3130# --- CONFIGURACIÓN INICIAL ---
3231app = Flask (__name__ )
33- #socketio = SocketIO(app, async_mode="eventlet")
34- socketio = SocketIO (app , cors_allowed_origins = "*" ) # este es el server real
35-
36-
37- # Se recomienda usar una variable de entorno para la clave secreta
32+ socketio = SocketIO (app , cors_allowed_origins = "*" ) # SocketIO envuelve a Flask
3833app .secret_key = os .environ .get ("SECRET_KEY" , "a-very-secret-key-for-dev" )
3934argon2 = Argon2 (app )
40- #socketio = SocketIO(app )
35+ ph = PasswordHasher ( )
4136
42- # --- CONFIGURACIÓN DE CARPETAS Y USUARIOS ---
37+ # --- CONFIGURACIÓN DE CARPETAS ---
4338UPLOAD_FOLDER = './cuarentena'
4439os .makedirs (UPLOAD_FOLDER , exist_ok = True )
4540app .config ['UPLOAD_FOLDER' ] = UPLOAD_FOLDER
4641
47- # En un entorno de producción, las contraseñas nunca deben estar hardcodeadas.
48- # Se generan hashes para las contraseñas por defecto.
49- '''
50- usuarios = {
51- 'admin': {'password': argon2.generate_password_hash('admin123'), 'rol': 'administrator'},
52- 'cliente': {'password': argon2.generate_password_hash('cliente123'), 'rol': 'cliente'},
53- 'usuario': {'password': argon2.generate_password_hash('usuario123'), 'rol': 'usuario'},
54- }
55- '''
56- #import os, json
57- #from argon2 import PasswordHasher
58-
59- ph = PasswordHasher ()
60-
61- # Diccionario inicial con los 3 base
42+ # --- USUARIOS BASE ---
6243users = {
6344 os .getenv ("ADMIN_USER" , "admin" ): {
6445 "password" : ph .hash (os .getenv ("ADMIN_PASS" , "admin123" )),
7455 }
7556}
7657
77- # Cargar DEMO_USERS desde variable de entorno
78- demo_users_env = os .getenv ("DEMO_USERS" , "[]" )
79- # import os, json
80-
81- demo_users = json .loads (os .getenv ("DEMO_USERS" , "[]" ))
58+ # --- DEMO USERS DESDE ENV ---
8259try :
83- for user in demo_users :
84- print ("Cargando usuario demo:" , user ["username" ], "rol:" , user ["role" ])
85- demo_users = json .loads (demo_users_env )
86- for u in demo_users :
87- users [u ["username" ]] = {
88- "password" : ph .hash (u ["password" ]),
89- "role" : u .get ("role" , u ['role' ])
90- }
91- except Exception as e :
92- print (f"[WARN] No se pudieron cargar demo_users: { e } " )
93-
94- '''try:
60+ demo_users_env = os .getenv ("DEMO_USERS" , "[]" )
9561 demo_users = json .loads (demo_users_env )
9662 for u in demo_users :
9763 users [u ["username" ]] = {
10066 }
10167except Exception as e :
10268 print (f"[WARN] No se pudieron cargar demo_users: { e } " )
103- '''
10469
105- # --- GESTIÓN DE LOGIN Y USUARIOS ---
70+ # --- LOGIN MANAGER ---
10671login_manager = LoginManager (app )
107- login_manager .login_view = 'login' # Redirige a /login si no está autenticado
72+ login_manager .login_view = 'login'
10873
10974class Usuario (UserMixin ):
110- def __init__ (self , username ):
75+ def __init__ (self , username , role ):
11176 self .id = username
112- self .rol = usuarios [ username ][ 'rol' ]
77+ self .rol = role
11378
11479@login_manager .user_loader
11580def load_user (user_id ):
116- if user_id in usuarios :
117- return Usuario (user_id )
81+ if user_id in users :
82+ return Usuario (user_id , users [ user_id ][ 'role' ] )
11883 return None
11984
120- # --- RUTAS WEB (VISTAS) ---
121- @app .get ("/" )
122- def root ():
123- return {"status" : "ok" }
85+ # --- RUTAS ---
12486@app .route ('/' )
12587def home ():
88+ if current_user .is_authenticated :
89+ return redirect (url_for ('inicio' ))
12690 return redirect (url_for ('login' ))
12791
12892@app .route ('/login' , methods = ['GET' , 'POST' ])
@@ -132,9 +96,13 @@ def login():
13296 if request .method == 'POST' :
13397 user = request .form ['usuario' ]
13498 password = request .form ['clave' ]
135- if user in usuarios and argon2 .check_password_hash (usuarios [user ]['password' ], password ):
136- login_user (Usuario (user ))
137- return redirect (url_for ('inicio' ))
99+ if user in users :
100+ try :
101+ ph .verify (users [user ]['password' ], password )
102+ login_user (Usuario (user , users [user ]['role' ]))
103+ return redirect (url_for ('inicio' ))
104+ except Exception :
105+ pass
138106 return render_template ("login.html" , error = "Credenciales inválidas." )
139107 return render_template ("login.html" )
140108
@@ -149,8 +117,7 @@ def logout():
149117def inicio ():
150118 return render_template ('inicio.html' , current_user = current_user )
151119
152- # --- FUNCIONALIDAD DE ARCHIVOS (MicroNAS) ---
153-
120+ # --- FUNCIONALIDAD DE ARCHIVOS ---
154121@app .route ('/subir' , methods = ['GET' , 'POST' ])
155122@login_required
156123def subir ():
@@ -162,8 +129,6 @@ def subir():
162129 archivo = request .files ['archivo' ]
163130 if archivo .filename == '' :
164131 return 'No se seleccionó ningún archivo' , 400
165- # Audit file -> I only trust UTF-8 (Concepto)
166- # Aquí iría la lógica para validar el tipo de archivo antes de guardarlo.
167132 filename = secure_filename (archivo .filename )
168133 archivo .save (os .path .join (app .config ['UPLOAD_FOLDER' ], filename ))
169134 return redirect (url_for ('listar' ))
@@ -188,112 +153,53 @@ def eliminar(nombre):
188153 try :
189154 os .remove (os .path .join (UPLOAD_FOLDER , secure_filename (nombre )))
190155 except FileNotFoundError :
191- pass # Ignorar si el archivo no existe
156+ pass
192157 return redirect (url_for ('listar' ))
193158
194- # --- RUTA PARA LA INTERFAZ DEL CHAT ---
195159@app .route ('/chat' )
196160@login_required
197161def chat ():
198162 return render_template ('chat.html' , current_user = current_user )
199163
200-
201- # --- LÓGICA DEL SERVIDOR DE CHAT CON SOCKET.IO ---
202-
203- # Almacenamiento en memoria para las salas y contraseñas (MVP)
204- # En un sistema real, esto debería estar en una base de datos.
205- chat_rooms = {}
164+ # --- SOCKET.IO ---
165+ chat_rooms = {}
206166
207167@socketio .on ('join' )
208168def on_join (data ):
209- """
210- Unirse a una sala de chat.
211- """
212169 username = current_user .id
213170 room_code = data ['room' ]
214171 password = data ['password' ]
215172 is_group = data .get ('is_group' , False )
216173
217- # Lógica de "candado" y contraseña
218174 if room_code not in chat_rooms :
219175 chat_rooms [room_code ] = argon2 .generate_password_hash (password )
220- room_owner = True
221176 else :
222- room_owner = False
223177 if not argon2 .check_password_hash (chat_rooms [room_code ], password ):
224- send ({'msg' : 'Contraseña de la sala incorrecta.' , 'type' : 'error' })
178+ send ({'msg' : 'Contraseña incorrecta.' , 'type' : 'error' })
225179 return
226180
227181 join_room (room_code )
228-
229- # Notificar a la sala que un nuevo usuario se ha unido
230- message_data = {
231- 'msg' : f"👋 { username } se ha unido a la sala." ,
232- 'user' : 'Servidor' ,
233- 'is_group' : is_group
234- }
235- send (message_data , to = room_code )
236-
182+ send ({'msg' : f"👋 { username } se ha unido." , 'user' : 'Servidor' , 'is_group' : is_group }, to = room_code )
237183
238184@socketio .on ('leave' )
239185def on_leave (data ):
240- """
241- Salir de una sala de chat.
242- """
243186 username = current_user .id
244187 room_code = data ['room' ]
245- is_group = data .get ('is_group' , False )
246-
247188 leave_room (room_code )
248-
249- message_data = {
250- 'msg' : f"🚪 { username } ha salido de la sala." ,
251- 'user' : 'Servidor' ,
252- 'is_group' : is_group
253- }
254- send (message_data , to = room_code )
255-
189+ send ({'msg' : f"🚪 { username } ha salido." , 'user' : 'Servidor' }, to = room_code )
256190
257191@socketio .on ('message' )
258192def handle_message (data ):
259- """
260- Recibir y reenviar mensajes.
261- """
262193 username = current_user .id
263194 room = data ['room' ]
264- message = data ['msg' ]
195+ msg = data ['msg' ]
265196 is_group = data .get ('is_group' , False )
266-
267- # No guardar mensajes si es un chat grupal
268- if is_group :
269- # El mensaje es efímero, solo se reenvía
270- pass
271- else :
272- # Aquí iría la lógica para guardar el mensaje en una base de datos o archivo
273- # Para el MVP, simplemente lo reenviamos.
274- pass
275-
276- response_data = {
277- 'msg' : message ,
278- 'user' : username ,
279- 'is_group' : is_group
280- }
281-
282- send (response_data , to = room )
283-
197+ send ({'msg' : msg , 'user' : username , 'is_group' : is_group }, to = room )
284198
285- # --- INICIO DE LA APLICACIÓN ---
199+ # --- INICIO ---
286200if __name__ == '__main__' :
287- # Usar host='0.0.0.0' para que sea accesible en la red local.
288- # El puerto 8000 es el principal. Otros servicios podrían correr en otros puertos
289- # usando el concepto de 'administrador_hilos.py' si fuera necesario.
290201 port = int (os .environ .get ("PORT" , 8080 ))
291202 socketio .run (app , host = '0.0.0.0' , port = port )
292203
293- # 👇 agregado: esto es para gunicorn
294- # le damos un alias 'app' que apunta a socketio
295- #app = socketio
296- socketio = SocketIO (app , async_mode = "eventlet" )
297- application = socketio # Gunicorn usará esto
298-
299-
204+ # 👉 Para gunicorn/render
205+ application = app
0 commit comments