Skip to content

Commit

Permalink
Merge branch 'main' into pkv/otp-sender-fixes
Browse files Browse the repository at this point in the history
  • Loading branch information
calellowitz authored Dec 13, 2024
2 parents 21b13bd + 161f4c7 commit 80fa00f
Show file tree
Hide file tree
Showing 40 changed files with 1,861 additions and 110 deletions.
45 changes: 44 additions & 1 deletion conftest.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import base64

import pytest
from oauth2_provider.models import Application
from rest_framework.test import APIClient

from users.factories import UserFactory, FCMDeviceFactory

from messaging.factories import ServerFactory

@pytest.fixture
def user(db):
Expand All @@ -11,3 +15,42 @@ def user(db):
@pytest.fixture
def fcm_device(user):
return FCMDeviceFactory(user=user)


@pytest.fixture
def api_client():
return APIClient()


@pytest.fixture
def auth_device(user, api_client):
"""
Create the Basic Authentication credentials for the test user.
"""
credentials = f"{user.username}:testpass".encode("utf-8")
base64_credentials = base64.b64encode(credentials).decode("utf-8")
cred = f"Basic {base64_credentials}"
api_client.credentials(HTTP_AUTHORIZATION=cred)
return api_client


@pytest.fixture
def oauth_app(user):
application = Application(
name="Test Application",
redirect_uris="http://localhost",
user=user,
client_type=Application.CLIENT_CONFIDENTIAL,
authorization_grant_type=Application.GRANT_CLIENT_CREDENTIALS,
)
application.raw_client_secret = application.client_secret
application.save()
return application


@pytest.fixture
def authed_client(api_client, oauth_app):
auth = f'{oauth_app.client_id}:{oauth_app.raw_client_secret}'.encode('utf-8')
credentials = base64.b64encode(auth).decode('utf-8')
api_client.defaults['HTTP_AUTHORIZATION'] = 'Basic ' + credentials
return api_client
5 changes: 5 additions & 0 deletions connectid/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# This will make sure the app is always imported when
# Django starts so that shared_task will use this app.
from .celery_app import app as celery_app

__all__ = ("celery_app",)
17 changes: 17 additions & 0 deletions connectid/celery_app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import os

from celery import Celery

# set the default Django settings module for the 'celery' program.
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "connectid.settings")

app = Celery("connectid")

# Using a string here means the worker doesn't have to serialize
# the configuration object to child processes.
# - namespace='CELERY' means all celery-related configuration keys
# should have a `CELERY_` prefix.
app.config_from_object("django.conf:settings", namespace="CELERY")

# Load task modules from all registered Django app configs.
app.autodiscover_tasks()
23 changes: 19 additions & 4 deletions connectid/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent

env = os.environ

# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/4.1/howto/deployment/checklist/
Expand All @@ -34,6 +35,7 @@
'users.apps.UsersConfig',
'messaging',
'oauth2_provider',
'payments',
'rest_framework',
'axes',
'fcm_django',
Expand Down Expand Up @@ -63,7 +65,7 @@
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
"DIRS": [BASE_DIR / "templates"],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
Expand All @@ -79,7 +81,11 @@
WSGI_APPLICATION = 'connectid.wsgi.application'



TRUSTED_COMMCAREHQ_HOSTS = [
"www.commcarehq.org",
"commcarehq.org",
"staging.commcarehq.org",
]

# Password validation
# https://docs.djangoproject.com/en/4.1/ref/settings/#auth-password-validators
Expand All @@ -93,7 +99,6 @@
},
]


# Internationalization
# https://docs.djangoproject.com/en/4.1/topics/i18n/

Expand All @@ -105,7 +110,6 @@

USE_TZ = True


# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/4.1/howto/static-files/

Expand Down Expand Up @@ -214,12 +218,23 @@
"DELETE_INACTIVE_DEVICES": False,
}

OAUTH2_PROVIDER_APPLICATION_MODEL = 'oauth2_provider.Application'

SITE_ID = 1

APP_HASH = "apphash"

from .localsettings import *

# Firebase
if FCM_CREDENTIALS:
from firebase_admin import credentials, initialize_app
creds = credentials.Certificate(FCM_CREDENTIALS)
default_app = initialize_app(credential=creds)

CELERY_TASK_ALWAYS_EAGER = True
CELERY_TASK_EAGER_PROPAGATES = True

CELERY_BROKER_URL = env.get("CELERY_BROKER_URL", default="redis://localhost:6379/0")


