Initial user management.
This commit is contained in:
337
app.py
337
app.py
@@ -1,54 +1,310 @@
|
|||||||
from flask import Flask, render_template, request, redirect, url_for, jsonify, flash
|
from flask import Flask, render_template, request, redirect, url_for, jsonify, flash, session, g
|
||||||
from models import db, TimeEntry, WorkConfig
|
from models import db, TimeEntry, WorkConfig, User
|
||||||
from datetime import datetime, time
|
import logging
|
||||||
|
from datetime import datetime, time, timedelta
|
||||||
import os
|
import os
|
||||||
from sqlalchemy import func
|
from sqlalchemy import func
|
||||||
|
from functools import wraps
|
||||||
|
|
||||||
|
# Configure logging
|
||||||
|
logging.basicConfig(level=logging.DEBUG)
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///timetrack.db'
|
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///timetrack.db'
|
||||||
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
||||||
app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'dev_key_for_timetrack') # Add secret key for flash messages
|
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
|
||||||
|
|
||||||
# Initialize the database with the app
|
# Initialize the database with the app
|
||||||
db.init_app(app)
|
db.init_app(app)
|
||||||
|
|
||||||
|
# Authentication decorator
|
||||||
|
def login_required(f):
|
||||||
|
@wraps(f)
|
||||||
|
def decorated_function(*args, **kwargs):
|
||||||
|
if 'user_id' not in session:
|
||||||
|
flash('Please log in to access this page', 'error')
|
||||||
|
return redirect(url_for('login', next=request.url))
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
return decorated_function
|
||||||
|
|
||||||
|
# Admin-only decorator
|
||||||
|
def admin_required(f):
|
||||||
|
@wraps(f)
|
||||||
|
def decorated_function(*args, **kwargs):
|
||||||
|
if 'user_id' not in session:
|
||||||
|
flash('Please log in 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 f(*args, **kwargs)
|
||||||
|
return decorated_function
|
||||||
|
|
||||||
|
@app.before_request
|
||||||
|
def load_logged_in_user():
|
||||||
|
user_id = session.get('user_id')
|
||||||
|
if user_id is None:
|
||||||
|
g.user = None
|
||||||
|
else:
|
||||||
|
g.user = User.query.get(user_id)
|
||||||
|
|
||||||
@app.route('/')
|
@app.route('/')
|
||||||
def home():
|
def home():
|
||||||
# Get active time entry (if any)
|
if g.user:
|
||||||
active_entry = TimeEntry.query.filter_by(departure_time=None).first()
|
# 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
|
# Get today's date
|
||||||
today = datetime.now().date()
|
today = datetime.now().date()
|
||||||
|
|
||||||
# Get time entries for today only
|
# Get time entries for today only for the current user
|
||||||
today_start = datetime.combine(today, time.min)
|
today_start = datetime.combine(today, time.min)
|
||||||
today_end = datetime.combine(today, time.max)
|
today_end = datetime.combine(today, time.max)
|
||||||
|
|
||||||
today_entries = TimeEntry.query.filter(
|
today_entries = TimeEntry.query.filter(
|
||||||
TimeEntry.arrival_time >= today_start,
|
TimeEntry.user_id == g.user.id,
|
||||||
TimeEntry.arrival_time <= today_end
|
TimeEntry.arrival_time >= today_start,
|
||||||
).order_by(TimeEntry.arrival_time.desc()).all()
|
TimeEntry.arrival_time <= today_end
|
||||||
|
).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', active_entry=active_entry, history=today_entries)
|
||||||
|
else:
|
||||||
|
# Show landing page for non-logged in users
|
||||||
|
return render_template('index.html', title='Home')
|
||||||
|
|
||||||
|
@app.route('/login', methods=['GET', 'POST'])
|
||||||
|
def login():
|
||||||
|
if request.method == 'POST':
|
||||||
|
username = request.form.get('username')
|
||||||
|
password = request.form.get('password')
|
||||||
|
remember = 'remember' in request.form
|
||||||
|
|
||||||
|
user = User.query.filter_by(username=username).first()
|
||||||
|
|
||||||
|
if user and user.check_password(password):
|
||||||
|
session.clear()
|
||||||
|
session['user_id'] = user.id
|
||||||
|
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')
|
||||||
|
|
||||||
|
return render_template('login.html', title='Login')
|
||||||
|
|
||||||
|
@app.route('/logout')
|
||||||
|
def logout():
|
||||||
|
session.clear()
|
||||||
|
flash('You have been logged out', 'info')
|
||||||
|
return redirect(url_for('login'))
|
||||||
|
|
||||||
|
@app.route('/register', methods=['GET', 'POST'])
|
||||||
|
def register():
|
||||||
|
if request.method == 'POST':
|
||||||
|
username = request.form.get('username')
|
||||||
|
email = request.form.get('email')
|
||||||
|
password = request.form.get('password')
|
||||||
|
confirm_password = request.form.get('confirm_password')
|
||||||
|
|
||||||
|
# Validate input
|
||||||
|
error = None
|
||||||
|
if not username:
|
||||||
|
error = 'Username is required'
|
||||||
|
elif not email:
|
||||||
|
error = 'Email is required'
|
||||||
|
elif not password:
|
||||||
|
error = 'Password is required'
|
||||||
|
elif password != confirm_password:
|
||||||
|
error = 'Passwords do not match'
|
||||||
|
elif User.query.filter_by(username=username).first():
|
||||||
|
error = 'Username already exists'
|
||||||
|
elif User.query.filter_by(email=email).first():
|
||||||
|
error = 'Email already registered'
|
||||||
|
|
||||||
|
if error is None:
|
||||||
|
new_user = User(username=username, email=email)
|
||||||
|
new_user.set_password(password)
|
||||||
|
|
||||||
|
db.session.add(new_user)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
flash('Registration successful! You can now log in.', 'success')
|
||||||
|
return redirect(url_for('login'))
|
||||||
|
|
||||||
|
flash(error, 'error')
|
||||||
|
|
||||||
|
return render_template('register.html', title='Register')
|
||||||
|
|
||||||
|
@app.route('/admin/users')
|
||||||
|
@admin_required
|
||||||
|
def admin_users():
|
||||||
|
users = User.query.all()
|
||||||
|
return render_template('admin_users.html', title='User Management', users=users)
|
||||||
|
|
||||||
|
@app.route('/admin/users/create', methods=['GET', 'POST'])
|
||||||
|
@admin_required
|
||||||
|
def create_user():
|
||||||
|
if request.method == 'POST':
|
||||||
|
username = request.form.get('username')
|
||||||
|
email = request.form.get('email')
|
||||||
|
password = request.form.get('password')
|
||||||
|
is_admin = 'is_admin' in request.form
|
||||||
|
|
||||||
|
# Validate input
|
||||||
|
error = None
|
||||||
|
if not username:
|
||||||
|
error = 'Username is required'
|
||||||
|
elif not email:
|
||||||
|
error = 'Email is required'
|
||||||
|
elif not password:
|
||||||
|
error = 'Password is required'
|
||||||
|
elif User.query.filter_by(username=username).first():
|
||||||
|
error = 'Username already exists'
|
||||||
|
elif User.query.filter_by(email=email).first():
|
||||||
|
error = 'Email already registered'
|
||||||
|
|
||||||
|
if error is None:
|
||||||
|
new_user = User(username=username, email=email, is_admin=is_admin)
|
||||||
|
new_user.set_password(password)
|
||||||
|
|
||||||
|
db.session.add(new_user)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
flash(f'User {username} created successfully!', 'success')
|
||||||
|
return redirect(url_for('admin_users'))
|
||||||
|
|
||||||
|
flash(error, 'error')
|
||||||
|
|
||||||
|
return render_template('create_user.html', title='Create User')
|
||||||
|
|
||||||
|
@app.route('/admin/users/edit/<int:user_id>', methods=['GET', 'POST'])
|
||||||
|
@admin_required
|
||||||
|
def edit_user(user_id):
|
||||||
|
user = User.query.get_or_404(user_id)
|
||||||
|
|
||||||
|
if request.method == 'POST':
|
||||||
|
username = request.form.get('username')
|
||||||
|
email = request.form.get('email')
|
||||||
|
password = request.form.get('password')
|
||||||
|
is_admin = 'is_admin' in request.form
|
||||||
|
|
||||||
|
# Validate input
|
||||||
|
error = None
|
||||||
|
if not username:
|
||||||
|
error = 'Username is required'
|
||||||
|
elif not email:
|
||||||
|
error = 'Email is required'
|
||||||
|
elif username != user.username and User.query.filter_by(username=username).first():
|
||||||
|
error = 'Username already exists'
|
||||||
|
elif email != user.email and User.query.filter_by(email=email).first():
|
||||||
|
error = 'Email already registered'
|
||||||
|
|
||||||
|
if error is None:
|
||||||
|
user.username = username
|
||||||
|
user.email = email
|
||||||
|
user.is_admin = is_admin
|
||||||
|
|
||||||
|
if password:
|
||||||
|
user.set_password(password)
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
flash(f'User {username} updated successfully!', 'success')
|
||||||
|
return redirect(url_for('admin_users'))
|
||||||
|
|
||||||
|
flash(error, 'error')
|
||||||
|
|
||||||
|
return render_template('edit_user.html', title='Edit User', user=user)
|
||||||
|
|
||||||
|
@app.route('/admin/users/delete/<int:user_id>', methods=['POST'])
|
||||||
|
@admin_required
|
||||||
|
def delete_user(user_id):
|
||||||
|
user = User.query.get_or_404(user_id)
|
||||||
|
|
||||||
|
# Prevent deleting yourself
|
||||||
|
if user.id == session.get('user_id'):
|
||||||
|
flash('You cannot delete your own account', 'error')
|
||||||
|
return redirect(url_for('admin_users'))
|
||||||
|
|
||||||
|
username = user.username
|
||||||
|
db.session.delete(user)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
flash(f'User {username} deleted successfully', 'success')
|
||||||
|
return redirect(url_for('admin_users'))
|
||||||
|
|
||||||
|
@app.route('/profile', methods=['GET', 'POST'])
|
||||||
|
@login_required
|
||||||
|
def profile():
|
||||||
|
user = User.query.get(session['user_id'])
|
||||||
|
|
||||||
|
if request.method == 'POST':
|
||||||
|
email = request.form.get('email')
|
||||||
|
current_password = request.form.get('current_password')
|
||||||
|
new_password = request.form.get('new_password')
|
||||||
|
confirm_password = request.form.get('confirm_password')
|
||||||
|
|
||||||
|
# Validate input
|
||||||
|
error = None
|
||||||
|
if not email:
|
||||||
|
error = 'Email is required'
|
||||||
|
elif email != user.email and User.query.filter_by(email=email).first():
|
||||||
|
error = 'Email already registered'
|
||||||
|
|
||||||
|
# Password change validation
|
||||||
|
if new_password:
|
||||||
|
if not current_password:
|
||||||
|
error = 'Current password is required to set a new password'
|
||||||
|
elif not user.check_password(current_password):
|
||||||
|
error = 'Current password is incorrect'
|
||||||
|
elif new_password != confirm_password:
|
||||||
|
error = 'New passwords do not match'
|
||||||
|
|
||||||
|
if error is None:
|
||||||
|
user.email = email
|
||||||
|
|
||||||
|
if new_password:
|
||||||
|
user.set_password(new_password)
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
flash('Profile updated successfully!', 'success')
|
||||||
|
return redirect(url_for('profile'))
|
||||||
|
|
||||||
|
flash(error, 'error')
|
||||||
|
|
||||||
|
return render_template('profile.html', title='My Profile', user=user)
|
||||||
|
|
||||||
@app.route('/about')
|
@app.route('/about')
|
||||||
|
@login_required
|
||||||
def about():
|
def about():
|
||||||
return render_template('about.html', title='About')
|
return render_template('about.html', title='About')
|
||||||
|
|
||||||
@app.route('/contact', methods=['GET', 'POST'])
|
@app.route('/contact', methods=['GET', 'POST'])
|
||||||
|
@login_required
|
||||||
def contact():
|
def contact():
|
||||||
# redacted
|
# redacted
|
||||||
return render_template('contact.html', title='Contact')
|
return render_template('contact.html', title='Contact')
|
||||||
|
|
||||||
# We can keep this route as a redirect to home for backward compatibility
|
# We can keep this route as a redirect to home for backward compatibility
|
||||||
@app.route('/timetrack')
|
@app.route('/timetrack')
|
||||||
|
@login_required
|
||||||
def timetrack():
|
def timetrack():
|
||||||
return redirect(url_for('home'))
|
return redirect(url_for('home'))
|
||||||
|
|
||||||
@app.route('/api/arrive', methods=['POST'])
|
@app.route('/api/arrive', methods=['POST'])
|
||||||
|
@login_required
|
||||||
def arrive():
|
def arrive():
|
||||||
# Create a new time entry with arrival time
|
# Create a new time entry with arrival time for the current user
|
||||||
new_entry = TimeEntry(arrival_time=datetime.now())
|
new_entry = TimeEntry(user_id=session['user_id'], arrival_time=datetime.now())
|
||||||
db.session.add(new_entry)
|
db.session.add(new_entry)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
@@ -58,9 +314,10 @@ def arrive():
|
|||||||
})
|
})
|
||||||
|
|
||||||
@app.route('/api/leave/<int:entry_id>', methods=['POST'])
|
@app.route('/api/leave/<int:entry_id>', methods=['POST'])
|
||||||
|
@login_required
|
||||||
def leave(entry_id):
|
def leave(entry_id):
|
||||||
# Find the time entry
|
# Find the time entry for the current user
|
||||||
entry = TimeEntry.query.get_or_404(entry_id)
|
entry = TimeEntry.query.filter_by(id=entry_id, user_id=session['user_id']).first_or_404()
|
||||||
|
|
||||||
# Set the departure time
|
# Set the departure time
|
||||||
departure_time = datetime.now()
|
departure_time = datetime.now()
|
||||||
@@ -93,9 +350,10 @@ def leave(entry_id):
|
|||||||
|
|
||||||
# Add this new route to handle pausing/resuming
|
# Add this new route to handle pausing/resuming
|
||||||
@app.route('/api/toggle-pause/<int:entry_id>', methods=['POST'])
|
@app.route('/api/toggle-pause/<int:entry_id>', methods=['POST'])
|
||||||
|
@login_required
|
||||||
def toggle_pause(entry_id):
|
def toggle_pause(entry_id):
|
||||||
# Find the time entry
|
# Find the time entry for the current user
|
||||||
entry = TimeEntry.query.get_or_404(entry_id)
|
entry = TimeEntry.query.filter_by(id=entry_id, user_id=session['user_id']).first_or_404()
|
||||||
|
|
||||||
now = datetime.now()
|
now = datetime.now()
|
||||||
|
|
||||||
@@ -124,6 +382,7 @@ def toggle_pause(entry_id):
|
|||||||
})
|
})
|
||||||
|
|
||||||
@app.route('/config', methods=['GET', 'POST'])
|
@app.route('/config', methods=['GET', 'POST'])
|
||||||
|
@login_required
|
||||||
def config():
|
def config():
|
||||||
# Get current configuration or create default if none exists
|
# Get current configuration or create default if none exists
|
||||||
config = WorkConfig.query.order_by(WorkConfig.id.desc()).first()
|
config = WorkConfig.query.order_by(WorkConfig.id.desc()).first()
|
||||||
@@ -164,15 +423,17 @@ def create_tables():
|
|||||||
print("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
|
||||||
def delete_entry(entry_id):
|
def delete_entry(entry_id):
|
||||||
entry = TimeEntry.query.get_or_404(entry_id)
|
entry = TimeEntry.query.filter_by(id=entry_id, user_id=session['user_id']).first_or_404()
|
||||||
db.session.delete(entry)
|
db.session.delete(entry)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
return jsonify({'success': True, 'message': 'Entry deleted successfully'})
|
return jsonify({'success': True, 'message': 'Entry deleted successfully'})
|
||||||
|
|
||||||
@app.route('/api/update/<int:entry_id>', methods=['PUT'])
|
@app.route('/api/update/<int:entry_id>', methods=['PUT'])
|
||||||
|
@login_required
|
||||||
def update_entry(entry_id):
|
def update_entry(entry_id):
|
||||||
entry = TimeEntry.query.get_or_404(entry_id)
|
entry = TimeEntry.query.filter_by(id=entry_id, user_id=session['user_id']).first_or_404()
|
||||||
data = request.json
|
data = request.json
|
||||||
|
|
||||||
if 'arrival_time' in data:
|
if 'arrival_time' in data:
|
||||||
@@ -210,9 +471,10 @@ def update_entry(entry_id):
|
|||||||
})
|
})
|
||||||
|
|
||||||
@app.route('/history')
|
@app.route('/history')
|
||||||
|
@login_required
|
||||||
def history():
|
def history():
|
||||||
# Get all time entries, ordered by most recent first
|
# Get all time entries for the current user, ordered by most recent first
|
||||||
all_entries = TimeEntry.query.order_by(TimeEntry.arrival_time.desc()).all()
|
all_entries = TimeEntry.query.filter_by(user_id=session['user_id']).order_by(TimeEntry.arrival_time.desc()).all()
|
||||||
|
|
||||||
return render_template('history.html', title='Time Entry History', entries=all_entries)
|
return render_template('history.html', title='Time Entry History', entries=all_entries)
|
||||||
|
|
||||||
@@ -263,12 +525,13 @@ def calculate_work_duration(arrival_time, departure_time, total_break_duration):
|
|||||||
return work_duration, effective_break_duration
|
return work_duration, effective_break_duration
|
||||||
|
|
||||||
@app.route('/api/resume/<int:entry_id>', methods=['POST'])
|
@app.route('/api/resume/<int:entry_id>', methods=['POST'])
|
||||||
|
@login_required
|
||||||
def resume_entry(entry_id):
|
def resume_entry(entry_id):
|
||||||
# Find the entry to resume
|
# Find the entry to resume for the current user
|
||||||
entry_to_resume = TimeEntry.query.get_or_404(entry_id)
|
entry_to_resume = TimeEntry.query.filter_by(id=entry_id, user_id=session['user_id']).first_or_404()
|
||||||
|
|
||||||
# Check if there's already an active entry
|
# Check if there's already an active entry
|
||||||
active_entry = TimeEntry.query.filter_by(departure_time=None).first()
|
active_entry = TimeEntry.query.filter_by(user_id=session['user_id'], departure_time=None).first()
|
||||||
if active_entry:
|
if active_entry:
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'success': False,
|
'success': False,
|
||||||
@@ -292,5 +555,21 @@ def resume_entry(entry_id):
|
|||||||
'total_break_duration': entry_to_resume.total_break_duration
|
'total_break_duration': entry_to_resume.total_break_duration
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@app.errorhandler(404)
|
||||||
|
def page_not_found(e):
|
||||||
|
return render_template('404.html'), 404
|
||||||
|
|
||||||
|
@app.errorhandler(500)
|
||||||
|
def internal_server_error(e):
|
||||||
|
return render_template('500.html'), 500
|
||||||
|
|
||||||
|
@app.route('/test')
|
||||||
|
def test():
|
||||||
|
return "App is working!"
|
||||||
|
|
||||||
|
@app.context_processor
|
||||||
|
def inject_current_year():
|
||||||
|
return {'current_year': datetime.now().year}
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
app.run(debug=True)
|
app.run(debug=True)
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
from app import app, db
|
from app import app, db
|
||||||
import sqlite3
|
import sqlite3
|
||||||
import os
|
import os
|
||||||
|
from models import User, TimeEntry, WorkConfig
|
||||||
|
from werkzeug.security import generate_password_hash
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
def migrate_database():
|
def migrate_database():
|
||||||
db_path = 'timetrack.db'
|
db_path = 'timetrack.db'
|
||||||
@@ -35,6 +38,11 @@ def migrate_database():
|
|||||||
print("Adding total_break_duration column to time_entry...")
|
print("Adding total_break_duration column to time_entry...")
|
||||||
cursor.execute("ALTER TABLE time_entry ADD COLUMN total_break_duration INTEGER DEFAULT 0")
|
cursor.execute("ALTER TABLE time_entry ADD COLUMN total_break_duration INTEGER DEFAULT 0")
|
||||||
|
|
||||||
|
# Add user_id column if it doesn't exist
|
||||||
|
if 'user_id' not in time_entry_columns:
|
||||||
|
print("Adding user_id column to time_entry...")
|
||||||
|
cursor.execute("ALTER TABLE time_entry ADD COLUMN user_id INTEGER")
|
||||||
|
|
||||||
# Check if the work_config table exists
|
# Check if the work_config table exists
|
||||||
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='work_config'")
|
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='work_config'")
|
||||||
if not cursor.fetchone():
|
if not cursor.fetchone():
|
||||||
@@ -46,7 +54,8 @@ def migrate_database():
|
|||||||
mandatory_break_minutes INTEGER DEFAULT 30,
|
mandatory_break_minutes INTEGER DEFAULT 30,
|
||||||
break_threshold_hours FLOAT DEFAULT 6.0,
|
break_threshold_hours FLOAT DEFAULT 6.0,
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
user_id INTEGER
|
||||||
)
|
)
|
||||||
""")
|
""")
|
||||||
# Insert default config
|
# Insert default config
|
||||||
@@ -68,11 +77,48 @@ def migrate_database():
|
|||||||
print("Adding additional_break_threshold_hours column to work_config...")
|
print("Adding additional_break_threshold_hours column to work_config...")
|
||||||
cursor.execute("ALTER TABLE work_config ADD COLUMN additional_break_threshold_hours FLOAT DEFAULT 9.0")
|
cursor.execute("ALTER TABLE work_config ADD COLUMN additional_break_threshold_hours FLOAT DEFAULT 9.0")
|
||||||
|
|
||||||
|
# Add user_id column to work_config if it doesn't exist
|
||||||
|
if 'user_id' not in work_config_columns:
|
||||||
|
print("Adding user_id column to work_config...")
|
||||||
|
cursor.execute("ALTER TABLE work_config ADD COLUMN user_id INTEGER")
|
||||||
|
|
||||||
# Commit changes and close connection
|
# Commit changes and close connection
|
||||||
conn.commit()
|
conn.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
print("Database migration completed successfully!")
|
with app.app_context():
|
||||||
|
# Create tables if they don't exist
|
||||||
|
db.create_all()
|
||||||
|
|
||||||
|
# Check if admin user exists
|
||||||
|
admin = User.query.filter_by(username='admin').first()
|
||||||
|
if not admin:
|
||||||
|
# Create admin user
|
||||||
|
admin = User(
|
||||||
|
username='admin',
|
||||||
|
email='admin@timetrack.local',
|
||||||
|
is_admin=True
|
||||||
|
)
|
||||||
|
admin.set_password('admin') # Default password, should be changed
|
||||||
|
db.session.add(admin)
|
||||||
|
db.session.commit()
|
||||||
|
print("Created admin user with username 'admin' and password 'admin'")
|
||||||
|
print("Please change the admin password after first login!")
|
||||||
|
|
||||||
|
# Update existing time entries to associate with admin user
|
||||||
|
orphan_entries = TimeEntry.query.filter_by(user_id=None).all()
|
||||||
|
for entry in orphan_entries:
|
||||||
|
entry.user_id = admin.id
|
||||||
|
|
||||||
|
# Update existing work configs to associate with admin user
|
||||||
|
orphan_configs = WorkConfig.query.filter_by(user_id=None).all()
|
||||||
|
for config in orphan_configs:
|
||||||
|
config.user_id = admin.id
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
print(f"Associated {len(orphan_entries)} existing time entries with admin user")
|
||||||
|
print(f"Associated {len(orphan_configs)} existing work configs with admin user")
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
migrate_database()
|
migrate_database()
|
||||||
|
print("Database migration completed")
|
||||||
23
models.py
23
models.py
@@ -1,8 +1,29 @@
|
|||||||
from flask_sqlalchemy import SQLAlchemy
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
|
from werkzeug.security import generate_password_hash, check_password_hash
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
db = SQLAlchemy()
|
db = SQLAlchemy()
|
||||||
|
|
||||||
|
class User(db.Model):
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
username = db.Column(db.String(64), unique=True, nullable=False)
|
||||||
|
email = db.Column(db.String(120), unique=True, nullable=False)
|
||||||
|
password_hash = db.Column(db.String(128))
|
||||||
|
is_admin = db.Column(db.Boolean, default=False)
|
||||||
|
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
|
|
||||||
|
# Relationship with TimeEntry
|
||||||
|
time_entries = db.relationship('TimeEntry', backref='user', lazy='dynamic')
|
||||||
|
|
||||||
|
def set_password(self, password):
|
||||||
|
self.password_hash = generate_password_hash(password)
|
||||||
|
|
||||||
|
def check_password(self, password):
|
||||||
|
return check_password_hash(self.password_hash, password)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f'<User {self.username}>'
|
||||||
|
|
||||||
class TimeEntry(db.Model):
|
class TimeEntry(db.Model):
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
arrival_time = db.Column(db.DateTime, nullable=False)
|
arrival_time = db.Column(db.DateTime, nullable=False)
|
||||||
@@ -11,6 +32,7 @@ class TimeEntry(db.Model):
|
|||||||
is_paused = db.Column(db.Boolean, default=False)
|
is_paused = db.Column(db.Boolean, default=False)
|
||||||
pause_start_time = db.Column(db.DateTime, nullable=True)
|
pause_start_time = db.Column(db.DateTime, nullable=True)
|
||||||
total_break_duration = db.Column(db.Integer, default=0) # Total break duration in seconds
|
total_break_duration = db.Column(db.Integer, default=0) # Total break duration in seconds
|
||||||
|
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=True)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f'<TimeEntry {self.id}: {self.arrival_time} - {self.departure_time}>'
|
return f'<TimeEntry {self.id}: {self.arrival_time} - {self.departure_time}>'
|
||||||
@@ -24,6 +46,7 @@ class WorkConfig(db.Model):
|
|||||||
additional_break_threshold_hours = db.Column(db.Float, default=9.0) # Work hours that trigger additional break
|
additional_break_threshold_hours = db.Column(db.Float, default=9.0) # Work hours that trigger additional break
|
||||||
created_at = db.Column(db.DateTime, default=datetime.now)
|
created_at = db.Column(db.DateTime, default=datetime.now)
|
||||||
updated_at = db.Column(db.DateTime, default=datetime.now, onupdate=datetime.now)
|
updated_at = db.Column(db.DateTime, default=datetime.now, onupdate=datetime.now)
|
||||||
|
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=True)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f'<WorkConfig {self.id}: {self.work_hours_per_day}h/day, {self.mandatory_break_minutes}min break>'
|
return f'<WorkConfig {self.id}: {self.work_hours_per_day}h/day, {self.mandatory_break_minutes}min break>'
|
||||||
@@ -132,6 +132,37 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add dropdown menu functionality
|
||||||
|
const dropdownToggles = document.querySelectorAll('.dropdown-toggle');
|
||||||
|
|
||||||
|
dropdownToggles.forEach(toggle => {
|
||||||
|
toggle.addEventListener('click', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const parent = this.parentElement;
|
||||||
|
const menu = parent.querySelector('.dropdown-menu');
|
||||||
|
|
||||||
|
// Close all other open dropdowns
|
||||||
|
document.querySelectorAll('.dropdown-menu').forEach(item => {
|
||||||
|
if (item !== menu && item.classList.contains('show')) {
|
||||||
|
item.classList.remove('show');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Toggle current dropdown
|
||||||
|
menu.classList.toggle('show');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Close dropdown when clicking outside
|
||||||
|
document.addEventListener('click', function(e) {
|
||||||
|
if (!e.target.matches('.dropdown-toggle')) {
|
||||||
|
const dropdowns = document.querySelectorAll('.dropdown-menu.show');
|
||||||
|
dropdowns.forEach(dropdown => {
|
||||||
|
dropdown.classList.remove('show');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add event listener for resume work buttons
|
// Add event listener for resume work buttons
|
||||||
|
|||||||
9
templates/404.html
Normal file
9
templates/404.html
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{% extends "layout.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="error-container">
|
||||||
|
<h1>404 - Page Not Found</h1>
|
||||||
|
<p>The page you are looking for does not exist.</p>
|
||||||
|
<a href="{{ url_for('home') }}" class="btn">Return to Home</a>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
0
templates/500.html
Normal file
0
templates/500.html
Normal file
88
templates/admin_users.html
Normal file
88
templates/admin_users.html
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
{% extends "layout.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="admin-container">
|
||||||
|
<h1>User Management</h1>
|
||||||
|
|
||||||
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||||
|
{% if messages %}
|
||||||
|
{% for category, message in messages %}
|
||||||
|
<div class="alert alert-{{ category }}">{{ message }}</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
<div class="admin-actions">
|
||||||
|
<a href="{{ url_for('create_user') }}" class="btn btn-success">Create New User</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="user-list">
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Username</th>
|
||||||
|
<th>Email</th>
|
||||||
|
<th>Role</th>
|
||||||
|
<th>Created</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for user in users %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ user.username }}</td>
|
||||||
|
<td>{{ user.email }}</td>
|
||||||
|
<td>{% if user.is_admin %}Admin{% else %}User{% endif %}</td>
|
||||||
|
<td>{{ user.created_at.strftime('%Y-%m-%d') }}</td>
|
||||||
|
<td>
|
||||||
|
<a href="{{ url_for('edit_user', user_id=user.id) }}" class="btn btn-sm btn-primary">Edit</a>
|
||||||
|
{% if user.id != g.user.id %}
|
||||||
|
<button class="btn btn-sm btn-danger" onclick="confirmDelete({{ user.id }}, '{{ user.username }}')">Delete</button>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Delete Confirmation Modal -->
|
||||||
|
<div id="delete-modal" class="modal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<span class="close">×</span>
|
||||||
|
<h2>Confirm Deletion</h2>
|
||||||
|
<p>Are you sure you want to delete user <span id="delete-username"></span>?</p>
|
||||||
|
<p>This action cannot be undone.</p>
|
||||||
|
<form id="delete-form" method="POST">
|
||||||
|
<button type="button" id="cancel-delete" class="btn btn-secondary">Cancel</button>
|
||||||
|
<button type="submit" class="btn btn-danger">Delete</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function confirmDelete(userId, username) {
|
||||||
|
document.getElementById('delete-username').textContent = username;
|
||||||
|
document.getElementById('delete-form').action = "{{ url_for('delete_user', user_id=0) }}".replace('0', userId);
|
||||||
|
document.getElementById('delete-modal').style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close modal when clicking the X
|
||||||
|
document.querySelector('.close').addEventListener('click', function() {
|
||||||
|
document.getElementById('delete-modal').style.display = 'none';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Close modal when clicking Cancel
|
||||||
|
document.getElementById('cancel-delete').addEventListener('click', function() {
|
||||||
|
document.getElementById('delete-modal').style.display = 'none';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Close modal when clicking outside
|
||||||
|
window.addEventListener('click', function(event) {
|
||||||
|
if (event.target == document.getElementById('delete-modal')) {
|
||||||
|
document.getElementById('delete-modal').style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
44
templates/create_user.html
Normal file
44
templates/create_user.html
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
{% extends "layout.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="admin-container">
|
||||||
|
<h1>Create New User</h1>
|
||||||
|
|
||||||
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||||
|
{% if messages %}
|
||||||
|
{% for category, message in messages %}
|
||||||
|
<div class="alert alert-{{ category }}">{{ message }}</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
<form method="POST" action="{{ url_for('create_user') }}" class="user-form">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="username">Username</label>
|
||||||
|
<input type="text" id="username" name="username" class="form-control" required autofocus>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="email">Email</label>
|
||||||
|
<input type="email" id="email" name="email" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="password">Password</label>
|
||||||
|
<input type="password" id="password" name="password" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="checkbox-container">
|
||||||
|
<input type="checkbox" name="is_admin"> Administrator privileges
|
||||||
|
<span class="checkmark"></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<button type="submit" class="btn btn-success">Create User</button>
|
||||||
|
<a href="{{ url_for('admin_users') }}" class="btn btn-secondary">Cancel</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
44
templates/edit_user.html
Normal file
44
templates/edit_user.html
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
{% extends "layout.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="admin-container">
|
||||||
|
<h1>Edit User: {{ user.username }}</h1>
|
||||||
|
|
||||||
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||||
|
{% if messages %}
|
||||||
|
{% for category, message in messages %}
|
||||||
|
<div class="alert alert-{{ category }}">{{ message }}</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
<form method="POST" action="{{ url_for('edit_user', user_id=user.id) }}" class="user-form">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="username">Username</label>
|
||||||
|
<input type="text" id="username" name="username" class="form-control" value="{{ user.username }}" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="email">Email</label>
|
||||||
|
<input type="email" id="email" name="email" class="form-control" value="{{ user.email }}" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="password">New Password (leave blank to keep current)</label>
|
||||||
|
<input type="password" id="password" name="password" class="form-control">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="checkbox-container">
|
||||||
|
<input type="checkbox" name="is_admin" {% if user.is_admin %}checked{% endif %}> Administrator privileges
|
||||||
|
<span class="checkmark"></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<button type="submit" class="btn btn-primary">Update User</button>
|
||||||
|
<a href="{{ url_for('admin_users') }}" class="btn btn-secondary">Cancel</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>TimeTrack - {{ title }}</title>
|
<title>{{ title }} - TimeTrack</title>
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -11,19 +11,48 @@
|
|||||||
<nav>
|
<nav>
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="{{ url_for('home') }}">Home</a></li>
|
<li><a href="{{ url_for('home') }}">Home</a></li>
|
||||||
<li><a href="{{ url_for('history') }}">Complete History</a></li>
|
{% if g.user %}
|
||||||
<li><a href="{{ url_for('config') }}" {% if title == 'Configuration' %}class="active"{% endif %}>Configuration</a></li>
|
<li><a href="{{ url_for('history') }}">History</a></li>
|
||||||
<li><a href="{{ url_for('about') }}">About</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('contact') }}">Contact</a></li>
|
||||||
|
|
||||||
|
<!-- Add Admin dropdown menu here -->
|
||||||
|
{% if g.user.is_admin %}
|
||||||
|
<li class="dropdown">
|
||||||
|
<a href="#" class="dropdown-toggle">Admin</a>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li><a href="{{ url_for('admin_users') }}">User Management</a></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<li><a href="{{ url_for('profile') }}">Profile</a></li>
|
||||||
|
<li><a href="{{ url_for('logout') }}">Logout</a></li>
|
||||||
|
{% else %}
|
||||||
|
<li><a href="{{ url_for('login') }}">Login</a></li>
|
||||||
|
<li><a href="{{ url_for('register') }}">Register</a></li>
|
||||||
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||||
|
{% if messages %}
|
||||||
|
<div class="flash-messages">
|
||||||
|
{% for category, message in messages %}
|
||||||
|
<div class="alert alert-{{ category }}">{{ message }}</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
{% block content %}{% endblock %}
|
{% block content %}{% endblock %}
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<footer>
|
<footer>
|
||||||
<p>© 2025 TimeTrack</p>
|
<p>© {{ current_year }} TimeTrack. All rights reserved.</p>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<script src="{{ url_for('static', filename='js/script.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/script.js') }}"></script>
|
||||||
|
|||||||
42
templates/login.html
Normal file
42
templates/login.html
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
{% extends "layout.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="auth-container">
|
||||||
|
<h1>Login to TimeTrack</h1>
|
||||||
|
|
||||||
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||||
|
{% if messages %}
|
||||||
|
{% for category, message in messages %}
|
||||||
|
<div class="alert alert-{{ category }}">{{ message }}</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
<form method="POST" action="{{ url_for('login') }}" class="auth-form">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="username">Username</label>
|
||||||
|
<input type="text" id="username" name="username" class="form-control" required autofocus>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="password">Password</label>
|
||||||
|
<input type="password" id="password" name="password" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="checkbox-container">
|
||||||
|
<input type="checkbox" name="remember"> Remember me
|
||||||
|
<span class="checkmark"></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<button type="submit" class="btn btn-primary">Login</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="auth-links">
|
||||||
|
<p>Don't have an account? <a href="{{ url_for('register') }}">Register here</a></p>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
49
templates/profile.html
Normal file
49
templates/profile.html
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
{% extends "layout.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="profile-container">
|
||||||
|
<h1>My Profile</h1>
|
||||||
|
|
||||||
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||||
|
{% if messages %}
|
||||||
|
{% for category, message in messages %}
|
||||||
|
<div class="alert alert-{{ category }}">{{ message }}</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
<div class="profile-info">
|
||||||
|
<p><strong>Username:</strong> {{ user.username }}</p>
|
||||||
|
<p><strong>Account Type:</strong> {% if user.is_admin %}Administrator{% else %}User{% endif %}</p>
|
||||||
|
<p><strong>Member Since:</strong> {{ user.created_at.strftime('%Y-%m-%d') }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>Update Profile</h2>
|
||||||
|
<form method="POST" action="{{ url_for('profile') }}" class="profile-form">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="email">Email</label>
|
||||||
|
<input type="email" id="email" name="email" class="form-control" value="{{ user.email }}" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3>Change Password</h3>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="current_password">Current Password</label>
|
||||||
|
<input type="password" id="current_password" name="current_password" class="form-control">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="new_password">New Password</label>
|
||||||
|
<input type="password" id="new_password" name="new_password" class="form-control">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="confirm_password">Confirm New Password</label>
|
||||||
|
<input type="password" id="confirm_password" name="confirm_password" class="form-control">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<button type="submit" class="btn btn-primary">Update Profile</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
45
templates/register.html
Normal file
45
templates/register.html
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
{% extends "layout.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="auth-container">
|
||||||
|
<h1>Register for TimeTrack</h1>
|
||||||
|
|
||||||
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||||
|
{% if messages %}
|
||||||
|
{% for category, message in messages %}
|
||||||
|
<div class="alert alert-{{ category }}">{{ message }}</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
<form method="POST" action="{{ url_for('register') }}" class="auth-form">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="username">Username</label>
|
||||||
|
<input type="text" id="username" name="username" class="form-control" required autofocus>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="email">Email</label>
|
||||||
|
<input type="email" id="email" name="email" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="password">Password</label>
|
||||||
|
<input type="password" id="password" name="password" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="confirm_password">Confirm Password</label>
|
||||||
|
<input type="password" id="confirm_password" name="confirm_password" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<button type="submit" class="btn btn-primary">Register</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="auth-links">
|
||||||
|
<p>Already have an account? <a href="{{ url_for('login') }}">Login here</a></p>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
Reference in New Issue
Block a user