first commit

This commit is contained in:
pmb
2025-06-26 12:44:28 -07:00
commit b672b249a6
15 changed files with 1476 additions and 0 deletions
+75
View File
@@ -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
View File
@@ -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
View File
@@ -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"]
+307
View File
@@ -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**
+136
View File
@@ -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)
+17
View File
@@ -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')
+49
View File
@@ -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
+7
View File
@@ -0,0 +1,7 @@
Flask==2.3.3
Werkzeug==2.3.7
Jinja2==3.1.2
MarkupSafe==2.1.3
itsdangerous==2.1.2
click==8.1.7
blinker==1.6.3
+205
View File
@@ -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;
}
+205
View File
@@ -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;');
+33
View File
@@ -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 %}
+28
View File
@@ -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 %}
+75
View File
@@ -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>&copy; 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>
+115
View File
@@ -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 %}
+17
View File
@@ -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 %}