commit
8f8d6db35d
32 changed files with 867 additions and 0 deletions
@ -0,0 +1,5 @@ |
|||||||
|
__pycache__ |
||||||
|
.venv |
||||||
|
**.sqlite3 |
||||||
|
.env |
||||||
|
token |
@ -0,0 +1,17 @@ |
|||||||
|
import ollama |
||||||
|
|
||||||
|
# Call chat with stream=True to enable streaming |
||||||
|
stream = ollama.chat( |
||||||
|
model='llama3:8b', |
||||||
|
messages=[ |
||||||
|
{ |
||||||
|
'role': 'user', |
||||||
|
'content': 'Hello, could you make a presentation of your self', |
||||||
|
} |
||||||
|
], |
||||||
|
stream=True |
||||||
|
) |
||||||
|
|
||||||
|
# Process the streamed response chunks |
||||||
|
for chunk in stream: |
||||||
|
print(chunk['message']['content'], end='', flush=True) |
@ -0,0 +1,58 @@ |
|||||||
|
from django.contrib import admin |
||||||
|
|
||||||
|
# Register your models here. |
||||||
|
from django.contrib import admin |
||||||
|
from .models import MasterKey |
||||||
|
from django.contrib.auth import get_user_model |
||||||
|
|
||||||
|
User = get_user_model() |
||||||
|
|
||||||
|
@admin.register(MasterKey) |
||||||
|
class MasterKeyAdmin(admin.ModelAdmin): |
||||||
|
list_display = ('key_id', 'description', 'created_at', 'last_used', 'is_active') |
||||||
|
list_filter = ('is_active', 'created_at') |
||||||
|
search_fields = ('description', 'key_id') |
||||||
|
readonly_fields = ('key_id', 'key_value', 'created_at', 'last_used') |
||||||
|
fieldsets = ( |
||||||
|
(None, { |
||||||
|
'fields': ('key_id', 'description', 'is_active') |
||||||
|
}), |
||||||
|
('Permissions', { |
||||||
|
'fields': ('permissions',) |
||||||
|
}), |
||||||
|
('Security Info', { |
||||||
|
'fields': ('created_at', 'last_used'), |
||||||
|
'classes': ('collapse',) |
||||||
|
}), |
||||||
|
) |
||||||
|
actions = ['deactivate_keys', 'activate_keys'] |
||||||
|
|
||||||
|
def deactivate_keys(self, request, queryset): |
||||||
|
queryset.update(is_active=False) |
||||||
|
deactivate_keys.short_description = "Deactivate selected keys" |
||||||
|
|
||||||
|
def activate_keys(self, request, queryset): |
||||||
|
queryset.update(is_active=True) |
||||||
|
activate_keys.short_description = "Activate selected keys" |
||||||
|
|
||||||
|
def has_change_permission(self, request, obj=None): |
||||||
|
# Prevent editing of key_value for security |
||||||
|
if obj is not None: |
||||||
|
return True |
||||||
|
return super().has_change_permission(request, obj) |
||||||
|
|
||||||
|
# @admin.register(User) |
||||||
|
# class UserAdmin(admin.ModelAdmin): |
||||||
|
# list_display = ('last_login', 'username', 'is_staff', 'date_joined', 'is_active') |
||||||
|
# list_filter = ('is_active', 'date_joined', 'is_staff') |
||||||
|
# search_fields = ('username', 'date_joined') |
||||||
|
# readonly_fields = ('username', 'date_joined', 'last_login') |
||||||
|
# fieldsets = ( |
||||||
|
# (None, { |
||||||
|
# 'fields': ('username', 'date_joined') |
||||||
|
# }), |
||||||
|
# ('Security Info', { |
||||||
|
# 'fields': ('is_staff', 'last_login', 'is_active'), |
||||||
|
# 'classes': ('collapse',) |
||||||
|
# }), |
||||||
|
# ) |
@ -0,0 +1,6 @@ |
|||||||
|
from django.apps import AppConfig |
||||||
|
|
||||||
|
|
||||||
|
class ApiConfig(AppConfig): |
||||||
|
default_auto_field = 'django.db.models.BigAutoField' |
||||||
|
name = 'api' |
@ -0,0 +1,21 @@ |
|||||||
|
from django.core.management.base import BaseCommand |
||||||
|
from api.models import MasterKey |
||||||
|
|
||||||
|
class Command(BaseCommand): |
||||||
|
help = 'Generate a new master key' |
||||||
|
|
||||||
|
def add_arguments(self, parser): |
||||||
|
parser.add_argument('--description', type=str, help='Description for the key') |
||||||
|
parser.add_argument('--permissions', nargs='+', help='List of permissions for the key') |
||||||
|
|
||||||
|
def handle(self, *args, **kwargs): |
||||||
|
description = kwargs.get('description') or 'Generated via management command' |
||||||
|
permissions = kwargs.get('permissions') or [] |
||||||
|
|
||||||
|
master_key = MasterKey.generate_key( |
||||||
|
description=description, |
||||||
|
permissions=permissions |
||||||
|
) |
||||||
|
|
||||||
|
self.stdout.write(self.style.SUCCESS(f'Successfully created master key: {master_key.key_id}')) |
||||||
|
self.stdout.write(self.style.WARNING(f'Key value (save this securely, it will not be shown again): {master_key.key_value}')) |
@ -0,0 +1,27 @@ |
|||||||
|
# Generated by Django 5.2.1 on 2025-05-11 16:40 |
||||||
|
|
||||||
|
import uuid |
||||||
|
from django.db import migrations, models |
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration): |
||||||
|
|
||||||
|
initial = True |
||||||
|
|
||||||
|
dependencies = [ |
||||||
|
] |
||||||
|
|
||||||
|
operations = [ |
||||||
|
migrations.CreateModel( |
||||||
|
name='MasterKey', |
||||||
|
fields=[ |
||||||
|
('key_id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), |
||||||
|
('key_value', models.CharField(max_length=255, unique=True)), |
||||||
|
('description', models.CharField(blank=True, max_length=255)), |
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)), |
||||||
|
('last_used', models.DateTimeField(blank=True, null=True)), |
||||||
|
('is_active', models.BooleanField(default=True)), |
||||||
|
('permissions', models.JSONField(default=list)), |
||||||
|
], |
||||||
|
), |
||||||
|
] |
@ -0,0 +1,20 @@ |
|||||||
|
# Generated by Django 5.2.1 on 2025-05-11 17:28 |
||||||
|
|
||||||
|
from django.db import migrations, models |
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration): |
||||||
|
|
||||||
|
dependencies = [ |
||||||
|
('api', '0001_initial'), |
||||||
|
] |
||||||
|
|
||||||
|
operations = [ |
||||||
|
migrations.CreateModel( |
||||||
|
name='AiModel', |
||||||
|
fields=[ |
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), |
||||||
|
('name', models.CharField(max_length=255)), |
||||||
|
], |
||||||
|
), |
||||||
|
] |
@ -0,0 +1,34 @@ |
|||||||
|
# Generated by Django 5.2.1 on 2025-05-11 17:46 |
||||||
|
|
||||||
|
import django.db.models.deletion |
||||||
|
from django.conf import settings |
||||||
|
from django.db import migrations, models |
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration): |
||||||
|
|
||||||
|
dependencies = [ |
||||||
|
('api', '0002_aimodel'), |
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL), |
||||||
|
] |
||||||
|
|
||||||
|
operations = [ |
||||||
|
migrations.CreateModel( |
||||||
|
name='Conversation', |
||||||
|
fields=[ |
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), |
||||||
|
('title', models.CharField(max_length=255)), |
||||||
|
('model', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.aimodel')), |
||||||
|
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), |
||||||
|
], |
||||||
|
), |
||||||
|
migrations.CreateModel( |
||||||
|
name='Message', |
||||||
|
fields=[ |
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), |
||||||
|
('content', models.TextField()), |
||||||
|
('role', models.CharField(max_length=255)), |
||||||
|
('conversation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.conversation')), |
||||||
|
], |
||||||
|
), |
||||||
|
] |
@ -0,0 +1,17 @@ |
|||||||
|
# Generated by Django 5.2.1 on 2025-05-12 11:19 |
||||||
|
|
||||||
|
from django.db import migrations |
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration): |
||||||
|
|
||||||
|
dependencies = [ |
||||||
|
('api', '0003_conversation_message'), |
||||||
|
] |
||||||
|
|
||||||
|
operations = [ |
||||||
|
migrations.RemoveField( |
||||||
|
model_name='conversation', |
||||||
|
name='model', |
||||||
|
), |
||||||
|
] |
@ -0,0 +1,26 @@ |
|||||||
|
# Generated by Django 5.2.1 on 2025-05-12 12:49 |
||||||
|
|
||||||
|
import django.db.models.deletion |
||||||
|
from django.conf import settings |
||||||
|
from django.db import migrations, models |
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration): |
||||||
|
|
||||||
|
dependencies = [ |
||||||
|
('api', '0004_remove_conversation_model'), |
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL), |
||||||
|
] |
||||||
|
|
||||||
|
operations = [ |
||||||
|
migrations.AlterField( |
||||||
|
model_name='conversation', |
||||||
|
name='user', |
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='conversations', to=settings.AUTH_USER_MODEL), |
||||||
|
), |
||||||
|
migrations.AlterField( |
||||||
|
model_name='message', |
||||||
|
name='conversation', |
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='messages', to='api.conversation'), |
||||||
|
), |
||||||
|
] |
@ -0,0 +1,17 @@ |
|||||||
|
# Generated by Django 5.2.1 on 2025-05-12 16:03 |
||||||
|
|
||||||
|
from django.db import migrations |
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration): |
||||||
|
|
||||||
|
dependencies = [ |
||||||
|
('api', '0005_alter_conversation_user_alter_message_conversation'), |
||||||
|
] |
||||||
|
|
||||||
|
operations = [ |
||||||
|
migrations.RemoveField( |
||||||
|
model_name='conversation', |
||||||
|
name='user', |
||||||
|
), |
||||||
|
] |
@ -0,0 +1,22 @@ |
|||||||
|
# Generated by Django 5.2.1 on 2025-05-12 16:53 |
||||||
|
|
||||||
|
import django.db.models.deletion |
||||||
|
from django.conf import settings |
||||||
|
from django.db import migrations, models |
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration): |
||||||
|
|
||||||
|
dependencies = [ |
||||||
|
('api', '0006_remove_conversation_user'), |
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL), |
||||||
|
] |
||||||
|
|
||||||
|
operations = [ |
||||||
|
migrations.AddField( |
||||||
|
model_name='conversation', |
||||||
|
name='user', |
||||||
|
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, related_name='conversations', to=settings.AUTH_USER_MODEL), |
||||||
|
preserve_default=False, |
||||||
|
), |
||||||
|
] |
@ -0,0 +1,47 @@ |
|||||||
|
from django.db import models |
||||||
|
from django.contrib.auth import get_user_model |
||||||
|
import uuid |
||||||
|
import secrets |
||||||
|
|
||||||
|
User = get_user_model() |
||||||
|
|
||||||
|
class MasterKey(models.Model): |
||||||
|
key_id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) |
||||||
|
key_value = models.CharField(max_length=255, unique=True) |
||||||
|
description = models.CharField(max_length=255, blank=True) |
||||||
|
created_at = models.DateTimeField(auto_now_add=True) |
||||||
|
last_used = models.DateTimeField(null=True, blank=True) |
||||||
|
is_active = models.BooleanField(default=True) |
||||||
|
permissions = models.JSONField(default=list) |
||||||
|
|
||||||
|
def __str__(self): |
||||||
|
return f"Master Key: {self.key_id}" |
||||||
|
|
||||||
|
@classmethod |
||||||
|
def generate_key(cls, description="", permissions=None): |
||||||
|
"""Generate a new secure master key""" |
||||||
|
if permissions is None: |
||||||
|
permissions = [] |
||||||
|
|
||||||
|
# Generate a secure random token |
||||||
|
key_value = secrets.token_urlsafe(64) |
||||||
|
|
||||||
|
return cls.objects.create( |
||||||
|
key_value=key_value, |
||||||
|
description=description, |
||||||
|
permissions=permissions |
||||||
|
) |
||||||
|
|
||||||
|
class AiModel(models.Model): |
||||||
|
name = models.CharField(max_length=255) |
||||||
|
|
||||||
|
class Conversation(models.Model): |
||||||
|
title = models.CharField(max_length=255) |
||||||
|
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="conversations") |
||||||
|
|
||||||
|
class Message(models.Model): |
||||||
|
content = models.TextField() |
||||||
|
role = models.CharField(max_length=255) |
||||||
|
conversation = models.ForeignKey(Conversation, on_delete=models.CASCADE, related_name="messages") |
||||||
|
|
||||||
|
|
@ -0,0 +1,13 @@ |
|||||||
|
from rest_framework import serializers |
||||||
|
from .models import Conversation, Message |
||||||
|
|
||||||
|
class ConversationSerializer(serializers.ModelSerializer): |
||||||
|
class Meta: |
||||||
|
model = Conversation |
||||||
|
fields = ['id', 'title'] |
||||||
|
|
||||||
|
|
||||||
|
class MessageSerializer(serializers.ModelSerializer): |
||||||
|
class Meta: |
||||||
|
model = Message |
||||||
|
fields = ['id', 'content', 'role'] |
@ -0,0 +1,14 @@ |
|||||||
|
{% extends "drf-yasg/redoc.html" %} |
||||||
|
|
||||||
|
{% block favicon %} |
||||||
|
<link rel="icon" type="image/ico" href="/static/favicon.ico"/> |
||||||
|
<style> |
||||||
|
img[alt="Botzilla logo"] { |
||||||
|
|
||||||
|
max-height: 80px !important; |
||||||
|
width: auto; |
||||||
|
padding-top: 3px !important; |
||||||
|
padding-bottom: 3px !important; |
||||||
|
} |
||||||
|
</style> |
||||||
|
{% endblock %} |
@ -0,0 +1,3 @@ |
|||||||
|
from django.test import TestCase |
||||||
|
|
||||||
|
# Create your tests here. |
@ -0,0 +1,34 @@ |
|||||||
|
""" |
||||||
|
URL configuration for botzilla project. |
||||||
|
|
||||||
|
The `urlpatterns` list routes URLs to views. For more information please see: |
||||||
|
https://docs.djangoproject.com/en/5.2/topics/http/urls/ |
||||||
|
Examples: |
||||||
|
Function views |
||||||
|
1. Add an import: from my_app import views |
||||||
|
2. Add a URL to urlpatterns: path('', views.home, name='home') |
||||||
|
Class-based views |
||||||
|
1. Add an import: from other_app.views import Home |
||||||
|
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') |
||||||
|
Including another URLconf |
||||||
|
1. Import the include() function: from django.urls import include, path |
||||||
|
2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) |
||||||
|
""" |
||||||
|
from django.urls import path, include |
||||||
|
from rest_framework_simplejwt.views import ( |
||||||
|
TokenObtainPairView, |
||||||
|
TokenRefreshView, |
||||||
|
TokenVerifyView, |
||||||
|
) |
||||||
|
from .views import MasterKeyTokenObtainView |
||||||
|
from .views import ConversationView |
||||||
|
from rest_framework.routers import DefaultRouter |
||||||
|
|
||||||
|
router = DefaultRouter() |
||||||
|
router.register(r'conversations', ConversationView) |
||||||
|
urlpatterns = [ |
||||||
|
path('token/', MasterKeyTokenObtainView.as_view(), name='token_obtain_pair'), |
||||||
|
path('token/refresh/', TokenRefreshView.as_view(), name='token_refresh'), |
||||||
|
path('token/verify/', TokenVerifyView.as_view(), name='token_verify'), |
||||||
|
path('', include(router.urls)), |
||||||
|
] |
@ -0,0 +1,185 @@ |
|||||||
|
from drf_yasg.utils import swagger_auto_schema |
||||||
|
from drf_yasg import openapi |
||||||
|
|
||||||
|
from rest_framework_simplejwt.views import TokenObtainPairView |
||||||
|
from rest_framework_simplejwt.tokens import RefreshToken |
||||||
|
from django.views.decorators.http import require_http_methods |
||||||
|
from rest_framework.response import Response |
||||||
|
from rest_framework import status |
||||||
|
from rest_framework.permissions import AllowAny, IsAuthenticated |
||||||
|
from django.utils import timezone |
||||||
|
from django.contrib.auth import get_user_model |
||||||
|
from .models import MasterKey |
||||||
|
from .models import Conversation |
||||||
|
from .models import Message |
||||||
|
from .serializers import ConversationSerializer, MessageSerializer |
||||||
|
from rest_framework import viewsets, status |
||||||
|
from rest_framework.decorators import action |
||||||
|
from rest_framework.response import Response |
||||||
|
import ollama |
||||||
|
|
||||||
|
User = get_user_model() |
||||||
|
|
||||||
|
class ConversationView(viewsets.ModelViewSet): |
||||||
|
|
||||||
|
queryset = Conversation.objects.all() |
||||||
|
serializer_class = ConversationSerializer |
||||||
|
permission_classes = [IsAuthenticated] # JWT token auth required |
||||||
|
|
||||||
|
def get_queryset(self): |
||||||
|
queryset = Conversation.objects.all() |
||||||
|
return queryset |
||||||
|
|
||||||
|
def perform_create(self, serializer): |
||||||
|
"""Associate new product with current user""" |
||||||
|
serializer.save(user=self.request.user) |
||||||
|
|
||||||
|
@swagger_auto_schema( |
||||||
|
method='get', |
||||||
|
operation_description="Get messages of the conversation", |
||||||
|
responses={ |
||||||
|
200: openapi.Response('List of messages', MessageSerializer(many=True)), |
||||||
|
400: 'Bad Request', |
||||||
|
404: 'Conversation not found' |
||||||
|
}, |
||||||
|
manual_parameters=[ |
||||||
|
openapi.Parameter( |
||||||
|
'category', |
||||||
|
openapi.IN_QUERY, |
||||||
|
description="Filter featured items by category", |
||||||
|
type=openapi.TYPE_STRING |
||||||
|
) |
||||||
|
] |
||||||
|
) |
||||||
|
@action(detail=True, methods=['get']) |
||||||
|
def contents(self, request, pk=None): |
||||||
|
conversation = self.get_object() |
||||||
|
messages = conversation.messages.all() |
||||||
|
return Response(data=list(messages.values())) |
||||||
|
|
||||||
|
@swagger_auto_schema( |
||||||
|
method='post', |
||||||
|
operation_description="Discutes with the ai", |
||||||
|
request_body=openapi.Schema( |
||||||
|
type=openapi.TYPE_OBJECT, |
||||||
|
properties={ |
||||||
|
'content': openapi.Schema(type=openapi.TYPE_STRING, description='Contents of the message'), |
||||||
|
}, |
||||||
|
required=['content'] |
||||||
|
), |
||||||
|
responses={ |
||||||
|
200: openapi.Response('Message processed successfully', MessageSerializer), |
||||||
|
400: 'Bad Request', |
||||||
|
} |
||||||
|
) |
||||||
|
@action(detail=True, methods=['post']) |
||||||
|
def prompt(self, request, pk=None): |
||||||
|
conversation = self.get_object() |
||||||
|
messages = [{ |
||||||
|
"role": "system", |
||||||
|
"content": """ |
||||||
|
You must strictly refuse to engage with ANY of the following: |
||||||
|
1. Violence or harm (even fictional or hypothetical scenarios) |
||||||
|
2. ANY explicit, suggestive, or romantic content |
||||||
|
3. Controversial political topics |
||||||
|
4. ANY content that could potentially be misused |
||||||
|
5. Medical, legal, or financial advice |
||||||
|
6. Personal information or privacy violations |
||||||
|
7. Anything that could be remotely offensive to anyone |
||||||
|
|
||||||
|
If you detect such content, immediately respond with: |
||||||
|
"I cannot assist with that request as it appears to be inappropriate. I'm designed to be helpful, but within strict ethical boundaries. Is there something else I can help you with?" |
||||||
|
""" |
||||||
|
}] |
||||||
|
for message in conversation.messages.all(): |
||||||
|
if message: |
||||||
|
messages.append({ |
||||||
|
"role": message.role, |
||||||
|
"content": message.content |
||||||
|
}) |
||||||
|
messages.append({ |
||||||
|
"role": "user", |
||||||
|
"content": request.data.get("content", "") |
||||||
|
}) |
||||||
|
res = ollama.chat(model="gemma3", messages=messages) |
||||||
|
Message(role="user", content=request.data.get("content", ""), conversation=conversation).save() |
||||||
|
Message(role="assistant", content=res['message']['content'], conversation=conversation).save() |
||||||
|
return Response(data={ |
||||||
|
"content": res['message']['content'] |
||||||
|
}) |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class MasterKeyTokenObtainView(TokenObtainPairView): |
||||||
|
permission_classes = [AllowAny] |
||||||
|
|
||||||
|
@swagger_auto_schema( |
||||||
|
operation_description="Creates a token for a user (creates the user if doesn't exists)", |
||||||
|
request_body=openapi.Schema( |
||||||
|
type=openapi.TYPE_OBJECT, |
||||||
|
properties={ |
||||||
|
'username': openapi.Schema(type=openapi.TYPE_STRING, description='Identifier of the user'), |
||||||
|
'master_key': openapi.Schema(type=openapi.TYPE_STRING, description='Key of the authorizied application'), |
||||||
|
}, |
||||||
|
required=['username', 'master_key'] |
||||||
|
), |
||||||
|
responses={ |
||||||
|
200: openapi.Response('API tokens of the user', openapi.Schema( |
||||||
|
type=openapi.TYPE_OBJECT, |
||||||
|
properties={ |
||||||
|
'access': openapi.Schema(type=openapi.TYPE_STRING, description='API access token of the user'), |
||||||
|
'refresh': openapi.Schema(type=openapi.TYPE_STRING, description='API refresh token of the user'), |
||||||
|
}, |
||||||
|
required=['access', 'access'] |
||||||
|
)), |
||||||
|
400: 'Bad Request', |
||||||
|
401: 'Bad master key' |
||||||
|
} |
||||||
|
) |
||||||
|
def post(self, request, *args, **kwargs): |
||||||
|
# Get the provided master key from the request |
||||||
|
master_key_value = request.data.get('master_key') |
||||||
|
|
||||||
|
if not master_key_value: |
||||||
|
return Response( |
||||||
|
{'error': 'Master key is required'}, |
||||||
|
status=status.HTTP_400_BAD_REQUEST |
||||||
|
) |
||||||
|
|
||||||
|
# Verify the master key locally |
||||||
|
try: |
||||||
|
master_key = MasterKey.objects.get(key_value=master_key_value, is_active=True) |
||||||
|
except MasterKey.DoesNotExist: |
||||||
|
return Response( |
||||||
|
{'error': 'Invalid or inactive master key'}, |
||||||
|
status=status.HTTP_401_UNAUTHORIZED |
||||||
|
) |
||||||
|
|
||||||
|
# Update last used timestamp |
||||||
|
master_key.last_used = timezone.now() |
||||||
|
master_key.save(update_fields=['last_used']) |
||||||
|
|
||||||
|
# Get user identifier from request or use a default |
||||||
|
user_identifier = request.data.get('username', f'service_user_{master_key.key_id}') |
||||||
|
|
||||||
|
# Get or create a user associated with this master key |
||||||
|
user, created = User.objects.get_or_create( |
||||||
|
username=user_identifier, |
||||||
|
defaults={ |
||||||
|
'is_active': True |
||||||
|
} |
||||||
|
) |
||||||
|
|
||||||
|
# Generate tokens manually |
||||||
|
refresh = RefreshToken.for_user(user) |
||||||
|
|
||||||
|
# Add custom claims from the master key |
||||||
|
refresh['key_id'] = str(master_key.key_id) |
||||||
|
refresh['permissions'] = master_key.permissions |
||||||
|
|
||||||
|
return Response({ |
||||||
|
'refresh': str(refresh), |
||||||
|
'access': str(refresh.access_token), |
||||||
|
}) |
@ -0,0 +1,16 @@ |
|||||||
|
""" |
||||||
|
ASGI config for botzilla project. |
||||||
|
|
||||||
|
It exposes the ASGI callable as a module-level variable named ``application``. |
||||||
|
|
||||||
|
For more information on this file, see |
||||||
|
https://docs.djangoproject.com/en/5.2/howto/deployment/asgi/ |
||||||
|
""" |
||||||
|
|
||||||
|
import os |
||||||
|
|
||||||
|
from django.core.asgi import get_asgi_application |
||||||
|
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'botzilla.settings') |
||||||
|
|
||||||
|
application = get_asgi_application() |
@ -0,0 +1,170 @@ |
|||||||
|
""" |
||||||
|
Django settings for botzilla project. |
||||||
|
|
||||||
|
Generated by 'django-admin startproject' using Django 5.2.1. |
||||||
|
|
||||||
|
For more information on this file, see |
||||||
|
https://docs.djangoproject.com/en/5.2/topics/settings/ |
||||||
|
|
||||||
|
For the full list of settings and their values, see |
||||||
|
https://docs.djangoproject.com/en/5.2/ref/settings/ |
||||||
|
""" |
||||||
|
|
||||||
|
from pathlib import Path |
||||||
|
from datetime import timedelta |
||||||
|
|
||||||
|
# Build paths inside the project like this: BASE_DIR / 'subdir'. |
||||||
|
BASE_DIR = Path(__file__).resolve().parent.parent |
||||||
|
|
||||||
|
|
||||||
|
# Quick-start development settings - unsuitable for production |
||||||
|
# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/ |
||||||
|
|
||||||
|
# SECURITY WARNING: keep the secret key used in production secret! |
||||||
|
SECRET_KEY = 'lpw-7tb-!hvdd(eb^v*e0viuoguhiewjlnkfsui8732fpd93emf02ngocbnob6h!$ihsqv)sv^y5k)j!)m_#+!v' |
||||||
|
|
||||||
|
# SECURITY WARNING: don't run with debug turned on in production! |
||||||
|
DEBUG = True |
||||||
|
|
||||||
|
ALLOWED_HOSTS = [] |
||||||
|
|
||||||
|
|
||||||
|
# Application definition |
||||||
|
|
||||||
|
INSTALLED_APPS = [ |
||||||
|
'django.contrib.admin', |
||||||
|
'django.contrib.auth', |
||||||
|
'django.contrib.contenttypes', |
||||||
|
'django.contrib.sessions', |
||||||
|
'django.contrib.messages', |
||||||
|
'django.contrib.staticfiles', |
||||||
|
'rest_framework', |
||||||
|
'rest_framework_simplejwt', |
||||||
|
'rest_framework_simplejwt.token_blacklist', |
||||||
|
'api', |
||||||
|
|
||||||
|
'drf_yasg', |
||||||
|
] |
||||||
|
|
||||||
|
# REST Framework settings |
||||||
|
REST_FRAMEWORK = { |
||||||
|
'DEFAULT_PERMISSION_CLASSES': [ |
||||||
|
'rest_framework.permissions.IsAuthenticated', |
||||||
|
], |
||||||
|
'DEFAULT_AUTHENTICATION_CLASSES': [ |
||||||
|
'rest_framework_simplejwt.authentication.JWTAuthentication', |
||||||
|
], |
||||||
|
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', |
||||||
|
'PAGE_SIZE': 10, |
||||||
|
} |
||||||
|
|
||||||
|
SIMPLE_JWT = { |
||||||
|
'ACCESS_TOKEN_LIFETIME': timedelta(minutes=60 * 12), |
||||||
|
'REFRESH_TOKEN_LIFETIME': timedelta(days=1), |
||||||
|
'ALGORITHM': 'HS256', |
||||||
|
'SIGNING_KEY': SECRET_KEY, |
||||||
|
'VERIFYING_KEY': None, |
||||||
|
'AUTH_HEADER_TYPES': ('Bearer',), |
||||||
|
'BLACKLIST_AFTER_ROTATION': True, |
||||||
|
} |
||||||
|
|
||||||
|
SWAGGER_SETTINGS = { |
||||||
|
'FAVICON_HREF': '/static/favicon.ico', |
||||||
|
'SECURITY_DEFINITIONS': { |
||||||
|
'Bearer': { |
||||||
|
'type': 'apiKey', |
||||||
|
'name': 'Authorization', |
||||||
|
'in': 'header' |
||||||
|
}, |
||||||
|
}, |
||||||
|
} |
||||||
|
|
||||||
|
REDOC_SETTINGS = { |
||||||
|
'LAZY_RENDERING': True, |
||||||
|
'HIDE_HOSTNAME': False, |
||||||
|
} |
||||||
|
|
||||||
|
MIDDLEWARE = [ |
||||||
|
'django.middleware.security.SecurityMiddleware', |
||||||
|
'django.contrib.sessions.middleware.SessionMiddleware', |
||||||
|
'django.middleware.common.CommonMiddleware', |
||||||
|
'django.middleware.csrf.CsrfViewMiddleware', |
||||||
|
'django.contrib.auth.middleware.AuthenticationMiddleware', |
||||||
|
'django.contrib.messages.middleware.MessageMiddleware', |
||||||
|
'django.middleware.clickjacking.XFrameOptionsMiddleware', |
||||||
|
] |
||||||
|
|
||||||
|
ROOT_URLCONF = 'botzilla.urls' |
||||||
|
|
||||||
|
TEMPLATES = [ |
||||||
|
{ |
||||||
|
'BACKEND': 'django.template.backends.django.DjangoTemplates', |
||||||
|
'DIRS': [ ], |
||||||
|
'APP_DIRS': True, |
||||||
|
'OPTIONS': { |
||||||
|
'context_processors': [ |
||||||
|
'django.template.context_processors.request', |
||||||
|
'django.contrib.auth.context_processors.auth', |
||||||
|
'django.contrib.messages.context_processors.messages', |
||||||
|
], |
||||||
|
}, |
||||||
|
}, |
||||||
|
] |
||||||
|
|
||||||
|
WSGI_APPLICATION = 'botzilla.wsgi.application' |
||||||
|
|
||||||
|
|
||||||
|
# Database |
||||||
|
# https://docs.djangoproject.com/en/5.2/ref/settings/#databases |
||||||
|
|
||||||
|
DATABASES = { |
||||||
|
'default': { |
||||||
|
'ENGINE': 'django.db.backends.sqlite3', |
||||||
|
'NAME': BASE_DIR / 'db.sqlite3', |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
# Password validation |
||||||
|
# https://docs.djangoproject.com/en/5.2/ref/settings/#auth-password-validators |
||||||
|
|
||||||
|
AUTH_PASSWORD_VALIDATORS = [ |
||||||
|
{ |
||||||
|
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', |
||||||
|
}, |
||||||
|
{ |
||||||
|
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', |
||||||
|
}, |
||||||
|
{ |
||||||
|
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', |
||||||
|
}, |
||||||
|
{ |
||||||
|
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', |
||||||
|
}, |
||||||
|
] |
||||||
|
|
||||||
|
|
||||||
|
# Internationalization |
||||||
|
# https://docs.djangoproject.com/en/5.2/topics/i18n/ |
||||||
|
|
||||||
|
LANGUAGE_CODE = 'en-us' |
||||||
|
|
||||||
|
TIME_ZONE = 'UTC' |
||||||
|
|
||||||
|
USE_I18N = True |
||||||
|
|
||||||
|
USE_TZ = True |
||||||
|
|
||||||
|
|
||||||
|
# Static files (CSS, JavaScript, Images) |
||||||
|
# https://docs.djangoproject.com/en/5.2/howto/static-files/ |
||||||
|
|
||||||
|
STATIC_URL = '/static/' |
||||||
|
STATICFILES_DIRS = [ |
||||||
|
BASE_DIR / 'static', |
||||||
|
] |
||||||
|
|
||||||
|
# Default primary key field type |
||||||
|
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field |
||||||
|
|
||||||
|
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' |
@ -0,0 +1,45 @@ |
|||||||
|
""" |
||||||
|
URL configuration for botzilla project. |
||||||
|
|
||||||
|
The `urlpatterns` list routes URLs to views. For more information please see: |
||||||
|
https://docs.djangoproject.com/en/5.2/topics/http/urls/ |
||||||
|
Examples: |
||||||
|
Function views |
||||||
|
1. Add an import: from my_app import views |
||||||
|
2. Add a URL to urlpatterns: path('', views.home, name='home') |
||||||
|
Class-based views |
||||||
|
1. Add an import: from other_app.views import Home |
||||||
|
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') |
||||||
|
Including another URLconf |
||||||
|
1. Import the include() function: from django.urls import include, path |
||||||
|
2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) |
||||||
|
""" |
||||||
|
from django.contrib import admin |
||||||
|
from django.urls import path, include |
||||||
|
from rest_framework import permissions |
||||||
|
from drf_yasg.views import get_schema_view |
||||||
|
from drf_yasg import openapi |
||||||
|
|
||||||
|
schema_view = get_schema_view( |
||||||
|
openapi.Info( |
||||||
|
title="Botzilla API", |
||||||
|
default_version='v1', |
||||||
|
description="Botzilla API documentation", |
||||||
|
terms_of_service="https://www.example.com/terms/", |
||||||
|
contact=openapi.Contact(email="contact@botzilla.ch"), |
||||||
|
license=openapi.License(name="BSD License"), |
||||||
|
x_logo={ |
||||||
|
"url": "/static/logo.svg", |
||||||
|
"backgroundColor": "#FFFFFF", |
||||||
|
"altText": "Botzilla logo" |
||||||
|
} |
||||||
|
), |
||||||
|
public=True, |
||||||
|
permission_classes=[permissions.AllowAny], |
||||||
|
) |
||||||
|
|
||||||
|
urlpatterns = [ |
||||||
|
path('admin/', admin.site.urls), |
||||||
|
path('api/', include('api.urls')), |
||||||
|
path('docs/', schema_view.with_ui('redoc', cache_timeout=0), name='schema-redoc'), |
||||||
|
] |
@ -0,0 +1,16 @@ |
|||||||
|
""" |
||||||
|
WSGI config for botzilla project. |
||||||
|
|
||||||
|
It exposes the WSGI callable as a module-level variable named ``application``. |
||||||
|
|
||||||
|
For more information on this file, see |
||||||
|
https://docs.djangoproject.com/en/5.2/howto/deployment/wsgi/ |
||||||
|
""" |
||||||
|
|
||||||
|
import os |
||||||
|
|
||||||
|
from django.core.wsgi import get_wsgi_application |
||||||
|
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'botzilla.settings') |
||||||
|
|
||||||
|
application = get_wsgi_application() |
@ -0,0 +1,22 @@ |
|||||||
|
#!/usr/bin/env python |
||||||
|
"""Django's command-line utility for administrative tasks.""" |
||||||
|
import os |
||||||
|
import sys |
||||||
|
|
||||||
|
|
||||||
|
def main(): |
||||||
|
"""Run administrative tasks.""" |
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'botzilla.settings') |
||||||
|
try: |
||||||
|
from django.core.management import execute_from_command_line |
||||||
|
except ImportError as exc: |
||||||
|
raise ImportError( |
||||||
|
"Couldn't import Django. Are you sure it's installed and " |
||||||
|
"available on your PYTHONPATH environment variable? Did you " |
||||||
|
"forget to activate a virtual environment?" |
||||||
|
) from exc |
||||||
|
execute_from_command_line(sys.argv) |
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__': |
||||||
|
main() |
@ -0,0 +1,25 @@ |
|||||||
|
annotated-types==0.7.0 |
||||||
|
anyio==4.9.0 |
||||||
|
asgiref==3.8.1 |
||||||
|
certifi==2025.4.26 |
||||||
|
Django==5.2.1 |
||||||
|
djangorestframework==3.16.0 |
||||||
|
djangorestframework_simplejwt==5.5.0 |
||||||
|
drf-yasg==1.21.10 |
||||||
|
h11==0.16.0 |
||||||
|
httpcore==1.0.9 |
||||||
|
httpx==0.28.1 |
||||||
|
idna==3.10 |
||||||
|
inflection==0.5.1 |
||||||
|
ollama==0.4.8 |
||||||
|
packaging==25.0 |
||||||
|
pydantic==2.11.4 |
||||||
|
pydantic_core==2.33.2 |
||||||
|
PyJWT==2.9.0 |
||||||
|
pytz==2025.2 |
||||||
|
PyYAML==6.0.2 |
||||||
|
sniffio==1.3.1 |
||||||
|
sqlparse==0.5.3 |
||||||
|
typing-inspection==0.4.0 |
||||||
|
typing_extensions==4.13.2 |
||||||
|
uritemplate==4.1.1 |
@ -0,0 +1,4 @@ |
|||||||
|
img { |
||||||
|
max-height: 20px; |
||||||
|
width: auto; |
||||||
|
} |
After Width: | Height: | Size: 4.2 KiB |
After Width: | Height: | Size: 1.2 KiB |
Loading…
Reference in New Issue