Initial commit

This commit is contained in:
admin 2025-05-01 00:24:26 +08:00
commit e157eb4e0f
25 changed files with 2481 additions and 0 deletions

15
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,15 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Python Debugger: Current File",
"type": "debugpy",
"request": "launch",
"program": "${file}",
"console": "integratedTerminal"
}
]
}

35
Dockerfile Normal file
View File

@ -0,0 +1,35 @@
# Use the official Python image from the Docker Hub
FROM python:3.10-slim
# Install dependencies (Including SQLite libraries and timezone data)
RUN apt-get update && \
apt-get install -y \
iputils-ping \
curl \
apt-transport-https \
sqlite3 \
libsqlite3-dev \
tzdata && \
rm -rf /var/lib/apt/lists/*
# Set the timezone environment variable
ENV TZ="Asia/Manila"
# Set the working directory in the container
WORKDIR /app
# Copy the requirements.txt file into the container
COPY requirements.txt .
# Install the dependencies
RUN pip install --upgrade pip && \
pip install --no-cache-dir -r requirements.txt
# Copy the rest of your application code into the container
COPY . .
# Expose port 1000 to the outside world
EXPOSE 1000
# Define the command to run your application (app.py as primary file)
CMD ["python", "app.py"]

1
Procfile Normal file
View File

@ -0,0 +1 @@
web: gunicorn app:app

816
app.py Normal file
View File

@ -0,0 +1,816 @@
import os
import sqlite3
import uuid
import hashlib
import datetime
from flask import Flask, render_template, request, redirect, url_for, flash, session, send_from_directory
from werkzeug.utils import secure_filename
from functools import wraps
app = Flask(__name__)
app.secret_key = os.environ.get('SECRET_KEY', os.urandom(24))
# Configuration
UPLOAD_FOLDER = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'uploads')
ALLOWED_EXTENSIONS = {'pdf', 'docx', 'xlsx', 'jpg', 'png', 'mp4', 'txt'}
DB_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'dms.db')
# Create upload folder if it doesn't exist
if not os.path.exists(UPLOAD_FOLDER):
os.makedirs(UPLOAD_FOLDER)
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # 16MB max upload size
# Production configuration
app.config['PREFERRED_URL_SCHEME'] = 'https'
app.config['SESSION_COOKIE_SECURE'] = False
app.config['SESSION_COOKIE_HTTPONLY'] = True
app.config['PERMANENT_SESSION_LIFETIME'] = datetime.timedelta(days=1)
# Database initialization
def init_db():
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
# Create users table
cursor.execute('''
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
password TEXT NOT NULL,
email TEXT UNIQUE NOT NULL,
role TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
# Create documents table
cursor.execute('''
CREATE TABLE IF NOT EXISTS documents (
id INTEGER PRIMARY KEY AUTOINCREMENT,
filename TEXT NOT NULL,
original_filename TEXT NOT NULL,
custom_filename TEXT NOT NULL,
file_type TEXT NOT NULL,
file_size INTEGER NOT NULL,
category TEXT NOT NULL,
user_id INTEGER NOT NULL,
visibility TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users (id)
)
''')
# Create document_shares table
cursor.execute('''
CREATE TABLE IF NOT EXISTS document_shares (
id INTEGER PRIMARY KEY AUTOINCREMENT,
document_id INTEGER NOT NULL,
shared_by INTEGER NOT NULL,
shared_with INTEGER,
share_link TEXT,
qr_code TEXT,
expires_at TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (document_id) REFERENCES documents (id),
FOREIGN KEY (shared_by) REFERENCES users (id),
FOREIGN KEY (shared_with) REFERENCES users (id)
)
''')
# Create document_versions table
cursor.execute('''
CREATE TABLE IF NOT EXISTS document_versions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
document_id INTEGER NOT NULL,
filename TEXT NOT NULL,
version_number INTEGER NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (document_id) REFERENCES documents (id)
)
''')
# Create analytics table
cursor.execute('''
CREATE TABLE IF NOT EXISTS analytics (
id INTEGER PRIMARY KEY AUTOINCREMENT,
document_id INTEGER NOT NULL,
action TEXT NOT NULL,
user_id INTEGER,
ip_address TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (document_id) REFERENCES documents (id),
FOREIGN KEY (user_id) REFERENCES users (id)
)
''')
# Insert default admin user if not exists
cursor.execute("SELECT * FROM users WHERE username = 'admin'")
if not cursor.fetchone():
# Password: admin123
hashed_password = hashlib.sha256("admin123".encode()).hexdigest()
cursor.execute("INSERT INTO users (username, password, email, role) VALUES (?, ?, ?, ?)",
("admin", hashed_password, "admin@example.com", "admin"))
# Insert default regular user if not exists
cursor.execute("SELECT * FROM users WHERE username = 'user'")
if not cursor.fetchone():
# Password: user123
hashed_password = hashlib.sha256("user123".encode()).hexdigest()
cursor.execute("INSERT INTO users (username, password, email, role) VALUES (?, ?, ?, ?)",
("user", hashed_password, "user@example.com", "user"))
conn.commit()
conn.close()
# Helper functions
def allowed_file(filename):
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
def get_file_type(filename):
ext = filename.rsplit('.', 1)[1].lower()
if ext in ['pdf']:
return 'pdf'
elif ext in ['docx', 'doc']:
return 'document'
elif ext in ['xlsx', 'xls']:
return 'spreadsheet'
elif ext in ['jpg', 'jpeg', 'png', 'gif']:
return 'image'
elif ext in ['mp4', 'avi', 'mov']:
return 'video'
else:
return 'other'
def get_db_connection():
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
return conn
# 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'))
return f(*args, **kwargs)
return decorated_function
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'))
conn = get_db_connection()
user = conn.execute('SELECT * FROM users WHERE id = ?', (session['user_id'],)).fetchone()
conn.close()
if user['role'] != 'admin':
flash('Admin access required', 'error')
return redirect(url_for('dashboard'))
return f(*args, **kwargs)
return decorated_function
# Routes
@app.route('/')
def index():
if 'user_id' in session:
return redirect(url_for('dashboard'))
return render_template('index.html')
@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
username = request.form['username']
password = request.form['password']
hashed_password = hashlib.sha256(password.encode()).hexdigest()
conn = get_db_connection()
user = conn.execute('SELECT * FROM users WHERE username = ? AND password = ?',
(username, hashed_password)).fetchone()
conn.close()
if user:
session['user_id'] = user['id']
session['username'] = user['username']
session['role'] = user['role']
flash(f'Welcome back, {username}!', 'success')
return redirect(url_for('dashboard'))
else:
flash('Invalid username or password', 'error')
return render_template('login.html')
else:
return render_template('login.html')
@app.route('/logout')
def logout():
session.clear()
flash('You have been logged out', 'info')
return redirect(url_for('index'))
@app.route('/dashboard')
@login_required
def dashboard():
conn = get_db_connection()
# Get document counts by category
category_counts = conn.execute('''
SELECT category, COUNT(*) as count
FROM documents
WHERE user_id = ? OR visibility = "public"
OR id IN (SELECT document_id FROM document_shares WHERE shared_with = ?)
GROUP BY category
''', (session['user_id'], session['user_id'])).fetchall()
# Get recent documents (limited to 5)
recent_documents = conn.execute('''
SELECT * FROM documents
WHERE user_id = ? OR visibility = "public"
OR id IN (SELECT document_id FROM document_shares WHERE shared_with = ?)
ORDER BY created_at DESC
LIMIT 5
''', (session['user_id'], session['user_id'])).fetchall()
# Get analytics data for admin
if session['role'] == 'admin':
analytics = conn.execute('''
SELECT documents.custom_filename, COUNT(analytics.id) as view_count
FROM documents
LEFT JOIN analytics ON documents.id = analytics.document_id
WHERE analytics.action = 'view'
GROUP BY documents.id
ORDER BY view_count DESC
LIMIT 5
''').fetchall()
# Get user activity
user_activity = conn.execute('''
SELECT users.username, COUNT(analytics.id) as action_count
FROM analytics
JOIN users ON analytics.user_id = users.id
GROUP BY analytics.user_id
ORDER BY action_count DESC
LIMIT 5
''').fetchall()
else:
analytics = None
user_activity = None
conn.close()
return render_template('dashboard.html',
recent_documents=recent_documents,
category_counts=category_counts,
analytics=analytics,
user_activity=user_activity)
@app.route('/upload', methods=['GET', 'POST'])
@login_required
def upload():
if request.method == 'POST':
# Check if the post request has the file part
if 'file' not in request.files:
flash('No file part', 'error')
return redirect(request.url)
file = request.files['file']
visibility = request.form.get('visibility', 'private')
custom_filename = request.form.get('custom_filename', '').strip()
category = request.form.get('category', 'general')
# If user does not select file, browser also
# submit an empty part without filename
if file.filename == '':
flash('No selected file', 'error')
return redirect(request.url)
if file and allowed_file(file.filename):
# Generate unique filename
original_filename = secure_filename(file.filename)
file_extension = original_filename.rsplit('.', 1)[1].lower()
unique_filename = f"{uuid.uuid4().hex}.{file_extension}"
# Use custom filename if provided, otherwise use original
if not custom_filename:
custom_filename = os.path.splitext(original_filename)[0]
# Save the file
file_path = os.path.join(app.config['UPLOAD_FOLDER'], unique_filename)
file.save(file_path)
# Get file size
file_size = os.path.getsize(file_path)
# Save to database
conn = get_db_connection()
cursor = conn.cursor()
cursor.execute('''
INSERT INTO documents (filename, original_filename, custom_filename, file_type, file_size, category, user_id, visibility)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
''', (unique_filename, original_filename, custom_filename, get_file_type(original_filename), file_size, category, session['user_id'], visibility))
document_id = cursor.lastrowid
# Add initial version
cursor.execute('''
INSERT INTO document_versions (document_id, filename, version_number)
VALUES (?, ?, ?)
''', (document_id, unique_filename, 1))
conn.commit()
conn.close()
flash('File successfully uploaded', 'success')
return redirect(url_for('files'))
else:
flash(f'Allowed file types are: {", ".join(ALLOWED_EXTENSIONS)}', 'error')
# Get available categories
categories = ['admin', 'accounting', 'hr', 'marketing', 'legal', 'general', 'other']
return render_template('upload.html', categories=categories)
@app.route('/files')
@login_required
def files():
# Get query parameters
search_query = request.args.get('search', '')
selected_category = request.args.get('category', 'all')
page = request.args.get('page', 1, type=int)
per_page = 10 # Number of documents per page
conn = get_db_connection()
# Base query
query = '''
SELECT * FROM documents
WHERE (user_id = ? OR visibility = "public"
OR id IN (SELECT document_id FROM document_shares WHERE shared_with = ?))
'''
params = [session['user_id'], session['user_id']]
# Add search condition if search query provided
if search_query:
query += ' AND (custom_filename LIKE ? OR original_filename LIKE ?)'
params.extend(['%' + search_query + '%', '%' + search_query + '%'])
# Add category filter if specific category selected
if selected_category != 'all':
query += ' AND category = ?'
params.append(selected_category)
# Count total matching documents
count_query = query.replace('SELECT *', 'SELECT COUNT(*)')
total_docs = conn.execute(count_query, params).fetchone()[0]
# Add pagination
query += ' ORDER BY created_at DESC LIMIT ? OFFSET ?'
offset = (page - 1) * per_page
params.extend([per_page, offset])
# Execute final query
documents = conn.execute(query, params).fetchall()
# Get available categories for filter dropdown
categories = ['admin', 'accounting', 'hr', 'marketing', 'legal', 'general', 'other']
# Calculate pagination info
total_pages = (total_docs + per_page - 1) // per_page # Ceiling division
pagination = {
'page': page,
'per_page': per_page,
'total': total_docs,
'pages': total_pages
}
conn.close()
return render_template('files.html',
documents=documents,
categories=categories,
selected_category=selected_category,
search_query=search_query,
pagination=pagination)
@app.route('/document/<int:document_id>/edit', methods=['GET', 'POST'])
@login_required
def edit_document(document_id):
conn = get_db_connection()
# Get document
document = conn.execute('SELECT * FROM documents WHERE id = ?', (document_id,)).fetchone()
if not document:
conn.close()
flash('Document not found', 'error')
return redirect(url_for('files'))
# Check ownership
if document['user_id'] != session['user_id'] and session['role'] != 'admin':
conn.close()
flash('You do not have permission to edit this document', 'error')
return redirect(url_for('files'))
# Get available categories
categories = ['admin', 'accounting', 'hr', 'marketing', 'legal', 'general', 'other']
if request.method == 'POST':
custom_filename = request.form.get('custom_filename', '').strip()
category = request.form.get('category', 'general')
visibility = request.form.get('visibility', 'private')
# Use custom filename if provided, otherwise keep existing
if not custom_filename:
custom_filename = document['custom_filename']
# Update document
conn.execute('''
UPDATE documents
SET custom_filename = ?, category = ?, visibility = ?, updated_at = CURRENT_TIMESTAMP
WHERE id = ?
''', (custom_filename, category, visibility, document_id))
conn.commit()
conn.close()
flash('Document updated successfully', 'success')
return redirect(url_for('view_document', document_id=document_id))
conn.close()
return render_template('edit_document.html', document=document, categories=categories)
@app.route('/document/<int:document_id>')
@login_required
def view_document(document_id):
conn = get_db_connection()
# Get document
document = conn.execute('SELECT * FROM documents WHERE id = ?', (document_id,)).fetchone()
if not document:
conn.close()
flash('Document not found', 'error')
return redirect(url_for('dashboard'))
# Check permissions
if document['visibility'] != 'public' and document['user_id'] != session['user_id']:
# Check if shared with user
shared = conn.execute('''
SELECT * FROM document_shares
WHERE document_id = ? AND shared_with = ?
''', (document_id, session['user_id'])).fetchone()
if not shared and session['role'] != 'admin':
conn.close()
flash('You do not have permission to view this document', 'error')
return redirect(url_for('dashboard'))
# Record view in analytics
conn.execute('''
INSERT INTO analytics (document_id, action, user_id, ip_address)
VALUES (?, ?, ?, ?)
''', (document_id, 'view', session['user_id'], request.remote_addr))
# Get document versions
versions = conn.execute('''
SELECT * FROM document_versions
WHERE document_id = ?
ORDER BY version_number DESC
''', (document_id,)).fetchall()
# Get share information
shares = conn.execute('''
SELECT document_shares.*, users.username
FROM document_shares
LEFT JOIN users ON document_shares.shared_with = users.id
WHERE document_id = ?
''', (document_id,)).fetchall()
conn.commit()
conn.close()
return render_template('view_document.html', document=document, versions=versions, shares=shares)
@app.route('/document/<int:document_id>/file')
@login_required
def serve_document_file(document_id):
conn = get_db_connection()
# Get document
document = conn.execute('SELECT * FROM documents WHERE id = ?', (document_id,)).fetchone()
if not document:
conn.close()
flash('Document not found', 'error')
return redirect(url_for('dashboard'))
# Check permissions
if document['visibility'] != 'public' and document['user_id'] != session['user_id']:
shared = conn.execute('''
SELECT * FROM document_shares
WHERE document_id = ? AND shared_with = ?
''', (document_id, session['user_id'])).fetchone()
if not shared and session['role'] != 'admin':
conn.close()
flash('You do not have permission to view this document', 'error')
return redirect(url_for('dashboard'))
try:
# Serve the document without download (viewable in browser)
return send_from_directory(app.config['UPLOAD_FOLDER'], document['filename'],
as_attachment=False, download_name=document['original_filename'])
except FileNotFoundError:
flash('Document file not found', 'error')
conn.close()
return redirect(url_for('dashboard'))
finally:
conn.close()
@app.route('/document/<int:document_id>/download')
@login_required
def download_document(document_id):
conn = get_db_connection()
# Get document
document = conn.execute('SELECT * FROM documents WHERE id = ?', (document_id,)).fetchone()
if not document:
conn.close()
flash('Document not found', 'error')
return redirect(url_for('dashboard'))
# Check permissions
if document['visibility'] != 'public' and document['user_id'] != session['user_id']:
# Check if shared with user
shared = conn.execute('''
SELECT * FROM document_shares
WHERE document_id = ? AND shared_with = ?
''', (document_id, session['user_id'])).fetchone()
if not shared and session['role'] != 'admin':
conn.close()
flash('You do not have permission to download this document', 'error')
return redirect(url_for('dashboard'))
# Record download in analytics
conn.execute('''
INSERT INTO analytics (document_id, action, user_id, ip_address)
VALUES (?, ?, ?, ?)
''', (document_id, 'download', session['user_id'], request.remote_addr))
conn.commit()
conn.close()
return send_from_directory(app.config['UPLOAD_FOLDER'], document['filename'],
as_attachment=True, download_name=document['original_filename'])
@app.route('/document/<int:document_id>/share', methods=['GET', 'POST'])
@login_required
def share_document(document_id):
conn = get_db_connection()
# Get document
document = conn.execute('SELECT * FROM documents WHERE id = ?', (document_id,)).fetchone()
if not document:
conn.close()
flash('Document not found', 'error')
return redirect(url_for('dashboard'))
# Check ownership
if document['user_id'] != session['user_id'] and session['role'] != 'admin':
conn.close()
flash('You do not have permission to share this document', 'error')
return redirect(url_for('dashboard'))
if request.method == 'POST':
share_type = request.form.get('share_type')
if share_type == 'user':
# Share with specific user
username = request.form.get('username')
user = conn.execute('SELECT * FROM users WHERE username = ?', (username,)).fetchone()
if not user:
flash(f'User {username} not found', 'error')
else:
# Check if already shared
existing = conn.execute('''
SELECT * FROM document_shares
WHERE document_id = ? AND shared_with = ?
''', (document_id, user['id'])).fetchone()
if existing:
flash(f'Document already shared with {username}', 'info')
else:
conn.execute('''
INSERT INTO document_shares (document_id, shared_by, shared_with)
VALUES (?, ?, ?)
''', (document_id, session['user_id'], user['id']))
flash(f'Document shared with {username}', 'success')
elif share_type == 'link':
# Generate shareable link
expiry_days = int(request.form.get('expiry', 7))
expires_at = datetime.datetime.now() + datetime.timedelta(days=expiry_days)
share_token = uuid.uuid4().hex
conn.execute('''
INSERT INTO document_shares (document_id, shared_by, share_link, expires_at)
VALUES (?, ?, ?, ?)
''', (document_id, session['user_id'], share_token, expires_at))
flash(f'Shareable link created (expires in {expiry_days} days)', 'success')
conn.commit()
# Get users for sharing
users = conn.execute('SELECT * FROM users WHERE id != ?', (session['user_id'],)).fetchall()
# Get existing shares
shares = conn.execute('''
SELECT document_shares.*, users.username
FROM document_shares
LEFT JOIN users ON document_shares.shared_with = users.id
WHERE document_id = ?
''', (document_id,)).fetchall()
conn.close()
return render_template('share_document.html', document=document, users=users, shares=shares)
@app.route('/document/<int:document_id>/delete')
@login_required
def delete_document(document_id):
conn = get_db_connection()
# Get document
document = conn.execute('SELECT * FROM documents WHERE id = ?', (document_id,)).fetchone()
if not document:
conn.close()
flash('Document not found', 'error')
return redirect(url_for('dashboard'))
# Check ownership
if document['user_id'] != session['user_id'] and session['role'] != 'admin':
conn.close()
flash('You do not have permission to delete this document', 'error')
return redirect(url_for('dashboard'))
# Get all versions
versions = conn.execute('SELECT * FROM document_versions WHERE document_id = ?', (document_id,)).fetchall()
# Delete files
for version in versions:
file_path = os.path.join(app.config['UPLOAD_FOLDER'], version['filename'])
if os.path.exists(file_path):
os.remove(file_path)
# Delete from database (cascade will handle related records)
conn.execute('DELETE FROM analytics WHERE document_id = ?', (document_id,))
conn.execute('DELETE FROM document_shares WHERE document_id = ?', (document_id,))
conn.execute('DELETE FROM document_versions WHERE document_id = ?', (document_id,))
conn.execute('DELETE FROM documents WHERE id = ?', (document_id,))
conn.commit()
conn.close()
flash('Document deleted successfully', 'success')
return redirect(url_for('dashboard'))
@app.route('/admin')
@admin_required
def admin_panel():
conn = get_db_connection()
# Get all users
users = conn.execute('SELECT * FROM users').fetchall()
# Get document statistics
doc_stats = conn.execute('''
SELECT
COUNT(*) as total_documents,
SUM(file_size) as total_size,
COUNT(DISTINCT user_id) as total_contributors
FROM documents
''').fetchone()
# Get activity statistics
activity_stats = conn.execute('''
SELECT
COUNT(*) as total_actions,
COUNT(DISTINCT user_id) as active_users,
COUNT(DISTINCT document_id) as accessed_documents
FROM analytics
''').fetchone()
conn.close()
return render_template('admin.html', users=users, doc_stats=doc_stats, activity_stats=activity_stats)
@app.route('/shared/<share_token>')
def access_shared(share_token):
conn = get_db_connection()
# Get share information
share = conn.execute('''
SELECT document_shares.*, documents.*
FROM document_shares
JOIN documents ON document_shares.document_id = documents.id
WHERE document_shares.share_link = ?
''', (share_token,)).fetchone()
if not share:
conn.close()
flash('Invalid or expired share link', 'error')
return redirect(url_for('index'))
# Check if expired
if share['expires_at'] and datetime.datetime.strptime(share['expires_at'], '%Y-%m-%d %H:%M:%S.%f') < datetime.datetime.now():
conn.close()
flash('This share link has expired', 'error')
return redirect(url_for('index'))
# Record view in analytics
user_id = session.get('user_id')
conn.execute('''
INSERT INTO analytics (document_id, action, user_id, ip_address)
VALUES (?, ?, ?, ?)
''', (share['document_id'], 'shared_view', user_id, request.remote_addr))
conn.commit()
# Get document versions
versions = conn.execute('''
SELECT * FROM document_versions
WHERE document_id = ?
ORDER BY version_number DESC
''', (share['document_id'],)).fetchall()
conn.close()
return render_template('shared_view.html', document=share, versions=versions, share_token=share_token)
@app.route('/shared/<share_token>/download')
def download_shared(share_token):
conn = get_db_connection()
# Get share information
share = conn.execute('''
SELECT document_shares.*, documents.*
FROM document_shares
JOIN documents ON document_shares.document_id = documents.id
WHERE document_shares.share_link = ?
''', (share_token,)).fetchone()
if not share:
conn.close()
flash('Invalid or expired share link', 'error')
return redirect(url_for('index'))
# Check if expired
if share['expires_at'] and datetime.datetime.strptime(share['expires_at'], '%Y-%m-%d %H:%M:%S.%f') < datetime.datetime.now():
conn.close()
flash('This share link has expired', 'error')
return redirect(url_for('index'))
# Record download in analytics
user_id = session.get('user_id')
conn.execute('''
INSERT INTO analytics (document_id, action, user_id, ip_address)
VALUES (?, ?, ?, ?)
''', (share['document_id'], 'shared_download', user_id, request.remote_addr))
conn.commit()
conn.close()
return send_from_directory(app.config['UPLOAD_FOLDER'], share['filename'],
as_attachment=True, download_name=share['original_filename'])
# Error handlers
@app.errorhandler(404)
def page_not_found(e):
return render_template('error.html', error_code=404, error_message="Page not found"), 404
@app.errorhandler(500)
def internal_server_error(e):
return render_template('error.html', error_code=500, error_message="Internal server error"), 500
# Initialize database and start app
if __name__ == '__main__':
init_db()
app.run(host='0.0.0.0', port=1000, debug=False)
else:
# For production WSGI servers
init_db()

BIN
dms.db Normal file

Binary file not shown.

47
edit_document_route.py Normal file
View File

@ -0,0 +1,47 @@
@app.route('/document/<int:document_id>/edit', methods=['GET', 'POST'])
@login_required
def edit_document(document_id):
conn = get_db_connection()
# Get document
document = conn.execute('SELECT * FROM documents WHERE id = ?', (document_id,)).fetchone()
if not document:
conn.close()
flash('Document not found', 'error')
return redirect(url_for('files'))
# Check ownership
if document['user_id'] != session['user_id'] and session['role'] != 'admin':
conn.close()
flash('You do not have permission to edit this document', 'error')
return redirect(url_for('files'))
# Get available categories
categories = ['admin', 'accounting', 'hr', 'marketing', 'legal', 'general', 'other']
if request.method == 'POST':
custom_filename = request.form.get('custom_filename', '').strip()
category = request.form.get('category', 'general')
visibility = request.form.get('visibility', 'private')
# Use custom filename if provided, otherwise keep existing
if not custom_filename:
custom_filename = document['custom_filename']
# Update document
conn.execute('''
UPDATE documents
SET custom_filename = ?, category = ?, visibility = ?, updated_at = CURRENT_TIMESTAMP
WHERE id = ?
''', (custom_filename, category, visibility, document_id))
conn.commit()
conn.close()
flash('Document updated successfully', 'success')
return redirect(url_for('view_document', document_id=document_id))
conn.close()
return render_template('edit_document.html', document=document, categories=categories)

61
files_route.py Normal file
View File

@ -0,0 +1,61 @@
@app.route('/files')
@login_required
def files():
# Get query parameters
search_query = request.args.get('search', '')
selected_category = request.args.get('category', 'all')
page = request.args.get('page', 1, type=int)
per_page = 10 # Number of documents per page
conn = get_db_connection()
# Base query
query = '''
SELECT * FROM documents
WHERE (user_id = ? OR visibility = "public"
OR id IN (SELECT document_id FROM document_shares WHERE shared_with = ?))
'''
params = [session['user_id'], session['user_id']]
# Add search condition if search query provided
if search_query:
query += ' AND (custom_filename LIKE ? OR original_filename LIKE ?)'
params.extend(['%' + search_query + '%', '%' + search_query + '%'])
# Add category filter if specific category selected
if selected_category != 'all':
query += ' AND category = ?'
params.append(selected_category)
# Count total matching documents
count_query = query.replace('SELECT *', 'SELECT COUNT(*)')
total_docs = conn.execute(count_query, params).fetchone()[0]
# Add pagination
query += ' ORDER BY created_at DESC LIMIT ? OFFSET ?'
offset = (page - 1) * per_page
params.extend([per_page, offset])
# Execute final query
documents = conn.execute(query, params).fetchall()
# Get available categories for filter dropdown
categories = ['admin', 'accounting', 'hr', 'marketing', 'legal', 'general', 'other']
# Calculate pagination info
total_pages = (total_docs + per_page - 1) // per_page # Ceiling division
pagination = {
'page': page,
'per_page': per_page,
'total': total_docs,
'pages': total_pages
}
conn.close()
return render_template('files.html',
documents=documents,
categories=categories,
selected_category=selected_category,
search_query=search_query,
pagination=pagination)

7
requirements.txt Normal file
View File

@ -0,0 +1,7 @@
Flask==2.3.3
Werkzeug==2.3.7
Jinja2==3.1.2
MarkupSafe==2.1.3
itsdangerous==2.1.2
click==8.1.7
gunicorn==21.2.0

64
static/scripts.js Normal file
View File

@ -0,0 +1,64 @@
// Document Management System JavaScript
document.addEventListener('DOMContentLoaded', function() {
// Auto-dismiss flash messages after 5 seconds
window.setTimeout(function() {
document.querySelectorAll('.alert').forEach(function(alert) {
if (bootstrap && bootstrap.Alert) {
var bsAlert = new bootstrap.Alert(alert);
bsAlert.close();
} else {
alert.style.display = 'none';
}
});
}, 5000);
// Copy link functionality
window.copyLink = function(elementId) {
var copyText = document.getElementById(elementId);
if (copyText) {
copyText.select();
copyText.setSelectionRange(0, 99999);
navigator.clipboard.writeText(copyText.value);
// Show a temporary tooltip
var tooltip = document.createElement("div");
tooltip.innerHTML = "Copied!";
tooltip.style.position = "fixed";
tooltip.style.backgroundColor = "#4CAF50";
tooltip.style.color = "white";
tooltip.style.padding = "5px 10px";
tooltip.style.borderRadius = "5px";
tooltip.style.zIndex = "1000";
tooltip.style.top = (event.clientY - 30) + "px";
tooltip.style.left = event.clientX + "px";
document.body.appendChild(tooltip);
setTimeout(function() {
document.body.removeChild(tooltip);
}, 2000);
}
};
// Email sharing functionality
window.shareViaEmail = function(filename, url) {
var subject = "Shared Document: " + filename;
var body = "I'm sharing a document with you: " + filename + "\n\nYou can access it here: " + url;
window.location.href = "mailto:?subject=" + encodeURIComponent(subject) + "&body=" + encodeURIComponent(body);
};
// WhatsApp sharing functionality
window.shareViaWhatsApp = function(filename, url) {
var text = "I'm sharing a document with you: " + filename + "\n\nYou can access it here: " + url;
window.open("https://wa.me/?text=" + encodeURIComponent(text), "_blank");
};
// Confirm delete
var deleteButtons = document.querySelectorAll('[data-confirm]');
deleteButtons.forEach(function(button) {
button.addEventListener('click', function(e) {
if (!confirm(this.getAttribute('data-confirm'))) {
e.preventDefault();
}
});
});
});

70
static/styles.css Normal file
View File

@ -0,0 +1,70 @@
/* Main Styles */
body {
padding-top: 60px;
background-color: #f8f9fa;
}
.card {
margin-bottom: 20px;
box-shadow: 0 4px 6px rgba(0,0,0,.1);
}
.document-card {
transition: transform 0.2s;
}
.document-card:hover {
transform: translateY(-5px);
}
.file-icon {
font-size: 2.5rem;
margin-bottom: 10px;
}
.pdf-icon { color: #e74c3c; }
.document-icon { color: #3498db; }
.spreadsheet-icon { color: #2ecc71; }
.image-icon { color: #9b59b6; }
.video-icon { color: #e67e22; }
.other-icon { color: #95a5a6; }
.flash-messages {
position: fixed;
top: 70px;
right: 20px;
z-index: 1000;
max-width: 300px;
}
/* Dashboard Styles */
.analytics-card {
background-color: #f8f9fa;
border-left: 4px solid #007bff;
}
/* Document Viewer Styles */
.document-viewer {
min-height: 400px;
background-color: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 0.25rem;
padding: 1rem;
}
/* Responsive Adjustments */
@media (max-width: 768px) {
.card-body {
padding: 1rem;
}
.btn-group {
display: flex;
flex-direction: column;
}
.btn-group .btn {
margin-bottom: 0.25rem;
border-radius: 0.25rem !important;
}
}

123
templates/admin.html Normal file
View File

@ -0,0 +1,123 @@
{% extends "layout.html" %}
{% block content %}
<h2 class="mb-4"><i class="bi bi-shield-lock"></i> Admin Panel</h2>
<div class="row">
<div class="col-md-4">
<div class="card mb-4">
<div class="card-header bg-primary text-white">
<h5 class="mb-0"><i class="bi bi-file-earmark-text"></i> Document Statistics</h5>
</div>
<div class="card-body">
<ul class="list-group list-group-flush">
<li class="list-group-item d-flex justify-content-between align-items-center">
Total Documents
<span class="badge bg-primary rounded-pill">{{ doc_stats.total_documents }}</span>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center">
Total Storage Used
<span class="badge bg-info rounded-pill">{{ (doc_stats.total_size / (1024*1024))|round(2) }} MB</span>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center">
Active Contributors
<span class="badge bg-success rounded-pill">{{ doc_stats.total_contributors }}</span>
</li>
</ul>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card mb-4">
<div class="card-header bg-success text-white">
<h5 class="mb-0"><i class="bi bi-activity"></i> Activity Statistics</h5>
</div>
<div class="card-body">
<ul class="list-group list-group-flush">
<li class="list-group-item d-flex justify-content-between align-items-center">
Total Actions
<span class="badge bg-primary rounded-pill">{{ activity_stats.total_actions }}</span>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center">
Active Users
<span class="badge bg-info rounded-pill">{{ activity_stats.active_users }}</span>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center">
Accessed Documents
<span class="badge bg-success rounded-pill">{{ activity_stats.accessed_documents }}</span>
</li>
</ul>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card mb-4">
<div class="card-header bg-warning text-dark">
<h5 class="mb-0"><i class="bi bi-gear"></i> System Actions</h5>
</div>
<div class="card-body">
<div class="d-grid gap-2">
<button class="btn btn-outline-primary">
<i class="bi bi-arrow-repeat"></i> Rebuild Search Index
</button>
<button class="btn btn-outline-warning">
<i class="bi bi-trash"></i> Clear Expired Shares
</button>
<button class="btn btn-outline-danger">
<i class="bi bi-exclamation-triangle"></i> System Maintenance
</button>
</div>
</div>
</div>
</div>
</div>
<div class="card">
<div class="card-header bg-info text-white">
<h5 class="mb-0"><i class="bi bi-people"></i> User Management</h5>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>ID</th>
<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.id }}</td>
<td>{{ user.username }}</td>
<td>{{ user.email }}</td>
<td>
<span class="badge bg-{{ 'danger' if user.role == 'admin' else 'primary' }}">
{{ user.role }}
</span>
</td>
<td>{{ user.created_at.split(' ')[0] }}</td>
<td>
<div class="btn-group btn-group-sm">
<button class="btn btn-outline-primary">
<i class="bi bi-pencil"></i> Edit
</button>
<button class="btn btn-outline-danger" {% if user.role == 'admin' %}disabled{% endif %}>
<i class="bi bi-trash"></i> Delete
</button>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endblock %}

194
templates/dashboard.html Normal file
View File

@ -0,0 +1,194 @@
{% extends "layout.html" %}
{% block content %}
<h2 class="mb-4"><i class="bi bi-speedometer2"></i> Dashboard</h2>
<div class="row">
<!-- Category Summary -->
<div class="col-md-6 mb-4">
<div class="card h-100">
<div class="card-header bg-primary text-white">
<h5 class="mb-0"><i class="bi bi-folder"></i> Document Categories</h5>
</div>
<div class="card-body">
{% if category_counts %}
<div class="row">
{% for category in category_counts %}
<div class="col-md-6 mb-3">
<div class="card border-0 shadow-sm">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div>
<h6 class="mb-0">{{ category.category|capitalize }}</h6>
<small class="text-muted">{{ category.count }} document{% if category.count != 1 %}s{% endif %}</small>
</div>
<a href="{{ url_for('files', category=category.category) }}" class="btn btn-sm btn-outline-primary">
<i class="bi bi-arrow-right"></i>
</a>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="alert alert-info">
<i class="bi bi-info-circle"></i> No documents found. Upload your first document to get started!
</div>
{% endif %}
<div class="text-center mt-3">
<a href="{{ url_for('files') }}" class="btn btn-primary">
<i class="bi bi-table"></i> View All Documents
</a>
</div>
</div>
</div>
</div>
<!-- Quick Actions -->
<div class="col-md-6 mb-4">
<div class="card h-100">
<div class="card-header bg-success text-white">
<h5 class="mb-0"><i class="bi bi-lightning"></i> Quick Actions</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-6 mb-3">
<a href="{{ url_for('upload') }}" class="btn btn-outline-success w-100 h-100 d-flex flex-column justify-content-center align-items-center p-4">
<i class="bi bi-upload fs-2 mb-2"></i>
<span>Upload Document</span>
</a>
</div>
<div class="col-6 mb-3">
<a href="{{ url_for('files') }}" class="btn btn-outline-primary w-100 h-100 d-flex flex-column justify-content-center align-items-center p-4">
<i class="bi bi-table fs-2 mb-2"></i>
<span>View Files</span>
</a>
</div>
<div class="col-6 mb-3">
<a href="{{ url_for('files', category='admin') }}" class="btn btn-outline-secondary w-100 h-100 d-flex flex-column justify-content-center align-items-center p-4">
<i class="bi bi-briefcase fs-2 mb-2"></i>
<span>Admin Files</span>
</a>
</div>
<div class="col-6 mb-3">
<a href="{{ url_for('files', category='accounting') }}" class="btn btn-outline-info w-100 h-100 d-flex flex-column justify-content-center align-items-center p-4">
<i class="bi bi-calculator fs-2 mb-2"></i>
<span>Accounting Files</span>
</a>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<!-- Recent Documents -->
<div class="col-md-6 mb-4">
<div class="card h-100">
<div class="card-header bg-info text-white">
<h5 class="mb-0"><i class="bi bi-clock-history"></i> Recent Documents</h5>
</div>
<div class="card-body p-0">
{% if recent_documents %}
<div class="list-group list-group-flush">
{% for doc in recent_documents %}
<a href="{{ url_for('view_document', document_id=doc.id) }}" class="list-group-item list-group-item-action">
<div class="d-flex w-100 justify-content-between align-items-center">
<div>
<div class="d-flex align-items-center">
{% if doc.file_type == 'pdf' %}
<i class="bi bi-file-earmark-pdf text-danger me-2"></i>
{% elif doc.file_type == 'document' %}
<i class="bi bi-file-earmark-word text-primary me-2"></i>
{% elif doc.file_type == 'spreadsheet' %}
<i class="bi bi-file-earmark-excel text-success me-2"></i>
{% elif doc.file_type == 'image' %}
<i class="bi bi-file-earmark-image text-info me-2"></i>
{% elif doc.file_type == 'video' %}
<i class="bi bi-file-earmark-play text-warning me-2"></i>
{% else %}
<i class="bi bi-file-earmark text-secondary me-2"></i>
{% endif %}
<h6 class="mb-0">{{ doc.custom_filename }}</h6>
</div>
<small class="text-muted">
<span class="badge bg-secondary">{{ doc.category|capitalize }}</span>
{{ (doc.file_size / 1024)|round(1) }} KB
</small>
</div>
<small class="text-muted">{{ doc.created_at.split(' ')[0] }}</small>
</div>
</a>
{% endfor %}
</div>
{% else %}
<div class="alert alert-info m-3">
<i class="bi bi-info-circle"></i> No documents found. Upload your first document to get started!
</div>
{% endif %}
</div>
{% if recent_documents %}
<div class="card-footer text-center">
<a href="{{ url_for('files') }}" class="btn btn-sm btn-outline-info">View All Documents</a>
</div>
{% endif %}
</div>
</div>
<!-- Analytics (Admin Only) -->
{% if session.role == 'admin' and analytics %}
<div class="col-md-6 mb-4">
<div class="card h-100">
<div class="card-header bg-warning text-dark">
<h5 class="mb-0"><i class="bi bi-graph-up"></i> Analytics Overview</h5>
</div>
<div class="card-body">
<h6>Most Viewed Documents</h6>
<div class="table-responsive">
<table class="table table-sm table-striped">
<thead>
<tr>
<th>Document</th>
<th>Views</th>
</tr>
</thead>
<tbody>
{% for doc in analytics %}
<tr>
<td>{{ doc.custom_filename }}</td>
<td>{{ doc.view_count }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% if user_activity %}
<h6 class="mt-4">Most Active Users</h6>
<div class="table-responsive">
<table class="table table-sm table-striped">
<thead>
<tr>
<th>User</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for user in user_activity %}
<tr>
<td>{{ user.username }}</td>
<td>{{ user.action_count }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
</div>
</div>
</div>
{% endif %}
</div>
{% endblock %}

View File

@ -0,0 +1,77 @@
{% extends "layout.html" %}
{% block content %}
<h2 class="mb-4"><i class="bi bi-pencil-square"></i> Edit Document</h2>
<div class="row">
<div class="col-md-8">
<div class="card">
<div class="card-header bg-primary text-white">
<h5 class="mb-0">Edit Document Details</h5>
</div>
<div class="card-body">
<form method="post" action="{{ url_for('edit_document', document_id=document.id) }}">
<div class="mb-3">
<label for="custom_filename" class="form-label">Document Name</label>
<input type="text" class="form-control" id="custom_filename" name="custom_filename"
value="{{ document.custom_filename }}" required>
</div>
<div class="mb-3">
<label for="category" class="form-label">Category</label>
<select class="form-select" id="category" name="category" required>
{% for category in categories %}
<option value="{{ category }}" {% if category == document.category %}selected{% endif %}>
{{ category|capitalize }}
</option>
{% endfor %}
</select>
</div>
<div class="mb-3">
<label class="form-label">Visibility</label>
<div class="form-check">
<input class="form-check-input" type="radio" name="visibility" id="private" value="private"
{% if document.visibility == 'private' %}checked{% endif %}>
<label class="form-check-label" for="private">
<i class="bi bi-lock"></i> Private (Only you can access)
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="visibility" id="public" value="public"
{% if document.visibility == 'public' %}checked{% endif %}>
<label class="form-check-label" for="public">
<i class="bi bi-globe"></i> Public (Anyone can access)
</label>
</div>
</div>
<div class="d-flex justify-content-between">
<a href="{{ url_for('view_document', document_id=document.id) }}" class="btn btn-secondary">
<i class="bi bi-x-circle"></i> Cancel
</a>
<button type="submit" class="btn btn-primary">
<i class="bi bi-save"></i> Save Changes
</button>
</div>
</form>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-header bg-info text-white">
<h5 class="mb-0"><i class="bi bi-info-circle"></i> Document Information</h5>
</div>
<div class="card-body">
<p><strong>Original Filename:</strong> {{ document.original_filename }}</p>
<p><strong>File Type:</strong> {{ document.file_type|capitalize }}</p>
<p><strong>File Size:</strong> {{ (document.file_size / 1024)|round(1) }} KB</p>
<p><strong>Uploaded:</strong> {{ document.created_at.split(' ')[0] }}</p>
<p><strong>Last Modified:</strong> {{ document.updated_at.split(' ')[0] }}</p>
</div>
</div>
</div>
</div>
{% endblock %}

49
templates/error.html Normal file
View File

@ -0,0 +1,49 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document Management System - Error</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.8.0/font/bootstrap-icons.css">
<link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
</head>
<body>
<!-- Navigation -->
<nav class="navbar navbar-expand-lg navbar-dark bg-dark fixed-top">
<div class="container">
<a class="navbar-brand" href="{{ url_for('index') }}">
<i class="bi bi-folder2-open"></i> DMS
</a>
</div>
</nav>
<!-- Main Content -->
<div class="container mt-5 pt-5">
<div class="row justify-content-center">
<div class="col-md-8 text-center">
<div class="card">
<div class="card-body">
<h1 class="display-1 text-danger">{{ error_code }}</h1>
<h2 class="mb-4">{{ error_message }}</h2>
<p class="lead">Sorry, something went wrong.</p>
<a href="{{ url_for('index') }}" class="btn btn-primary mt-3">
<i class="bi bi-house"></i> Return to Home
</a>
</div>
</div>
</div>
</div>
</div>
<!-- Footer -->
<footer class="bg-dark text-white py-4 mt-5">
<div class="container text-center">
<p>Document Management System</p>
</div>
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js"></script>
<script src="{{ url_for('static', filename='scripts.js') }}"></script>
</body>
</html>

145
templates/files.html Normal file
View File

@ -0,0 +1,145 @@
{% extends "layout.html" %}
{% block content %}
<div class="row mb-3">
<div class="col-md-6">
<h2><i class="bi bi-table"></i> Files</h2>
</div>
<div class="col-md-6 text-end">
<a href="{{ url_for('upload') }}" class="btn btn-success">
<i class="bi bi-upload"></i> Upload New Document
</a>
</div>
</div>
<div class="card mb-4">
<div class="card-header bg-primary text-white d-flex justify-content-between align-items-center">
<h5 class="mb-0"><i class="bi bi-file-earmark"></i> Document Library</h5>
<form class="d-flex" action="{{ url_for('files') }}" method="get">
<select name="category" class="form-select form-select-sm me-2" style="width: 150px;" onchange="this.form.submit()">
<option value="all" {% if selected_category == 'all' %}selected{% endif %}>All Categories</option>
{% for category in categories %}
<option value="{{ category }}" {% if selected_category == category %}selected{% endif %}>
{{ category|capitalize }}
</option>
{% endfor %}
</select>
<div class="input-group">
<input type="text" class="form-control form-control-sm" placeholder="Search files..." name="search" value="{{ search_query }}">
<button class="btn btn-outline-light btn-sm" type="submit">
<i class="bi bi-search"></i>
</button>
</div>
</form>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-striped table-hover mb-0">
<thead class="table-light">
<tr>
<th>Name</th>
<th>Category</th>
<th>Type</th>
<th>Size</th>
<th>Uploaded</th>
<th>Visibility</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% if documents %}
{% for doc in documents %}
<tr>
<td>
<div class="d-flex align-items-center">
{% if doc.file_type == 'pdf' %}
<i class="bi bi-file-earmark-pdf text-danger me-2"></i>
{% elif doc.file_type == 'document' %}
<i class="bi bi-file-earmark-word text-primary me-2"></i>
{% elif doc.file_type == 'spreadsheet' %}
<i class="bi bi-file-earmark-excel text-success me-2"></i>
{% elif doc.file_type == 'image' %}
<i class="bi bi-file-earmark-image text-info me-2"></i>
{% elif doc.file_type == 'video' %}
<i class="bi bi-file-earmark-play text-warning me-2"></i>
{% else %}
<i class="bi bi-file-earmark text-secondary me-2"></i>
{% endif %}
<span>{{ doc.custom_filename }}</span>
</div>
</td>
<td><span class="badge bg-secondary">{{ doc.category|capitalize }}</span></td>
<td>{{ doc.file_type|capitalize }}</td>
<td>{{ (doc.file_size / 1024)|round(1) }} KB</td>
<td>{{ doc.created_at.split(' ')[0] }}</td>
<td>
{% if doc.visibility == 'public' %}
<span class="badge bg-success">Public</span>
{% else %}
<span class="badge bg-warning text-dark">Private</span>
{% endif %}
</td>
<td>
<div class="btn-group btn-group-sm">
<a href="{{ url_for('view_document', document_id=doc.id) }}" class="btn btn-outline-primary" title="View">
<i class="bi bi-eye"></i>
</a>
<a href="{{ url_for('edit_document', document_id=doc.id) }}" class="btn btn-outline-secondary" title="Edit">
<i class="bi bi-pencil"></i>
</a>
<a href="{{ url_for('share_document', document_id=doc.id) }}" class="btn btn-outline-info" title="Share">
<i class="bi bi-share"></i>
</a>
<a href="{{ url_for('download_document', document_id=doc.id) }}" class="btn btn-outline-success" title="Download">
<i class="bi bi-download"></i>
</a>
<a href="{{ url_for('delete_document', document_id=doc.id) }}" class="btn btn-outline-danger"
onclick="return confirm('Are you sure you want to delete this document?')" title="Delete">
<i class="bi bi-trash"></i>
</a>
</div>
</td>
</tr>
{% endfor %}
{% else %}
<tr>
<td colspan="7" class="text-center py-4">
<div class="alert alert-info mb-0">
<i class="bi bi-info-circle"></i>
{% if search_query %}
No documents found matching your search criteria.
{% elif selected_category != 'all' %}
No documents found in the selected category.
{% else %}
No documents found. Upload your first document to get started!
{% endif %}
</div>
</td>
</tr>
{% endif %}
</tbody>
</table>
</div>
</div>
</div>
{% if pagination and pagination.pages > 1 %}
<nav aria-label="Document pagination">
<ul class="pagination justify-content-center">
<li class="page-item {% if pagination.page == 1 %}disabled{% endif %}">
<a class="page-link" href="{{ url_for('files', page=pagination.page-1, search=search_query, category=selected_category) }}">Previous</a>
</li>
{% for p in range(1, pagination.pages + 1) %}
<li class="page-item {% if pagination.page == p %}active{% endif %}">
<a class="page-link" href="{{ url_for('files', page=p, search=search_query, category=selected_category) }}">{{ p }}</a>
</li>
{% endfor %}
<li class="page-item {% if pagination.page == pagination.pages %}disabled{% endif %}">
<a class="page-link" href="{{ url_for('files', page=pagination.page+1, search=search_query, category=selected_category) }}">Next</a>
</li>
</ul>
</nav>
{% endif %}
{% endblock %}

17
templates/index.html Normal file
View File

@ -0,0 +1,17 @@
{% extends "layout.html" %}
{% block content %}
<div class="row justify-content-center">
<div class="col-md-8 text-center">
<div class="card mt-5">
<div class="card-body">
<h1 class="display-4 mb-4">Document Management System</h1>
<p class="lead">A secure platform for managing, sharing, and collaborating on documents.</p>
<hr class="my-4">
<p>Please login to access your documents and start managing your files.</p>
<a href="{{ url_for('login') }}" class="btn btn-primary btn-lg">Login</a>
</div>
</div>
</div>
</div>
{% endblock %}

92
templates/layout.html Normal file
View File

@ -0,0 +1,92 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document Management System</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.8.0/font/bootstrap-icons.css">
<link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
</head>
<body>
<!-- Navigation -->
<nav class="navbar navbar-expand-lg navbar-dark bg-dark fixed-top">
<div class="container">
<a class="navbar-brand" href="{{ url_for('index') }}">
<i class="bi bi-folder2-open"></i> DMS
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto">
{% if session.user_id %}
<li class="nav-item">
<a class="nav-link" href="{{ url_for('dashboard') }}">Dashboard</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('files') }}">Files</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('upload') }}">Upload</a>
</li>
{% if session.role == 'admin' %}
<li class="nav-item">
<a class="nav-link" href="{{ url_for('admin_panel') }}">Admin</a>
</li>
{% endif %}
{% endif %}
</ul>
<ul class="navbar-nav">
{% if session.user_id %}
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="userDropdown" role="button" data-bs-toggle="dropdown">
<i class="bi bi-person-circle"></i> {{ session.username }}
</a>
<ul class="dropdown-menu dropdown-menu-end">
<li><a class="dropdown-item" href="{{ url_for('logout') }}">Logout</a></li>
</ul>
</li>
{% else %}
<li class="nav-item">
<a class="nav-link" href="{{ url_for('login') }}">Login</a>
</li>
{% endif %}
</ul>
</div>
</div>
</nav>
<!-- Flash Messages -->
<div class="flash-messages">
{% for category, message in get_flashed_messages(with_categories=true) %}
{% set alert_class = 'alert-info' %}
{% if category == 'success' %}
{% set alert_class = 'alert-success' %}
{% elif category == 'error' %}
{% set alert_class = 'alert-danger' %}
{% endif %}
<div class="alert {{ alert_class }} alert-dismissible fade show">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
</div>
<!-- Main Content -->
<div class="container mt-4">
{% block content %}{% endblock %}
</div>
<!-- Footer -->
<footer class="bg-dark text-white py-4 mt-5">
<div class="container text-center">
<p>Document Management System</p>
</div>
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js"></script>
<script src="{{ url_for('static', filename='scripts.js') }}"></script>
{% block scripts %}{% endblock %}
</body>
</html>

