diff --git a/apiserver/.env.example b/apiserver/.env.example index d3ad596e5..d589e3d0a 100644 --- a/apiserver/.env.example +++ b/apiserver/.env.example @@ -1,7 +1,7 @@ # Backend # Debug value for api server use it as 0 for production use DEBUG=0 -DJANGO_SETTINGS_MODULE="plane.settings.production" +CORS_ALLOWED_ORIGINS="http://localhost" # Error logs SENTRY_DSN="" @@ -70,6 +70,5 @@ ENABLE_MAGIC_LINK_LOGIN="0" # Email redirections and minio domain settings WEB_URL="http://localhost" - # Gunicorn Workers -GUNICORN_WORKERS=2 +GUNICORN_WORKERS=2 \ No newline at end of file diff --git a/apiserver/plane/api/views/auth_extended.py b/apiserver/plane/api/views/auth_extended.py index fbffacff8..e2ec9d5b6 100644 --- a/apiserver/plane/api/views/auth_extended.py +++ b/apiserver/plane/api/views/auth_extended.py @@ -33,7 +33,7 @@ from plane.bgtasks.forgot_password_task import forgot_password class RequestEmailVerificationEndpoint(BaseAPIView): def get(self, request): token = RefreshToken.for_user(request.user).access_token - current_site = settings.WEB_URL + current_site = request.META.get('HTTP_ORIGIN') email_verification.delay( request.user.first_name, request.user.email, token, current_site ) @@ -76,7 +76,7 @@ class ForgotPasswordEndpoint(BaseAPIView): uidb64 = urlsafe_base64_encode(smart_bytes(user.id)) token = PasswordResetTokenGenerator().make_token(user) - current_site = settings.WEB_URL + current_site = request.META.get('HTTP_ORIGIN') forgot_password.delay( user.first_name, user.email, uidb64, token, current_site diff --git a/apiserver/plane/api/views/authentication.py b/apiserver/plane/api/views/authentication.py index eadfeef61..dadee4a48 100644 --- a/apiserver/plane/api/views/authentication.py +++ b/apiserver/plane/api/views/authentication.py @@ -287,7 +287,8 @@ class MagicSignInGenerateEndpoint(BaseAPIView): ri.set(key, json.dumps(value), ex=expiry) - current_site = settings.WEB_URL + + current_site = request.META.get('HTTP_ORIGIN') magic_link.delay(email, key, token, current_site) return Response({"key": key}, status=status.HTTP_200_OK) diff --git a/apiserver/plane/api/views/project.py b/apiserver/plane/api/views/project.py index 37e491e83..494760b8a 100644 --- a/apiserver/plane/api/views/project.py +++ b/apiserver/plane/api/views/project.py @@ -386,7 +386,7 @@ class InviteProjectEndpoint(BaseAPIView): token=token, role=role, ) - domain = settings.WEB_URL + domain = request.META.get('HTTP_ORIGIN') project_invitation.delay(email, project_id, token, domain) return Response( diff --git a/apiserver/plane/api/views/workspace.py b/apiserver/plane/api/views/workspace.py index c53fbf126..a30d68469 100644 --- a/apiserver/plane/api/views/workspace.py +++ b/apiserver/plane/api/views/workspace.py @@ -330,7 +330,7 @@ class InviteWorkspaceEndpoint(BaseAPIView): invitation.email, workspace.id, invitation.token, - settings.WEB_URL, + request.META.get('HTTP_ORIGIN'), request.user.email, ) diff --git a/apiserver/plane/settings/common.py b/apiserver/plane/settings/common.py index 27da44d9c..dee424c44 100644 --- a/apiserver/plane/settings/common.py +++ b/apiserver/plane/settings/common.py @@ -1,22 +1,35 @@ +"""Global Settings""" +# Python imports import os -import datetime +import ssl +import certifi from datetime import timedelta +from urllib.parse import urlparse +# Django imports from django.core.management.utils import get_random_secret_key +# Third party imports +import dj_database_url +import sentry_sdk +from sentry_sdk.integrations.django import DjangoIntegration +from sentry_sdk.integrations.redis import RedisIntegration +from sentry_sdk.integrations.celery import CeleryIntegration BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) - +# Secret Key SECRET_KEY = os.environ.get("SECRET_KEY", get_random_secret_key()) # SECURITY WARNING: don't run with debug turned on in production! -DEBUG = True +DEBUG = False -ALLOWED_HOSTS = [] +# Allowed Hosts +ALLOWED_HOSTS = ["*"] +# Redirect if / is not present +APPEND_SLASH = True # Application definition - INSTALLED_APPS = [ "django.contrib.auth", "django.contrib.contenttypes", @@ -36,12 +49,13 @@ INSTALLED_APPS = [ "corsheaders", "taggit", "django_celery_beat", + "storages", ] +# Middlewares MIDDLEWARE = [ "corsheaders.middleware.CorsMiddleware", "django.middleware.security.SecurityMiddleware", - # "whitenoise.middleware.WhiteNoiseMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", "django.middleware.common.CommonMiddleware", "django.middleware.csrf.CsrfViewMiddleware", @@ -49,8 +63,9 @@ MIDDLEWARE = [ "django.middleware.clickjacking.XFrameOptionsMiddleware", "crum.CurrentRequestUserMiddleware", "django.middleware.gzip.GZipMiddleware", - ] +] +# Rest Framework settings REST_FRAMEWORK = { "DEFAULT_AUTHENTICATION_CLASSES": ( "rest_framework_simplejwt.authentication.JWTAuthentication", @@ -60,13 +75,13 @@ REST_FRAMEWORK = { "DEFAULT_FILTER_BACKENDS": ("django_filters.rest_framework.DjangoFilterBackend",), } -AUTHENTICATION_BACKENDS = ( - "django.contrib.auth.backends.ModelBackend", # default - # "guardian.backends.ObjectPermissionBackend", -) +# Django Auth Backend +AUTHENTICATION_BACKENDS = ("django.contrib.auth.backends.ModelBackend",) # default +# Root Urls ROOT_URLCONF = "plane.urls" +# Templates TEMPLATES = [ { "BACKEND": "django.template.backends.django.DjangoTemplates", @@ -85,52 +100,68 @@ TEMPLATES = [ }, ] +# Cookie Settings +SESSION_COOKIE_SECURE = True +CSRF_COOKIE_SECURE = True -JWT_AUTH = { - "JWT_ENCODE_HANDLER": "rest_framework_jwt.utils.jwt_encode_handler", - "JWT_DECODE_HANDLER": "rest_framework_jwt.utils.jwt_decode_handler", - "JWT_PAYLOAD_HANDLER": "rest_framework_jwt.utils.jwt_payload_handler", - "JWT_PAYLOAD_GET_USER_ID_HANDLER": "rest_framework_jwt.utils.jwt_get_user_id_from_payload_handler", - "JWT_RESPONSE_PAYLOAD_HANDLER": "rest_framework_jwt.utils.jwt_response_payload_handler", - "JWT_SECRET_KEY": SECRET_KEY, - "JWT_GET_USER_SECRET_KEY": None, - "JWT_PUBLIC_KEY": None, - "JWT_PRIVATE_KEY": None, - "JWT_ALGORITHM": "HS256", - "JWT_VERIFY": True, - "JWT_VERIFY_EXPIRATION": True, - "JWT_LEEWAY": 0, - "JWT_EXPIRATION_DELTA": datetime.timedelta(seconds=604800), - "JWT_AUDIENCE": None, - "JWT_ISSUER": None, - "JWT_ALLOW_REFRESH": False, - "JWT_REFRESH_EXPIRATION_DELTA": datetime.timedelta(days=7), - "JWT_AUTH_HEADER_PREFIX": "JWT", - "JWT_AUTH_COOKIE": None, -} +# CORS Settings +CORS_ALLOW_CREDENTIALS = True +CORS_ALLOWED_ORIGINS = os.environ.get("CORS_ALLOWED_ORIGINS", "").split(",") +# Application Settings WSGI_APPLICATION = "plane.wsgi.application" ASGI_APPLICATION = "plane.asgi.application" # Django Sites - SITE_ID = 1 # User Model AUTH_USER_MODEL = "db.User" # Database - -DATABASES = { - "default": { - "ENGINE": "django.db.backends.sqlite3", - "NAME": os.path.join(BASE_DIR, "db.sqlite3"), +if bool(os.environ.get("DATABASE_URL")): + # Parse database configuration from $DATABASE_URL + DATABASES = { + "default": dj_database_url.config(), + } +else: + DATABASES = { + "default": { + "ENGINE": "django.db.backends.postgresql", + "NAME": os.environ.get("POSTGRES_DB"), + "USER": os.environ.get("POSTGRES_USER"), + "PASSWORD": os.environ.get("POSTGRES_PASSWORD"), + "HOST": os.environ.get("POSTGRES_HOST"), + } } -} +# Redis Config +REDIS_URL = os.environ.get("REDIS_URL") +REDIS_SSL = "rediss" in REDIS_URL -# Password validation +if REDIS_SSL: + CACHES = { + "default": { + "BACKEND": "django_redis.cache.RedisCache", + "LOCATION": REDIS_URL, + "OPTIONS": { + "CLIENT_CLASS": "django_redis.client.DefaultClient", + "CONNECTION_POOL_KWARGS": {"ssl_cert_reqs": False}, + }, + } + } +else: + CACHES = { + "default": { + "BACKEND": "django_redis.cache.RedisCache", + "LOCATION": REDIS_URL, + "OPTIONS": { + "CLIENT_CLASS": "django_redis.client.DefaultClient", + }, + } + } +# Password validations AUTH_PASSWORD_VALIDATORS = [ { "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", @@ -147,7 +178,6 @@ AUTH_PASSWORD_VALIDATORS = [ ] # Static files (CSS, JavaScript, Images) - STATIC_URL = "/static/" STATIC_ROOT = os.path.join(BASE_DIR, "static-assets", "collected-static") STATICFILES_DIRS = (os.path.join(BASE_DIR, "static"),) @@ -156,21 +186,19 @@ STATICFILES_DIRS = (os.path.join(BASE_DIR, "static"),) MEDIA_ROOT = "mediafiles" MEDIA_URL = "/media/" - # Internationalization - LANGUAGE_CODE = "en-us" - -TIME_ZONE = "UTC" - USE_I18N = True - USE_L10N = True +# Timezones USE_TZ = True +TIME_ZONE = "UTC" +# Default Auto Field DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" +# Email settings EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend" # Host for sending e-mail. EMAIL_HOST = os.environ.get("EMAIL_HOST") @@ -183,7 +211,30 @@ EMAIL_USE_TLS = os.environ.get("EMAIL_USE_TLS", "1") == "1" EMAIL_USE_SSL = os.environ.get("EMAIL_USE_SSL", "0") == "1" EMAIL_FROM = os.environ.get("EMAIL_FROM", "Team Plane ") +# Storage Settings +STORAGES = { + "staticfiles": { + "BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage", + }, +} +STORAGES["default"] = {"BACKEND": "storages.backends.s3boto3.S3Boto3Storage"} +AWS_ACCESS_KEY_ID = os.environ.get("AWS_ACCESS_KEY_ID", "access-key") +AWS_SECRET_ACCESS_KEY = os.environ.get("AWS_SECRET_ACCESS_KEY", "secret-key") +AWS_STORAGE_BUCKET_NAME = os.environ.get("AWS_S3_BUCKET_NAME", "uploads") +AWS_DEFAULT_ACL = "public-read" +AWS_QUERYSTRING_AUTH = False +AWS_S3_FILE_OVERWRITE = False +AWS_S3_ENDPOINT_URL = os.environ.get("AWS_S3_ENDPOINT_URL", None) or os.environ.get( + "MINIO_ENDPOINT_URL", None +) +if AWS_S3_ENDPOINT_URL: + parsed_url = urlparse(os.environ.get("WEB_URL", "http://localhost")) + AWS_S3_CUSTOM_DOMAIN = f"{parsed_url.netloc}/{AWS_STORAGE_BUCKET_NAME}" + AWS_S3_URL_PROTOCOL = f"{parsed_url.scheme}:" + + +# JWT Auth Configuration SIMPLE_JWT = { "ACCESS_TOKEN_LIFETIME": timedelta(minutes=10080), "REFRESH_TOKEN_LIFETIME": timedelta(days=43200), @@ -211,7 +262,71 @@ SIMPLE_JWT = { "SLIDING_TOKEN_REFRESH_LIFETIME": timedelta(days=1), } + +# Celery Configuration CELERY_TIMEZONE = TIME_ZONE -CELERY_TASK_SERIALIZER = 'json' -CELERY_ACCEPT_CONTENT = ['application/json'] -CELERY_IMPORTS = ("plane.bgtasks.issue_automation_task","plane.bgtasks.exporter_expired_task") +CELERY_TASK_SERIALIZER = "json" +CELERY_ACCEPT_CONTENT = ["application/json"] + +if REDIS_SSL: + redis_url = os.environ.get("REDIS_URL") + broker_url = ( + f"{redis_url}?ssl_cert_reqs={ssl.CERT_NONE.name}&ssl_ca_certs={certifi.where()}" + ) + CELERY_BROKER_URL = broker_url + CELERY_RESULT_BACKEND = broker_url +else: + CELERY_BROKER_URL = REDIS_URL + CELERY_RESULT_BACKEND = REDIS_URL + +CELERY_IMPORTS = ( + "plane.bgtasks.issue_automation_task", + "plane.bgtasks.exporter_expired_task", +) + + +# Sentry Settings +# Enable Sentry Settings +if bool(os.environ.get("SENTRY_DSN", False)): + sentry_sdk.init( + dsn=os.environ.get("SENTRY_DSN", ""), + integrations=[ + DjangoIntegration(), + RedisIntegration(), + CeleryIntegration(monitor_beat_tasks=True), + ], + traces_sample_rate=1, + send_default_pii=True, + environment=os.environ.get("ENVIRONMENT", "development"), + profiles_sample_rate=1.0, + ) + + +# Application Envs +PROXY_BASE_URL = os.environ.get("PROXY_BASE_URL", False) # For External +SLACK_BOT_TOKEN = os.environ.get("SLACK_BOT_TOKEN", False) +FILE_SIZE_LIMIT = int(os.environ.get("FILE_SIZE_LIMIT", 5242880)) +ENABLE_SIGNUP = os.environ.get("ENABLE_SIGNUP", "1") == "1" + +# Unsplash Access key +UNSPLASH_ACCESS_KEY = os.environ.get("UNSPLASH_ACCESS_KEY") +# Github Access Token +GITHUB_ACCESS_TOKEN = os.environ.get("GITHUB_ACCESS_TOKEN", False) + +# Analytics +ANALYTICS_SECRET_KEY = os.environ.get("ANALYTICS_SECRET_KEY", False) +ANALYTICS_BASE_API = os.environ.get("ANALYTICS_BASE_API", False) + +# Open AI Settings +OPENAI_API_BASE = os.environ.get("OPENAI_API_BASE", "https://api.openai.com/v1") +OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", False) +GPT_ENGINE = os.environ.get("GPT_ENGINE", "gpt-3.5-turbo") + +# Scout Settings +SCOUT_MONITOR = os.environ.get("SCOUT_MONITOR", False) +SCOUT_KEY = os.environ.get("SCOUT_KEY", "") +SCOUT_NAME = "Plane" + +# Set the variable true if running in docker environment +DOCKERIZED = int(os.environ.get("DOCKERIZED", 1)) == 1 +USE_MINIO = int(os.environ.get("USE_MINIO", 0)) == 1 diff --git a/apiserver/plane/settings/local.py b/apiserver/plane/settings/local.py index 76586b0fe..9fa5ed0aa 100644 --- a/apiserver/plane/settings/local.py +++ b/apiserver/plane/settings/local.py @@ -1,123 +1,39 @@ -"""Development settings and globals.""" - -from __future__ import absolute_import - -import dj_database_url -import sentry_sdk -from sentry_sdk.integrations.django import DjangoIntegration -from sentry_sdk.integrations.redis import RedisIntegration - - +"""Development settings""" from .common import * # noqa -DEBUG = int(os.environ.get("DEBUG", 1)) == 1 +DEBUG = True ALLOWED_HOSTS = [ "*", ] +# Debug Toolbar settings +INSTALLED_APPS += ("debug_toolbar",) +MIDDLEWARE += ("debug_toolbar.middleware.DebugToolbarMiddleware",) + +DEBUG_TOOLBAR_PATCH_SETTINGS = False + +# Only show emails in console don't send it to smtp EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" - -DATABASES = { - "default": { - "ENGINE": "django.db.backends.postgresql", - "NAME": os.environ.get("PGUSER", "plane"), - "USER": "", - "PASSWORD": "", - "HOST": os.environ.get("PGHOST", "localhost"), - } -} - -DOCKERIZED = int(os.environ.get("DOCKERIZED", 0)) == 1 - -USE_MINIO = int(os.environ.get("USE_MINIO", 0)) == 1 - -FILE_SIZE_LIMIT = int(os.environ.get("FILE_SIZE_LIMIT", 5242880)) - -if DOCKERIZED: - DATABASES["default"] = dj_database_url.config() - CACHES = { "default": { "BACKEND": "django.core.cache.backends.locmem.LocMemCache", } } -INSTALLED_APPS += ("debug_toolbar",) - -MIDDLEWARE += ("debug_toolbar.middleware.DebugToolbarMiddleware",) - -DEBUG_TOOLBAR_PATCH_SETTINGS = False - INTERNAL_IPS = ("127.0.0.1",) CORS_ORIGIN_ALLOW_ALL = True -if os.environ.get("SENTRY_DSN", False): - sentry_sdk.init( - dsn=os.environ.get("SENTRY_DSN"), - integrations=[DjangoIntegration(), RedisIntegration()], - # If you wish to associate users to errors (assuming you are using - # django.contrib.auth) you may enable sending PII data. - send_default_pii=True, - environment="local", - traces_sample_rate=0.7, - profiles_sample_rate=1.0, - ) -else: - LOGGING = { - "version": 1, - "disable_existing_loggers": False, - "handlers": { - "console": { - "class": "logging.StreamHandler", - }, - }, - "root": { - "handlers": ["console"], - "level": "DEBUG", - }, - "loggers": { - "*": { - "handlers": ["console"], - "level": "DEBUG", - "propagate": True, - }, - }, - } - -REDIS_HOST = "localhost" -REDIS_PORT = 6379 -REDIS_URL = os.environ.get("REDIS_URL") - - MEDIA_URL = "/uploads/" MEDIA_ROOT = os.path.join(BASE_DIR, "uploads") -if DOCKERIZED: - REDIS_URL = os.environ.get("REDIS_URL") - -WEB_URL = os.environ.get("WEB_URL", "http://localhost:3000") -PROXY_BASE_URL = os.environ.get("PROXY_BASE_URL", False) - -ANALYTICS_SECRET_KEY = os.environ.get("ANALYTICS_SECRET_KEY", False) -ANALYTICS_BASE_API = os.environ.get("ANALYTICS_BASE_API", False) - -OPENAI_API_BASE = os.environ.get("OPENAI_API_BASE", "https://api.openai.com/v1") -OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", False) -GPT_ENGINE = os.environ.get("GPT_ENGINE", "gpt-3.5-turbo") - -SLACK_BOT_TOKEN = os.environ.get("SLACK_BOT_TOKEN", False) - -LOGGER_BASE_URL = os.environ.get("LOGGER_BASE_URL", False) - -CELERY_RESULT_BACKEND = os.environ.get("REDIS_URL") -CELERY_BROKER_URL = os.environ.get("REDIS_URL") - -GITHUB_ACCESS_TOKEN = os.environ.get("GITHUB_ACCESS_TOKEN", False) - -ENABLE_SIGNUP = os.environ.get("ENABLE_SIGNUP", "1") == "1" - -# Unsplash Access key -UNSPLASH_ACCESS_KEY = os.environ.get("UNSPLASH_ACCESS_KEY") +# For local settings +CORS_ALLOW_ALL_ORIGINS = True +CORS_ALLOWED_ORIGINS = [ + "http://localhost:3000", + "http://127.0.0.1:3000", + "http://localhost:4000", + "http://127.0.0.1:4000", +] diff --git a/apiserver/plane/settings/production.py b/apiserver/plane/settings/production.py index 541a0cfd4..b230bbc32 100644 --- a/apiserver/plane/settings/production.py +++ b/apiserver/plane/settings/production.py @@ -1,282 +1,13 @@ -"""Production settings and globals.""" -import ssl -import certifi - -import dj_database_url - -import sentry_sdk -from sentry_sdk.integrations.django import DjangoIntegration -from sentry_sdk.integrations.redis import RedisIntegration -from urllib.parse import urlparse - +"""Production settings""" from .common import * # noqa -# Database +# SECURITY WARNING: don't run with debug turned on in production! DEBUG = int(os.environ.get("DEBUG", 0)) == 1 -if bool(os.environ.get("DATABASE_URL")): - # Parse database configuration from $DATABASE_URL - DATABASES["default"] = dj_database_url.config() -else: - DATABASES = { - "default": { - "ENGINE": "django.db.backends.postgresql", - "NAME": os.environ.get("POSTGRES_DB"), - "USER": os.environ.get("POSTGRES_USER"), - "PASSWORD": os.environ.get("POSTGRES_PASSWORD"), - "HOST": os.environ.get("POSTGRES_HOST"), - } - } - - -SITE_ID = 1 - -# Set the variable true if running in docker environment -DOCKERIZED = int(os.environ.get("DOCKERIZED", 0)) == 1 - -USE_MINIO = int(os.environ.get("USE_MINIO", 0)) == 1 - -FILE_SIZE_LIMIT = int(os.environ.get("FILE_SIZE_LIMIT", 5242880)) - -# Enable Connection Pooling (if desired) -# DATABASES['default']['ENGINE'] = 'django_postgrespool' - # Honor the 'X-Forwarded-Proto' header for request.is_secure() SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") - -# TODO: Make it FALSE and LIST DOMAINS IN FULL PROD. -CORS_ALLOW_ALL_ORIGINS = True - - -CORS_ALLOW_METHODS = [ - "DELETE", - "GET", - "OPTIONS", - "PATCH", - "POST", - "PUT", -] - -CORS_ALLOW_HEADERS = [ - "accept", - "accept-encoding", - "authorization", - "content-type", - "dnt", - "origin", - "user-agent", - "x-csrftoken", - "x-requested-with", -] - -CORS_ALLOW_CREDENTIALS = True - INSTALLED_APPS += ("scout_apm.django",) -STORAGES = { - "staticfiles": { - "BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage", - }, -} - -if bool(os.environ.get("SENTRY_DSN", False)): - sentry_sdk.init( - dsn=os.environ.get("SENTRY_DSN", ""), - integrations=[DjangoIntegration(), RedisIntegration()], - # If you wish to associate users to errors (assuming you are using - # django.contrib.auth) you may enable sending PII data. - traces_sample_rate=1, - send_default_pii=True, - environment="production", - profiles_sample_rate=1.0, - ) - -if DOCKERIZED and USE_MINIO: - INSTALLED_APPS += ("storages",) - STORAGES["default"] = {"BACKEND": "storages.backends.s3boto3.S3Boto3Storage"} - # The AWS access key to use. - AWS_ACCESS_KEY_ID = os.environ.get("AWS_ACCESS_KEY_ID", "access-key") - # The AWS secret access key to use. - AWS_SECRET_ACCESS_KEY = os.environ.get("AWS_SECRET_ACCESS_KEY", "secret-key") - # The name of the bucket to store files in. - AWS_STORAGE_BUCKET_NAME = os.environ.get("AWS_S3_BUCKET_NAME", "uploads") - # The full URL to the S3 endpoint. Leave blank to use the default region URL. - AWS_S3_ENDPOINT_URL = os.environ.get( - "AWS_S3_ENDPOINT_URL", "http://plane-minio:9000" - ) - # Default permissions - AWS_DEFAULT_ACL = "public-read" - AWS_QUERYSTRING_AUTH = False - AWS_S3_FILE_OVERWRITE = False - - # Custom Domain settings - parsed_url = urlparse(os.environ.get("WEB_URL", "http://localhost")) - AWS_S3_CUSTOM_DOMAIN = f"{parsed_url.netloc}/{AWS_STORAGE_BUCKET_NAME}" - AWS_S3_URL_PROTOCOL = f"{parsed_url.scheme}:" -else: - # The AWS region to connect to. - AWS_REGION = os.environ.get("AWS_REGION", "") - - # The AWS access key to use. - AWS_ACCESS_KEY_ID = os.environ.get("AWS_ACCESS_KEY_ID", "") - - # The AWS secret access key to use. - AWS_SECRET_ACCESS_KEY = os.environ.get("AWS_SECRET_ACCESS_KEY", "") - - # The optional AWS session token to use. - # AWS_SESSION_TOKEN = "" - - # The name of the bucket to store files in. - AWS_S3_BUCKET_NAME = os.environ.get("AWS_S3_BUCKET_NAME") - - # How to construct S3 URLs ("auto", "path", "virtual"). - AWS_S3_ADDRESSING_STYLE = "auto" - - # The full URL to the S3 endpoint. Leave blank to use the default region URL. - AWS_S3_ENDPOINT_URL = os.environ.get("AWS_S3_ENDPOINT_URL", "") - - # A prefix to be applied to every stored file. This will be joined to every filename using the "/" separator. - AWS_S3_KEY_PREFIX = "" - - # Whether to enable authentication for stored files. If True, then generated URLs will include an authentication - # token valid for `AWS_S3_MAX_AGE_SECONDS`. If False, then generated URLs will not include an authentication token, - # and their permissions will be set to "public-read". - AWS_S3_BUCKET_AUTH = False - - # How long generated URLs are valid for. This affects the expiry of authentication tokens if `AWS_S3_BUCKET_AUTH` - # is True. It also affects the "Cache-Control" header of the files. - # Important: Changing this setting will not affect existing files. - AWS_S3_MAX_AGE_SECONDS = 60 * 60 # 1 hours. - - # A URL prefix to be used for generated URLs. This is useful if your bucket is served through a CDN. This setting - # cannot be used with `AWS_S3_BUCKET_AUTH`. - AWS_S3_PUBLIC_URL = "" - - # If True, then files will be stored with reduced redundancy. Check the S3 documentation and make sure you - # understand the consequences before enabling. - # Important: Changing this setting will not affect existing files. - AWS_S3_REDUCED_REDUNDANCY = False - - # The Content-Disposition header used when the file is downloaded. This can be a string, or a function taking a - # single `name` argument. - # Important: Changing this setting will not affect existing files. - AWS_S3_CONTENT_DISPOSITION = "" - - # The Content-Language header used when the file is downloaded. This can be a string, or a function taking a - # single `name` argument. - # Important: Changing this setting will not affect existing files. - AWS_S3_CONTENT_LANGUAGE = "" - - # A mapping of custom metadata for each file. Each value can be a string, or a function taking a - # single `name` argument. - # Important: Changing this setting will not affect existing files. - AWS_S3_METADATA = {} - - # If True, then files will be stored using AES256 server-side encryption. - # If this is a string value (e.g., "aws:kms"), that encryption type will be used. - # Otherwise, server-side encryption is not be enabled. - # Important: Changing this setting will not affect existing files. - AWS_S3_ENCRYPT_KEY = False - - # The AWS S3 KMS encryption key ID (the `SSEKMSKeyId` parameter) is set from this string if present. - # This is only relevant if AWS S3 KMS server-side encryption is enabled (above). - # AWS_S3_KMS_ENCRYPTION_KEY_ID = "" - - # If True, then text files will be stored using gzip content encoding. Files will only be gzipped if their - # compressed size is smaller than their uncompressed size. - # Important: Changing this setting will not affect existing files. - AWS_S3_GZIP = True - - # The signature version to use for S3 requests. - AWS_S3_SIGNATURE_VERSION = None - - # If True, then files with the same name will overwrite each other. By default it's set to False to have - # extra characters appended. - AWS_S3_FILE_OVERWRITE = False - - STORAGES["default"] = { - "BACKEND": "django_s3_storage.storage.S3Storage", - } -# AWS Settings End - -# Enable Connection Pooling (if desired) -# DATABASES['default']['ENGINE'] = 'django_postgrespool' - # Honor the 'X-Forwarded-Proto' header for request.is_secure() SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") - -# Allow all host headers -ALLOWED_HOSTS = [ - "*", -] - - -SESSION_COOKIE_SECURE = True -CSRF_COOKIE_SECURE = True - - -REDIS_URL = os.environ.get("REDIS_URL") - -if DOCKERIZED: - CACHES = { - "default": { - "BACKEND": "django_redis.cache.RedisCache", - "LOCATION": REDIS_URL, - "OPTIONS": { - "CLIENT_CLASS": "django_redis.client.DefaultClient", - }, - } - } -else: - CACHES = { - "default": { - "BACKEND": "django_redis.cache.RedisCache", - "LOCATION": REDIS_URL, - "OPTIONS": { - "CLIENT_CLASS": "django_redis.client.DefaultClient", - "CONNECTION_POOL_KWARGS": {"ssl_cert_reqs": False}, - }, - } - } - - -WEB_URL = os.environ.get("WEB_URL", "https://app.plane.so") - -PROXY_BASE_URL = os.environ.get("PROXY_BASE_URL", False) - -ANALYTICS_SECRET_KEY = os.environ.get("ANALYTICS_SECRET_KEY", False) -ANALYTICS_BASE_API = os.environ.get("ANALYTICS_BASE_API", False) - -OPENAI_API_BASE = os.environ.get("OPENAI_API_BASE", "https://api.openai.com/v1") -OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", False) -GPT_ENGINE = os.environ.get("GPT_ENGINE", "gpt-3.5-turbo") - -SLACK_BOT_TOKEN = os.environ.get("SLACK_BOT_TOKEN", False) - -LOGGER_BASE_URL = os.environ.get("LOGGER_BASE_URL", False) - -redis_url = os.environ.get("REDIS_URL") -broker_url = ( - f"{redis_url}?ssl_cert_reqs={ssl.CERT_NONE.name}&ssl_ca_certs={certifi.where()}" -) - -if DOCKERIZED: - CELERY_BROKER_URL = REDIS_URL - CELERY_RESULT_BACKEND = REDIS_URL -else: - CELERY_BROKER_URL = broker_url - CELERY_RESULT_BACKEND = broker_url - -GITHUB_ACCESS_TOKEN = os.environ.get("GITHUB_ACCESS_TOKEN", False) - -# Enable or Disable signups -ENABLE_SIGNUP = os.environ.get("ENABLE_SIGNUP", "1") == "1" - -# Scout Settings -SCOUT_MONITOR = os.environ.get("SCOUT_MONITOR", False) -SCOUT_KEY = os.environ.get("SCOUT_KEY", "") -SCOUT_NAME = "Plane" - -# Unsplash Access key -UNSPLASH_ACCESS_KEY = os.environ.get("UNSPLASH_ACCESS_KEY") diff --git a/apiserver/plane/settings/selfhosted.py b/apiserver/plane/settings/selfhosted.py deleted file mode 100644 index ee529a7c3..000000000 --- a/apiserver/plane/settings/selfhosted.py +++ /dev/null @@ -1,129 +0,0 @@ -"""Self hosted settings and globals.""" -from urllib.parse import urlparse - -import dj_database_url -from urllib.parse import urlparse - - -from .common import * # noqa - -# Database -DEBUG = int(os.environ.get("DEBUG", 0)) == 1 - -# Docker configurations -DOCKERIZED = 1 -USE_MINIO = 1 - -DATABASES = { - "default": { - "ENGINE": "django.db.backends.postgresql", - "NAME": "plane", - "USER": os.environ.get("PGUSER", ""), - "PASSWORD": os.environ.get("PGPASSWORD", ""), - "HOST": os.environ.get("PGHOST", ""), - } -} - -# Parse database configuration from $DATABASE_URL -DATABASES["default"] = dj_database_url.config() -SITE_ID = 1 - -# File size limit -FILE_SIZE_LIMIT = int(os.environ.get("FILE_SIZE_LIMIT", 5242880)) - -CORS_ALLOW_METHODS = [ - "DELETE", - "GET", - "OPTIONS", - "PATCH", - "POST", - "PUT", -] - -CORS_ALLOW_HEADERS = [ - "accept", - "accept-encoding", - "authorization", - "content-type", - "dnt", - "origin", - "user-agent", - "x-csrftoken", - "x-requested-with", -] - -CORS_ALLOW_CREDENTIALS = True -CORS_ALLOW_ALL_ORIGINS = True - -STORAGES = { - "staticfiles": { - "BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage", - }, -} - -INSTALLED_APPS += ("storages",) -STORAGES["default"] = {"BACKEND": "storages.backends.s3boto3.S3Boto3Storage"} -# The AWS access key to use. -AWS_ACCESS_KEY_ID = os.environ.get("AWS_ACCESS_KEY_ID", "access-key") -# The AWS secret access key to use. -AWS_SECRET_ACCESS_KEY = os.environ.get("AWS_SECRET_ACCESS_KEY", "secret-key") -# The name of the bucket to store files in. -AWS_STORAGE_BUCKET_NAME = os.environ.get("AWS_S3_BUCKET_NAME", "uploads") -# The full URL to the S3 endpoint. Leave blank to use the default region URL. -AWS_S3_ENDPOINT_URL = os.environ.get( - "AWS_S3_ENDPOINT_URL", "http://plane-minio:9000" -) -# Default permissions -AWS_DEFAULT_ACL = "public-read" -AWS_QUERYSTRING_AUTH = False -AWS_S3_FILE_OVERWRITE = False - -# Custom Domain settings -parsed_url = urlparse(os.environ.get("WEB_URL", "http://localhost")) -AWS_S3_CUSTOM_DOMAIN = f"{parsed_url.netloc}/{AWS_STORAGE_BUCKET_NAME}" -AWS_S3_URL_PROTOCOL = f"{parsed_url.scheme}:" - -# Honor the 'X-Forwarded-Proto' header for request.is_secure() -SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") - -# Allow all host headers -ALLOWED_HOSTS = [ - "*", -] - -# Security settings -SESSION_COOKIE_SECURE = True -CSRF_COOKIE_SECURE = True - -# Redis URL -REDIS_URL = os.environ.get("REDIS_URL") - -# Caches -CACHES = { - "default": { - "BACKEND": "django_redis.cache.RedisCache", - "LOCATION": REDIS_URL, - "OPTIONS": { - "CLIENT_CLASS": "django_redis.client.DefaultClient", - }, - } -} - -# URL used for email redirects -WEB_URL = os.environ.get("WEB_URL", "http://localhost") - -# Celery settings -CELERY_BROKER_URL = REDIS_URL -CELERY_RESULT_BACKEND = REDIS_URL - -# Enable or Disable signups -ENABLE_SIGNUP = os.environ.get("ENABLE_SIGNUP", "1") == "1" - -# Analytics -ANALYTICS_BASE_API = False - -# OPEN AI Settings -OPENAI_API_BASE = os.environ.get("OPENAI_API_BASE", "https://api.openai.com/v1") -OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", False) -GPT_ENGINE = os.environ.get("GPT_ENGINE", "gpt-3.5-turbo") - diff --git a/apiserver/plane/settings/staging.py b/apiserver/plane/settings/staging.py deleted file mode 100644 index fe4732343..000000000 --- a/apiserver/plane/settings/staging.py +++ /dev/null @@ -1,223 +0,0 @@ -"""Production settings and globals.""" -from urllib.parse import urlparse -import ssl -import certifi - -import dj_database_url - -import sentry_sdk -from sentry_sdk.integrations.django import DjangoIntegration -from sentry_sdk.integrations.redis import RedisIntegration - -from .common import * # noqa - -# Database -DEBUG = int(os.environ.get("DEBUG", 1)) == 1 -DATABASES = { - "default": { - "ENGINE": "django.db.backends.postgresql", - "NAME": os.environ.get("PGUSER", "plane"), - "USER": "", - "PASSWORD": "", - "HOST": os.environ.get("PGHOST", "localhost"), - } -} - -# CORS WHITELIST ON PROD -CORS_ORIGIN_WHITELIST = [ - # "https://example.com", - # "https://sub.example.com", - # "http://localhost:8080", - # "http://127.0.0.1:9000" -] -# Parse database configuration from $DATABASE_URL -DATABASES["default"] = dj_database_url.config() -SITE_ID = 1 - -# Enable Connection Pooling (if desired) -# DATABASES['default']['ENGINE'] = 'django_postgrespool' - -# Honor the 'X-Forwarded-Proto' header for request.is_secure() -SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") - -# Allow all host headers -ALLOWED_HOSTS = ["*"] - -# TODO: Make it FALSE and LIST DOMAINS IN FULL PROD. -CORS_ALLOW_ALL_ORIGINS = True - -STORAGES = { - "staticfiles": { - "BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage", - }, -} - - -# Make true if running in a docker environment -DOCKERIZED = int(os.environ.get("DOCKERIZED", 0)) == 1 -FILE_SIZE_LIMIT = int(os.environ.get("FILE_SIZE_LIMIT", 5242880)) -USE_MINIO = int(os.environ.get("USE_MINIO", 0)) == 1 - -sentry_sdk.init( - dsn=os.environ.get("SENTRY_DSN"), - integrations=[DjangoIntegration(), RedisIntegration()], - # If you wish to associate users to errors (assuming you are using - # django.contrib.auth) you may enable sending PII data. - traces_sample_rate=1, - send_default_pii=True, - environment="staging", - profiles_sample_rate=1.0, -) - -# The AWS region to connect to. -AWS_REGION = os.environ.get("AWS_REGION") - -# The AWS access key to use. -AWS_ACCESS_KEY_ID = os.environ.get("AWS_ACCESS_KEY_ID") - -# The AWS secret access key to use. -AWS_SECRET_ACCESS_KEY = os.environ.get("AWS_SECRET_ACCESS_KEY") - -# The optional AWS session token to use. -# AWS_SESSION_TOKEN = "" - - -# The name of the bucket to store files in. -AWS_S3_BUCKET_NAME = os.environ.get("AWS_S3_BUCKET_NAME") - -# How to construct S3 URLs ("auto", "path", "virtual"). -AWS_S3_ADDRESSING_STYLE = "auto" - -# The full URL to the S3 endpoint. Leave blank to use the default region URL. -AWS_S3_ENDPOINT_URL = os.environ.get("AWS_S3_ENDPOINT_URL", "") - -# A prefix to be applied to every stored file. This will be joined to every filename using the "/" separator. -AWS_S3_KEY_PREFIX = "" - -# Whether to enable authentication for stored files. If True, then generated URLs will include an authentication -# token valid for `AWS_S3_MAX_AGE_SECONDS`. If False, then generated URLs will not include an authentication token, -# and their permissions will be set to "public-read". -AWS_S3_BUCKET_AUTH = False - -# How long generated URLs are valid for. This affects the expiry of authentication tokens if `AWS_S3_BUCKET_AUTH` -# is True. It also affects the "Cache-Control" header of the files. -# Important: Changing this setting will not affect existing files. -AWS_S3_MAX_AGE_SECONDS = 60 * 60 # 1 hours. - -# A URL prefix to be used for generated URLs. This is useful if your bucket is served through a CDN. This setting -# cannot be used with `AWS_S3_BUCKET_AUTH`. -AWS_S3_PUBLIC_URL = "" - -# If True, then files will be stored with reduced redundancy. Check the S3 documentation and make sure you -# understand the consequences before enabling. -# Important: Changing this setting will not affect existing files. -AWS_S3_REDUCED_REDUNDANCY = False - -# The Content-Disposition header used when the file is downloaded. This can be a string, or a function taking a -# single `name` argument. -# Important: Changing this setting will not affect existing files. -AWS_S3_CONTENT_DISPOSITION = "" - -# The Content-Language header used when the file is downloaded. This can be a string, or a function taking a -# single `name` argument. -# Important: Changing this setting will not affect existing files. -AWS_S3_CONTENT_LANGUAGE = "" - -# A mapping of custom metadata for each file. Each value can be a string, or a function taking a -# single `name` argument. -# Important: Changing this setting will not affect existing files. -AWS_S3_METADATA = {} - -# If True, then files will be stored using AES256 server-side encryption. -# If this is a string value (e.g., "aws:kms"), that encryption type will be used. -# Otherwise, server-side encryption is not be enabled. -# Important: Changing this setting will not affect existing files. -AWS_S3_ENCRYPT_KEY = False - -# The AWS S3 KMS encryption key ID (the `SSEKMSKeyId` parameter) is set from this string if present. -# This is only relevant if AWS S3 KMS server-side encryption is enabled (above). -# AWS_S3_KMS_ENCRYPTION_KEY_ID = "" - -# If True, then text files will be stored using gzip content encoding. Files will only be gzipped if their -# compressed size is smaller than their uncompressed size. -# Important: Changing this setting will not affect existing files. -AWS_S3_GZIP = True - -# The signature version to use for S3 requests. -AWS_S3_SIGNATURE_VERSION = None - -# If True, then files with the same name will overwrite each other. By default it's set to False to have -# extra characters appended. -AWS_S3_FILE_OVERWRITE = False - -# AWS Settings End -STORAGES["default"] = { - "BACKEND": "django_s3_storage.storage.S3Storage", -} - -# Enable Connection Pooling (if desired) -# DATABASES['default']['ENGINE'] = 'django_postgrespool' - -# Honor the 'X-Forwarded-Proto' header for request.is_secure() -SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") - -# Allow all host headers -ALLOWED_HOSTS = [ - "*", -] - -SESSION_COOKIE_SECURE = True -CSRF_COOKIE_SECURE = True - - -REDIS_URL = os.environ.get("REDIS_URL") - -CACHES = { - "default": { - "BACKEND": "django_redis.cache.RedisCache", - "LOCATION": REDIS_URL, - "OPTIONS": { - "CLIENT_CLASS": "django_redis.client.DefaultClient", - "CONNECTION_POOL_KWARGS": {"ssl_cert_reqs": False}, - }, - } -} - -RQ_QUEUES = { - "default": { - "USE_REDIS_CACHE": "default", - } -} - - -WEB_URL = os.environ.get("WEB_URL") - -PROXY_BASE_URL = os.environ.get("PROXY_BASE_URL", False) - -ANALYTICS_SECRET_KEY = os.environ.get("ANALYTICS_SECRET_KEY", False) -ANALYTICS_BASE_API = os.environ.get("ANALYTICS_BASE_API", False) - - -OPENAI_API_BASE = os.environ.get("OPENAI_API_BASE", "https://api.openai.com/v1") -OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", False) -GPT_ENGINE = os.environ.get("GPT_ENGINE", "gpt-3.5-turbo") - -SLACK_BOT_TOKEN = os.environ.get("SLACK_BOT_TOKEN", False) - -LOGGER_BASE_URL = os.environ.get("LOGGER_BASE_URL", False) - -redis_url = os.environ.get("REDIS_URL") -broker_url = ( - f"{redis_url}?ssl_cert_reqs={ssl.CERT_NONE.name}&ssl_ca_certs={certifi.where()}" -) - -CELERY_RESULT_BACKEND = broker_url -CELERY_BROKER_URL = broker_url - -GITHUB_ACCESS_TOKEN = os.environ.get("GITHUB_ACCESS_TOKEN", False) - -ENABLE_SIGNUP = os.environ.get("ENABLE_SIGNUP", "1") == "1" - - -# Unsplash Access key -UNSPLASH_ACCESS_KEY = os.environ.get("UNSPLASH_ACCESS_KEY") diff --git a/apiserver/plane/settings/test.py b/apiserver/plane/settings/test.py index 6c009997c..34ae16555 100644 --- a/apiserver/plane/settings/test.py +++ b/apiserver/plane/settings/test.py @@ -1,45 +1,9 @@ -from __future__ import absolute_import - +"""Test Settings""" from .common import * # noqa DEBUG = True -INSTALLED_APPS.append("plane.tests") +# Send it in a dummy outbox +EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend" -if os.environ.get('GITHUB_WORKFLOW'): - DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.postgresql', - 'NAME': 'github_actions', - 'USER': 'postgres', - 'PASSWORD': 'postgres', - 'HOST': '127.0.0.1', - 'PORT': '5432', - } - } -else: - DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.postgresql', - 'NAME': 'plane_test', - 'USER': 'postgres', - 'PASSWORD': 'password123', - 'HOST': '127.0.0.1', - 'PORT': '5432', - } - } - -REDIS_HOST = "localhost" -REDIS_PORT = 6379 -REDIS_URL = False - -RQ_QUEUES = { - "default": { - "HOST": "localhost", - "PORT": 6379, - "DB": 0, - "DEFAULT_TIMEOUT": 360, - }, -} - -WEB_URL = "http://localhost:3000" +INSTALLED_APPS.append("plane.tests",) diff --git a/deploy/selfhost/docker-compose.yml b/deploy/selfhost/docker-compose.yml index c571291cf..70a88d92b 100644 --- a/deploy/selfhost/docker-compose.yml +++ b/deploy/selfhost/docker-compose.yml @@ -10,6 +10,7 @@ x-app-env : &app-env - SENTRY_DSN=${SENTRY_DSN:-""} - GITHUB_CLIENT_SECRET=${GITHUB_CLIENT_SECRET:-""} - DOCKERIZED=${DOCKERIZED:-1} + - CORS_ALLOWED_ORIGINS=${CORS_ALLOWED_ORIGINS:-http://localhost} # Gunicorn Workers - GUNICORN_WORKERS=${GUNICORN_WORKERS:-2} #DB SETTINGS diff --git a/deploy/selfhost/variables.env b/deploy/selfhost/variables.env index b12031126..abbb84a52 100644 --- a/deploy/selfhost/variables.env +++ b/deploy/selfhost/variables.env @@ -12,6 +12,7 @@ NEXT_PUBLIC_DEPLOY_URL=http://localhost/spaces SENTRY_DSN="" GITHUB_CLIENT_SECRET="" DOCKERIZED=1 +CORS_ALLOWED_ORIGINS="http://localhost" #DB SETTINGS PGHOST=plane-db