Skip to content

Commit 35afccb

Browse files
authored
Merge pull request #5 from WithAgency/feature/lef-148-canva-chatfaq-guardar-mis-conversaciones
lef-148: secure wss and conversation endpoints to rely on django use without leaking sender_uuid
2 parents 904b92a + dcf5425 commit 35afccb

10 files changed

Lines changed: 73 additions & 10 deletions

File tree

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -250,4 +250,5 @@ back/models/
250250
*.ipynb
251251
chat_rag/data/
252252
chat_rag/examples/
253-
*/.ragatouille/*
253+
*/.ragatouille/*
254+
/back/dump.rdb

admin/nuxt.config.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ export default envManager((env) => {
2323
});
2424
const viteNuxtConfig = defineNuxtConfig({
2525
ssr: true,
26+
devServer: {
27+
port: process.env.NUXT_PORT || 3000,
28+
},
2629
css: ["@/assets/styles/global.scss"],
2730
buildModules: [],
2831
modules: [...config.modules, "@pinia/nuxt", "@element-plus/nuxt"],

back/back/apps/broker/consumers/bots/custom_ws.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,11 @@ async def gather_fsm_def(self):
2727
return fsm, None if fsm else f"`No FSM found with name {name}`"
2828

2929
async def gather_user_id(self):
30-
return self.scope["url_route"]["kwargs"]["sender_id"]
30+
# If user is authenticated, use their sender_uuid
31+
if self.scope.get("user") and self.scope["user"].is_authenticated:
32+
return str(self.scope["user"].sender_uuid)
33+
# Otherwise, fall back to URL parameter
34+
return self.scope["url_route"]["kwargs"].get("sender_id")
3135

3236
async def gather_initial_conversation_metadata(self):
3337
params = parse_qs(self.scope["query_string"])

back/back/apps/broker/serializers/messages/custom_ws.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,16 @@ def to_mml(self, ctx: BotConsumer) -> Union[bool, "Message"]:
2929
if not self.is_valid():
3030
return False
3131

32+
sender_data = self.data["sender"].copy()
33+
34+
# If user is authenticated, use their sender_uuid as the sender ID
35+
if ctx.scope.get("user") and ctx.scope["user"].is_authenticated:
36+
sender_data["id"] = str(ctx.scope["user"].sender_uuid)
37+
3238
s = MessageSerializer(
3339
data={
3440
"stack": self.data["stack"],
35-
"sender": self.data["sender"],
41+
"sender": sender_data,
3642
"send_time": int(time.time() * 1000),
3743
"conversation": ctx.conversation.pk,
3844
}

back/back/apps/broker/views/__init__.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -100,13 +100,19 @@ def instance_permissions(self, request):
100100

101101
@action(methods=("get",), detail=False, permission_classes=[AllowAny])
102102
def from_sender(self, request, *args, **kwargs):
103-
if not request.query_params.get("sender"):
104-
return JsonResponse(
105-
{"error": "sender is required"},
106-
status=400,
107-
)
103+
# Use authenticated user's sender_uuid if available, otherwise fall back to query param
104+
if request.user.is_authenticated:
105+
sender_id = str(request.user.sender_uuid)
106+
else:
107+
sender_id = request.query_params.get("sender")
108+
if not sender_id:
109+
return JsonResponse(
110+
{"error": "sender is required"},
111+
status=400,
112+
)
113+
108114
results = []
109-
for c in Conversation.conversations_from_sender(request.query_params.get("sender")):
115+
for c in Conversation.conversations_from_sender(sender_id):
110116
if error := self._instance_permissions(c, request):
111117
return error
112118
results.append(ConversationSerializer(c).data)
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Generated by Django 5.2.6 on 2025-11-18 00:00
2+
3+
import uuid
4+
from django.db import migrations, models
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
dependencies = [
10+
("people", "0006_alter_user_email"),
11+
]
12+
13+
operations = [
14+
migrations.AddField(
15+
model_name="user",
16+
name="sender_uuid",
17+
field=models.UUIDField(
18+
db_index=True,
19+
default=uuid.uuid4,
20+
editable=False,
21+
help_text="Universal sender identifier for lefebvre-chatfaq integration",
22+
unique=True,
23+
),
24+
),
25+
]

back/back/apps/people/models.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,13 @@ class User(UuidPkModel, AbstractBaseUser, PermissionsMixin):
9999
_("date joined"),
100100
auto_now_add=True,
101101
)
102+
sender_uuid = models.UUIDField(
103+
unique=True,
104+
default=uuid4,
105+
editable=False,
106+
db_index=True,
107+
help_text=_("Universal sender identifier for lefebvre-chatfaq integration"),
108+
)
102109
remember_me = models.TextField(
103110
_("remember me"),
104111
null=True,

back/back/apps/people/serializers.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ class Meta:
3737
class AuthUserSerializer(serializers.ModelSerializer):
3838
"""
3939
Serializer used when the user is authenticated on /api/me/
40+
41+
NOTE: sender_uuid intentionally NOT included here to avoid exposing it to
42+
frontend clients. AdminUserSerializer still exposes it for admin use.
4043
"""
4144

4245
class Meta:

back/back/apps/people/views.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,8 +113,14 @@ def post(self, request, format=None):
113113

114114

115115
class UserAPIViewSet(viewsets.ModelViewSet):
116+
"""
117+
Admin-only CRUD for users. Keep AdminUserSerializer (fields="__all__"),
118+
but make access explicit and restricted to admins so sender_uuid is not
119+
leaked to non-admins.
120+
"""
116121
queryset = User.objects.all()
117122
serializer_class = AdminUserSerializer
123+
permission_classes = [permissions.IsAdminUser] # Ensure only admins can read/write all fields
118124
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
119125
filterset_fields = ["id"]
120126

back/back/config/middelware.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,9 @@ def has_permission(self, request, view):
9292
widget = Widget.objects.get(id=widget_id)
9393
except Widget.DoesNotExist:
9494
return False
95-
if fnmatch.fnmatch(urlparse(origin).netloc, widget.domain):
95+
# Compare netloc to netloc (both should be like "localhost:3000")
96+
widget_domain_netloc = urlparse(widget.domain).netloc if widget.domain else widget.domain
97+
if fnmatch.fnmatch(urlparse(origin).netloc, widget_domain_netloc):
9698
return True
9799
return False
98100

0 commit comments

Comments
 (0)