This commit is contained in:
2025-05-25 10:31:22 +02:00
parent 1caeb8fc98
commit 225e33056a
102 changed files with 8390 additions and 0 deletions

33
routes/__init__.py Normal file
View File

@@ -0,0 +1,33 @@
from flask import Blueprint, Flask, render_template
from flask_login import login_required
def init_app(app: Flask):
# Create blueprints
main_bp = Blueprint('main', __name__)
auth_bp = Blueprint('auth', __name__, url_prefix='/auth')
rooms_bp = Blueprint('rooms', __name__)
# Import and initialize routes
from .main import init_routes as init_main_routes
from .auth import init_routes as init_auth_routes
from .contacts import contacts_bp as contacts_routes
from .rooms import rooms_bp as rooms_routes
# Initialize routes
init_main_routes(main_bp)
init_auth_routes(auth_bp)
# Register blueprints
app.register_blueprint(main_bp)
app.register_blueprint(auth_bp)
app.register_blueprint(rooms_routes)
app.register_blueprint(contacts_routes)
@app.route('/rooms/<int:room_id>/trash')
@login_required
def trash_page(room_id):
return render_template('trash.html')
return app
# This file makes the routes directory a Python package

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

62
routes/auth.py Normal file
View File

@@ -0,0 +1,62 @@
from flask import render_template, request, flash, redirect, url_for
from flask_login import login_user, logout_user, login_required, current_user
from models import db, User
def init_routes(auth_bp):
@auth_bp.route('/login', methods=['GET', 'POST'])
def login():
if current_user.is_authenticated:
return redirect(url_for('main.dashboard'))
if request.method == 'POST':
email = request.form.get('email')
password = request.form.get('password')
remember = True if request.form.get('remember') else False
user = User.query.filter_by(email=email).first()
if not user or not user.check_password(password):
flash('Please check your login details and try again.', 'danger')
return redirect(url_for('auth.login'))
login_user(user, remember=remember)
return redirect(url_for('main.dashboard'))
return render_template('login.html')
@auth_bp.route('/register', methods=['GET', 'POST'])
def register():
if current_user.is_authenticated:
return redirect(url_for('main.dashboard'))
if request.method == 'POST':
email = request.form.get('email')
username = request.form.get('username')
password = request.form.get('password')
user = User.query.filter_by(email=email).first()
if user:
flash('Email address already exists', 'danger')
return redirect(url_for('auth.register'))
user = User.query.filter_by(username=username).first()
if user:
flash('Username already exists', 'danger')
return redirect(url_for('auth.register'))
new_user = User(email=email, username=username)
new_user.set_password(password)
db.session.add(new_user)
db.session.commit()
flash('Registration successful! Please login.', 'success')
return redirect(url_for('auth.login'))
return render_template('register.html')
@auth_bp.route('/logout')
@login_required
def logout():
logout_user()
return redirect(url_for('main.home'))

264
routes/contacts.py Normal file
View File

@@ -0,0 +1,264 @@
from flask import Blueprint, render_template, redirect, url_for, flash, request
from flask_login import login_required, current_user
from models import db, User
from forms import UserForm
from flask import abort
from sqlalchemy import or_
import json
import os
from werkzeug.utils import secure_filename
contacts_bp = Blueprint('contacts', __name__, url_prefix='/contacts')
UPLOAD_FOLDER = os.path.join(os.getcwd(), 'uploads', 'profile_pics')
if not os.path.exists(UPLOAD_FOLDER):
os.makedirs(UPLOAD_FOLDER)
def admin_required():
if not current_user.is_authenticated:
return redirect(url_for('auth.login'))
if not current_user.is_admin:
flash('You must be an admin to access this page.', 'error')
return redirect(url_for('main.dashboard'))
@contacts_bp.route('/')
@login_required
def contacts_list():
result = admin_required()
if result: return result
# Get query parameters
page = request.args.get('page', 1, type=int)
per_page = 10 # Number of items per page
search = request.args.get('search', '')
status = request.args.get('status', '')
role = request.args.get('role', '')
# Start with base query
query = User.query
# Apply search filter
if search:
search_term = f"%{search}%"
query = query.filter(
or_(
User.username.ilike(search_term),
User.last_name.ilike(search_term),
User.email.ilike(search_term),
User.company.ilike(search_term),
User.position.ilike(search_term)
)
)
# Apply status filter
if status == 'active':
query = query.filter(User.is_active == True)
elif status == 'inactive':
query = query.filter(User.is_active == False)
# Apply role filter
if role == 'admin':
query = query.filter(User.is_admin == True)
elif role == 'user':
query = query.filter(User.is_admin == False)
# Order by creation date
query = query.order_by(User.created_at.desc())
# Get pagination
pagination = query.paginate(page=page, per_page=per_page, error_out=False)
users = pagination.items
return render_template('contacts/list.html',
users=users,
pagination=pagination,
current_user=current_user)
@contacts_bp.route('/new', methods=['GET', 'POST'])
@login_required
def new_contact():
result = admin_required()
if result: return result
form = UserForm()
total_admins = User.query.filter_by(is_admin=True).count()
if request.method == 'GET':
form.is_admin.data = False # Ensure admin role is unchecked by default
elif request.method == 'POST' and 'is_admin' not in request.form:
form.is_admin.data = False # Explicitly set to False if not present in POST
if form.validate_on_submit():
# Check if a user with this email already exists
existing_user = User.query.filter_by(email=form.email.data).first()
if existing_user:
flash('A user with this email already exists.', 'error')
return render_template('contacts/form.html', form=form, title='New User', total_admins=total_admins)
# Handle profile picture upload
profile_picture = None
file = request.files.get('profile_picture')
if file and file.filename:
filename = secure_filename(file.filename)
file_path = os.path.join(UPLOAD_FOLDER, filename)
file.save(file_path)
profile_picture = filename
# Create new user account
user = User(
username=form.first_name.data,
last_name=form.last_name.data,
email=form.email.data,
phone=form.phone.data,
company=form.company.data,
position=form.position.data,
notes=form.notes.data,
is_active=form.is_active.data,
is_admin=form.is_admin.data,
profile_picture=profile_picture
)
user.set_password('changeme') # Set a default password that must be changed
db.session.add(user)
db.session.commit()
flash('User created successfully! They will need to set their password on first login.', 'success')
return redirect(url_for('contacts.contacts_list'))
return render_template('contacts/form.html', form=form, title='New User', total_admins=total_admins)
@contacts_bp.route('/profile/edit', methods=['GET', 'POST'])
@login_required
def edit_profile():
form = UserForm()
total_admins = User.query.filter_by(is_admin=True).count()
if form.validate_on_submit():
# Check if trying to remove admin status from the only admin
if not form.is_admin.data and current_user.is_admin:
if total_admins <= 1:
flash('There must be at least one admin user in the system.', 'error')
return render_template('contacts/form.html', form=form, title='Edit Profile', total_admins=total_admins)
current_user.username = form.first_name.data
current_user.last_name = form.last_name.data
current_user.email = form.email.data
current_user.phone = form.phone.data
current_user.company = form.company.data
current_user.position = form.position.data
current_user.notes = form.notes.data
current_user.is_active = form.is_active.data
current_user.is_admin = form.is_admin.data
# Set password if provided
if form.new_password.data:
current_user.set_password(form.new_password.data)
db.session.commit()
flash('Profile updated successfully!', 'success')
return redirect(url_for('contacts.contacts_list'))
# Pre-fill the form with current user data
if request.method == 'GET':
form.first_name.data = current_user.username
form.last_name.data = current_user.last_name
form.email.data = current_user.email
form.phone.data = current_user.phone
form.company.data = current_user.company
form.position.data = current_user.position
form.notes.data = current_user.notes
form.is_active.data = current_user.is_active
form.is_admin.data = current_user.is_admin
return render_template('contacts/form.html', form=form, title='Edit Profile', total_admins=total_admins)
@contacts_bp.route('/<int:id>/edit', methods=['GET', 'POST'])
@login_required
def edit_contact(id):
result = admin_required()
if result: return result
total_admins = User.query.filter_by(is_admin=True).count()
user = User.query.get_or_404(id)
form = UserForm()
if request.method == 'GET':
form.first_name.data = user.username
form.last_name.data = user.last_name
form.email.data = user.email
form.phone.data = user.phone
form.company.data = user.company
form.position.data = user.position
form.notes.data = user.notes
form.is_active.data = user.is_active
form.is_admin.data = user.is_admin
if form.validate_on_submit():
# Handle profile picture removal
if 'remove_picture' in request.form:
if user.profile_picture:
# Delete the old profile picture file
old_picture_path = os.path.join(UPLOAD_FOLDER, user.profile_picture)
if os.path.exists(old_picture_path):
os.remove(old_picture_path)
user.profile_picture = None
db.session.commit()
flash('Profile picture removed successfully!', 'success')
return redirect(url_for('contacts.edit_contact', id=user.id))
# Handle profile picture upload
file = request.files.get('profile_picture')
if file and file.filename:
# Delete old profile picture if it exists
if user.profile_picture:
old_picture_path = os.path.join(UPLOAD_FOLDER, user.profile_picture)
if os.path.exists(old_picture_path):
os.remove(old_picture_path)
filename = secure_filename(file.filename)
file_path = os.path.join(UPLOAD_FOLDER, filename)
file.save(file_path)
user.profile_picture = filename
# Prevent removing admin from the last admin
if not form.is_admin.data and user.is_admin and total_admins <= 1:
flash('There must be at least one admin user in the system.', 'error')
return render_template('contacts/form.html', form=form, title='Edit User', total_admins=total_admins, user=user)
# Check if the new email is already used by another user
if form.email.data != user.email:
existing_user = User.query.filter_by(email=form.email.data).first()
if existing_user:
flash('A user with this email already exists.', 'error')
return render_template('contacts/form.html', form=form, title='Edit User', total_admins=total_admins, user=user)
user.username = form.first_name.data
user.last_name = form.last_name.data
user.email = form.email.data
user.phone = form.phone.data
user.company = form.company.data
user.position = form.position.data
user.notes = form.notes.data
user.is_active = form.is_active.data
user.is_admin = form.is_admin.data
# Set password if provided
if form.new_password.data:
user.set_password(form.new_password.data)
db.session.commit()
flash('User updated successfully!', 'success')
return redirect(url_for('contacts.contacts_list'))
return render_template('contacts/form.html', form=form, title='Edit User', total_admins=total_admins, user=user)
@contacts_bp.route('/<int:id>/delete', methods=['POST'])
@login_required
def delete_contact(id):
result = admin_required()
if result: return result
user = User.query.get_or_404(id)
if user.email == current_user.email:
flash('You cannot delete your own account.', 'error')
return redirect(url_for('contacts.contacts_list'))
db.session.delete(user)
db.session.commit()
flash('User deleted successfully!', 'success')
return redirect(url_for('contacts.contacts_list'))
@contacts_bp.route('/<int:id>/toggle-active', methods=['POST'])
@login_required
def toggle_active(id):
result = admin_required()
if result: return result
user = User.query.get_or_404(id)
if user.email == current_user.email:
flash('You cannot deactivate your own account.', 'error')
return redirect(url_for('contacts.contacts_list'))
user.is_active = not user.is_active
db.session.commit()
flash(f'User marked as {"active" if user.is_active else "inactive"}!', 'success')
return redirect(url_for('contacts.contacts_list'))

