diff --git a/.flaskenv b/.flaskenv new file mode 100644 index 00000000..80b05632 --- /dev/null +++ b/.flaskenv @@ -0,0 +1,17 @@ +# The settings in this file are secondary to .env, which overrides + +# Assume production by default, unset debug and testing state +FLASK_DEBUG=false +FLASK_DEBUG_TB_ENABLED=false +FLASK_TESTING=false + +# To only support HTTPS, set Secure Cookies to True +FLASK_SESSION_COOKIE_SECURE=true + +# --- App configuration +# Default timezone when user timezone is not known +FLASK_TIMEZONE='Asia/Kolkata' +FLASK_ASSET_MANIFEST_PATH=static/build/manifest.json +# Asset base path – without a trailing slash +FLASK_ASSET_BASE_PATH=/static/build +FLASK_STATIC_SUBDOMAIN=static diff --git a/.gitignore b/.gitignore index 927e184b..38f335d1 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ test.db .project .pydevproject .vscode +.python-version .settings .sass-cache .webassets-cache diff --git a/hasjob/__init__.py b/hasjob/__init__.py index f05d99f8..abf1d872 100644 --- a/hasjob/__init__.py +++ b/hasjob/__init__.py @@ -38,7 +38,7 @@ from .models import db # isort:skip # Configure the app -coaster.app.init_app(app) +coaster.app.init_app(app, ['py', 'env'], env_prefix=['FLASK', 'APP_HASJOB']) db.init_app(app) db.app = app migrate = Migrate(app, db) diff --git a/hasjob/uploads.py b/hasjob/uploads.py index 68ab86cb..77edf7c8 100644 --- a/hasjob/uploads.py +++ b/hasjob/uploads.py @@ -42,7 +42,7 @@ def process_image(requestfile, maxsize=(170, 130)): raise UploadNotAllowed("This image is too large to process") img.load() if img.size[0] > maxsize[0] or img.size[1] > maxsize[1]: - img.thumbnail(maxsize, Image.ANTIALIAS) + img.thumbnail(maxsize, Image.LANCZOS) boximg = Image.new(img.mode, maxsize, '#fff0') boximg.paste( img, ((boximg.size[0] - img.size[0]) // 2, (boximg.size[1] - img.size[1]) // 2) diff --git a/instance/settings.py b/instance/settings.py index 25f13e41..2f5704eb 100644 --- a/instance/settings.py +++ b/instance/settings.py @@ -1,69 +1,5 @@ import re -#: The title of this site -SITE_TITLE = 'Job Board' -#: TypeKit code for fonts -TYPEKIT_CODE = '' -#: Google Analytics code UA-XXXXXX-X -GA_CODE = '' -#: Database backend -SQLALCHEMY_DATABASE_URI = 'sqlite:///' -#: Secret key -SECRET_KEY = 'make this something random' # nosec B105 -#: Timezone -TIMEZONE = 'Asia/Kolkata' -#: Static resource subdomain (defaults to 'static') -STATIC_SUBDOMAIN = 'static' -#: Upload path -UPLOADED_LOGOS_DEST = '/tmp/uploads' # nosec B108 -#: Cache settings -CACHE_TYPE = 'redis' -#: RQ settings -RQ_REDIS_URL = 'redis://localhost:6379/0' -RQ_SCHEDULER_INTERVAL = 1 -#: GeoIP database file (GeoIP2 or GeoLite2 city mmdb) -#: On Ubuntu: /usr/share/GeoIP/GeoLite2-City.mmdb -#: On Homebrew: /usr/local/var/GeoIP/GeoLite2-City.mmdb -GEOIP_PATH = '/usr/share/GeoIP/GeoLite2-City.mmdb' -#: Mail settings -#: MAIL_FAIL_SILENTLY : default True -#: MAIL_SERVER : default 'localhost' -#: MAIL_PORT : default 25 -#: MAIL_USE_TLS : default False -#: MAIL_USE_SSL : default False -#: MAIL_USERNAME : default None -#: MAIL_PASSWORD : default None -#: DEFAULT_MAIL_SENDER : default None -MAIL_FAIL_SILENTLY = False -MAIL_SERVER = 'localhost' -# Mail sender for crash reports and other automated mail -DEFAULT_MAIL_SENDER = 'Hasjob ' -MAIL_DEFAULT_SENDER = DEFAULT_MAIL_SENDER -# Mail sender for job application responses (email address only) -MAIL_SENDER = 'test@example.com' -#: Logging: recipients of error emails -ADMINS = [] -#: Log file -LOGFILE = 'error.log' -#: Use SSL for some URLs -USE_SSL = False -#: Twitter integration (register as a "client" app) -TWITTER_ENABLED = False -TWITTER_CONSUMER_KEY = '' -TWITTER_CONSUMER_SECRET = '' # nosec B105 -TWITTER_ACCESS_KEY = '' -TWITTER_ACCESS_SECRET = '' # nosec B105 -#: Bit.ly integration for short URLs -BITLY_USER = '' -BITLY_KEY = '' -#: Access key for periodic server-only tasks -PERIODIC_KEY = '' -#: Throttle limit for email domain -THROTTLE_LIMIT = 5 -#: Don't show year for dates within this many days -SHORTDATE_THRESHOLD_DAYS = 60 -#: Email address to display when asking users to contact support -SUPPORT_EMAIL = 'support@hasgeek.com' #: Words banned in the title and their error messages BANNED_WORDS = [ [ @@ -119,9 +55,3 @@ "Candidates must apply via Hasjob", ) ] -#: LastUser server -LASTUSER_SERVER = 'https://hasgeek.com/' -#: LastUser client id -LASTUSER_CLIENT_ID = '' -#: LastUser client secret -LASTUSER_CLIENT_SECRET = '' # nosec B105 diff --git a/requirements.txt b/requirements.txt index 8e5f6c63..8eee7d60 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,30 +1,31 @@ git+https://github.com/hasgeek/baseframe#egg=baseframe -bleach==6.0.0 +bleach git+https://github.com/hasgeek/coaster#egg=coaster -coverage==7.2.5 -dnspython==2.3.0 -Flask==2.2.5 -Flask-Assets==2.0 +coverage +dnspython +Flask +Flask-Assets git+https://github.com/hasgeek/flask-lastuser#egg=Flask-Lastuser -Flask-Mail==0.9.1 -Flask-Migrate==4.0.4 -Flask-Redis==0.4.0 -Flask-RQ2==18.3 -Flask-SQLAlchemy==3.0.3 -Flask-Testing==0.8.1 +Flask-Mail +Flask-Migrate +Flask-Redis +Flask-RQ2 +Flask-SQLAlchemy +Flask-Testing git+https://github.com/maxcountryman/flask-uploads.git#egg=Flask-Uploads -Flask-WTF==1.1.1 -geoip2==4.6.0 -html2text==2020.1.16 -jsmin==3.0.1 -langid==1.1.6 -Markdown==3.4.3 -Pillow==9.5.0 -premailer==3.10.0 -progressbar2==4.2.0 -psycopg2==2.9.6 -pytz==2023.3 -SQLAlchemy==2.0.12 -SQLAlchemy-Utils==0.41.1 -tldextract==3.4.1 -tweepy==4.14.0 +Flask-WTF +geoip2 +gunicorn +html2text +jsmin +langid +Pillow +premailer +progressbar2 +psycopg2-binary +python-dotenv +pytz +SQLAlchemy +SQLAlchemy-Utils +tldextract +tweepy diff --git a/sample.env b/sample.env new file mode 100644 index 00000000..fab18bcb --- /dev/null +++ b/sample.env @@ -0,0 +1,146 @@ +# Sample configuration for the `.env` settings file. These values are interpeted as +# JSON, falling back to plain strings. This file must be a valid shell script, so +# values using shell glob characters '*?[]' must be enclosed in single quotes + +# --- Development mode (remove these three in production) +# Coaster uses this value; Flask 2.2's deprecation warning can be ignored +FLASK_ENV=development +# Flask >=2.2 requires this value +FLASK_DEBUG=1 +# Flask-DebugToolbar (optional) is useful for dev, but MUST NOT BE enabled in production +FLASK_DEBUG_TB_ENABLED=true + +# --- Domain configuration (these must point to 127.0.0.1 in /etc/hosts in dev and test) +# Hasjob app's server name (Hasjob uses 'hasjob.co' in production) +FLASK_SERVER_NAME=hasjob.test:5001 +# Auth cookie domain (auth cookie is shared across apps in subdomains) +FLASK_LASTUSER_COOKIE_DOMAIN=.hasjob.test +FLASK_SESSION_COOKIE_DOMAIN=.hasjob.test +# For dev, uncomment to override secure cookies, when not using HTTPS +# FLASK_SESSION_COOKIE_SECURE=false + +# --- Secrets +# Secret keys with key rotation -- put older keys further down the list. Older keys will +# be used to decode tokens as fallback, but will not be used to encode. Remove old keys +# when they are considered expired +FLASK_SECRET_KEYS='["make-this-something-random", "older-secret-keys-here"]' +# Secret key for sitemap URLs submitted to Google +SITEMAP_KEY=make-this-something-random + +# --- Analytics +# Google Analytics code +FLASK_GA_CODE=null +# Matomo analytics (shared config across apps; URL must have trailing slash) +FLASK_MATOMO_URL=https://... +# MATOMO_JS and MATOMO_FILE have default values; override if your installation varies +# FLASK_MATOMO_JS=matomo.js +# FLASK_MATOMO_FILE=matomo.php +# Matomo API key, used in funnel.cli.periodic.stats +FLASK_MATOMO_TOKEN=null +# Matomo site id (app-specific) +FLASK_MATOMO_ID= + +# --- Statsd logging (always enabled, emits to UDP) +# Support for tagging varies between implementations: +# Etsy's statsd doesn't support tagging (default `false` merges tags into metric name) +# Telegraf uses `,` as a tag separator +# Prometheus uses `;` as a tag separator +FLASK_STATSD_TAGS=, +# Other statsd settings have default values: +# FLASK_STATSD_HOST=127.0.0.1 +# FLASK_STATSD_PORT=8125 +# FLASK_STATSD_MAXUDPSIZE=512 +# FLASK_STATSD_IPV6=false +# Sampling rate, 0.0-1.0, default 1 logs 100% +# FLASK_STATSD_RATE=1 +# FLASK_STATSD_TAGS=false +# Log all Flask requests (time to serve, response status code) +# FLASK_STATSD_REQUEST_LOG=true +# Log all WTForms validations (when using baseframe.forms.Form subclass) +# FLASK_STATSD_FORM_LOG=true + +# --- Redis Queue and Redis cache (use separate dbs to isolate) +# Redis server host +REDIS_HOST=localhost +# RQ and cache +FLASK_RQ_REDIS_URL=redis://${REDIS_HOST}:6379/1 +FLASK_CACHE_TYPE=flask_caching.backends.RedisCache +FLASK_CACHE_REDIS_URL=redis://${REDIS_HOST}:6379/0 + +# --- Database configuration +DB_HOST=localhost +FLASK_SQLALCHEMY_DATABASE_URI='postgresql:///hasjob' + +# --- Email configuration +# SMTP mail server ('localhost' if Postfix is configured as a relay email server) +FLASK_MAIL_SERVER=localhost +# If not using localhost, SMTP will need authentication +# Port number (25 is default, but 587 is more likely for non-localhost) +FLASK_MAIL_PORT=25 +# Port 25 uses neither TLS nor SSL. Port 587 uses TLS and port 465 uses SSL (obsolete) +FLASK_MAIL_USE_TLS=false +FLASK_MAIL_USE_SSL=false +# Authentication if using port 587 or 465 +FLASK_MAIL_USERNAME=null +FLASK_MAIL_PASSWORD=null +# Default "From:" address in email +FLASK_MAIL_DEFAULT_SENDER="Hasjob " + +# --- App configuration +# Support email and phone numbers (must be syntactically valid) +FLASK_SITE_SUPPORT_EMAIL=support@example.com +# Posting throttle limit +FLASK_THROTTLE_LIMIT=5 + +# --- GeoIP databases for IP address geolocation (used in account settings) +# Obtain a free license key from Maxmind, install geoipupdate, place the account id and +# key in GeoIP.conf and enable the GeoLite2-ASN database. The location of GeoIP.conf +# varies between Ubuntu and macOS. +# https://support.maxmind.com/hc/en-us/articles/4407111582235-Generate-a-License-Key + +# Ubuntu: +# sudo add-apt-repository ppa:maxmind/ppa +# sudo apt install geoipupdate +# vim /etc/GeoIP.conf +# sudo geoipupdate -f /etc/GeoIP.conf +# FLASK_GEOIP_PATH=/usr/share/GeoIP/GeoLite2-City.mmdb + +# macOS with Homebrew on Apple Silicon: +# brew install geoipupdate +# vim /opt/homebrew/etc/GeoIP.conf +# geoipupdate -f /opt/homebrew/etc/GeoIP.conf +# FLASK_GEOIP_PATH=/opt/homebrew/var/GeoIP/GeoLite2-City.mmdb + +# --- Logging +# Optional path to log file, or default null to disable file logging +FLASK_LOG_FILE=null +# Optional config for file logging: +# FLASK_LOG_FILE_LEVEL accepts NOTSET, DEBUG, INFO, WARNING (default), ERROR, CRITICAL +# FLASK_LOG_FILE_DELAY (bool, default true, delays log file creation until first log) +# FLASK_LOG_FILE_ROTATE (bool, default true, controls logrotate on the basis of time) +# FLASK_LOG_FILE_ROTATE_WHEN (default "midnight", other options: S, M, H, D, W0-W6) +# FLASK_LOG_FILE_ROTATE_COUNT (count of old files to keep, default 7 for a week's worth) +# FLASK_LOG_FILE_ROTATE_UTC (default false, if set uses UTC for midnight and W0-W6) + +# List of email addresses to send error reports with traceback and local var dump +# This requires SMTP config (above) +FLASK_LOG_EMAIL_TO='["webmaster@example.com"]' +# Additional options: FLASK_LOG_EMAIL_FROM, defaults to FLASK_MAIL_DEFAULT_SENDER + +# Send error reports to a Telegram chat +FLASK_LOG_TELEGRAM_CHATID=null +# Use these bot API credentials (configure your bot at https://t.me/botfather) +FLASK_LOG_TELEGRAM_APIKEY=null +# Optional settings: +# FLASK_LOG_TELEGRAM_THREADID (if the chat has topic threads, use a specific thread) +# FLASK_LOG_TELEGRAM_LEVEL=NOTSET, DEBUG, INFO, WARNING (default), ERROR, CRITICAL + +# --- Hasgeek app integrations +FLASK_LASTUSER_SERVER=http://funnel.test:3000 +FLASK_LASTUSER_CLIENT_ID=client_id_from_funnel +FLASK_LASTUSER_CLIENT_SECRET=client_secret_from_funnel + +# --- External app integrations + + +# --- OAuth2 login integrations diff --git a/wsgi.py b/wsgi.py index d8b41975..9cf9dbe9 100644 --- a/wsgi.py +++ b/wsgi.py @@ -1,7 +1,17 @@ import os.path import sys +from flask.cli import load_dotenv +from flask.helpers import get_load_dotenv +from werkzeug.middleware.proxy_fix import ProxyFix + __all__ = ['application'] sys.path.insert(0, os.path.dirname(__file__)) +if get_load_dotenv(): + load_dotenv() + +# pylint: disable=wrong-import-position from hasjob import app as application # isort:skip + +application.wsgi_app = ProxyFix(application.wsgi_app) # type: ignore[method-assign]