33
templates/login.html Normal file
View File

@ -0,0 +1,33 @@
{% extends "layout.html" %}
{% block content %}
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card mt-5">
<div class="card-header bg-primary text-white">
<h4 class="mb-0"><i class="bi bi-person"></i> Login</h4>
</div>
<div class="card-body">
<form method="post" action="{{ url_for('login') }}">
<div class="mb-3">
<label for="username" class="form-label">Username</label>
<input type="text" class="form-control" id="username" name="username" required>
</div>
<div class="mb-3">
<label for="password" class="form-label">Password</label>
<input type="password" class="form-control" id="password" name="password" required>
</div>
<div class="d-grid">
<button type="submit" class="btn btn-primary">Login</button>
</div>
</form>
<div class="mt-3 text-center">
<!-- <p class="text-muted">Demo Accounts:</p>
<p><small>Admin: username: <strong>admin</strong>, password: <strong>admin123</strong></small></p>
<p><small>User: username: <strong>user</strong>, password: <strong>user123</strong></small></p> -->
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,163 @@
{% extends "layout.html" %}
{% block content %}
<div class="row mb-3">
<div class="col-md-8">
<h2><i class="bi bi-share"></i> Share Document: {{ document.original_filename }}</h2>
</div>
<div class="col-md-4 text-end">
<a href="{{ url_for('view_document', document_id=document.id) }}" class="btn btn-secondary">
<i class="bi bi-arrow-left"></i> Back to Document
</a>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="card mb-4">
<div class="card-header bg-primary text-white">
<h5 class="mb-0"><i class="bi bi-person-plus"></i> Share with User</h5>
</div>
<div class="card-body">
<form method="post" action="{{ url_for('share_document', document_id=document.id) }}">
<input type="hidden" name="share_type" value="user">
<div class="mb-3">
<label for="username" class="form-label">Username</label>
<select class="form-select" id="username" name="username" required>
<option value="" selected disabled>Select a user</option>
{% for user in users %}
<option value="{{ user.username }}">{{ user.username }}</option>
{% endfor %}
</select>
</div>
<div class="d-grid">
<button type="submit" class="btn btn-primary">
<i class="bi bi-share"></i> Share with User
</button>
</div>
</form>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card mb-4">
<div class="card-header bg-info text-white">
<h5 class="mb-0"><i class="bi bi-link"></i> Create Shareable Link</h5>
</div>
<div class="card-body">
<form method="post" action="{{ url_for('share_document', document_id=document.id) }}">
<input type="hidden" name="share_type" value="link">
<div class="mb-3">
<label for="expiry" class="form-label">Link Expiry</label>
<select class="form-select" id="expiry" name="expiry">
<option value="1">1 day</option>
<option value="7" selected>7 days</option>
<option value="30">30 days</option>
<option value="90">90 days</option>
</select>
</div>
<div class="d-grid">
<button type="submit" class="btn btn-info">
<i class="bi bi-link"></i> Generate Link
</button>
</div>
</form>
</div>
</div>
</div>
</div>
<div class="card">
<div class="card-header bg-success text-white">
<h5 class="mb-0"><i class="bi bi-people"></i> Current Shares</h5>
</div>
<div class="card-body">
{% if shares %}
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>Type</th>
<th>Shared With</th>
<th>Date Shared</th>
<th>Expiry</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for share in shares %}
<tr>
<td>
{% if share.shared_with %}
<span class="badge bg-primary"><i class="bi bi-person"></i> User</span>
{% elif share.share_link %}
<span class="badge bg-info"><i class="bi bi-link"></i> Link</span>
{% endif %}
</td>
<td>
{% if share.shared_with %}
{{ share.username }}
{% elif share.share_link %}
<div class="input-group input-group-sm">
<input type="text" class="form-control form-control-sm" value="{{ request.host_url }}shared/{{ share.share_link }}" readonly id="link-{{ share.id }}">
<button class="btn btn-outline-secondary" type="button" onclick="copyLink('link-{{ share.id }}')">
<i class="bi bi-clipboard"></i>
</button>
</div>
{% endif %}
</td>
<td>{{ share.created_at.split(' ')[0] }}</td>
<td>
{% if share.expires_at %}
{{ share.expires_at.split(' ')[0] }}
{% else %}
Never
{% endif %}
</td>
<td>
<a href="#" class="btn btn-sm btn-danger">
<i class="bi bi-x-circle"></i> Revoke
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="alert alert-info">
<i class="bi bi-info-circle"></i> This document hasn't been shared yet.
</div>
{% endif %}
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
function copyLink(elementId) {
var copyText = document.getElementById(elementId);
copyText.select();
copyText.setSelectionRange(0, 99999);
navigator.clipboard.writeText(copyText.value);
// Show a temporary tooltip
var tooltip = document.createElement("div");
tooltip.innerHTML = "Copied!";
tooltip.style.position = "fixed";
tooltip.style.backgroundColor = "#4CAF50";
tooltip.style.color = "white";
tooltip.style.padding = "5px 10px";
tooltip.style.borderRadius = "5px";
tooltip.style.zIndex = "1000";
tooltip.style.top = (event.clientY - 30) + "px";
tooltip.style.left = event.clientX + "px";
document.body.appendChild(tooltip);
setTimeout(function() {
document.body.removeChild(tooltip);
}, 2000);
}
</script>
{% endblock %}