284
routes/main.py Normal file
View File

@@ -0,0 +1,284 @@
from flask import render_template, Blueprint, redirect, url_for, request, flash
from flask_login import current_user, login_required
from models import User, db, Room, RoomFile, RoomMemberPermission
import os
from werkzeug.utils import secure_filename
from sqlalchemy import func, case, literal_column, text
from datetime import datetime, timedelta
def init_routes(main_bp):
@main_bp.route('/')
def home():
if current_user.is_authenticated:
return redirect(url_for('main.dashboard'))
return render_template('home.html')
@main_bp.route('/dashboard')
@login_required
def dashboard():
# Get 3 most recent users
recent_contacts = User.query.order_by(User.created_at.desc()).limit(3).all()
# Count active and inactive users
active_count = User.query.filter_by(is_active=True).count()
inactive_count = User.query.filter_by(is_active=False).count()
# Room count and size logic
if current_user.is_admin:
room_count = Room.query.count()
# Get total file and folder counts for admin
file_count = RoomFile.query.filter_by(type='file').count()
folder_count = RoomFile.query.filter_by(type='folder').count()
# Get total size of all files including trash
total_size = db.session.query(func.sum(RoomFile.size)).filter(RoomFile.type == 'file').scalar() or 0
# Get recent activity for all files
recent_activity = db.session.query(
RoomFile,
Room,
User
).join(
Room, RoomFile.room_id == Room.id
).join(
User, RoomFile.uploaded_by == User.id
).order_by(
RoomFile.uploaded_at.desc()
).limit(10).all()
# Format the activity data
formatted_activity = []
for file, room, user in recent_activity:
activity = {
'name': file.name,
'type': file.type,
'room': room,
'uploader': user,
'uploaded_at': file.uploaded_at,
'is_starred': current_user in file.starred_by,
'is_deleted': file.deleted,
'can_download': True # Admin can download everything
}
formatted_activity.append(activity)
recent_activity = formatted_activity
# Get storage usage by file type including trash
storage_by_type = db.session.query(
case(
(RoomFile.name.like('%.%'),
func.split_part(RoomFile.name, '.', -1)),
else_=literal_column("'unknown'")
).label('extension'),
func.count(RoomFile.id).label('count'),
func.sum(RoomFile.size).label('total_size')
).filter(
RoomFile.type == 'file'
).group_by('extension').all()
# Get trash and starred stats for admin
trash_count = RoomFile.query.filter_by(deleted=True).count()
starred_count = RoomFile.query.filter(RoomFile.starred_by.contains(current_user)).count()
# Get oldest trash date and total trash size
oldest_trash = RoomFile.query.filter_by(deleted=True).order_by(RoomFile.deleted_at.asc()).first()
oldest_trash_date = oldest_trash.deleted_at.strftime('%Y-%m-%d') if oldest_trash else None
trash_size = db.session.query(db.func.sum(RoomFile.size)).filter(RoomFile.deleted==True).scalar() or 0
# Get files that will be deleted in next 7 days
seven_days_from_now = datetime.utcnow() + timedelta(days=7)
thirty_days_ago = datetime.utcnow() - timedelta(days=30)
pending_deletion = RoomFile.query.filter(
RoomFile.deleted==True,
RoomFile.deleted_at <= thirty_days_ago,
RoomFile.deleted_at > thirty_days_ago - timedelta(days=7)
).count()
# Get trash file type breakdown
trash_by_type = db.session.query(
case(
(RoomFile.name.like('%.%'),
func.split_part(RoomFile.name, '.', -1)),
else_=literal_column("'unknown'")
).label('extension'),
func.count(RoomFile.id).label('count')
).filter(
RoomFile.deleted==True
).group_by('extension').all()
else:
# Get rooms the user has access to
accessible_rooms = Room.query.filter(Room.members.any(id=current_user.id)).all()
room_count = len(accessible_rooms)
# Get file and folder counts for accessible rooms
room_ids = [room.id for room in accessible_rooms]
file_count = RoomFile.query.filter(RoomFile.room_id.in_(room_ids), RoomFile.type == 'file').count()
folder_count = RoomFile.query.filter(RoomFile.room_id.in_(room_ids), RoomFile.type == 'folder').count()
# Get total size of files in accessible rooms including trash
total_size = db.session.query(func.sum(RoomFile.size)).filter(
RoomFile.room_id.in_(room_ids),
RoomFile.type == 'file'
).scalar() or 0
# Get recent activity for accessible rooms
recent_activity = db.session.query(
RoomFile,
Room,
User
).join(
Room, RoomFile.room_id == Room.id
).join(
User, RoomFile.uploaded_by == User.id
).filter(
RoomFile.room_id.in_(room_ids)
).order_by(
RoomFile.uploaded_at.desc()
).limit(10).all()
# Format the activity data
formatted_activity = []
user_perms = {p.room_id: p for p in RoomMemberPermission.query.filter(
RoomMemberPermission.room_id.in_(room_ids),
RoomMemberPermission.user_id==current_user.id
).all()}
for file, room, user in recent_activity:
perm = user_perms.get(room.id)
activity = {
'name': file.name,
'type': file.type,
'room': room,
'uploader': user,
'uploaded_at': file.uploaded_at,
'is_starred': current_user in file.starred_by,
'is_deleted': file.deleted,
'can_download': perm.can_download if perm else False
}
formatted_activity.append(activity)
recent_activity = formatted_activity
# Get storage usage by file type for accessible rooms including trash
storage_by_type = db.session.query(
case(
(RoomFile.name.like('%.%'),
func.split_part(RoomFile.name, '.', -1)),
else_=literal_column("'unknown'")
).label('extension'),
func.count(RoomFile.id).label('count'),
func.sum(RoomFile.size).label('total_size')
).filter(
RoomFile.room_id.in_(room_ids),
RoomFile.type == 'file'
).group_by('extension').all()
# Get trash and starred stats for user's accessible rooms
trash_count = RoomFile.query.filter(RoomFile.room_id.in_(room_ids), RoomFile.deleted==True).count()
starred_count = RoomFile.query.filter(
RoomFile.room_id.in_(room_ids),
RoomFile.starred_by.contains(current_user)
).count()
# Get oldest trash date and total trash size for user's rooms
oldest_trash = RoomFile.query.filter(RoomFile.room_id.in_(room_ids), RoomFile.deleted==True).order_by(RoomFile.deleted_at.asc()).first()
oldest_trash_date = oldest_trash.deleted_at.strftime('%Y-%m-%d') if oldest_trash else None
trash_size = db.session.query(db.func.sum(RoomFile.size)).filter(RoomFile.room_id.in_(room_ids), RoomFile.deleted==True).scalar() or 0
# Get files that will be deleted in next 7 days for user's rooms
seven_days_from_now = datetime.utcnow() + timedelta(days=7)
thirty_days_ago = datetime.utcnow() - timedelta(days=30)
pending_deletion = RoomFile.query.filter(
RoomFile.room_id.in_(room_ids),
RoomFile.deleted==True,
RoomFile.deleted_at <= thirty_days_ago,
RoomFile.deleted_at > thirty_days_ago - timedelta(days=7)
).count()
# Get trash file type breakdown for user's rooms
trash_by_type = db.session.query(
case(
(RoomFile.name.like('%.%'),
func.split_part(RoomFile.name, '.', -1)),
else_=literal_column("'unknown'")
).label('extension'),
func.count(RoomFile.id).label('count')
).filter(
RoomFile.room_id.in_(room_ids),
RoomFile.deleted==True
).group_by('extension').all()
return render_template('dashboard.html',
recent_contacts=recent_contacts,
active_count=active_count,
inactive_count=inactive_count,
room_count=room_count,
file_count=file_count,
folder_count=folder_count,
total_size=total_size,
recent_activity=recent_activity,
storage_by_type=storage_by_type,
trash_count=trash_count,
starred_count=starred_count,
oldest_trash_date=oldest_trash_date,
trash_size=trash_size,
pending_deletion=pending_deletion,
trash_by_type=trash_by_type)
UPLOAD_FOLDER = os.path.join(os.getcwd(), 'uploads', 'profile_pics')
if not os.path.exists(UPLOAD_FOLDER):
os.makedirs(UPLOAD_FOLDER)
@main_bp.route('/profile', methods=['GET', 'POST'])
@login_required
def profile():
if request.method == 'POST':
# Handle profile picture removal
if 'remove_picture' in request.form:
if current_user.profile_picture:
# Delete the old profile picture file
old_picture_path = os.path.join(UPLOAD_FOLDER, current_user.profile_picture)
if os.path.exists(old_picture_path):
os.remove(old_picture_path)
current_user.profile_picture = None
db.session.commit()
flash('Profile picture removed successfully!', 'success')
return redirect(url_for('main.profile'))
new_email = request.form.get('email')
# Check if the new email is already used by another user
if new_email != current_user.email:
existing_user = User.query.filter_by(email=new_email).first()
if existing_user:
flash('A user with this email already exists.', 'error')
return render_template('profile.html')
# Handle profile picture upload
file = request.files.get('profile_picture')
if file and file.filename:
filename = secure_filename(file.filename)
file_path = os.path.join(UPLOAD_FOLDER, filename)
file.save(file_path)
current_user.profile_picture = filename
# Update user information
current_user.username = request.form.get('first_name')
current_user.last_name = request.form.get('last_name')
current_user.email = new_email
current_user.phone = request.form.get('phone')
current_user.company = request.form.get('company')
current_user.position = request.form.get('position')
current_user.notes = request.form.get('notes')
# Handle password change if provided
new_password = request.form.get('new_password')
confirm_password = request.form.get('confirm_password')
if new_password:
if new_password != confirm_password:
flash('Passwords do not match.', 'error')
return render_template('profile.html')
current_user.set_password(new_password)
flash('Password updated successfully.', 'success')
try:
db.session.commit()
flash('Profile updated successfully!', 'success')
except Exception as e:
db.session.rollback()
flash('An error occurred while updating your profile.', 'error')
return redirect(url_for('main.profile'))
return render_template('profile.html')
@main_bp.route('/starred')
@login_required
def starred():
return render_template('starred.html')
@main_bp.route('/trash')
@login_required
def trash():
return render_template('trash.html')

