login
This commit is contained in:
@@ -1,19 +1,34 @@
|
||||
from flask import Flask
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from flask_login import LoginManager
|
||||
from config import config
|
||||
from app.models import db, Base
|
||||
from app.models import db, Base, User
|
||||
from flask_migrate import Migrate
|
||||
|
||||
|
||||
# Initialize Flask-Login
|
||||
login_manager = LoginManager()
|
||||
login_manager.login_view = 'auth.login'
|
||||
login_manager.login_message = 'Please log in to access this page.'
|
||||
login_manager.login_message_category = 'warning'
|
||||
|
||||
def create_app(config_name='default'):
|
||||
app = Flask(__name__)
|
||||
app.config.from_object(config[config_name])
|
||||
|
||||
# Initialize extensions
|
||||
db.init_app(app)
|
||||
migrate = Migrate(app, db)
|
||||
login_manager.init_app(app)
|
||||
|
||||
# Register blueprints
|
||||
from app.routes import main
|
||||
from app.auth import auth
|
||||
app.register_blueprint(main)
|
||||
app.register_blueprint(auth, url_prefix='/auth')
|
||||
|
||||
# User loader for Flask-Login
|
||||
@login_manager.user_loader
|
||||
def load_user(user_id):
|
||||
return User.query.get(int(user_id))
|
||||
|
||||
return app
|
||||
131
app/auth.py
Normal file
131
app/auth.py
Normal file
@@ -0,0 +1,131 @@
|
||||
from flask import Blueprint, render_template, request, redirect, url_for, flash, jsonify
|
||||
from flask_login import login_user, logout_user, login_required, current_user
|
||||
from werkzeug.security import generate_password_hash, check_password_hash
|
||||
from app import db
|
||||
from app.models import User
|
||||
from datetime import datetime
|
||||
import re
|
||||
|
||||
auth = Blueprint('auth', __name__)
|
||||
|
||||
def validate_password(password):
|
||||
"""Validate password strength."""
|
||||
if len(password) < 8:
|
||||
return False, "Password must be at least 8 characters long"
|
||||
if not re.search(r'[A-Z]', password):
|
||||
return False, "Password must contain at least one uppercase letter"
|
||||
if not re.search(r'[a-z]', password):
|
||||
return False, "Password must contain at least one lowercase letter"
|
||||
if not re.search(r'\d', password):
|
||||
return False, "Password must contain at least one digit"
|
||||
return True, "Password is valid"
|
||||
|
||||
@auth.route('/login', methods=['GET', 'POST'])
|
||||
def login():
|
||||
"""Handle user login."""
|
||||
if current_user.is_authenticated:
|
||||
return redirect(url_for('main.index'))
|
||||
|
||||
if request.method == 'POST':
|
||||
email = request.form.get('email')
|
||||
password = request.form.get('password')
|
||||
remember = request.form.get('remember', False)
|
||||
|
||||
# Validate input
|
||||
if not email:
|
||||
flash('Email is required', 'error')
|
||||
return render_template('auth/login.html')
|
||||
|
||||
if not password:
|
||||
flash('Password is required', 'error')
|
||||
return render_template('auth/login.html')
|
||||
|
||||
# Find user by email
|
||||
user = User.query.filter_by(email=email).first()
|
||||
|
||||
if user and user.check_password(password):
|
||||
# Login successful
|
||||
login_user(user, remember=remember)
|
||||
next_page = request.args.get('next')
|
||||
return redirect(next_page) if next_page else redirect(url_for('main.index'))
|
||||
else:
|
||||
# Login failed
|
||||
flash('Invalid email or password', 'error')
|
||||
|
||||
return render_template('auth/login.html')
|
||||
|
||||
@auth.route('/signup', methods=['GET', 'POST'])
|
||||
def signup():
|
||||
"""Handle user registration."""
|
||||
if current_user.is_authenticated:
|
||||
return redirect(url_for('main.index'))
|
||||
|
||||
if request.method == 'POST':
|
||||
first_name = request.form.get('first_name')
|
||||
last_name = request.form.get('last_name')
|
||||
email = request.form.get('email')
|
||||
password = request.form.get('password')
|
||||
confirm_password = request.form.get('confirm_password')
|
||||
|
||||
# Validate input
|
||||
errors = []
|
||||
|
||||
if not first_name:
|
||||
errors.append('First name is required')
|
||||
elif len(first_name) > 50:
|
||||
errors.append('First name must be less than 50 characters')
|
||||
|
||||
if not last_name:
|
||||
errors.append('Last name is required')
|
||||
elif len(last_name) > 50:
|
||||
errors.append('Last name must be less than 50 characters')
|
||||
|
||||
if not email:
|
||||
errors.append('Email is required')
|
||||
elif not re.match(r'^[^\s@]+@[^\s@]+\.[^\s@]+$', email):
|
||||
errors.append('Please enter a valid email address')
|
||||
elif User.query.filter_by(email=email).first():
|
||||
errors.append('Email already registered')
|
||||
|
||||
if not password:
|
||||
errors.append('Password is required')
|
||||
else:
|
||||
is_valid, message = validate_password(password)
|
||||
if not is_valid:
|
||||
errors.append(message)
|
||||
|
||||
if not confirm_password:
|
||||
errors.append('Please confirm your password')
|
||||
elif password != confirm_password:
|
||||
errors.append('Passwords do not match')
|
||||
|
||||
if errors:
|
||||
return render_template('auth/signup.html', errors=errors,
|
||||
first_name=first_name, last_name=last_name, email=email)
|
||||
|
||||
# Create new user
|
||||
user = User(
|
||||
first_name=first_name.strip(),
|
||||
last_name=last_name.strip(),
|
||||
email=email.strip()
|
||||
)
|
||||
user.set_password(password)
|
||||
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
|
||||
# Login the new user
|
||||
login_user(user)
|
||||
|
||||
flash('Account created successfully!', 'success')
|
||||
return redirect(url_for('main.index'))
|
||||
|
||||
return render_template('auth/signup.html')
|
||||
|
||||
@auth.route('/logout')
|
||||
@login_required
|
||||
def logout():
|
||||
"""Handle user logout."""
|
||||
logout_user()
|
||||
flash('You have been logged out', 'info')
|
||||
return redirect(url_for('auth.login'))
|
||||
@@ -1,19 +1,36 @@
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from sqlalchemy.orm import declarative_base
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from werkzeug.security import generate_password_hash, check_password_hash
|
||||
from datetime import datetime
|
||||
from flask_login import UserMixin
|
||||
|
||||
import uuid
|
||||
|
||||
Base = declarative_base()
|
||||
db = SQLAlchemy(model_class=Base)
|
||||
|
||||
class User(Base):
|
||||
class User(Base, UserMixin):
|
||||
__tablename__ = 'users'
|
||||
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
||||
first_name = db.Column(db.String(255), nullable=False)
|
||||
last_name = db.Column(db.String(255), nullable=False)
|
||||
email = db.Column(db.String(255), unique=True, nullable=False)
|
||||
# Placeholders for Milestone 1
|
||||
password_hash = db.Column(db.LargeBinary)
|
||||
password_hash = db.Column(db.String(2048), nullable=False)
|
||||
imap_config = db.Column(db.JSON) # Using db.JSON instead of db.JSONB for compatibility
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
|
||||
def set_password(self, password):
|
||||
"""Hash and set the password."""
|
||||
self.password_hash = generate_password_hash(password)
|
||||
|
||||
def check_password(self, password):
|
||||
"""Check if the provided password matches the stored hash."""
|
||||
return check_password_hash(self.password_hash, password)
|
||||
|
||||
def __repr__(self):
|
||||
return f'<User {self.first_name} {self.last_name} ({self.email})>'
|
||||
|
||||
class Folder(Base):
|
||||
__tablename__ = 'folders'
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from flask import Blueprint, render_template, request, jsonify, make_response
|
||||
from flask import Blueprint, render_template, request, jsonify, make_response, flash, redirect, url_for
|
||||
from flask_login import login_required, current_user
|
||||
from app import db
|
||||
from app.models import Folder, User
|
||||
import uuid
|
||||
@@ -6,22 +7,15 @@ import logging
|
||||
|
||||
main = Blueprint('main', __name__)
|
||||
|
||||
# For prototype, use a fixed user ID
|
||||
MOCK_USER_ID = 1
|
||||
|
||||
@main.route('/')
|
||||
@login_required
|
||||
def index():
|
||||
# Ensure the mock user exists
|
||||
user = db.session.get(User, MOCK_USER_ID)
|
||||
if not user:
|
||||
user = User(id=MOCK_USER_ID, email='prototype@example.com')
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
|
||||
folders = Folder.query.filter_by(user_id=MOCK_USER_ID).all()
|
||||
# Get folders for the current authenticated user
|
||||
folders = Folder.query.filter_by(user_id=current_user.id).all()
|
||||
return render_template('index.html', folders=folders)
|
||||
|
||||
@main.route('/api/folders/new', methods=['GET'])
|
||||
@login_required
|
||||
def new_folder_modal():
|
||||
# Return the add folder modal
|
||||
response = make_response(render_template('partials/folder_modal.html'))
|
||||
@@ -29,6 +23,7 @@ def new_folder_modal():
|
||||
return response
|
||||
|
||||
@main.route('/api/folders', methods=['POST'])
|
||||
@login_required
|
||||
def add_folder():
|
||||
try:
|
||||
# Get form data instead of JSON
|
||||
@@ -59,9 +54,9 @@ def add_folder():
|
||||
response.headers['HX-Reswap'] = 'outerHTML'
|
||||
return response
|
||||
|
||||
# Create new folder
|
||||
# Create new folder for the current user
|
||||
folder = Folder(
|
||||
user_id=MOCK_USER_ID,
|
||||
user_id=current_user.id,
|
||||
name=name.strip(),
|
||||
rule_text=rule_text.strip(),
|
||||
priority=int(priority) if priority else 0
|
||||
@@ -70,8 +65,8 @@ def add_folder():
|
||||
db.session.add(folder)
|
||||
db.session.commit()
|
||||
|
||||
# Get updated list of folders
|
||||
folders = Folder.query.filter_by(user_id=MOCK_USER_ID).all()
|
||||
# Get updated list of folders for the current user
|
||||
folders = Folder.query.filter_by(user_id=current_user.id).all()
|
||||
|
||||
# Return the updated folders list HTML
|
||||
response = make_response(render_template('partials/folders_list.html', folders=folders))
|
||||
@@ -91,22 +86,23 @@ def add_folder():
|
||||
return response
|
||||
|
||||
@main.route('/api/folders/<folder_id>', methods=['DELETE'])
|
||||
@login_required
|
||||
def delete_folder(folder_id):
|
||||
try:
|
||||
# Find the folder by ID
|
||||
folder = Folder.query.filter_by(id=folder_id, user_id=MOCK_USER_ID).first()
|
||||
# Find the folder by ID and ensure it belongs to the current user
|
||||
folder = Folder.query.filter_by(id=folder_id, user_id=current_user.id).first()
|
||||
|
||||
if not folder:
|
||||
# Folder not found
|
||||
folders = Folder.query.filter_by(user_id=MOCK_USER_ID).all()
|
||||
folders = Folder.query.filter_by(user_id=current_user.id).all()
|
||||
return render_template('partials/folders_list.html', folders=folders)
|
||||
|
||||
# Delete the folder
|
||||
db.session.delete(folder)
|
||||
db.session.commit()
|
||||
|
||||
# Get updated list of folders
|
||||
folders = Folder.query.filter_by(user_id=MOCK_USER_ID).all()
|
||||
# Get updated list of folders for the current user
|
||||
folders = Folder.query.filter_by(user_id=current_user.id).all()
|
||||
|
||||
# Return the updated folders list HTML
|
||||
return render_template('partials/folders_list.html', folders=folders)
|
||||
@@ -116,14 +112,15 @@ def delete_folder(folder_id):
|
||||
logging.exception("Error deleting folder: %s", e)
|
||||
db.session.rollback()
|
||||
# Return the folders list unchanged
|
||||
folders = Folder.query.filter_by(user_id=MOCK_USER_ID).all()
|
||||
folders = Folder.query.filter_by(user_id=current_user.id).all()
|
||||
return render_template('partials/folders_list.html', folders=folders)
|
||||
|
||||
@main.route('/api/folders/<folder_id>/edit', methods=['GET'])
|
||||
@login_required
|
||||
def edit_folder_modal(folder_id):
|
||||
try:
|
||||
# Find the folder by ID
|
||||
folder = Folder.query.filter_by(id=folder_id, user_id=MOCK_USER_ID).first()
|
||||
# Find the folder by ID and ensure it belongs to the current user
|
||||
folder = Folder.query.filter_by(id=folder_id, user_id=current_user.id).first()
|
||||
|
||||
if not folder:
|
||||
return jsonify({'error': 'Folder not found'}), 404
|
||||
@@ -139,14 +136,15 @@ def edit_folder_modal(folder_id):
|
||||
return jsonify({'error': 'Error retrieving folder'}), 500
|
||||
|
||||
@main.route('/api/folders/<folder_id>', methods=['PUT'])
|
||||
@login_required
|
||||
def update_folder(folder_id):
|
||||
try:
|
||||
# Find the folder by ID
|
||||
folder = Folder.query.filter_by(id=folder_id, user_id=MOCK_USER_ID).first()
|
||||
# Find the folder by ID and ensure it belongs to the current user
|
||||
folder = Folder.query.filter_by(id=folder_id, user_id=current_user.id).first()
|
||||
|
||||
if not folder:
|
||||
# Folder not found
|
||||
folders = Folder.query.filter_by(user_id=MOCK_USER_ID).all()
|
||||
folders = Folder.query.filter_by(user_id=current_user.id).all()
|
||||
return render_template('partials/folders_list.html', folders=folders)
|
||||
|
||||
# Get form data
|
||||
@@ -184,8 +182,8 @@ def update_folder(folder_id):
|
||||
|
||||
db.session.commit()
|
||||
|
||||
# Get updated list of folders
|
||||
folders = Folder.query.filter_by(user_id=MOCK_USER_ID).all()
|
||||
# Get updated list of folders for the current user
|
||||
folders = Folder.query.filter_by(user_id=current_user.id).all()
|
||||
|
||||
response = make_response(render_template('partials/folders_list.html', folders=folders))
|
||||
response.headers['HX-Trigger'] = 'close-modal'
|
||||
|
||||
82
app/templates/auth/login.html
Normal file
82
app/templates/auth/login.html
Normal file
@@ -0,0 +1,82 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Login - Email Organizer</title>
|
||||
<script src="https://unpkg.com/htmx.org@1.9.12"></script>
|
||||
<link href="https://cdn.jsdelivr.net/npm/daisyui@5" rel="stylesheet" type="text/css" />
|
||||
<link href="https://cdn.jsdelivr.net/npm/daisyui@5/themes.css" rel="stylesheet" type="text/css" />
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
<script src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
||||
</head>
|
||||
<body class="min-h-screen flex bg-base-200">
|
||||
<div class="flex flex-col justify-center items-center min-h-screen w-full p-4">
|
||||
<div class="card bg-base-100 shadow-xl w-full max-w-md">
|
||||
<div class="card-body">
|
||||
<div class="text-center mb-6">
|
||||
<h1 class="text-3xl font-bold text-primary">
|
||||
<i class="fas fa-envelope mr-2"></i>
|
||||
Email Organizer
|
||||
</h1>
|
||||
<p class="text-base-content/70 mt-2">Sign in to your account</p>
|
||||
</div>
|
||||
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
<div class="alert alert-{{ 'error' if category == 'error' else 'success' }} mb-4">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<span>{{ message }}</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<form method="POST" action="{{ url_for('auth.login') }}">
|
||||
<div class="form-control mb-4">
|
||||
<label class="label">
|
||||
<span class="label-text">Email</span>
|
||||
</label>
|
||||
<input type="email" name="email" class="input input-bordered w-full"
|
||||
placeholder="Enter your email" required>
|
||||
</div>
|
||||
|
||||
<div class="form-control mb-4">
|
||||
<label class="label">
|
||||
<span class="label-text">Password</span>
|
||||
</label>
|
||||
<input type="password" name="password" class="input input-bordered w-full"
|
||||
placeholder="Enter your password" required>
|
||||
</div>
|
||||
|
||||
<div class="form-control mb-6">
|
||||
<label class="label cursor-pointer">
|
||||
<span class="label-text">Remember me</span>
|
||||
<input type="checkbox" name="remember" class="checkbox checkbox-primary">
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<button type="submit" class="btn btn-primary w-full">
|
||||
<i class="fas fa-sign-in-alt mr-2"></i>
|
||||
Sign In
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="text-center mt-6">
|
||||
<p class="text-base-content/70">
|
||||
Don't have an account?
|
||||
<a href="{{ url_for('auth.signup') }}" class="link link-primary">
|
||||
Sign up
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
130
app/templates/auth/signup.html
Normal file
130
app/templates/auth/signup.html
Normal file
@@ -0,0 +1,130 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-theme="cupcake">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Create Account - Email Organizer</title>
|
||||
<script src="https://unpkg.com/htmx.org@1.9.12"></script>
|
||||
<link href="https://cdn.jsdelivr.net/npm/daisyui@5" rel="stylesheet" type="text/css" />
|
||||
<link href="https://cdn.jsdelivr.net/npm/daisyui@5/themes.css" rel="stylesheet" type="text/css" />
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
<script src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
||||
</head>
|
||||
<body class="min-h-screen flex bg-base-200">
|
||||
<div class="flex flex-col justify-center items-center min-h-screen w-full p-4">
|
||||
<div class="card bg-base-100 shadow-xl w-full max-w-md">
|
||||
<div class="card-body">
|
||||
<div class="text-center mb-6">
|
||||
<h1 class="text-3xl font-bold text-primary">
|
||||
<i class="fas fa-envelope mr-2"></i>
|
||||
Email Organizer
|
||||
</h1>
|
||||
<p class="text-base-content/70 mt-2">Create your account</p>
|
||||
</div>
|
||||
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
<div class="alert alert-{{ 'error' if category == 'error' else 'success' }} mb-4">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<span>{{ message }}</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
{% if errors %}
|
||||
<div class="alert alert-error mb-4">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<span>Please fix the following errors:</span>
|
||||
</div>
|
||||
<ul class="text-error text-sm mb-4">
|
||||
{% for error in errors %}
|
||||
<li>• {{ error }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
<form method="POST" action="{{ url_for('auth.signup') }}">
|
||||
<div class="form-control mb-4">
|
||||
<label class="label">
|
||||
<span class="label-text">First Name</span>
|
||||
</label>
|
||||
<input type="text" name="first_name" class="input input-bordered w-full"
|
||||
placeholder="Enter your first name" value="{{ first_name or '' }}" required>
|
||||
</div>
|
||||
|
||||
<div class="form-control mb-4">
|
||||
<label class="label">
|
||||
<span class="label-text">Last Name</span>
|
||||
</label>
|
||||
<input type="text" name="last_name" class="input input-bordered w-full"
|
||||
placeholder="Enter your last name" value="{{ last_name or '' }}" required>
|
||||
</div>
|
||||
|
||||
<div class="form-control mb-4">
|
||||
<label class="label">
|
||||
<span class="label-text">Email</span>
|
||||
</label>
|
||||
<input type="email" name="email" class="input input-bordered w-full"
|
||||
placeholder="Enter your email" value="{{ email or '' }}" required>
|
||||
</div>
|
||||
|
||||
<div class="form-control mb-4">
|
||||
<label class="label">
|
||||
<span class="label-text">Password</span>
|
||||
</label>
|
||||
<input type="password" name="password" class="input input-bordered w-full"
|
||||
placeholder="Create a password" required>
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-xs text-base-content/50">
|
||||
Password must be at least 8 characters with uppercase, lowercase, and numbers
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-control mb-6">
|
||||
<label class="label">
|
||||
<span class="label-text">Confirm Password</span>
|
||||
</label>
|
||||
<input type="password" name="confirm_password" class="input input-bordered w-full"
|
||||
placeholder="Confirm your password" required>
|
||||
</div>
|
||||
|
||||
<div class="form-control mb-6">
|
||||
<label class="label cursor-pointer">
|
||||
<input type="checkbox" name="terms" class="checkbox checkbox-primary" required>
|
||||
<span class="label-text">
|
||||
I agree to the
|
||||
<a href="#" class="link link-primary">Terms of Service</a>
|
||||
and
|
||||
<a href="#" class="link link-primary">Privacy Policy</a>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<button type="submit" class="btn btn-primary w-full">
|
||||
<i class="fas fa-user-plus mr-2"></i>
|
||||
Create Account
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="text-center mt-6">
|
||||
<p class="text-base-content/70">
|
||||
Already have an account?
|
||||
<a href="{{ url_for('auth.login') }}" class="link link-primary">
|
||||
Sign in
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -70,7 +70,7 @@
|
||||
|
||||
<div class="mt-auto pt-4 border-t border-base-300">
|
||||
<div>
|
||||
<a class="btn btn-ghost justify-start">
|
||||
<a href="{{ url_for('auth.logout') }}" class="btn btn-ghost justify-start">
|
||||
<i class="fas fa-sign-out-alt mr-3 text-error"></i>
|
||||
Logout
|
||||
</a>
|
||||
@@ -96,9 +96,9 @@
|
||||
<div class="relative" x-data="{ open: false }" @click.outside="open = false">
|
||||
<button id="user-menu-button" class="flex items-center space-x-2 p-2 rounded-lg hover:bg-base-300" @click="open = !open">
|
||||
<div class="w-8 h-8 rounded-full bg-primary flex items-center justify-center">
|
||||
<span class="font-semibold text-primary-content">U</span>
|
||||
<span class="font-semibold text-primary-content">{{ current_user.first_name[0] }}{{ current_user.last_name[0] }}</span>
|
||||
</div>
|
||||
<span class="hidden md:inline">User Name</span>
|
||||
<span class="hidden md:inline">{{ current_user.first_name }} {{ current_user.last_name }}</span>
|
||||
<i class="fas fa-chevron-down"></i>
|
||||
</button>
|
||||
|
||||
@@ -106,7 +106,7 @@
|
||||
<div id="user-dropdown" class="user-dropdown absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-base-100 z-10" x-show="open">
|
||||
<a href="#" class="block px-4 py-2 text-sm hover:bg-base-300">Profile</a>
|
||||
<a href="#" class="block px-4 py-2 text-sm hover:bg-base-300">Settings</a>
|
||||
<a href="#" class="block px-4 py-2 text-sm hover:bg-base-300">Logout</a>
|
||||
<a href="{{ url_for('auth.logout') }}" class="block px-4 py-2 text-sm hover:bg-base-300">Logout</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user