diff --git a/backend/Dockerfile b/backend/Dockerfile index c50cde3..f466160 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -13,6 +13,13 @@ COPY requirements.txt . RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/* RUN pip install --no-cache-dir -r requirements.txt +# Non-root user for celery workers/beat. UID matches typical host (1000) so +# bind-mounted volumes (./backend, ./legacy, media/) stay writable. Override +# by rebuilding with --build-arg APP_UID= if your host differs. +ARG APP_UID=1000 +ARG APP_GID=1000 +RUN groupadd -g ${APP_GID} app && useradd -u ${APP_UID} -g app -m -s /bin/bash app + # Copy the rest of the backend code COPY . . diff --git a/backend/api/views/admin.py b/backend/api/views/admin.py index 4676bfc..627fd1d 100644 --- a/backend/api/views/admin.py +++ b/backend/api/views/admin.py @@ -1,4 +1,5 @@ import json +from typing import Any from django.conf import settings from django.core.mail import send_mail @@ -14,15 +15,26 @@ from api.serializers import ( JobSerializer, SystemConfigSerializer, ) +from api.tasks import ( + backup_db_task, + cleanup_duplicates_task, + seed_banners_task, + seed_legacy_task, + sync_flavors_task, +) -JOB_TASK_MAP = { - "sync_flavors": "api.sync_flavors", - "cleanup_duplicates": "api.cleanup_duplicates", - "backup_db": "api.backup_db", - "seed_legacy": "api.seed_legacy", - "seed_banners": "api.seed_banners", +# Maps Job.name → (celery task name for beat schedules, callable for direct dispatch). +# Direct callables let .delay() honor CELERY_TASK_ALWAYS_EAGER in dev. +JOB_TASKS: dict[str, tuple[str, Any]] = { + "sync_flavors": ("api.sync_flavors", sync_flavors_task), + "cleanup_duplicates": ("api.cleanup_duplicates", cleanup_duplicates_task), + "backup_db": ("api.backup_db", backup_db_task), + "seed_legacy": ("api.seed_legacy", seed_legacy_task), + "seed_banners": ("api.seed_banners", seed_banners_task), } +JOB_TASK_MAP = {name: meta[0] for name, meta in JOB_TASKS.items()} + def _sync_periodic_task(job: Job) -> None: """Create/update a django_celery_beat PeriodicTask for this Job. @@ -106,25 +118,24 @@ class AdminViewSet(viewsets.ViewSet): @action(detail=True, methods=["post"]) def trigger_job(self, request: Request, pk=None) -> Response: - from celery import current_app - try: job = Job.objects.get(pk=pk) except Job.DoesNotExist: return Response(status=status.HTTP_404_NOT_FOUND) - task_name = JOB_TASK_MAP.get(job.name) - if not task_name: + meta = JOB_TASKS.get(job.name) + if not meta: return Response( {"error": f"No celery task registered for {job.name}"}, status=status.HTTP_400_BAD_REQUEST, ) + task_name, task_fn = meta job.status = "pending" job.error_message = "" job.save(update_fields=["status", "error_message"]) - current_app.send_task(task_name) + task_fn.delay() return Response({"status": "Job queued", "task": task_name}) @action(detail=True, methods=["patch"]) diff --git a/backend/schema.yaml b/backend/schema.yaml new file mode 100644 index 0000000..961743f --- /dev/null +++ b/backend/schema.yaml @@ -0,0 +1,2968 @@ +openapi: 3.0.3 +info: + title: Holy Flavors API + version: 1.0.0 + description: API for rating flavors +paths: + /api/admin-custom/{id}/trigger_job/: + post: + operationId: admin_custom_trigger_job_create + parameters: + - in: path + name: id + schema: + type: string + required: true + tags: + - admin-custom + security: + - jwtAuth: [] + responses: + '200': + description: No response body + /api/admin-custom/{id}/update_job_schedule/: + patch: + operationId: admin_custom_update_job_schedule_partial_update + parameters: + - in: path + name: id + schema: + type: string + required: true + tags: + - admin-custom + security: + - jwtAuth: [] + responses: + '200': + description: No response body + /api/admin-custom/{id}/user_detail/: + get: + operationId: admin_custom_user_detail_retrieve + parameters: + - in: path + name: id + schema: + type: string + required: true + tags: + - admin-custom + security: + - jwtAuth: [] + responses: + '200': + description: No response body + patch: + operationId: admin_custom_user_detail_partial_update + parameters: + - in: path + name: id + schema: + type: string + required: true + tags: + - admin-custom + security: + - jwtAuth: [] + responses: + '200': + description: No response body + delete: + operationId: admin_custom_user_detail_destroy + parameters: + - in: path + name: id + schema: + type: string + required: true + tags: + - admin-custom + security: + - jwtAuth: [] + responses: + '204': + description: No response body + /api/admin-custom/config/: + get: + operationId: admin_custom_config_retrieve + tags: + - admin-custom + security: + - jwtAuth: [] + responses: + '200': + description: No response body + patch: + operationId: admin_custom_config_partial_update + tags: + - admin-custom + security: + - jwtAuth: [] + responses: + '200': + description: No response body + /api/admin-custom/jobs/: + get: + operationId: admin_custom_jobs_retrieve + tags: + - admin-custom + security: + - jwtAuth: [] + responses: + '200': + description: No response body + /api/admin-custom/send_test_email/: + post: + operationId: admin_custom_send_test_email_create + tags: + - admin-custom + security: + - jwtAuth: [] + responses: + '200': + description: No response body + /api/admin-custom/stats/: + get: + operationId: admin_custom_stats_retrieve + tags: + - admin-custom + security: + - jwtAuth: [] + responses: + '200': + description: No response body + /api/admin-custom/users/: + get: + operationId: admin_custom_users_retrieve + tags: + - admin-custom + security: + - jwtAuth: [] + responses: + '200': + description: No response body + /api/auth/token/: + post: + operationId: auth_token_create + description: |- + Takes a set of user credentials and returns an access and refresh JSON web + token pair to prove the authentication of those credentials. + tags: + - auth + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/TokenObtainPair' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/TokenObtainPair' + multipart/form-data: + schema: + $ref: '#/components/schemas/TokenObtainPair' + required: true + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/TokenObtainPair' + description: '' + /api/auth/token/refresh/: + post: + operationId: auth_token_refresh_create + description: |- + Takes a refresh type JSON web token and returns an access type JSON web + token if the refresh token is valid. + tags: + - auth + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/TokenRefresh' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/TokenRefresh' + multipart/form-data: + schema: + $ref: '#/components/schemas/TokenRefresh' + required: true + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/TokenRefresh' + description: '' + /api/auth/token/verify/: + post: + operationId: auth_token_verify_create + description: |- + Takes a token and indicates if it is valid. This view provides no + information about a token's fitness for a particular use. + tags: + - auth + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/TokenVerify' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/TokenVerify' + multipart/form-data: + schema: + $ref: '#/components/schemas/TokenVerify' + required: true + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/TokenVerify' + description: '' + /api/banners/: + get: + operationId: banners_list + tags: + - banners + security: + - jwtAuth: [] + - {} + responses: + '200': + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Banner' + description: '' + post: + operationId: banners_create + tags: + - banners + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Banner' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/Banner' + multipart/form-data: + schema: + $ref: '#/components/schemas/Banner' + required: true + security: + - jwtAuth: [] + responses: + '201': + content: + application/json: + schema: + $ref: '#/components/schemas/Banner' + description: '' + /api/banners/{id}/: + get: + operationId: banners_retrieve + parameters: + - in: path + name: id + schema: + type: integer + description: A unique integer value identifying this banner. + required: true + tags: + - banners + security: + - jwtAuth: [] + - {} + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Banner' + description: '' + put: + operationId: banners_update + parameters: + - in: path + name: id + schema: + type: integer + description: A unique integer value identifying this banner. + required: true + tags: + - banners + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Banner' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/Banner' + multipart/form-data: + schema: + $ref: '#/components/schemas/Banner' + required: true + security: + - jwtAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Banner' + description: '' + patch: + operationId: banners_partial_update + parameters: + - in: path + name: id + schema: + type: integer + description: A unique integer value identifying this banner. + required: true + tags: + - banners + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedBanner' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedBanner' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedBanner' + security: + - jwtAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Banner' + description: '' + delete: + operationId: banners_destroy + parameters: + - in: path + name: id + schema: + type: integer + description: A unique integer value identifying this banner. + required: true + tags: + - banners + security: + - jwtAuth: [] + responses: + '204': + description: No response body + /api/banners/{id}/activate/: + post: + operationId: banners_activate_create + parameters: + - in: path + name: id + schema: + type: integer + description: A unique integer value identifying this banner. + required: true + tags: + - banners + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Banner' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/Banner' + multipart/form-data: + schema: + $ref: '#/components/schemas/Banner' + required: true + security: + - jwtAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Banner' + description: '' + /api/banners/{id}/toggle_enabled/: + post: + operationId: banners_toggle_enabled_create + parameters: + - in: path + name: id + schema: + type: integer + description: A unique integer value identifying this banner. + required: true + tags: + - banners + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Banner' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/Banner' + multipart/form-data: + schema: + $ref: '#/components/schemas/Banner' + required: true + security: + - jwtAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Banner' + description: '' + /api/banners/active/: + get: + operationId: banners_active_retrieve + tags: + - banners + security: + - jwtAuth: [] + - {} + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Banner' + description: '' + /api/categories/: + get: + operationId: categories_list + parameters: + - name: page + required: false + in: query + description: A page number within the paginated result set. + schema: + type: integer + tags: + - categories + security: + - jwtAuth: [] + - {} + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PaginatedCategoryList' + description: '' + /api/categories/{id}/: + get: + operationId: categories_retrieve + parameters: + - in: path + name: id + schema: + type: integer + description: A unique integer value identifying this category. + required: true + tags: + - categories + security: + - jwtAuth: [] + - {} + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Category' + description: '' + /api/flavors/: + get: + operationId: flavors_list + parameters: + - in: query + name: category + schema: + type: integer + - in: query + name: category__slug + schema: + type: string + - name: page + required: false + in: query + description: A page number within the paginated result set. + schema: + type: integer + - name: search + required: false + in: query + description: A search term. + schema: + type: string + tags: + - flavors + security: + - jwtAuth: [] + - {} + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PaginatedFlavorList' + description: '' + post: + operationId: flavors_create + tags: + - flavors + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Flavor' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/Flavor' + multipart/form-data: + schema: + $ref: '#/components/schemas/Flavor' + required: true + security: + - jwtAuth: [] + responses: + '201': + content: + application/json: + schema: + $ref: '#/components/schemas/Flavor' + description: '' + /api/flavors/{id}/: + get: + operationId: flavors_retrieve + parameters: + - in: path + name: id + schema: + type: integer + description: A unique integer value identifying this flavor. + required: true + tags: + - flavors + security: + - jwtAuth: [] + - {} + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Flavor' + description: '' + put: + operationId: flavors_update + parameters: + - in: path + name: id + schema: + type: integer + description: A unique integer value identifying this flavor. + required: true + tags: + - flavors + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Flavor' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/Flavor' + multipart/form-data: + schema: + $ref: '#/components/schemas/Flavor' + required: true + security: + - jwtAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Flavor' + description: '' + patch: + operationId: flavors_partial_update + parameters: + - in: path + name: id + schema: + type: integer + description: A unique integer value identifying this flavor. + required: true + tags: + - flavors + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedFlavor' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedFlavor' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedFlavor' + security: + - jwtAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Flavor' + description: '' + delete: + operationId: flavors_destroy + parameters: + - in: path + name: id + schema: + type: integer + description: A unique integer value identifying this flavor. + required: true + tags: + - flavors + security: + - jwtAuth: [] + responses: + '204': + description: No response body + /api/flavors/followed_top/: + get: + operationId: flavors_followed_top_retrieve + tags: + - flavors + security: + - jwtAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Flavor' + description: '' + /api/flavors/newest/: + get: + operationId: flavors_newest_retrieve + tags: + - flavors + security: + - jwtAuth: [] + - {} + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Flavor' + description: '' + /api/flavors/search/: + get: + operationId: flavors_search_retrieve + tags: + - flavors + security: + - jwtAuth: [] + - {} + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Flavor' + description: '' + /api/flavors/top/: + get: + operationId: flavors_top_retrieve + tags: + - flavors + security: + - jwtAuth: [] + - {} + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Flavor' + description: '' + /api/notifications/: + get: + operationId: notifications_list + tags: + - notifications + security: + - jwtAuth: [] + responses: + '200': + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Notification' + description: '' + /api/notifications/{id}/: + get: + operationId: notifications_retrieve + parameters: + - in: path + name: id + schema: + type: string + required: true + tags: + - notifications + security: + - jwtAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Notification' + description: '' + /api/notifications/{id}/mark_read/: + post: + operationId: notifications_mark_read_create + parameters: + - in: path + name: id + schema: + type: string + required: true + tags: + - notifications + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Notification' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/Notification' + multipart/form-data: + schema: + $ref: '#/components/schemas/Notification' + required: true + security: + - jwtAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Notification' + description: '' + /api/notifications/mark_all_read/: + post: + operationId: notifications_mark_all_read_create + tags: + - notifications + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Notification' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/Notification' + multipart/form-data: + schema: + $ref: '#/components/schemas/Notification' + required: true + security: + - jwtAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Notification' + description: '' + /api/ratings/: + get: + operationId: ratings_list + parameters: + - name: page + required: false + in: query + description: A page number within the paginated result set. + schema: + type: integer + tags: + - ratings + security: + - jwtAuth: [] + - {} + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PaginatedRatingList' + description: '' + post: + operationId: ratings_create + tags: + - ratings + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Rating' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/Rating' + multipart/form-data: + schema: + $ref: '#/components/schemas/Rating' + required: true + security: + - jwtAuth: [] + responses: + '201': + content: + application/json: + schema: + $ref: '#/components/schemas/Rating' + description: '' + /api/ratings/{id}/: + get: + operationId: ratings_retrieve + parameters: + - in: path + name: id + schema: + type: integer + description: A unique integer value identifying this rating. + required: true + tags: + - ratings + security: + - jwtAuth: [] + - {} + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Rating' + description: '' + put: + operationId: ratings_update + parameters: + - in: path + name: id + schema: + type: integer + description: A unique integer value identifying this rating. + required: true + tags: + - ratings + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Rating' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/Rating' + multipart/form-data: + schema: + $ref: '#/components/schemas/Rating' + required: true + security: + - jwtAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Rating' + description: '' + patch: + operationId: ratings_partial_update + parameters: + - in: path + name: id + schema: + type: integer + description: A unique integer value identifying this rating. + required: true + tags: + - ratings + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedRating' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedRating' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedRating' + security: + - jwtAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Rating' + description: '' + delete: + operationId: ratings_destroy + parameters: + - in: path + name: id + schema: + type: integer + description: A unique integer value identifying this rating. + required: true + tags: + - ratings + security: + - jwtAuth: [] + responses: + '204': + description: No response body + /api/ratings/{id}/reply/: + post: + operationId: ratings_reply_create + parameters: + - in: path + name: id + schema: + type: integer + description: A unique integer value identifying this rating. + required: true + tags: + - ratings + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Rating' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/Rating' + multipart/form-data: + schema: + $ref: '#/components/schemas/Rating' + required: true + security: + - jwtAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Rating' + description: '' + /api/ratings/feed/: + get: + operationId: ratings_feed_retrieve + tags: + - ratings + security: + - jwtAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Rating' + description: '' + /api/ratings/recent/: + get: + operationId: ratings_recent_retrieve + tags: + - ratings + security: + - jwtAuth: [] + - {} + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Rating' + description: '' + /api/replies/: + get: + operationId: replies_list + parameters: + - name: page + required: false + in: query + description: A page number within the paginated result set. + schema: + type: integer + tags: + - replies + security: + - jwtAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PaginatedReplyList' + description: '' + post: + operationId: replies_create + tags: + - replies + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Reply' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/Reply' + multipart/form-data: + schema: + $ref: '#/components/schemas/Reply' + required: true + security: + - jwtAuth: [] + responses: + '201': + content: + application/json: + schema: + $ref: '#/components/schemas/Reply' + description: '' + /api/replies/{id}/: + get: + operationId: replies_retrieve + parameters: + - in: path + name: id + schema: + type: integer + description: A unique integer value identifying this reply. + required: true + tags: + - replies + security: + - jwtAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Reply' + description: '' + put: + operationId: replies_update + parameters: + - in: path + name: id + schema: + type: integer + description: A unique integer value identifying this reply. + required: true + tags: + - replies + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Reply' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/Reply' + multipart/form-data: + schema: + $ref: '#/components/schemas/Reply' + required: true + security: + - jwtAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Reply' + description: '' + patch: + operationId: replies_partial_update + parameters: + - in: path + name: id + schema: + type: integer + description: A unique integer value identifying this reply. + required: true + tags: + - replies + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedReply' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedReply' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedReply' + security: + - jwtAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Reply' + description: '' + delete: + operationId: replies_destroy + parameters: + - in: path + name: id + schema: + type: integer + description: A unique integer value identifying this reply. + required: true + tags: + - replies + security: + - jwtAuth: [] + responses: + '204': + description: No response body + /api/tickets/: + get: + operationId: tickets_list + parameters: + - name: page + required: false + in: query + description: A page number within the paginated result set. + schema: + type: integer + tags: + - tickets + security: + - jwtAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PaginatedTicketList' + description: '' + post: + operationId: tickets_create + tags: + - tickets + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Ticket' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/Ticket' + multipart/form-data: + schema: + $ref: '#/components/schemas/Ticket' + required: true + security: + - jwtAuth: [] + responses: + '201': + content: + application/json: + schema: + $ref: '#/components/schemas/Ticket' + description: '' + /api/tickets/{id}/: + get: + operationId: tickets_retrieve + parameters: + - in: path + name: id + schema: + type: string + required: true + tags: + - tickets + security: + - jwtAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Ticket' + description: '' + put: + operationId: tickets_update + parameters: + - in: path + name: id + schema: + type: string + required: true + tags: + - tickets + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Ticket' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/Ticket' + multipart/form-data: + schema: + $ref: '#/components/schemas/Ticket' + required: true + security: + - jwtAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Ticket' + description: '' + patch: + operationId: tickets_partial_update + parameters: + - in: path + name: id + schema: + type: string + required: true + tags: + - tickets + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedTicket' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedTicket' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedTicket' + security: + - jwtAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Ticket' + description: '' + delete: + operationId: tickets_destroy + parameters: + - in: path + name: id + schema: + type: string + required: true + tags: + - tickets + security: + - jwtAuth: [] + responses: + '204': + description: No response body + /api/tickets/{id}/add_message/: + post: + operationId: tickets_add_message_create + parameters: + - in: path + name: id + schema: + type: string + required: true + tags: + - tickets + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Ticket' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/Ticket' + multipart/form-data: + schema: + $ref: '#/components/schemas/Ticket' + required: true + security: + - jwtAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Ticket' + description: '' + /api/tickets/{id}/update_status/: + post: + operationId: tickets_update_status_create + parameters: + - in: path + name: id + schema: + type: string + required: true + tags: + - tickets + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Ticket' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/Ticket' + multipart/form-data: + schema: + $ref: '#/components/schemas/Ticket' + required: true + security: + - jwtAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Ticket' + description: '' + /api/token/: + post: + operationId: token_create + description: |- + Takes a set of user credentials and returns an access and refresh JSON web + token pair to prove the authentication of those credentials. + tags: + - token + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/TokenObtainPair' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/TokenObtainPair' + multipart/form-data: + schema: + $ref: '#/components/schemas/TokenObtainPair' + required: true + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/TokenObtainPair' + description: '' + /api/users/: + get: + operationId: users_list + parameters: + - name: page + required: false + in: query + description: A page number within the paginated result set. + schema: + type: integer + tags: + - users + security: + - jwtAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PaginatedUserList' + description: '' + post: + operationId: users_create + tags: + - users + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/User' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/User' + multipart/form-data: + schema: + $ref: '#/components/schemas/User' + required: true + security: + - jwtAuth: [] + responses: + '201': + content: + application/json: + schema: + $ref: '#/components/schemas/User' + description: '' + /api/users/{id}/: + get: + operationId: users_retrieve + parameters: + - in: path + name: id + schema: + type: integer + description: A unique integer value identifying this user. + required: true + tags: + - users + security: + - jwtAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/User' + description: '' + put: + operationId: users_update + parameters: + - in: path + name: id + schema: + type: integer + description: A unique integer value identifying this user. + required: true + tags: + - users + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/User' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/User' + multipart/form-data: + schema: + $ref: '#/components/schemas/User' + required: true + security: + - jwtAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/User' + description: '' + patch: + operationId: users_partial_update + parameters: + - in: path + name: id + schema: + type: integer + description: A unique integer value identifying this user. + required: true + tags: + - users + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedUser' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedUser' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedUser' + security: + - jwtAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/User' + description: '' + delete: + operationId: users_destroy + parameters: + - in: path + name: id + schema: + type: integer + description: A unique integer value identifying this user. + required: true + tags: + - users + security: + - jwtAuth: [] + responses: + '204': + description: No response body + /api/users/{id}/add_comment/: + post: + operationId: users_add_comment_create + parameters: + - in: path + name: id + schema: + type: integer + description: A unique integer value identifying this user. + required: true + tags: + - users + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/User' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/User' + multipart/form-data: + schema: + $ref: '#/components/schemas/User' + required: true + security: + - jwtAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/User' + description: '' + /api/users/{id}/delete_comment/{comment_id}/: + delete: + operationId: users_delete_comment_destroy + parameters: + - in: path + name: comment_id + schema: + type: string + required: true + - in: path + name: id + schema: + type: integer + description: A unique integer value identifying this user. + required: true + tags: + - users + security: + - jwtAuth: [] + responses: + '204': + description: No response body + /api/users/{id}/follow/: + post: + operationId: users_follow_create + parameters: + - in: path + name: id + schema: + type: integer + description: A unique integer value identifying this user. + required: true + tags: + - users + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/User' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/User' + multipart/form-data: + schema: + $ref: '#/components/schemas/User' + required: true + security: + - jwtAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/User' + description: '' + /api/users/{id}/unfollow/: + post: + operationId: users_unfollow_create + parameters: + - in: path + name: id + schema: + type: integer + description: A unique integer value identifying this user. + required: true + tags: + - users + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/User' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/User' + multipart/form-data: + schema: + $ref: '#/components/schemas/User' + required: true + security: + - jwtAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/User' + description: '' + /api/users/change_password/: + post: + operationId: users_change_password_create + tags: + - users + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/User' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/User' + multipart/form-data: + schema: + $ref: '#/components/schemas/User' + required: true + security: + - jwtAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/User' + description: '' + /api/users/complete_password_reset/: + post: + operationId: users_complete_password_reset_create + tags: + - users + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/User' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/User' + multipart/form-data: + schema: + $ref: '#/components/schemas/User' + required: true + security: + - jwtAuth: [] + - {} + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/User' + description: '' + /api/users/confirm_account_deletion/: + post: + operationId: users_confirm_account_deletion_create + tags: + - users + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/User' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/User' + multipart/form-data: + schema: + $ref: '#/components/schemas/User' + required: true + security: + - jwtAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/User' + description: '' + /api/users/confirm_email/: + post: + operationId: users_confirm_email_create + tags: + - users + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/User' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/User' + multipart/form-data: + schema: + $ref: '#/components/schemas/User' + required: true + security: + - jwtAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/User' + description: '' + /api/users/dashboard/: + get: + operationId: users_dashboard_retrieve + tags: + - users + security: + - jwtAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/User' + description: '' + /api/users/following_list/: + get: + operationId: users_following_list_retrieve + tags: + - users + security: + - jwtAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/User' + description: '' + /api/users/me/: + get: + operationId: users_me_retrieve + tags: + - users + security: + - jwtAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/User' + description: '' + /api/users/profile/{username}/: + get: + operationId: users_profile_retrieve + parameters: + - in: path + name: username + schema: + type: string + description: Required. 150 characters or fewer. Letters, digits and @/./+/-/_ + only. + required: true + tags: + - users + security: + - jwtAuth: [] + - {} + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/User' + description: '' + /api/users/request_account_deletion/: + post: + operationId: users_request_account_deletion_create + tags: + - users + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/User' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/User' + multipart/form-data: + schema: + $ref: '#/components/schemas/User' + required: true + security: + - jwtAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/User' + description: '' + /api/users/request_password_reset/: + post: + operationId: users_request_password_reset_create + tags: + - users + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/User' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/User' + multipart/form-data: + schema: + $ref: '#/components/schemas/User' + required: true + security: + - jwtAuth: [] + - {} + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/User' + description: '' + /api/users/resend_verification/: + post: + operationId: users_resend_verification_create + tags: + - users + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/User' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/User' + multipart/form-data: + schema: + $ref: '#/components/schemas/User' + required: true + security: + - jwtAuth: [] + - {} + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/User' + description: '' + /api/users/signup/: + post: + operationId: users_signup_create + tags: + - users + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/User' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/User' + multipart/form-data: + schema: + $ref: '#/components/schemas/User' + required: true + security: + - jwtAuth: [] + - {} + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/User' + description: '' + /api/users/update_avatar/: + post: + operationId: users_update_avatar_create + tags: + - users + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/User' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/User' + multipart/form-data: + schema: + $ref: '#/components/schemas/User' + required: true + security: + - jwtAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/User' + description: '' + /api/users/update_preferences/: + patch: + operationId: users_update_preferences_partial_update + tags: + - users + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedUser' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedUser' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedUser' + security: + - jwtAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/User' + description: '' + /api/users/update_profile/: + patch: + operationId: users_update_profile_partial_update + tags: + - users + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedUser' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedUser' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedUser' + security: + - jwtAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/User' + description: '' + /api/users/verify_signup/: + post: + operationId: users_verify_signup_create + tags: + - users + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/User' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/User' + multipart/form-data: + schema: + $ref: '#/components/schemas/User' + required: true + security: + - jwtAuth: [] + - {} + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/User' + description: '' +components: + schemas: + Banner: + type: object + properties: + id: + type: integer + readOnly: true + name: + type: string + maxLength: 100 + slug: + type: string + maxLength: 50 + pattern: ^[-a-zA-Z0-9_]+$ + description: + type: string + is_active: + type: boolean + description: Designates this as the global default banner. + is_enabled: + type: boolean + description: Allows users to select this banner. + settings: {} + schema: {} + created_at: + type: string + format: date-time + readOnly: true + updated_at: + type: string + format: date-time + readOnly: true + required: + - created_at + - id + - name + - slug + - updated_at + Category: + type: object + properties: + id: + type: integer + readOnly: true + name: + type: string + maxLength: 50 + slug: + type: string + maxLength: 50 + pattern: ^[-a-zA-Z0-9_]+$ + required: + - id + - name + - slug + Flavor: + type: object + properties: + id: + type: integer + readOnly: true + name: + type: string + maxLength: 100 + category: + type: integer + category_name: + type: string + readOnly: true + category_slug: + type: string + readOnly: true + description: + type: string + average_rating: + type: number + format: double + readOnly: true + followed_average_rating: + type: number + format: double + readOnly: true + user_rating: + type: integer + nullable: true + readOnly: true + ratings: + type: array + items: + $ref: '#/components/schemas/Rating' + readOnly: true + image_url: + type: string + nullable: true + readOnly: true + image_urls: + type: array + items: + type: string + readOnly: true + image: + type: string + format: uri + writeOnly: true + nullable: true + is_available: + type: boolean + is_legacy: + type: boolean + shop_url: + type: string + format: uri + nullable: true + maxLength: 500 + rating_distribution: + type: object + additionalProperties: + type: integer + readOnly: true + required: + - average_rating + - category + - category_name + - category_slug + - followed_average_rating + - id + - image_url + - image_urls + - name + - rating_distribution + - ratings + - user_rating + LanguageEnum: + enum: + - en + - de + type: string + description: |- + * `en` - English + * `de` - Deutsch + Notification: + type: object + properties: + id: + type: integer + readOnly: true + actor_username: + type: string + readOnly: true + actor_avatar: + type: string + nullable: true + readOnly: true + notification_type: + $ref: '#/components/schemas/NotificationTypeEnum' + rating: + type: integer + nullable: true + reply: + type: integer + nullable: true + ticket: + type: integer + nullable: true + profile_comment: + type: integer + nullable: true + profile_owner_username: + type: string + nullable: true + readOnly: true + is_read: + type: boolean + created_at: + type: string + format: date-time + readOnly: true + flavor_name: + type: string + nullable: true + readOnly: true + flavor_id: + type: integer + nullable: true + readOnly: true + required: + - actor_avatar + - actor_username + - created_at + - flavor_id + - flavor_name + - id + - notification_type + - profile_owner_username + NotificationTypeEnum: + enum: + - reply + - mention + - follow + - ticket_new + - ticket_reply + - profile_comment + type: string + description: |- + * `reply` - Reply + * `mention` - Mention + * `follow` - Follow + * `ticket_new` - New Ticket + * `ticket_reply` - Ticket Reply + * `profile_comment` - Profile Comment + PaginatedCategoryList: + type: object + required: + - count + - results + properties: + count: + type: integer + example: 123 + next: + type: string + nullable: true + format: uri + example: http://api.example.org/accounts/?page=4 + previous: + type: string + nullable: true + format: uri + example: http://api.example.org/accounts/?page=2 + results: + type: array + items: + $ref: '#/components/schemas/Category' + PaginatedFlavorList: + type: object + required: + - count + - results + properties: + count: + type: integer + example: 123 + next: + type: string + nullable: true + format: uri + example: http://api.example.org/accounts/?page=4 + previous: + type: string + nullable: true + format: uri + example: http://api.example.org/accounts/?page=2 + results: + type: array + items: + $ref: '#/components/schemas/Flavor' + PaginatedRatingList: + type: object + required: + - count + - results + properties: + count: + type: integer + example: 123 + next: + type: string + nullable: true + format: uri + example: http://api.example.org/accounts/?page=4 + previous: + type: string + nullable: true + format: uri + example: http://api.example.org/accounts/?page=2 + results: + type: array + items: + $ref: '#/components/schemas/Rating' + PaginatedReplyList: + type: object + required: + - count + - results + properties: + count: + type: integer + example: 123 + next: + type: string + nullable: true + format: uri + example: http://api.example.org/accounts/?page=4 + previous: + type: string + nullable: true + format: uri + example: http://api.example.org/accounts/?page=2 + results: + type: array + items: + $ref: '#/components/schemas/Reply' + PaginatedTicketList: + type: object + required: + - count + - results + properties: + count: + type: integer + example: 123 + next: + type: string + nullable: true + format: uri + example: http://api.example.org/accounts/?page=4 + previous: + type: string + nullable: true + format: uri + example: http://api.example.org/accounts/?page=2 + results: + type: array + items: + $ref: '#/components/schemas/Ticket' + PaginatedUserList: + type: object + required: + - count + - results + properties: + count: + type: integer + example: 123 + next: + type: string + nullable: true + format: uri + example: http://api.example.org/accounts/?page=4 + previous: + type: string + nullable: true + format: uri + example: http://api.example.org/accounts/?page=2 + results: + type: array + items: + $ref: '#/components/schemas/User' + PatchedBanner: + type: object + properties: + id: + type: integer + readOnly: true + name: + type: string + maxLength: 100 + slug: + type: string + maxLength: 50 + pattern: ^[-a-zA-Z0-9_]+$ + description: + type: string + is_active: + type: boolean + description: Designates this as the global default banner. + is_enabled: + type: boolean + description: Allows users to select this banner. + settings: {} + schema: {} + created_at: + type: string + format: date-time + readOnly: true + updated_at: + type: string + format: date-time + readOnly: true + PatchedFlavor: + type: object + properties: + id: + type: integer + readOnly: true + name: + type: string + maxLength: 100 + category: + type: integer + category_name: + type: string + readOnly: true + category_slug: + type: string + readOnly: true + description: + type: string + average_rating: + type: number + format: double + readOnly: true + followed_average_rating: + type: number + format: double + readOnly: true + user_rating: + type: integer + nullable: true + readOnly: true + ratings: + type: array + items: + $ref: '#/components/schemas/Rating' + readOnly: true + image_url: + type: string + nullable: true + readOnly: true + image_urls: + type: array + items: + type: string + readOnly: true + image: + type: string + format: uri + writeOnly: true + nullable: true + is_available: + type: boolean + is_legacy: + type: boolean + shop_url: + type: string + format: uri + nullable: true + maxLength: 500 + rating_distribution: + type: object + additionalProperties: + type: integer + readOnly: true + PatchedRating: + type: object + properties: + id: + type: integer + readOnly: true + user: + type: string + readOnly: true + user_id: + type: integer + readOnly: true + user_avatar: + type: string + nullable: true + readOnly: true + flavor: + type: integer + flavor_name: + type: string + readOnly: true + flavor_image: + type: string + nullable: true + readOnly: true + category_name: + type: string + readOnly: true + category_slug: + type: string + readOnly: true + is_available: + type: boolean + readOnly: true + is_legacy: + type: boolean + readOnly: true + score: + type: integer + maximum: 10 + minimum: 1 + comment: + type: string + nullable: true + created_at: + type: string + format: date-time + readOnly: true + replies: + type: array + items: + $ref: '#/components/schemas/Reply' + readOnly: true + PatchedReply: + type: object + properties: + id: + type: integer + readOnly: true + user: + type: string + readOnly: true + rating: + type: integer + readOnly: true + text: + type: string + created_at: + type: string + format: date-time + readOnly: true + PatchedTicket: + type: object + properties: + id: + type: integer + readOnly: true + user: + type: integer + readOnly: true + username: + type: string + readOnly: true + user_email: + type: string + format: email + readOnly: true + user_avatar: + type: string + nullable: true + readOnly: true + subject: + type: string + maxLength: 200 + description: + type: string + status: + allOf: + - $ref: '#/components/schemas/StatusEnum' + readOnly: true + created_at: + type: string + format: date-time + readOnly: true + updated_at: + type: string + format: date-time + readOnly: true + messages: + type: array + items: + $ref: '#/components/schemas/TicketMessage' + readOnly: true + PatchedUser: + type: object + properties: + id: + type: integer + readOnly: true + username: + type: string + description: Required. 150 characters or fewer. Letters, digits and @/./+/-/_ + only. + pattern: ^[\w.@+-]+$ + maxLength: 150 + email: + type: string + format: email + title: Email address + maxLength: 254 + theme: + $ref: '#/components/schemas/ThemeEnum' + language: + $ref: '#/components/schemas/LanguageEnum' + avatar: + type: string + nullable: true + readOnly: true + following_count: + type: integer + readOnly: true + followers_count: + type: integer + readOnly: true + is_following: + type: boolean + readOnly: true + unread_notifications_count: + type: integer + readOnly: true + is_superuser: + type: boolean + readOnly: true + selected_banner: + type: integer + nullable: true + selected_banner_slug: + type: string + readOnly: true + nullable: true + Rating: + type: object + properties: + id: + type: integer + readOnly: true + user: + type: string + readOnly: true + user_id: + type: integer + readOnly: true + user_avatar: + type: string + nullable: true + readOnly: true + flavor: + type: integer + flavor_name: + type: string + readOnly: true + flavor_image: + type: string + nullable: true + readOnly: true + category_name: + type: string + readOnly: true + category_slug: + type: string + readOnly: true + is_available: + type: boolean + readOnly: true + is_legacy: + type: boolean + readOnly: true + score: + type: integer + maximum: 10 + minimum: 1 + comment: + type: string + nullable: true + created_at: + type: string + format: date-time + readOnly: true + replies: + type: array + items: + $ref: '#/components/schemas/Reply' + readOnly: true + required: + - category_name + - category_slug + - created_at + - flavor + - flavor_image + - flavor_name + - id + - is_available + - is_legacy + - replies + - score + - user + - user_avatar + - user_id + Reply: + type: object + properties: + id: + type: integer + readOnly: true + user: + type: string + readOnly: true + rating: + type: integer + readOnly: true + text: + type: string + created_at: + type: string + format: date-time + readOnly: true + required: + - created_at + - id + - rating + - text + - user + StatusEnum: + enum: + - open + - in_progress + - resolved + - closed + type: string + description: |- + * `open` - Open + * `in_progress` - In Progress + * `resolved` - Resolved + * `closed` - Closed + ThemeEnum: + enum: + - holy_light + - holy_dark + - latte + - frappe + - macchiato + - mocha + - pink_pastel + - mint_pastel + - lavender_pastel + - dracula + - nord + - gruvbox + - oceanic + - t0p_sai + - t0p_trench + - t0p_blurryface + - t0p_clancy + type: string + description: |- + * `holy_light` - Holy Light + * `holy_dark` - Holy Dark + * `latte` - Catppuccin Latte + * `frappe` - Catppuccin Frappé + * `macchiato` - Catppuccin Macchiato + * `mocha` - Catppuccin Mocha + * `pink_pastel` - Pink Pastel + * `mint_pastel` - Mint Pastel + * `lavender_pastel` - Lavender Pastel + * `dracula` - Dracula + * `nord` - Nordic Frost + * `gruvbox` - Gruvbox Retro + * `oceanic` - Oceanic Deep + * `t0p_sai` - SAI (T0P) + * `t0p_trench` - Trench (T0P) + * `t0p_blurryface` - Blurryface (T0P) + * `t0p_clancy` - Clancy (T0P) + Ticket: + type: object + properties: + id: + type: integer + readOnly: true + user: + type: integer + readOnly: true + username: + type: string + readOnly: true + user_email: + type: string + format: email + readOnly: true + user_avatar: + type: string + nullable: true + readOnly: true + subject: + type: string + maxLength: 200 + description: + type: string + status: + allOf: + - $ref: '#/components/schemas/StatusEnum' + readOnly: true + created_at: + type: string + format: date-time + readOnly: true + updated_at: + type: string + format: date-time + readOnly: true + messages: + type: array + items: + $ref: '#/components/schemas/TicketMessage' + readOnly: true + required: + - created_at + - description + - id + - messages + - status + - subject + - updated_at + - user + - user_avatar + - user_email + - username + TicketMessage: + type: object + properties: + id: + type: integer + readOnly: true + username: + type: string + readOnly: true + text: + type: string + created_at: + type: string + format: date-time + readOnly: true + is_admin: + type: boolean + readOnly: true + required: + - created_at + - id + - is_admin + - text + - username + TokenObtainPair: + type: object + properties: + username: + type: string + writeOnly: true + password: + type: string + writeOnly: true + access: + type: string + readOnly: true + refresh: + type: string + readOnly: true + required: + - access + - password + - refresh + - username + TokenRefresh: + type: object + properties: + access: + type: string + readOnly: true + refresh: + type: string + required: + - access + - refresh + TokenVerify: + type: object + properties: + token: + type: string + writeOnly: true + required: + - token + User: + type: object + properties: + id: + type: integer + readOnly: true + username: + type: string + description: Required. 150 characters or fewer. Letters, digits and @/./+/-/_ + only. + pattern: ^[\w.@+-]+$ + maxLength: 150 + email: + type: string + format: email + title: Email address + maxLength: 254 + theme: + $ref: '#/components/schemas/ThemeEnum' + language: + $ref: '#/components/schemas/LanguageEnum' + avatar: + type: string + nullable: true + readOnly: true + following_count: + type: integer + readOnly: true + followers_count: + type: integer + readOnly: true + is_following: + type: boolean + readOnly: true + unread_notifications_count: + type: integer + readOnly: true + is_superuser: + type: boolean + readOnly: true + selected_banner: + type: integer + nullable: true + selected_banner_slug: + type: string + readOnly: true + nullable: true + required: + - avatar + - followers_count + - following_count + - id + - is_following + - is_superuser + - selected_banner_slug + - unread_notifications_count + - username + securitySchemes: + jwtAuth: + type: http + scheme: bearer + bearerFormat: JWT diff --git a/docker-compose.yml b/docker-compose.yml index 3d102b8..68c0f51 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,6 +5,9 @@ services: container_name: holy-redis command: redis-server --appendonly yes --save 60 1 restart: unless-stopped + sysctls: + - net.core.somaxconn=512 + - vm.overcommit_memory=1 networks: - holy-net volumes: @@ -51,6 +54,7 @@ services: context: ./backend container_name: holy-worker command: celery -A holy_backend worker -l info --concurrency=2 + user: "app" restart: unless-stopped networks: - holy-net @@ -83,6 +87,7 @@ services: context: ./backend container_name: holy-beat command: celery -A holy_backend beat -l info --scheduler django_celery_beat.schedulers:DatabaseScheduler --pidfile /tmp/celerybeat.pid + user: "app" restart: unless-stopped networks: - holy-net diff --git a/frontend/package-lock.json b/frontend/package-lock.json index e8e7f6e..6598f71 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -42,6 +42,7 @@ "eslint-plugin-react-refresh": "^0.4.24", "globals": "^16.5.0", "jsdom": "^29.0.2", + "openapi-typescript": "^7.13.0", "prettier": "^3.8.2", "typescript": "~5.9.3", "typescript-eslint": "^8.48.0", @@ -2039,6 +2040,82 @@ "url": "https://opencollective.com/popperjs" } }, + "node_modules/@redocly/ajv": { + "version": "8.11.2", + "resolved": "https://registry.npmjs.org/@redocly/ajv/-/ajv-8.11.2.tgz", + "integrity": "sha512-io1JpnwtIcvojV7QKDUSIuMN/ikdOUd1ReEnUnMKGfDVridQZ31J0MmIuqwuRjWDZfmvr+Q0MqCcfHM2gTivOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js-replace": "^1.0.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@redocly/ajv/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/@redocly/config": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/@redocly/config/-/config-0.22.0.tgz", + "integrity": "sha512-gAy93Ddo01Z3bHuVdPWfCwzgfaYgMdaZPcfL7JZ7hWJoK9V0lXDbigTWkhiPFAaLWzbOJ+kbUQG1+XwIm0KRGQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@redocly/openapi-core": { + "version": "1.34.12", + "resolved": "https://registry.npmjs.org/@redocly/openapi-core/-/openapi-core-1.34.12.tgz", + "integrity": "sha512-b32XWsz6enN6K4bx8xWsqUaXTJR/DnYT3lL1CzDYzIYKw243NNlz6fexmr71q/U4HrEcMoJGBvwAfcxOb8ymQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@redocly/ajv": "8.11.2", + "@redocly/config": "0.22.0", + "colorette": "1.4.0", + "https-proxy-agent": "7.0.6", + "js-levenshtein": "1.1.6", + "js-yaml": "4.1.1", + "minimatch": "5.1.9", + "pluralize": "8.0.0", + "yaml-ast-parser": "0.0.43" + }, + "engines": { + "node": ">=18.17.0", + "npm": ">=9.5.0" + } + }, + "node_modules/@redocly/openapi-core/node_modules/brace-expansion": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@redocly/openapi-core/node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-rc.3", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz", @@ -3163,6 +3240,16 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -3180,6 +3267,16 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -3452,6 +3549,13 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/change-case": { + "version": "5.4.4", + "resolved": "https://registry.npmjs.org/change-case/-/change-case-5.4.4.tgz", + "integrity": "sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==", + "dev": true, + "license": "MIT" + }, "node_modules/clsx": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", @@ -3502,6 +3606,13 @@ "simple-swizzle": "^0.2.2" } }, + "node_modules/colorette": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz", + "integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==", + "dev": true, + "license": "MIT" + }, "node_modules/colorthief": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/colorthief/-/colorthief-2.6.0.tgz", @@ -4513,6 +4624,20 @@ "void-elements": "3.1.0" } }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/i18next": { "version": "24.2.3", "resolved": "https://registry.npmjs.org/i18next/-/i18next-24.2.3.tgz", @@ -4619,6 +4744,19 @@ "node": ">=8" } }, + "node_modules/index-to-position": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/index-to-position/-/index-to-position-1.2.0.tgz", + "integrity": "sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/iota-array": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/iota-array/-/iota-array-1.0.0.tgz", @@ -4689,6 +4827,16 @@ "dev": true, "license": "ISC" }, + "node_modules/js-levenshtein": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.6.tgz", + "integrity": "sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -5067,6 +5215,58 @@ ], "license": "MIT" }, + "node_modules/openapi-typescript": { + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/openapi-typescript/-/openapi-typescript-7.13.0.tgz", + "integrity": "sha512-EFP392gcqXS7ntPvbhBzbF8TyBA+baIYEm791Hy5YkjDYKTnk/Tn5OQeKm5BIZvJihpp8Zzr4hzx0Irde1LNGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@redocly/openapi-core": "^1.34.6", + "ansi-colors": "^4.1.3", + "change-case": "^5.4.4", + "parse-json": "^8.3.0", + "supports-color": "^10.2.2", + "yargs-parser": "^21.1.1" + }, + "bin": { + "openapi-typescript": "bin/cli.js" + }, + "peerDependencies": { + "typescript": "^5.x" + } + }, + "node_modules/openapi-typescript/node_modules/parse-json": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-8.3.0.tgz", + "integrity": "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.26.2", + "index-to-position": "^1.1.0", + "type-fest": "^4.39.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/openapi-typescript/node_modules/supports-color": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz", + "integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -5234,6 +5434,16 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pluralize": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/postcss": { "version": "8.5.6", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", @@ -6055,6 +6265,19 @@ "node": ">= 0.8.0" } }, + "node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -6157,6 +6380,13 @@ "punycode": "^2.1.0" } }, + "node_modules/uri-js-replace": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/uri-js-replace/-/uri-js-replace-1.0.1.tgz", + "integrity": "sha512-W+C9NWNLFOoBI2QWDp4UT9pv65r2w5Cx+3sTYFvtMdDBxkKt1syCqsUdSFAChbEe1uK5TfS04wt/nGwmaeIQ0g==", + "dev": true, + "license": "MIT" + }, "node_modules/vite": { "version": "7.3.1", "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", @@ -6446,6 +6676,23 @@ "dev": true, "license": "ISC" }, + "node_modules/yaml-ast-parser": { + "version": "0.0.43", + "resolved": "https://registry.npmjs.org/yaml-ast-parser/-/yaml-ast-parser-0.0.43.tgz", + "integrity": "sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 4c778a1..f37a3a0 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -12,7 +12,10 @@ "format:check": "prettier --check \"src/**/*.{ts,tsx,js,json,css}\"", "test": "vitest run", "test:watch": "vitest", - "preview": "vite preview" + "preview": "vite preview", + "schema:export": "cd ../backend && ./venv/bin/python manage.py spectacular --file schema.yaml", + "openapi:gen": "openapi-typescript ../backend/schema.yaml -o src/api/schema.d.ts", + "openapi:sync": "npm run schema:export && npm run openapi:gen" }, "dependencies": { "@catppuccin/palette": "^1.7.1", @@ -49,6 +52,7 @@ "eslint-plugin-react-refresh": "^0.4.24", "globals": "^16.5.0", "jsdom": "^29.0.2", + "openapi-typescript": "^7.13.0", "prettier": "^3.8.2", "typescript": "~5.9.3", "typescript-eslint": "^8.48.0", diff --git a/frontend/src/api/queries/useCurrentUserLite.ts b/frontend/src/api/queries/useCurrentUserLite.ts index d94412e..e56f4b8 100644 --- a/frontend/src/api/queries/useCurrentUserLite.ts +++ b/frontend/src/api/queries/useCurrentUserLite.ts @@ -1,5 +1,6 @@ import { useQuery } from '@tanstack/react-query'; import api from '../../lib/api'; +import { useAuth } from '../../hooks/useAuth'; export interface CurrentUserLite { username: string; @@ -10,8 +11,9 @@ export interface CurrentUserLite { * Reads /users/me/ for cases where we need a quick "is this me / am I admin" * check outside of AuthContext. Disabled when not authenticated. */ -export const useCurrentUserLite = () => - useQuery({ +export const useCurrentUserLite = () => { + const { user } = useAuth(); + return useQuery({ queryKey: ['currentUserLite'], queryFn: async () => { const res = await api.get('users/me/', { @@ -21,6 +23,7 @@ export const useCurrentUserLite = () => }); return res.data; }, - enabled: !!localStorage.getItem('access'), + enabled: !!user, staleTime: 60_000, }); +}; diff --git a/frontend/src/api/queries/useMe.ts b/frontend/src/api/queries/useMe.ts index 9f4acf2..edfa96f 100644 --- a/frontend/src/api/queries/useMe.ts +++ b/frontend/src/api/queries/useMe.ts @@ -1,5 +1,6 @@ import { useQuery } from '@tanstack/react-query'; import api from '../../lib/api'; +import { useAuth } from '../../hooks/useAuth'; export interface CurrentUserFull { id: number; @@ -12,8 +13,9 @@ export interface CurrentUserFull { unread_notifications_count?: number; } -export const useMe = () => - useQuery({ +export const useMe = () => { + const { user } = useAuth(); + return useQuery({ queryKey: ['me'], queryFn: async () => { const res = await api.get('users/me/', { @@ -21,5 +23,6 @@ export const useMe = () => }); return res.data; }, - enabled: !!localStorage.getItem('access'), + enabled: !!user, }); +}; diff --git a/frontend/src/api/schema.d.ts b/frontend/src/api/schema.d.ts new file mode 100644 index 0000000..af135d9 --- /dev/null +++ b/frontend/src/api/schema.d.ts @@ -0,0 +1,3506 @@ +/** + * This file was auto-generated by openapi-typescript. + * Do not make direct changes to the file. + */ + +export interface paths { + '/api/admin-custom/{id}/trigger_job/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations['admin_custom_trigger_job_create']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/admin-custom/{id}/update_job_schedule/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch: operations['admin_custom_update_job_schedule_partial_update']; + trace?: never; + }; + '/api/admin-custom/{id}/user_detail/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['admin_custom_user_detail_retrieve']; + put?: never; + post?: never; + delete: operations['admin_custom_user_detail_destroy']; + options?: never; + head?: never; + patch: operations['admin_custom_user_detail_partial_update']; + trace?: never; + }; + '/api/admin-custom/config/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['admin_custom_config_retrieve']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch: operations['admin_custom_config_partial_update']; + trace?: never; + }; + '/api/admin-custom/jobs/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['admin_custom_jobs_retrieve']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/admin-custom/send_test_email/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations['admin_custom_send_test_email_create']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/admin-custom/stats/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['admin_custom_stats_retrieve']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/admin-custom/users/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['admin_custom_users_retrieve']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/auth/token/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * @description Takes a set of user credentials and returns an access and refresh JSON web + * token pair to prove the authentication of those credentials. + */ + post: operations['auth_token_create']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/auth/token/refresh/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * @description Takes a refresh type JSON web token and returns an access type JSON web + * token if the refresh token is valid. + */ + post: operations['auth_token_refresh_create']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/auth/token/verify/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * @description Takes a token and indicates if it is valid. This view provides no + * information about a token's fitness for a particular use. + */ + post: operations['auth_token_verify_create']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/banners/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['banners_list']; + put?: never; + post: operations['banners_create']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/banners/{id}/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['banners_retrieve']; + put: operations['banners_update']; + post?: never; + delete: operations['banners_destroy']; + options?: never; + head?: never; + patch: operations['banners_partial_update']; + trace?: never; + }; + '/api/banners/{id}/activate/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations['banners_activate_create']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/banners/{id}/toggle_enabled/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations['banners_toggle_enabled_create']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/banners/active/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['banners_active_retrieve']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/categories/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['categories_list']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/categories/{id}/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['categories_retrieve']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/flavors/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['flavors_list']; + put?: never; + post: operations['flavors_create']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/flavors/{id}/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['flavors_retrieve']; + put: operations['flavors_update']; + post?: never; + delete: operations['flavors_destroy']; + options?: never; + head?: never; + patch: operations['flavors_partial_update']; + trace?: never; + }; + '/api/flavors/followed_top/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['flavors_followed_top_retrieve']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/flavors/newest/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['flavors_newest_retrieve']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/flavors/search/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['flavors_search_retrieve']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/flavors/top/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['flavors_top_retrieve']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/notifications/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['notifications_list']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/notifications/{id}/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['notifications_retrieve']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/notifications/{id}/mark_read/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations['notifications_mark_read_create']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/notifications/mark_all_read/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations['notifications_mark_all_read_create']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/ratings/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['ratings_list']; + put?: never; + post: operations['ratings_create']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/ratings/{id}/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['ratings_retrieve']; + put: operations['ratings_update']; + post?: never; + delete: operations['ratings_destroy']; + options?: never; + head?: never; + patch: operations['ratings_partial_update']; + trace?: never; + }; + '/api/ratings/{id}/reply/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations['ratings_reply_create']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/ratings/feed/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['ratings_feed_retrieve']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/ratings/recent/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['ratings_recent_retrieve']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/replies/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['replies_list']; + put?: never; + post: operations['replies_create']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/replies/{id}/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['replies_retrieve']; + put: operations['replies_update']; + post?: never; + delete: operations['replies_destroy']; + options?: never; + head?: never; + patch: operations['replies_partial_update']; + trace?: never; + }; + '/api/tickets/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['tickets_list']; + put?: never; + post: operations['tickets_create']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/tickets/{id}/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['tickets_retrieve']; + put: operations['tickets_update']; + post?: never; + delete: operations['tickets_destroy']; + options?: never; + head?: never; + patch: operations['tickets_partial_update']; + trace?: never; + }; + '/api/tickets/{id}/add_message/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations['tickets_add_message_create']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/tickets/{id}/update_status/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations['tickets_update_status_create']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/token/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * @description Takes a set of user credentials and returns an access and refresh JSON web + * token pair to prove the authentication of those credentials. + */ + post: operations['token_create']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/users/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['users_list']; + put?: never; + post: operations['users_create']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/users/{id}/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['users_retrieve']; + put: operations['users_update']; + post?: never; + delete: operations['users_destroy']; + options?: never; + head?: never; + patch: operations['users_partial_update']; + trace?: never; + }; + '/api/users/{id}/add_comment/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations['users_add_comment_create']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/users/{id}/delete_comment/{comment_id}/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + delete: operations['users_delete_comment_destroy']; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/users/{id}/follow/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations['users_follow_create']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/users/{id}/unfollow/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations['users_unfollow_create']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/users/change_password/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations['users_change_password_create']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/users/complete_password_reset/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations['users_complete_password_reset_create']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/users/confirm_account_deletion/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations['users_confirm_account_deletion_create']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/users/confirm_email/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations['users_confirm_email_create']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/users/dashboard/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['users_dashboard_retrieve']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/users/following_list/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['users_following_list_retrieve']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/users/me/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['users_me_retrieve']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/users/profile/{username}/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['users_profile_retrieve']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/users/request_account_deletion/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations['users_request_account_deletion_create']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/users/request_password_reset/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations['users_request_password_reset_create']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/users/resend_verification/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations['users_resend_verification_create']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/users/signup/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations['users_signup_create']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/users/update_avatar/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations['users_update_avatar_create']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/users/update_preferences/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch: operations['users_update_preferences_partial_update']; + trace?: never; + }; + '/api/users/update_profile/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch: operations['users_update_profile_partial_update']; + trace?: never; + }; + '/api/users/verify_signup/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations['users_verify_signup_create']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; +} +export type webhooks = Record; +export interface components { + schemas: { + Banner: { + readonly id: number; + name: string; + slug: string; + description?: string; + /** @description Designates this as the global default banner. */ + is_active?: boolean; + /** @description Allows users to select this banner. */ + is_enabled?: boolean; + settings?: unknown; + schema?: unknown; + /** Format: date-time */ + readonly created_at: string; + /** Format: date-time */ + readonly updated_at: string; + }; + Category: { + readonly id: number; + name: string; + slug: string; + }; + Flavor: { + readonly id: number; + name: string; + category: number; + readonly category_name: string; + readonly category_slug: string; + description?: string; + /** Format: double */ + readonly average_rating: number; + /** Format: double */ + readonly followed_average_rating: number; + readonly user_rating: number | null; + readonly ratings: components['schemas']['Rating'][]; + readonly image_url: string | null; + readonly image_urls: string[]; + /** Format: uri */ + image?: string | null; + is_available?: boolean; + is_legacy?: boolean; + /** Format: uri */ + shop_url?: string | null; + readonly rating_distribution: { + [key: string]: number; + }; + }; + /** + * @description * `en` - English + * * `de` - Deutsch + * @enum {string} + */ + LanguageEnum: 'en' | 'de'; + Notification: { + readonly id: number; + readonly actor_username: string; + readonly actor_avatar: string | null; + notification_type: components['schemas']['NotificationTypeEnum']; + rating?: number | null; + reply?: number | null; + ticket?: number | null; + profile_comment?: number | null; + readonly profile_owner_username: string | null; + is_read?: boolean; + /** Format: date-time */ + readonly created_at: string; + readonly flavor_name: string | null; + readonly flavor_id: number | null; + }; + /** + * @description * `reply` - Reply + * * `mention` - Mention + * * `follow` - Follow + * * `ticket_new` - New Ticket + * * `ticket_reply` - Ticket Reply + * * `profile_comment` - Profile Comment + * @enum {string} + */ + NotificationTypeEnum: + | 'reply' + | 'mention' + | 'follow' + | 'ticket_new' + | 'ticket_reply' + | 'profile_comment'; + PaginatedCategoryList: { + /** @example 123 */ + count: number; + /** + * Format: uri + * @example http://api.example.org/accounts/?page=4 + */ + next?: string | null; + /** + * Format: uri + * @example http://api.example.org/accounts/?page=2 + */ + previous?: string | null; + results: components['schemas']['Category'][]; + }; + PaginatedFlavorList: { + /** @example 123 */ + count: number; + /** + * Format: uri + * @example http://api.example.org/accounts/?page=4 + */ + next?: string | null; + /** + * Format: uri + * @example http://api.example.org/accounts/?page=2 + */ + previous?: string | null; + results: components['schemas']['Flavor'][]; + }; + PaginatedRatingList: { + /** @example 123 */ + count: number; + /** + * Format: uri + * @example http://api.example.org/accounts/?page=4 + */ + next?: string | null; + /** + * Format: uri + * @example http://api.example.org/accounts/?page=2 + */ + previous?: string | null; + results: components['schemas']['Rating'][]; + }; + PaginatedReplyList: { + /** @example 123 */ + count: number; + /** + * Format: uri + * @example http://api.example.org/accounts/?page=4 + */ + next?: string | null; + /** + * Format: uri + * @example http://api.example.org/accounts/?page=2 + */ + previous?: string | null; + results: components['schemas']['Reply'][]; + }; + PaginatedTicketList: { + /** @example 123 */ + count: number; + /** + * Format: uri + * @example http://api.example.org/accounts/?page=4 + */ + next?: string | null; + /** + * Format: uri + * @example http://api.example.org/accounts/?page=2 + */ + previous?: string | null; + results: components['schemas']['Ticket'][]; + }; + PaginatedUserList: { + /** @example 123 */ + count: number; + /** + * Format: uri + * @example http://api.example.org/accounts/?page=4 + */ + next?: string | null; + /** + * Format: uri + * @example http://api.example.org/accounts/?page=2 + */ + previous?: string | null; + results: components['schemas']['User'][]; + }; + PatchedBanner: { + readonly id?: number; + name?: string; + slug?: string; + description?: string; + /** @description Designates this as the global default banner. */ + is_active?: boolean; + /** @description Allows users to select this banner. */ + is_enabled?: boolean; + settings?: unknown; + schema?: unknown; + /** Format: date-time */ + readonly created_at?: string; + /** Format: date-time */ + readonly updated_at?: string; + }; + PatchedFlavor: { + readonly id?: number; + name?: string; + category?: number; + readonly category_name?: string; + readonly category_slug?: string; + description?: string; + /** Format: double */ + readonly average_rating?: number; + /** Format: double */ + readonly followed_average_rating?: number; + readonly user_rating?: number | null; + readonly ratings?: components['schemas']['Rating'][]; + readonly image_url?: string | null; + readonly image_urls?: string[]; + /** Format: uri */ + image?: string | null; + is_available?: boolean; + is_legacy?: boolean; + /** Format: uri */ + shop_url?: string | null; + readonly rating_distribution?: { + [key: string]: number; + }; + }; + PatchedRating: { + readonly id?: number; + readonly user?: string; + readonly user_id?: number; + readonly user_avatar?: string | null; + flavor?: number; + readonly flavor_name?: string; + readonly flavor_image?: string | null; + readonly category_name?: string; + readonly category_slug?: string; + readonly is_available?: boolean; + readonly is_legacy?: boolean; + score?: number; + comment?: string | null; + /** Format: date-time */ + readonly created_at?: string; + readonly replies?: components['schemas']['Reply'][]; + }; + PatchedReply: { + readonly id?: number; + readonly user?: string; + readonly rating?: number; + text?: string; + /** Format: date-time */ + readonly created_at?: string; + }; + PatchedTicket: { + readonly id?: number; + readonly user?: number; + readonly username?: string; + /** Format: email */ + readonly user_email?: string; + readonly user_avatar?: string | null; + subject?: string; + description?: string; + readonly status?: components['schemas']['StatusEnum']; + /** Format: date-time */ + readonly created_at?: string; + /** Format: date-time */ + readonly updated_at?: string; + readonly messages?: components['schemas']['TicketMessage'][]; + }; + PatchedUser: { + readonly id?: number; + /** @description Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only. */ + username?: string; + /** + * Email address + * Format: email + */ + email?: string; + theme?: components['schemas']['ThemeEnum']; + language?: components['schemas']['LanguageEnum']; + readonly avatar?: string | null; + readonly following_count?: number; + readonly followers_count?: number; + readonly is_following?: boolean; + readonly unread_notifications_count?: number; + readonly is_superuser?: boolean; + selected_banner?: number | null; + readonly selected_banner_slug?: string | null; + }; + Rating: { + readonly id: number; + readonly user: string; + readonly user_id: number; + readonly user_avatar: string | null; + flavor: number; + readonly flavor_name: string; + readonly flavor_image: string | null; + readonly category_name: string; + readonly category_slug: string; + readonly is_available: boolean; + readonly is_legacy: boolean; + score: number; + comment?: string | null; + /** Format: date-time */ + readonly created_at: string; + readonly replies: components['schemas']['Reply'][]; + }; + Reply: { + readonly id: number; + readonly user: string; + readonly rating: number; + text: string; + /** Format: date-time */ + readonly created_at: string; + }; + /** + * @description * `open` - Open + * * `in_progress` - In Progress + * * `resolved` - Resolved + * * `closed` - Closed + * @enum {string} + */ + StatusEnum: 'open' | 'in_progress' | 'resolved' | 'closed'; + /** + * @description * `holy_light` - Holy Light + * * `holy_dark` - Holy Dark + * * `latte` - Catppuccin Latte + * * `frappe` - Catppuccin Frappé + * * `macchiato` - Catppuccin Macchiato + * * `mocha` - Catppuccin Mocha + * * `pink_pastel` - Pink Pastel + * * `mint_pastel` - Mint Pastel + * * `lavender_pastel` - Lavender Pastel + * * `dracula` - Dracula + * * `nord` - Nordic Frost + * * `gruvbox` - Gruvbox Retro + * * `oceanic` - Oceanic Deep + * * `t0p_sai` - SAI (T0P) + * * `t0p_trench` - Trench (T0P) + * * `t0p_blurryface` - Blurryface (T0P) + * * `t0p_clancy` - Clancy (T0P) + * @enum {string} + */ + ThemeEnum: + | 'holy_light' + | 'holy_dark' + | 'latte' + | 'frappe' + | 'macchiato' + | 'mocha' + | 'pink_pastel' + | 'mint_pastel' + | 'lavender_pastel' + | 'dracula' + | 'nord' + | 'gruvbox' + | 'oceanic' + | 't0p_sai' + | 't0p_trench' + | 't0p_blurryface' + | 't0p_clancy'; + Ticket: { + readonly id: number; + readonly user: number; + readonly username: string; + /** Format: email */ + readonly user_email: string; + readonly user_avatar: string | null; + subject: string; + description: string; + readonly status: components['schemas']['StatusEnum']; + /** Format: date-time */ + readonly created_at: string; + /** Format: date-time */ + readonly updated_at: string; + readonly messages: components['schemas']['TicketMessage'][]; + }; + TicketMessage: { + readonly id: number; + readonly username: string; + text: string; + /** Format: date-time */ + readonly created_at: string; + readonly is_admin: boolean; + }; + TokenObtainPair: { + username: string; + password: string; + readonly access: string; + readonly refresh: string; + }; + TokenRefresh: { + readonly access: string; + refresh: string; + }; + TokenVerify: { + token: string; + }; + User: { + readonly id: number; + /** @description Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only. */ + username: string; + /** + * Email address + * Format: email + */ + email?: string; + theme?: components['schemas']['ThemeEnum']; + language?: components['schemas']['LanguageEnum']; + readonly avatar: string | null; + readonly following_count: number; + readonly followers_count: number; + readonly is_following: boolean; + readonly unread_notifications_count: number; + readonly is_superuser: boolean; + selected_banner?: number | null; + readonly selected_banner_slug: string | null; + }; + }; + responses: never; + parameters: never; + requestBodies: never; + headers: never; + pathItems: never; +} +export type $defs = Record; +export interface operations { + admin_custom_trigger_job_create: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No response body */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + admin_custom_update_job_schedule_partial_update: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No response body */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + admin_custom_user_detail_retrieve: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No response body */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + admin_custom_user_detail_destroy: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No response body */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + admin_custom_user_detail_partial_update: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No response body */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + admin_custom_config_retrieve: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No response body */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + admin_custom_config_partial_update: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No response body */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + admin_custom_jobs_retrieve: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No response body */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + admin_custom_send_test_email_create: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No response body */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + admin_custom_stats_retrieve: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No response body */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + admin_custom_users_retrieve: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No response body */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + auth_token_create: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['TokenObtainPair']; + 'application/x-www-form-urlencoded': components['schemas']['TokenObtainPair']; + 'multipart/form-data': components['schemas']['TokenObtainPair']; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['TokenObtainPair']; + }; + }; + }; + }; + auth_token_refresh_create: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['TokenRefresh']; + 'application/x-www-form-urlencoded': components['schemas']['TokenRefresh']; + 'multipart/form-data': components['schemas']['TokenRefresh']; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['TokenRefresh']; + }; + }; + }; + }; + auth_token_verify_create: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['TokenVerify']; + 'application/x-www-form-urlencoded': components['schemas']['TokenVerify']; + 'multipart/form-data': components['schemas']['TokenVerify']; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['TokenVerify']; + }; + }; + }; + }; + banners_list: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Banner'][]; + }; + }; + }; + }; + banners_create: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['Banner']; + 'application/x-www-form-urlencoded': components['schemas']['Banner']; + 'multipart/form-data': components['schemas']['Banner']; + }; + }; + responses: { + 201: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Banner']; + }; + }; + }; + }; + banners_retrieve: { + parameters: { + query?: never; + header?: never; + path: { + /** @description A unique integer value identifying this banner. */ + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Banner']; + }; + }; + }; + }; + banners_update: { + parameters: { + query?: never; + header?: never; + path: { + /** @description A unique integer value identifying this banner. */ + id: number; + }; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['Banner']; + 'application/x-www-form-urlencoded': components['schemas']['Banner']; + 'multipart/form-data': components['schemas']['Banner']; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Banner']; + }; + }; + }; + }; + banners_destroy: { + parameters: { + query?: never; + header?: never; + path: { + /** @description A unique integer value identifying this banner. */ + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No response body */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + banners_partial_update: { + parameters: { + query?: never; + header?: never; + path: { + /** @description A unique integer value identifying this banner. */ + id: number; + }; + cookie?: never; + }; + requestBody?: { + content: { + 'application/json': components['schemas']['PatchedBanner']; + 'application/x-www-form-urlencoded': components['schemas']['PatchedBanner']; + 'multipart/form-data': components['schemas']['PatchedBanner']; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Banner']; + }; + }; + }; + }; + banners_activate_create: { + parameters: { + query?: never; + header?: never; + path: { + /** @description A unique integer value identifying this banner. */ + id: number; + }; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['Banner']; + 'application/x-www-form-urlencoded': components['schemas']['Banner']; + 'multipart/form-data': components['schemas']['Banner']; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Banner']; + }; + }; + }; + }; + banners_toggle_enabled_create: { + parameters: { + query?: never; + header?: never; + path: { + /** @description A unique integer value identifying this banner. */ + id: number; + }; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['Banner']; + 'application/x-www-form-urlencoded': components['schemas']['Banner']; + 'multipart/form-data': components['schemas']['Banner']; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Banner']; + }; + }; + }; + }; + banners_active_retrieve: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Banner']; + }; + }; + }; + }; + categories_list: { + parameters: { + query?: { + /** @description A page number within the paginated result set. */ + page?: number; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['PaginatedCategoryList']; + }; + }; + }; + }; + categories_retrieve: { + parameters: { + query?: never; + header?: never; + path: { + /** @description A unique integer value identifying this category. */ + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Category']; + }; + }; + }; + }; + flavors_list: { + parameters: { + query?: { + category?: number; + category__slug?: string; + /** @description A page number within the paginated result set. */ + page?: number; + /** @description A search term. */ + search?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['PaginatedFlavorList']; + }; + }; + }; + }; + flavors_create: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['Flavor']; + 'application/x-www-form-urlencoded': components['schemas']['Flavor']; + 'multipart/form-data': components['schemas']['Flavor']; + }; + }; + responses: { + 201: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Flavor']; + }; + }; + }; + }; + flavors_retrieve: { + parameters: { + query?: never; + header?: never; + path: { + /** @description A unique integer value identifying this flavor. */ + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Flavor']; + }; + }; + }; + }; + flavors_update: { + parameters: { + query?: never; + header?: never; + path: { + /** @description A unique integer value identifying this flavor. */ + id: number; + }; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['Flavor']; + 'application/x-www-form-urlencoded': components['schemas']['Flavor']; + 'multipart/form-data': components['schemas']['Flavor']; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Flavor']; + }; + }; + }; + }; + flavors_destroy: { + parameters: { + query?: never; + header?: never; + path: { + /** @description A unique integer value identifying this flavor. */ + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No response body */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + flavors_partial_update: { + parameters: { + query?: never; + header?: never; + path: { + /** @description A unique integer value identifying this flavor. */ + id: number; + }; + cookie?: never; + }; + requestBody?: { + content: { + 'application/json': components['schemas']['PatchedFlavor']; + 'application/x-www-form-urlencoded': components['schemas']['PatchedFlavor']; + 'multipart/form-data': components['schemas']['PatchedFlavor']; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Flavor']; + }; + }; + }; + }; + flavors_followed_top_retrieve: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Flavor']; + }; + }; + }; + }; + flavors_newest_retrieve: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Flavor']; + }; + }; + }; + }; + flavors_search_retrieve: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Flavor']; + }; + }; + }; + }; + flavors_top_retrieve: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Flavor']; + }; + }; + }; + }; + notifications_list: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Notification'][]; + }; + }; + }; + }; + notifications_retrieve: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Notification']; + }; + }; + }; + }; + notifications_mark_read_create: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['Notification']; + 'application/x-www-form-urlencoded': components['schemas']['Notification']; + 'multipart/form-data': components['schemas']['Notification']; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Notification']; + }; + }; + }; + }; + notifications_mark_all_read_create: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['Notification']; + 'application/x-www-form-urlencoded': components['schemas']['Notification']; + 'multipart/form-data': components['schemas']['Notification']; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Notification']; + }; + }; + }; + }; + ratings_list: { + parameters: { + query?: { + /** @description A page number within the paginated result set. */ + page?: number; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['PaginatedRatingList']; + }; + }; + }; + }; + ratings_create: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['Rating']; + 'application/x-www-form-urlencoded': components['schemas']['Rating']; + 'multipart/form-data': components['schemas']['Rating']; + }; + }; + responses: { + 201: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Rating']; + }; + }; + }; + }; + ratings_retrieve: { + parameters: { + query?: never; + header?: never; + path: { + /** @description A unique integer value identifying this rating. */ + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Rating']; + }; + }; + }; + }; + ratings_update: { + parameters: { + query?: never; + header?: never; + path: { + /** @description A unique integer value identifying this rating. */ + id: number; + }; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['Rating']; + 'application/x-www-form-urlencoded': components['schemas']['Rating']; + 'multipart/form-data': components['schemas']['Rating']; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Rating']; + }; + }; + }; + }; + ratings_destroy: { + parameters: { + query?: never; + header?: never; + path: { + /** @description A unique integer value identifying this rating. */ + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No response body */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + ratings_partial_update: { + parameters: { + query?: never; + header?: never; + path: { + /** @description A unique integer value identifying this rating. */ + id: number; + }; + cookie?: never; + }; + requestBody?: { + content: { + 'application/json': components['schemas']['PatchedRating']; + 'application/x-www-form-urlencoded': components['schemas']['PatchedRating']; + 'multipart/form-data': components['schemas']['PatchedRating']; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Rating']; + }; + }; + }; + }; + ratings_reply_create: { + parameters: { + query?: never; + header?: never; + path: { + /** @description A unique integer value identifying this rating. */ + id: number; + }; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['Rating']; + 'application/x-www-form-urlencoded': components['schemas']['Rating']; + 'multipart/form-data': components['schemas']['Rating']; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Rating']; + }; + }; + }; + }; + ratings_feed_retrieve: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Rating']; + }; + }; + }; + }; + ratings_recent_retrieve: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Rating']; + }; + }; + }; + }; + replies_list: { + parameters: { + query?: { + /** @description A page number within the paginated result set. */ + page?: number; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['PaginatedReplyList']; + }; + }; + }; + }; + replies_create: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['Reply']; + 'application/x-www-form-urlencoded': components['schemas']['Reply']; + 'multipart/form-data': components['schemas']['Reply']; + }; + }; + responses: { + 201: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Reply']; + }; + }; + }; + }; + replies_retrieve: { + parameters: { + query?: never; + header?: never; + path: { + /** @description A unique integer value identifying this reply. */ + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Reply']; + }; + }; + }; + }; + replies_update: { + parameters: { + query?: never; + header?: never; + path: { + /** @description A unique integer value identifying this reply. */ + id: number; + }; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['Reply']; + 'application/x-www-form-urlencoded': components['schemas']['Reply']; + 'multipart/form-data': components['schemas']['Reply']; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Reply']; + }; + }; + }; + }; + replies_destroy: { + parameters: { + query?: never; + header?: never; + path: { + /** @description A unique integer value identifying this reply. */ + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No response body */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + replies_partial_update: { + parameters: { + query?: never; + header?: never; + path: { + /** @description A unique integer value identifying this reply. */ + id: number; + }; + cookie?: never; + }; + requestBody?: { + content: { + 'application/json': components['schemas']['PatchedReply']; + 'application/x-www-form-urlencoded': components['schemas']['PatchedReply']; + 'multipart/form-data': components['schemas']['PatchedReply']; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Reply']; + }; + }; + }; + }; + tickets_list: { + parameters: { + query?: { + /** @description A page number within the paginated result set. */ + page?: number; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['PaginatedTicketList']; + }; + }; + }; + }; + tickets_create: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['Ticket']; + 'application/x-www-form-urlencoded': components['schemas']['Ticket']; + 'multipart/form-data': components['schemas']['Ticket']; + }; + }; + responses: { + 201: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Ticket']; + }; + }; + }; + }; + tickets_retrieve: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Ticket']; + }; + }; + }; + }; + tickets_update: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['Ticket']; + 'application/x-www-form-urlencoded': components['schemas']['Ticket']; + 'multipart/form-data': components['schemas']['Ticket']; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Ticket']; + }; + }; + }; + }; + tickets_destroy: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No response body */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + tickets_partial_update: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: { + content: { + 'application/json': components['schemas']['PatchedTicket']; + 'application/x-www-form-urlencoded': components['schemas']['PatchedTicket']; + 'multipart/form-data': components['schemas']['PatchedTicket']; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Ticket']; + }; + }; + }; + }; + tickets_add_message_create: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['Ticket']; + 'application/x-www-form-urlencoded': components['schemas']['Ticket']; + 'multipart/form-data': components['schemas']['Ticket']; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Ticket']; + }; + }; + }; + }; + tickets_update_status_create: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['Ticket']; + 'application/x-www-form-urlencoded': components['schemas']['Ticket']; + 'multipart/form-data': components['schemas']['Ticket']; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Ticket']; + }; + }; + }; + }; + token_create: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['TokenObtainPair']; + 'application/x-www-form-urlencoded': components['schemas']['TokenObtainPair']; + 'multipart/form-data': components['schemas']['TokenObtainPair']; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['TokenObtainPair']; + }; + }; + }; + }; + users_list: { + parameters: { + query?: { + /** @description A page number within the paginated result set. */ + page?: number; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['PaginatedUserList']; + }; + }; + }; + }; + users_create: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['User']; + 'application/x-www-form-urlencoded': components['schemas']['User']; + 'multipart/form-data': components['schemas']['User']; + }; + }; + responses: { + 201: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['User']; + }; + }; + }; + }; + users_retrieve: { + parameters: { + query?: never; + header?: never; + path: { + /** @description A unique integer value identifying this user. */ + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['User']; + }; + }; + }; + }; + users_update: { + parameters: { + query?: never; + header?: never; + path: { + /** @description A unique integer value identifying this user. */ + id: number; + }; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['User']; + 'application/x-www-form-urlencoded': components['schemas']['User']; + 'multipart/form-data': components['schemas']['User']; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['User']; + }; + }; + }; + }; + users_destroy: { + parameters: { + query?: never; + header?: never; + path: { + /** @description A unique integer value identifying this user. */ + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No response body */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + users_partial_update: { + parameters: { + query?: never; + header?: never; + path: { + /** @description A unique integer value identifying this user. */ + id: number; + }; + cookie?: never; + }; + requestBody?: { + content: { + 'application/json': components['schemas']['PatchedUser']; + 'application/x-www-form-urlencoded': components['schemas']['PatchedUser']; + 'multipart/form-data': components['schemas']['PatchedUser']; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['User']; + }; + }; + }; + }; + users_add_comment_create: { + parameters: { + query?: never; + header?: never; + path: { + /** @description A unique integer value identifying this user. */ + id: number; + }; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['User']; + 'application/x-www-form-urlencoded': components['schemas']['User']; + 'multipart/form-data': components['schemas']['User']; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['User']; + }; + }; + }; + }; + users_delete_comment_destroy: { + parameters: { + query?: never; + header?: never; + path: { + comment_id: string; + /** @description A unique integer value identifying this user. */ + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No response body */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + users_follow_create: { + parameters: { + query?: never; + header?: never; + path: { + /** @description A unique integer value identifying this user. */ + id: number; + }; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['User']; + 'application/x-www-form-urlencoded': components['schemas']['User']; + 'multipart/form-data': components['schemas']['User']; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['User']; + }; + }; + }; + }; + users_unfollow_create: { + parameters: { + query?: never; + header?: never; + path: { + /** @description A unique integer value identifying this user. */ + id: number; + }; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['User']; + 'application/x-www-form-urlencoded': components['schemas']['User']; + 'multipart/form-data': components['schemas']['User']; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['User']; + }; + }; + }; + }; + users_change_password_create: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['User']; + 'application/x-www-form-urlencoded': components['schemas']['User']; + 'multipart/form-data': components['schemas']['User']; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['User']; + }; + }; + }; + }; + users_complete_password_reset_create: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['User']; + 'application/x-www-form-urlencoded': components['schemas']['User']; + 'multipart/form-data': components['schemas']['User']; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['User']; + }; + }; + }; + }; + users_confirm_account_deletion_create: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['User']; + 'application/x-www-form-urlencoded': components['schemas']['User']; + 'multipart/form-data': components['schemas']['User']; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['User']; + }; + }; + }; + }; + users_confirm_email_create: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['User']; + 'application/x-www-form-urlencoded': components['schemas']['User']; + 'multipart/form-data': components['schemas']['User']; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['User']; + }; + }; + }; + }; + users_dashboard_retrieve: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['User']; + }; + }; + }; + }; + users_following_list_retrieve: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['User']; + }; + }; + }; + }; + users_me_retrieve: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['User']; + }; + }; + }; + }; + users_profile_retrieve: { + parameters: { + query?: never; + header?: never; + path: { + username: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['User']; + }; + }; + }; + }; + users_request_account_deletion_create: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['User']; + 'application/x-www-form-urlencoded': components['schemas']['User']; + 'multipart/form-data': components['schemas']['User']; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['User']; + }; + }; + }; + }; + users_request_password_reset_create: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['User']; + 'application/x-www-form-urlencoded': components['schemas']['User']; + 'multipart/form-data': components['schemas']['User']; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['User']; + }; + }; + }; + }; + users_resend_verification_create: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['User']; + 'application/x-www-form-urlencoded': components['schemas']['User']; + 'multipart/form-data': components['schemas']['User']; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['User']; + }; + }; + }; + }; + users_signup_create: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['User']; + 'application/x-www-form-urlencoded': components['schemas']['User']; + 'multipart/form-data': components['schemas']['User']; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['User']; + }; + }; + }; + }; + users_update_avatar_create: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['User']; + 'application/x-www-form-urlencoded': components['schemas']['User']; + 'multipart/form-data': components['schemas']['User']; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['User']; + }; + }; + }; + }; + users_update_preferences_partial_update: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + 'application/json': components['schemas']['PatchedUser']; + 'application/x-www-form-urlencoded': components['schemas']['PatchedUser']; + 'multipart/form-data': components['schemas']['PatchedUser']; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['User']; + }; + }; + }; + }; + users_update_profile_partial_update: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + 'application/json': components['schemas']['PatchedUser']; + 'application/x-www-form-urlencoded': components['schemas']['PatchedUser']; + 'multipart/form-data': components['schemas']['PatchedUser']; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['User']; + }; + }; + }; + }; + users_verify_signup_create: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['User']; + 'application/x-www-form-urlencoded': components['schemas']['User']; + 'multipart/form-data': components['schemas']['User']; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['User']; + }; + }; + }; + }; +} diff --git a/frontend/src/api/types.ts b/frontend/src/api/types.ts index 7e02316..c4bc484 100644 --- a/frontend/src/api/types.ts +++ b/frontend/src/api/types.ts @@ -1,25 +1,25 @@ -export interface Reply { - id: number; - user: string; - text: string; - created_at: string; -} +import type { components } from './schema'; -export interface RatedItem { - id: number; - flavor: number; - flavor_name: string; - flavor_image: string | null; - category_name: string; - category_slug: string; - score: number; - comment: string | null; - created_at: string; - replies: Reply[]; - is_legacy?: boolean; - is_available?: boolean; -} +export type Schemas = components['schemas']; +// Re-exported from generated OpenAPI schema. +// Regenerate with `npm run openapi:sync` after backend serializer changes. +export type Banner = Schemas['Banner']; +export type Category = Schemas['Category']; +export type Flavor = Schemas['Flavor']; +export type Notification = Schemas['Notification']; +export type Rating = Schemas['Rating']; +export type Reply = Schemas['Reply']; +export type Ticket = Schemas['Ticket']; +export type TicketMessage = Schemas['TicketMessage']; +export type User = Schemas['User']; + +// `BannerConfig` was the legacy alias for the Banner schema; kept for callers. +export type BannerConfig = Banner; +// `RatedItem` was the legacy alias for Rating in dashboard payload. +export type RatedItem = Rating; + +// Custom (non-DRF-serialized) shapes for endpoints without a serializer_class. export interface MissingFlavor { id: number; name: string; @@ -39,22 +39,3 @@ export interface DashboardData { missing_flavors: MissingFlavor[]; my_ratings: RatedItem[]; } - -export interface BannerConfig { - id: number; - name: string; - slug: string; - description: string; - is_active: boolean; - settings: Record | null; -} - -export interface NotificationItem { - id: number; - verb: string; - actor: string; - target: string | null; - url: string | null; - is_read: boolean; - created_at: string; -} diff --git a/frontend/src/contexts/NotificationContext.tsx b/frontend/src/contexts/NotificationContext.tsx index 0e272a7..eac31f3 100644 --- a/frontend/src/contexts/NotificationContext.tsx +++ b/frontend/src/contexts/NotificationContext.tsx @@ -30,13 +30,12 @@ export const NotificationProvider = ({ children }: { children: ReactNode }) => { // Refetch the auth user on the same 60s cadence so the unread count badge // stays in sync with anything that mutated server-side outside this client. useEffect(() => { + if (!user) return; const interval = setInterval(() => { - if (localStorage.getItem('access')) { - refetchUser(); - } + refetchUser(); }, 60_000); return () => clearInterval(interval); - }, [refetchUser]); + }, [refetchUser, user]); const fetchNotifications = useCallback(async () => { await refetch(); diff --git a/frontend/src/pages/CommunityFeed.tsx b/frontend/src/pages/CommunityFeed.tsx index 11e8642..fd2d6cb 100644 --- a/frontend/src/pages/CommunityFeed.tsx +++ b/frontend/src/pages/CommunityFeed.tsx @@ -35,6 +35,7 @@ import { import { useNotificationsQuery } from '../api/queries/useNotifications'; import { useCreateReply } from '../api/mutations/useRatingMutations'; import { useTitle } from '../hooks/useTitle'; +import { useAuth } from '../hooks/useAuth'; import RichText from '../components/RichText'; import { formatDate } from '../utils/date'; import { useTranslation } from 'react-i18next'; @@ -82,7 +83,8 @@ const CommunityFeed: React.FC = ({ adminMode }) => { const [replyInputs, setReplyInputs] = useState>({}); const [expandedReplies, setExpandedReplies] = useState>({}); - const isAuthed = !!localStorage.getItem('access'); + const { user } = useAuth(); + const isAuthed = !!user; const { data: feed, isLoading: feedLoading, error: feedError } = useCommunityFeed(page); const { data: following = [] } = useFollowingList(); const { data: notificationsAll = [] } = useNotificationsQuery(isAuthed); diff --git a/frontend/src/pages/MainPage.tsx b/frontend/src/pages/MainPage.tsx index 269344e..7ed6a86 100644 --- a/frontend/src/pages/MainPage.tsx +++ b/frontend/src/pages/MainPage.tsx @@ -20,6 +20,7 @@ import { useTopFlavors, } from '../api/queries/useMainPageQueries'; import { useTitle } from '../hooks/useTitle'; +import { useAuth } from '../hooks/useAuth'; import RatingBadge from '../components/RatingBadge'; import StatusBadge from '../components/StatusBadge'; import { @@ -86,7 +87,8 @@ const MainPage: React.FC = (_props) => { const [activeIndex, setActiveIndex] = useState(0); const query = new URLSearchParams(location.search).get('q') || ''; - const isLoggedIn = !!localStorage.getItem('access'); + const { user } = useAuth(); + const isLoggedIn = !!user; const { data: searchResults = [], isLoading: searchLoading } = useFlavorSearch(query); const { data: topFlavorsAll = [], isLoading: topLoading } = useTopFlavors(null);