668
routes/room_files.py Normal file
View File

@@ -0,0 +1,668 @@
from flask import Blueprint, jsonify, request, abort, send_from_directory, send_file
from flask_login import login_required, current_user
import os
from models import Room, RoomMemberPermission, RoomFile, TrashedFile, db
from werkzeug.utils import secure_filename, safe_join
import time
import shutil
import io
import zipfile
from datetime import datetime
room_files_bp = Blueprint('room_files', __name__, url_prefix='/api/rooms')
DATA_ROOT = '/data/rooms' # This should be a Docker volume
ALLOWED_EXTENSIONS = {
# Documents
'pdf', 'docx', 'doc', 'txt', 'rtf', 'odt', 'md', 'csv',
# Spreadsheets
'xlsx', 'xls', 'ods', 'xlsm',
# Presentations
'pptx', 'ppt', 'odp',
# Images
'jpg', 'jpeg', 'png', 'gif', 'bmp', 'svg', 'webp', 'tiff',
# Archives
'zip', 'rar', '7z', 'tar', 'gz',
# Code/Text
'py', 'js', 'html', 'css', 'json', 'xml', 'sql', 'sh', 'bat',
# Audio
'mp3', 'wav', 'ogg', 'm4a', 'flac',
# Video
'mp4', 'avi', 'mov', 'wmv', 'flv', 'mkv', 'webm',
# CAD/Design
'dwg', 'dxf', 'ai', 'psd', 'eps', 'indd',
# Other
'eml', 'msg', 'vcf', 'ics'
}
def get_room_dir(room_id):
return os.path.join(DATA_ROOT, str(room_id))
def user_has_permission(room, perm_name):
if current_user.is_admin:
return True
perm = RoomMemberPermission.query.filter_by(room_id=room.id, user_id=current_user.id).first()
return getattr(perm, perm_name, False) if perm else False
def allowed_file(filename):
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
def clean_path(path):
if not path:
return ''
return path.strip('/\\')
@room_files_bp.route('/<int:room_id>/files', methods=['GET'])
@login_required
def list_room_files(room_id):
room = Room.query.get_or_404(room_id)
if not user_has_permission(room, 'can_view'):
abort(403)
path = request.args.get('path', '')
path = clean_path(path)
# Get files in the current path
files = RoomFile.query.filter_by(room_id=room_id, path=path, deleted=False).all()
# Debug: Check user permissions
if not current_user.is_admin:
perm = RoomMemberPermission.query.filter_by(room_id=room.id, user_id=current_user.id).first()
print("=== User Permissions ===")
print(f" - can_view: {getattr(perm, 'can_view', False) if perm else False}")
print(f" - can_download: {getattr(perm, 'can_download', False) if perm else False}")
print(f" - can_upload: {getattr(perm, 'can_upload', False) if perm else False}")
print(f" - can_delete: {getattr(perm, 'can_delete', False) if perm else False}")
print(f" - can_rename: {getattr(perm, 'can_rename', False) if perm else False}")
print(f" - can_move: {getattr(perm, 'can_move', False) if perm else False}")
print(f" - can_share: {getattr(perm, 'can_share', False) if perm else False}")
print("-------------------")
result = []
for f in files:
uploader_full_name = None
uploader_profile_pic = None
if f.uploader:
uploader_full_name = f.uploader.username
if getattr(f.uploader, 'last_name', None):
uploader_full_name += ' ' + f.uploader.last_name
uploader_profile_pic = f.uploader.profile_picture if getattr(f.uploader, 'profile_picture', None) else None
result.append({
'name': f.name,
'type': f.type,
'size': f.size if f.type == 'file' else '-',
'modified': f.modified,
'uploaded_by': uploader_full_name,
'uploader_profile_pic': uploader_profile_pic,
'uploaded_at': f.uploaded_at.isoformat() if f.uploaded_at else None,
'path': f.path,
'starred': current_user in f.starred_by
})
print(f"Returning {len(result)} files") # Debug log
return jsonify(result)
@room_files_bp.route('/<int:room_id>/files/upload', methods=['POST'])
@login_required
def upload_room_file(room_id):
room = Room.query.get_or_404(room_id)
if not user_has_permission(room, 'can_upload'):
abort(403)
if 'file' not in request.files:
return jsonify({'error': 'No file part'}), 400
file = request.files['file']
if file.filename == '':
return jsonify({'error': 'No selected file'}), 400
if not allowed_file(file.filename):
return jsonify({'error': 'File type not allowed'}), 400
filename = secure_filename(file.filename)
room_dir = get_room_dir(room_id)
rel_path = clean_path(request.form.get('path', ''))
target_dir = os.path.join(room_dir, rel_path) if rel_path else room_dir
os.makedirs(target_dir, exist_ok=True)
file_path = os.path.join(target_dir, filename)
# Check for overwrite flag
overwrite = request.form.get('overwrite', 'false').lower() == 'true'
# First check for non-deleted files
existing_file = RoomFile.query.filter_by(room_id=room_id, name=filename, path=rel_path, deleted=False).first()
if existing_file and not overwrite:
return jsonify({'error': 'A file with this name already exists in this location', 'conflict': True}), 409
# Then check for deleted files
trashed_file = RoomFile.query.filter_by(room_id=room_id, name=filename, path=rel_path, deleted=True).first()
if trashed_file:
# If we're not overwriting, return conflict
if not overwrite:
return jsonify({'error': 'A file with this name exists in the trash', 'conflict': True}), 409
# If we are overwriting, delete the trashed file record
db.session.delete(trashed_file)
db.session.commit()
existing_file = None
file.save(file_path)
stat = os.stat(file_path)
if existing_file:
# Overwrite: update the RoomFile record
existing_file.size = stat.st_size
existing_file.modified = stat.st_mtime
existing_file.uploaded_by = current_user.id
existing_file.uploaded_at = datetime.utcnow()
db.session.commit()
return jsonify({'success': True, 'filename': filename, 'overwritten': True})
else:
rf = RoomFile(
room_id=room_id,
name=filename,
path=rel_path,
type='file',
size=stat.st_size,
modified=stat.st_mtime,
uploaded_by=current_user.id,
uploaded_at=datetime.utcnow()
)
db.session.add(rf)
db.session.commit()
return jsonify({'success': True, 'filename': filename})
@room_files_bp.route('/<int:room_id>/files/<filename>', methods=['GET'])
@login_required
def download_room_file(room_id, filename):
room = Room.query.get_or_404(room_id)
if not user_has_permission(room, 'can_download'):
abort(403)
rel_path = clean_path(request.args.get('path', ''))
# Lookup in RoomFile
rf = RoomFile.query.filter_by(room_id=room_id, name=filename, path=rel_path).first()
if not rf or rf.type != 'file':
return jsonify({'error': 'File not found'}), 404
room_dir = get_room_dir(room_id)
file_path = os.path.join(room_dir, rel_path, filename) if rel_path else os.path.join(room_dir, filename)
if not os.path.exists(file_path):
return jsonify({'error': 'File not found'}), 404
return send_from_directory(os.path.dirname(file_path), filename, as_attachment=True)
@room_files_bp.route('/<int:room_id>/files/<path:filename>', methods=['DELETE'])
@login_required
def delete_file(room_id, filename):
room = Room.query.get_or_404(room_id)
if not user_has_permission(room, 'can_delete'):
abort(403)
rel_path = clean_path(request.args.get('path', ''))
# Lookup in RoomFile
rf = RoomFile.query.filter_by(room_id=room_id, name=filename, path=rel_path).first()
if not rf:
return jsonify({'error': 'File not found'}), 404
# Mark as deleted and record who deleted it and when
rf.deleted = True
rf.deleted_by = current_user.id
rf.deleted_at = datetime.utcnow()
db.session.commit()
return jsonify({'success': True})
@room_files_bp.route('/<int:room_id>/folders', methods=['POST'])
@login_required
def create_room_folder(room_id):
room = Room.query.get_or_404(room_id)
if not user_has_permission(room, 'can_upload'):
abort(403)
data = request.get_json()
folder_name = data.get('name', '').strip()
rel_path = clean_path(data.get('path', ''))
if not folder_name or '/' in folder_name or '\\' in folder_name or folder_name.startswith('.'):
return jsonify({'error': 'Invalid folder name'}), 400
room_dir = get_room_dir(room_id)
target_dir = os.path.join(room_dir, rel_path) if rel_path else room_dir
os.makedirs(target_dir, exist_ok=True)
folder_path = os.path.join(target_dir, folder_name)
# First check for trashed folder
trashed_folder = RoomFile.query.filter_by(
room_id=room_id,
name=folder_name,
path=rel_path,
type='folder',
deleted=True
).first()
if trashed_folder:
return jsonify({'error': 'A folder with this name exists in the trash'}), 409
# Then check for existing folder in current location
existing_folder = RoomFile.query.filter_by(
room_id=room_id,
name=folder_name,
path=rel_path,
type='folder',
deleted=False
).first()
if existing_folder:
return jsonify({'error': 'A folder with this name already exists in this location'}), 400
if os.path.exists(folder_path):
return jsonify({'error': 'A folder with this name already exists in this location'}), 400
os.makedirs(folder_path)
# Add RoomFile entry
stat = os.stat(folder_path)
rf = RoomFile(
room_id=room_id,
name=folder_name,
path=rel_path,
type='folder',
size=None,
modified=stat.st_mtime,
uploaded_by=current_user.id,
uploaded_at=datetime.utcnow()
)
db.session.add(rf)
db.session.commit()
return jsonify({'success': True, 'name': folder_name})
@room_files_bp.route('/<int:room_id>/rename', methods=['POST'])
@login_required
def rename_room_file(room_id):
room = Room.query.get_or_404(room_id)
# Allow rename if user can upload or delete
if not (user_has_permission(room, 'can_upload') or user_has_permission(room, 'can_delete')):
abort(403)
data = request.get_json()
old_name = data.get('old_name', '').strip()
new_name = data.get('new_name', '').strip()
rel_path = clean_path(data.get('path', ''))
if not old_name or not new_name or '/' in new_name or '\\' in new_name or new_name.startswith('.'):
return jsonify({'error': 'Invalid name'}), 400
# Lookup in RoomFile
rf = RoomFile.query.filter_by(room_id=room_id, name=old_name, path=rel_path).first()
if not rf:
return jsonify({'error': 'Original file/folder not found'}), 404
room_dir = get_room_dir(room_id)
base_dir = os.path.join(room_dir, rel_path) if rel_path else room_dir
old_path = os.path.join(base_dir, old_name)
new_path = os.path.join(base_dir, new_name)
if not os.path.exists(old_path):
return jsonify({'error': 'Original file/folder not found'}), 404
if os.path.exists(new_path):
return jsonify({'error': 'A file or folder with the new name already exists'}), 400
# Prevent file extension change for files
if os.path.isfile(old_path):
old_ext = os.path.splitext(old_name)[1].lower()
new_ext = os.path.splitext(new_name)[1].lower()
if old_ext != new_ext:
return jsonify({'error': 'File extension cannot be changed'}), 400
os.rename(old_path, new_path)
# Update RoomFile entry
rf.name = new_name
rf.modified = os.path.getmtime(new_path)
db.session.commit()
return jsonify({'success': True, 'old_name': old_name, 'new_name': new_name})
@room_files_bp.route('/<int:room_id>/download-zip', methods=['POST'])
@login_required
def download_zip(room_id):
room = Room.query.get_or_404(room_id)
if not user_has_permission(room, 'can_view'):
abort(403)
data = request.get_json()
items = data.get('items', [])
if not items or not isinstance(items, list):
return jsonify({'error': 'No items selected'}), 400
room_dir = get_room_dir(room_id)
mem_zip = io.BytesIO()
with zipfile.ZipFile(mem_zip, 'w', zipfile.ZIP_DEFLATED) as zf:
for item in items:
name = item.get('name')
rel_path = item.get('path', '').strip('/\\')
abs_path = os.path.join(room_dir, rel_path, name) if rel_path else os.path.join(room_dir, name)
if not os.path.exists(abs_path):
continue
if os.path.isfile(abs_path):
zf.write(abs_path, arcname=name)
elif os.path.isdir(abs_path):
for root, dirs, files in os.walk(abs_path):
for file in files:
file_path = os.path.join(root, file)
arcname = os.path.relpath(file_path, room_dir)
zf.write(file_path, arcname=arcname)
mem_zip.seek(0)
return send_file(mem_zip, mimetype='application/zip', as_attachment=True, download_name='download.zip')
@room_files_bp.route('/<int:room_id>/search', methods=['GET'])
@login_required
def search_room_files(room_id):
room = Room.query.get_or_404(room_id)
if not user_has_permission(room, 'can_view'):
abort(403)
query = request.args.get('query', '').strip().lower()
# Search RoomFile for this room
files = RoomFile.query.filter(RoomFile.room_id==room_id).all()
matches = []
for f in files:
if query in f.name.lower():
matches.append({
'name': f.name,
'type': f.type,
'size': f.size if f.type == 'file' else '-',
'modified': f.modified,
'uploaded_by': f.uploader.username if f.uploader else None,
'uploaded_at': f.uploaded_at.isoformat() if f.uploaded_at else None,
'path': f.path,
})
return jsonify(matches)
@room_files_bp.route('/<int:room_id>/move', methods=['POST'])
@login_required
def move_room_file(room_id):
room = Room.query.get_or_404(room_id)
if not user_has_permission(room, 'can_move'):
abort(403)
data = request.get_json()
filename = data.get('filename', '').strip()
source_path = clean_path(data.get('source_path', ''))
target_path = clean_path(data.get('target_path', ''))
if not filename or '/' in filename or '\\' in filename or filename.startswith('.'):
return jsonify({'error': 'Invalid filename'}), 400
# Lookup in RoomFile
rf = RoomFile.query.filter_by(room_id=room_id, name=filename, path=source_path).first()
if not rf:
return jsonify({'error': 'File not found'}), 404
room_dir = get_room_dir(room_id)
source_dir = os.path.join(room_dir, source_path) if source_path else room_dir
target_dir = os.path.join(room_dir, target_path) if target_path else room_dir
source_file_path = os.path.join(source_dir, filename)
target_file_path = os.path.join(target_dir, filename)
if not os.path.exists(source_file_path):
return jsonify({'error': 'Source file not found'}), 404
if os.path.exists(target_file_path):
return jsonify({'error': 'A file with this name already exists in the target location'}), 400
# Create target directory if it doesn't exist
os.makedirs(target_dir, exist_ok=True)
# Move the file
shutil.move(source_file_path, target_file_path)
# Update RoomFile entry
rf.path = target_path
rf.modified = os.path.getmtime(target_file_path)
db.session.commit()
return jsonify({'success': True})
@room_files_bp.route('/<int:room_id>/folders', methods=['GET'])
@login_required
def list_room_folders(room_id):
room = Room.query.get_or_404(room_id)
if not user_has_permission(room, 'can_view'):
abort(403)
# Get all files in the room that are not deleted
files = RoomFile.query.filter_by(room_id=room_id, deleted=False).all()
# Extract unique folder paths
folders = set()
for f in files:
if f.type == 'folder':
full_path = f.path + '/' + f.name if f.path else f.name
folders.add(full_path)
# Convert to sorted list
folder_list = sorted(list(folders))
return jsonify(folder_list)
@room_files_bp.route('/<int:room_id>/star', methods=['POST'])
@login_required
def toggle_star(room_id):
room = Room.query.get_or_404(room_id)
if not user_has_permission(room, 'can_view'):
abort(403)
data = request.get_json()
filename = data.get('filename', '').strip()
rel_path = clean_path(data.get('path', ''))
if not filename:
return jsonify({'error': 'Invalid filename'}), 400
# Lookup in RoomFile
rf = RoomFile.query.filter_by(room_id=room_id, name=filename, path=rel_path).first()
if not rf:
return jsonify({'error': 'File not found'}), 404
# Check if the file is already starred by this user
is_starred = current_user in rf.starred_by
if is_starred:
# Unstar the file
rf.starred_by.remove(current_user)
else:
# Star the file
rf.starred_by.append(current_user)
db.session.commit()
return jsonify({'success': True, 'starred': not is_starred})
@room_files_bp.route('/<int:room_id>/starred', methods=['GET'])
@login_required
def get_starred_files(room_id):
room = Room.query.get_or_404(room_id)
if not user_has_permission(room, 'can_view'):
abort(403)
# Get all starred files in the room
files = RoomFile.query.filter_by(room_id=room_id, starred=True).all()
result = []
for f in files:
uploader_full_name = None
uploader_profile_pic = None
if f.uploader:
uploader_full_name = f.uploader.username
if getattr(f.uploader, 'last_name', None):
uploader_full_name += ' ' + f.uploader.last_name
uploader_profile_pic = f.uploader.profile_picture if getattr(f.uploader, 'profile_picture', None) else None
result.append({
'name': f.name,
'type': f.type,
'size': f.size if f.type == 'file' else '-',
'modified': f.modified,
'uploaded_by': uploader_full_name,
'uploader_profile_pic': uploader_profile_pic,
'uploaded_at': f.uploaded_at.isoformat() if f.uploaded_at else None,
'path': f.path,
'starred': f.starred
})
return jsonify(result)
@room_files_bp.route('/starred', methods=['GET'])
@login_required
def get_all_starred_files():
# Get all rooms the user has access to
if current_user.is_admin:
rooms = Room.query.all()
else:
rooms = Room.query.filter(Room.members.any(id=current_user.id)).all()
room_ids = [room.id for room in rooms]
room_names = {room.id: room.name for room in rooms}
# Get all files starred by the current user from accessible rooms
files = RoomFile.query.filter(
RoomFile.room_id.in_(room_ids),
RoomFile.starred_by.contains(current_user)
).all()
result = []
for f in files:
uploader_full_name = None
uploader_profile_pic = None
if f.uploader:
uploader_full_name = f.uploader.username
if getattr(f.uploader, 'last_name', None):
uploader_full_name += ' ' + f.uploader.last_name
uploader_profile_pic = f.uploader.profile_picture if getattr(f.uploader, 'profile_picture', None) else None
result.append({
'name': f.name,
'type': f.type,
'size': f.size if f.type == 'file' else '-',
'modified': f.modified,
'uploaded_by': uploader_full_name,
'uploader_profile_pic': uploader_profile_pic,
'uploaded_at': f.uploaded_at.isoformat() if f.uploaded_at else None,
'path': f.path,
'starred': True,
'room_id': f.room_id,
'room_name': room_names.get(f.room_id)
})
return jsonify(result)
@room_files_bp.route('/trash', methods=['GET'])
@login_required
def get_trash_files():
# Get all rooms the user has access to
if current_user.is_admin:
rooms = Room.query.all()
else:
rooms = Room.query.filter(Room.members.any(id=current_user.id)).all()
room_ids = [room.id for room in rooms]
room_names = {room.id: room.name for room in rooms}
# Get all deleted files from accessible rooms
files = RoomFile.query.filter(
RoomFile.room_id.in_(room_ids),
RoomFile.deleted == True
).all()
result = []
for f in files:
uploader_full_name = None
uploader_profile_pic = None
if f.uploader:
uploader_full_name = f.uploader.username
if getattr(f.uploader, 'last_name', None):
uploader_full_name += ' ' + f.uploader.last_name
uploader_profile_pic = f.uploader.profile_picture if getattr(f.uploader, 'profile_picture', None) else None
deleter_full_name = None
if f.deleter:
deleter_full_name = f.deleter.username
if getattr(f.deleter, 'last_name', None):
deleter_full_name += ' ' + f.deleter.last_name
# Check if user has delete permission in this room
room = Room.query.get(f.room_id)
has_delete_permission = user_has_permission(room, 'can_delete') if room else False
# Check if user can restore this file
can_restore = has_delete_permission and f.deleted_by == current_user.id
result.append({
'name': f.name,
'type': f.type,
'size': f.size if f.type == 'file' else '-',
'modified': f.modified,
'uploaded_by': uploader_full_name,
'uploader_profile_pic': uploader_profile_pic,
'uploaded_at': f.uploaded_at.isoformat() if f.uploaded_at else None,
'path': f.path,
'room_id': f.room_id,
'room_name': room_names.get(f.room_id),
'deleted_by': deleter_full_name,
'deleted_at': f.deleted_at.isoformat() if f.deleted_at else None,
'can_restore': can_restore
})
return jsonify(result)
@room_files_bp.route('/<int:room_id>/restore', methods=['POST'])
@login_required
def restore_file(room_id):
room = Room.query.get_or_404(room_id)
# Check for delete permission instead of view permission
if not user_has_permission(room, 'can_delete'):
abort(403)
data = request.get_json()
filename = data.get('filename', '').strip()
rel_path = clean_path(data.get('path', ''))
if not filename:
return jsonify({'error': 'Invalid filename'}), 400
# Lookup in RoomFile
rf = RoomFile.query.filter_by(room_id=room_id, name=filename, path=rel_path).first()
if not rf:
return jsonify({'error': 'File not found'}), 404
# Check if the current user was the one who deleted the file
if rf.deleted_by != current_user.id:
return jsonify({'error': 'You can only restore files that you deleted'}), 403
# Restore file by setting deleted to False
rf.deleted = False
db.session.commit()
return jsonify({'success': True})
@room_files_bp.route('/<int:room_id>/delete-permanent', methods=['POST'])
@login_required
def delete_permanent(room_id):
# Only allow admin users to permanently delete files
if not current_user.is_admin:
abort(403)
room = Room.query.get_or_404(room_id)
data = request.get_json()
filename = data.get('filename', '').strip()
rel_path = clean_path(data.get('path', ''))
if not filename:
return jsonify({'error': 'Invalid filename'}), 400
# If filename is '*', delete all deleted files in the room
if filename == '*':
files_to_delete = RoomFile.query.filter_by(room_id=room_id, deleted=True).all()
else:
# Lookup specific file
files_to_delete = [RoomFile.query.filter_by(room_id=room_id, name=filename, path=rel_path).first()]
if not files_to_delete[0]:
return jsonify({'error': 'File not found'}), 404
for rf in files_to_delete:
if not rf:
continue
# Delete the file from storage if it's a file
if rf.type == 'file':
try:
file_path = os.path.join(get_room_dir(room_id), rf.path, rf.name)
if os.path.exists(file_path):
os.remove(file_path)
except Exception as e:
print(f"Error deleting file from storage: {e}")
# Delete the database record
db.session.delete(rf)
db.session.commit()
return jsonify({'success': True})