140
templates/shared_view.html Normal file
View File

@ -0,0 +1,140 @@
{% extends "layout.html" %}
{% block content %}
<div class="row mb-3">
<div class="col-md-12">
<div class="alert alert-info">
<i class="bi bi-info-circle"></i> You are viewing a shared document.
{% if not session.user_id %}
<a href="{{ url_for('login') }}" class="alert-link">Login</a> to access more features.
{% endif %}
</div>
</div>
</div>
<div class="row">
<div class="col-md-8">
<div class="card mb-4">
<div class="card-header bg-primary text-white">
<h5 class="mb-0"><i class="bi bi-file-earmark"></i> {{ document.original_filename }}</h5>
</div>
<div class="card-body">
<div class="document-viewer">
{% if document.file_type == 'pdf' %}
<div class="ratio ratio-16x9">
<iframe src="{{ url_for('download_shared', share_token=share_token) }}" allowfullscreen></iframe>
</div>
{% elif document.file_type == 'image' %}
<img src="{{ url_for('download_shared', share_token=share_token) }}" class="img-fluid" alt="{{ document.original_filename }}">
{% elif document.file_type == 'video' %}
<div class="ratio ratio-16x9">
<video controls>
<source src="{{ url_for('download_shared', share_token=share_token) }}" type="video/mp4">
Your browser does not support the video tag.
</video>
</div>
{% else %}
<div class="alert alert-info">
<i class="bi bi-info-circle"></i> Preview not available for this file type. Please download the file to view its contents.
</div>
{% endif %}
</div>
<div class="mt-3">
<a href="{{ url_for('download_shared', share_token=share_token) }}" class="btn btn-primary">
<i class="bi bi-download"></i> Download
</a>
</div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card mb-4">
<div class="card-header bg-secondary text-white">
<h5 class="mb-0"><i class="bi bi-info-circle"></i> Document Information</h5>
</div>
<div class="card-body">
<ul class="list-group list-group-flush">
<li class="list-group-item d-flex justify-content-between align-items-center">
File Type
<span class="badge bg-primary rounded-pill">{{ document.file_type }}</span>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center">
Size
<span class="badge bg-secondary rounded-pill">{{ (document.file_size / 1024)|round(1) }} KB</span>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center">
Uploaded
<span class="badge bg-info rounded-pill">{{ document.created_at.split(' ')[0] }}</span>
</li>
</ul>
</div>
</div>
<div class="card">
<div class="card-header bg-success text-white">
<h5 class="mb-0"><i class="bi bi-share"></i> Sharing Options</h5>
</div>
<div class="card-body">
<p>This document has been shared with you via a secure link.</p>
<div class="input-group mb-3">
<input type="text" class="form-control" value="{{ request.url }}" readonly id="share-link">
<button class="btn btn-outline-secondary" type="button" onclick="copyLink('share-link')">
<i class="bi bi-clipboard"></i> Copy
</button>
</div>
<div class="d-grid gap-2">
<button class="btn btn-outline-primary" onclick="shareViaEmail()">
<i class="bi bi-envelope"></i> Share via Email
</button>
<button class="btn btn-outline-success" onclick="shareViaWhatsApp()">
<i class="bi bi-whatsapp"></i> Share via WhatsApp
</button>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
function copyLink(elementId) {
var copyText = document.getElementById(elementId);
copyText.select();
copyText.setSelectionRange(0, 99999);
navigator.clipboard.writeText(copyText.value);
// Show a temporary tooltip
var tooltip = document.createElement("div");
tooltip.innerHTML = "Copied!";
tooltip.style.position = "fixed";
tooltip.style.backgroundColor = "#4CAF50";
tooltip.style.color = "white";
tooltip.style.padding = "5px 10px";
tooltip.style.borderRadius = "5px";
tooltip.style.zIndex = "1000";
tooltip.style.top = (event.clientY - 30) + "px";
tooltip.style.left = event.clientX + "px";
document.body.appendChild(tooltip);
setTimeout(function() {
document.body.removeChild(tooltip);
}, 2000);
}
function shareViaEmail() {
var subject = "Shared Document: {{ document.original_filename }}";
var body = "I'm sharing a document with you: {{ document.original_filename }}\\n\\nYou can access it here: " + window.location.href;
window.location.href = "mailto:?subject=" + encodeURIComponent(subject) + "&body=" + encodeURIComponent(body);
}
function shareViaWhatsApp() {
var text = "I'm sharing a document with you: {{ document.original_filename }}\\n\\nYou can access it here: " + window.location.href;
window.open("https://wa.me/?text=" + encodeURIComponent(text), "_blank");
}
</script>
{% endblock %}

