From 4bbab6cb1c091d8ebf2f88a7cff9bf2bef74376e Mon Sep 17 00:00:00 2001 From: Vinicius Date: Sun, 24 Mar 2024 11:33:56 -0300 Subject: [PATCH] feat: add auth app from prefeitura do rio --- bd_api/apps/account_auth/__init__.py | 0 bd_api/apps/account_auth/admin.py | 31 ++ bd_api/apps/account_auth/apps.py | 7 + .../account_auth/migrations/0001_initial.py | 106 ++++++ .../apps/account_auth/migrations/__init__.py | 0 bd_api/apps/account_auth/models.py | 60 ++++ bd_api/apps/account_auth/static/css/main.css | 303 ++++++++++++++++++ .../account_auth/static/css/recaptcha.css | 3 + .../account_auth/static/js/captcha_submit.js | 3 + .../apps/account_auth/templates/signin.html | 30 ++ bd_api/apps/account_auth/tests.py | 3 + bd_api/apps/account_auth/urls.py | 10 + bd_api/apps/account_auth/views.py | 211 ++++++++++++ bd_api/apps/payment/apps.py | 2 +- bd_api/settings/base.py | 9 +- bd_api/urls.py | 1 + 16 files changed, 776 insertions(+), 3 deletions(-) create mode 100644 bd_api/apps/account_auth/__init__.py create mode 100644 bd_api/apps/account_auth/admin.py create mode 100644 bd_api/apps/account_auth/apps.py create mode 100644 bd_api/apps/account_auth/migrations/0001_initial.py create mode 100644 bd_api/apps/account_auth/migrations/__init__.py create mode 100644 bd_api/apps/account_auth/models.py create mode 100644 bd_api/apps/account_auth/static/css/main.css create mode 100644 bd_api/apps/account_auth/static/css/recaptcha.css create mode 100644 bd_api/apps/account_auth/static/js/captcha_submit.js create mode 100644 bd_api/apps/account_auth/templates/signin.html create mode 100644 bd_api/apps/account_auth/tests.py create mode 100644 bd_api/apps/account_auth/urls.py create mode 100644 bd_api/apps/account_auth/views.py diff --git a/bd_api/apps/account_auth/__init__.py b/bd_api/apps/account_auth/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/bd_api/apps/account_auth/admin.py b/bd_api/apps/account_auth/admin.py new file mode 100644 index 00000000..da31cb82 --- /dev/null +++ b/bd_api/apps/account_auth/admin.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +from django.contrib import admin + +from bd_api.apps.account_auth.models import ( + Access, + Domain, + Token, +) + + +class AccessInline(admin.TabularInline): + model = Access + + +class DomainAdmin(admin.ModelAdmin): + list_display = ("name", "description", "is_active") + inlines = [AccessInline] + + +class TokenAdmin(admin.ModelAdmin): + list_display = ("user", "domain", "is_active") + inlines = [AccessInline] + + +class AccessAdmin(admin.ModelAdmin): + list_display = ("timestamp", "success", "domain") + + +admin.site.register(Domain, DomainAdmin) +admin.site.register(Token, TokenAdmin) +admin.site.register(Access, AccessAdmin) diff --git a/bd_api/apps/account_auth/apps.py b/bd_api/apps/account_auth/apps.py new file mode 100644 index 00000000..40e4ef05 --- /dev/null +++ b/bd_api/apps/account_auth/apps.py @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -*- +from django.apps import AppConfig + + +class AuthConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "bd_api.apps.account_auth" diff --git a/bd_api/apps/account_auth/migrations/0001_initial.py b/bd_api/apps/account_auth/migrations/0001_initial.py new file mode 100644 index 00000000..deea551e --- /dev/null +++ b/bd_api/apps/account_auth/migrations/0001_initial.py @@ -0,0 +1,106 @@ +# -*- coding: utf-8 -*- +# Generated by Django 4.2.10 on 2024-03-24 14:37 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name="Domain", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + ("name", models.CharField(max_length=255)), + ("description", models.TextField(blank=True, null=True)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("is_active", models.BooleanField(default=True)), + ], + ), + migrations.CreateModel( + name="Token", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + ("token", models.CharField(editable=False, max_length=255, unique=True)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("is_active", models.BooleanField(default=True)), + ("expiry_date", models.DateTimeField(blank=True, null=True)), + ( + "domain", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="tokens", + to="account_auth.domain", + ), + ), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="tokens", + to=settings.AUTH_USER_MODEL, + ), + ), + ], + ), + migrations.CreateModel( + name="Access", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + ("timestamp", models.DateTimeField(auto_now_add=True)), + ("success", models.BooleanField()), + ( + "domain", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="accesses", + to="account_auth.domain", + ), + ), + ( + "token", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="accesses", + to="account_auth.token", + ), + ), + ( + "user", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), + ], + ), + ] diff --git a/bd_api/apps/account_auth/migrations/__init__.py b/bd_api/apps/account_auth/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/bd_api/apps/account_auth/models.py b/bd_api/apps/account_auth/models.py new file mode 100644 index 00000000..2ecc2237 --- /dev/null +++ b/bd_api/apps/account_auth/models.py @@ -0,0 +1,60 @@ +# -*- coding: utf-8 -*- +from uuid import uuid4 + +from django.conf import settings +from django.db import models + +USER_MODEL = settings.AUTH_USER_MODEL + + +class Domain(models.Model): + name = models.CharField(max_length=255) + description = models.TextField(null=True, blank=True) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + is_active = models.BooleanField(default=True) + + def __str__(self): + return self.name + + +class Token(models.Model): + user = models.ForeignKey(USER_MODEL, on_delete=models.CASCADE, related_name="tokens") + domain = models.ForeignKey(Domain, on_delete=models.CASCADE, related_name="tokens") + token = models.CharField(max_length=255, editable=False, unique=True) + created_at = models.DateTimeField(auto_now_add=True) + is_active = models.BooleanField(default=True) + expiry_date = models.DateTimeField(null=True, blank=True) + + def __str__(self): + return f"{self.user.username} - {self.domain} - {self.token}" + + def generate_token(self): + return str(uuid4()) + + def save(self): + self.token = self.generate_token() + super().save() + + +class Access(models.Model): + user = models.ForeignKey(USER_MODEL, on_delete=models.CASCADE, null=True, blank=True) + token = models.ForeignKey( + Token, on_delete=models.CASCADE, related_name="accesses", null=True, blank=True + ) + domain = models.ForeignKey( + Domain, + on_delete=models.CASCADE, + related_name="accesses", + null=True, + blank=True, + ) + timestamp = models.DateTimeField(auto_now_add=True) + success = models.BooleanField() + + def __str__(self): + return ( + f"{self.timestamp} - {'OK' if self.success else 'ERR'} - " + f"{self.domain if self.domain else 'NO_DOMAIN'} - " + f"{self.token.user.username if self.token else 'NO_TOKEN'}" + ) diff --git a/bd_api/apps/account_auth/static/css/main.css b/bd_api/apps/account_auth/static/css/main.css new file mode 100644 index 00000000..cc67418e --- /dev/null +++ b/bd_api/apps/account_auth/static/css/main.css @@ -0,0 +1,303 @@ +@import url('https://fonts.googleapis.com/css?family=Poppins'); +/* BASIC */ + +html { + background-color: #56baed; +} + +body { + font-family: "Poppins", sans-serif; + height: 100vh; +} + +a { + color: #92badd; + display: inline-block; + text-decoration: none; + font-weight: 400; +} + +h2 { + text-align: center; + font-size: 16px; + font-weight: 600; + text-transform: uppercase; + display: inline-block; + margin: 40px 8px 10px 8px; + color: #cccccc; +} +/* STRUCTURE */ + +.wrapper { + display: flex; + align-items: center; + flex-direction: column; + justify-content: center; + width: 100%; + min-height: 100%; + padding: 20px; +} + +#formContent { + -webkit-border-radius: 10px 10px 10px 10px; + border-radius: 10px 10px 10px 10px; + background: #fff; + padding: 30px; + width: 90%; + max-width: 450px; + position: relative; + padding: 0px; + -webkit-box-shadow: 0 30px 60px 0 rgba(0, 0, 0, 0.3); + box-shadow: 0 30px 60px 0 rgba(0, 0, 0, 0.3); + text-align: center; +} + +#formFooter { + background-color: #f6f6f6; + border-top: 1px solid #dce8f1; + padding: 25px; + text-align: center; + -webkit-border-radius: 0 0 10px 10px; + border-radius: 0 0 10px 10px; +} +/* TABS */ + +h2.inactive { + color: #cccccc; +} + +h2.active { + color: #0d0d0d; + border-bottom: 2px solid #5fbae9; +} +/* FORM TYPOGRAPHY*/ + +input[type=button], +input[type=submit], +input[type=reset] { + background-color: #56baed; + border: none; + color: white; + padding: 15px 80px; + text-align: center; + text-decoration: none; + display: inline-block; + text-transform: uppercase; + font-size: 13px; + -webkit-box-shadow: 0 10px 30px 0 rgba(95, 186, 233, 0.4); + box-shadow: 0 10px 30px 0 rgba(95, 186, 233, 0.4); + -webkit-border-radius: 5px 5px 5px 5px; + border-radius: 5px 5px 5px 5px; + margin: 5px 20px 40px 20px; + -webkit-transition: all 0.3s ease-in-out; + -moz-transition: all 0.3s ease-in-out; + -ms-transition: all 0.3s ease-in-out; + -o-transition: all 0.3s ease-in-out; + transition: all 0.3s ease-in-out; +} + +input[type=button]:hover, +input[type=submit]:hover, +input[type=reset]:hover { + background-color: #39ace7; +} + +input[type=button]:active, +input[type=submit]:active, +input[type=reset]:active { + -moz-transform: scale(0.95); + -webkit-transform: scale(0.95); + -o-transform: scale(0.95); + -ms-transform: scale(0.95); + transform: scale(0.95); +} + +input[type=text] { + background-color: #f6f6f6; + border: none; + color: #0d0d0d; + padding: 15px 32px; + text-align: center; + text-decoration: none; + display: inline-block; + font-size: 16px; + margin: 5px; + width: 85%; + border: 2px solid #f6f6f6; + -webkit-transition: all 0.5s ease-in-out; + -moz-transition: all 0.5s ease-in-out; + -ms-transition: all 0.5s ease-in-out; + -o-transition: all 0.5s ease-in-out; + transition: all 0.5s ease-in-out; + -webkit-border-radius: 5px 5px 5px 5px; + border-radius: 5px 5px 5px 5px; +} + +input[type=text]:focus { + background-color: #fff; + border-bottom: 2px solid #5fbae9; +} + +input[type=text]:placeholder { + color: #cccccc; +} + +input[type=password] { + background-color: #f6f6f6; + border: none; + color: #0d0d0d; + padding: 15px 32px; + text-align: center; + text-decoration: none; + display: inline-block; + font-size: 16px; + margin: 5px; + width: 85%; + border: 2px solid #f6f6f6; + -webkit-transition: all 0.5s ease-in-out; + -moz-transition: all 0.5s ease-in-out; + -ms-transition: all 0.5s ease-in-out; + -o-transition: all 0.5s ease-in-out; + transition: all 0.5s ease-in-out; + -webkit-border-radius: 5px 5px 5px 5px; + border-radius: 5px 5px 5px 5px; +} + +input[type=password]:focus { + background-color: #fff; + border-bottom: 2px solid #5fbae9; +} + +input[type=password]:placeholder { + color: #cccccc; +} + +.fadeInDown { + -webkit-animation-name: fadeInDown; + animation-name: fadeInDown; + -webkit-animation-duration: 1s; + animation-duration: 1s; + -webkit-animation-fill-mode: both; + animation-fill-mode: both; +} + +@-webkit-keyframes fadeInDown { + 0% { + opacity: 0; + -webkit-transform: translate3d(0, -100%, 0); + transform: translate3d(0, -100%, 0); + } + 100% { + opacity: 1; + -webkit-transform: none; + transform: none; + } +} + +@keyframes fadeInDown { + 0% { + opacity: 0; + -webkit-transform: translate3d(0, -100%, 0); + transform: translate3d(0, -100%, 0); + } + 100% { + opacity: 1; + -webkit-transform: none; + transform: none; + } +} + +@-webkit-keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +@-moz-keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +.fadeIn { + opacity: 0; + -webkit-animation: fadeIn ease-in 1; + -moz-animation: fadeIn ease-in 1; + animation: fadeIn ease-in 1; + -webkit-animation-fill-mode: forwards; + -moz-animation-fill-mode: forwards; + animation-fill-mode: forwards; + -webkit-animation-duration: 1s; + -moz-animation-duration: 1s; + animation-duration: 1s; +} + +.fadeIn.first { + -webkit-animation-delay: 0.4s; + -moz-animation-delay: 0.4s; + animation-delay: 0.4s; +} + +.fadeIn.second { + -webkit-animation-delay: 0.6s; + -moz-animation-delay: 0.6s; + animation-delay: 0.6s; +} + +.fadeIn.third { + -webkit-animation-delay: 0.8s; + -moz-animation-delay: 0.8s; + animation-delay: 0.8s; +} + +.fadeIn.fourth { + -webkit-animation-delay: 1s; + -moz-animation-delay: 1s; + animation-delay: 1s; +} + +.underlineHover:after { + display: block; + left: 0; + bottom: -10px; + width: 0; + height: 2px; + background-color: #56baed; + content: ""; + transition: width 0.2s; +} + +.underlineHover:hover { + color: #0d0d0d; +} + +.underlineHover:hover:after { + width: 100%; +} + +*:focus { + outline: none; +} + +#icon { + width: 60%; +} + +* { + box-sizing: border-box; +} diff --git a/bd_api/apps/account_auth/static/css/recaptcha.css b/bd_api/apps/account_auth/static/css/recaptcha.css new file mode 100644 index 00000000..b944659c --- /dev/null +++ b/bd_api/apps/account_auth/static/css/recaptcha.css @@ -0,0 +1,3 @@ +.g-recaptcha { + display: inline-block; +} diff --git a/bd_api/apps/account_auth/static/js/captcha_submit.js b/bd_api/apps/account_auth/static/js/captcha_submit.js new file mode 100644 index 00000000..239aee52 --- /dev/null +++ b/bd_api/apps/account_auth/static/js/captcha_submit.js @@ -0,0 +1,3 @@ +function captchaSubmit(data) { + document.getElementById("login-form").submit(); +} diff --git a/bd_api/apps/account_auth/templates/signin.html b/bd_api/apps/account_auth/templates/signin.html new file mode 100644 index 00000000..919b1af1 --- /dev/null +++ b/bd_api/apps/account_auth/templates/signin.html @@ -0,0 +1,30 @@ + + + + {% load static %} + + + + + + + + Login + + + +
+
+

