re-Made server totally - version 1.0 now come.

This commit is contained in:
Anry Das 2025-10-19 09:22:23 +03:00
parent 74c658b2dd
commit 738fb9f179
17 changed files with 2308 additions and 724 deletions

9
.env.exampl Normal file
View File

@ -0,0 +1,9 @@
# Database Configuration
DATABASE_URL=postgresql://username:password@localhost:5432/filestore
# Upload Directory
UPLOAD_DIR=./uploads
# Server Configuration
HOST=0.0.0.0
PORT=8000

367
README.md Normal file
View File

@ -0,0 +1,367 @@
# Das File Storage Web Application
A production-ready file storage service built with FastAPI, PostgreSQL, and session-based authentication.
## Features
- **Web Interface**: Modern, responsive GUI for users and admins
- **User Authentication**: Session-based auth for web, HTTP Basic Auth and API Key for REST API
- **Role-Based Access**: Admin and regular user roles
- **File Management**: Upload, download, delete files via web UI or API
- **Secure Sharing**: Generate secure share links without exposing usernames or filenames
- **Admin Panel**: Full user management interface (create, delete, block users)
- **Settings Page**: Change password, manage API keys
- **Password Security**: PBKDF2-SHA256 password hashing
- **Database**: PostgreSQL for all metadata storage
## Quick Start
### Using Docker Compose (Recommended)
1. Clone the repository
2. Update database credentials in `docker-compose.yml`
3. Run:
```bash
docker-compose up -d
```
The application will be available at `http://localhost:8000` by default
### Project Structure
```
project/
├── main.py # Main application file
├── requirements.txt # Python dependencies
├── docker-compose.yml # Docker configuration
├── Dockerfile # Container image
├── .env # Environment variables
├── templates/ # HTML templates
│ ├── base.html # Base template
│ ├── login.html # Login page
│ ├── user_files.html # User dashboard
│ ├── settings.html # Settings page
│ └── admin.html # Admin panel
└── uploads/ # File storage directory
```
### Manual Installation
1. Install PostgreSQL and create a database:
```sql
CREATE DATABASE filestore;
```
2. Install Python dependencies:
```bash
pip install -r requirements.txt
```
3. Create `.env` file:
```bash
cp .env.example .env
# Edit .env with your database credentials
```
4. Update `configs/server_config.yaml` file with your own server's configuration (including `secrets.yaml` if needed)
5. Create templates directory and add HTML files:
```bash
mkdir templates
# Copy all template files (base.html, login.html, user_files.html, settings.html, admin.html)
```
6. Run the application:
```bash
python main.py
```
Or with uvicorn:
```bash
uvicorn main:app --host 0.0.0.0 --port 8000 --reload
```
## Default Admin Account
On first startup, a default admin account is created:
- **Username**: `admin`
- **Password**: `admin123`
- **⚠️ Change this password immediately in production!**
The admin API key will be printed in the console on first startup.
## Web Interface
### User Interface
1. **Login Page** (`/login`)
- Clean, modern login interface
- Username and password authentication
2. **Dashboard** (`/dashboard`)
- View all your uploaded files
- Upload new files with drag-and-drop
- Download files
- Generate and copy share links
- Delete files
- File size and upload date information
3. **Settings** (`/settings`)
- Change your password
- View and copy your API key
- Regenerate API key
- View account information
### Admin Interface
4. **Admin Panel** (`/admin`)
- Dashboard statistics (total users, active/blocked, total files)
- User management table
- Create new users
- Block/unblock users
- Delete users
- View file counts per user
### Navigation
All pages have a navigation bar with:
- User badge showing username and role
- Quick access to dashboard, settings, and admin panel (if admin)
- Logout button
## API Documentation
Once running, visit:
- Interactive API docs: `http://localhost:8000/docs`
- Alternative docs: `http://localhost:8000/redoc`
## API Endpoints
### Authentication
**HTTP Basic Auth**: Use username and password in the Authorization header
```bash
curl -u username:password http://localhost:8000/api/files
```
**API Key Auth**: Use X-API-Key header
```bash
curl -H "X-API-Key: your-api-key" http://localhost:8000/api/files
```
### User Management (Admin Only)
#### Create User
```bash
POST /api/users
Authorization: Basic admin:admin123
Content-Type: application/json
{
"username": "newuser",
"password": "securepass123",
"is_admin": false
}
```
#### List Users
```bash
GET /api/users
Authorization: Basic admin:admin123
```
#### Delete User
```bash
DELETE /api/users/{user_id}
Authorization: Basic admin:admin123
```
#### Block/Unblock User
```bash
PATCH /api/users/{user_id}/block?block=true
Authorization: Basic admin:admin123
```
#### Get Your API Key
```bash
GET /api/users/me/api-key
Authorization: Basic username:password
```
#### Regenerate API Key
```bash
POST /api/users/me/api-key/regenerate
Authorization: Basic username:password
```
### File Management
#### Upload File
```bash
POST /api/files/upload
Authorization: Basic username:password
Content-Type: multipart/form-data
file=@/path/to/file.pdf
```
With API Key:
```bash
curl -X POST \
-H "X-API-Key: your-api-key" \
-F "file=@document.pdf" \
http://localhost:8000/api/files/upload
```
#### List Your Files
```bash
GET /api/files
Authorization: Basic username:password
```
#### Download File
```bash
GET /api/files/{file_id}/download
Authorization: Basic username:password
```
#### Delete File
```bash
DELETE /api/files/{file_id}
Authorization: Basic username:password
```
### Public File Sharing
#### Download Shared File (No Auth Required)
```bash
GET /share/{share_token}
```
Example: `http://localhost:8000/share/abc123xyz...`
## Database Schema
### Users Table
- `id`: Primary key
- `username`: Unique username
- `password_hash`: PBKDF2-SHA256 hashed password
- `is_admin`: Admin role flag
- `is_blocked`: Block status
- `api_key`: Unique API key for REST authentication
- `created_at`: Account creation timestamp
### Files Table
- `id`: Primary key
- `filename`: Original filename
- `stored_filename`: Unique stored filename
- `file_size`: File size in bytes
- `content_type`: MIME type
- `share_token`: Secure token for public sharing
- `user_id`: Foreign key to users
- `uploaded_at`: Upload timestamp
## Security Features
1. **Password Hashing**: PBKDF2-SHA256 with 100,000 iterations
2. **Secure Tokens**: Cryptographically secure random tokens for API keys and share links
3. **User Isolation**: Users can only access their own files
4. **Admin Controls**: Block/unblock users, manage accounts
5. **Share Links**: No exposure of usernames or original filenames
## Production Deployment
### Environment Variables
```bash
DATABASE_URL=postgresql://user:pass@host:port/dbname
UPLOAD_DIR=/var/app/uploads
```
### Security Checklist
- [ ] Change default admin password
- [ ] Use strong database credentials
- [ ] Enable HTTPS/TLS
- [ ] Set up firewall rules
- [ ] Configure backup strategy
- [ ] Set up monitoring and logging
- [ ] Limit file upload sizes
- [ ] Implement rate limiting
- [ ] Regular security updates
### Nginx Configuration Example
```nginx
server {
listen 80;
server_name yourdomain.com;
client_max_body_size 100M;
location / {
proxy_pass http://localhost:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
```
### PostgreSQL Optimization
```sql
-- Create indexes for better performance
CREATE INDEX idx_files_user_id ON files(user_id);
CREATE INDEX idx_files_share_token ON files(share_token);
CREATE INDEX idx_users_username ON users(username);
CREATE INDEX idx_users_api_key ON users(api_key);
```
## Testing
### Create a Test User
```bash
curl -X POST http://localhost:8000/api/users \
-u admin:admin123 \
-H "Content-Type: application/json" \
-d '{"username": "testuser", "password": "testpass123"}'
```
### Upload a Test File
```bash
echo "Hello World" > test.txt
curl -X POST http://localhost:8000/api/files/upload \
-u testuser:testpass123 \
-F "file=@test.txt"
```
### List Files
```bash
curl http://localhost:8000/api/files \
-u testuser:testpass123
```
## Troubleshooting
### Database Connection Issues
- Verify PostgreSQL is running
- Check DATABASE_URL is correct
- Ensure database exists and user has permissions
### File Upload Issues
- Check UPLOAD_DIR exists and has write permissions
- Verify disk space is available
- Check file size limits
### Authentication Issues
- Verify username/password are correct
- Check if user is blocked
- Ensure API key is valid and not regenerated
## Support
For issues and questions, please open an issue on the repository.
###### _Made by -=:dAs:=-_

