Skip to content

Commit

Permalink
feature/enterprise-dashboard (dongri#66)
Browse files Browse the repository at this point in the history
* enhancement: split route tree between user + enterprise (dongri#64)

* enhancement: add initial dashboard views (dongri#65)

* add inital dashboard

* updates

* integrate with backend v1

* migrate state to reducers

* linter

* update charts

* update charts

* update charts

* updates

* updates

* add heatmap

* add settings view

* update enterprise

* update lockfile

* eslint

* add login button

* rename loggers

* fix pkce enterprise auth

* rebase origin/develop

* fix rebase

* yarn pretty

* fix enterprise login
  • Loading branch information
ra0x3 authored Jan 2, 2024
1 parent 74ed913 commit 23090be
Show file tree
Hide file tree
Showing 84 changed files with 4,527 additions and 817 deletions.
6 changes: 6 additions & 0 deletions www/backend/helpar/brands/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,9 @@ class CreateMessageRequest(BaseModel):

class CanonicalBrandInfoRequest(BaseModel):
canonical_name: str


class BrandDashboardRequest(BaseModel):
start_date: str
end_date: str
brand_id: UUID4
1 change: 1 addition & 0 deletions www/backend/helpar/brands/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,5 @@
path("brands/<str:brand_id>/threads/<str:thread_id>/messages/", brand_messages, name="brand-messages"),
path(r"brands/<str:brand_id>/products/<str:product_id>/faqs/", brand_product_faqs),
path(r"brands/canonical-brand-info/<str:canonical_name>/", canonical_brand_info_view),
path(r"brands/<str:brand_id>/dashboard/", brand_dashboard_view),
]
4 changes: 4 additions & 0 deletions www/backend/helpar/brands/uses.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,7 @@ def post_llm_prompt(message: Message) -> Optional[Message]:
return Message.objects.create(
**{"content": response, "thread": message.thread, "actor": LLM_ACTOR, "created_at": timezone.now()}
)


def brand_dashboard(params: BrandDashboardRequest) -> Tuple[status, Dict[str, Any]]:
return (status.HTTP_200_OK, {})
17 changes: 17 additions & 0 deletions www/backend/helpar/brands/views.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import logging
from rest_framework.response import Response
from rest_framework import status
from rest_framework.decorators import api_view
Expand All @@ -13,6 +14,9 @@
from helpar.lib.constants import *


logger = logging.getLogger("helpar")


class BrandViewSet(BaseViewSet):
queryset = Brand.objects.all()
model = Brand
Expand Down Expand Up @@ -335,3 +339,16 @@ def create(self, request: Request, brand_id: str, thread_id: str, *args, **kwarg
messages = [message, llm_message]
serializer = self.serializer_class(messages, many=True)
return Response(status=status.HTTP_201_CREATED, data={"data": serializer.data})


@api_view(["GET"])
def brand_dashboard_view(request: Request, brand_id: uuid.UUID) -> Response:
try:
start_date = request.query_params["start_date"]
end_date = request.query_params["end_date"]
params = BrandDashboardRequest(start_date=start_date, end_date=end_date, brand_id=brand_id)
(resp_status, data) = brand_dashboard(params)
return Response(status=resp_status, data={"success": True, "data": data})
except Exception as err:
logger.error("Error retrieving dashboard data: %s", str(err))
return Response(status=status.HTTP_500_INTERNAL_SERVER_ERROR, data={"success": False, "details": str(err)})
6 changes: 3 additions & 3 deletions www/backend/helpar/helpar/lib/middlewares.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ def __call__(self, request: Request) -> MiddlewareResponse:
class CatchallMiddleware(BaseMiddleware):
def __init__(self, get_response: Callable[[Request], MiddlewareResponse]):
super().__init__(get_response)
self.logger = logging.getLogger(__name__)
self.logger = logging.getLogger("helpar")

@staticmethod
def _normalize_error_msg(msg: str) -> str:
Expand Down Expand Up @@ -60,7 +60,7 @@ def __call__(self, request: Request):
class LoggingMiddleware(BaseMiddleware):
def __init__(self, get_response: Callable[[Request], MiddlewareResponse]):
super().__init__(get_response)
self.logger = logging.getLogger(__name__)
self.logger = logging.getLogger("helpar")

def __call__(self, request: Request) -> MiddlewareResponse:
method = request.method or ""
Expand Down Expand Up @@ -99,7 +99,7 @@ def is_unauthenticated_action(self, request: Request) -> bool:
class JwtAuthMiddleware(BaseMiddleware):
def __init__(self, get_response: Callable[[Request], MiddlewareResponse]):
super().__init__(get_response)
self.logger = logging.getLogger(__name__)
self.logger = logging.getLogger("helpar")
self.unauthenticated_routes = UnauthenticatedRoutes()

def __call__(self, request: Request) -> MiddlewareResponse:
Expand Down
42 changes: 41 additions & 1 deletion www/backend/helpar/helpar/lib/services.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import logging
import base64
import json
import uuid
import datetime as dt
import jwt
import httpx
from openai import OpenAI
from django.conf import settings
from t import *

logger = logging.getLogger(__name__)
logger = logging.getLogger("helpar")

client = OpenAI(
api_key=settings.OPENAI_API_KEY,
Expand Down Expand Up @@ -138,3 +139,42 @@ def _decode_jwt(token: str):
decoded_bytes = base64.urlsafe_b64decode(payload)
decoded_payload = decoded_bytes.decode("utf-8")
return json.loads(decoded_payload)


# TODO: We can cache these somehow so that a given brand and set of dates
# only needs to be queried once


class DashboardStatistics:
def __init__(self, brand_id: uuid.UUID, start_date: str, end_date: str):
self.brand_id = brand_id
self.start_date = start_date
self.end_date = end_date
self._data = {
"num_customers": 0,
"preferences_recorded": 0,
"frustrations_resolved": 0,
"reorder_revenue": 0,
"customer_data": {
"nps": 0,
"customer_types": {},
"registrations": [
{
"user_name": "",
"user_identifier": "",
}
],
},
"setup_process": {
"num_happy_setups": 0,
"num_sad_setups": 0,
"num_neutral_setups": 0,
"setup_process_breakdown": {},
},
"reorder_revenue_breakdown": {},
"support_topics": {},
"social_clicks": {},
}

def data(self) -> Dict[str, Any]:
return self._data
2 changes: 1 addition & 1 deletion www/backend/helpar/helpar/settings/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
datefmt="%Y/%m/%d %H:%M:%S",
)

logger = logging.getLogger(__name__)
logger = logging.getLogger("helpar")

env = os.environ["ENV"]
proc = multiprocessing.current_process()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Generated by Django 5.0 on 2023-12-30 16:18

from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("users", "0017_alter_user_username"),
]

operations = [
migrations.AddField(
model_name="oauthrequest",
name="enterprise",
field=models.BooleanField(default=False),
),
]
17 changes: 17 additions & 0 deletions www/backend/helpar/users/migrations/0019_pkcerequest_enterprise.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Generated by Django 5.0 on 2023-12-30 16:24

from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("users", "0018_oauthrequest_enterprise"),
]

operations = [
migrations.AddField(
model_name="pkcerequest",
name="enterprise",
field=models.BooleanField(default=False),
),
]
37 changes: 37 additions & 0 deletions www/backend/helpar/users/migrations/0020_session.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Generated by Django 5.0 on 2023-12-31 03:11

import django.db.models.deletion
import uuid
from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("users", "0019_pkcerequest_enterprise"),
]

