88from apscheduler .triggers .cron import CronTrigger
99import logging
1010from backup_service import BackupService
11- from models import db , User , Repository , BackupJob
11+ from models import db , User , Repository , BackupJob , PasswordResetCode
1212import atexit
1313
1414# Configure logging
@@ -82,6 +82,12 @@ def dashboard():
8282
8383@app .route ('/login' , methods = ['GET' , 'POST' ])
8484def login ():
85+ # Auto-create default admin if no users
86+ if User .query .count () == 0 :
87+ admin = User (username = 'admin' , password_hash = generate_password_hash ('changeme' ), is_admin = True )
88+ db .session .add (admin )
89+ db .session .commit ()
90+ logger .warning ('Default admin user created with username=admin password=changeme; please change immediately.' )
8591 if request .method == 'POST' :
8692 username = request .form ['username' ]
8793 password = request .form ['password' ]
@@ -101,37 +107,84 @@ def logout():
101107 logout_user ()
102108 return redirect (url_for ('login' ))
103109
104- @app .route ('/register' , methods = ['GET' , 'POST' ])
105- def register ():
106- # Check if this is the first user (admin)
107- user_count = User .query .count ()
108-
110+ @app .route ('/settings' , methods = ['GET' , 'POST' ])
111+ @login_required
112+ def user_settings ():
109113 if request .method == 'POST' :
110- username = request .form ['username' ]
111- password = request .form ['password' ]
112- confirm_password = request .form ['confirm_password' ]
113-
114- if password != confirm_password :
115- flash ('Passwords do not match' , 'error' )
116- return render_template ('register.html' , first_user = user_count == 0 )
117-
118- if User .query .filter_by (username = username ).first ():
119- flash ('Username already exists' , 'error' )
120- return render_template ('register.html' , first_user = user_count == 0 )
121-
122- user = User (
123- username = username ,
124- password_hash = generate_password_hash (password ),
125- is_admin = (user_count == 0 ) # First user becomes admin
126- )
127- db .session .add (user )
114+ new_username = request .form .get ('username' , '' ).strip ()
115+ current_password = request .form .get ('current_password' , '' )
116+ new_password = request .form .get ('new_password' , '' )
117+ confirm_password = request .form .get ('confirm_password' , '' )
118+
119+ # Change username
120+ if new_username and new_username != current_user .username :
121+ if User .query .filter_by (username = new_username ).first ():
122+ flash ('Username already taken' , 'error' )
123+ return redirect (url_for ('user_settings' ))
124+ current_user .username = new_username
125+ flash ('Username updated' , 'success' )
126+
127+ # Change password
128+ if new_password :
129+ if not check_password_hash (current_user .password_hash , current_password ):
130+ flash ('Current password incorrect' , 'error' )
131+ return redirect (url_for ('user_settings' ))
132+ if new_password != confirm_password :
133+ flash ('New passwords do not match' , 'error' )
134+ return redirect (url_for ('user_settings' ))
135+ current_user .password_hash = generate_password_hash (new_password )
136+ flash ('Password updated' , 'success' )
137+
128138 db .session .commit ()
129-
130- login_user (user )
131- flash ('Registration successful' , 'success' )
132- return redirect (url_for ('dashboard' ))
133-
134- return render_template ('register.html' , first_user = user_count == 0 )
139+ return redirect (url_for ('user_settings' ))
140+
141+ return render_template ('settings.html' )
142+
143+ import secrets
144+
145+ @app .route ('/forgot-password' , methods = ['GET' , 'POST' ])
146+ def forgot_password ():
147+ if request .method == 'POST' :
148+ username = request .form .get ('username' , '' ).strip ()
149+ user = User .query .filter_by (username = username ).first ()
150+ if not user :
151+ flash ('If that user exists, a reset code has been generated (check logs).' , 'info' )
152+ return redirect (url_for ('forgot_password' ))
153+ # Invalidate previous unused codes for this user
154+ PasswordResetCode .query .filter_by (user_id = user .id , used = False ).delete ()
155+ code = secrets .token_hex (4 )
156+ prc = PasswordResetCode (user_id = user .id , code = code )
157+ db .session .add (prc )
158+ db .session .commit ()
159+ logger .warning (f'PASSWORD RESET CODE for user={ user .username } : { code } ' )
160+ flash ('Reset code generated. Check server logs.' , 'info' )
161+ return redirect (url_for ('reset_password' ))
162+ return render_template ('forgot_password.html' )
163+
164+ @app .route ('/reset-password' , methods = ['GET' , 'POST' ])
165+ def reset_password ():
166+ if request .method == 'POST' :
167+ username = request .form .get ('username' , '' ).strip ()
168+ code = request .form .get ('code' , '' ).strip ()
169+ new_password = request .form .get ('new_password' , '' )
170+ confirm_password = request .form .get ('confirm_password' , '' )
171+ user = User .query .filter_by (username = username ).first ()
172+ if not user :
173+ flash ('Invalid code or user' , 'error' )
174+ return redirect (url_for ('reset_password' ))
175+ prc = PasswordResetCode .query .filter_by (user_id = user .id , code = code , used = False ).first ()
176+ if not prc :
177+ flash ('Invalid or already used code' , 'error' )
178+ return redirect (url_for ('reset_password' ))
179+ if new_password != confirm_password or not new_password :
180+ flash ('Passwords do not match or empty' , 'error' )
181+ return redirect (url_for ('reset_password' ))
182+ user .password_hash = generate_password_hash (new_password )
183+ prc .used = True
184+ db .session .commit ()
185+ flash ('Password reset successful. You can now log in.' , 'success' )
186+ return redirect (url_for ('login' ))
187+ return render_template ('reset_password.html' )
135188
136189@app .route ('/repositories' )
137190@login_required
0 commit comments