98
templates/upload.html Normal file
View File

@ -0,0 +1,98 @@
{% extends "layout.html" %}
{% block content %}
<h2 class="mb-4"><i class="bi bi-upload"></i> Upload Document</h2>
<div class="row">
<div class="col-md-8">
<div class="card">
<div class="card-header bg-success text-white">
<h5 class="mb-0">Upload New Document</h5>
</div>
<div class="card-body">
<form method="post" action="{{ url_for('upload') }}" enctype="multipart/form-data">
<div class="mb-3">
<label for="file" class="form-label">Select File</label>
<input type="file" class="form-control" id="file" name="file" required>
<div class="form-text">
Allowed file types: PDF, DOCX, XLSX, JPG, PNG, MP4, TXT
</div>
</div>
<div class="mb-3">
<label for="custom_filename" class="form-label">Custom File Name (Optional)</label>
<input type="text" class="form-control" id="custom_filename" name="custom_filename" placeholder="Enter a custom name for this file">
<div class="form-text">
If left blank, the original filename will be used
</div>
</div>
<div class="mb-3">
<label for="category" class="form-label">Category</label>
<select class="form-select" id="category" name="category" required>
{% for category in categories %}
<option value="{{ category }}" {% if category == 'general' %}selected{% endif %}>
{{ category|capitalize }}
</option>
{% endfor %}
</select>
</div>
<div class="mb-3">
<label class="form-label">Visibility</label>
<div class="form-check">
<input class="form-check-input" type="radio" name="visibility" id="private" value="private" checked>
<label class="form-check-label" for="private">
<i class="bi bi-lock"></i> Private (Only you can access)
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="visibility" id="public" value="public">
<label class="form-check-label" for="public">
<i class="bi bi-globe"></i> Public (Anyone can access)
</label>
</div>
</div>
<div class="d-grid">
<button type="submit" class="btn btn-success">
<i class="bi bi-upload"></i> Upload Document
</button>
</div>
</form>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-header bg-info text-white">
<h5 class="mb-0"><i class="bi bi-info-circle"></i> Information</h5>
</div>
<div class="card-body">
<h6>File Size Limits</h6>
<p>Maximum file size: 16MB</p>
<h6>Supported File Types</h6>
<ul>
<li>Documents: PDF, DOCX, TXT</li>
<li>Spreadsheets: XLSX</li>
<li>Images: JPG, PNG</li>
<li>Videos: MP4</li>
</ul>
<h6>Categories</h6>
<ul>
<li><strong>Admin:</strong> Administrative documents</li>
<li><strong>Accounting:</strong> Financial documents</li>
<li><strong>HR:</strong> Human resources documents</li>
<li><strong>Marketing:</strong> Marketing materials</li>
<li><strong>Legal:</strong> Legal documents</li>
<li><strong>General:</strong> General purpose documents</li>
<li><strong>Other:</strong> Miscellaneous documents</li>
</ul>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,160 @@
{% extends "layout.html" %}
{% block content %}
<div class="row mb-3">
<div class="col-md-8">
<h2><i class="bi bi-file-earmark"></i> {{ document.original_filename }}</h2>
</div>
<div class="col-md-4 text-end">
<a href="{{ url_for('dashboard') }}" class="btn btn-secondary">
<i class="bi bi-arrow-left"></i> Back to Dashboard
</a>
</div>
</div>
<div class="row">
<div class="col-md-8">
<div class="card mb-4">
<div class="card-header bg-primary text-white">
<h5 class="mb-0"><i class="bi bi-eye"></i> Document Viewer</h5>
</div>
<div class="card-body">
<div class="document-viewer">
{% if document.file_type == 'pdf' %}
<div class="ratio ratio-16x9">
<iframe src="{{ url_for('serve_document_file', document_id=document.id) }}" allowfullscreen></iframe>
</div>
{% elif document.file_type == 'image' %}
<img src="{{ url_for('download_document', document_id=document.id) }}" class="img-fluid" alt="{{ document.original_filename }}">
{% elif document.file_type == 'video' %}
<div class="ratio ratio-16x9">
<video controls>
<source src="{{ url_for('download_document', document_id=document.id) }}" type="video/mp4">
Your browser does not support the video tag.
</video>
</div>
{% else %}
<div class="alert alert-info">
<i class="bi bi-info-circle"></i> Preview not available for this file type. Please download the file to view its contents.
</div>
{% endif %}
</div>
<div class="mt-3">
<a href="{{ url_for('download_document', document_id=document.id) }}" class="btn btn-primary">
<i class="bi bi-download"></i> Download
</a>
<a href="{{ url_for('share_document', document_id=document.id) }}" class="btn btn-info">
<i class="bi bi-share"></i> Share
</a>
{% if document.user_id == session.user_id or session.role == 'admin' %}
<a href="{{ url_for('delete_document', document_id=document.id) }}" class="btn btn-danger"
onclick="return confirm('Are you sure you want to delete this document?')">
<i class="bi bi-trash"></i> Delete
</a>
{% endif %}
</div>
</div>
</div>
<div class="card mb-4">
<div class="card-header bg-info text-white">
<h5 class="mb-0"><i class="bi bi-clock-history"></i> Version History</h5>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>Version</th>
<th>Date</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for version in versions %}
<tr>
<td>v{{ version.version_number }}</td>
<td>{{ version.created_at }}</td>
<td>
<a href="#" class="btn btn-sm btn-primary">
<i class="bi bi-download"></i> Download
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card mb-4">
<div class="card-header bg-secondary text-white">
<h5 class="mb-0"><i class="bi bi-info-circle"></i> Document Information</h5>
</div>
<div class="card-body">
<ul class="list-group list-group-flush">
<li class="list-group-item d-flex justify-content-between align-items-center">
File Type
<span class="badge bg-primary rounded-pill">{{ document.file_type }}</span>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center">
Size
<span class="badge bg-secondary rounded-pill">{{ (document.file_size / 1024)|round(1) }} KB</span>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center">
Uploaded
<span class="badge bg-info rounded-pill">{{ document.created_at.split(' ')[0] }}</span>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center">
Visibility
<span class="badge bg-{{ 'success' if document.visibility == 'public' else 'warning' }} rounded-pill">
{{ document.visibility }}
</span>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center">
Versions
<span class="badge bg-dark rounded-pill">{{ versions|length }}</span>
</li>
</ul>
</div>
</div>
<div class="card">
<div class="card-header bg-success text-white">
<h5 class="mb-0"><i class="bi bi-share"></i> Sharing Information</h5>
</div>
<div class="card-body">
{% if shares %}
<h6>Shared With</h6>
<ul class="list-group list-group-flush mb-3">
{% for share in shares %}
<li class="list-group-item d-flex justify-content-between align-items-center">
{% if share.shared_with %}
<span><i class="bi bi-person"></i> {{ share.username }}</span>
{% elif share.share_link %}
<span><i class="bi bi-link"></i> Via Link</span>
<small class="text-muted">
{% if share.expires_at %}
Expires: {{ share.expires_at.split(' ')[0] }}
{% endif %}
</small>
{% endif %}
</li>
{% endfor %}
</ul>
{% else %}
<p>This document hasn't been shared yet.</p>
{% endif %}
<a href="{{ url_for('share_document', document_id=document.id) }}" class="btn btn-success w-100">
<i class="bi bi-share"></i> Manage Sharing
</a>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,74 @@
%PDF-1.4
%“Œ‹ž ReportLab Generated PDF document http://www.reportlab.com
1 0 obj
<<
/F1 2 0 R /F2 3 0 R
>>
endobj
2 0 obj
<<
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
>>
endobj
3 0 obj
<<
/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font
>>
endobj
4 0 obj
<<
/Contents 8 0 R /MediaBox [ 0 0 612 792 ] /Parent 7 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
5 0 obj
<<
/PageMode /UseNone /Pages 7 0 R /Type /Catalog
>>
endobj
6 0 obj
<<
/Author (\(anonymous\)) /CreationDate (D:20250419002510-06'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20250419002510-06'00') /Producer (ReportLab PDF Library - www.reportlab.com)
/Subject (\(unspecified\)) /Title (\(anonymous\)) /Trapped /False
>>
endobj
7 0 obj
<<
/Count 1 /Kids [ 4 0 R ] /Type /Pages
>>
endobj
8 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 529
>>
stream
Gat='gMYb"%"6H'SB&'?;G!(UO2I8!__@a?J&$#f<eY*rZ@\g#5>$*'2bAXE&8YLFaE%h_F"gc[^)I0%X)Pc5hu_FE>V7;%?hdLCrlk.^1WtmrJhE7H7@Uo];&Ff-6nsG%#k0re&L-;G\@#YN=BTFDp9ifP=K.j6rY!a+WiH,?8*`<7RKoKQCQb9A(BZdV?r=iBJ"o'dC9KP">m\>811tXQ$SYV=Uu^knO09pVM#ZD<e?)fT8%mZb>&?k_>AG8SQa&"d2VRuiZY\8C2G'57>>o'CP=IOs]eQpANnCr;]'2E^joo27AAD=.,*N>RiD-=WOId&C`12"KYe?bV@?"Uao/':&Y&I]D@Rg.udZNXeAjg2S-&i6?/h62Q@A'Kn:T<L`HO6.L3GA:]cf5kL\tR$jMZ=DndRT5g8DB*AV\\uLAT/^/KfT?HfLOuXED$sc*Oeu`g#lT9?27[52!:'0mG'P?XSD]-s0cm?nCZHC[t1N,o:W^Bp]lM%V]*7Ffgb!Yc+:%o#Pa\b"X!jKIf~>endstream
endobj
xref
0 9
0000000000 65535 f
0000000073 00000 n
0000000114 00000 n
0000000221 00000 n
0000000333 00000 n
0000000526 00000 n
0000000594 00000 n
0000000877 00000 n
0000000936 00000 n
trailer
<<
/ID
[<e4c6428afa3c84fcf9371b13758dd6b1><e4c6428afa3c84fcf9371b13758dd6b1>]
% ReportLab generated PDF document -- digest (http://www.reportlab.com)
/Info 6 0 R
/Root 5 0 R
/Size 9
>>
startxref
1555
%%EOF

Binary file not shown.

Binary file not shown.