121
routes/room_members.py Normal file
View File

@@ -0,0 +1,121 @@
from flask import Blueprint, jsonify, request, abort
from flask_login import login_required, current_user
from models import db, Room, User, RoomMemberPermission
from utils import user_has_permission
room_members_bp = Blueprint('room_members', __name__)
@room_members_bp.route('/<int:room_id>/members', methods=['GET'])
@login_required
def list_room_members(room_id):
room = Room.query.get_or_404(room_id)
if not user_has_permission(room, 'can_view'):
abort(403)
members = []
for member in room.members:
permission = RoomMemberPermission.query.filter_by(room_id=room_id, user_id=member.id).first()
members.append({
'id': member.id,
'username': member.username,
'last_name': member.last_name,
'email': member.email,
'profile_picture': member.profile_picture,
'permissions': {
'can_view': permission.can_view if permission else False,
'can_download': permission.can_download if permission else False,
'can_upload': permission.can_upload if permission else False,
'can_delete': permission.can_delete if permission else False,
'can_rename': permission.can_rename if permission else False,
'can_move': permission.can_move if permission else False,
'can_share': permission.can_share if permission else False
}
})
return jsonify(members)
@room_members_bp.route('/<int:room_id>/members', methods=['POST'])
@login_required
def add_room_member(room_id):
room = Room.query.get_or_404(room_id)
if not user_has_permission(room, 'can_share'):
abort(403)
data = request.get_json()
user_id = data.get('user_id')
permissions = data.get('permissions', {})
if not user_id:
return jsonify({'error': 'User ID is required'}), 400
user = User.query.get_or_404(user_id)
# Add user to room members
if user not in room.members:
room.members.append(user)
# Update permissions
permission = RoomMemberPermission.query.filter_by(room_id=room_id, user_id=user_id).first()
if not permission:
permission = RoomMemberPermission(room_id=room_id, user_id=user_id)
db.session.add(permission)
permission.can_view = permissions.get('can_view', True)
permission.can_download = permissions.get('can_download', False)
permission.can_upload = permissions.get('can_upload', False)
permission.can_delete = permissions.get('can_delete', False)
permission.can_rename = permissions.get('can_rename', False)
permission.can_move = permissions.get('can_move', False)
permission.can_share = permissions.get('can_share', False)
db.session.commit()
return jsonify({'success': True})
@room_members_bp.route('/<int:room_id>/members/<int:user_id>', methods=['DELETE'])
@login_required
def remove_room_member(room_id, user_id):
room = Room.query.get_or_404(room_id)
if not user_has_permission(room, 'can_share'):
abort(403)
user = User.query.get_or_404(user_id)
# Remove user from room members
if user in room.members:
room.members.remove(user)
# Remove permissions
permission = RoomMemberPermission.query.filter_by(room_id=room_id, user_id=user_id).first()
if permission:
db.session.delete(permission)
db.session.commit()
return jsonify({'success': True})
@room_members_bp.route('/<int:room_id>/members/<int:user_id>/permissions', methods=['PUT'])
@login_required
def update_member_permissions(room_id, user_id):
room = Room.query.get_or_404(room_id)
if not user_has_permission(room, 'can_share'):
abort(403)
data = request.get_json()
permissions = data.get('permissions', {})
permission = RoomMemberPermission.query.filter_by(room_id=room_id, user_id=user_id).first()
if not permission:
return jsonify({'error': 'User is not a member of this room'}), 404
permission.can_view = permissions.get('can_view', permission.can_view)
permission.can_download = permissions.get('can_download', permission.can_download)
permission.can_upload = permissions.get('can_upload', permission.can_upload)
permission.can_delete = permissions.get('can_delete', permission.can_delete)
permission.can_rename = permissions.get('can_rename', permission.can_rename)
permission.can_move = permissions.get('can_move', permission.can_move)
permission.can_share = permissions.get('can_share', permission.can_share)
db.session.commit()
return jsonify({'success': True})

