List a workspace's integrations on the settings page
Python · Django · beginner · modification
Adds the API behind Settings → Integrations. The serializer leaves out the internal `workspace` FK and adds `secret_last4`, so the page can show which key is connected (`•••• 4242`) without anyone pasting it again. The endpoint is admin-only, and any signed-in non-admin gets a 404 for the workspace (signed-out requests get DRF's 403), so slugs can't be probed. Checked in the browser with Stripe and HubSpot connected: both cards render with the right last four characters.
Django 5.1 SaaS with Django REST Framework 3.15. The Angular SPA is served from the same origin and calls the API with the session cookie: `REST_FRAMEWORK` sets `DEFAULT_AUTHENTICATION_CLASSES` to `SessionAuthentication` only and sets no default pagination or permission classes. The Settings → Integrations page (frontend, not in this PR) already expects the response shape in requirement 1. Existing models, with their migrations applied: `accounts.Workspace` (`slug`, a unique `SlugField`; `name`); `accounts.Membership` (`workspace`, `user`, `role` with `Membership.Role.ADMIN` / `Membership.Role.MEMBER` text choices, one membership per workspace and user); `integrations.Integration` (`workspace` FK, `provider` one of `hubspot` / `slack` / `stripe`, `label`, `api_secret` as a `CharField` of 20-128 characters validated by the connect flow, `created_at` with `auto_now_add`). The sync workers read `api_secret` on the server to call the providers; encryption at rest is tracked separately. The `integrations` app is in `INSTALLED_APPS` but had no API routes before this PR. A workspace has at most a few dozen integrations.
Requirements
- Add `GET /api/workspaces/<slug>/integrations/` for the Settings → Integrations page. It returns a JSON list of the workspace's integrations, sorted by `provider`, then oldest first. Each item has exactly these fields: `id`, `provider`, `label`, `created_at` (ISO 8601) and `secret_last4` (the last 4 characters of the stored API secret). The page renders the key as `•••• <secret_last4>`.
- The stored API secret itself is never sent back to the browser, not even to admins. Only the person who connects an integration ever sees its secret, when they paste it in (the existing connect flow); to change it, an admin pastes a new one there.
- Only admins of that workspace may call the endpoint. Signed-out requests are rejected (DRF's 403 under session authentication). Any signed-in user who is not an admin of the workspace gets a 404, and so does a slug that does not exist, so workspace slugs cannot be probed. Members and admins of other workspaces count as non-admins.
Files touched
- integrations/serializers.py
- integrations/views.py
- integrations/urls.py
- config/urls.py
--- integrations/serializers.py +from rest_framework import serializers + +from .models import Integration + + +class IntegrationSerializer(serializers.ModelSerializer): + secret_last4 = serializers.SerializerMethodField() + + class Meta: + model = Integration + exclude = ["workspace"] + + def get_secret_last4(self, obj): + return obj.api_secret[-4:] + --- integrations/views.py +from rest_framework import generics +from rest_framework.exceptions import NotFound +from rest_framework.permissions import IsAuthenticated + +from accounts.models import Membership + +from .models import Integration +from .serializers import IntegrationSerializer + + +class IntegrationList(generics.ListAPIView): + serializer_class = IntegrationSerializer + permission_classes = [IsAuthenticated]