5 changes: 4 additions & 1 deletion connectid/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,14 @@
"""
from django.contrib import admin
from django.urls import include, path
from django.views.generic import TemplateView
from . import views

urlpatterns = [
path('users/', include('users.urls')),
path('messaging/', include('messaging.urls')),
path('admin/', admin.site.urls),
path('o/', include('oauth2_provider.urls', namespace='oauth2_provider')),
path('hq_invite/', TemplateView.as_view(template_name="connectid/deeplink.html"), name='deeplink'),
path('.well-known/assetlinks.json', views.assetlinks_json, name='assetlinks_json'),
]

30 changes: 30 additions & 0 deletions connectid/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
from django.http import HttpResponse, JsonResponse


def assetlinks_json(request):
assetfile = [
{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "org.commcare.dalvik",
"sha256_cert_fingerprints":
[
"88:57:18:F8:E8:7D:74:04:97:AE:83:65:74:ED:EF:10:40:D9:4C:E2:54:F0:E0:40:64:77:96:7F:D1:39:F9:81",
"89:55:DF:D8:0E:66:63:06:D2:6D:88:A4:A3:88:A4:D9:16:5A:C4:1A:7E:E1:C6:78:87:00:37:55:93:03:7B:03"
]
}
},
{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "org.commcare.dalvik.debug",
"sha256_cert_fingerprints":
[
"88:57:18:F8:E8:7D:74:04:97:AE:83:65:74:ED:EF:10:40:D9:4C:E2:54:F0:E0:40:64:77:96:7F:D1:39:F9:81"
]
}
},
]
return JsonResponse(assetfile, safe=False)
10 changes: 10 additions & 0 deletions messaging/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from django.contrib import admin

from .models import MessageServer


@admin.register(MessageServer)
class MessageServerAdmin(admin.ModelAdmin):
list_display = ('name', 'key_url', 'callback_url', 'delivery_url', 'consent_url', 'server_id', 'secret_key')
search_fields = ('name',)

68 changes: 68 additions & 0 deletions messaging/factories.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import base64
import os
from uuid import uuid4

import factory
from django.utils import timezone
from factory import LazyFunction
from factory.django import DjangoModelFactory
from oauth2_provider.models import Application

from messaging.models import Channel, Message, MessageServer
from users.factories import UserFactory


class ApplicationFactory(DjangoModelFactory):
class Meta:
model = Application

client_id = factory.Faker("uuid4")
client_secret = factory.Faker("uuid4")
client_type = "confidential"
authorization_grant_type = factory.Faker("random_element", elements=["authorization-code", "implicit", "password",
"client-credentials"])
name = factory.Faker("company")


class ServerFactory(DjangoModelFactory):
class Meta:
model = MessageServer

delivery_url = factory.Faker("url")
consent_url = factory.Faker("url")
callback_url = factory.Faker("url")
key_url = factory.Faker("url")
oauth_application = factory.SubFactory(ApplicationFactory)


class ChannelFactory(DjangoModelFactory):
class Meta:
model = Channel

channel_id = factory.LazyFunction(uuid4)
user_consent = True
connect_user = factory.SubFactory(UserFactory)
server = factory.SubFactory(ServerFactory)


def generate_random_content():
nonce = base64.b64encode(os.urandom(12)).decode('utf-8')
tag = base64.b64encode(os.urandom(16)).decode('utf-8')
ciphertext = base64.b64encode(os.urandom(32)).decode('utf-8')

return {
"nonce": nonce,
"tag": tag,
"ciphertext": ciphertext
}


class MessageFactory(DjangoModelFactory):
class Meta:
model = Message

message_id = factory.LazyFunction(uuid4)
channel = factory.SubFactory(ChannelFactory)
content = LazyFunction(generate_random_content)
timestamp = factory.LazyFunction(timezone.now)
received = None
114 changes: 114 additions & 0 deletions messaging/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
# Generated by Django 4.1.7 on 2024-10-24 09:00

from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
import uuid


class Migration(migrations.Migration):

initial = True

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
migrations.swappable_dependency(settings.OAUTH2_PROVIDER_APPLICATION_MODEL),
]

operations = [
migrations.CreateModel(
name="Channel",
fields=[
(
"channel_id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
("user_consent", models.BooleanField(default=False)),
("channel_source", models.TextField()),
(
"connect_user",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
),
),
],
),
migrations.CreateModel(
name="MessageServer",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(max_length=255)),
("key_url", models.URLField()),
("callback_url", models.URLField()),
("delivery_url", models.URLField()),
("consent_url", models.URLField()),
(
"oauth_application",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to=settings.OAUTH2_PROVIDER_APPLICATION_MODEL,
),
),
],
),
migrations.CreateModel(
name="Message",
fields=[
(
"message_id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
("content", models.JSONField()),
("timestamp", models.DateTimeField(default=django.utils.timezone.now)),
("received", models.DateTimeField(blank=True, null=True)),
(
"status",
models.CharField(
choices=[
("PENDING", "Pending"),
("SENT_TO_SERVICE", "Sent To Service"),
("DELIVERED", "Delivered"),
("CONFIRMED_RECEIVED", "Confirmed Received"),
],
default="PENDING",
max_length=50,
),
),
(
"channel",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="messaging.channel",
),
),
],
),
migrations.AddField(
model_name="channel",
name="server",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="messaging.messageserver",
),
),
]
Loading

0 comments on commit 80fa00f

Please sign in to comment.