223
routes/rooms.py Normal file
View File

@@ -0,0 +1,223 @@
from flask import Blueprint, render_template, redirect, url_for, flash, request
from flask_login import login_required, current_user
from models import db, Room, User, RoomMemberPermission, RoomFile
from forms import RoomForm
from routes.room_files import user_has_permission
rooms_bp = Blueprint('rooms', __name__, url_prefix='/rooms')
@rooms_bp.route('/')
@login_required
def rooms():
search = request.args.get('search', '').strip()
if current_user.is_admin:
query = Room.query
else:
query = Room.query.filter(Room.members.any(id=current_user.id))
if search:
query = query.filter(Room.name.ilike(f'%{search}%'))
rooms = query.order_by(Room.created_at.desc()).all()
return render_template('rooms.html', rooms=rooms, search=search)
@rooms_bp.route('/create', methods=['GET', 'POST'])
@login_required
def create_room():
form = RoomForm()
if form.validate_on_submit():
room = Room(
name=form.name.data,
description=form.description.data,
created_by=current_user.id
)
# Add creator as a member with full permissions
room.members.append(current_user)
creator_permission = RoomMemberPermission(
room=room,
user=current_user,
can_view=True,
can_upload=True,
can_delete=True,
can_share=True
)
db.session.add(room)
db.session.add(creator_permission)
db.session.commit()
flash('Room created successfully!', 'success')
return redirect(url_for('rooms.rooms'))
return render_template('create_room.html', form=form)
@rooms_bp.route('/<int:room_id>')
@login_required
def room(room_id):
room = Room.query.get_or_404(room_id)
# Admins always have access
if not current_user.is_admin:
is_member = RoomMemberPermission.query.filter_by(room_id=room_id, user_id=current_user.id).first() is not None
if not is_member:
flash('You do not have access to this room.', 'error')
return redirect(url_for('rooms.rooms'))
can_download = user_has_permission(room, 'can_download')
can_upload = user_has_permission(room, 'can_upload')
can_delete = user_has_permission(room, 'can_delete')
can_rename = user_has_permission(room, 'can_rename')
can_move = user_has_permission(room, 'can_move')
can_share = user_has_permission(room, 'can_share')
return render_template('room.html', room=room, can_download=can_download, can_upload=can_upload, can_delete=can_delete, can_rename=can_rename, can_move=can_move, can_share=can_share)
@rooms_bp.route('/<int:room_id>/members')
@login_required
def room_members(room_id):
room = Room.query.get_or_404(room_id)
# Admins always have access
if not current_user.is_admin:
is_member = RoomMemberPermission.query.filter_by(room_id=room_id, user_id=current_user.id).first() is not None
if not is_member:
flash('You do not have access to this room.', 'error')
return redirect(url_for('rooms.rooms'))
if not current_user.is_admin:
flash('Only administrators can manage room members.', 'error')
return redirect(url_for('rooms.room', room_id=room_id))
member_permissions = {p.user_id: p for p in room.member_permissions}
available_users = User.query.filter(~User.id.in_(member_permissions.keys())).all()
return render_template('room_members.html', room=room, available_users=available_users, member_permissions=member_permissions)
@rooms_bp.route('/<int:room_id>/members/add', methods=['POST'])
@login_required
def add_member(room_id):
room = Room.query.get_or_404(room_id)
# Membership check using RoomMemberPermission
is_member = RoomMemberPermission.query.filter_by(room_id=room_id, user_id=current_user.id).first() is not None
if not is_member:
flash('You do not have access to this room.', 'error')
return redirect(url_for('rooms.rooms'))
if not current_user.is_admin:
flash('Only administrators can manage room members.', 'error')
return redirect(url_for('rooms.room', room_id=room_id))
user_id = request.form.get('user_id')
if not user_id:
flash('Please select a user to add.', 'error')
return redirect(url_for('rooms.room_members', room_id=room_id))
user = User.query.get_or_404(user_id)
# Check if already a member
if RoomMemberPermission.query.filter_by(room_id=room_id, user_id=user.id).first():
flash('User is already a member of this room.', 'error')
else:
perm = RoomMemberPermission(room_id=room_id, user_id=user.id, can_view=True)
db.session.add(perm)
# Ensure user is added to the room.members relationship
if user not in room.members:
room.members.append(user)
db.session.commit()
flash(f'{user.username} has been added to the room.', 'success')
return redirect(url_for('rooms.room_members', room_id=room_id))
@rooms_bp.route('/<int:room_id>/members/<int:user_id>/remove', methods=['POST'])
@login_required
def remove_member(room_id, user_id):
room = Room.query.get_or_404(room_id)
# Membership check using RoomMemberPermission
is_member = RoomMemberPermission.query.filter_by(room_id=room_id, user_id=current_user.id).first() is not None
if not is_member:
flash('You do not have access to this room.', 'error')
return redirect(url_for('rooms.rooms'))
if not current_user.is_admin:
flash('Only administrators can manage room members.', 'error')
return redirect(url_for('rooms.room', room_id=room_id))
if user_id == room.created_by:
flash('Cannot remove the room creator.', 'error')
else:
perm = RoomMemberPermission.query.filter_by(room_id=room_id, user_id=user_id).first()
if not perm:
flash('User is not a member of this room.', 'error')
else:
db.session.delete(perm)
db.session.commit()
flash('User has been removed from the room.', 'success')
return redirect(url_for('rooms.room_members', room_id=room_id))
@rooms_bp.route('/<int:room_id>/members/<int:user_id>/permissions', methods=['POST'])
@login_required
def update_member_permissions(room_id, user_id):
room = Room.query.get_or_404(room_id)
if not current_user.is_admin:
flash('Only administrators can update permissions.', 'error')
return redirect(url_for('rooms.room_members', room_id=room_id))
perm = RoomMemberPermission.query.filter_by(room_id=room_id, user_id=user_id).first()
if not perm:
flash('Member not found.', 'error')
return redirect(url_for('rooms.room_members', room_id=room_id))
perm.can_view = bool(request.form.get('can_view'))
perm.can_download = bool(request.form.get('can_download'))
perm.can_upload = bool(request.form.get('can_upload'))
perm.can_delete = bool(request.form.get('can_delete'))
perm.can_rename = bool(request.form.get('can_rename'))
perm.can_move = bool(request.form.get('can_move'))
perm.can_share = bool(request.form.get('can_share'))
db.session.commit()
flash('Permissions updated.', 'success')
return redirect(url_for('rooms.room_members', room_id=room_id))
@rooms_bp.route('/<int:room_id>/edit', methods=['GET', 'POST'])
@login_required
def edit_room(room_id):
if not current_user.is_admin:
flash('Only administrators can edit rooms.', 'error')
return redirect(url_for('rooms.rooms'))
room = Room.query.get_or_404(room_id)
form = RoomForm()
if form.validate_on_submit():
room.name = form.name.data
room.description = form.description.data
db.session.commit()
flash('Room updated successfully!', 'success')
return redirect(url_for('rooms.rooms'))
# Pre-populate form with existing room data
if request.method == 'GET':
form.name.data = room.name
form.description.data = room.description
return render_template('edit_room.html', form=form, room=room)
@rooms_bp.route('/<int:room_id>/delete', methods=['POST'])
@login_required
def delete_room(room_id):
if not current_user.is_admin:
flash('Only administrators can delete rooms.', 'error')
return redirect(url_for('rooms.rooms'))
room = Room.query.get_or_404(room_id)
room_name = room.name
try:
print(f"Attempting to delete room {room_id} ({room_name})")
# Delete the room (cascade will handle the rest)
db.session.delete(room)
db.session.commit()
print("Room deleted successfully")
flash(f'Room "{room_name}" has been deleted.', 'success')
except Exception as e:
db.session.rollback()
flash('An error occurred while deleting the room. Please try again.', 'error')
print(f"Error deleting room: {str(e)}")
return redirect(url_for('rooms.rooms'))
@rooms_bp.route('/room/<int:room_id>/view/<path:file_path>')
@login_required
def view_file(room_id, file_path):
room = Room.query.get_or_404(room_id)
# Check if user has access to the room
if not current_user.is_admin and current_user not in room.members:
flash('You do not have access to this room.', 'error')
return redirect(url_for('rooms.rooms'))
file = RoomFile.query.filter_by(room_id=room_id, path=file_path).first_or_404()
# Continue with file viewing logic...

