re-Made server totally - version 1.0 now come.
This commit is contained in:
parent
74c658b2dd
commit
738fb9f179
9
.env.exampl
Normal file
9
.env.exampl
Normal 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
367
README.md
Normal 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:=-_
|
||||||
@ -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()
|
||||||
|
|||||||
@ -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'
|
|
||||||
|
|||||||
64
css/main.css
64
css/main.css
@ -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
38
docker-compose.yaml
Normal 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
24
dockerfile
Normal 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"]
|
||||||
@ -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>
|
|
||||||
171
js/main.js
171
js/main.js
@ -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)}) <a href="/dn/${file.file_id}" title="Direct link">⬇</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();
|
|
||||||
@ -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
72
setup.sh
Normal 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
266
templates/admin.html
Normal 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
334
templates/base.html
Normal 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
139
templates/login.html
Normal 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
134
templates/settings.html
Normal 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
228
templates/user_files.html
Normal 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 %}
|
||||||
Loading…
x
Reference in New Issue
Block a user