Skip to content

Commit 1e335e8

Browse files
user login fixes
1 parent 93b7b01 commit 1e335e8

File tree

7 files changed

+230
-107
lines changed

7 files changed

+230
-107
lines changed

app.py

Lines changed: 83 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from apscheduler.triggers.cron import CronTrigger
99
import logging
1010
from backup_service import BackupService
11-
from models import db, User, Repository, BackupJob
11+
from models import db, User, Repository, BackupJob, PasswordResetCode
1212
import atexit
1313

1414
# Configure logging
@@ -82,6 +82,12 @@ def dashboard():
8282

8383
@app.route('/login', methods=['GET', 'POST'])
8484
def 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

models.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,10 @@ class BackupJob(db.Model):
4040
started_at = db.Column(db.DateTime)
4141
completed_at = db.Column(db.DateTime)
4242
created_at = db.Column(db.DateTime, default=datetime.utcnow)
43+
44+
class PasswordResetCode(db.Model):
45+
id = db.Column(db.Integer, primary_key=True)
46+
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
47+
code = db.Column(db.String(32), nullable=False, index=True)
48+
created_at = db.Column(db.DateTime, default=datetime.utcnow)
49+
used = db.Column(db.Boolean, default=False)

templates/base.html

Lines changed: 47 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -7,84 +7,59 @@
77
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
88
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
99
<style>
10-
.sidebar {
11-
min-height: 100vh;
12-
background-color: #f8f9fa;
13-
}
14-
.content {
15-
min-height: 100vh;
16-
}
17-
.navbar-brand {
18-
font-weight: bold;
19-
}
20-
.card {
21-
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
22-
}
23-
.status-badge {
24-
font-size: 0.75rem;
25-
}
10+
:root { --bs-primary: #6f42c1; }
11+
.navbar-brand { font-weight: 600; }
12+
.navbar-purple { background: #6f42c1; }
13+
.navbar-purple .navbar-nav .nav-link, .navbar-purple .navbar-brand, .navbar-purple .navbar-text { color: #fff; }
14+
.navbar-purple .nav-link.active { font-weight: 600; text-decoration: underline; }
15+
.btn-primary { background:#6f42c1; border-color:#6f42c1; }
16+
.btn-primary:hover { background:#5a34a1; border-color:#5a34a1; }
17+
a { color:#6f42c1; }
18+
a:hover { color:#5a34a1; }
19+
.card { box-shadow: 0 2px 4px rgba(0,0,0,0.05); }
20+
body { background:#f5f5fa; }
21+
main { padding: 1rem 1rem 3rem; }
22+
@media (max-width: 576px){ main { padding-top: .75rem; } }
2623
</style>
2724
</head>
2825
<body>
29-
<div class="container-fluid">
30-
<div class="row">
31-
{% if current_user.is_authenticated %}
32-
<!-- Sidebar -->
33-
<div class="col-md-2 sidebar p-3">
34-
<h5 class="text-primary">
35-
<i class="fas fa-cloud-download-alt"></i>
36-
GitHub Backup
37-
</h5>
38-
<hr>
39-
<nav class="nav flex-column">
40-
<a class="nav-link" href="{{ url_for('dashboard') }}">
41-
<i class="fas fa-tachometer-alt"></i> Dashboard
42-
</a>
43-
<a class="nav-link" href="{{ url_for('repositories') }}">
44-
<i class="fas fa-code-branch"></i> Repositories
45-
</a>
46-
<a class="nav-link" href="{{ url_for('backup_jobs') }}">
47-
<i class="fas fa-history"></i> Backup Jobs
48-
</a>
49-
<hr>
50-
<a class="nav-link text-danger" href="{{ url_for('logout') }}">
51-
<i class="fas fa-sign-out-alt"></i> Logout
52-
</a>
53-
</nav>
54-
<div class="mt-auto">
55-
<small class="text-muted">
56-
Logged in as: <strong>{{ current_user.username }}</strong>
57-
</small>
26+
{% if current_user.is_authenticated %}
27+
<nav class="navbar navbar-expand-lg navbar-purple navbar-dark">
28+
<div class="container-fluid">
29+
<a class="navbar-brand" href="{{ url_for('dashboard') }}"><i class="fas fa-cloud-download-alt"></i> GitHub Backup</a>
30+
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#mainNav">
31+
<span class="navbar-toggler-icon"></span>
32+
</button>
33+
<div class="collapse navbar-collapse" id="mainNav">
34+
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
35+
<li class="nav-item"><a class="nav-link {% if request.endpoint=='dashboard' %}active{% endif %}" href="{{ url_for('dashboard') }}"><i class="fas fa-tachometer-alt"></i> Dashboard</a></li>
36+
<li class="nav-item"><a class="nav-link {% if request.endpoint=='repositories' %}active{% endif %}" href="{{ url_for('repositories') }}"><i class="fas fa-code-branch"></i> Repositories</a></li>
37+
<li class="nav-item"><a class="nav-link {% if request.endpoint=='backup_jobs' %}active{% endif %}" href="{{ url_for('backup_jobs') }}"><i class="fas fa-history"></i> Jobs</a></li>
38+
<li class="nav-item"><a class="nav-link {% if request.endpoint=='user_settings' %}active{% endif %}" href="{{ url_for('user_settings') }}"><i class="fas fa-user-cog"></i> Settings</a></li>
39+
<li class="nav-item"><a class="nav-link {% if request.endpoint in ['forgot_password','reset_password'] %}active{% endif %}" href="{{ url_for('forgot_password') }}"><i class="fas fa-key"></i> Forgot Password</a></li>
40+
</ul>
41+
<span class="navbar-text me-3 d-none d-lg-inline">Logged in as <strong>{{ current_user.username }}</strong></span>
42+
<a class="btn btn-outline-light" href="{{ url_for('logout') }}"><i class="fas fa-sign-out-alt"></i> Logout</a>
5843
</div>
5944
</div>
60-
{% endif %}
61-
62-
<!-- Main content -->
63-
<div class="{% if current_user.is_authenticated %}col-md-10{% else %}col-12{% endif %} content p-4">
64-
{% if current_user.is_authenticated %}
65-
<nav class="navbar navbar-expand-lg navbar-light bg-white mb-4">
66-
<div class="container-fluid">
67-
<span class="navbar-brand">{% block page_title %}Dashboard{% endblock %}</span>
68-
</div>
69-
</nav>
45+
</nav>
46+
{% endif %}
47+
48+
<main class="container-fluid">
49+
<!-- Flash messages -->
50+
{% with messages = get_flashed_messages(with_categories=true) %}
51+
{% if messages %}
52+
{% for category, message in messages %}
53+
<div class="alert alert-{{ 'danger' if category == 'error' else 'success' if category == 'success' else 'info' }} alert-dismissible fade show" role="alert">
54+
{{ message }}
55+
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
56+
</div>
57+
{% endfor %}
7058
{% endif %}
71-
72-
<!-- Flash messages -->
73-
{% with messages = get_flashed_messages(with_categories=true) %}
74-
{% if messages %}
75-
{% for category, message in messages %}
76-
<div class="alert alert-{{ 'danger' if category == 'error' else 'success' if category == 'success' else 'info' }} alert-dismissible fade show" role="alert">
77-
{{ message }}
78-
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
79-
</div>
80-
{% endfor %}
81-
{% endif %}
82-
{% endwith %}
83-
84-
{% block content %}{% endblock %}
85-
</div>
86-
</div>
87-
</div>
59+
{% endwith %}
60+
<h1 class="h4 mb-3">{% block page_title %}Dashboard{% endblock %}</h1>
61+
{% block content %}{% endblock %}
62+
</main>
8863

8964
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
9065
{% block scripts %}{% endblock %}

templates/forgot_password.html

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{% extends "base.html" %}
2+
{% block page_title %}Forgot Password{% endblock %}
3+
{% block content %}
4+
<div class="row justify-content-center">
5+
<div class="col-md-5">
6+
<div class="card">
7+
<div class="card-header"><i class="fas fa-key"></i> Generate Reset Code</div>
8+
<div class="card-body">
9+
<form method="POST">
10+
<div class="mb-3">
11+
<label class="form-label">Username</label>
12+
<input type="text" class="form-control" name="username" required>
13+
</div>
14+
<button class="btn btn-primary w-100" type="submit">Generate Code</button>
15+
</form>
16+
<hr>
17+
<a href="{{ url_for('reset_password') }}">Already have a code? Reset password</a>
18+
</div>
19+
</div>
20+
</div>
21+
</div>
22+
{% endblock %}

templates/login.html

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,11 @@ <h3 class="card-title text-center mb-4">
1717
<label for="password" class="form-label">Password</label>
1818
<input type="password" class="form-control" id="password" name="password" required>
1919
</div>
20-
<button type="submit" class="btn btn-primary w-100">Login</button>
20+
<button type="submit" class="btn btn-primary w-100">Login</button>
2121
</form>
22-
<hr>
23-
<div class="text-center">
24-
<a href="{{ url_for('register') }}" class="btn btn-outline-secondary">Create Account</a>
25-
</div>
22+
<div class="mt-3 text-center">
23+
<a href="{{ url_for('forgot_password') }}" class="small">Forgot password?</a>
24+
</div>
2625
</div>
2726
</div>
2827
</div>

templates/reset_password.html

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
{% extends "base.html" %}
2+
{% block page_title %}Reset Password{% endblock %}
3+
{% block content %}
4+
<div class="row justify-content-center">
5+
<div class="col-md-5">
6+
<div class="card">
7+
<div class="card-header"><i class="fas fa-unlock-alt"></i> Reset Password</div>
8+
<div class="card-body">
9+
<form method="POST">
10+
<div class="mb-3">
11+
<label class="form-label">Username</label>
12+
<input type="text" class="form-control" name="username" required>
13+
</div>
14+
<div class="mb-3">
15+
<label class="form-label">Reset Code</label>
16+
<input type="text" class="form-control" name="code" required>
17+
</div>
18+
<div class="mb-3">
19+
<label class="form-label">New Password</label>
20+
<input type="password" class="form-control" name="new_password" required>
21+
</div>
22+
<div class="mb-3">
23+
<label class="form-label">Confirm New Password</label>
24+
<input type="password" class="form-control" name="confirm_password" required>
25+
</div>
26+
<button class="btn btn-primary w-100" type="submit">Reset Password</button>
27+
</form>
28+
<hr>
29+
<a href="{{ url_for('forgot_password') }}">Need a code? Generate one</a>
30+
</div>
31+
</div>
32+
</div>
33+
</div>
34+
{% endblock %}

templates/settings.html

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
{% extends "base.html" %}
2+
{% block page_title %}User Settings{% endblock %}
3+
{% block content %}
4+
<div class="row justify-content-center">
5+
<div class="col-md-6">
6+
<div class="card mb-4">
7+
<div class="card-header"><i class="fas fa-user-cog"></i> Update Account</div>
8+
<div class="card-body">
9+
<form method="POST">
10+
<div class="mb-3">
11+
<label class="form-label">Username</label>
12+
<input type="text" class="form-control" name="username" value="{{ current_user.username }}">
13+
</div>
14+
<hr>
15+
<div class="mb-3">
16+
<label class="form-label">Current Password</label>
17+
<input type="password" class="form-control" name="current_password" placeholder="Enter current password to change it">
18+
</div>
19+
<div class="mb-3">
20+
<label class="form-label">New Password</label>
21+
<input type="password" class="form-control" name="new_password">
22+
</div>
23+
<div class="mb-3">
24+
<label class="form-label">Confirm New Password</label>
25+
<input type="password" class="form-control" name="confirm_password">
26+
</div>
27+
<button class="btn btn-primary w-100" type="submit">Save Changes</button>
28+
</form>
29+
</div>
30+
</div>
31+
</div>
32+
</div>
33+
{% endblock %}

0 commit comments

Comments
 (0)