110
routes/trash.py Normal file
View File

@@ -0,0 +1,110 @@
from flask import Blueprint, jsonify, request, abort
from flask_login import login_required, current_user
from models import db, Room, RoomFile, TrashedFile
from utils import user_has_permission, clean_path
import os
from datetime import datetime
trash_bp = Blueprint('trash', __name__)
@trash_bp.route('/<int:room_id>/trash', methods=['GET'])
@login_required
def list_trashed_files(room_id):
room = Room.query.get_or_404(room_id)
if not user_has_permission(room, 'can_view'):
abort(403)
# Get all trashed files in the room
files = TrashedFile.query.filter_by(room_id=room_id).order_by(TrashedFile.deleted_at.desc()).all()
result = []
for f in files:
uploader_full_name = None
uploader_profile_pic = None
if f.uploader:
uploader_full_name = f.uploader.username
if getattr(f.uploader, 'last_name', None):
uploader_full_name += ' ' + f.uploader.last_name
uploader_profile_pic = f.uploader.profile_picture if getattr(f.uploader, 'profile_picture', None) else None
deleter_full_name = None
if f.deleter:
deleter_full_name = f.deleter.username
if getattr(f.deleter, 'last_name', None):
deleter_full_name += ' ' + f.deleter.last_name
result.append({
'id': f.id,
'name': f.name,
'type': f.type,
'size': f.size if f.type == 'file' else '-',
'modified': f.modified,
'uploaded_by': uploader_full_name,
'uploader_profile_pic': uploader_profile_pic,
'uploaded_at': f.uploaded_at.isoformat() if f.uploaded_at else None,
'original_path': f.original_path,
'deleted_by': deleter_full_name,
'deleted_at': f.deleted_at.isoformat()
})
return jsonify(result)
@trash_bp.route('/<int:room_id>/trash/<int:trash_id>/restore', methods=['POST'])
@login_required
def restore_file(room_id, trash_id):
room = Room.query.get_or_404(room_id)
if not user_has_permission(room, 'can_upload'):
abort(403)
trashed_file = TrashedFile.query.get_or_404(trash_id)
if trashed_file.room_id != room_id:
abort(404)
# Create new RoomFile entry
rf = RoomFile(
room_id=room_id,
name=trashed_file.name,
path=trashed_file.original_path,
type=trashed_file.type,
size=trashed_file.size,
modified=trashed_file.modified,
uploaded_by=trashed_file.uploaded_by,
uploaded_at=trashed_file.uploaded_at
)
db.session.add(rf)
# Delete the trashed file entry
db.session.delete(trashed_file)
db.session.commit()
return jsonify({'success': True})
@trash_bp.route('/<int:room_id>/trash/<int:trash_id>', methods=['DELETE'])
@login_required
def permanently_delete_file(room_id, trash_id):
room = Room.query.get_or_404(room_id)
if not user_has_permission(room, 'can_delete'):
abort(403)
trashed_file = TrashedFile.query.get_or_404(trash_id)
if trashed_file.room_id != room_id:
abort(404)
# Delete the trashed file entry
db.session.delete(trashed_file)
db.session.commit()
return jsonify({'success': True})
@trash_bp.route('/<int:room_id>/trash/empty', methods=['POST'])
@login_required
def empty_trash(room_id):
room = Room.query.get_or_404(room_id)
if not user_has_permission(room, 'can_delete'):
abort(403)
# Delete all trashed files for this room
TrashedFile.query.filter_by(room_id=room_id).delete()
db.session.commit()
return jsonify({'success': True})

22
routes/user.py Normal file
View File

@@ -0,0 +1,22 @@
from flask import Blueprint, jsonify, request
from flask_login import login_required, current_user
from models import db
user_bp = Blueprint('user', __name__, url_prefix='/api/user')
@user_bp.route('/preferred_view', methods=['GET'])
@login_required
def get_preferred_view():
return jsonify({'preferred_view': current_user.preferred_view})
@user_bp.route('/preferred_view', methods=['POST'])
@login_required
def update_preferred_view():
data = request.get_json()
if not data or 'preferred_view' not in data:
return jsonify({'error': 'Missing preferred_view'}), 400
if data['preferred_view'] not in ['grid', 'list']:
return jsonify({'error': 'Invalid preferred_view'}), 400
current_user.preferred_view = data['preferred_view']
db.session.commit()
return jsonify({'preferred_view': current_user.preferred_view})