diff --git a/README.md b/README.md index 4af888d6..1e1cf320 100644 --- a/README.md +++ b/README.md @@ -7,33 +7,80 @@ ## Built With - - Django 4.0.5 - - Bootstrap 5.2.0-beta1 - - htmx 1.7.0 +- Django 4.0.5 +- Bootstrap 5.2.0-beta1 +- htmx 1.7.0 -# Getting Started +## Getting Started -## Prerequisites +### Prerequisites -### Installing dependencies -- pip - ```bash - cd ./app/entirety - pip install -r requirements.txt - ``` -- pre-commit - ```bash - pre-commit install - ``` +#### Installing dependencies + +pip + +```bash + cd ./app/entirety + pip install -r requirements.txt +``` + +pre-commit + +```bash + pre-commit install +``` + +#### create .env File + +```bash + cp .env.EXAMPLE .env +``` ## Usage +Migrate Database + +```bash + python manage.py makemigrations projects users examples + python manage.py migrate +``` + Starting the Django server: ```bash python manage.py runserver ``` +## Contributing + +See the [contributing guide](./docs/CONTRIBUTING.md) for detailed instructions on how to get started with our project. + +## Development + +To run the application in your development setup you'll need to +provide following settings in your env file. + +### Required + +* [DJANGO_SECRET_KEY](./docs/SETTINGS.md#django_secret_key) +* [OIDC_OP_AUTHORIZATION_ENDPOINT](./docs/SETTINGS.md#oidc_op_authorization_endpoint) +* [OIDC_OP_JWKS_ENDPOINT](./docs/SETTINGS.md#oidc_op_jwks_endpoint) +* [OIDC_OP_TOKEN_ENDPOINT](./docs/SETTINGS.md#oidc_op_token_endpoint) +* [OIDC_OP_USER_ENDPOINT](./docs/SETTINGS.md#oidc_op_user_endpoint) +* [OIDC_RP_CLIENT_ID](./docs/SETTINGS.md#oidc_rp_client_id) +* [OIDC_RP_CLIENT_SECRET](./docs/SETTINGS.md#oidc_rp_client_secret) + +### Optional + +* [DJANGO_DEBUG](./docs/SETTINGS.md#django_debug) +* [COMPRESS_ENABLED](./docs/SETTINGS.md#compress_enabled) + +For a full list of settings see [settings](./docs/SETTINGS.md). + +## Changelog + +See [changelog](./docs/CHANGELOG.md) for detailed overview of changes. + ## Contact [@SBlechmann](https://github.com/SBlechmann) diff --git a/app/Entirety/.env.EXAMPLE b/app/Entirety/.env.EXAMPLE new file mode 100644 index 00000000..a95802ad --- /dev/null +++ b/app/Entirety/.env.EXAMPLE @@ -0,0 +1,25 @@ +# Django +DJANGO_SECRET_KEY= +DJANGO_DEBUG=False +ALLOWED_HOSTS=["localhost","127.0.0.1"] +LANGUAGE_CODE=en-us +TIME_ZONE=Europe/Berlin + +# JS/SCSS compression +COMPRESS_ENABLED=True + +# OIDC +LOGIN_URL=/oidc/authenticate +LOGIN_REDIRECT_URL=/oidc/callback/ +LOGOUT_REDIRECT_URL=/ +OIDC_OP_AUTHORIZATION_ENDPOINT= +OIDC_OP_JWKS_ENDPOINT= +OIDC_OP_TOKEN_ENDPOINT= +OIDC_OP_USER_ENDPOINT= +OIDC_RP_CLIENT_ID= +OIDC_RP_CLIENT_SECRET= +OIDC_SUPER_ADMIN_ROLE=super_admin +OIDC_SERVER_ADMIN_ROLE=server_admin +OIDC_PROJECT_ADMIN_ROLE=project_admin +OIDC_USER_ROLE=user +OIDC_TOKEN_ROLE_FIELD=roles diff --git a/app/Entirety/projects/migrations/__init__.py b/app/Entirety/alarming/__init__.py similarity index 100% rename from app/Entirety/projects/migrations/__init__.py rename to app/Entirety/alarming/__init__.py diff --git a/app/Entirety/alarming/admin.py b/app/Entirety/alarming/admin.py new file mode 100644 index 00000000..847a83a4 --- /dev/null +++ b/app/Entirety/alarming/admin.py @@ -0,0 +1,5 @@ +from django.contrib import admin + +from alarming.models import Subscription + +admin.site.register(Subscription) diff --git a/app/Entirety/alarming/apps.py b/app/Entirety/alarming/apps.py new file mode 100644 index 00000000..57bcf63f --- /dev/null +++ b/app/Entirety/alarming/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class AlarmingConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "alarming" diff --git a/app/Entirety/alarming/models.py b/app/Entirety/alarming/models.py new file mode 100644 index 00000000..9cc55436 --- /dev/null +++ b/app/Entirety/alarming/models.py @@ -0,0 +1,20 @@ +from django.db import models + +from utils.generators import generate_uuid + +from projects.models import Project + + +class Subscription(models.Model): + uuid = models.CharField( + unique=True, max_length=64, default=generate_uuid, primary_key=True + ) # later uuid from cb + name = models.CharField(max_length=64) + + project = models.ForeignKey(Project, on_delete=models.CASCADE) + + def __str__(self): + return self.name + + class Meta: + ordering = ["name"] diff --git a/app/Entirety/alarming/templates/alarming/subscription_list.html b/app/Entirety/alarming/templates/alarming/subscription_list.html new file mode 100644 index 00000000..8a15d5ba --- /dev/null +++ b/app/Entirety/alarming/templates/alarming/subscription_list.html @@ -0,0 +1,9 @@ +{% extends '_base.html' %} + +{% block title %}Subscriptions{% endblock %} + +{% block content %} + {% for subscription in subscription_list %} + {{ subscription.name }} + {% endfor %} +{% endblock %} diff --git a/app/Entirety/alarming/tests.py b/app/Entirety/alarming/tests.py new file mode 100644 index 00000000..7ce503c2 --- /dev/null +++ b/app/Entirety/alarming/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/app/Entirety/alarming/urls.py b/app/Entirety/alarming/urls.py new file mode 100644 index 00000000..d2c8dd8f --- /dev/null +++ b/app/Entirety/alarming/urls.py @@ -0,0 +1,10 @@ +from django.contrib.auth.decorators import login_required +from django.urls import path +from django.views.generic.base import RedirectView + +from alarming.views import SubscriptionList + +urlpatterns = [ + path("subscriptions/", SubscriptionList.as_view(), name="subscriptions"), + path("", RedirectView.as_view(pattern_name="subscriptions")), +] diff --git a/app/Entirety/alarming/views.py b/app/Entirety/alarming/views.py new file mode 100644 index 00000000..8d8ee56a --- /dev/null +++ b/app/Entirety/alarming/views.py @@ -0,0 +1,12 @@ +from django.shortcuts import render +from django.views.generic import ListView + +from alarming.models import Subscription +from projects.mixins import ProjectContextMixin + + +class SubscriptionList(ProjectContextMixin, ListView): + model = Subscription + + def get_queryset(self): + return Subscription.objects.filter(project=self.project) diff --git a/app/Entirety/entirety/asgi.py b/app/Entirety/entirety/asgi.py index 4affac82..6ff89c56 100644 --- a/app/Entirety/entirety/asgi.py +++ b/app/Entirety/entirety/asgi.py @@ -10,7 +10,8 @@ import os from django.core.asgi import get_asgi_application +from pydantic_settings import SetUp -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "entirety.settings") - +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "entirety.settings.Settings") +SetUp().configure() application = get_asgi_application() diff --git a/app/Entirety/entirety/models.py b/app/Entirety/entirety/models.py new file mode 100644 index 00000000..e69de29b diff --git a/app/Entirety/entirety/oidc.py b/app/Entirety/entirety/oidc.py new file mode 100644 index 00000000..5f042582 --- /dev/null +++ b/app/Entirety/entirety/oidc.py @@ -0,0 +1,34 @@ +from mozilla_django_oidc.auth import OIDCAuthenticationBackend +from django.conf import settings + + +class CustomOIDCAB(OIDCAuthenticationBackend): + def create_user(self, claims): + user = super(CustomOIDCAB, self).create_user(claims) + + return self.__set_user_values(user, claims) + + def update_user(self, user, claims): + return self.__set_user_values(user, claims) + + def __set_user_values(self, user, claims): + roles = claims.get("roles", []) + + user.first_name = claims.get("given_name", "") + user.last_name = claims.get("family_name", "") + user.username = claims.get("preferred_username", "") + + user.is_superuser = user.is_staff = settings.OIDC_SUPER_ADMIN_ROLE in roles + user.is_server_admin = settings.OIDC_SERVER_ADMIN_ROLE in roles + user.is_project_admin = settings.OIDC_PROJECT_ADMIN_ROLE in roles + + user.save() + + return user + + def verify_claims(self, claims): + verified = super(CustomOIDCAB, self).verify_claims(claims) + is_user = settings.OIDC_USER_ROLE in claims.get( + settings.OIDC_TOKEN_ROLE_FIELD, [] + ) + return verified and is_user diff --git a/app/Entirety/entirety/settings.py b/app/Entirety/entirety/settings.py index 1729ea02..e5d2ec63 100644 --- a/app/Entirety/entirety/settings.py +++ b/app/Entirety/entirety/settings.py @@ -1,143 +1,196 @@ -""" -Django settings for entirety project. +import os -Generated by 'django-admin startproject' using Django 4.0.5. +from pathlib import Path +from typing import List +from mimetypes import add_type +from pydantic import BaseSettings, Field, AnyUrl, validator -For more information on this file, see -https://docs.djangoproject.com/en/4.0/topics/settings/ +from utils.generators import generate_secret_key -For the full list of settings and their values, see -https://docs.djangoproject.com/en/4.0/ref/settings/ -""" +__version__ = "0.1.0" -import os -from pathlib import Path +class Settings(BaseSettings): + add_type("text/css", ".css", True) + # Build paths inside the project like this: BASE_DIR / 'subdir'. + BASE_DIR = Path(__file__).resolve().parent.parent + + VERSION = __version__ + + # Application definition + INSTALLED_APPS = [ + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + "mozilla_django_oidc", + "compressor", + "crispy_forms", + "crispy_bootstrap5", + "projects.apps.ProjectsConfig", + "examples.apps.ExamplesConfig", + "users.apps.UsersConfig", + "alarming.apps.AlarmingConfig", + ] + + CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap5" + + CRISPY_TEMPLATE_PACK = "bootstrap5" + + MIDDLEWARE = [ + "django.middleware.security.SecurityMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", + "mozilla_django_oidc.middleware.SessionRefresh", + ] + + ROOT_URLCONF = "entirety.urls" + + TEMPLATES = [ + { + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": ["templates"], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", + ], + }, + }, + ] -# Build paths inside the project like this: BASE_DIR / 'subdir'. -BASE_DIR = Path(__file__).resolve().parent.parent + WSGI_APPLICATION = "entirety.wsgi.application" -__version__ = "0.1.0" + # Password validation + # https://docs.djangoproject.com/en/4.0/ref/settings/#auth-password-validators -VERSION = __version__ -# Quick-start development settings - unsuitable for production -# See https://docs.djangoproject.com/en/4.0/howto/deployment/checklist/ - -# SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = "django-insecure-_8e5-lw61o*9ml#eds^!-wc%0g7kabh^8go)!_(7)8x13+fort" - -# SECURITY WARNING: don't run with debug turned on in production! -DEBUG = True - -ALLOWED_HOSTS = [] - -# Application definition - -INSTALLED_APPS = [ - "django.contrib.admin", - "django.contrib.auth", - "django.contrib.contenttypes", - "django.contrib.sessions", - "django.contrib.messages", - "django.contrib.staticfiles", - "compressor", - "crispy_forms", - "crispy_bootstrap5", - "projects.apps.ProjectsConfig", - "examples.apps.ExamplesConfig", -] - -CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap5" - -CRISPY_TEMPLATE_PACK = "bootstrap5" - -MIDDLEWARE = [ - "django.middleware.security.SecurityMiddleware", - "django.contrib.sessions.middleware.SessionMiddleware", - "django.middleware.common.CommonMiddleware", - "django.middleware.csrf.CsrfViewMiddleware", - "django.contrib.auth.middleware.AuthenticationMiddleware", - "django.contrib.messages.middleware.MessageMiddleware", - "django.middleware.clickjacking.XFrameOptionsMiddleware", -] - -ROOT_URLCONF = "entirety.urls" - -TEMPLATES = [ - { - "BACKEND": "django.template.backends.django.DjangoTemplates", - "DIRS": ["templates"], - "APP_DIRS": True, - "OPTIONS": { - "context_processors": [ - "django.template.context_processors.debug", - "django.template.context_processors.request", - "django.contrib.auth.context_processors.auth", - "django.contrib.messages.context_processors.messages", - ], + AUTH_PASSWORD_VALIDATORS = [ + { + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", }, - }, -] + ] -WSGI_APPLICATION = "entirety.wsgi.application" + AUTHENTICATION_BACKENDS = ("entirety.oidc.CustomOIDCAB",) + AUTH_USER_MODEL = "users.User" -# Database -# https://docs.djangoproject.com/en/4.0/ref/settings/#databases + USE_I18N = True -DATABASES = { - "default": { - "ENGINE": "django.db.backends.sqlite3", - "NAME": BASE_DIR / "db.sqlite3", - } -} + USE_TZ = True + + # Static files (CSS, JavaScript, Images) + # https://docs.djangoproject.com/en/4.0/howto/static-files/ + + STATIC_URL = "static/" + STATIC_ROOT = os.path.join(BASE_DIR, "static/") + + STATICFILES_DIRS = [ + os.path.join(BASE_DIR, "static"), + ] + + STATICFILES_FINDERS = [ + "django.contrib.staticfiles.finders.AppDirectoriesFinder", + "compressor.finders.CompressorFinder", + ] -# Password validation -# https://docs.djangoproject.com/en/4.0/ref/settings/#auth-password-validators + COMPRESS_PRECOMPILERS = (("text/x-scss", "django_libsass.SassCompiler"),) -AUTH_PASSWORD_VALIDATORS = [ - { - "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", - }, - { - "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", - }, - { - "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", - }, - { - "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", - }, -] + # Default primary key field type + # https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field -# Internationalization -# https://docs.djangoproject.com/en/4.0/topics/i18n/ + DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" -LANGUAGE_CODE = "en-us" + LOGGING = { + "version": 1, + "disable_existing_loggers": False, + "handlers": { + "console": {"class": "logging.StreamHandler", "level": "DEBUG"}, + }, + "loggers": { + "mozilla_django_oidc": {"handlers": ["console"], "level": "DEBUG"}, + }, + } + + # Settings provided by environment + SECRET_KEY: str = Field(default=generate_secret_key(), env="DJANGO_SECRET_KEY") + + @validator("SECRET_KEY") + def secret_key_not_empty(cls, v) -> str: + v_cleaned = v.strip() + if not v_cleaned: + v_cleaned = generate_secret_key() + elif len(v_cleaned) < 32: + raise ValueError("Django secret should be at least 32 characters long") + return v_cleaned + + # SECURITY WARNING: don't run with debug turned on in production! + DEBUG: bool = Field(default=False, env="DJANGO_DEBUG") + + ALLOWED_HOSTS: List = Field(default=[], env="ALLOWED_HOSTS") + + # Database + # https://docs.djangoproject.com/en/4.0/ref/settings/#databases + # TODO + DATABASES = { + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": BASE_DIR / "db.sqlite3", + } + } -TIME_ZONE = "UTC" + LOGIN_URL: str = Field(default="/oidc/authenticate", env="LOGIN_URL") -USE_I18N = True + LOGIN_REDIRECT_URL: str = Field(default="/oidc/callback/", env="LOGIN_REDIRECT_URL") + LOGOUT_REDIRECT_URL: str = Field(default="/", env="LOGOUT_REDIRECT_URL") -USE_TZ = True + OIDC_RP_SIGN_ALGO: str = Field(default="RS256", env="OIDC_RP_SIGN_ALGO") + OIDC_OP_JWKS_ENDPOINT: str = Field(env="OIDC_OP_JWKS_ENDPOINT") -# Static files (CSS, JavaScript, Images) -# https://docs.djangoproject.com/en/4.0/howto/static-files/ + OIDC_RP_CLIENT_ID: str = Field(env="OIDC_RP_CLIENT_ID") + OIDC_RP_CLIENT_SECRET: str = Field(env="OIDC_RP_CLIENT_SECRET") + OIDC_OP_AUTHORIZATION_ENDPOINT: str = Field(env="OIDC_OP_AUTHORIZATION_ENDPOINT") + OIDC_OP_TOKEN_ENDPOINT: str = Field(env="OIDC_OP_TOKEN_ENDPOINT") + OIDC_OP_USER_ENDPOINT: str = Field(env="OIDC_OP_USER_ENDPOINT") -STATIC_URL = "static/" -STATIC_ROOT = os.path.join(BASE_DIR, "static/") + OIDC_SUPER_ADMIN_ROLE: str = Field( + default="super_admin", env="OIDC_SUPER_ADMIN_ROLE" + ) + OIDC_SERVER_ADMIN_ROLE: str = Field( + default="server_admin", env="OIDC_SERVER_ADMIN_ROLE" + ) + OIDC_PROJECT_ADMIN_ROLE: str = Field( + default="project_admin", env="OIDC_PROJECT_ADMIN_ROLE" + ) + OIDC_USER_ROLE: str = Field(default="user", env="OIDC_USER_ROLE") + OIDC_TOKEN_ROLE_FIELD: str = Field(default="roles", env="OIDC_TOKEN_ROLE_FIELD") -STATICFILES_DIRS = [ - os.path.join(BASE_DIR, "static"), -] + # Internationalization + # https://docs.djangoproject.com/en/4.0/topics/i18n/ -STATICFILES_FINDERS = [ - "django.contrib.staticfiles.finders.AppDirectoriesFinder", - "compressor.finders.CompressorFinder", -] + LANGUAGE_CODE: str = Field(default="en-us", env="LANGUAGE_CODE") -COMPRESS_PRECOMPILERS = (("text/x-scss", "django_libsass.SassCompiler"),) + TIME_ZONE: str = Field(default="Europe/Berlin", env="TIME_ZONE") -# Default primary key field type -# https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field + COMPRESS_ENABLED: bool = Field(default=not DEBUG, env="COMPRESS_ENABLED") -DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" + class Config: + case_sensitive = False + env_file = ".env" + env_file_encoding = "utf-8" diff --git a/app/Entirety/entirety/urls.py b/app/Entirety/entirety/urls.py index b100c25f..84c7c751 100644 --- a/app/Entirety/entirety/urls.py +++ b/app/Entirety/entirety/urls.py @@ -14,11 +14,20 @@ 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ from django.contrib import admin -from django.urls import path, include, re_path +from django.urls import path, include + +from . import views + +handler404 = "entirety.views.custom_page_not_found_view" +handler500 = "entirety.views.custom_error_view" +handler403 = "entirety.views.custom_permission_denied_view" +handler400 = "entirety.views.custom_bad_request_view" urlpatterns = [ path("admin/", admin.site.urls), - path("", include("projects.urls")), + path("", views.home, name="home"), path("projects/", include("projects.urls")), + path("user/", include("users.urls")), path("examples/", include("examples.urls")), + path("oidc/", include("mozilla_django_oidc.urls")), ] diff --git a/app/Entirety/entirety/views.py b/app/Entirety/entirety/views.py new file mode 100644 index 00000000..cb0c3dac --- /dev/null +++ b/app/Entirety/entirety/views.py @@ -0,0 +1,29 @@ +import requests + +from django.shortcuts import render + + +def home(request): + context = {} + return render(request, "home.html", context) + + +def __generic_error_handler(request, status_code: int): + context = {"error_code": status_code} + return render(request, "error.html", context) + + +def custom_page_not_found_view(request, *args, **kwargs): + return __generic_error_handler(request, status_code=404) + + +def custom_error_view(request, *args, **kwargs): + return __generic_error_handler(request, status_code=500) + + +def custom_permission_denied_view(request, *args, **kwargs): + return __generic_error_handler(request, status_code=403) + + +def custom_bad_request_view(request, *args, **kwargs): + return __generic_error_handler(request, status_code=400) diff --git a/app/Entirety/entirety/wsgi.py b/app/Entirety/entirety/wsgi.py index c6483e9d..f1b1bb37 100644 --- a/app/Entirety/entirety/wsgi.py +++ b/app/Entirety/entirety/wsgi.py @@ -10,7 +10,10 @@ import os from django.core.wsgi import get_wsgi_application +from pydantic_settings import SetUp -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "entirety.settings") +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "entirety.settings.Settings") + +SetUp().configure() application = get_wsgi_application() diff --git a/app/Entirety/examples/templates/examples/dialog.html b/app/Entirety/examples/templates/dialog.html similarity index 81% rename from app/Entirety/examples/templates/examples/dialog.html rename to app/Entirety/examples/templates/dialog.html index 84be7857..e4276e10 100644 --- a/app/Entirety/examples/templates/examples/dialog.html +++ b/app/Entirety/examples/templates/dialog.html @@ -1,5 +1,7 @@ {% extends '_base.html' %} +{% block title %}Example Dialog{% endblock %} + {% block content %} - + project logo fiware +
+ {% include 'auth.html' %} +
{% include 'sidebar.html' %} -
+
{% block content %}(no content - should not be here) {% endblock %}
{% include 'modal.html' %} diff --git a/app/Entirety/templates/auth.html b/app/Entirety/templates/auth.html new file mode 100644 index 00000000..293054d0 --- /dev/null +++ b/app/Entirety/templates/auth.html @@ -0,0 +1,38 @@ +{% if user.is_authenticated %} + + +{% else %} + + + Login + + + + +{% endif %} diff --git a/app/Entirety/templates/error.html b/app/Entirety/templates/error.html new file mode 100644 index 00000000..8f884432 --- /dev/null +++ b/app/Entirety/templates/error.html @@ -0,0 +1,10 @@ +{% extends '_base.html' %} + +{% block title %}Error {{ error_code }}{% endblock %} + +{% block content %} +
+ {# TODO: use custom error layout, but hey cats :D #} + ... +
+{% endblock %} diff --git a/app/Entirety/templates/home.html b/app/Entirety/templates/home.html new file mode 100644 index 00000000..68960899 --- /dev/null +++ b/app/Entirety/templates/home.html @@ -0,0 +1,7 @@ +{% extends '_base.html' %} + +{% block title %}Home{% endblock %} + +{% block content %} + Home Page +{% endblock %} diff --git a/app/Entirety/templates/sidebar.html b/app/Entirety/templates/sidebar.html index d2d5c924..2b671fe5 100644 --- a/app/Entirety/templates/sidebar.html +++ b/app/Entirety/templates/sidebar.html @@ -1,41 +1,64 @@
- diff --git a/app/Entirety/users/__init__.py b/app/Entirety/users/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/app/Entirety/users/admin.py b/app/Entirety/users/admin.py new file mode 100644 index 00000000..8c38f3f3 --- /dev/null +++ b/app/Entirety/users/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/app/Entirety/users/apps.py b/app/Entirety/users/apps.py new file mode 100644 index 00000000..88f7b179 --- /dev/null +++ b/app/Entirety/users/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class UsersConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "users" diff --git a/app/Entirety/users/forms.py b/app/Entirety/users/forms.py new file mode 100644 index 00000000..3490f6e6 --- /dev/null +++ b/app/Entirety/users/forms.py @@ -0,0 +1,25 @@ +from django import forms +from users.models import User + +# from crispy_forms.helper import FormHelper + + +class UserForm(forms.ModelForm): + def __init__(self, *args, **kwargs): + super(UserForm, self).__init__(*args, **kwargs) + for x, field in self.fields.items(): + field.disabled = True + # self.helper = FormHelper() + # self.helper.form_tag = False + # self.helper.field_class = "col-lg-2" + + class Meta: + model = User + fields = [ + "first_name", + "last_name", + "email", + "is_superuser", + "is_server_admin", + "is_project_admin", + ] diff --git a/app/Entirety/users/models.py b/app/Entirety/users/models.py new file mode 100644 index 00000000..f28f81e1 --- /dev/null +++ b/app/Entirety/users/models.py @@ -0,0 +1,15 @@ +from django.contrib.auth.models import AbstractUser +from django.db import models + + +class User(AbstractUser): + is_server_admin = models.BooleanField( + default=False, + help_text="User can create and update projects for any project admin.", + verbose_name="server admin status", + ) + is_project_admin = models.BooleanField( + default=False, + help_text="User can create projects and update self created.", + verbose_name="project admin status", + ) diff --git a/app/Entirety/users/templates/profile.html b/app/Entirety/users/templates/profile.html new file mode 100644 index 00000000..215e4c02 --- /dev/null +++ b/app/Entirety/users/templates/profile.html @@ -0,0 +1,41 @@ +{% extends '_base.html' %} +{% load crispy_forms_tags %} + + +{% block title %}Profile{% endblock %} + +{% block content %} + {% if user.is_authenticated %} +
+ {{ user.username }} +
+
+ {# {% crispy form %}#} +
+ {{ form.first_name|as_crispy_field }} +
+
+ {{ form.last_name|as_crispy_field }} +
+
+
+
+ {{ form.email|as_crispy_field }} +
+
+
+ Roles +
+
+
+ {{ form.is_superuser|as_crispy_field }} +
+
+ {{ form.is_server_admin|as_crispy_field }} +
+
+ {{ form.is_project_admin|as_crispy_field }} +
+
+ {% endif %} +{% endblock %} diff --git a/app/Entirety/users/tests.py b/app/Entirety/users/tests.py new file mode 100644 index 00000000..7ce503c2 --- /dev/null +++ b/app/Entirety/users/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/app/Entirety/users/urls.py b/app/Entirety/users/urls.py new file mode 100644 index 00000000..d522546b --- /dev/null +++ b/app/Entirety/users/urls.py @@ -0,0 +1,5 @@ +from django.urls import path + +from users import views + +urlpatterns = [path("", views.user_info, name="user")] diff --git a/app/Entirety/users/views.py b/app/Entirety/users/views.py new file mode 100644 index 00000000..4f7b91c7 --- /dev/null +++ b/app/Entirety/users/views.py @@ -0,0 +1,11 @@ +from django.contrib.auth.decorators import login_required +from django.shortcuts import render + +from users.forms import UserForm + + +@login_required() +def user_info(request, *args, **kwargs): + form = UserForm(instance=request.user) + context = {"form": form} + return render(request, "profile.html", context=context) diff --git a/app/Entirety/utils/__init__.py b/app/Entirety/utils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/app/Entirety/utils/generators.py b/app/Entirety/utils/generators.py new file mode 100644 index 00000000..9dd5aa1b --- /dev/null +++ b/app/Entirety/utils/generators.py @@ -0,0 +1,10 @@ +from secrets import token_urlsafe +from uuid import uuid4 + + +def generate_uuid(): + return str(uuid4()) + + +def generate_secret_key(length: int = 64) -> str: + return token_urlsafe(length) diff --git a/CHANGELOG.md b/docs/CHANGELOG.md similarity index 100% rename from CHANGELOG.md rename to docs/CHANGELOG.md diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md new file mode 100644 index 00000000..e100737e --- /dev/null +++ b/docs/CONTRIBUTING.md @@ -0,0 +1,31 @@ +# Entirety contribution guidelines + +Thank you for investing your time in contributing to our project! +Please read our contribution guideline carefully. + +## Conventional commits + +We are using [conventional commits](https://www.conventionalcommits.org/) +for automatic release management and changelog creation. +Please format your commit messages according to the standard. + +Following commit types will affect the version of the next release: + +1. **fix:** a commit of the type fix patches a bug in your codebase (this correlates with PATCH in Semantic Versioning). +2. **feat:** a commit of the type feat introduces a new feature to the codebase (this correlates with MINOR in Semantic Versioning). +3. **BREAKING CHANGE:** a commit that has a footer BREAKING CHANGE:, or appends a ! after the type/scope, introduces a breaking API change (correlating with MAJOR in Semantic Versioning). A BREAKING CHANGE can be part of commits of any type. + + +> **_NOTE:_** Descriptions copied from [official docs](https://www.conventionalcommits.org/en/v1.0.0/#specification) + +> **_NOTE:_** Following commit types are also supported but will not affect app's version: _build:, chore:, ci:, docs:, +> style:, test:_ + +## Naming conventions + +### HTML + +* **dialog:** _dg\_\_ +* **div:** _d\_\_ +* **dropdown:** _dd\_\_ +* **modal**: _m\_\_ diff --git a/docs/SETTINGS.md b/docs/SETTINGS.md new file mode 100644 index 00000000..8eb9c044 --- /dev/null +++ b/docs/SETTINGS.md @@ -0,0 +1,124 @@ +# Settings + +### ALLOWED_HOSTS + +> *description:* Hosts that are allowed to access the application. +> Only neccessary if DJANGO_DEBUG=FALSE +> +> *default:* [] + +### COMPRESS_ENABLED + +> *description:* Compress js/css files +> +> *default:* not DJANGO_DEBUG + +### DJANGO_DEBUG + +> *description:* Run Django with debug options. Not for production use! +> +> *default:* False + +### DJANGO_SECRET_KEY + +> *description:* Django secret (min. 32 characters) +> +> *default:* Auto generated key + +### LANGUAGE_CODE + +> *description:* Application default language +> +> *default:* en-us + +### LOGIN_REDIRECT_URL + +> *description:* Application successful login redirect url. +> +> *default:* /oidc/callback/ + +### LOGIN_URL + +> *description:* Application login url. Requires further changes. +> +> *default:* /oidc/authenticate + +### LOGOUT_REDIRECT_URL + +> *description:* Application successful logout redirect url. +> +> *default:* / + +### OIDC_OP_AUTHORIZATION_ENDPOINT + +> *description:* OIDC provider authorization endpoint. +> +> *default:* None + +### OIDC_OP_JWKS_ENDPOINT + +> *description:* OIDC provider jwks endpoint. +> +> *default:* None + +### OIDC_OP_TOKEN_ENDPOINT + +> *description:* OIDC provider token endpoint. +> +> *default:* None + +### OIDC_OP_USER_ENDPOINT + +> *description:* OIDC provider user endpoint. +> +> *default:* None + +### OIDC_PROJECT_ADMIN_ROLE + +> *description:* Project admin role configured in OIDC provider. +Project admins can create projects and edit their own projects. +> +> *default:* project_admin + +### OIDC_RP_CLIENT_ID + +> *description:* Client id from OIDC provider. +> +> *default:* None + +### OIDC_RP_CLIENT_SECRET + +> *description:* Client secret from OIDC provider. +> +> *default:* None + +### OIDC_SERVER_ADMIN_ROLE + +> *description:* Server admin role configured in OIDC provider. +Server admins can create/update projects for any project admin. +> +> *default:* server_admin + +### OIDC_SUPER_ADMIN_ROLE + +> *description:* Super admin role configured in OIDC provider. +> +> *default:* super_admin + +### OIDC_TOKEN_ROLE_FIELD + +> *description:* Field in ID token that represents user roles. +> +> *default:* roles + +### OIDC_USER_ROLE + +> *description:* User role configured in OIDC provider. +> +> *default:* user + +### TIME_ZONE + +> *description:* Application timezone +> +> *default:* Europe/Berlin