operations = [
migrations.CreateModel(
name="Session",
fields=[
(
"id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
("enterprise", models.BooleanField(default=False)),
("created_at", models.DateTimeField()),
(
"user",
models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="users.user"),
),
],
options={
"db_table": "sessions",
},
),
]
15 changes: 15 additions & 0 deletions www/backend/helpar/users/migrations/0021_delete_session.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Generated by Django 5.0 on 2023-12-31 03:14

from django.db import migrations


class Migration(migrations.Migration):
dependencies = [
("users", "0020_session"),
]

operations = [
migrations.DeleteModel(
name="Session",
),
]
2 changes: 2 additions & 0 deletions www/backend/helpar/users/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ class Meta:

code: models.CharField = models.CharField(max_length=255, unique=True, db_index=True)
user: models.ForeignKey = models.ForeignKey(User, on_delete=models.CASCADE)
enterprise: models.BooleanField = models.BooleanField(default=False)
created_at: models.DateTimeField = models.DateTimeField(null=True)
expiry: models.DateTimeField = models.DateTimeField()
deleted_at: models.DateTimeField = models.DateTimeField(null=True)
Expand All @@ -58,6 +59,7 @@ class Meta:
id: models.UUIDField = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
provider: models.CharField = models.CharField(max_length=255)
origin: models.CharField = models.CharField(max_length=255)
enterprise: models.BooleanField = models.BooleanField(default=False)
state: models.CharField = models.CharField(max_length=255, db_index=True)
created_at: models.DateTimeField = models.DateTimeField()
expiry: models.DateTimeField = models.DateTimeField()
Expand Down
1 change: 1 addition & 0 deletions www/backend/helpar/users/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ def canoncial_phone(self) -> str:

class PKCEAuthRequest(BaseModel):
code: Annotated[str, StringConstraints(min_length=6, max_length=6)]
enterprise: bool = False


class UpdateUserRequest(BaseModel):
Expand Down
35 changes: 28 additions & 7 deletions www/backend/helpar/users/uses.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
from helpar.lib.utils import *
from helpar.lib.services import AppleAuthentication

logger = logging.getLogger(__name__)
logger = logging.getLogger("helpar")

