diff --git a/assets/pasted-20260209-182712-203381fe.png b/assets/pasted-20260209-182712-203381fe.png new file mode 100644 index 0000000..e59710a Binary files /dev/null and b/assets/pasted-20260209-182712-203381fe.png differ diff --git a/celery.log b/celery.log index 4cefb20..01f6805 100644 --- a/celery.log +++ b/celery.log @@ -152,3 +152,15 @@ you should set broker_connection_retry_on_startup to True. [2026-02-09 04:08:51,673: INFO/ForkPoolWorker-1] Decoded JSON for 15: summary=True, tags=['marketing', 'company', 'productivity', 'sharing'] [2026-02-09 04:08:51,759: INFO/ForkPoolWorker-1] Successfully added tags ['marketing', 'company', 'productivity', 'sharing'] to bookmark 15 [2026-02-09 04:08:51,894: INFO/ForkPoolWorker-1] Task core.tasks.generate_summary[ba68ccad-d413-41cc-8b1c-65384d2792ab] succeeded in 6.7007439360022545s: 'Generated summary and tags for bookmark 15' +[2026-02-09 18:30:44,317: INFO/MainProcess] Task core.tasks.process_bookmark[db2f07fc-75d0-4a11-961e-0a34e1cde744] received +[2026-02-09 18:30:48,712: INFO/ForkPoolWorker-2] HTTP Request: GET https://github.com/pydantic/monty "HTTP/1.1 200 OK" +[2026-02-09 18:30:50,263: INFO/MainProcess] Task core.tasks.generate_summary[944ef664-f321-45b6-82f8-607291008ea6] received +[2026-02-09 18:30:50,374: INFO/ForkPoolWorker-2] Task core.tasks.process_bookmark[db2f07fc-75d0-4a11-961e-0a34e1cde744] succeeded in 5.521026417030953s: 'Processed bookmark 27' +[2026-02-09 18:30:51,857: INFO/ForkPoolWorker-1] Generating summary/tags for bookmark 27... +[2026-02-09 18:31:03,704: INFO/ForkPoolWorker-1] AI Raw Response for 27: { + "summary": "monty is a minimal, secure Python interpreter implemented in Rust, designed for safely executing Python code in AI contexts. The GitHub repository from pydantic provides the codebase and tooling to sandbox and integrate this interpreter for AI agents and applications.", + "tags": ["development", "research", "new", "company"] +} +[2026-02-09 18:31:03,713: INFO/ForkPoolWorker-1] Decoded JSON for 27: summary=True, tags=['development', 'research', 'new', 'company'] +[2026-02-09 18:31:03,987: INFO/ForkPoolWorker-1] Successfully added tags ['development', 'research', 'new', 'company'] to bookmark 27 +[2026-02-09 18:31:04,321: INFO/ForkPoolWorker-1] Task core.tasks.generate_summary[944ef664-f321-45b6-82f8-607291008ea6] succeeded in 13.666462198016234s: 'Generated summary and tags for bookmark 27' diff --git a/config/__pycache__/settings.cpython-311.pyc b/config/__pycache__/settings.cpython-311.pyc index 1dd5fe8..59678c3 100644 Binary files a/config/__pycache__/settings.cpython-311.pyc and b/config/__pycache__/settings.cpython-311.pyc differ diff --git a/config/settings.py b/config/settings.py index 19e2b83..5c78f07 100644 --- a/config/settings.py +++ b/config/settings.py @@ -192,6 +192,7 @@ REST_FRAMEWORK = { 'DEFAULT_AUTHENTICATION_CLASSES': [ 'rest_framework.authentication.SessionAuthentication', 'rest_framework.authentication.BasicAuthentication', + 'core.authentication.APITokenAuthentication', ], 'DEFAULT_FILTER_BACKENDS': ( 'django_filters.rest_framework.DjangoFilterBackend', @@ -212,4 +213,4 @@ CELERY_TASK_EAGER_PROPAGATES = True # Login/Logout Redirects LOGIN_REDIRECT_URL = 'home' -LOGOUT_REDIRECT_URL = 'home' \ No newline at end of file +LOGOUT_REDIRECT_URL = 'home' diff --git a/core/__pycache__/admin.cpython-311.pyc b/core/__pycache__/admin.cpython-311.pyc index 9e20c27..4827136 100644 Binary files a/core/__pycache__/admin.cpython-311.pyc and b/core/__pycache__/admin.cpython-311.pyc differ diff --git a/core/__pycache__/authentication.cpython-311.pyc b/core/__pycache__/authentication.cpython-311.pyc new file mode 100644 index 0000000..750125a Binary files /dev/null and b/core/__pycache__/authentication.cpython-311.pyc differ diff --git a/core/__pycache__/models.cpython-311.pyc b/core/__pycache__/models.cpython-311.pyc index 991a04b..9b27003 100644 Binary files a/core/__pycache__/models.cpython-311.pyc and b/core/__pycache__/models.cpython-311.pyc differ diff --git a/core/__pycache__/serializers.cpython-311.pyc b/core/__pycache__/serializers.cpython-311.pyc index 198adfc..4e849a3 100644 Binary files a/core/__pycache__/serializers.cpython-311.pyc and b/core/__pycache__/serializers.cpython-311.pyc differ diff --git a/core/admin.py b/core/admin.py index 0c091ca..6b39a54 100644 --- a/core/admin.py +++ b/core/admin.py @@ -1,5 +1,5 @@ from django.contrib import admin -from .models import Team, TeamMembership, Bookmark, BookmarkShare, Extraction, Summary +from .models import Team, TeamMembership, Bookmark, BookmarkShare, Extraction, Summary, APIToken class TeamMembershipInline(admin.TabularInline): model = TeamMembership @@ -27,4 +27,11 @@ class ExtractionAdmin(admin.ModelAdmin): @admin.register(Summary) class SummaryAdmin(admin.ModelAdmin): - list_display = ('bookmark', 'generated_at') \ No newline at end of file + list_display = ('bookmark', 'generated_at') + +@admin.register(APIToken) +class APITokenAdmin(admin.ModelAdmin): + list_display = ('name', 'user', 'created_at', 'last_used_at', 'is_active') + list_filter = ('is_active', 'created_at', 'last_used_at') + search_fields = ('name', 'user__username', 'token') + readonly_fields = ('token', 'created_at', 'last_used_at') diff --git a/core/authentication.py b/core/authentication.py new file mode 100644 index 0000000..abc87fb --- /dev/null +++ b/core/authentication.py @@ -0,0 +1,35 @@ +from rest_framework.authentication import BaseAuthentication +from rest_framework.exceptions import AuthenticationFailed +from django.utils import timezone +from .models import APIToken + +class APITokenAuthentication(BaseAuthentication): + def authenticate(self, request): + auth_header = request.headers.get('Authorization') + if not auth_header: + return None + + try: + # Support "Bearer " and "Token " + prefix, token = auth_header.split() + if prefix.lower() not in ['bearer', 'token']: + return None + except ValueError: + return None + + return self._authenticate_credentials(token) + + def _authenticate_credentials(self, key): + try: + token = APIToken.objects.select_related('user').get(token=key, is_active=True) + except APIToken.DoesNotExist: + raise AuthenticationFailed('Invalid or inactive API token') + + if not token.user.is_active: + raise AuthenticationFailed('User is inactive or deleted') + + # Update last used timestamp + token.last_used_at = timezone.now() + token.save(update_fields=['last_used_at']) + + return (token.user, token) diff --git a/core/migrations/0002_apitoken.py b/core/migrations/0002_apitoken.py new file mode 100644 index 0000000..c11166c --- /dev/null +++ b/core/migrations/0002_apitoken.py @@ -0,0 +1,28 @@ +# Generated by Django 5.2.7 on 2026-02-09 18:35 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='APIToken', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('token', models.CharField(editable=False, max_length=64, unique=True)), + ('name', models.CharField(default='Default Token', help_text="A recognizable name for this token (e.g. 'Mobile App')", max_length=255)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('last_used_at', models.DateTimeField(blank=True, null=True)), + ('is_active', models.BooleanField(default=True)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='api_tokens', to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/core/migrations/__pycache__/0002_apitoken.cpython-311.pyc b/core/migrations/__pycache__/0002_apitoken.cpython-311.pyc new file mode 100644 index 0000000..ef6d7ef Binary files /dev/null and b/core/migrations/__pycache__/0002_apitoken.cpython-311.pyc differ diff --git a/core/models.py b/core/models.py index 901c8e1..072391c 100644 --- a/core/models.py +++ b/core/models.py @@ -1,6 +1,7 @@ from django.db import models from django.contrib.auth.models import User from taggit.managers import TaggableManager +import secrets class Team(models.Model): name = models.CharField(max_length=255) @@ -66,3 +67,19 @@ class Summary(models.Model): def __str__(self): return f"Summary for {self.bookmark}" + +class APIToken(models.Model): + user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='api_tokens') + token = models.CharField(max_length=64, unique=True, editable=False) + name = models.CharField(max_length=255, help_text="A recognizable name for this token (e.g. 'Mobile App')", default="Default Token") + created_at = models.DateTimeField(auto_now_add=True) + last_used_at = models.DateTimeField(null=True, blank=True) + is_active = models.BooleanField(default=True) + + def save(self, *args, **kwargs): + if not self.token: + self.token = secrets.token_hex(20) + super().save(*args, **kwargs) + + def __str__(self): + return f"{self.name} - {self.user.username}" \ No newline at end of file diff --git a/core/serializers.py b/core/serializers.py index 32a5784..8f3d2de 100644 --- a/core/serializers.py +++ b/core/serializers.py @@ -1,4 +1,5 @@ from rest_framework import serializers +from django.db import transaction from django.contrib.auth.models import User from core.models import Bookmark, Team, TeamMembership, BookmarkShare, Extraction, Summary from taggit.serializers import TagListSerializerField, TaggitSerializer @@ -35,10 +36,19 @@ class BookmarkSerializer(TaggitSerializer, serializers.ModelSerializer): def create(self, validated_data): user = self.context['request'].user tags = validated_data.pop('tags', []) - bookmark = Bookmark.objects.create(user=user, **validated_data) - bookmark.tags.set(tags) + with transaction.atomic(): + bookmark = Bookmark.objects.create(user=user, **validated_data) + bookmark.tags.set(tags) return bookmark + def update(self, instance, validated_data): + tags = validated_data.pop('tags', None) + with transaction.atomic(): + instance = super().update(instance, validated_data) + if tags is not None: + instance.tags.set(tags) + return instance + class BookmarkDetailSerializer(BookmarkSerializer): extraction = ExtractionSerializer(read_only=True) diff --git a/core/templates/core/index.html b/core/templates/core/index.html index 9438c95..0a3bb5c 100644 --- a/core/templates/core/index.html +++ b/core/templates/core/index.html @@ -58,50 +58,71 @@ -
+
{% for bookmark in bookmarks %} -
-
-
-
-
-
-
- {{ bookmark.title|default:bookmark.url }} -
- {% if bookmark.is_favorite %} - - {% endif %} -
-

{{ bookmark.url }}

- - {% if bookmark.notes %} -

{{ bookmark.notes|truncatewords:40 }}

- {% endif %} - -
- {% if bookmark.summary %} -

{{ bookmark.summary.content|truncatewords:50 }}

+
+
+ -
- {% for tag in bookmark.tags.all %} - #{{ tag.name }} - {% endfor %} -
+
+ + {{ bookmark.url }} + +
+ +
+ {% if bookmark.notes %} +

{{ bookmark.notes|truncatewords:20 }}

+ {% endif %} + +
+ {% if bookmark.summary %} +

+ {{ bookmark.summary.content|truncatewords:20 }} +

+ {% else %} +
+
+ Generating... +
+ {% endif %}
-
- + +
+
+ {% for tag in bookmark.tags.all|slice:":3" %} + #{{ tag.name }} + {% endfor %} + {% if bookmark.tags.all.count > 3 %} + +{{ bookmark.tags.all.count|add:"-3" }} + {% endif %} +
+ +
+ + {{ bookmark.created_at|date:"M d, Y" }} + + + - {{ bookmark.created_at|date:"M d" }}
@@ -134,7 +154,7 @@
{% if is_paginated %} -