View File

@ -1,30 +1,17 @@
import os import os
from config_file import read_config as read_cfg from config_file import read_config as read_cfg
APP_VERSION="1.0" APP_VERSION="1.0.0"
SCRIPT_PATH = os.path.dirname(__file__) SCRIPT_PATH = os.path.dirname(__file__)
CONFIGS_DIR = SCRIPT_PATH + "/configs" CONFIGS_DIR = SCRIPT_PATH + "/configs"
CONFIG_FILE_NAME = CONFIGS_DIR + "/server_config.yaml" CONFIG_FILE_NAME = CONFIGS_DIR + "/server_config.yaml"
API_KEY = '' API_KEY = ''
UPLOAD_DIR = 'uploads' UPLOAD_DIR = '.uploads'
DB_URL = '' DB_URL = ''
APP_PORT = 25100 APP_PORT = 25100
IS_DEBUG = False IS_DEBUG = False
IS_DOCS_AVAILABLE = False IS_DOCS_AVAILABLE = False
HTML_FILE = 'html/main.html'
CSS_FILE = 'css/main.css'
JS_FILE = 'js/main.js'
ALIAS_NUM_LETTERS = 3
ALIAS_NUM_DIGITS = 4
"""
For 3 + 4 defaults there are 1 406 080 000 possible values:
Кількість варіантів для кожної літери (великої або малої) дорівнює 52 (26 великих + 26 малих).
Кількість комбінацій для трьох літер: 52 * 52 * 52 = 140 608.
Кількість варіантів для кожної цифри: 10 (0-9).
Кількість комбінацій для чотирьох цифр: 10 * 10 * 10 * 10 = 10 000.
Загальна кількість комбінацій: 140 608 * 10 000 = 1 406 080 000.
"""
def read_app_config(): def read_app_config():
j, _ = read_cfg(CONFIG_FILE_NAME) j, _ = read_cfg(CONFIG_FILE_NAME)
@ -40,18 +27,12 @@ def get_config_value(cfg, section, key, default):
return cfg[section][key] if section in cfg and key in cfg[section] else default return cfg[section][key] if section in cfg and key in cfg[section] else default
def parse_config(cfg): def parse_config(cfg):
global UPLOAD_DIR, APP_PORT, IS_DEBUG, DB_URL, ALIAS_NUM_LETTERS, ALIAS_NUM_DIGITS\ global UPLOAD_DIR, APP_PORT, IS_DEBUG, DB_URL, IS_DOCS_AVAILABLE
, IS_DOCS_AVAILABLE, HTML_FILE, CSS_FILE, JS_FILE
UPLOAD_DIR = get_config_value(cfg, 'server', 'directory', UPLOAD_DIR) UPLOAD_DIR = get_config_value(cfg, 'server', 'directory', UPLOAD_DIR)
APP_PORT = get_config_value(cfg, 'server', 'port', APP_PORT) APP_PORT = get_config_value(cfg, 'server', 'port', APP_PORT)
DB_URL = get_config_value(cfg, 'server', 'db_url', DB_URL) DB_URL = get_config_value(cfg, 'server', 'db_url', DB_URL)
IS_DEBUG = get_config_value(cfg, 'server', 'debug', IS_DEBUG) IS_DEBUG = get_config_value(cfg, 'server', 'debug', IS_DEBUG)
ALIAS_NUM_LETTERS = get_config_value(cfg, 'server', 'alias_letters', ALIAS_NUM_LETTERS)
ALIAS_NUM_DIGITS = get_config_value(cfg, 'server', 'alias_digits', ALIAS_NUM_DIGITS)
IS_DOCS_AVAILABLE = get_config_value(cfg, 'server', 'docs_available', IS_DOCS_AVAILABLE) IS_DOCS_AVAILABLE = get_config_value(cfg, 'server', 'docs_available', IS_DOCS_AVAILABLE)
HTML_FILE = get_config_value(cfg, 'interface', 'html', HTML_FILE)
CSS_FILE = get_config_value(cfg, 'interface', 'css', CSS_FILE)
JS_FILE = get_config_value(cfg, 'interface', 'js', JS_FILE)
def init_config(): def init_config():
config = read_app_config() config = read_app_config()

