Repository: PostHog/posthog
Pull request: Disable Link Sharing
## Changes
Fix #1465 

*Please describe.*  
This adds a switch to allow the user to disable link sharing on the frontend

## Checklist

- [x] Add backend endpoint to disable link sharing
- [x] Update frontend to hide link sharing when link sharing param is set to None




Problem statement:
Allow disabling of sharing link in settings
## Is your feature request related to a problem?

I want to be able to disable the sharing link for extra security

## Describe the solution you'd like

A toggle to disable the sharing link in the sharing modal

## Describe alternatives you've considered

-

## Additional context



#### *Thank you* for your feature request – we love each and every one!

Hints from issue discussion:
A slightly friendlier solution may be just a "Regenerate invite" button, which'd replace the team's current `signup_token` with a new one (possibly less work to implement on the frontend too).
On the backend, this would be best as an `@action(methods=["POST"], detail=False)` on an `/api/team/` endpoint. The endpoint itself could be very basic, it'd just serve as a logical place for an action on the Team model.
Hey @timgl @Twixes,

Can I take up this issue? Not sure if the pairing offer in the README still holds but if it does I'd love to schedule a pairing session to start on this issue and get familiar with this codebase.

Thanks
Joel
@J0 For sure! Feel free to [schedule some time in my calendar](https://calendly.com/timgl/30min?month=2020-08)
Done. Looking forward to building this feature! :)

Commit message: Remove unused import and stray comment

Patch to review:
diff --git a/frontend/src/lib/components/TeamInvitation.js b/frontend/src/lib/components/TeamInvitation.js
index 5d66e656f631e..2bed7d617f298 100644
--- a/frontend/src/lib/components/TeamInvitation.js
+++ b/frontend/src/lib/components/TeamInvitation.js
@@ -1,6 +1,7 @@
 import React from 'react'
-import { Modal } from 'antd'
+import { Modal, Switch } from 'antd'
 import { CopyToClipboard } from 'lib/components/CopyToClipboard'
+import api from '../api'
 
 export function TeamInvitationLink({ user }) {
     return (
@@ -12,16 +13,29 @@ export function TeamInvitationLink({ user }) {
 }
 
 export function TeamInvitationContent({ user }) {
-    return (
-        <div>
-            <p>
-                <TeamInvitationLink user={user} />
-            </p>
-            Invite teammates with the link above.
-            <br />
-            Build an even better product, <i>together</i>.
-        </div>
-    )
+    const url = window.location.origin
+    const signup_token_state = user['team']['signup_token']
+    const signup_data = `{'team':{'signup_token': ${signup_token_state}}}`
+    if (signup_token_state != null) {
+        return (
+            <div>
+                <p>
+                    <TeamInvitationLink user={user} />
+                </p>
+                Invite teammates with the link above.
+                <br />
+                Build an even better product, <i>together</i>.
+                <br />
+                Link Sharing: <Switch defaultChecked onChange={() => api.update(url, signup_data)} />
+            </div>
+        )
+    } else {
+        return (
+            <div>
+                Link Sharing: <Switch onChange={() => api.update(url, signup_data)} />
+            </div>
+        )
+    }
 }
 
 export function TeamInvitationModal({ user, visible, onCancel }) {
diff --git a/posthog/api/test/test_user.py b/posthog/api/test/test_user.py
index 8f2d396a22ae6..d2bf34e475533 100644
--- a/posthog/api/test/test_user.py
+++ b/posthog/api/test/test_user.py
@@ -1,5 +1,5 @@
+from unittest.mock import patch
 from posthog.models import Team, User
-
 from .base import BaseTest
 
 
@@ -40,6 +40,27 @@ def test_user_team_update(self):
         self.assertEqual(team.opt_out_capture, True)
         self.assertEqual(team.anonymize_ips, False)
 
+    @patch("secrets.token_urlsafe")
+    def test_user_team_update_signup_token(self, patch_token):
+        patch_token.return_value = "abcde"
+        response = self.client.patch(
+            "/api/user/", data={"team": {"signup_token": False}}, content_type="application/json",
+        ).json()
+
+        self.assertEqual(response["team"]["signup_token"], None)
+
+        team = Team.objects.get(id=self.team.id)
+        self.assertEqual(team.signup_token, None)
+
+        response = self.client.patch(
+            "/api/user/", data={"team": {"signup_token": True}}, content_type="application/json",
+        ).json()
+
+        self.assertEqual(response["team"]["signup_token"], "abcde")
+
+        team = Team.objects.get(id=self.team.id)
+        self.assertEqual(team.signup_token, "abcde")
+
 
 class TestUserChangePassword(BaseTest):
     TESTS_API = True
diff --git a/posthog/api/user.py b/posthog/api/user.py
index aabfc62f1b14b..527cff61c33c8 100644
--- a/posthog/api/user.py
+++ b/posthog/api/user.py
@@ -35,6 +35,12 @@ def user(request):
             team.completed_snippet_onboarding = data["team"].get(
                 "completed_snippet_onboarding", team.completed_snippet_onboarding
             )
+            signup_state = data["team"].get("signup_token")
+            if signup_state == True:
+                team.signup_token = secrets.token_urlsafe(22)
+            elif signup_state == False:
+                team.signup_token = None
+
             team.save()
 
         if "user" in data:


Code review comments:
diff_hunk: @@ -12,16 +13,29 @@ export function TeamInvitationLink({ user }) {
 }
 
 export function TeamInvitationContent({ user }) {
-    return (
-        <div>
-            <p>
-                <TeamInvitationLink user={user} />
-            </p>
-            Invite teammates with the link above.
-            <br />
-            Build an even better product, <i>together</i>.
-        </div>
-    )
+    const url = window.location.origin
+    const signup_token_state = user['team']['signup_token']
+    const signup_data = `{'team':{'signup_token': ${signup_token_state}}}`
path: frontend/src/lib/components/TeamInvitation.js
text: @user1:
This should be just a JS object. Also, JSON strings are enclosed in **double** quotes (like "so"), so ultimately this couldn't have been decoded as JSON.

@author:
Ah right, that makes sense. Thanks!

diff_hunk: @@ -12,16 +13,29 @@ export function TeamInvitationLink({ user }) {
 }
 
 export function TeamInvitationContent({ user }) {
-    return (
-        <div>
-            <p>
-                <TeamInvitationLink user={user} />
-            </p>
-            Invite teammates with the link above.
-            <br />
-            Build an even better product, <i>together</i>.
-        </div>
-    )
+    const url = window.location.origin
+    const signup_token_state = user['team']['signup_token']
+    const signup_data = `{'team':{'signup_token': ${signup_token_state}}}`
+    if (signup_token_state != null) {
+        return (
+            <div>
+                <p>
+                    <TeamInvitationLink user={user} />
+                </p>
+                Invite teammates with the link above.
+                <br />
+                Build an even better product, <i>together</i>.
+                <br />
+                Link Sharing: <Switch defaultChecked onChange={() => api.update(url, signup_data)} />
path: frontend/src/lib/components/TeamInvitation.js
text: @user1:
Actually, since this is updating `user`, it should use `userLogic.actions.userUpdateRequest`. Sorry for the confusion! Kea logics are way better for state though then syncing state and API responses inside components.

diff_hunk: @@ -35,6 +35,12 @@ def user(request):
             team.completed_snippet_onboarding = data["team"].get(
                 "completed_snippet_onboarding", team.completed_snippet_onboarding
             )
+            signup_state = data["team"].get("signup_token")
path: posthog/api/user.py
text: @user1:
This should be `"signup_state"`.