scopes = [
"https://www.googleapis.com/auth/userinfo.profile",
Expand All @@ -34,7 +34,7 @@ def _fetch_google_user_info(token: str) -> Optional[Dict[str, Any]]:
return response.json()


def google_auth(origin: str) -> str:
def google_auth(origin: str, enterprise: bool) -> str:
mins_expiry = 5
flow = Flow.from_client_config(
client_config={
Expand All @@ -55,6 +55,7 @@ def google_auth(origin: str) -> str:
_ = OauthRequest.objects.create(
**{
"provider": "google",
"enterprise": enterprise,
"state": params["state"][0],
"created_at": timezone.now(),
"origin": origin,
Expand Down Expand Up @@ -140,6 +141,7 @@ def twilio_phone_auth_verification(
if twilio_auth.expiry <= timezone.now():
twilio_auth.deleted_at = timezone.now()
twilio_auth.save()
logger.error("Twilio verification code expired.")
return (status.HTTP_400_BAD_REQUEST, {"success": False, "message": "Verification code expired."})

response = httpx.post(
Expand Down Expand Up @@ -176,19 +178,33 @@ def twilio_phone_auth_verification(


def pkce_auth(params: PKCEAuthRequest) -> Tuple[status, Dict[str, Any]]:
pkce_request = PKCERequest.objects.filter(code=params.code, deleted_at__isnull=True).order_by("-created_at").first()
from brands.models import Brand

pkce_request = (
PKCERequest.objects.filter(code=params.code, enterprise=params.enterprise, deleted_at__isnull=True)
.order_by("-created_at")
.first()
)
if not pkce_request:
logger.error("No PKCE request found for code: %s", params.code)
return (status.HTTP_400_BAD_REQUEST, {"success": False, "message": "Invalid code."})

if pkce_request.expiry <= timezone.now():
logger.error("PKCE code %s expired.", params.code)
pkce_request.deleted_at = timezone.now()
pkce_request.save()
return (status.HTTP_400_BAD_REQUEST, {"success": False, "message": "Code expired."})

data = UserSerializer(pkce_request.user).data
pkce_request.deleted_at = timezone.now()
pkce_request.save()

if params.enterprise:
brand = Brand.objects.filter(users__id=pkce_request.user.id).first()
if not brand:
logger.error("User %s not associated with a brand.", pkce_request.user.id)
return (status.HTTP_400_BAD_REQUEST, {"success": False, "message": "User not associated with a brand."})

data = UserSerializer(pkce_request.user).data
return (status.HTTP_200_OK, {"success": True, "user": data})


Expand Down Expand Up @@ -216,11 +232,13 @@ def apple_auth_callback(form: AppleAuthForm) -> HttpResponse:
.first()
)
if not oauth_req:
logger.error("No oauth request found for state: %s", form.cleaned_data["state"])
return HttpResponseServerError(
content_type="application/json",
content=json.dumps({"success": False, "details": "No oauth request found for state."}),
)
if oauth_req.expiry < timezone.now():
logger.error("Apple auth request expired.")
return HttpResponseBadRequest(
content_type="application/json",
content=json.dumps({"success": False, "details": "Apple auth request expired."}),
Expand All @@ -233,6 +251,9 @@ def apple_auth_callback(form: AppleAuthForm) -> HttpResponse:
code = random_digits()
expiry = timezone.now() + dt.timedelta(minutes=mins_expiry)

path = "d/hq?code=" if oauth_req.enterprise else "m/hq?code="
url = oauth_req.origin + path + code

if not email or not first_name or not last_name:
derived_email = AppleAuthentication.derive_user_email_with_idcode(form.cleaned_data["id_token"])
if not derived_email:
Expand All @@ -244,13 +265,13 @@ def apple_auth_callback(form: AppleAuthForm) -> HttpResponse:
if User.objects.filter(email=derived_email).exists():
user = User.objects.get(email=derived_email)
_ = PKCERequest.objects.create(code=code, expiry=expiry, user_id=user.id, created_at=timezone.now())
return HttpResponseRedirect(oauth_req.origin + "hq?code=" + code)
return HttpResponseRedirect(url)

username = first_name + " " + last_name
if User.objects.filter(email=email, username=username).exists():
user = User.objects.get(email=email, username=username)
_ = PKCERequest.objects.create(code=code, expiry=expiry, user_id=user.id, created_at=timezone.now())
return HttpResponseRedirect(oauth_req.origin + "hq?code=" + code)
return HttpResponseRedirect(url)

user_data = {
"email": email,
Expand All @@ -270,7 +291,7 @@ def apple_auth_callback(form: AppleAuthForm) -> HttpResponse:

_ = PKCERequest.objects.create(code=code, expiry=expiry, user_id=user.id, created_at=timezone.now())

return HttpResponseRedirect(oauth_req.origin + "hq?code=" + code)
return HttpResponseRedirect(oauth_req.origin + "m/hq?code=" + code)


def update_user(params: UpdateUserRequest, user_id: str) -> Tuple[status, Dict[str, Any]]:
Expand Down
Loading

0 comments on commit 23090be

Please sign in to comment.