Sign In

+
+ {% csrf_token %} + + +
+ +
+
+
+ + + diff --git a/bd_api/apps/account_auth/tests.py b/bd_api/apps/account_auth/tests.py new file mode 100644 index 00000000..23e1480d --- /dev/null +++ b/bd_api/apps/account_auth/tests.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +# Create your tests here. diff --git a/bd_api/apps/account_auth/urls.py b/bd_api/apps/account_auth/urls.py new file mode 100644 index 00000000..5d525e6c --- /dev/null +++ b/bd_api/apps/account_auth/urls.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +from django.urls import path + +from . import views + +urlpatterns = [ + path("", views.auth, name="auth"), + path("login/", views.signin, name="login"), + path("logout/", views.signout, name="logout"), +] diff --git a/bd_api/apps/account_auth/views.py b/bd_api/apps/account_auth/views.py new file mode 100644 index 00000000..61d1b406 --- /dev/null +++ b/bd_api/apps/account_auth/views.py @@ -0,0 +1,211 @@ +# -*- coding: utf-8 -*- +from typing import Tuple, Union +from urllib.parse import urlparse + +from django.conf import settings +from django.contrib.auth import authenticate, login, logout +from django.http import HttpRequest, HttpResponse +from django.shortcuts import redirect, render +from django.utils import timezone +from requests import post + +from bd_api.apps.account.models import Account +from bd_api.apps.account_auth.models import Access, Domain, Token + + +def get_redirect_uri(request: HttpRequest, default: str = None) -> str: + # First from X-Original-Url header. + redirect_uri = request.headers.get("X-Original-URL", None) + if redirect_uri: + return redirect_uri + + # Then from the query string. + redirect_uri = request.GET.get("rd", None) + if redirect_uri: + return redirect_uri + + # Finally, from the default. + return default + + +def is_authenticated(request: HttpRequest) -> Tuple[str, Token, Domain, Account, bool]: + """ + This checks for authentication. It returns a tuple with 5 elements: + - The redirect URI. + - The token. + - The domain. + - The user. + - Whether the authentication was successful. + + How it works: + + - First, it tries to extract the desired domain from the request. + - If it fails, it returns a 401. + + - Then, if there's an user logged in, it iterates over its tokens, + looking for a token with the desired domain. + - If it finds one, it returns a 200. + - If it doesn't, it returns a 401. + + - Finally, if no user is logged in, tries to extract the token from + the request headers. + - If it finds one, it checks if it's valid for the domain. + - If it is, it returns a 200. + - If it isn't, it returns a 401. + + - If no token is found, it returns a 401. + """ + # Tries to extract the desired domain from the request. + redirect_uri = get_redirect_uri(request) + if not redirect_uri: + # If it fails, it returns false. + return redirect_uri, None, None, None, False + + # Tries to find the domain in the database. + redirect_domain = urlparse(redirect_uri).netloc + try: + domain = Domain.objects.get(name=redirect_domain) + except Domain.DoesNotExist: + # If it fails, it returns false. + return redirect_uri, None, None, None, False + + # If there's an user logged in, it iterates over its tokens + if request.user.is_authenticated: + for token in request.user.tokens.all(): + # If it finds one, it returns true. + if token.domain == domain: + return redirect_uri, token, domain, request.user, True + # If it doesn't, it returns false. + return redirect_uri, None, domain, request.user, False + + # Finally, if no user is logged in, tries to extract the token from + # the request headers. + token = request.headers.get("Authorization", None) + if not token: + # If it fails, it returns false. + return redirect_uri, None, domain, None, False + + # If it finds the Authorization header, extract token + try: + token = token.split(" ")[1] + except IndexError: + # If it fails, it returns false. + return redirect_uri, None, domain, None, False + + # If it finds one, it checks if it's valid for the domain. + try: + token: Token = Token.objects.get(token=token) + except Token.DoesNotExist: + # If it fails, it returns false. + return redirect_uri, None, domain, None, False + + # Token must have same domain, its expiry date must be in the future, + # and it must be active. + if (token.domain == domain) and (token.expiry_date > timezone.now()) and (token.is_active): + return redirect_uri, token, domain, token.user, True + + # If it isn't, it returns a 401. + return redirect_uri, token, domain, token.user, False + + +def store_access( + token: Union[str, Token], domain: Union[str, Domain], user: Account, success: bool +) -> None: + try: + if isinstance(token, str): + token: Token = Token.objects.get(token=token) + except Token.DoesNotExist: + token = None + try: + if isinstance(domain, str): + domain: Domain = Domain.objects.get(name=domain) + except Domain.DoesNotExist: + domain = None + if user is None and token is not None: + user = token.user + access = Access(token=token, domain=domain, success=success, user=user) + access.save() + + +def validate_recaptcha_token(token: str) -> bool: + """ + Validates the recaptcha token. + """ + url = "https://www.google.com/recaptcha/api/siteverify" + data = {"secret": settings.RECAPTCHA_SECRET_KEY, "response": token} + response = post(url, data=data) + return response.json()["success"] + + +def auth(request: HttpRequest) -> HttpResponse: + """ + This returns either 200 or 401. For either case, we store the + access log in the database. + + How it works: + + - First, it tries to extract the desired domain from the request. + - If it fails, it returns a 401. + + - Then, if there's an user logged in, it iterates over its tokens, + looking for a token with the desired domain. + - If it finds one, it returns a 200. + - If it doesn't, it returns a 401. + + - Finally, if no user is logged in, tries to extract the token from + the request headers. + - If it finds one, it checks if it's valid for the domain. + - If it is, it returns a 200. + - If it isn't, it returns a 401. + + - If no token is found, it returns a 401. + """ + _, token, domain, user, success = is_authenticated(request) + if not success: + store_access(token=token, domain=domain, user=user, success=success) + if success: + return HttpResponse(status=200) + return HttpResponse(status=401) + + +def signin(request: HttpRequest) -> HttpResponse: + redirect_uri, _, _, user, success = is_authenticated(request) + if success: + if redirect_uri: + return redirect(redirect_uri) + return HttpResponse("You are already signed in. Sign out.") + if request.user and request.user.is_authenticated: + if redirect_uri: + return HttpResponse("You do not have access to this page.", status=401) + return HttpResponse("You are already signed in. Sign out.") + if request.method == "POST": + username = request.POST.get("username", None) + password = request.POST.get("password", None) + recaptcha_response = request.POST.get("g-recaptcha-response", None) + if (not recaptcha_response) or (not validate_recaptcha_token(recaptcha_response)): + return HttpResponse( + "Captcha is invalid. Try again.", status=401 + ) + if username and password: + user = authenticate(request, username=username, password=password) + if user is not None: + login(request, user) + if redirect_uri: + return redirect(redirect_uri) + return HttpResponse("You are now signed in. Sign out.") + return HttpResponse("Invalid username or password.", status=401) + return render( + request, + "signin.html", + context={"recaptcha_site_key": settings.RECAPTCHA_SITE_KEY}, + ) + + +def signout(request: HttpRequest) -> HttpResponse: + if request.user.is_authenticated: + logout(request) + redirect_url = get_redirect_uri(request, default=None) + if redirect_url: + return redirect(redirect_url) + return HttpResponse("You are now signed out. Sign in.") + return HttpResponse("You are not signed in. Sign in.", status=401) diff --git a/bd_api/apps/payment/apps.py b/bd_api/apps/payment/apps.py index 67be1a48..cc698c86 100644 --- a/bd_api/apps/payment/apps.py +++ b/bd_api/apps/payment/apps.py @@ -2,7 +2,7 @@ from djstripe.apps import DjstripeAppConfig -class PaymentsConfig(DjstripeAppConfig): +class PaymentConfig(DjstripeAppConfig): verbose_name = "Stripe" def ready(self): diff --git a/bd_api/settings/base.py b/bd_api/settings/base.py index 78780d33..66372eb3 100644 --- a/bd_api/settings/base.py +++ b/bd_api/settings/base.py @@ -56,9 +56,10 @@ "huey.contrib.djhuey", # "bd_api.apps.account", - "bd_api.apps.core", "bd_api.apps.api.v1", - "bd_api.apps.payment.apps.PaymentsConfig", + "bd_api.apps.account_auth", + "bd_api.apps.core", + "bd_api.apps.payment.apps.PaymentConfig", ] MIDDLEWARE = [ @@ -302,3 +303,7 @@ # BetterStack BETTERSTACK_BEARER_TOKEN = getenv("BETTERSTACK_BEARER_TOKEN") + +# reCAPTCHA +RECAPTCHA_SITE_KEY = getenv("RECAPTCHA_SITE_KEY") +RECAPTCHA_SECRET_KEY = getenv("RECAPTCHA_SECRET_KEY") diff --git a/bd_api/urls.py b/bd_api/urls.py index 63d5bbb7..1d6891c7 100644 --- a/bd_api/urls.py +++ b/bd_api/urls.py @@ -36,6 +36,7 @@ def graphql_view(): path("api/", include("bd_api.apps.api.v1.urls")), path("api/graphql/", graphql_view()), path("account/", include("bd_api.apps.account.urls")), + path("auth/", include("bd_api.apps.account_auth.urls")), path("search/", DatasetSearchV1View.as_view()), path("search/v2/", DatasetSearchV2View.as_view()), path("search/debug/", include("haystack.urls")),