View File

@ -1,13 +1,8 @@
server: server:
port: !secret port port: !secret port
directory: 'uploads' directory: '.uploads'
db_url: !secret db_url db_url: !secret db_url
alias_letters: 3 alias_letters: 3
alias_digits: 4 alias_digits: 4
debug: false debug: false
docs_available: false docs_available: true
interface:
html: 'html/main.html'
css: 'css/main.css'
script: 'js/main.js'

View File

@ -1,64 +0,0 @@
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 50px auto;
padding: 20px;
}
h1 { color: #333; }
.section {
background: #f5f5f5;
padding: 20px;
margin: 20px 0;
border-radius: 8px;
}
input[type="file"], input[type="text"] {
margin: 10px 0;
padding: 8px;
width: 100%;
max-width: 400px;
}
button {
background: #007bff;
color: white;
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
margin: 5px;
}
button:hover {
background: #0056b3;
}
.file-list {
list-style: none;
padding: 0;
}
.file-item {
background: white;
padding: 10px;
margin: 5px 0;
border-radius: 4px;
display: flex;
justify-content: space-between;
align-items: center;
}
.message {
padding: 10px;
margin: 10px 0;
border-radius: 4px;
}
.success { background: #d4edda; color: #155724; }
.error { background: #f8d7da; color: #721c24; }
.info { background: #d1ecf1; color: #0c5460; }
.hidden { display: none; }
.api-key-display {
background: white;
padding: 10px;
border-radius: 4px;
font-family: monospace;
word-break: break-all;
}
.file-date {
font-size: 0.85em;
color: #676;
}

38
docker-compose.yaml Normal file
View File

@ -0,0 +1,38 @@
version: '3.8'
services:
postgres:
image: postgres:15-alpine
container_name: filestore_db
environment:
POSTGRES_USER: filestore_user
POSTGRES_PASSWORD: secure_password_here
POSTGRES_DB: filestore
volumes:
- postgres_data:/var/lib/postgresql/data
ports:
- "5432:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U filestore_user"]
interval: 10s
timeout: 5s
retries: 5
app:
build: .
container_name: filestore_app
environment:
DATABASE_URL: postgresql://filestore_user:secure_password_here@postgres:5432/filestore
UPLOAD_DIR: /app/uploads
volumes:
- ./uploads:/app/uploads
- ./app:/app
ports:
- "8000:8000"
depends_on:
postgres:
condition: service_healthy
command: uvicorn main:app --host 0.0.0.0 --port 8000 --reload
volumes:
postgres_data:

24
dockerfile Normal file
View File

@ -0,0 +1,24 @@
FROM python:3.13-slim
WORKDIR /app
# Install system dependencies
RUN apt-get update && apt-get install -y \
postgresql-client \
&& rm -rf /var/lib/apt/lists/*
# Copy requirements and install Python dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy application code
COPY main.py .
# Create uploads directory
RUN mkdir -p /app/uploads
# Expose port
EXPOSE 8000
# Run the application
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

View File

@ -1,51 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Das File Server</title>
<!--<link rel="stylesheet" href="/main.css">-->
<style>
#$STYLE$#
</style>
</head>
<body>
<h1>🔐 Secure File Server by -=:dAs:=-</h1>
<div class="section" id="authSection">
<h2>API Key Management</h2>
<div id="noKeySection">
<!-- Remove it section in prod!
<button onclick="generateKey()">Generate New API Key</button>
<p>or</p>
-->
<input type="text" id="existingKey" placeholder="Enter your existing API key">
<button onclick="useExistingKey()">Use This Key</button>
</div>
<div id="hasKeySection" class="hidden">
<div class="message info">
<strong>Your API Key:</strong>
<div class="api-key-display" id="apiKeyDisplay"></div>
<small>⚠️ Save this key securely!</small>
</div>
<button onclick="clearKey()" style="background: #dc3545;">Logout</button>
</div>
<div id="authMessage"></div>
</div>
<div class="section hidden" id="uploadSection">
<h2>Upload Files</h2>
<input type="file" id="fileInput" multiple>
<button onclick="uploadFiles()">Upload</button>
<div id="uploadMessage"></div>
</div>
<div class="section hidden" id="filesSection">
<h2>Your Files</h2>
<button onclick="loadFiles()">Refresh List</button>
<ul class="file-list" id="fileList"></ul>
</div>
<script>
#$SCRIPT$#
</script>
</body>
</html>

View File

@ -1,171 +0,0 @@
let apiKey = localStorage.getItem('apiKey');
function updateUI() {
const hasKey = !!apiKey;
document.getElementById('noKeySection').classList.toggle('hidden', hasKey);
document.getElementById('hasKeySection').classList.toggle('hidden', !hasKey);
document.getElementById('uploadSection').classList.toggle('hidden', !hasKey);
document.getElementById('filesSection').classList.toggle('hidden', !hasKey);
if (hasKey) {
document.getElementById('apiKeyDisplay').textContent = apiKey;
loadFiles();
}
}
async function generateKey() {
const messageDiv = document.getElementById('authMessage');
try {
const response = await fetch('/add-user', {
method: 'POST'
});
const result = await response.json();
if (response.ok) {
apiKey = result.api_key;
localStorage.setItem('apiKey', apiKey);
messageDiv.innerHTML = '<div class="message success">API key generated successfully!</div>';
updateUI();
} else {
messageDiv.innerHTML = `<div class="message error">${result.detail}</div>`;
}
} catch (error) {
messageDiv.innerHTML = '<div class="message error">Failed to generate key</div>';
}
}
function useExistingKey() {
const key = document.getElementById('existingKey').value.trim();
const messageDiv = document.getElementById('authMessage');
if (!key) {
messageDiv.innerHTML = '<div class="message error">Please enter an API key</div>';
return;
}
apiKey = key;
localStorage.setItem('apiKey', apiKey);
messageDiv.innerHTML = '<div class="message success">API key set!</div>';
document.getElementById('existingKey').value = '';
updateUI();
}
function clearKey() {
apiKey = null;
localStorage.removeItem('apiKey');
document.getElementById('authMessage').innerHTML = '';
updateUI();
}
async function uploadFiles() {
const input = document.getElementById('fileInput');
const files = input.files;
const messageDiv = document.getElementById('uploadMessage');
if (files.length === 0) {
messageDiv.innerHTML = '<div class="message error">Please select files</div>';
return;
}
const formData = new FormData();
for (let file of files) {
formData.append('files', file);
}
try {
const response = await fetch('/upload', {
method: 'POST',
headers: {
'X-API-Key': apiKey
},
body: formData
});
const result = await response.json();
if (response.ok) {
messageDiv.innerHTML = '<div class="message success">Files uploaded successfully!</div>';
input.value = '';
loadFiles();
} else {
messageDiv.innerHTML = `<div class="message error">${result.detail}</div>`;
}
} catch (error) {
messageDiv.innerHTML = '<div class="message error">Upload failed</div>';
}
}
async function loadFiles() {
if (!apiKey) return;
try {
const response = await fetch('/files', {
headers: {
'X-API-Key': apiKey
}
});
const files = await response.json();
const fileList = document.getElementById('fileList');
if (response.status === 401) {
document.getElementById('authMessage').innerHTML = '<div class="message error">Invalid API key</div>';
clearKey();
return;
}
if (files.length === 0) {
fileList.innerHTML = '<li>No files available</li>';
return;
}
fileList.innerHTML = files.map(file => `
<li class="file-item">
<div>
<div><strong>${file.original_name}</strong> (${formatBytes(file.size)})&nbsp;<a href="/dn/${file.file_id}" title="Direct link">&#11015;</a></div>
<div class="file-date">Uploaded: ${new Date(file.uploaded_at).toLocaleString()}</div>
</div>
<div>
<button onclick="downloadFile('${file.file_id}', '${file.original_name}')">Download</button>
<button onclick="deleteFile('${file.file_id}')" style="background: #dc3545;">Delete</button>
</div>
</li>
`).join('');
} catch (error) {
console.error('Failed to load files:', error);
}
}
async function downloadFile(fileId, filename) {
window.location.href = `/download/${fileId}?api_key=${apiKey}`;
}
async function deleteFile(fileId) {
if (!confirm('Are you sure you want to delete this file?')) return;
try {
const response = await fetch(`/files/${fileId}`, {
method: 'DELETE',
headers: {
'X-API-Key': apiKey
}
});
if (response.ok) {
loadFiles();
} else {
alert('Failed to delete file');
}
} catch (error) {
alert('Failed to delete file');
}
}
function formatBytes(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
}
// Initialize UI
updateUI();

1076
main.py

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,9 @@
fastapi~=0.118.0 fastapi==0.104.1
uvicorn~=0.37.0 uvicorn[standard]==0.24.0
python-multipart sqlalchemy==2.0.23
fastapi[standard] psycopg2-binary==2.9.9
asyncpg~=0.30.0 python-multipart==0.0.19
PyYAML~=6.0.3 pydantic==2.5.0
pydantic-settings==2.1.0
python-dotenv==1.0.0
jinja2==3.1.6

72
setup.sh Normal file
View File

@ -0,0 +1,72 @@
#!/bin/bash
echo "🚀 Setting up Das File Storage Application..."
# Create necessary directories
echo "📁 Creating directories..."
mkdir -p uploads
mkdir -p templates
# Check if templates exist
if [ ! -f "templates/base.html" ]; then
echo "⚠️ Warning: Template files not found in templates/ directory"
echo "Please make sure to copy all HTML template files to the templates/ directory:"
echo " - base.html"
echo " - login.html"
echo " - user_files.html"
echo " - settings.html"
echo " - admin.html"
fi
# Create .env if it doesn't exist
if [ ! -f ".env" ]; then
echo "📝 Creating .env file..."
cp .env.example .env
echo "✅ Created .env file. Please update with your database credentials."
fi
# Check if Python is installed
if ! command -v python3 &> /dev/null; then
echo "❌ Python 3 is not installed. Please install Python 3.8 or higher."
exit 1
fi
# Check if PostgreSQL is running (optional check)
if command -v pg_isready &> /dev/null; then
if pg_isready &> /dev/null; then
echo "✅ PostgreSQL is running"
else
echo "⚠️ PostgreSQL is not running. Please start PostgreSQL."
fi
fi
# Create virtual environment
if [ ! -d "venv" ]; then
echo "🐍 Creating virtual environment..."
python3 -m venv venv
fi
# Activate virtual environment
echo "🔧 Activating virtual environment..."
source venv/bin/activate
# Install requirements
echo "📦 Installing Python packages..."
pip install --upgrade pip
pip install -r requirements.txt
echo ""
echo "✅ Setup complete!"
echo ""
echo "📋 Next steps:"
echo "1. Update .env file with your PostgreSQL credentials"
echo "2. Make sure all template files are in the templates/ directory"
echo "3. Create PostgreSQL database: createdb filestore"
echo "4. Run the application: python main.py"
echo "5. Open browser: http://localhost:8000"
echo "6. Login with default admin credentials:"
echo " Username: admin"
echo " Password: admin123"
echo ""
echo "⚠️ Remember to change the admin password immediately!"
echo ""

266
templates/admin.html Normal file
View File

@ -0,0 +1,266 @@
{% extends "base.html" %}
{% block title %}Admin Panel - Das File Storage{% endblock %}
{% block content %}
<div class="nav-tabs">
<a href="/dashboard" class="nav-tab">📂 My Files</a>
<a href="/settings" class="nav-tab">⚙️ Settings</a>
<a href="/admin" class="nav-tab active">👑 Admin Panel</a>
</div>
{% if message %}
<div class="alert alert-success">
{{ message }}
</div>
{% endif %}
{% if error %}
<div class="alert alert-error">
{{ error }}
</div>
{% endif %}
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
<h2>User Management</h2>
<button onclick="openCreateUserModal()" class="btn btn-primary">
Create New User
</button>
</div>
<div style="background: #f9fafb; padding: 20px; border-radius: 8px; margin-bottom: 20px;">
<h3 style="margin-bottom: 15px; color: #374151;">📊 Statistics</h3>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px;">
<div style="background: white; padding: 15px; border-radius: 6px; border-left: 4px solid #667eea;">
<div style="color: #6b7280; font-size: 12px;">Total Users</div>
<div style="font-size: 24px; font-weight: bold; color: #374151;">{{ stats.total_users }}</div>
</div>
<div style="background: white; padding: 15px; border-radius: 6px; border-left: 4px solid #10b981;">
<div style="color: #6b7280; font-size: 12px;">Active Users</div>
<div style="font-size: 24px; font-weight: bold; color: #374151;">{{ stats.active_users }}</div>
</div>
<div style="background: white; padding: 15px; border-radius: 6px; border-left: 4px solid #ef4444;">
<div style="color: #6b7280; font-size: 12px;">Blocked Users</div>
<div style="font-size: 24px; font-weight: bold; color: #374151;">{{ stats.blocked_users }}</div>
</div>
<div style="background: white; padding: 15px; border-radius: 6px; border-left: 4px solid #f59e0b;">
<div style="color: #6b7280; font-size: 12px;">Total Files</div>
<div style="font-size: 24px; font-weight: bold; color: #374151;">{{ stats.total_files }}</div>
</div>
</div>
</div>
<table>
<thead>
<tr>
<th>Username</th>
<th>Role</th>
<th>Files</th>
<th>Created</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for user in users %}
<tr>
<td>
<strong>{{ user.username }}</strong>
</td>
<td>
{% if user.is_admin %}
<span class="admin-badge">Admin</span>
{% else %}
User
{% endif %}
</td>
<td class="file-size">{{ user.file_count }} files</td>
<td class="file-size">{{ user.created_at.strftime('%Y-%m-%d') }}</td>
<td>
{% if user.is_blocked %}
<span style="color: #ef4444; font-weight: bold;">🚫 Blocked</span>
{% else %}
<span style="color: #10b981; font-weight: bold;">✅ Active</span>
{% endif %}
</td>
<td>
{% if user.username != username %}
{% if user.is_blocked %}
<button onclick="unblockUser({{ user.id }}, '{{ user.username }}')" class="btn btn-success btn-small">
✅ Unblock
</button>
{% else %}
<button onclick="blockUser({{ user.id }}, '{{ user.username }}')" class="btn btn-secondary btn-small">
🚫 Block
</button>
{% endif %}
<button onclick="deleteUser({{ user.id }}, '{{ user.username }}')" class="btn btn-danger btn-small">
🗑️ Delete
</button>
{% else %}
<span style="color: #6b7280; font-size: 12px;">Current User</span>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
<!-- Create User Modal -->
<div id="createUserModal" class="modal">
<div class="modal-content">
<div class="modal-header">Create New User</div>
<form id="createUserForm">
<div class="form-group">
<label for="new_username">Username</label>
<input type="text" id="new_username" name="username" required minlength="3" maxlength="50">
</div>
<div class="form-group">
<label for="new_password">Password</label>
<input type="password" id="new_password" name="password" required minlength="8">
<small style="color: #6b7280;">Minimum 8 characters</small>
</div>
<div class="form-group">
<label for="confirm_new_password">Confirm Password</label>
<input type="password" id="confirm_new_password" required>
</div>
<div class="form-group">
<div class="checkbox-group">
<input type="checkbox" id="is_admin" name="is_admin">
<label for="is_admin" style="margin: 0;">Administrator privileges</label>
</div>
</div>
<div class="form-actions">
<button type="button" onclick="closeCreateUserModal()" class="btn btn-secondary">Cancel</button>
<button type="submit" class="btn btn-primary">Create User</button>
</div>
</form>
</div>
</div>
{% endblock %}
{% block extra_scripts %}
<script>
function openCreateUserModal() {
document.getElementById('createUserModal').classList.add('active');
}
function closeCreateUserModal() {
document.getElementById('createUserModal').classList.remove('active');
document.getElementById('createUserForm').reset();
}
document.getElementById('createUserForm').addEventListener('submit', async (e) => {
e.preventDefault();
const password = document.getElementById('new_password').value;
const confirmPassword = document.getElementById('confirm_new_password').value;
if (password !== confirmPassword) {
alert('Passwords do not match!');
return;
}
const formData = {
username: document.getElementById('new_username').value,
password: password,
is_admin: document.getElementById('is_admin').checked
};
try {
const response = await fetch('/api/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(formData)
});
if (response.ok) {
window.location.reload();
} else {
const error = await response.json();
alert('Failed to create user: ' + (error.detail || 'Unknown error'));
}
} catch (error) {
alert('Failed to create user: ' + error.message);
}
});
async function blockUser(userId, username) {
if (!confirm(`Are you sure you want to block user "${username}"?`)) {
return;
}
try {
const response = await fetch(`/api/users/${userId}/block?block=true`, {
method: 'PATCH'
});
if (response.ok) {
window.location.reload();
} else {
alert('Failed to block user');
}
} catch (error) {
alert('Failed to block user: ' + error.message);
}
}
async function unblockUser(userId, username) {
if (!confirm(`Are you sure you want to unblock user "${username}"?`)) {
return;
}
try {
const response = await fetch(`/api/users/${userId}/block?block=false`, {
method: 'PATCH'
});
if (response.ok) {
window.location.reload();
} else {
alert('Failed to unblock user');
}
} catch (error) {
alert('Failed to unblock user: ' + error.message);
}
}
async function deleteUser(userId, username) {
if (!confirm(`Are you sure you want to DELETE user "${username}"?\n\nThis will also delete all their files. This action cannot be undone!`)) {
return;
}
const confirmation = prompt(`Type "${username}" to confirm deletion:`);
if (confirmation !== username) {
alert('Deletion cancelled - username did not match');
return;
}
try {
const response = await fetch(`/api/users/${userId}`, {
method: 'DELETE'
});
if (response.ok) {
window.location.reload();
} else {
alert('Failed to delete user');
}
} catch (error) {
alert('Failed to delete user: ' + error.message);
}
}
// Close modals when clicking outside
document.querySelectorAll('.modal').forEach(modal => {
modal.addEventListener('click', (e) => {
if (e.target === modal) {
modal.classList.remove('active');
}
});
});
</script>
{% endblock %}

334
templates/base.html Normal file
View File

@ -0,0 +1,334 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Das File Storage{% endblock %}</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
.header {
background: white;
padding: 20px 30px;
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
margin-bottom: 20px;
display: flex;
justify-content: space-between;
align-items: center;
}
.header h1 {
color: #667eea;
font-size: 24px;
}
.user-info {
display: flex;
align-items: center;
gap: 15px;
}
.user-badge {
background: #667eea;
color: white;
padding: 8px 15px;
border-radius: 20px;
font-size: 14px;
}
.admin-badge {
background: #f59e0b;
color: white;
padding: 4px 10px;
border-radius: 12px;
font-size: 12px;
font-weight: bold;
}
.btn {
padding: 10px 20px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
text-decoration: none;
display: inline-block;
transition: all 0.3s;
}
.btn-primary {
background: #667eea;
color: white;
}
.btn-primary:hover {
background: #5568d3;
}
.btn-danger {
background: #ef4444;
color: white;
}
.btn-danger:hover {
background: #dc2626;
}
.btn-secondary {
background: #6b7280;
color: white;
}
.btn-secondary:hover {
background: #4b5563;
}
.btn-success {
background: #10b981;
color: white;
}
.btn-success:hover {
background: #059669;
}
.btn-small {
padding: 6px 12px;
font-size: 12px;
}
.content {
background: white;
padding: 30px;
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.nav-tabs {
display: flex;
gap: 10px;
margin-bottom: 30px;
border-bottom: 2px solid #e5e7eb;
}
.nav-tab {
padding: 12px 24px;
background: none;
border: none;
cursor: pointer;
font-size: 16px;
color: #6b7280;
border-bottom: 3px solid transparent;
transition: all 0.3s;
}
.nav-tab.active {
color: #667eea;
border-bottom-color: #667eea;
}
.nav-tab:hover {
color: #667eea;
}
.alert {
padding: 15px 20px;
border-radius: 6px;
margin-bottom: 20px;
}
.alert-success {
background: #d1fae5;
color: #065f46;
border: 1px solid #10b981;
}
.alert-error {
background: #fee2e2;
color: #991b1b;
border: 1px solid #ef4444;
}
.alert-info {
background: #dbeafe;
color: #1e40af;
border: 1px solid #3b82f6;
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
th, td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #e5e7eb;
}
th {
background: #f9fafb;
font-weight: 600;
color: #374151;
}
tr:hover {
background: #f9fafb;
}
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
z-index: 1000;
}
.modal.active {
display: flex;
justify-content: center;
align-items: center;
}
.modal-content {
background: white;
padding: 30px;
border-radius: 10px;
max-width: 500px;
width: 90%;
max-height: 90vh;
overflow-y: auto;
}
.modal-header {
font-size: 20px;
font-weight: 600;
margin-bottom: 20px;
color: #111827;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
font-weight: 500;
color: #374151;
}
.form-group input,
.form-group select,
.form-group textarea {
width: 100%;
padding: 10px;
border: 1px solid #d1d5db;
border-radius: 6px;
font-size: 14px;
}
.form-group input:focus,
.form-group select:focus,
.form-group textarea:focus {
outline: none;
border-color: #667eea;
}
.form-actions {
display: flex;
gap: 10px;
justify-content: flex-end;
margin-top: 20px;
}
.empty-state {
text-align: center;
padding: 60px 20px;
color: #6b7280;
}
.empty-state svg {
width: 80px;
height: 80px;
margin-bottom: 20px;
opacity: 0.5;
}
.file-size {
color: #6b7280;
font-size: 13px;
}
.share-link {
display: flex;
gap: 10px;
align-items: center;
margin-top: 10px;
}
.share-link input {
flex: 1;
padding: 8px;
border: 1px solid #d1d5db;
border-radius: 4px;
font-size: 12px;
}
.checkbox-group {
display: flex;
align-items: center;
gap: 8px;
}
.checkbox-group input[type="checkbox"] {
width: auto;
}
</style>
{% block extra_style %}{% endblock %}
</head>
<body>
<div class="container">
<div class="header">
<h1>📁 File Storage Service by -=:dAs:=-</h1>
<div class="user-info">
<div class="user-badge">
👤 {{ username }}
{% if is_admin %}
<span class="admin-badge">ADMIN</span>
{% endif %}
</div>
<a href="/logout" class="btn btn-secondary btn-small">Logout</a>
</div>
</div>
<div class="content">
{% block content %}{% endblock %}
</div>
</div>
{% block extra_scripts %}{% endblock %}
</body>
</html>

139
templates/login.html Normal file
View File

@ -0,0 +1,139 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Das File Storage</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
}
.login-container {
background: white;
padding: 40px;
border-radius: 10px;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2);
max-width: 400px;
width: 100%;
}
.login-header {
text-align: center;
margin-bottom: 30px;
}
.login-header h1 {
color: #667eea;
font-size: 28px;
margin-bottom: 10px;
}
.login-header p {
color: #6b7280;
font-size: 14px;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
font-weight: 500;
color: #374151;
}
.form-group input {
width: 100%;
padding: 12px;
border: 1px solid #d1d5db;
border-radius: 6px;
font-size: 14px;
}
.form-group input:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.btn-login {
width: 100%;
padding: 12px;
background: #667eea;
color: white;
border: none;
border-radius: 6px;
font-size: 16px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s;
}
.btn-login:hover {
background: #5568d3;
}
.alert {
padding: 12px;
border-radius: 6px;
margin-bottom: 20px;
font-size: 14px;
}
.alert-error {
background: #fee2e2;
color: #991b1b;
border: 1px solid #ef4444;
}
.icon {
font-size: 48px;
margin-bottom: 20px;
}
</style>
</head>
<body>
<div class="login-container">
<div class="login-header">
<div class="icon">📁</div>
<h1>Das File Storage</h1>
<p>Sign in to your account</p>
</div>
{% if error %}
<div class="alert alert-error">
{{ error }}
</div>
{% endif %}
<form method="POST" action="/login">
<div class="form-group">
<label for="username">Username</label>
<input type="text" id="username" name="username" required autocomplete="username">
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" id="password" name="password" required autocomplete="current-password">
</div>
<button type="submit" class="btn-login">Sign In</button>
</form>
</div>
</body>
</html>

134
templates/settings.html Normal file
View File

@ -0,0 +1,134 @@
{% extends "base.html" %}
{% block title %}Settings - Das File Storage{% endblock %}
{% block content %}
<div class="nav-tabs">
<a href="/dashboard" class="nav-tab">📂 My Files</a>
<a href="/settings" class="nav-tab active">⚙️ Settings</a>
{% if is_admin %}
<a href="/admin" class="nav-tab">👑 Admin Panel</a>
{% endif %}
</div>
{% if message %}
<div class="alert alert-success">
{{ message }}
</div>
{% endif %}
{% if error %}
<div class="alert alert-error">
{{ error }}
</div>
{% endif %}
<h2>Account Settings</h2>
<div style="display: grid; gap: 30px; margin-top: 30px;">
<!-- Change Password -->
<div style="border: 1px solid #e5e7eb; padding: 25px; border-radius: 8px;">
<h3 style="margin-bottom: 15px; color: #374151;">🔒 Change Password</h3>
<form method="POST" action="/settings/change-password">
<div class="form-group">
<label for="current_password">Current Password</label>
<input type="password" id="current_password" name="current_password" required>
</div>
<div class="form-group">
<label for="new_password">New Password</label>
<input type="password" id="new_password" name="new_password" required minlength="8">
<small style="color: #6b7280;">Minimum 8 characters</small>
</div>
<div class="form-group">
<label for="confirm_password">Confirm New Password</label>
<input type="password" id="confirm_password" name="confirm_password" required>
</div>
<button type="submit" class="btn btn-primary">Update Password</button>
</form>
</div>
<!-- API Key -->
<div style="border: 1px solid #e5e7eb; padding: 25px; border-radius: 8px;">
<h3 style="margin-bottom: 15px; color: #374151;">🔑 API Key</h3>
<p style="color: #6b7280; margin-bottom: 15px;">
Use this key for API authentication with the X-API-Key header.
</p>
<div style="background: #f9fafb; padding: 15px; border-radius: 6px; margin-bottom: 15px;">
<div style="display: flex; gap: 10px; align-items: center;">
<input
type="text"
id="apiKey"
value="{{ api_key }}"
readonly
style="flex: 1; padding: 10px; border: 1px solid #d1d5db; border-radius: 4px; font-family: monospace; font-size: 12px;"
>
<button onclick="copyApiKey()" class="btn btn-secondary btn-small">📋 Copy</button>
</div>
</div>
<div class="alert alert-info" style="margin-bottom: 15px;">
<strong>⚠️ Warning:</strong> Regenerating will invalidate your current API key. All applications using the old key will stop working.
</div>
<button onclick="regenerateApiKey()" class="btn btn-danger">🔄 Regenerate API Key</button>
</div>
<!-- Account Info -->
<div style="border: 1px solid #e5e7eb; padding: 25px; border-radius: 8px;">
<h3 style="margin-bottom: 15px; color: #374151;">👤 Account Information</h3>
<div style="display: grid; gap: 15px;">
<div>
<strong style="color: #6b7280;">Username:</strong>
<div style="margin-top: 5px;">{{ username }}</div>
</div>
<div>
<strong style="color: #6b7280;">Account Type:</strong>
<div style="margin-top: 5px;">
{% if is_admin %}
<span class="admin-badge">Administrator</span>
{% else %}
Regular User
{% endif %}
</div>
</div>
<div>
<strong style="color: #6b7280;">Total Files:</strong>
<div style="margin-top: 5px;">{{ file_count }} files</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_scripts %}
<script>
function copyApiKey() {
const input = document.getElementById('apiKey');
input.select();
document.execCommand('copy');
alert('API Key copied to clipboard!');
}
async function regenerateApiKey() {
if (!confirm('Are you sure? This will invalidate your current API key!')) {
return;
}
try {
const response = await fetch('/api/users/me/api-key/regenerate', {
method: 'POST'
});
if (response.ok) {
window.location.reload();
} else {
alert('Failed to regenerate API key');
}
} catch (error) {
alert('Failed to regenerate API key: ' + error.message);
}
}
</script>
{% endblock %}

228
templates/user_files.html Normal file
View File

@ -0,0 +1,228 @@
{% extends "base.html" %}
{% block title %}My Files - Das File Storage{% endblock %}
{% block content %}
<div class="nav-tabs">
<a href="/dashboard" class="nav-tab active">📂 My Files</a>
<a href="/settings" class="nav-tab">⚙️ Settings</a>
{% if is_admin %}
<a href="/admin" class="nav-tab">👑 Admin Panel</a>
{% endif %}
</div>
{% if message %}
<div class="alert alert-success">
{{ message }}
</div>
{% endif %}
{% if error %}
<div class="alert alert-error">
{{ error }}
</div>
{% endif %}
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
<h2>My Files</h2>
<button onclick="openUploadModal()" class="btn btn-primary">
⬆️ Upload File
</button>
</div>
{% if files %}
<table>
<thead>
<tr>
<th>Filename</th>
<th>Size</th>
<th>Uploaded</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for file in files %}
<tr>
<td>
<strong>{{ file.filename }}</strong>
<div class="file-size">{{ file.content_type or 'Unknown' }}</div>
</td>
<td class="file-size">{{ format_size(file.file_size) }}</td>
<td class="file-size">{{ file.uploaded_at.strftime('%Y-%m-%d %H:%M') }}</td>
<td>
<button onclick="downloadFile({{ file.id }}, '{{ file.filename }}')" class="btn btn-primary btn-small">
⬇️ Download
</button>
<button onclick="showShareLink('{{ file.share_token }}')" class="btn btn-success btn-small">
🔗 Share
</button>
<button onclick="deleteFile({{ file.id }}, '{{ file.filename }}')" class="btn btn-danger btn-small">
🗑️ Delete
</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<div class="empty-state">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" />
</svg>
<h3>No files yet</h3>
<p>Upload your first file to get started</p>
</div>
{% endif %}
<!-- Upload Modal -->
<div id="uploadModal" class="modal">
<div class="modal-content">
<div class="modal-header">Upload File</div>
<form id="uploadForm" enctype="multipart/form-data">
<div class="form-group">
<label for="fileInput">Choose file</label>
<input type="file" id="fileInput" name="file" required>
</div>
<div class="form-actions">
<button type="button" onclick="closeUploadModal()" class="btn btn-secondary">Cancel</button>
<button type="submit" class="btn btn-primary">Upload</button>
</div>
</form>
<div id="uploadProgress" style="display: none; margin-top: 20px;">
<div style="background: #e5e7eb; height: 20px; border-radius: 10px; overflow: hidden;">
<div id="progressBar" style="background: #667eea; height: 100%; width: 0%; transition: width 0.3s;"></div>
</div>
<p style="text-align: center; margin-top: 10px; color: #6b7280;">Uploading...</p>
</div>
</div>
</div>
<!-- Share Modal -->
<div id="shareModal" class="modal">
<div class="modal-content">
<div class="modal-header">Share File</div>
<p style="margin-bottom: 15px; color: #6b7280;">Anyone with this link can download the file:</p>
<div class="share-link">
<input type="text" id="shareLink" readonly>
<button onclick="copyShareLink()" class="btn btn-primary btn-small">📋 Copy</button>
</div>
<div class="form-actions" style="margin-top: 20px;">
<button onclick="closeShareModal()" class="btn btn-secondary">Close</button>
</div>
</div>
</div>
{% endblock %}
{% block extra_scripts %}
<script>
function openUploadModal() {
document.getElementById('uploadModal').classList.add('active');
}
function closeUploadModal() {
document.getElementById('uploadModal').classList.remove('active');
document.getElementById('uploadForm').reset();
document.getElementById('uploadProgress').style.display = 'none';
}
document.getElementById('uploadForm').addEventListener('submit', async (e) => {
e.preventDefault();
const fileInput = document.getElementById('fileInput');
const file = fileInput.files[0];
if (!file) return;
const formData = new FormData();
formData.append('file', file);
document.getElementById('uploadProgress').style.display = 'block';
try {
const response = await fetch('/api/files/upload', {
method: 'POST',
body: formData
});
if (response.ok) {
window.location.reload();
} else {
const error = await response.json();
alert('Upload failed: ' + (error.detail || 'Unknown error'));
document.getElementById('uploadProgress').style.display = 'none';
}
} catch (error) {
alert('Upload failed: ' + error.message);
document.getElementById('uploadProgress').style.display = 'none';
}
});
async function downloadFile(fileId, filename) {
try {
const response = await fetch(`/api/files/${fileId}/download`);
if (response.ok) {
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
} else {
alert('Download failed');
}
} catch (error) {
alert('Download failed: ' + error.message);
}
}
function showShareLink(shareToken) {
const shareLink = window.location.origin + '/share/' + shareToken;
document.getElementById('shareLink').value = shareLink;
document.getElementById('shareModal').classList.add('active');
}
function closeShareModal() {
document.getElementById('shareModal').classList.remove('active');
}
function copyShareLink() {
const input = document.getElementById('shareLink');
input.select();
document.execCommand('copy');
alert('Link copied to clipboard!');
}
async function deleteFile(fileId, filename) {
if (!confirm(`Are you sure you want to delete "${filename}"?`)) {
return;
}
try {
const response = await fetch(`/api/files/${fileId}`, {
method: 'DELETE'
});
if (response.ok) {
window.location.reload();
} else {
alert('Delete failed');
}
} catch (error) {
alert('Delete failed: ' + error.message);
}
}
// Close modals when clicking outside
document.querySelectorAll('.modal').forEach(modal => {
modal.addEventListener('click', (e) => {
if (e.target === modal) {
modal.classList.remove('active');
}
});
});
</script>
{% endblock %}