diff --git a/myauth/migrations/0037_plugin_description.py b/myauth/migrations/0037_plugin_description.py new file mode 100644 index 0000000..afc1b12 --- /dev/null +++ b/myauth/migrations/0037_plugin_description.py @@ -0,0 +1,36 @@ +# Generated by Django 3.1.4 on 2022-06-29 09:35 + +from django.db import migrations, models + + +def add_default_author(apps, schema_editor): + Plugin = apps.get_model("myauth", "Plugin") + for plugin in Plugin.objects.all(): + plugin.author = plugin.team.name + plugin.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ("myauth", "0036_usage"), + ] + + operations = [ + migrations.AddField( + model_name="plugin", + name="description", + field=models.CharField(blank=True, max_length=500), + ), + migrations.AddField( + model_name="plugin", + name="author", + field=models.CharField(max_length=50, blank=True, null=True), + ), + migrations.RunPython(add_default_author), + migrations.AlterField( + model_name="plugin", + name="author", + field=models.CharField(max_length=50), + ), + ] diff --git a/myauth/migrations/0038_auto_20220706_1130.py b/myauth/migrations/0038_auto_20220706_1130.py new file mode 100644 index 0000000..eab60c7 --- /dev/null +++ b/myauth/migrations/0038_auto_20220706_1130.py @@ -0,0 +1,33 @@ +# Generated by Django 3.1.4 on 2022-07-06 11:30 + +from django.db import migrations, models +import django.db.models.deletion + + +def add_default_origin(apps, schema_editor): + Plugin = apps.get_model("myauth", "Plugin") + for plugin in Plugin.objects.all(): + plugin.origin_id = plugin.id + plugin.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ("myauth", "0037_plugin_description"), + ] + + operations = [ + migrations.RemoveField( + model_name="plugin", + name="author", + ), + migrations.AddField( + model_name="plugin", + name="origin", + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to="myauth.plugin" + ), + ), + migrations.RunPython(add_default_origin), + ] diff --git a/myauth/migrations/0039_auto_20220707_1016.py b/myauth/migrations/0039_auto_20220707_1016.py new file mode 100644 index 0000000..d0a99dd --- /dev/null +++ b/myauth/migrations/0039_auto_20220707_1016.py @@ -0,0 +1,28 @@ +# Generated by Django 3.1.4 on 2022-07-07 10:16 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("myauth", "0038_auto_20220706_1130"), + ] + + operations = [ + migrations.AddField( + model_name="plugin", + name="is_public", + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name="trustedservice", + name="encrypted_access_key", + field=models.CharField(blank=True, max_length=100, null=True), + ), + migrations.AddField( + model_name="trustedservice", + name="public_key", + field=models.CharField(blank=True, max_length=100, null=True), + ), + ] diff --git a/myauth/migrations/0040_auto_20220714_1157.py b/myauth/migrations/0040_auto_20220714_1157.py new file mode 100644 index 0000000..2cfeeb3 --- /dev/null +++ b/myauth/migrations/0040_auto_20220714_1157.py @@ -0,0 +1,18 @@ +# Generated by Django 3.1.4 on 2022-07-14 11:57 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("myauth", "0039_auto_20220707_1016"), + ] + + operations = [ + migrations.AlterField( + model_name="plugin", + name="is_public", + field=models.BooleanField(blank=True, default=False, null=True), + ), + ] diff --git a/myauth/migrations/0037_userfeedback.py b/myauth/migrations/0041_userfeedback.py similarity index 96% rename from myauth/migrations/0037_userfeedback.py rename to myauth/migrations/0041_userfeedback.py index 419b93e..2e188c5 100644 --- a/myauth/migrations/0037_userfeedback.py +++ b/myauth/migrations/0041_userfeedback.py @@ -8,7 +8,7 @@ class Migration(migrations.Migration): dependencies = [ - ("myauth", "0036_usage"), + ("myauth", "0040_auto_20220714_1157"), ] operations = [ diff --git a/myauth/models.py b/myauth/models.py index 332be1d..21822a1 100644 --- a/myauth/models.py +++ b/myauth/models.py @@ -163,20 +163,24 @@ class Plugin(models.Model): TYPE = (("Javascript", "Javascript"), ("Python", "Python"), ("AI", "AI")) id: int - team = models.ForeignKey(Team, on_delete=models.CASCADE) - type = models.CharField(max_length=20, choices=TYPE) + team = models.ForeignKey(Team, on_delete=models.CASCADE) # cannot be edited by user + type = models.CharField(max_length=20, choices=TYPE) # cannot be edited by user + url = models.URLField(blank=False, null=False, max_length=200) # cannot be edited by user + origin = models.ForeignKey("self", blank=True, null=True, on_delete=models.CASCADE) # cannot be edited by user name = models.CharField(blank=False, null=False, max_length=50) - url = models.URLField(blank=False, null=False, max_length=200) + description = models.CharField(blank=True, null=False, max_length=500) products = models.CharField(max_length=20, choices=PRODUCTS) enabled = models.BooleanField(null=False, default=False) collections = models.ManyToManyField(Collection, blank=True) + is_public = models.BooleanField(blank=True, null=True, default=False) class TrustedService(models.Model): - id: int user = models.ForeignKey(User, on_delete=models.CASCADE) plugin = models.OneToOneField(Plugin, on_delete=models.CASCADE) + public_key = models.CharField(max_length=100, blank=True, null=True) + encrypted_access_key = models.CharField(max_length=100, blank=True, null=True) class UserFeedback(models.Model): diff --git a/server/api/helpers.py b/server/api/helpers.py new file mode 100644 index 0000000..3278a99 --- /dev/null +++ b/server/api/helpers.py @@ -0,0 +1,88 @@ +from loguru import logger +from typing import List, Dict, Union +from myauth.models import Plugin, TrustedService +from .schemas import PluginInSchema +from django.core.exceptions import ObjectDoesNotExist, ValidationError +from django.core.validators import URLValidator +from django_etebase.models import Collection, CollectionMember + + +def process_collection_uids(model, collection_uids): + if collection_uids is not None: + model.collections.clear() + for uid in collection_uids: + try: + collection = Collection.objects.get(uid=uid) + model.collections.add(collection) + + except ObjectDoesNotExist: + logger.error(f"Project {uid} does not exist.") + model.save() + + +def process_plugins(plugins: List[Plugin]) -> List[Plugin]: + for p in plugins: + if p is not None: + p.author = get_author(p) + if p.type == "Javascript": + p.collection_uids = p.collections.values_list("uid", flat=True) + + if p.type == "Python" or p.type == "AI": + ts = TrustedService.objects.get(plugin_id=p.id) + + current_collections = CollectionMember.objects.filter(user__email=ts.user.email).values_list( + "collection__uid", flat=True + ) + p.collection_uids: List[Dict[str, Union[str, bool]]] = [ + {"uid": uid, "is_invite_pending": uid not in current_collections} + for uid in p.collections.values_list("uid", flat=True) + ] + p.username = ts.user.username + p.public_key = ts.public_key + p.encrypted_access_key = ts.encrypted_access_key + + return plugins + + +def add_plugin(payload: PluginInSchema, team_id: int) -> Plugin: + is_origin = payload.origin_id is None + plugin = Plugin.objects.create( + team_id=team_id, + name=payload.name, + type=payload.type, + description=payload.description, + url=payload.url, + products=payload.products, + enabled=payload.enabled if is_origin else True, + is_public=payload.is_public if is_origin else None, + ) + plugin.origin_id = plugin.id if payload.origin_id is None else payload.origin_id + plugin.save() + + process_collection_uids(plugin, payload.collection_uids) + return plugin + + +def edit_plugin(plugin: Plugin, payload: PluginInSchema) -> None: + plugin.name = payload.name + plugin.description = payload.description + plugin.products = payload.products + plugin.enabled = payload.enabled + plugin.is_public = payload.is_public + plugin.save() + + process_collection_uids(plugin, payload.collection_uids) + + +def get_author(plugin: Plugin) -> str: + return plugin.team.name if plugin.origin is None else plugin.origin.team.name + + +def is_valid_url(url): + validator = URLValidator() + try: + validator(url) + return True + except ValidationError as e: + logger.warning(f"Received ValidationError: {e}") + return False diff --git a/server/api/plugin.py b/server/api/plugin.py index 18dc11b..d2bff0d 100644 --- a/server/api/plugin.py +++ b/server/api/plugin.py @@ -1,32 +1,48 @@ +from datetime import datetime, timezone from typing import List - +from loguru import logger from ninja import Router - -from myauth.models import Plugin -from .schemas import PluginSchema, PluginCreated, Error +from django.db.models import Q from django.core.exceptions import ObjectDoesNotExist -from .trusted_service import process_collection_uids, is_valid_url -from django_etebase.models import Collection +from myauth.models import Plugin, TrustedService, User, UserProfile +from .schemas import PluginOutSchema, PluginInSchema, PluginCreatedSchema, PluginDeleteSchema, Error +from .helpers import is_valid_url, edit_plugin, add_plugin, process_plugins -from loguru import logger router = Router() -@router.get("/", response={200: List[PluginSchema], 403: Error}) +@router.get("/", response={200: List[PluginOutSchema], 403: Error}) def get_plugins(request): user = request.auth - filter_args = {"team_id": user.userprofile.team.id, "type": "Javascript"} - plugins = Plugin.objects.filter(**filter_args) + plugins = Plugin.objects.filter(team_id=user.userprofile.team.id) + + return process_plugins(plugins) + + +@router.get("/zoo/", response={200: List[PluginOutSchema], 403: Error}) +def get_zoo_plugins(request): + user = request.auth + + plugins = Plugin.objects.filter(Q(is_public=True)).exclude(team_id=user.userprofile.team.id) + excluded_plugin_ids = [] # further exclude all plugins that have already been activated for p in plugins: - p.collection_uids = p.collections.values_list("uid", flat=True) + try: + activated_plugin = Plugin.objects.get(Q(team_id=user.userprofile.team.id) & Q(origin_id=p.id)) + if activated_plugin is not None: + excluded_plugin_ids.append(p.id) + + except ObjectDoesNotExist: + pass - return plugins + plugins = plugins.filter(~Q(id__in=excluded_plugin_ids)) + return process_plugins(plugins) -@router.post("/", response={200: PluginCreated, 403: Error, 500: Error, 400: Error}) -def create_plugin(request, payload: PluginSchema): + +@router.post("/", response={200: PluginCreatedSchema, 403: Error, 500: Error, 400: Error}) +def create_plugin(request, payload: PluginInSchema): user = request.auth if user.team.owner_id is not user.id: @@ -35,34 +51,62 @@ def create_plugin(request, payload: PluginSchema): if not is_valid_url(payload.url): return 400, {"message": "The URL is invalid."} + if payload.type != "Javascript" and ( + not hasattr(payload, "username") + or not hasattr(payload, "public_key") + or not hasattr(payload, "encrypted_access_key") + ): + return 400, {"message": "Missing data in the POST request."} + try: - filter_args = {"team_id": user.userprofile.team.id, "url": payload.url} + filter_args = {"team_id": user.userprofile.team.id, "url": payload.url, "origin_id": payload.origin_id} plugin = Plugin.objects.get(**filter_args) if plugin is not None: - return 409, {"message": "Plugin already exists."} - + return 409, {"message": "Plugin already in use."} except ObjectDoesNotExist: pass try: - plugin = Plugin.objects.create( - name=payload.name, - type=payload.type, - team_id=user.team.id, - url=payload.url, - products=payload.products, - enabled=payload.enabled, - ) - - process_collection_uids(plugin, payload.collection_uids) + plugin = add_plugin(payload, user.team.id) + + if payload.type == "Python" or payload.type == "AI": + try: + # Get the "user" for the service + ts_user = User.objects.get(email=payload.username) + + # Create a profile + user_profile = UserProfile.objects.create( + user_id=ts_user.id, + team_id=user.team.id, + name=payload.name, + recovery_key=None, + accepted_terms_and_conditions=datetime.now(tz=timezone.utc), + is_collaborator=False, + is_trusted_service=True, + email_verified=datetime.now(tz=timezone.utc), + ) + user_profile.id = user_profile.user_id + user_profile.email = user.email + user_profile.save() + + TrustedService.objects.create( + user_id=ts_user.id, + plugin_id=plugin.id, + public_key=payload.public_key, + encrypted_access_key=payload.encrypted_access_key, + ) + except Exception as e: + logger.error(e) + plugin.delete() + return 500, {"message": "Something went wrong"} return {"id": plugin.id} except Exception as e: logger.error(e) -@router.put("/", response={200: PluginCreated, 403: Error, 500: Error}) -def update_plugin(request, payload: PluginSchema): +@router.put("/", response={200: PluginCreatedSchema, 403: Error, 500: Error}) +def update_plugin(request, payload: PluginInSchema): user = request.auth if user.team.owner_id is not user.id: @@ -72,20 +116,23 @@ def update_plugin(request, payload: PluginSchema): filter_args = {"team_id": user.userprofile.team.id, "url": payload.url} plugin = Plugin.objects.get(**filter_args) - plugin.name = payload.name - plugin.products = payload.products - plugin.enabled = payload.enabled - plugin.save() + edit_plugin(plugin, payload) - process_collection_uids(plugin, payload.collection_uids) + try: + ts = TrustedService.objects.get(plugin_id=plugin.id) + user_profile = ts.user.userprofile + user_profile.name = payload.name + user_profile.save() + except ObjectDoesNotExist: + pass return {"id": plugin.id} except Exception as e: logger.error(e) -@router.delete("/", response={200: PluginCreated, 403: Error, 500: Error}) -def delete_plugin(request, payload: PluginSchema): +@router.delete("/", response={200: PluginCreatedSchema, 403: Error, 500: Error}) +def delete_plugin(request, payload: PluginDeleteSchema): user = request.auth if user.team.owner_id is not user.id: @@ -95,7 +142,16 @@ def delete_plugin(request, payload: PluginSchema): filter_args = {"team_id": user.userprofile.team.id, "url": payload.url} plugin = Plugin.objects.get(**filter_args) plugin_id = plugin.id - plugin.delete() + try: + ts = TrustedService.objects.get(plugin_id=plugin.id) + user = ts.user + logger.info(user.email) + user.delete() + except ObjectDoesNotExist: + pass + finally: + plugin.delete() + return {"id": plugin_id} except Exception as e: logger.error(e) diff --git a/server/api/schemas.py b/server/api/schemas.py index 9ab9819..e4c1aab 100644 --- a/server/api/schemas.py +++ b/server/api/schemas.py @@ -2,7 +2,6 @@ from ninja import Schema from ninja.orm import create_schema from pydantic import validator, typing, conint - from myauth.models import Tier TierSchema = create_schema(Tier) @@ -16,21 +15,34 @@ class CollectionUid(Schema): class PluginSchema(Schema): type: str name: str + description: str url: str products: str enabled: bool - collection_uids: Optional[Union[List[CollectionUid], List[str]]] = None + is_public: Union[bool, None] # is_public is only set for origin plugins, while copies of a plugin cannot be shared + origin_id: Union[int, None] -class PluginCreated(Schema): - id: int +class TrustedServiceSchema(Schema): + username: Optional[str] + public_key: Optional[Union[str, None]] + encrypted_access_key: Optional[Union[str, None]] + + +class PluginOutSchema(PluginSchema, TrustedServiceSchema): + collection_uids: Union[List[CollectionUid], List[str]] = None + author: str -class TrustedServiceSchema(PluginSchema): - username: str +class PluginInSchema(PluginSchema, TrustedServiceSchema): + collection_uids: List[str] = None + + +class PluginDeleteSchema(Schema): + url: str -class TrustedServiceCreated(Schema): +class PluginCreatedSchema(Schema): id: int diff --git a/server/api/trusted_service.py b/server/api/trusted_service.py deleted file mode 100644 index 4a93c67..0000000 --- a/server/api/trusted_service.py +++ /dev/null @@ -1,163 +0,0 @@ -from datetime import datetime, timezone -from typing import List, Dict, Union -from django.core.exceptions import ObjectDoesNotExist, ValidationError -from django.core.validators import URLValidator - -from ninja import Router - -from loguru import logger - -from myauth.models import TrustedService, Plugin, User, UserProfile -from django_etebase.models import Collection, CollectionMember -from .schemas import TrustedServiceSchema, TrustedServiceCreated, Error - -router = Router() - - -def process_collection_uids(model, collection_uids): - if collection_uids is not None: - model.collections.clear() - for uid in collection_uids: - try: - collection = Collection.objects.get(uid=uid) - model.collections.add(collection) - - except ObjectDoesNotExist: - logger.error(f"Project {uid} does not exist.") - model.save() - - -def is_valid_url(url): - validator = URLValidator() - try: - validator(url) - return True - except ValidationError as e: - logger.warning(f"Received ValidationError: {e}") - return False - - -@router.get("/", response={200: List[TrustedServiceSchema], 403: Error}) -def get_trusted_service(request, response=TrustedServiceSchema): - user = request.auth - - plugins = Plugin.objects.filter(team_id=user.userprofile.team.id).exclude(type="Javascript") - - for p in plugins: - ts = TrustedService.objects.get(plugin_id=p.id) - current_collections = CollectionMember.objects.filter(user__email=ts.user.email).values_list( - "collection__uid", flat=True - ) - p.collection_uids: List[Dict[str, Union[str, bool]]] = [ - {"uid": uid, "is_invite_pending": uid not in current_collections} - for uid in p.collections.values_list("uid", flat=True) - ] - p.username = ts.user.username - return plugins - - -@router.post("/", response={200: TrustedServiceCreated, 403: Error, 409: Error, 500: Error, 400: Error}) -def create_trusted_service(request, payload: TrustedServiceSchema): - user = request.auth - - if user.team.owner_id is not user.id: - return 403, {"message": "Only owners can create trusted services."} - - if not is_valid_url(payload.url): - return 400, {"message": "The URL is invalid."} - - try: - filter_args = {"team_id": user.userprofile.team.id, "url": payload.url} - plugin = Plugin.objects.get(**filter_args) - if plugin is not None: - return 409, {"message": "Plugin already exists."} - - except ObjectDoesNotExist: - pass - - try: - # Get the "user" for the service - ts_user = User.objects.get(email=payload.username) - - # Create a profile - user_profile = UserProfile.objects.create( - user_id=ts_user.id, - team_id=user.team.id, - name=payload.name, - recovery_key=None, - accepted_terms_and_conditions=datetime.now(tz=timezone.utc), - is_collaborator=False, - is_trusted_service=True, - email_verified=datetime.now(tz=timezone.utc), - ) - user_profile.id = user_profile.user_id - user_profile.email = user.email - user_profile.save() - - plugin = Plugin.objects.create( - team_id=user.team.id, - type=payload.type, - name=payload.name, - url=payload.url, - products=payload.products, - enabled=payload.enabled, - ) - - process_collection_uids(plugin, payload.collection_uids) - - ts = TrustedService.objects.create( - user_id=ts_user.id, - plugin_id=plugin.id, - ) - - return {"id": ts.id} - except Exception as e: - logger.error(e) - - -@router.put("/", response={200: TrustedServiceCreated, 403: Error, 500: Error}) -def update_trusted_service(request, payload: TrustedServiceSchema): - - user = request.auth - - if user.team.owner_id is not user.id: - return 403, {"message": "Only owners can update trusted services."} - - try: - filter_args = {"team_id": user.userprofile.team.id, "url": payload.url} - plugin = Plugin.objects.get(**filter_args) - - plugin.name = payload.name - plugin.products = payload.products - plugin.enabled = payload.enabled - plugin.save() - - process_collection_uids(plugin, payload.collection_uids) - - ts = TrustedService.objects.get(plugin_id=plugin.id) - user_profile = ts.user.userprofile - user_profile.name = payload.name - user_profile.save() - - return {"id": ts.id} - except Exception as e: - logger.error(e) - - -@router.delete("/", response={200: TrustedServiceCreated, 403: Error, 500: Error}) -def delete_trusted_service(request, payload: TrustedServiceSchema): - - user = request.auth - - if user.team.owner_id is not user.id: - return 403, {"message": "Only owners can delete trusted services."} - - try: - ts_user = User.objects.get(email=payload.username) - ts = TrustedService.objects.get(user_id=ts_user.id) - ts_id = ts.id - ts.plugin.delete() - ts.user.delete() - return {"id": ts_id} - except Exception as e: - logger.error(e) diff --git a/server/urls.py b/server/urls.py index 9cbe31c..a15b126 100644 --- a/server/urls.py +++ b/server/urls.py @@ -10,9 +10,6 @@ from django.contrib.staticfiles import finders from ninja import NinjaAPI from ninja.security import APIKeyHeader -from myauth.models import TrustedService -from myauth.models import Plugin - from etebase_fastapi.dependencies import get_authenticated_user from .api.user import router as users_router @@ -20,7 +17,6 @@ from .api.team import router as teams_router from .api.project import router as project_router from .api.billing import router as billing_router -from .api.trusted_service import router as trusted_service_router from .api.plugin import router as plugin_router from .api.sentry import router as sentry_router from .api.feedback import router as feedback_router @@ -58,7 +54,6 @@ def healthcheck(request): api.add_router("/billing", billing_router) api.add_router("/project", project_router) api.add_router("/tunnel", sentry_router) -api.add_router("/trusted_service", trusted_service_router) api.add_router("/plugin", plugin_router) api.add_router("/feedback", feedback_router)