first commit
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
# Git
|
||||
.git
|
||||
.gitignore
|
||||
|
||||
# Python
|
||||
__pycache__
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.pyd
|
||||
.Python
|
||||
env
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
.tox
|
||||
.coverage
|
||||
.coverage.*
|
||||
.pytest_cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.log
|
||||
.cache
|
||||
.mypy_cache
|
||||
|
||||
# Virtual environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
.DS_Store?
|
||||
._*
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
ehthumbs.db
|
||||
Thumbs.db
|
||||
|
||||
# Documentation
|
||||
README.md
|
||||
*.md
|
||||
|
||||
# Docker
|
||||
Dockerfile
|
||||
.dockerignore
|
||||
|
||||
# Temporary files
|
||||
*.tmp
|
||||
*.temp
|
||||
temp/
|
||||
tmp/
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
logs/
|
||||
|
||||
# Database files (if any)
|
||||
*.db
|
||||
*.sqlite
|
||||
*.sqlite3
|
||||
|
||||
# Backup files
|
||||
*.bak
|
||||
*.backup
|
||||
+165
@@ -0,0 +1,165 @@
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
pip-wheel-metadata/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
.python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# IDE specific files
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS specific files
|
||||
.DS_Store
|
||||
.DS_Store?
|
||||
._*
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
ehthumbs.db
|
||||
Thumbs.db
|
||||
|
||||
# Flask specific
|
||||
*.db
|
||||
*.sqlite
|
||||
*.sqlite3
|
||||
app.db
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
logs/
|
||||
|
||||
# Temporary files
|
||||
*.tmp
|
||||
*.temp
|
||||
temp/
|
||||
tmp/
|
||||
|
||||
# Backup files
|
||||
*.bak
|
||||
*.backup
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
# Use Python 3.11 slim image as base
|
||||
FROM python:3.11-slim
|
||||
|
||||
# Set working directory in container
|
||||
WORKDIR /app
|
||||
|
||||
# Set environment variables
|
||||
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||
PYTHONUNBUFFERED=1 \
|
||||
FLASK_APP=app.py \
|
||||
FLASK_ENV=production
|
||||
|
||||
# Install system dependencies
|
||||
RUN apt-get update && apt-get install -y \
|
||||
gcc \
|
||||
finger \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Copy requirements first to leverage Docker cache
|
||||
COPY requirements.txt .
|
||||
|
||||
# Install Python dependencies
|
||||
RUN pip install --no-cache-dir --upgrade pip && \
|
||||
pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# Copy application code
|
||||
COPY . .
|
||||
|
||||
# Create non-root user for security
|
||||
RUN adduser --disabled-password --gecos '' appuser && \
|
||||
chown -R appuser:appuser /app
|
||||
USER appuser
|
||||
|
||||
# Expose port
|
||||
EXPOSE 5000
|
||||
|
||||
# Health check
|
||||
HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 \
|
||||
CMD curl -f http://localhost:5000/ || exit 1
|
||||
|
||||
# Run the application
|
||||
CMD ["python", "app.py"]
|
||||
@@ -0,0 +1,307 @@
|
||||
# Finger Web Flask Application
|
||||
|
||||
A simple, modern Flask web application demonstrating basic web development concepts with clean code structure, responsive design, and best practices.
|
||||
|
||||
## 🚀 Features
|
||||
|
||||
- **Multiple Routes**: Home, About, Contact pages with clean navigation
|
||||
- **Contact Form**: Functional contact form with validation and flash messages
|
||||
- **JSON API**: RESTful API endpoints for data exchange
|
||||
- **Responsive Design**: Mobile-first design using Bootstrap 5
|
||||
- **Error Handling**: Custom 404 and 500 error pages
|
||||
- **Modern UI**: Clean, professional interface with animations
|
||||
- **Form Validation**: Client-side and server-side validation
|
||||
- **Configuration Management**: Environment-based configuration
|
||||
|
||||
## 📁 Project Structure
|
||||
|
||||
```
|
||||
finger-web/
|
||||
├── app.py # Main Flask application
|
||||
├── config.py # Configuration settings
|
||||
├── requirements.txt # Python dependencies
|
||||
├── README.md # Project documentation
|
||||
├── .gitignore # Git ignore file
|
||||
├── Dockerfile # Docker container configuration
|
||||
├── .dockerignore # Docker ignore file
|
||||
├── docker-compose.yml # Docker Compose configuration
|
||||
├── templates/ # HTML templates
|
||||
│ ├── base.html # Base template with navigation
|
||||
│ ├── index.html # Home page
|
||||
│ ├── about.html # About page
|
||||
│ ├── contact.html # Contact form
|
||||
│ ├── 404.html # 404 error page
|
||||
│ └── 500.html # 500 error page
|
||||
└── static/ # Static assets
|
||||
├── css/
|
||||
│ └── style.css # Custom styles
|
||||
└── js/
|
||||
└── main.js # JavaScript functionality
|
||||
```
|
||||
|
||||
## 🛠️ Technologies Used
|
||||
|
||||
### Backend
|
||||
- **Python 3.x** - Programming language
|
||||
- **Flask 2.3.3** - Web framework
|
||||
- **Jinja2** - Template engine
|
||||
- **Werkzeug** - WSGI toolkit
|
||||
|
||||
### Frontend
|
||||
- **HTML5** - Markup language
|
||||
- **CSS3** - Styling with custom animations
|
||||
- **JavaScript (ES6+)** - Interactive functionality
|
||||
- **Bootstrap 5.3** - CSS framework for responsive design
|
||||
|
||||
## 📋 Prerequisites
|
||||
|
||||
- Python 3.7 or higher
|
||||
- pip (Python package installer)
|
||||
- Virtual environment (recommended)
|
||||
|
||||
## 🚀 Installation & Setup
|
||||
|
||||
### 1. Clone or Download the Project
|
||||
|
||||
```bash
|
||||
# If using git
|
||||
git clone <repository-url>
|
||||
cd finger-web
|
||||
|
||||
# Or download and extract the project files
|
||||
```
|
||||
|
||||
### 2. Create Virtual Environment (Recommended)
|
||||
|
||||
```bash
|
||||
# Create virtual environment
|
||||
python -m venv venv
|
||||
|
||||
# Activate virtual environment
|
||||
# On Windows:
|
||||
venv\Scripts\activate
|
||||
# On macOS/Linux:
|
||||
source venv/bin/activate
|
||||
```
|
||||
|
||||
### 3. Install Dependencies
|
||||
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### 4. Run the Application
|
||||
|
||||
```bash
|
||||
python app.py
|
||||
```
|
||||
|
||||
The application will start on `http://localhost:5000`
|
||||
|
||||
## 🌐 Available Routes
|
||||
|
||||
| Route | Method | Description |
|
||||
|-------|--------|-------------|
|
||||
| `/` | GET | Home page |
|
||||
| `/about` | GET | About page |
|
||||
| `/contact` | GET, POST | Contact form |
|
||||
| `/api/hello` | GET | Simple JSON API endpoint |
|
||||
| `/api/info` | GET | Application information API |
|
||||
|
||||
## 🔧 Configuration
|
||||
|
||||
The application uses environment variables for configuration. You can set these in your environment or create a `.env` file:
|
||||
|
||||
```bash
|
||||
# Flask Configuration
|
||||
FLASK_DEBUG=True
|
||||
SECRET_KEY=your-secret-key-here
|
||||
|
||||
# Database (for future use)
|
||||
DATABASE_URL=sqlite:///app.db
|
||||
|
||||
# Mail Configuration (for future use)
|
||||
MAIL_SERVER=smtp.gmail.com
|
||||
MAIL_PORT=587
|
||||
MAIL_USE_TLS=True
|
||||
MAIL_USERNAME=[email protected]
|
||||
MAIL_PASSWORD=your-app-password
|
||||
```
|
||||
|
||||
## 📱 API Endpoints
|
||||
|
||||
### GET /api/hello
|
||||
Returns a simple greeting message.
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"message": "Hello from Flask API!",
|
||||
"status": "success",
|
||||
"version": "1.0"
|
||||
}
|
||||
```
|
||||
|
||||
### GET /api/info
|
||||
Returns application information and available routes.
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"app_name": "Finger Web Flask App",
|
||||
"routes": ["/", "/about", "/contact", "/api/hello", "/api/info"],
|
||||
"framework": "Flask"
|
||||
}
|
||||
```
|
||||
|
||||
## 🎨 Customization
|
||||
|
||||
### Styling
|
||||
- Edit `static/css/style.css` to customize the appearance
|
||||
- The app uses Bootstrap 5 classes for responsive design
|
||||
- Custom CSS variables and animations are included
|
||||
|
||||
### JavaScript
|
||||
- Modify `static/js/main.js` for additional functionality
|
||||
- Includes form validation, animations, and keyboard shortcuts
|
||||
- API helper functions are available
|
||||
|
||||
### Templates
|
||||
- All HTML templates extend `templates/base.html`
|
||||
- Use Jinja2 template syntax for dynamic content
|
||||
- Bootstrap components are readily available
|
||||
|
||||
## 🔍 Features in Detail
|
||||
|
||||
### Contact Form
|
||||
- Client-side validation with real-time feedback
|
||||
- Server-side validation and sanitization
|
||||
- Flash messages for user feedback
|
||||
- Form submission with loading states
|
||||
|
||||
### Responsive Design
|
||||
- Mobile-first approach
|
||||
- Bootstrap grid system
|
||||
- Custom breakpoints and animations
|
||||
- Touch-friendly interface
|
||||
|
||||
### Error Handling
|
||||
- Custom 404 and 500 error pages
|
||||
- Graceful error handling in routes
|
||||
- User-friendly error messages
|
||||
|
||||
### JavaScript Features
|
||||
- Form validation and enhancement
|
||||
- Smooth scrolling navigation
|
||||
- Card animations on scroll
|
||||
- Keyboard shortcuts (Alt+H, Alt+A, Alt+C)
|
||||
- API interaction helpers
|
||||
|
||||
## 🚀 Deployment
|
||||
|
||||
### Development
|
||||
```bash
|
||||
python app.py
|
||||
```
|
||||
|
||||
### Docker Deployment
|
||||
|
||||
#### Option 1: Using Docker directly
|
||||
```bash
|
||||
# Build the Docker image
|
||||
docker build -t finger-web .
|
||||
|
||||
# Run the container
|
||||
docker run -d -p 5000:5000 --name finger-web-app finger-web
|
||||
```
|
||||
|
||||
#### Option 2: Using Docker Compose (Recommended)
|
||||
```bash
|
||||
# Build and start the application
|
||||
docker-compose up -d
|
||||
|
||||
# View logs
|
||||
docker-compose logs -f
|
||||
|
||||
# Stop the application
|
||||
docker-compose down
|
||||
```
|
||||
|
||||
#### Docker Commands
|
||||
```bash
|
||||
# Build image
|
||||
docker build -t finger-web .
|
||||
|
||||
# Run container with environment variables
|
||||
docker run -d \
|
||||
-p 5000:5000 \
|
||||
-e FLASK_ENV=production \
|
||||
-e SECRET_KEY=your-secret-key \
|
||||
--name finger-web-app \
|
||||
finger-web
|
||||
|
||||
# View container logs
|
||||
docker logs finger-web-app
|
||||
|
||||
# Stop and remove container
|
||||
docker stop finger-web-app
|
||||
docker rm finger-web-app
|
||||
```
|
||||
|
||||
### Production
|
||||
For production deployment, consider using:
|
||||
- **Docker** for containerization (included)
|
||||
- **Docker Compose** for orchestration (included)
|
||||
- **Gunicorn** as WSGI server
|
||||
- **Nginx** as reverse proxy
|
||||
- **Heroku**, **DigitalOcean**, or **AWS** for hosting
|
||||
|
||||
Example with Gunicorn:
|
||||
```bash
|
||||
pip install gunicorn
|
||||
gunicorn -w 4 -b 0.0.0.0:5000 app:app
|
||||
```
|
||||
|
||||
## 🔒 Security Considerations
|
||||
|
||||
- Change the `SECRET_KEY` in production
|
||||
- Use environment variables for sensitive data
|
||||
- Enable HTTPS in production
|
||||
- Implement rate limiting for forms
|
||||
- Validate and sanitize all user inputs
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
1. Fork the project
|
||||
2. Create a feature branch
|
||||
3. Make your changes
|
||||
4. Test thoroughly
|
||||
5. Submit a pull request
|
||||
|
||||
## 📝 License
|
||||
|
||||
This project is open source and available under the [MIT License](LICENSE).
|
||||
|
||||
## 📞 Support
|
||||
|
||||
If you encounter any issues or have questions:
|
||||
1. Check the existing documentation
|
||||
2. Review the code comments
|
||||
3. Test in a clean virtual environment
|
||||
4. Create an issue with detailed information
|
||||
|
||||
## 🎯 Future Enhancements
|
||||
|
||||
- Database integration with SQLAlchemy
|
||||
- User authentication and sessions
|
||||
- Email functionality for contact form
|
||||
- Admin dashboard
|
||||
- API rate limiting
|
||||
- Unit tests
|
||||
- CI/CD pipeline
|
||||
- Kubernetes deployment manifests
|
||||
- Monitoring and logging integration
|
||||
|
||||
---
|
||||
|
||||
**Built with ❤️ using Flask and Bootstrap**
|
||||
@@ -0,0 +1,136 @@
|
||||
from flask import Flask, render_template, request, jsonify, redirect, url_for, flash
|
||||
import os
|
||||
import subprocess
|
||||
import shlex
|
||||
from config import Config
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config.from_object(Config)
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
"""Home page route"""
|
||||
return render_template('index.html', title='Home')
|
||||
|
||||
@app.route('/finger', methods=['GET', 'POST'])
|
||||
def finger():
|
||||
"""Finger command route"""
|
||||
result = None
|
||||
error = None
|
||||
username = request.args.get('user', '') or request.form.get('username', '')
|
||||
|
||||
if request.method == 'POST' or username:
|
||||
try:
|
||||
# Sanitize username input
|
||||
if username:
|
||||
# Allow alphanumeric characters, dots, hyphens, underscores, and @ symbol for email-style usernames
|
||||
if not all(c.isalnum() or c in '.-_@' for c in username):
|
||||
error = "Invalid username format. Only alphanumeric characters, dots, hyphens, underscores, and @ symbol are allowed."
|
||||
else:
|
||||
# Execute finger command with specific user
|
||||
cmd = ['finger', username]
|
||||
else:
|
||||
# Execute finger command without user (show all logged in users)
|
||||
cmd = ['finger']
|
||||
|
||||
if not error:
|
||||
# Execute the command safely
|
||||
process = subprocess.run(
|
||||
cmd,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10
|
||||
)
|
||||
|
||||
if process.returncode == 0:
|
||||
result = process.stdout
|
||||
if not result.strip():
|
||||
result = "No information available."
|
||||
else:
|
||||
error = process.stderr or "Finger command failed."
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
error = "Command timed out. Please try again."
|
||||
except FileNotFoundError:
|
||||
error = "Finger command not available on this system."
|
||||
except Exception as e:
|
||||
error = f"An error occurred: {str(e)}"
|
||||
|
||||
return render_template('finger.html', title='Finger', result=result, error=error, username=username)
|
||||
|
||||
@app.route('/finger/<path:username>')
|
||||
def finger_direct(username):
|
||||
"""Direct finger command route with username in URL"""
|
||||
result = None
|
||||
error = None
|
||||
|
||||
try:
|
||||
# Sanitize username input
|
||||
if username:
|
||||
# Allow alphanumeric characters, dots, hyphens, underscores, and @ symbol for email-style usernames
|
||||
if not all(c.isalnum() or c in '.-_@' for c in username):
|
||||
error = "Invalid username format. Only alphanumeric characters, dots, hyphens, underscores, and @ symbol are allowed."
|
||||
else:
|
||||
# Execute finger command with specific user
|
||||
cmd = ['finger', username]
|
||||
|
||||
# Execute the command safely
|
||||
process = subprocess.run(
|
||||
cmd,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10
|
||||
)
|
||||
|
||||
if process.returncode == 0:
|
||||
result = process.stdout
|
||||
if not result.strip():
|
||||
result = "No information available."
|
||||
else:
|
||||
error = process.stderr or "Finger command failed."
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
error = "Command timed out. Please try again."
|
||||
except FileNotFoundError:
|
||||
error = "Finger command not available on this system."
|
||||
except Exception as e:
|
||||
error = f"An error occurred: {str(e)}"
|
||||
|
||||
return render_template('finger.html', title=f'Finger - {username}', result=result, error=error, username=username)
|
||||
|
||||
@app.route('/api/hello')
|
||||
def api_hello():
|
||||
"""Simple JSON API endpoint"""
|
||||
return jsonify({
|
||||
'message': 'Hello from Flask API!',
|
||||
'status': 'success',
|
||||
'version': '1.0'
|
||||
})
|
||||
|
||||
@app.route('/api/info')
|
||||
def api_info():
|
||||
"""API info endpoint"""
|
||||
return jsonify({
|
||||
'app_name': 'Finger Web Flask App',
|
||||
'routes': [
|
||||
'/',
|
||||
'/finger',
|
||||
'/finger/<username>',
|
||||
'/api/hello',
|
||||
'/api/info'
|
||||
],
|
||||
'framework': 'Flask'
|
||||
})
|
||||
|
||||
@app.errorhandler(404)
|
||||
def not_found_error(error):
|
||||
"""Handle 404 errors"""
|
||||
return render_template('404.html', title='Page Not Found'), 404
|
||||
|
||||
@app.errorhandler(500)
|
||||
def internal_error(error):
|
||||
"""Handle 500 errors"""
|
||||
return render_template('500.html', title='Server Error'), 500
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(debug=True, host='0.0.0.0', port=5000)
|
||||
@@ -0,0 +1,17 @@
|
||||
import os
|
||||
|
||||
class Config:
|
||||
"""Flask configuration class"""
|
||||
SECRET_KEY = os.environ.get('SECRET_KEY') or 'dev-secret-key-change-in-production'
|
||||
DEBUG = os.environ.get('FLASK_DEBUG', 'True').lower() == 'true'
|
||||
|
||||
# Database configuration (for future use)
|
||||
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or 'sqlite:///app.db'
|
||||
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
||||
|
||||
# Mail configuration (for future use)
|
||||
MAIL_SERVER = os.environ.get('MAIL_SERVER')
|
||||
MAIL_PORT = int(os.environ.get('MAIL_PORT') or 587)
|
||||
MAIL_USE_TLS = os.environ.get('MAIL_USE_TLS', 'true').lower() in ['true', 'on', '1']
|
||||
MAIL_USERNAME = os.environ.get('MAIL_USERNAME')
|
||||
MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD')
|
||||
@@ -0,0 +1,49 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
finger-web:
|
||||
build: .
|
||||
container_name: finger-web-app
|
||||
ports:
|
||||
- "5000:5000"
|
||||
environment:
|
||||
- FLASK_ENV=production
|
||||
- FLASK_DEBUG=False
|
||||
- SECRET_KEY=your-production-secret-key-here
|
||||
volumes:
|
||||
# Mount logs directory for persistence (optional)
|
||||
- ./logs:/app/logs
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:5000/"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 40s
|
||||
networks:
|
||||
- finger-web-network
|
||||
|
||||
# Optional: Add nginx reverse proxy for production
|
||||
# nginx:
|
||||
# image: nginx:alpine
|
||||
# container_name: finger-web-nginx
|
||||
# ports:
|
||||
# - "80:80"
|
||||
# - "443:443"
|
||||
# volumes:
|
||||
# - ./nginx.conf:/etc/nginx/nginx.conf:ro
|
||||
# - ./ssl:/etc/nginx/ssl:ro
|
||||
# depends_on:
|
||||
# - finger-web
|
||||
# restart: unless-stopped
|
||||
# networks:
|
||||
# - finger-web-network
|
||||
|
||||
networks:
|
||||
finger-web-network:
|
||||
driver: bridge
|
||||
|
||||
# Optional: Add volumes for data persistence
|
||||
# volumes:
|
||||
# finger-web-data:
|
||||
# driver: local
|
||||
@@ -0,0 +1,7 @@
|
||||
Flask==2.3.3
|
||||
Werkzeug==2.3.7
|
||||
Jinja2==3.1.2
|
||||
MarkupSafe==2.1.3
|
||||
itsdangerous==2.1.2
|
||||
click==8.1.7
|
||||
blinker==1.6.3
|
||||
@@ -0,0 +1,205 @@
|
||||
/* Custom styles for Finger Web Flask App */
|
||||
|
||||
/* Global styles */
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* Navigation enhancements */
|
||||
.navbar-brand {
|
||||
font-weight: bold;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.navbar-nav .nav-link {
|
||||
font-weight: 500;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
.navbar-nav .nav-link:hover {
|
||||
color: #fff !important;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Card enhancements */
|
||||
.card {
|
||||
border: none;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
/* Jumbotron styling */
|
||||
.jumbotron {
|
||||
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
|
||||
border: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
/* Button enhancements */
|
||||
.btn {
|
||||
border-radius: 6px;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(45deg, #007bff, #0056b3);
|
||||
border: none;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: linear-gradient(45deg, #0056b3, #004085);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.btn-outline-primary:hover {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* Form enhancements */
|
||||
.form-control {
|
||||
border-radius: 6px;
|
||||
border: 2px solid #e9ecef;
|
||||
transition: border-color 0.3s ease, box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
border-color: #007bff;
|
||||
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
|
||||
}
|
||||
|
||||
/* Alert enhancements */
|
||||
.alert {
|
||||
border-radius: 8px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.alert-success {
|
||||
background: linear-gradient(45deg, #d4edda, #c3e6cb);
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.alert-danger {
|
||||
background: linear-gradient(45deg, #f8d7da, #f5c6cb);
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
/* Footer styling */
|
||||
footer {
|
||||
margin-top: auto;
|
||||
border-top: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
/* Code block styling */
|
||||
pre {
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
pre code {
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.jumbotron {
|
||||
padding: 2rem 1rem;
|
||||
}
|
||||
|
||||
.display-4 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Animation for page load */
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
main {
|
||||
animation: fadeIn 0.6s ease-out;
|
||||
}
|
||||
|
||||
/* Custom utility classes */
|
||||
.text-gradient {
|
||||
background: linear-gradient(45deg, #007bff, #6610f2);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.shadow-custom {
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* Loading animation for forms */
|
||||
.btn.loading {
|
||||
position: relative;
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
.btn.loading::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
margin-left: -8px;
|
||||
margin-top: -8px;
|
||||
border: 2px solid #ffffff;
|
||||
border-radius: 50%;
|
||||
border-top-color: transparent;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* Finger command specific styles */
|
||||
.finger-output pre {
|
||||
font-family: 'Courier New', Consolas, monospace;
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.4;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.finger-output pre::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.finger-output pre::-webkit-scrollbar-track {
|
||||
background: #2d3748;
|
||||
}
|
||||
|
||||
.finger-output pre::-webkit-scrollbar-thumb {
|
||||
background: #4a5568;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.finger-output pre::-webkit-scrollbar-thumb:hover {
|
||||
background: #718096;
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
// Main JavaScript file for Finger Web Flask App
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Initialize all functionality when DOM is loaded
|
||||
initializeFormValidation();
|
||||
initializeNavigation();
|
||||
initializeAnimations();
|
||||
initializeTooltips();
|
||||
});
|
||||
|
||||
// Form validation and enhancement
|
||||
function initializeFormValidation() {
|
||||
const forms = document.querySelectorAll('form');
|
||||
|
||||
forms.forEach(form => {
|
||||
form.addEventListener('submit', function(event) {
|
||||
if (!form.checkValidity()) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
} else {
|
||||
// Add loading state to submit button
|
||||
const submitBtn = form.querySelector('button[type="submit"]');
|
||||
if (submitBtn) {
|
||||
submitBtn.classList.add('loading');
|
||||
submitBtn.disabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
form.classList.add('was-validated');
|
||||
});
|
||||
|
||||
// Real-time validation feedback
|
||||
const inputs = form.querySelectorAll('input, textarea');
|
||||
inputs.forEach(input => {
|
||||
input.addEventListener('blur', function() {
|
||||
if (this.checkValidity()) {
|
||||
this.classList.remove('is-invalid');
|
||||
this.classList.add('is-valid');
|
||||
} else {
|
||||
this.classList.remove('is-valid');
|
||||
this.classList.add('is-invalid');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Navigation enhancements
|
||||
function initializeNavigation() {
|
||||
// Highlight current page in navigation
|
||||
const currentPath = window.location.pathname;
|
||||
const navLinks = document.querySelectorAll('.navbar-nav .nav-link');
|
||||
|
||||
navLinks.forEach(link => {
|
||||
if (link.getAttribute('href') === currentPath) {
|
||||
link.classList.add('active');
|
||||
}
|
||||
});
|
||||
|
||||
// Smooth scrolling for anchor links
|
||||
const anchorLinks = document.querySelectorAll('a[href^="#"]');
|
||||
anchorLinks.forEach(link => {
|
||||
link.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
const target = document.querySelector(this.getAttribute('href'));
|
||||
if (target) {
|
||||
target.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'start'
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Animation and visual enhancements
|
||||
function initializeAnimations() {
|
||||
// Fade in cards on scroll
|
||||
const observerOptions = {
|
||||
threshold: 0.1,
|
||||
rootMargin: '0px 0px -50px 0px'
|
||||
};
|
||||
|
||||
const observer = new IntersectionObserver(function(entries) {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
entry.target.style.opacity = '1';
|
||||
entry.target.style.transform = 'translateY(0)';
|
||||
}
|
||||
});
|
||||
}, observerOptions);
|
||||
|
||||
// Observe all cards
|
||||
const cards = document.querySelectorAll('.card');
|
||||
cards.forEach(card => {
|
||||
card.style.opacity = '0';
|
||||
card.style.transform = 'translateY(20px)';
|
||||
card.style.transition = 'opacity 0.6s ease, transform 0.6s ease';
|
||||
observer.observe(card);
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize Bootstrap tooltips
|
||||
function initializeTooltips() {
|
||||
// Enable tooltips if Bootstrap is available
|
||||
if (typeof bootstrap !== 'undefined') {
|
||||
const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
|
||||
tooltipTriggerList.map(function(tooltipTriggerEl) {
|
||||
return new bootstrap.Tooltip(tooltipTriggerEl);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Utility functions
|
||||
function showNotification(message, type = 'info') {
|
||||
// Create a notification element
|
||||
const notification = document.createElement('div');
|
||||
notification.className = `alert alert-${type} alert-dismissible fade show position-fixed`;
|
||||
notification.style.cssText = 'top: 20px; right: 20px; z-index: 9999; min-width: 300px;';
|
||||
notification.innerHTML = `
|
||||
${message}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
`;
|
||||
|
||||
document.body.appendChild(notification);
|
||||
|
||||
// Auto-remove after 5 seconds
|
||||
setTimeout(() => {
|
||||
if (notification.parentNode) {
|
||||
notification.remove();
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
// API interaction helpers
|
||||
async function fetchAPI(endpoint) {
|
||||
try {
|
||||
const response = await fetch(endpoint);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('API fetch error:', error);
|
||||
showNotification('Failed to fetch data from API', 'danger');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Form submission with AJAX (optional enhancement)
|
||||
function submitFormAjax(form, successCallback) {
|
||||
const formData = new FormData(form);
|
||||
|
||||
fetch(form.action, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showNotification(data.message, 'success');
|
||||
if (successCallback) successCallback(data);
|
||||
} else {
|
||||
showNotification(data.message || 'An error occurred', 'danger');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Form submission error:', error);
|
||||
showNotification('Failed to submit form', 'danger');
|
||||
})
|
||||
.finally(() => {
|
||||
// Remove loading state
|
||||
const submitBtn = form.querySelector('button[type="submit"]');
|
||||
if (submitBtn) {
|
||||
submitBtn.classList.remove('loading');
|
||||
submitBtn.disabled = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Keyboard shortcuts
|
||||
document.addEventListener('keydown', function(e) {
|
||||
// Alt + H for Home
|
||||
if (e.altKey && e.key === 'h') {
|
||||
e.preventDefault();
|
||||
window.location.href = '/';
|
||||
}
|
||||
|
||||
// Alt + A for About
|
||||
if (e.altKey && e.key === 'a') {
|
||||
e.preventDefault();
|
||||
window.location.href = '/about';
|
||||
}
|
||||
|
||||
// Alt + F for Finger
|
||||
if (e.altKey && e.key === 'f') {
|
||||
e.preventDefault();
|
||||
window.location.href = '/finger';
|
||||
}
|
||||
});
|
||||
|
||||
// Console welcome message
|
||||
console.log('%c🚀 Finger Web Flask App', 'color: #007bff; font-size: 16px; font-weight: bold;');
|
||||
console.log('%cWelcome to the developer console!', 'color: #6c757d;');
|
||||
console.log('%cKeyboard shortcuts: Alt+H (Home), Alt+A (About), Alt+F (Finger)', 'color: #6c757d;');
|
||||
@@ -0,0 +1,33 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-lg-6 mx-auto text-center">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h1 class="display-1 text-muted">404</h1>
|
||||
<h2 class="mb-3">Page Not Found</h2>
|
||||
<p class="lead">
|
||||
Oops! The page you're looking for doesn't exist. It might have been moved, deleted, or you entered the wrong URL.
|
||||
</p>
|
||||
<hr class="my-4">
|
||||
<div class="d-grid gap-2 d-md-block">
|
||||
<a href="{{ url_for('index') }}" class="btn btn-primary">Go Home</a>
|
||||
<a href="javascript:history.back()" class="btn btn-outline-secondary">Go Back</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<h5>Popular Pages:</h5>
|
||||
<ul class="list-inline">
|
||||
<li class="list-inline-item">
|
||||
<a href="{{ url_for('index') }}" class="text-decoration-none">Home</a>
|
||||
</li>
|
||||
<li class="list-inline-item">•</li>
|
||||
<li class="list-inline-item"></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,28 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-lg-6 mx-auto text-center">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h1 class="display-1 text-danger">500</h1>
|
||||
<h2 class="mb-3">Internal Server Error</h2>
|
||||
<p class="lead">
|
||||
Something went wrong on our end. We're working to fix the issue. Please try again later.
|
||||
</p>
|
||||
<hr class="my-4">
|
||||
<div class="d-grid gap-2 d-md-block">
|
||||
<a href="{{ url_for('index') }}" class="btn btn-primary">Go Home</a>
|
||||
<a href="javascript:location.reload()" class="btn btn-outline-secondary">Try Again</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<div class="alert alert-info" role="alert">
|
||||
<strong>Need help?</strong> If this problem persists, please <a href="{{ url_for('contact') }}" class="alert-link">contact us</a> and let us know what you were trying to do.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,75 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% if title %}{{ title }} - {% endif %}Finger Web App</title>
|
||||
|
||||
<!-- Bootstrap CSS -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
|
||||
<!-- Font Awesome -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
|
||||
<!-- Custom CSS -->
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
|
||||
</head>
|
||||
<body>
|
||||
<!-- Navigation -->
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
|
||||
<div class="container">
|
||||
<a class="navbar-brand" href="{{ url_for('index') }}">Finger Web</a>
|
||||
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<ul class="navbar-nav ms-auto">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('index') }}">Home</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('finger') }}">Finger</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('api_hello') }}" target="_blank">API</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Flash Messages -->
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
<div class="container mt-3">
|
||||
{% for category, message in messages %}
|
||||
<div class="alert alert-{{ 'danger' if category == 'error' else 'success' }} alert-dismissible fade show" role="alert">
|
||||
{{ message }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="container my-4">
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="bg-light text-center text-muted py-3 mt-5">
|
||||
<div class="container">
|
||||
<p>© 2025 Finger Web App. Built with Flask and Bootstrap.</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<!-- Bootstrap JS -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
<!-- Custom JS -->
|
||||
<script src="{{ url_for('static', filename='js/main.js') }}"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,115 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-lg-10 mx-auto">
|
||||
<h1 class="mb-4">🔍 Finger Command</h1>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">User Lookup</h5>
|
||||
<form method="POST" action="{{ url_for('finger') }}">
|
||||
<div class="mb-3">
|
||||
<label for="username" class="form-label">Username (optional)</label>
|
||||
<input type="text" class="form-control" id="username" name="username"
|
||||
value="{{ username or '' }}"
|
||||
placeholder="Enter username or email (e.g., [email protected])"
|
||||
pattern="[a-zA-Z0-9.\-_@]*"
|
||||
title="Alphanumeric characters, dots, hyphens, underscores, and @ symbol allowed">
|
||||
<div class="form-text">
|
||||
Leave empty to show all logged-in users, or enter a username or email address.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fas fa-search"></i> Run Finger
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary ms-2" onclick="clearForm()">
|
||||
Clear
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mt-3">
|
||||
<div class="card-body">
|
||||
<h6 class="card-title">ℹ️ About Finger</h6>
|
||||
<p class="card-text small">
|
||||
The finger command displays information about users on the system, including:
|
||||
</p>
|
||||
<ul class="small">
|
||||
<li>Login name and real name</li>
|
||||
<li>Terminal and login time</li>
|
||||
<li>Idle time</li>
|
||||
<li>Home directory and shell</li>
|
||||
<li>Plan and project files (if available)</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-8">
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0">
|
||||
{% if username %}
|
||||
Results for "{{ username }}"
|
||||
{% else %}
|
||||
System Users
|
||||
{% endif %}
|
||||
</h5>
|
||||
{% if result or error %}
|
||||
<small class="text-muted">
|
||||
{{ moment().format('YYYY-MM-DD HH:mm:ss') if moment else '' }}
|
||||
</small>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if error %}
|
||||
<div class="alert alert-danger" role="alert">
|
||||
<i class="fas fa-exclamation-triangle"></i>
|
||||
<strong>Error:</strong> {{ error }}
|
||||
</div>
|
||||
{% elif result %}
|
||||
<div class="alert alert-success mb-3" role="alert">
|
||||
<i class="fas fa-check-circle"></i>
|
||||
Command executed successfully
|
||||
</div>
|
||||
<div class="finger-output">
|
||||
<pre class="rounded"><code>{{ result }}</code></pre>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-center text-muted py-5">
|
||||
<i class="fas fa-terminal fa-3x mb-3"></i>
|
||||
<p>Enter a username and click "Run Finger" to see user information,<br>
|
||||
or leave username empty to see all logged-in users.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-center mt-4">
|
||||
<a href="{{ url_for('index') }}" class="btn btn-outline-primary">
|
||||
<i class="fas fa-home"></i> Back to Home
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function clearForm() {
|
||||
document.getElementById('username').value = '';
|
||||
// Optionally reload the page to clear results
|
||||
window.location.href = "{{ url_for('finger') }}";
|
||||
}
|
||||
|
||||
// Auto-focus on username input when page loads
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
document.getElementById('username').focus();
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,17 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-lg-8 mx-auto">
|
||||
<div class="jumbotron bg-light p-5 rounded">
|
||||
<h1 class="display-4">Welcome to Finger Web!</h1>
|
||||
<p class="lead">A simple web interface to the unix finger command.</p>
|
||||
<hr class="my-4">
|
||||
<p><a href="https://en.wikipedia.org/wiki/Finger_(protocol)">Finger</a> is a utility from 1971 written to discover user information on remote unix systems. While it's use has fallen off, you can sometimes still find active finger servers where users provide information about themselves.</p><p>See <a href="/finger/[email protected]">[email protected]</a> as an example :)</p>
|
||||
<hl>
|
||||
<p>This site was written by <a href="https://yttrx.com/@waffles">waffles</a> but inspired by <a href="https://benbrown.com">Ben Brown's</a> <a href="https://happynetbox.com/">happy net box</a> project.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user