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
|
## Features
|
||||||
|
|
||||||
- Anonymous inquiries and messaging
|
* **Anonymous Inquiries:** Allows users to submit inquiries without revealing their identity.
|
||||||
- Admin dashboard to manage inquiries
|
* **Admin Interface:** Secure area for administrators to view and manage inquiries.
|
||||||
- Customizable site title
|
* **Webhook Notifications:** Optionally sends notifications to a specified URL upon new inquiry submissions.
|
||||||
- Redis-based session storage for improved scalability
|
* **Automatic Data Deletion:** Periodically deletes inquiries older than a configurable duration (default: 48 hours) to maintain privacy.
|
||||||
- Integrated error tracking with Sentry
|
* **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.
|
* **Backend:** Python 3.11+, Flask
|
||||||
|
* **Database:** PostgreSQL (recommended) or SQLite (default) via Flask-SQLAlchemy
|
||||||
Rest assured though, I know what I'm (or the AI is) doing. Here's what would happen if I didn't:
|
* **Session Management:** Redis (recommended) or Filesystem via Flask-Session
|
||||||
1. [my saas was built with Cursor, zero hand written code \
|
* **Rate Limiting:** Redis via Flask-Limiter
|
||||||
AI is no longer just an assistant, it's also the builder \
|
* **Task Scheduling:** Flask-APScheduler
|
||||||
Now, you can continue to whine about it or start building.](https://xcancel.com/leojr94_/status/1900767509621674109)
|
* **Password Hashing:** Argon2 via argon2-cffi
|
||||||
2. [random thing are happening, maxed out usage on api keys, people bypassing the subscription, creating random shit on db \
|
* **WSGI Server:** Gunicorn
|
||||||
there are just some weird ppl out there](https://xcancel.com/leojr94_/status/1901560276488511759)
|
* **Dependency Management:** Poetry
|
||||||
|
* **Containerization:** Docker, Docker Compose
|
||||||
|
* **Deployment:** Configured for Fly.io (optional)
|
||||||
|
|
||||||
## Configuration
|
## 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
|
**Required:**
|
||||||
- `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)
|
|
||||||
|
|
||||||
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.
|
||||||
|
|
||||||
```
|
**Optional:**
|
||||||
SECRET_KEY=your_secret_key_here
|
|
||||||
FLASK_APP=src/anonchat
|
* `SITE_TITLE`: The title displayed on the website (default: `AnonChat`).
|
||||||
FLASK_ENV=development
|
* `WEBHOOK_ENABLED`: Set to `true` to enable webhook notifications (default: `false`).
|
||||||
SITE_TITLE=My Custom Chat
|
* `WEBHOOK_URL`: The URL to send webhook notifications to.
|
||||||
BEHIND_PROXY=true
|
* `WEBHOOK_SECRET`: A secret key to sign webhook payloads (recommended if webhooks are enabled).
|
||||||
REDIS_URL=redis://redis:6379/0
|
* `ADMIN_USERNAME`: The username for the admin user (default: `admin`).
|
||||||
AUTO_DELETE_HOURS=72
|
* `ADMIN_FORCE_RESET`: Set to `true` to force reset the admin password on startup using `ADMIN_PASSWORD`.
|
||||||
SENTRY_DSN=https://your-sentry-dsn
|
* `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
|
**3. Local Development (Without Docker):**
|
||||||
server {
|
|
||||||
listen 80;
|
|
||||||
server_name your-domain.com;
|
|
||||||
|
|
||||||
location / {
|
* **Install Dependencies:**
|
||||||
proxy_pass http://localhost:5000;
|
```bash
|
||||||
proxy_set_header Host $host;
|
poetry install
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
```
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
* **Set Environment Variables:** Create a `.env` file in the project root and define the necessary variables (see Configuration).
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
* **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
|
## Deployment
|
||||||
- Performance monitoring
|
|
||||||
- Contextual information for better debugging
|
|
||||||
- Real-time alerts for critical issues
|
|
||||||
|
|
||||||
### 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)
|
Refer to the specific platform's documentation for deployment steps using these files. Ensure all necessary environment variables are set in your deployment environment.
|
||||||
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:
|
|
||||||
|
|
||||||
```
|
## License
|
||||||
SENTRY_DSN=https://your-sentry-project-key@sentry.io/your-project-id
|
|
||||||
```
|
|
||||||
|
|
||||||
When the `SENTRY_DSN` variable is set, error tracking will be automatically enabled when the application starts.
|
This project is licensed under the 0BSD license - see the [LICENSE.txt](LICENSE.txt) file for details.
|
||||||
|
|
||||||
## 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
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import logging
|
||||||
from flask import Flask
|
from flask import Flask
|
||||||
from flask_sqlalchemy import SQLAlchemy
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
import os
|
import os
|
||||||
|
|
@ -15,43 +16,65 @@ from flask_apscheduler import APScheduler
|
||||||
# Load environment variables from .env file
|
# Load environment variables from .env file
|
||||||
load_dotenv()
|
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 = Flask(__name__)
|
||||||
app.config['SQLALCHEMY_DATABASE_URI'] = os.environ.get('DATABASE_URL', 'sqlite:///anonchat.db')
|
app.config['SECRET_KEY'] = get('SECRET_KEY')
|
||||||
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
app.config['SITE_TITLE'] = get('SITE_TITLE')
|
||||||
app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', os.urandom(24).hex())
|
app.config['BEHIND_PROXY'] = get('BEHIND_PROXY', bool)
|
||||||
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))
|
|
||||||
# Rate limit configurations
|
# Rate limit configurations
|
||||||
app.config['RATELIMIT_STORAGE_URI'] = os.environ.get('RATELIMIT_STORAGE_URI', os.environ.get('REDIS_URL'))
|
app.config['RATELIMIT_STORAGE_URI'] = get('RATELIMIT_STORAGE_URI')
|
||||||
app.config['RATELIMIT_HEADERS_ENABLED'] = True
|
app.config['RATELIMIT_HEADERS_ENABLED'] = get('RATELIMIT_HEADERS_ENABLED')
|
||||||
app.config['RATELIMIT_KEY_PREFIX'] = 'anonchat_rate_limit'
|
app.config['RATELIMIT_KEY_PREFIX'] = get('RATELIMIT_KEY_PREFIX')
|
||||||
# Whether app is behind a proxy (get from env, default to False)
|
|
||||||
app.config['BEHIND_PROXY'] = os.environ.get('BEHIND_PROXY', 'false').lower() == 'true'
|
# Database configuration
|
||||||
|
app.config['SQLALCHEMY_DATABASE_URI'] = get('DATABASE_URL')
|
||||||
|
|
||||||
# Redis session configuration
|
# Redis session configuration
|
||||||
app.config['SESSION_TYPE'] = os.environ.get('SESSION_TYPE', 'redis')
|
app.config['SESSION_TYPE'] = get('SESSION_TYPE')
|
||||||
app.config['SESSION_PERMANENT'] = False
|
app.config['SESSION_PERMANENT'] = get('SESSION_PERMANENT')
|
||||||
app.config['SESSION_USE_SIGNER'] = True
|
app.config['SESSION_USE_SIGNER'] = get('SESSION_USE_SIGNER')
|
||||||
if app.config['SESSION_TYPE'] == 'redis':
|
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':
|
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_DIR'] = get('SESSION_FILE_DIR', '/dev/shm/flask_session')
|
||||||
app.config['SESSION_FILE_THRESHOLD'] = os.environ.get('SESSION_FILE_THRESHOLD', 100)
|
app.config['SESSION_FILE_THRESHOLD'] = get('SESSION_FILE_THRESHOLD', 100)
|
||||||
app.config['SESSION_FILE_MODE'] = os.environ.get('SESSION_FILE_MODE', 384)
|
app.config['SESSION_FILE_MODE'] = get('SESSION_FILE_MODE', 384)
|
||||||
app.config['SESSION_KEY_PREFIX'] = 'anonchat_session:'
|
app.config['SESSION_KEY_PREFIX'] = get('SESSION_KEY_PREFIX')
|
||||||
|
|
||||||
# Scheduler configuration
|
|
||||||
app.config['SCHEDULER_API_ENABLED'] = False
|
|
||||||
app.config['SCHEDULER_TIMEZONE'] = 'UTC'
|
|
||||||
|
|
||||||
app.url_map.strict_slashes = False
|
app.url_map.strict_slashes = False
|
||||||
|
|
||||||
|
|
@ -139,27 +162,25 @@ def run_migrations_command():
|
||||||
# Import migrations module here to avoid circular imports
|
# Import migrations module here to avoid circular imports
|
||||||
from .migrations import run_migrations
|
from .migrations import run_migrations
|
||||||
|
|
||||||
print("Running database migrations...")
|
app.logger.info("Running database migrations...")
|
||||||
migrations_run = run_migrations(db)
|
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
|
def _init_db():
|
||||||
@app.cli.command("init-db")
|
|
||||||
def init_db_command():
|
|
||||||
"""Create database tables and initialize default data."""
|
"""Create database tables and initialize default data."""
|
||||||
# Explicitly import models here to ensure they are registered before create_all
|
# Explicitly import models here to ensure they are registered before create_all
|
||||||
from .models import Inquiry, Message, Settings, Admin
|
from .models import Inquiry, Message, Settings, Admin
|
||||||
with app.app_context():
|
with app.app_context():
|
||||||
print("Creating database tables...")
|
app.logger.info("Creating database tables...")
|
||||||
db.create_all() # Should now create all tables including Inquiry
|
db.create_all() # Should now create all tables including Inquiry
|
||||||
|
|
||||||
# Run migrations after creating tables
|
# Run migrations after creating tables
|
||||||
# This ensures any new columns added to existing tables are properly added
|
# 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
|
from .migrations import run_migrations
|
||||||
run_migrations(db)
|
run_migrations(db)
|
||||||
|
|
||||||
print("Initializing default settings...")
|
app.logger.info("Initializing default settings...")
|
||||||
# Initialize settings if they don't exist
|
# Initialize settings if they don't exist
|
||||||
# Note: Models already imported above
|
# Note: Models already imported above
|
||||||
if not Settings.query.first():
|
if not Settings.query.first():
|
||||||
|
|
@ -170,60 +191,46 @@ def init_db_command():
|
||||||
)
|
)
|
||||||
db.session.add(default_settings)
|
db.session.add(default_settings)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
print("Default settings initialized.")
|
app.logger.info("Default settings initialized.")
|
||||||
else:
|
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
|
# Initialize admin user if it doesn't exist
|
||||||
# Note: Models already imported above
|
# Note: Models already imported above
|
||||||
admin_user = Admin.query.filter_by(username=app.config['ADMIN_USERNAME']).first()
|
admin_user = Admin.query.first()
|
||||||
if not admin_user and app.config['ADMIN_PASSWORD'] == None:
|
|
||||||
print("Admin user not found and no password provided. Skipping admin user initialization.")
|
if not admin_user or os.environ.get('ADMIN_FORCE_RESET'):
|
||||||
elif app.config['ADMIN_FORCE_RESET'] or not admin_user:
|
admin_password = os.environ.get('ADMIN_PASSWORD')
|
||||||
if admin_user:
|
if admin_password is not None:
|
||||||
admin_user.password_hash = Admin.hash_password(app.config['ADMIN_PASSWORD'])
|
if admin_user:
|
||||||
print("Admin user password reset.")
|
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:
|
else:
|
||||||
admin_user = Admin(
|
if os.environ.get('ADMIN_FORCE_RESET'):
|
||||||
username=app.config['ADMIN_USERNAME'],
|
app.logger.warning("Admin force reset is enabled, but no password was provided. Skipping admin user initialization.")
|
||||||
password_hash=Admin.hash_password(app.config['ADMIN_PASSWORD'])
|
else:
|
||||||
)
|
app.logger.warning("Admin user not found and no password provided. Skipping admin user initialization.")
|
||||||
db.session.add(admin_user)
|
|
||||||
db.session.commit()
|
|
||||||
print("Admin user initialized.")
|
|
||||||
else:
|
else:
|
||||||
print("Admin user already exists.")
|
app.logger.info("No need to initialize admin user.")
|
||||||
print("Database initialization complete.")
|
|
||||||
|
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:
|
def run() -> None:
|
||||||
# Note: The database initialization is now handled by the 'init-db' command
|
app.logger.setLevel(logging.DEBUG)
|
||||||
# and should be run separately, e.g., via the entrypoint script.
|
_init_db()
|
||||||
# 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.run(debug=True)
|
app.run(debug=True)
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,8 @@ def run_migration(db):
|
||||||
'smtp_username': 'VARCHAR(255)',
|
'smtp_username': 'VARCHAR(255)',
|
||||||
'smtp_password': 'VARCHAR(255)',
|
'smtp_password': 'VARCHAR(255)',
|
||||||
'recipient_email': '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
|
# 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
|
smtp_password = db.Column(db.String(255), nullable=True) # Consider storing this securely
|
||||||
recipient_email = db.Column(db.String(255), nullable=True)
|
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>
|
<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">
|
<div class="notification-settings">
|
||||||
<h3>Notification Settings</h3>
|
<h3>Notification Settings</h3>
|
||||||
|
|
||||||
|
|
@ -150,15 +172,4 @@
|
||||||
<button type="submit" style="margin-top: 1rem;">Save Email Settings</button>
|
<button type="submit" style="margin-top: 1rem;">Save Email Settings</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</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 %}
|
{% endblock %}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue