Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions backend/api/migrations/0116_app_description.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 4.2.27 on 2026-02-03 10:03

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('api', '0115_migrate_free_tier_to_v2_pricing'),
]

operations = [
migrations.AddField(
model_name='app',
name='description',
field=models.TextField(blank=True, null=True),
),
]
1 change: 1 addition & 0 deletions backend/api/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ class App(models.Model):
Organisation, related_name="apps", on_delete=models.CASCADE
)
name = models.CharField(max_length=64)
description = models.TextField(null=True, blank=True)
identity_key = models.CharField(max_length=256)
app_version = models.IntegerField(null=False, blank=False, default=1)
app_token = models.CharField(max_length=64)
Expand Down
40 changes: 40 additions & 0 deletions backend/backend/graphene/mutations/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,46 @@ def mutate(cls, root, info, id, name):
return UpdateAppNameMutation(app=app)


class UpdateAppInfoMutation(graphene.Mutation):
class Arguments:
id = graphene.ID(required=True)
name = graphene.String(required=False)
description = graphene.String(required=False)

app = graphene.Field(AppType)

@classmethod
def mutate(cls, root, info, id, name=None, description=None):
user = info.context.user
app = App.objects.get(id=id)

if not user_can_access_app(user.userId, app.id):
raise GraphQLError("You don't have access to this app")

if not user_has_permission(
info.context.user, "update", "Apps", app.organisation
):
raise GraphQLError("You don't have permission to update Apps")

if name is not None:
# Validate name is not blank
if not name or name.strip() == "":
raise GraphQLError("App name cannot be blank")

# Validate name length
if len(name) > 64:
raise GraphQLError("App name cannot exceed 64 characters")

app.name = name

if description is not None:
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing validation for description length. Unlike the name field which has a maximum length validation (64 characters), the description field has no length limit. This could potentially allow very large text inputs that may cause performance or storage issues. Consider adding a reasonable maximum length constraint (e.g., 10,000 or 50,000 characters) to prevent abuse.

Suggested change
if description is not None:
if description is not None:
# Validate description length
if len(description) > 10000:
raise GraphQLError("App description cannot exceed 10000 characters")

Copilot uses AI. Check for mistakes.
app.description = description

app.save()

return UpdateAppInfoMutation(app=app)


class DeleteAppMutation(graphene.Mutation):
class Arguments:
id = graphene.ID(required=True)
Expand Down
1 change: 1 addition & 0 deletions backend/backend/graphene/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -635,6 +635,7 @@ class Meta:
fields = (
"id",
"name",
"description",
"identity_key",
"wrapped_key_share",
"created_at",
Expand Down
2 changes: 2 additions & 0 deletions backend/backend/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@
RemoveAppMemberMutation,
RotateAppKeysMutation,
UpdateAppNameMutation,
UpdateAppInfoMutation,
)
from .graphene.mutations.organisation import (
BulkInviteOrganisationMembersMutation,
Expand Down Expand Up @@ -1051,6 +1052,7 @@ class Mutation(graphene.ObjectType):
rotate_app_keys = RotateAppKeysMutation.Field()
delete_app = DeleteAppMutation.Field()
update_app_name = UpdateAppNameMutation.Field()
update_app_info = UpdateAppInfoMutation.Field()
add_app_member = AddAppMemberMutation.Field()
bulk_add_app_members = BulkAddAppMembersMutation.Field()
remove_app_member = RemoveAppMemberMutation.Field()
Expand Down
8 changes: 4 additions & 4 deletions frontend/apollo/gql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,9 +111,9 @@ const documents = {
"query GetSubscriptionDetails($organisationId: ID!) {\n stripeSubscriptionDetails(organisationId: $organisationId) {\n subscriptionId\n planName\n planType\n billingPeriod\n status\n nextPaymentAmount\n currentPeriodStart\n currentPeriodEnd\n renewalDate\n cancelAt\n cancelAtPeriodEnd\n paymentMethods {\n id\n brand\n last4\n expMonth\n expYear\n isDefault\n }\n }\n}": types.GetSubscriptionDetailsDocument,
"query GetStripeSubscriptionEstimate($organisationId: ID!, $planType: PlanTypeEnum!, $billingPeriod: BillingPeriodEnum!, $previewV2: Boolean) {\n estimateStripeSubscription(\n organisationId: $organisationId\n planType: $planType\n billingPeriod: $billingPeriod\n previewV2: $previewV2\n ) {\n estimatedTotal\n seatCount\n unitPrice\n currency\n priceId\n }\n}": types.GetStripeSubscriptionEstimateDocument,
"query GetAppActivityChart($appId: ID!, $period: TimeRange) {\n appActivityChart(appId: $appId, period: $period) {\n index\n date\n data\n }\n}": types.GetAppActivityChartDocument,
"query GetAppDetail($organisationId: ID!, $appId: ID!) {\n apps(organisationId: $organisationId, appId: $appId) {\n id\n name\n identityKey\n createdAt\n appToken\n appSeed\n appVersion\n sseEnabled\n }\n}": types.GetAppDetailDocument,
"query GetAppDetail($organisationId: ID!, $appId: ID!) {\n apps(organisationId: $organisationId, appId: $appId) {\n id\n name\n description\n identityKey\n createdAt\n appToken\n appSeed\n appVersion\n sseEnabled\n }\n}": types.GetAppDetailDocument,
"query GetAppKmsLogs($appId: ID!, $start: BigInt, $end: BigInt) {\n kmsLogs(appId: $appId, start: $start, end: $end) {\n logs {\n id\n timestamp\n phaseNode\n eventType\n ipAddress\n country\n city\n phSize\n }\n count\n }\n}": types.GetAppKmsLogsDocument,
"query GetApps($organisationId: ID!, $appId: ID) {\n apps(organisationId: $organisationId, appId: $appId) {\n id\n name\n identityKey\n createdAt\n updatedAt\n sseEnabled\n members {\n id\n email\n fullName\n avatarUrl\n }\n serviceAccounts {\n id\n name\n }\n environments {\n id\n name\n envType\n syncs {\n id\n serviceInfo {\n id\n name\n provider {\n id\n name\n }\n }\n status\n }\n }\n }\n}": types.GetAppsDocument,
"query GetApps($organisationId: ID!, $appId: ID) {\n apps(organisationId: $organisationId, appId: $appId) {\n id\n name\n description\n identityKey\n createdAt\n updatedAt\n sseEnabled\n members {\n id\n email\n fullName\n avatarUrl\n }\n serviceAccounts {\n id\n name\n }\n environments {\n id\n name\n envType\n syncs {\n id\n serviceInfo {\n id\n name\n provider {\n id\n name\n }\n }\n status\n }\n }\n }\n}": types.GetAppsDocument,
"query GetDashboard($organisationId: ID!) {\n apps(organisationId: $organisationId) {\n id\n name\n sseEnabled\n }\n userTokens(organisationId: $organisationId) {\n id\n }\n organisationInvites(orgId: $organisationId) {\n id\n }\n organisationMembers(organisationId: $organisationId, role: null) {\n id\n }\n savedCredentials(orgId: $organisationId) {\n id\n }\n syncs(orgId: $organisationId) {\n id\n }\n}": types.GetDashboardDocument,
"query GetOrganisations {\n organisations {\n id\n name\n identityKey\n createdAt\n plan\n planDetail {\n name\n maxUsers\n maxApps\n maxEnvsPerApp\n seatsUsed {\n users\n serviceAccounts\n total\n }\n appCount\n }\n role {\n name\n description\n color\n permissions\n }\n memberId\n keyring\n recovery\n pricingVersion\n }\n}": types.GetOrganisationsDocument,
"query GetAwsStsEndpoints {\n awsStsEndpoints\n}": types.GetAwsStsEndpointsDocument,
Expand Down Expand Up @@ -580,15 +580,15 @@ export function graphql(source: "query GetAppActivityChart($appId: ID!, $period:
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "query GetAppDetail($organisationId: ID!, $appId: ID!) {\n apps(organisationId: $organisationId, appId: $appId) {\n id\n name\n identityKey\n createdAt\n appToken\n appSeed\n appVersion\n sseEnabled\n }\n}"): (typeof documents)["query GetAppDetail($organisationId: ID!, $appId: ID!) {\n apps(organisationId: $organisationId, appId: $appId) {\n id\n name\n identityKey\n createdAt\n appToken\n appSeed\n appVersion\n sseEnabled\n }\n}"];
export function graphql(source: "query GetAppDetail($organisationId: ID!, $appId: ID!) {\n apps(organisationId: $organisationId, appId: $appId) {\n id\n name\n description\n identityKey\n createdAt\n appToken\n appSeed\n appVersion\n sseEnabled\n }\n}"): (typeof documents)["query GetAppDetail($organisationId: ID!, $appId: ID!) {\n apps(organisationId: $organisationId, appId: $appId) {\n id\n name\n description\n identityKey\n createdAt\n appToken\n appSeed\n appVersion\n sseEnabled\n }\n}"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "query GetAppKmsLogs($appId: ID!, $start: BigInt, $end: BigInt) {\n kmsLogs(appId: $appId, start: $start, end: $end) {\n logs {\n id\n timestamp\n phaseNode\n eventType\n ipAddress\n country\n city\n phSize\n }\n count\n }\n}"): (typeof documents)["query GetAppKmsLogs($appId: ID!, $start: BigInt, $end: BigInt) {\n kmsLogs(appId: $appId, start: $start, end: $end) {\n logs {\n id\n timestamp\n phaseNode\n eventType\n ipAddress\n country\n city\n phSize\n }\n count\n }\n}"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "query GetApps($organisationId: ID!, $appId: ID) {\n apps(organisationId: $organisationId, appId: $appId) {\n id\n name\n identityKey\n createdAt\n updatedAt\n sseEnabled\n members {\n id\n email\n fullName\n avatarUrl\n }\n serviceAccounts {\n id\n name\n }\n environments {\n id\n name\n envType\n syncs {\n id\n serviceInfo {\n id\n name\n provider {\n id\n name\n }\n }\n status\n }\n }\n }\n}"): (typeof documents)["query GetApps($organisationId: ID!, $appId: ID) {\n apps(organisationId: $organisationId, appId: $appId) {\n id\n name\n identityKey\n createdAt\n updatedAt\n sseEnabled\n members {\n id\n email\n fullName\n avatarUrl\n }\n serviceAccounts {\n id\n name\n }\n environments {\n id\n name\n envType\n syncs {\n id\n serviceInfo {\n id\n name\n provider {\n id\n name\n }\n }\n status\n }\n }\n }\n}"];
export function graphql(source: "query GetApps($organisationId: ID!, $appId: ID) {\n apps(organisationId: $organisationId, appId: $appId) {\n id\n name\n description\n identityKey\n createdAt\n updatedAt\n sseEnabled\n members {\n id\n email\n fullName\n avatarUrl\n }\n serviceAccounts {\n id\n name\n }\n environments {\n id\n name\n envType\n syncs {\n id\n serviceInfo {\n id\n name\n provider {\n id\n name\n }\n }\n status\n }\n }\n }\n}"): (typeof documents)["query GetApps($organisationId: ID!, $appId: ID) {\n apps(organisationId: $organisationId, appId: $appId) {\n id\n name\n description\n identityKey\n createdAt\n updatedAt\n sseEnabled\n members {\n id\n email\n fullName\n avatarUrl\n }\n serviceAccounts {\n id\n name\n }\n environments {\n id\n name\n envType\n syncs {\n id\n serviceInfo {\n id\n name\n provider {\n id\n name\n }\n }\n status\n }\n }\n }\n}"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
Expand Down
Loading
Loading