Refactor init and readme
This commit is contained in:
parent
194311ff92
commit
ab7757cf46
6 changed files with 226 additions and 301 deletions
11
.env.dev
Normal file
11
.env.dev
Normal file
|
@ -0,0 +1,11 @@
|
|||
# .env for development
|
||||
|
||||
SECRET_KEY=123456
|
||||
FLASK_ENV=development
|
||||
|
||||
DATABASE_URL=sqlite:///anonchat.db
|
||||
SESSION_TYPE=filesystem
|
||||
RATELIMIT_STORAGE_URL=memory://
|
||||
|
||||
ADMIN_USERNAME=admin
|
||||
ADMIN_PASSWORD=admin
|
295
README.md
295
README.md
|
@ -1,229 +1,122 @@
|
|||
# AnonChat
|
||||
# anonchat
|
||||
|
||||
An anonymous chat application built with Flask.
|
||||
A simple Flask-based web application for anonymous inquiries or messages, designed for easy deployment and management.
|
||||
|
||||
## Features
|
||||
|
||||
- Anonymous inquiries and messaging
|
||||
- Admin dashboard to manage inquiries
|
||||
- Customizable site title
|
||||
- Redis-based session storage for improved scalability
|
||||
- Integrated error tracking with Sentry
|
||||
* **Anonymous Inquiries:** Allows users to submit inquiries without revealing their identity.
|
||||
* **Admin Interface:** Secure area for administrators to view and manage inquiries.
|
||||
* **Webhook Notifications:** Optionally sends notifications to a specified URL upon new inquiry submissions.
|
||||
* **Automatic Data Deletion:** Periodically deletes inquiries older than a configurable duration (default: 48 hours) to maintain privacy.
|
||||
* **Rate Limiting:** Protects against abuse by limiting the number of requests from a single IP address.
|
||||
* **CSRF Protection:** Implemented using Flask-WTF to prevent cross-site request forgery attacks.
|
||||
* **Configurable:** Easily configured using environment variables.
|
||||
* **Containerized:** Ready for deployment using Docker and Docker Compose.
|
||||
* **Database Migrations:** Simple migration system managed via Flask CLI commands.
|
||||
* **Optional Sentry Integration:** Supports error tracking with Sentry.
|
||||
|
||||
## Development Approach
|
||||
## Technology Stack
|
||||
|
||||
AnonChat was created using "vibe coding" - a programming approach where developers leverage AI tools to generate code through natural language prompts rather than writing code manually. This modern development method allows focusing on high-level problem-solving and design while letting AI handle implementation details.
|
||||
|
||||
Rest assured though, I know what I'm (or the AI is) doing. Here's what would happen if I didn't:
|
||||
1. [my saas was built with Cursor, zero hand written code \
|
||||
AI is no longer just an assistant, it's also the builder \
|
||||
Now, you can continue to whine about it or start building.](https://xcancel.com/leojr94_/status/1900767509621674109)
|
||||
2. [random thing are happening, maxed out usage on api keys, people bypassing the subscription, creating random shit on db \
|
||||
there are just some weird ppl out there](https://xcancel.com/leojr94_/status/1901560276488511759)
|
||||
* **Backend:** Python 3.11+, Flask
|
||||
* **Database:** PostgreSQL (recommended) or SQLite (default) via Flask-SQLAlchemy
|
||||
* **Session Management:** Redis (recommended) or Filesystem via Flask-Session
|
||||
* **Rate Limiting:** Redis via Flask-Limiter
|
||||
* **Task Scheduling:** Flask-APScheduler
|
||||
* **Password Hashing:** Argon2 via argon2-cffi
|
||||
* **WSGI Server:** Gunicorn
|
||||
* **Dependency Management:** Poetry
|
||||
* **Containerization:** Docker, Docker Compose
|
||||
* **Deployment:** Configured for Fly.io (optional)
|
||||
|
||||
## Configuration
|
||||
|
||||
AnonChat can be configured using environment variables:
|
||||
The application is configured via environment variables. You can set these in a `.env` file in the project root or directly in your deployment environment.
|
||||
|
||||
- `SECRET_KEY`: Secret key for session management
|
||||
- `DATABASE_URL`: Database connection string (defaults to SQLite)
|
||||
- `ADMIN_USERNAME`: Admin username for admin dashboard
|
||||
- `ADMIN_PASSWORD`: Admin password for admin dashboard
|
||||
- `ADMIN_FORCE_RESET`: When set to "true", forces a reset of the admin password to the value in ADMIN_PASSWORD (defaults to "false")
|
||||
- `SITE_TITLE`: Customizable site title (defaults to "AnonChat")
|
||||
- `BEHIND_PROXY`: Set to "true" when running behind a reverse proxy to properly handle client IP addresses (defaults to "false")
|
||||
- `RATELIMIT_STORAGE_URL`: Storage backend for rate limiting (defaults to memory storage)
|
||||
- `REDIS_URL`: Redis connection URL for session storage (defaults to "redis://localhost:6379/0")
|
||||
- `AUTO_DELETE_HOURS`: Number of hours after which closed inquiries are automatically deleted (defaults to 48)
|
||||
- `SENTRY_DSN`: Sentry Data Source Name for error tracking and monitoring (optional)
|
||||
**Required:**
|
||||
|
||||
You can set these variables in a `.env` file:
|
||||
* `SECRET_KEY`: A long, random string used for session signing and security. Generate one using `python -c 'import secrets; print(secrets.token_hex(24))'`.
|
||||
* `DATABASE_URL`: The connection string for your database (e.g., `postgresql://user:password@host:port/dbname` or `sqlite:///instance/anonchat.db`).
|
||||
* `REDIS_URL`: The connection string for your Redis instance (e.g., `redis://localhost:6379/0`). Used for sessions (if `SESSION_TYPE=redis`) and rate limiting.
|
||||
* `ADMIN_PASSWORD`: The password for the admin user. Set this on first run or when needing to reset.
|
||||
|
||||
```
|
||||
SECRET_KEY=your_secret_key_here
|
||||
FLASK_APP=src/anonchat
|
||||
FLASK_ENV=development
|
||||
SITE_TITLE=My Custom Chat
|
||||
BEHIND_PROXY=true
|
||||
REDIS_URL=redis://redis:6379/0
|
||||
AUTO_DELETE_HOURS=72
|
||||
SENTRY_DSN=https://your-sentry-dsn
|
||||
**Optional:**
|
||||
|
||||
* `SITE_TITLE`: The title displayed on the website (default: `AnonChat`).
|
||||
* `WEBHOOK_ENABLED`: Set to `true` to enable webhook notifications (default: `false`).
|
||||
* `WEBHOOK_URL`: The URL to send webhook notifications to.
|
||||
* `WEBHOOK_SECRET`: A secret key to sign webhook payloads (recommended if webhooks are enabled).
|
||||
* `ADMIN_USERNAME`: The username for the admin user (default: `admin`).
|
||||
* `ADMIN_FORCE_RESET`: Set to `true` to force reset the admin password on startup using `ADMIN_PASSWORD`.
|
||||
* `AUTO_DELETE_HOURS`: The number of hours after which inquiries are automatically deleted (default: `48`).
|
||||
* `RATELIMIT_STORAGE_URI`: Overrides `REDIS_URL` specifically for rate limiting.
|
||||
* `BEHIND_PROXY`: Set to `true` if the application is running behind a reverse proxy (e.g., Nginx, Traefik) to correctly identify client IPs (default: `false`).
|
||||
* `SESSION_TYPE`: `redis` (default) or `filesystem`.
|
||||
* `SESSION_FILE_DIR`: Directory for filesystem sessions (if `SESSION_TYPE=filesystem`, default: `/dev/shm/flask_session`).
|
||||
* `SENTRY_DSN`: Your Sentry DSN to enable error tracking.
|
||||
|
||||
## Setup and Installation
|
||||
|
||||
**Prerequisites:**
|
||||
|
||||
* Python 3.11+
|
||||
* Poetry
|
||||
* Docker & Docker Compose (Recommended for ease of setup)
|
||||
* Redis Server (if using Redis for sessions/rate limiting)
|
||||
* PostgreSQL Server (if using PostgreSQL)
|
||||
|
||||
**1. Clone the Repository:**
|
||||
|
||||
```bash
|
||||
git clone <repository-url>
|
||||
cd anonchat
|
||||
```
|
||||
|
||||
## Reverse Proxy Configuration
|
||||
**2. Using Docker (Recommended):**
|
||||
|
||||
When running AnonChat behind a reverse proxy (like Nginx or Apache), set the `BEHIND_PROXY` environment variable to "true" to ensure rate limiting works correctly. This enables the application to use the X-Forwarded-For header to determine the client's real IP address.
|
||||
This is the easiest way to get started.
|
||||
|
||||
Your reverse proxy should be configured to pass the client IP address in the X-Forwarded-For header:
|
||||
* **Create a `.env` file:** Copy `.env.example` (if it exists) or create a new `.env` file and fill in the required environment variables (see Configuration section). Pay special attention to `SECRET_KEY`, `DATABASE_URL`, `REDIS_URL`, and `ADMIN_PASSWORD`.
|
||||
* **Build and Run:**
|
||||
```bash
|
||||
docker-compose up --build -d
|
||||
```
|
||||
|
||||
### Nginx Example
|
||||
The application should now be accessible at `http://localhost:5000` (or the port mapped in `docker-compose.yml`).
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name your-domain.com;
|
||||
**3. Local Development (Without Docker):**
|
||||
|
||||
location / {
|
||||
proxy_pass http://localhost:5000;
|
||||
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;
|
||||
}
|
||||
}
|
||||
```
|
||||
* **Install Dependencies:**
|
||||
```bash
|
||||
poetry install
|
||||
```
|
||||
* **Set Environment Variables:** Create a `.env` file in the project root and define the necessary variables (see Configuration).
|
||||
* **Ensure Services are Running:** Make sure your PostgreSQL (if used) and Redis servers are running and accessible based on your `.env` configuration.
|
||||
* **Run the Development Server:**
|
||||
```bash
|
||||
poetry run start
|
||||
```
|
||||
|
||||
## Error Tracking with Sentry
|
||||
The application should be running at `http://localhost:5000`.
|
||||
|
||||
AnonChat includes integration with Sentry for error tracking and performance monitoring. This helps identify and diagnose issues in production environments.
|
||||
## Running the Application
|
||||
|
||||
### Features
|
||||
* **Docker:** Use `docker-compose up -d` to start and `docker-compose down` to stop.
|
||||
* **Local:** Use `poetry run start` for the development server. For production-like environments without Docker, use `gunicorn`:
|
||||
```bash
|
||||
# Example gunicorn command (adjust workers as needed)
|
||||
poetry run gunicorn --workers 4 --bind 0.0.0.0:5000 "anonchat:app"
|
||||
```
|
||||
|
||||
- Automatic error capturing and reporting
|
||||
- Performance monitoring
|
||||
- Contextual information for better debugging
|
||||
- Real-time alerts for critical issues
|
||||
## Deployment
|
||||
|
||||
### Configuration
|
||||
This application includes configuration files for deployment:
|
||||
|
||||
To enable Sentry integration:
|
||||
* `Dockerfile`: Defines the container image.
|
||||
* `docker-compose.yml`: For local multi-container setups (app, db, redis).
|
||||
* `fly.toml`: Configuration for deploying to Fly.io.
|
||||
|
||||
1. Sign up for a free Sentry account at [sentry.io](https://sentry.io)
|
||||
2. Create a new project and get your DSN (Data Source Name)
|
||||
3. Set the `SENTRY_DSN` environment variable in your `.env` file or deployment environment:
|
||||
Refer to the specific platform's documentation for deployment steps using these files. Ensure all necessary environment variables are set in your deployment environment.
|
||||
|
||||
```
|
||||
SENTRY_DSN=https://your-sentry-project-key@sentry.io/your-project-id
|
||||
```
|
||||
## License
|
||||
|
||||
When the `SENTRY_DSN` variable is set, error tracking will be automatically enabled when the application starts.
|
||||
|
||||
## Installation
|
||||
|
||||
1. Clone the repository
|
||||
2. Install dependencies with Poetry: `poetry install`
|
||||
3. Create `.env` file with your configuration
|
||||
4. Run the application: `poetry run start`
|
||||
|
||||
## Development
|
||||
|
||||
This project uses Poetry for dependency management.
|
||||
|
||||
- Install dependencies: `poetry install`
|
||||
- Run tests: `poetry run pytest`
|
||||
- Run the application: `poetry run start`
|
||||
|
||||
### Database Migrations
|
||||
|
||||
AnonChat includes a custom database migration system to handle schema changes. When you make changes to the database models, you should create a migration script to apply these changes to existing databases.
|
||||
|
||||
#### Running Migrations
|
||||
|
||||
- Run all pending migrations: `poetry run flask --app src/anonchat run-migrations`
|
||||
- The migrations are also automatically run when using the `init-db` command or when starting the application with the entrypoint script.
|
||||
|
||||
#### Creating New Migrations
|
||||
|
||||
To create a new migration:
|
||||
|
||||
1. Create a new Python file in the `src/anonchat/migrations` directory with a descriptive name (e.g., `add_new_column.py`)
|
||||
2. Implement a `run_migration(db)` function that performs the necessary schema changes
|
||||
3. The migration script should be idempotent (safe to run multiple times)
|
||||
|
||||
Example migration script:
|
||||
|
||||
```python
|
||||
from sqlalchemy import inspect
|
||||
from sqlalchemy.sql import text
|
||||
from flask import current_app
|
||||
|
||||
def run_migration(db):
|
||||
"""Add a new column to a table."""
|
||||
# Check if the column already exists
|
||||
inspector = inspect(db.engine)
|
||||
columns = [col['name'] for col in inspector.get_columns('your_table')]
|
||||
|
||||
# Only apply changes if needed
|
||||
if 'your_new_column' not in columns:
|
||||
current_app.logger.info("Adding new column to table")
|
||||
with db.engine.connect() as conn:
|
||||
conn.execute(text("ALTER TABLE your_table ADD COLUMN your_new_column TEXT"))
|
||||
conn.commit()
|
||||
return True
|
||||
|
||||
return False # Return True if changes were made, False otherwise
|
||||
```
|
||||
|
||||
Migrations are run in alphabetical order, so you may want to prefix migration filenames with a timestamp or sequence number for more complex projects.
|
||||
|
||||
## Admin Authentication
|
||||
|
||||
AnonChat includes a secure admin authentication system that protects administrative routes and functions. This ensures that only authorized users can access the admin dashboard, manage inquiries, and configure system settings.
|
||||
|
||||
### Security Features
|
||||
|
||||
- **Secure Password Storage**: Admin passwords are securely hashed using SHA-256 with the application's secret key as salt
|
||||
- **Session-Based Authentication**: Uses Flask sessions to maintain admin login state
|
||||
- **Protected Routes**: All admin routes are protected by middleware that verifies authentication
|
||||
- **Password Management**: Admins can change their password through the Admin Settings page
|
||||
- **Logout Functionality**: Secure logout to clear session data
|
||||
|
||||
### Setting Admin Credentials
|
||||
|
||||
Admin credentials are set using environment variables:
|
||||
|
||||
```
|
||||
ADMIN_USERNAME=admin
|
||||
ADMIN_PASSWORD=your-secure-password
|
||||
ADMIN_FORCE_RESET=false
|
||||
```
|
||||
|
||||
These values should be set in your `.env` file or server environment. The default admin user is created automatically when the application first runs.
|
||||
|
||||
#### Password Reset
|
||||
|
||||
You can force a reset of the admin password by setting `ADMIN_FORCE_RESET=true` in your environment variables. This is useful when:
|
||||
|
||||
- You need to recover from a forgotten admin password
|
||||
- You're deploying to a new environment and want to ensure the admin credentials are set correctly
|
||||
- You want to update the admin password during deployment without accessing the admin interface
|
||||
|
||||
When enabled, the application will update the admin user's password to match the value in `ADMIN_PASSWORD` during initialization or when running the `init-db` command.
|
||||
|
||||
### Admin Functions
|
||||
|
||||
- View and respond to user inquiries
|
||||
- Delete inquiries
|
||||
- Configure webhook settings
|
||||
- Change admin password
|
||||
|
||||
### Security Best Practices
|
||||
|
||||
- Always use a strong, unique password for the admin account
|
||||
- Keep your SECRET_KEY secure and unique for each deployment
|
||||
- In production, ensure you're using HTTPS to protect admin credentials during transmission
|
||||
- Change the default admin password immediately after deployment
|
||||
|
||||
## TODO: Security Improvements
|
||||
|
||||
The following security enhancements are planned for future releases:
|
||||
|
||||
- [ ] Implement CAPTCHA protection for admin login
|
||||
- Add CAPTCHA verification to prevent brute force attacks
|
||||
- Support multiple CAPTCHA providers (reCAPTCHA, hCaptcha)
|
||||
- Implement rate limiting for failed login attempts
|
||||
- Add IP-based blocking after multiple failed attempts
|
||||
|
||||
### Authentication Methods
|
||||
- [ ] Add OAuth 2.0 support for admin authentication
|
||||
- Integrate with common providers (Google, GitHub, Microsoft)
|
||||
- Implement proper PKCE flow for added security
|
||||
- Support for custom OAuth providers for enterprise deployments
|
||||
- Add multi-factor authentication options
|
||||
|
||||
### Read-Only Links
|
||||
- [ ] Implement read-only sharing links for inquiries
|
||||
- Generate unique, cryptographically secure sharing links
|
||||
- Allow users to create links that provide view-only access
|
||||
- Set optional expiration times for sharing links
|
||||
- Allow users to revoke sharing links at any time
|
||||
This project is licensed under the 0BSD license - see the [LICENSE.txt](LICENSE.txt) file for details.
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import logging
|
||||
from flask import Flask
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
import os
|
||||
|
@ -15,43 +16,65 @@ from flask_apscheduler import APScheduler
|
|||
# Load environment variables from .env file
|
||||
load_dotenv()
|
||||
|
||||
DEFAULTS = {
|
||||
'SECRET_KEY': '',
|
||||
'SITE_TITLE': 'AnonChat',
|
||||
'BEHIND_PROXY': False,
|
||||
|
||||
'RATELIMIT_STORAGE_URI': os.environ.get('REDIS_URL'),
|
||||
'RATELIMIT_HEADERS_ENABLED': True,
|
||||
'RATELIMIT_KEY_PREFIX': 'anonchat_rate_limit',
|
||||
|
||||
'DATABASE_URL': 'sqlite:///anonchat.db', # Actual key: SQLALCHEMY_DATABASE_URI
|
||||
|
||||
'SESSION_TYPE': 'redis',
|
||||
'SESSION_PERMANENT': False,
|
||||
'SESSION_USE_SIGNER': True,
|
||||
|
||||
# If SESSION_TYPE is redis
|
||||
'SESSION_REDIS': redis.from_url(os.environ.get('REDIS_URL', 'redis://localhost:6379/0')),
|
||||
|
||||
# If SESSION_TYPE is filesystem
|
||||
'SESSION_FILE_DIR': '/dev/shm/flask_session',
|
||||
'SESSION_FILE_THRESHOLD': 100,
|
||||
'SESSION_FILE_MODE': 384,
|
||||
|
||||
'SESSION_KEY_PREFIX': 'anonchat_session:'
|
||||
}
|
||||
|
||||
def get(key, type: type = str):
|
||||
env_value = os.environ.get(key)
|
||||
|
||||
if env_value is not None:
|
||||
if type == bool:
|
||||
env_value = env_value.lower() == 'true'
|
||||
|
||||
return env_value or DEFAULTS[key]
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config['SQLALCHEMY_DATABASE_URI'] = os.environ.get('DATABASE_URL', 'sqlite:///anonchat.db')
|
||||
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
||||
app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', os.urandom(24).hex())
|
||||
app.config['SITE_TITLE'] = os.environ.get('SITE_TITLE', 'AnonChat')
|
||||
# Webhook configurations
|
||||
app.config['WEBHOOK_ENABLED'] = os.environ.get('WEBHOOK_ENABLED', 'false').lower() == 'true'
|
||||
app.config['WEBHOOK_URL'] = os.environ.get('WEBHOOK_URL', '')
|
||||
app.config['WEBHOOK_SECRET'] = os.environ.get('WEBHOOK_SECRET', '')
|
||||
# Admin configurations
|
||||
app.config['ADMIN_USERNAME'] = os.environ.get('ADMIN_USERNAME', 'admin')
|
||||
app.config['ADMIN_PASSWORD'] = os.environ.get('ADMIN_PASSWORD', None)
|
||||
app.config['ADMIN_FORCE_RESET'] = os.environ.get('ADMIN_FORCE_RESET', 'false').lower() == 'true'
|
||||
# Auto-deletion configuration
|
||||
app.config['AUTO_DELETE_HOURS'] = int(os.environ.get('AUTO_DELETE_HOURS', 48))
|
||||
app.config['SECRET_KEY'] = get('SECRET_KEY')
|
||||
app.config['SITE_TITLE'] = get('SITE_TITLE')
|
||||
app.config['BEHIND_PROXY'] = get('BEHIND_PROXY', bool)
|
||||
|
||||
# Rate limit configurations
|
||||
app.config['RATELIMIT_STORAGE_URI'] = os.environ.get('RATELIMIT_STORAGE_URI', os.environ.get('REDIS_URL'))
|
||||
app.config['RATELIMIT_HEADERS_ENABLED'] = True
|
||||
app.config['RATELIMIT_KEY_PREFIX'] = 'anonchat_rate_limit'
|
||||
# Whether app is behind a proxy (get from env, default to False)
|
||||
app.config['BEHIND_PROXY'] = os.environ.get('BEHIND_PROXY', 'false').lower() == 'true'
|
||||
app.config['RATELIMIT_STORAGE_URI'] = get('RATELIMIT_STORAGE_URI')
|
||||
app.config['RATELIMIT_HEADERS_ENABLED'] = get('RATELIMIT_HEADERS_ENABLED')
|
||||
app.config['RATELIMIT_KEY_PREFIX'] = get('RATELIMIT_KEY_PREFIX')
|
||||
|
||||
# Database configuration
|
||||
app.config['SQLALCHEMY_DATABASE_URI'] = get('DATABASE_URL')
|
||||
|
||||
# Redis session configuration
|
||||
app.config['SESSION_TYPE'] = os.environ.get('SESSION_TYPE', 'redis')
|
||||
app.config['SESSION_PERMANENT'] = False
|
||||
app.config['SESSION_USE_SIGNER'] = True
|
||||
app.config['SESSION_TYPE'] = get('SESSION_TYPE')
|
||||
app.config['SESSION_PERMANENT'] = get('SESSION_PERMANENT')
|
||||
app.config['SESSION_USE_SIGNER'] = get('SESSION_USE_SIGNER')
|
||||
if app.config['SESSION_TYPE'] == 'redis':
|
||||
app.config['SESSION_REDIS'] = redis.from_url(os.environ.get('REDIS_URL', 'redis://localhost:6379/0'))
|
||||
app.config['SESSION_REDIS'] = get('SESSION_REDIS')
|
||||
elif app.config['SESSION_TYPE'] == 'filesystem':
|
||||
app.config['SESSION_FILE_DIR'] = os.environ.get('SESSION_FILE_DIR', '/dev/shm/flask_session')
|
||||
app.config['SESSION_FILE_THRESHOLD'] = os.environ.get('SESSION_FILE_THRESHOLD', 100)
|
||||
app.config['SESSION_FILE_MODE'] = os.environ.get('SESSION_FILE_MODE', 384)
|
||||
app.config['SESSION_KEY_PREFIX'] = 'anonchat_session:'
|
||||
|
||||
# Scheduler configuration
|
||||
app.config['SCHEDULER_API_ENABLED'] = False
|
||||
app.config['SCHEDULER_TIMEZONE'] = 'UTC'
|
||||
app.config['SESSION_FILE_DIR'] = get('SESSION_FILE_DIR', '/dev/shm/flask_session')
|
||||
app.config['SESSION_FILE_THRESHOLD'] = get('SESSION_FILE_THRESHOLD', 100)
|
||||
app.config['SESSION_FILE_MODE'] = get('SESSION_FILE_MODE', 384)
|
||||
app.config['SESSION_KEY_PREFIX'] = get('SESSION_KEY_PREFIX')
|
||||
|
||||
app.url_map.strict_slashes = False
|
||||
|
||||
|
@ -139,27 +162,25 @@ def run_migrations_command():
|
|||
# Import migrations module here to avoid circular imports
|
||||
from .migrations import run_migrations
|
||||
|
||||
print("Running database migrations...")
|
||||
app.logger.info("Running database migrations...")
|
||||
migrations_run = run_migrations(db)
|
||||
print(f"Database migrations completed: {migrations_run} migrations applied.")
|
||||
app.logger.info(f"Database migrations completed: {migrations_run} migrations applied.")
|
||||
|
||||
# Flask CLI command to initialize the database
|
||||
@app.cli.command("init-db")
|
||||
def init_db_command():
|
||||
def _init_db():
|
||||
"""Create database tables and initialize default data."""
|
||||
# Explicitly import models here to ensure they are registered before create_all
|
||||
from .models import Inquiry, Message, Settings, Admin
|
||||
with app.app_context():
|
||||
print("Creating database tables...")
|
||||
app.logger.info("Creating database tables...")
|
||||
db.create_all() # Should now create all tables including Inquiry
|
||||
|
||||
# Run migrations after creating tables
|
||||
# This ensures any new columns added to existing tables are properly added
|
||||
print("Running database migrations...")
|
||||
app.logger.info("Running database migrations...")
|
||||
from .migrations import run_migrations
|
||||
run_migrations(db)
|
||||
|
||||
print("Initializing default settings...")
|
||||
app.logger.info("Initializing default settings...")
|
||||
# Initialize settings if they don't exist
|
||||
# Note: Models already imported above
|
||||
if not Settings.query.first():
|
||||
|
@ -170,60 +191,46 @@ def init_db_command():
|
|||
)
|
||||
db.session.add(default_settings)
|
||||
db.session.commit()
|
||||
print("Default settings initialized.")
|
||||
app.logger.info("Default settings initialized.")
|
||||
else:
|
||||
print("Settings already exist.")
|
||||
app.logger.info("Settings already exist.")
|
||||
|
||||
print("Initializing admin user...")
|
||||
app.logger.info("Initializing admin user...")
|
||||
# Initialize admin user if it doesn't exist
|
||||
# Note: Models already imported above
|
||||
admin_user = Admin.query.filter_by(username=app.config['ADMIN_USERNAME']).first()
|
||||
if not admin_user and app.config['ADMIN_PASSWORD'] == None:
|
||||
print("Admin user not found and no password provided. Skipping admin user initialization.")
|
||||
elif app.config['ADMIN_FORCE_RESET'] or not admin_user:
|
||||
if admin_user:
|
||||
admin_user.password_hash = Admin.hash_password(app.config['ADMIN_PASSWORD'])
|
||||
print("Admin user password reset.")
|
||||
admin_user = Admin.query.first()
|
||||
|
||||
if not admin_user or os.environ.get('ADMIN_FORCE_RESET'):
|
||||
admin_password = os.environ.get('ADMIN_PASSWORD')
|
||||
if admin_password is not None:
|
||||
if admin_user:
|
||||
admin_user.password_hash = Admin.hash_password(admin_password)
|
||||
app.logger.info("Admin user password reset.")
|
||||
else:
|
||||
admin_user = Admin(
|
||||
username=os.environ.get('ADMIN_USERNAME'),
|
||||
password_hash=Admin.hash_password(admin_password)
|
||||
)
|
||||
db.session.add(admin_user)
|
||||
db.session.commit()
|
||||
app.logger.info("Admin user initialized.")
|
||||
else:
|
||||
admin_user = Admin(
|
||||
username=app.config['ADMIN_USERNAME'],
|
||||
password_hash=Admin.hash_password(app.config['ADMIN_PASSWORD'])
|
||||
)
|
||||
db.session.add(admin_user)
|
||||
db.session.commit()
|
||||
print("Admin user initialized.")
|
||||
if os.environ.get('ADMIN_FORCE_RESET'):
|
||||
app.logger.warning("Admin force reset is enabled, but no password was provided. Skipping admin user initialization.")
|
||||
else:
|
||||
app.logger.warning("Admin user not found and no password provided. Skipping admin user initialization.")
|
||||
else:
|
||||
print("Admin user already exists.")
|
||||
print("Database initialization complete.")
|
||||
app.logger.info("No need to initialize admin user.")
|
||||
|
||||
app.logger.info("Database initialization complete.")
|
||||
|
||||
# Flask CLI command to initialize the database
|
||||
@app.cli.command("init-db")
|
||||
def init_db_command():
|
||||
_init_db()
|
||||
|
||||
def run() -> None:
|
||||
# Note: The database initialization is now handled by the 'init-db' command
|
||||
# and should be run separately, e.g., via the entrypoint script.
|
||||
# Keeping the original db.create_all() here might be redundant or could
|
||||
# be useful for local development outside Docker.
|
||||
# Consider if you still need the initialization logic within run().
|
||||
with app.app_context():
|
||||
# Explicitly import all models to ensure they're registered before db.create_all()
|
||||
from .models import Inquiry, Message, Settings, Admin
|
||||
db.create_all()
|
||||
|
||||
# Initialize settings if they don't exist
|
||||
if not Settings.query.first():
|
||||
default_settings = Settings(
|
||||
webhook_enabled=app.config['WEBHOOK_ENABLED'],
|
||||
webhook_url=app.config['WEBHOOK_URL'],
|
||||
webhook_secret=app.config['WEBHOOK_SECRET']
|
||||
)
|
||||
db.session.add(default_settings)
|
||||
db.session.commit()
|
||||
|
||||
# Initialize admin user if it doesn't exist
|
||||
if not Admin.query.filter_by(username=app.config['ADMIN_USERNAME']).first():
|
||||
admin_user = Admin(
|
||||
username=app.config['ADMIN_USERNAME'],
|
||||
password_hash=Admin.hash_password(app.config['ADMIN_PASSWORD'])
|
||||
)
|
||||
db.session.add(admin_user)
|
||||
db.session.commit()
|
||||
|
||||
app.logger.setLevel(logging.DEBUG)
|
||||
_init_db()
|
||||
|
||||
app.run(debug=True)
|
||||
|
|
|
@ -26,7 +26,8 @@ def run_migration(db):
|
|||
'smtp_username': 'VARCHAR(255)',
|
||||
'smtp_password': 'VARCHAR(255)',
|
||||
'recipient_email': 'VARCHAR(255)',
|
||||
'notification_show_message': 'BOOLEAN DEFAULT FALSE'
|
||||
'notification_show_message': 'BOOLEAN DEFAULT FALSE',
|
||||
'auto_delete_hours': 'INTEGER DEFAULT 48'
|
||||
}
|
||||
|
||||
# Add each column if it doesn't exist
|
||||
|
|
|
@ -16,4 +16,6 @@ class Settings(db.Model):
|
|||
smtp_password = db.Column(db.String(255), nullable=True) # Consider storing this securely
|
||||
recipient_email = db.Column(db.String(255), nullable=True)
|
||||
|
||||
notification_show_message = db.Column(db.Boolean, default=False)
|
||||
notification_show_message = db.Column(db.Boolean, default=False)
|
||||
|
||||
auto_delete_hours = db.Column(db.Integer, default=48)
|
|
@ -30,6 +30,28 @@
|
|||
|
||||
<br>
|
||||
|
||||
<div class="auto-delete-settings">
|
||||
<h3>Auto-Delete Settings</h3>
|
||||
|
||||
<div style="margin-bottom: 2rem;">
|
||||
<p>Configure how long inquiries are kept before being automatically deleted.</p>
|
||||
</div>
|
||||
|
||||
<form method="POST" action="{{ url_for('admin_settings_auto_delete') }}">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
|
||||
<div style="margin-top: 1rem;">
|
||||
<label for="auto_delete_hours">Auto-Delete After (hours):</label>
|
||||
<input type="number" id="auto_delete_hours" name="auto_delete_hours" value="{{ settings.auto_delete_hours }}" min="1" max="8760">
|
||||
<small style="display: block; margin-top: 0.5rem;">Set to how many hours inquiries should be kept before automatic deletion. (1-8760 hours, 8760 = 1 year)</small>
|
||||
</div>
|
||||
|
||||
<button type="submit" style="margin-top: 1rem;">Save Auto-Delete Settings</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
|
||||
<div class="notification-settings">
|
||||
<h3>Notification Settings</h3>
|
||||
|
||||
|
@ -150,15 +172,4 @@
|
|||
<button type="submit" style="margin-top: 1rem;">Save Email Settings</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
|
||||
<div class="settings-info">
|
||||
<h3>System Configuration</h3>
|
||||
<p>The following settings are configured through environment variables:</p>
|
||||
<ul style="margin-left: 1.5rem;">
|
||||
<li><strong>Auto Delete Delay:</strong> {{ config.AUTO_DELETE_HOURS }} hours (set with AUTO_DELETE_HOURS)</li>
|
||||
</ul>
|
||||
<p><small>To change these values, update your environment variables or .env file and restart the application.</small></p>
|
||||
</div>
|
||||
{% endblock %}
|
Loading…
Add table
Add a link
Reference in a new issue