Require registration by mail link.
This commit is contained in:
185
app.py
185
app.py
@@ -5,9 +5,15 @@ from datetime import datetime, time, timedelta
|
|||||||
import os
|
import os
|
||||||
from sqlalchemy import func
|
from sqlalchemy import func
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
|
from flask_mail import Mail, Message
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
# Load environment variables from .env file
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
# Configure logging
|
# Configure logging
|
||||||
logging.basicConfig(level=logging.DEBUG)
|
logging.basicConfig(level=logging.DEBUG)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///timetrack.db'
|
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///timetrack.db'
|
||||||
@@ -15,6 +21,23 @@ app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
|||||||
app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'dev_key_for_timetrack')
|
app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'dev_key_for_timetrack')
|
||||||
app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=7) # Session lasts for 7 days
|
app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=7) # Session lasts for 7 days
|
||||||
|
|
||||||
|
# Configure Flask-Mail
|
||||||
|
app.config['MAIL_SERVER'] = os.environ.get('MAIL_SERVER', 'smtp.example.com')
|
||||||
|
app.config['MAIL_PORT'] = int(os.environ.get('MAIL_PORT', 587))
|
||||||
|
app.config['MAIL_USE_TLS'] = os.environ.get('MAIL_USE_TLS', 'true').lower() in ['true', 'on', '1']
|
||||||
|
app.config['MAIL_USERNAME'] = os.environ.get('MAIL_USERNAME', 'your-email@example.com')
|
||||||
|
app.config['MAIL_PASSWORD'] = os.environ.get('MAIL_PASSWORD', 'your-password')
|
||||||
|
app.config['MAIL_DEFAULT_SENDER'] = os.environ.get('MAIL_DEFAULT_SENDER', 'TimeTrack <noreply@timetrack.com>')
|
||||||
|
|
||||||
|
# Log mail configuration (without password)
|
||||||
|
logger.info(f"Mail server: {app.config['MAIL_SERVER']}")
|
||||||
|
logger.info(f"Mail port: {app.config['MAIL_PORT']}")
|
||||||
|
logger.info(f"Mail use TLS: {app.config['MAIL_USE_TLS']}")
|
||||||
|
logger.info(f"Mail username: {app.config['MAIL_USERNAME']}")
|
||||||
|
logger.info(f"Mail default sender: {app.config['MAIL_DEFAULT_SENDER']}")
|
||||||
|
|
||||||
|
mail = Mail(app)
|
||||||
|
|
||||||
# Initialize the database with the app
|
# Initialize the database with the app
|
||||||
db.init_app(app)
|
db.init_app(app)
|
||||||
|
|
||||||
@@ -22,8 +45,7 @@ db.init_app(app)
|
|||||||
def login_required(f):
|
def login_required(f):
|
||||||
@wraps(f)
|
@wraps(f)
|
||||||
def decorated_function(*args, **kwargs):
|
def decorated_function(*args, **kwargs):
|
||||||
if 'user_id' not in session:
|
if g.user is None:
|
||||||
flash('Please log in to access this page', 'error')
|
|
||||||
return redirect(url_for('login', next=request.url))
|
return redirect(url_for('login', next=request.url))
|
||||||
return f(*args, **kwargs)
|
return f(*args, **kwargs)
|
||||||
return decorated_function
|
return decorated_function
|
||||||
@@ -32,13 +54,8 @@ def login_required(f):
|
|||||||
def admin_required(f):
|
def admin_required(f):
|
||||||
@wraps(f)
|
@wraps(f)
|
||||||
def decorated_function(*args, **kwargs):
|
def decorated_function(*args, **kwargs):
|
||||||
if 'user_id' not in session:
|
if g.user is None or not g.user.is_admin:
|
||||||
flash('Please log in to access this page', 'error')
|
flash('You need administrator privileges to access this page.', 'error')
|
||||||
return redirect(url_for('login', next=request.url))
|
|
||||||
|
|
||||||
user = User.query.get(session['user_id'])
|
|
||||||
if not user or not user.is_admin:
|
|
||||||
flash('You need administrator privileges to access this page', 'error')
|
|
||||||
return redirect(url_for('home'))
|
return redirect(url_for('home'))
|
||||||
return f(*args, **kwargs)
|
return f(*args, **kwargs)
|
||||||
return decorated_function
|
return decorated_function
|
||||||
@@ -50,29 +67,25 @@ def load_logged_in_user():
|
|||||||
g.user = None
|
g.user = None
|
||||||
else:
|
else:
|
||||||
g.user = User.query.get(user_id)
|
g.user = User.query.get(user_id)
|
||||||
|
if g.user and not g.user.is_verified and request.endpoint not in ['verify_email', 'static', 'logout']:
|
||||||
|
# Allow unverified users to access only verification and static resources
|
||||||
|
if request.endpoint not in ['login', 'register']:
|
||||||
|
flash('Please verify your email address before accessing this page.', 'warning')
|
||||||
|
session.clear()
|
||||||
|
return redirect(url_for('login'))
|
||||||
|
|
||||||
@app.route('/')
|
@app.route('/')
|
||||||
def home():
|
def home():
|
||||||
if g.user:
|
if g.user:
|
||||||
# Get active time entry (if any) for the current user
|
|
||||||
active_entry = TimeEntry.query.filter_by(user_id=g.user.id, departure_time=None).first()
|
|
||||||
|
|
||||||
# Get today's date
|
|
||||||
today = datetime.now().date()
|
today = datetime.now().date()
|
||||||
|
entries = TimeEntry.query.filter(
|
||||||
# Get time entries for today only for the current user
|
|
||||||
today_start = datetime.combine(today, time.min)
|
|
||||||
today_end = datetime.combine(today, time.max)
|
|
||||||
|
|
||||||
today_entries = TimeEntry.query.filter(
|
|
||||||
TimeEntry.user_id == g.user.id,
|
TimeEntry.user_id == g.user.id,
|
||||||
TimeEntry.arrival_time >= today_start,
|
TimeEntry.arrival_time >= datetime.combine(today, time.min),
|
||||||
TimeEntry.arrival_time <= today_end
|
TimeEntry.arrival_time <= datetime.combine(today, time.max)
|
||||||
).order_by(TimeEntry.arrival_time.desc()).all()
|
).order_by(TimeEntry.arrival_time.desc()).all()
|
||||||
|
|
||||||
return render_template('index.html', title='Home', active_entry=active_entry, history=today_entries)
|
return render_template('index.html', title='Home', entries=entries)
|
||||||
else:
|
else:
|
||||||
# Show landing page for non-logged in users
|
|
||||||
return render_template('index.html', title='Home')
|
return render_template('index.html', title='Home')
|
||||||
|
|
||||||
@app.route('/login', methods=['GET', 'POST'])
|
@app.route('/login', methods=['GET', 'POST'])
|
||||||
@@ -80,31 +93,27 @@ def login():
|
|||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
username = request.form.get('username')
|
username = request.form.get('username')
|
||||||
password = request.form.get('password')
|
password = request.form.get('password')
|
||||||
remember = 'remember' in request.form
|
|
||||||
|
|
||||||
user = User.query.filter_by(username=username).first()
|
user = User.query.filter_by(username=username).first()
|
||||||
|
|
||||||
if user and user.check_password(password):
|
if user is None or not user.check_password(password):
|
||||||
session.clear()
|
flash('Invalid username or password', 'error')
|
||||||
session['user_id'] = user.id
|
return redirect(url_for('login'))
|
||||||
if remember:
|
|
||||||
session.permanent = True
|
|
||||||
|
|
||||||
next_page = request.args.get('next')
|
|
||||||
if not next_page or not next_page.startswith('/'):
|
|
||||||
next_page = url_for('home')
|
|
||||||
|
|
||||||
flash(f'Welcome back, {user.username}!', 'success')
|
|
||||||
return redirect(next_page)
|
|
||||||
|
|
||||||
flash('Invalid username or password', 'error')
|
if not user.is_verified:
|
||||||
|
flash('Please verify your email address before logging in. Check your inbox for the verification link.', 'warning')
|
||||||
|
return redirect(url_for('login'))
|
||||||
|
|
||||||
|
session.clear()
|
||||||
|
session['user_id'] = user.id
|
||||||
|
return redirect(url_for('home'))
|
||||||
|
|
||||||
return render_template('login.html', title='Login')
|
return render_template('login.html', title='Login')
|
||||||
|
|
||||||
@app.route('/logout')
|
@app.route('/logout')
|
||||||
def logout():
|
def logout():
|
||||||
session.clear()
|
session.clear()
|
||||||
flash('You have been logged out', 'info')
|
flash('You have been logged out.', 'info')
|
||||||
return redirect(url_for('login'))
|
return redirect(url_for('login'))
|
||||||
|
|
||||||
@app.route('/register', methods=['GET', 'POST'])
|
@app.route('/register', methods=['GET', 'POST'])
|
||||||
@@ -131,19 +140,62 @@ def register():
|
|||||||
error = 'Email already registered'
|
error = 'Email already registered'
|
||||||
|
|
||||||
if error is None:
|
if error is None:
|
||||||
new_user = User(username=username, email=email)
|
try:
|
||||||
new_user.set_password(password)
|
new_user = User(username=username, email=email, is_verified=False)
|
||||||
|
new_user.set_password(password)
|
||||||
db.session.add(new_user)
|
|
||||||
db.session.commit()
|
# Generate verification token
|
||||||
|
token = new_user.generate_verification_token()
|
||||||
flash('Registration successful! You can now log in.', 'success')
|
|
||||||
return redirect(url_for('login'))
|
db.session.add(new_user)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
# Send verification email
|
||||||
|
verification_url = url_for('verify_email', token=token, _external=True)
|
||||||
|
msg = Message('Verify your TimeTrack account', recipients=[email])
|
||||||
|
msg.body = f'''Hello {username},
|
||||||
|
|
||||||
|
Thank you for registering with TimeTrack. To complete your registration, please click on the link below:
|
||||||
|
|
||||||
|
{verification_url}
|
||||||
|
|
||||||
|
This link will expire in 24 hours.
|
||||||
|
|
||||||
|
If you did not register for TimeTrack, please ignore this email.
|
||||||
|
|
||||||
|
Best regards,
|
||||||
|
The TimeTrack Team
|
||||||
|
'''
|
||||||
|
mail.send(msg)
|
||||||
|
logger.info(f"Verification email sent to {email}")
|
||||||
|
|
||||||
|
flash('Registration initiated! Please check your email to verify your account.', 'success')
|
||||||
|
return redirect(url_for('login'))
|
||||||
|
except Exception as e:
|
||||||
|
db.session.rollback()
|
||||||
|
logger.error(f"Error during registration: {str(e)}")
|
||||||
|
error = f"An error occurred during registration: {str(e)}"
|
||||||
|
|
||||||
flash(error, 'error')
|
flash(error, 'error')
|
||||||
|
|
||||||
return render_template('register.html', title='Register')
|
return render_template('register.html', title='Register')
|
||||||
|
|
||||||
|
@app.route('/verify_email/<token>')
|
||||||
|
def verify_email(token):
|
||||||
|
user = User.query.filter_by(verification_token=token).first()
|
||||||
|
|
||||||
|
if not user:
|
||||||
|
flash('Invalid or expired verification link.', 'error')
|
||||||
|
return redirect(url_for('login'))
|
||||||
|
|
||||||
|
if user.verify_token(token):
|
||||||
|
db.session.commit()
|
||||||
|
flash('Email verified successfully! You can now log in.', 'success')
|
||||||
|
else:
|
||||||
|
flash('Invalid or expired verification link.', 'error')
|
||||||
|
|
||||||
|
return redirect(url_for('login'))
|
||||||
|
|
||||||
@app.route('/admin/dashboard')
|
@app.route('/admin/dashboard')
|
||||||
@admin_required
|
@admin_required
|
||||||
def admin_dashboard():
|
def admin_dashboard():
|
||||||
@@ -163,6 +215,7 @@ def create_user():
|
|||||||
email = request.form.get('email')
|
email = request.form.get('email')
|
||||||
password = request.form.get('password')
|
password = request.form.get('password')
|
||||||
is_admin = 'is_admin' in request.form
|
is_admin = 'is_admin' in request.form
|
||||||
|
auto_verify = 'auto_verify' in request.form # New checkbox for auto verification
|
||||||
|
|
||||||
# Validate input
|
# Validate input
|
||||||
error = None
|
error = None
|
||||||
@@ -178,13 +231,34 @@ def create_user():
|
|||||||
error = 'Email already registered'
|
error = 'Email already registered'
|
||||||
|
|
||||||
if error is None:
|
if error is None:
|
||||||
new_user = User(username=username, email=email, is_admin=is_admin)
|
new_user = User(username=username, email=email, is_admin=is_admin, is_verified=auto_verify)
|
||||||
new_user.set_password(password)
|
new_user.set_password(password)
|
||||||
|
|
||||||
|
if not auto_verify:
|
||||||
|
# Generate verification token and send email
|
||||||
|
token = new_user.generate_verification_token()
|
||||||
|
verification_url = url_for('verify_email', token=token, _external=True)
|
||||||
|
msg = Message('Verify your TimeTrack account', recipients=[email])
|
||||||
|
msg.body = f'''Hello {username},
|
||||||
|
|
||||||
|
An administrator has created an account for you on TimeTrack. To activate your account, please click on the link below:
|
||||||
|
|
||||||
|
{verification_url}
|
||||||
|
|
||||||
|
This link will expire in 24 hours.
|
||||||
|
|
||||||
|
Best regards,
|
||||||
|
The TimeTrack Team
|
||||||
|
'''
|
||||||
|
mail.send(msg)
|
||||||
|
|
||||||
db.session.add(new_user)
|
db.session.add(new_user)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
flash(f'User {username} created successfully!', 'success')
|
if auto_verify:
|
||||||
|
flash(f'User {username} created and automatically verified!', 'success')
|
||||||
|
else:
|
||||||
|
flash(f'User {username} created! Verification email sent.', 'success')
|
||||||
return redirect(url_for('admin_users'))
|
return redirect(url_for('admin_users'))
|
||||||
|
|
||||||
flash(error, 'error')
|
flash(error, 'error')
|
||||||
@@ -422,10 +496,15 @@ def create_tables():
|
|||||||
# Check if we need to add new columns
|
# Check if we need to add new columns
|
||||||
from sqlalchemy import inspect
|
from sqlalchemy import inspect
|
||||||
inspector = inspect(db.engine)
|
inspector = inspect(db.engine)
|
||||||
columns = [column['name'] for column in inspector.get_columns('time_entry')]
|
|
||||||
|
# Check if user table exists
|
||||||
if 'is_paused' not in columns or 'pause_start_time' not in columns or 'total_break_duration' not in columns:
|
if 'user' in inspector.get_table_names():
|
||||||
print("WARNING: Database schema is outdated. Please run migrate_db.py to update it.")
|
columns = [column['name'] for column in inspector.get_columns('user')]
|
||||||
|
|
||||||
|
# Check for verification columns
|
||||||
|
if 'is_verified' not in columns or 'verification_token' not in columns or 'token_expiry' not in columns:
|
||||||
|
logger.warning("Database schema is outdated. Please run migrate_db.py to update it.")
|
||||||
|
print("WARNING: Database schema is outdated. Please run migrate_db.py to update it.")
|
||||||
|
|
||||||
@app.route('/api/delete/<int:entry_id>', methods=['DELETE'])
|
@app.route('/api/delete/<int:entry_id>', methods=['DELETE'])
|
||||||
@login_required
|
@login_required
|
||||||
|
|||||||
@@ -82,6 +82,23 @@ def migrate_database():
|
|||||||
print("Adding user_id column to work_config...")
|
print("Adding user_id column to work_config...")
|
||||||
cursor.execute("ALTER TABLE work_config ADD COLUMN user_id INTEGER")
|
cursor.execute("ALTER TABLE work_config ADD COLUMN user_id INTEGER")
|
||||||
|
|
||||||
|
# Check if the user table exists and has the verification columns
|
||||||
|
cursor.execute("PRAGMA table_info(user)")
|
||||||
|
user_columns = [column[1] for column in cursor.fetchall()]
|
||||||
|
|
||||||
|
# Add new columns to user table for email verification
|
||||||
|
if 'is_verified' not in user_columns:
|
||||||
|
print("Adding is_verified column to user table...")
|
||||||
|
cursor.execute("ALTER TABLE user ADD COLUMN is_verified BOOLEAN DEFAULT 0")
|
||||||
|
|
||||||
|
if 'verification_token' not in user_columns:
|
||||||
|
print("Adding verification_token column to user table...")
|
||||||
|
cursor.execute("ALTER TABLE user ADD COLUMN verification_token VARCHAR(100)")
|
||||||
|
|
||||||
|
if 'token_expiry' not in user_columns:
|
||||||
|
print("Adding token_expiry column to user table...")
|
||||||
|
cursor.execute("ALTER TABLE user ADD COLUMN token_expiry TIMESTAMP")
|
||||||
|
|
||||||
# Commit changes and close connection
|
# Commit changes and close connection
|
||||||
conn.commit()
|
conn.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
@@ -97,13 +114,20 @@ def migrate_database():
|
|||||||
admin = User(
|
admin = User(
|
||||||
username='admin',
|
username='admin',
|
||||||
email='admin@timetrack.local',
|
email='admin@timetrack.local',
|
||||||
is_admin=True
|
is_admin=True,
|
||||||
|
is_verified=True # Admin is automatically verified
|
||||||
)
|
)
|
||||||
admin.set_password('admin') # Default password, should be changed
|
admin.set_password('admin') # Default password, should be changed
|
||||||
db.session.add(admin)
|
db.session.add(admin)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
print("Created admin user with username 'admin' and password 'admin'")
|
print("Created admin user with username 'admin' and password 'admin'")
|
||||||
print("Please change the admin password after first login!")
|
print("Please change the admin password after first login!")
|
||||||
|
else:
|
||||||
|
# Make sure existing admin user is verified
|
||||||
|
if not hasattr(admin, 'is_verified') or not admin.is_verified:
|
||||||
|
admin.is_verified = True
|
||||||
|
db.session.commit()
|
||||||
|
print("Marked existing admin user as verified")
|
||||||
|
|
||||||
# Update existing time entries to associate with admin user
|
# Update existing time entries to associate with admin user
|
||||||
orphan_entries = TimeEntry.query.filter_by(user_id=None).all()
|
orphan_entries = TimeEntry.query.filter_by(user_id=None).all()
|
||||||
@@ -115,9 +139,15 @@ def migrate_database():
|
|||||||
for config in orphan_configs:
|
for config in orphan_configs:
|
||||||
config.user_id = admin.id
|
config.user_id = admin.id
|
||||||
|
|
||||||
|
# Mark all existing users as verified for backward compatibility
|
||||||
|
existing_users = User.query.filter_by(is_verified=None).all()
|
||||||
|
for user in existing_users:
|
||||||
|
user.is_verified = True
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
print(f"Associated {len(orphan_entries)} existing time entries with admin user")
|
print(f"Associated {len(orphan_entries)} existing time entries with admin user")
|
||||||
print(f"Associated {len(orphan_configs)} existing work configs with admin user")
|
print(f"Associated {len(orphan_configs)} existing work configs with admin user")
|
||||||
|
print(f"Marked {len(existing_users)} existing users as verified")
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
migrate_database()
|
migrate_database()
|
||||||
|
|||||||
30
models.py
30
models.py
@@ -1,19 +1,26 @@
|
|||||||
from flask_sqlalchemy import SQLAlchemy
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
from werkzeug.security import generate_password_hash, check_password_hash
|
from werkzeug.security import generate_password_hash, check_password_hash
|
||||||
from datetime import datetime
|
from datetime import datetime, timedelta
|
||||||
|
import secrets
|
||||||
|
|
||||||
db = SQLAlchemy()
|
db = SQLAlchemy()
|
||||||
|
|
||||||
class User(db.Model):
|
class User(db.Model):
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
username = db.Column(db.String(64), unique=True, nullable=False)
|
username = db.Column(db.String(80), unique=True, nullable=False)
|
||||||
email = db.Column(db.String(120), unique=True, nullable=False)
|
email = db.Column(db.String(120), unique=True, nullable=False)
|
||||||
password_hash = db.Column(db.String(128))
|
password_hash = db.Column(db.String(128))
|
||||||
is_admin = db.Column(db.Boolean, default=False)
|
is_admin = db.Column(db.Boolean, default=False)
|
||||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
|
|
||||||
# Relationship with TimeEntry
|
# Email verification fields
|
||||||
time_entries = db.relationship('TimeEntry', backref='user', lazy='dynamic')
|
is_verified = db.Column(db.Boolean, default=False)
|
||||||
|
verification_token = db.Column(db.String(100), unique=True, nullable=True)
|
||||||
|
token_expiry = db.Column(db.DateTime, nullable=True)
|
||||||
|
|
||||||
|
# Relationships
|
||||||
|
time_entries = db.relationship('TimeEntry', backref='user', lazy=True)
|
||||||
|
work_config = db.relationship('WorkConfig', backref='user', lazy=True, uselist=False)
|
||||||
|
|
||||||
def set_password(self, password):
|
def set_password(self, password):
|
||||||
self.password_hash = generate_password_hash(password)
|
self.password_hash = generate_password_hash(password)
|
||||||
@@ -21,6 +28,21 @@ class User(db.Model):
|
|||||||
def check_password(self, password):
|
def check_password(self, password):
|
||||||
return check_password_hash(self.password_hash, password)
|
return check_password_hash(self.password_hash, password)
|
||||||
|
|
||||||
|
def generate_verification_token(self):
|
||||||
|
"""Generate a verification token that expires in 24 hours"""
|
||||||
|
self.verification_token = secrets.token_urlsafe(32)
|
||||||
|
self.token_expiry = datetime.utcnow() + timedelta(hours=24)
|
||||||
|
return self.verification_token
|
||||||
|
|
||||||
|
def verify_token(self, token):
|
||||||
|
"""Verify the token and mark user as verified if valid"""
|
||||||
|
if token == self.verification_token and self.token_expiry > datetime.utcnow():
|
||||||
|
self.is_verified = True
|
||||||
|
self.verification_token = None
|
||||||
|
self.token_expiry = None
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f'<User {self.username}>'
|
return f'<User {self.username}>'
|
||||||
|
|
||||||
|
|||||||
@@ -35,6 +35,13 @@
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="checkbox-container">
|
||||||
|
<input type="checkbox" name="auto_verify"> Auto-verify (skip email verification)
|
||||||
|
<span class="checkmark"></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<button type="submit" class="btn btn-success">Create User</button>
|
<button type="submit" class="btn btn-success">Create User</button>
|
||||||
<a href="{{ url_for('admin_users') }}" class="btn btn-secondary">Cancel</a>
|
<a href="{{ url_for('admin_users') }}" class="btn btn-secondary">Cancel</a>
|
||||||
|
|||||||
@@ -13,7 +13,6 @@
|
|||||||
<li><a href="{{ url_for('home') }}">Home</a></li>
|
<li><a href="{{ url_for('home') }}">Home</a></li>
|
||||||
{% if g.user %}
|
{% if g.user %}
|
||||||
<li><a href="{{ url_for('history') }}">History</a></li>
|
<li><a href="{{ url_for('history') }}">History</a></li>
|
||||||
<li><a href="{{ url_for('config') }}">Config</a></li>
|
|
||||||
<li><a href="{{ url_for('about') }}">About</a></li>
|
<li><a href="{{ url_for('about') }}">About</a></li>
|
||||||
|
|
||||||
<!-- Admin dropdown menu moved to the rightmost position -->
|
<!-- Admin dropdown menu moved to the rightmost position -->
|
||||||
@@ -22,10 +21,21 @@
|
|||||||
<a href="#" class="dropdown-toggle">Admin</a>
|
<a href="#" class="dropdown-toggle">Admin</a>
|
||||||
<ul class="dropdown-menu">
|
<ul class="dropdown-menu">
|
||||||
<li><a href="{{ url_for('profile') }}">Profile</a></li>
|
<li><a href="{{ url_for('profile') }}">Profile</a></li>
|
||||||
|
<li><a href="{{ url_for('config') }}">Config</a></li>
|
||||||
<li><a href="{{ url_for('admin_dashboard') }}">Dashboard</a></li>
|
<li><a href="{{ url_for('admin_dashboard') }}">Dashboard</a></li>
|
||||||
<li><a href="{{ url_for('logout') }}">Logout</a></li>
|
<li><a href="{{ url_for('logout') }}">Logout</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
|
{% else %}
|
||||||
|
<!-- User dropdown menu for non-admin users -->
|
||||||
|
<li class="dropdown admin-dropdown">
|
||||||
|
<a href="#" class="dropdown-toggle">{{ g.user.username }}</a>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li><a href="{{ url_for('profile') }}">Profile</a></li>
|
||||||
|
<li><a href="{{ url_for('config') }}">Config</a></li>
|
||||||
|
<li><a href="{{ url_for('logout') }}">Logout</a></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<li><a href="{{ url_for('login') }}">Login</a></li>
|
<li><a href="{{ url_for('login') }}">Login</a></li>
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="email">Email</label>
|
<label for="email">Email</label>
|
||||||
<input type="email" id="email" name="email" class="form-control" required>
|
<input type="email" id="email" name="email" class="form-control" required>
|
||||||
|
<small class="form-text text-muted">A verification link will be sent to this email address.</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
@@ -40,6 +41,10 @@
|
|||||||
<div class="auth-links">
|
<div class="auth-links">
|
||||||
<p>Already have an account? <a href="{{ url_for('login') }}">Login here</a></p>
|
<p>Already have an account? <a href="{{ url_for('login') }}">Login here</a></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="verification-notice">
|
||||||
|
<p>After registration, you'll need to verify your email address before you can log in.</p>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
Reference in New Issue
Block a user