diff --git a/backend/apps/accounts/migrations/0002_user_e621_api_key_user_e621_base_url_and_more.py b/backend/apps/accounts/migrations/0002_user_e621_api_key_user_e621_base_url_and_more.py
new file mode 100644
index 0000000..1eadaef
--- /dev/null
+++ b/backend/apps/accounts/migrations/0002_user_e621_api_key_user_e621_base_url_and_more.py
@@ -0,0 +1,28 @@
+# Generated by Django 6.1.1 on 2026-09-17 13:47
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('accounts', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='user',
+ name='e621_api_key',
+ field=models.CharField(blank=True, default='', max_length=100),
+ ),
+ migrations.AddField(
+ model_name='user',
+ name='e621_base_url',
+ field=models.CharField(default='https://e621.net', max_length=200),
+ ),
+ migrations.AddField(
+ model_name='user',
+ name='e621_username',
+ field=models.CharField(blank=True, default='', max_length=100),
+ ),
+ ]
diff --git a/backend/apps/accounts/models.py b/backend/apps/accounts/models.py
index 5e8894d..7878011 100644
--- a/backend/apps/accounts/models.py
+++ b/backend/apps/accounts/models.py
@@ -1,5 +1,14 @@
from django.contrib.auth.models import AbstractUser
+from django.db import models
class User(AbstractUser):
- """Project user. Extended later with avatars and e621 credentials."""
+ """Project user with optional e621 API credentials."""
+
+ e621_username = models.CharField(max_length=100, blank=True, default="")
+ e621_api_key = models.CharField(max_length=100, blank=True, default="")
+ e621_base_url = models.CharField(max_length=200, default="https://e621.net")
+
+ @property
+ def e621_configured(self):
+ return bool(self.e621_username and self.e621_api_key)
diff --git a/backend/apps/accounts/serializers.py b/backend/apps/accounts/serializers.py
index 2ffe1c2..5d4c6c1 100644
--- a/backend/apps/accounts/serializers.py
+++ b/backend/apps/accounts/serializers.py
@@ -5,9 +5,19 @@ from .models import User
class UserSerializer(serializers.ModelSerializer):
+ e621_configured = serializers.BooleanField(read_only=True)
+
class Meta:
model = User
- fields = ["id", "username", "email", "is_staff", "date_joined"]
+ fields = [
+ "id",
+ "username",
+ "email",
+ "is_staff",
+ "date_joined",
+ "e621_username",
+ "e621_configured",
+ ]
read_only_fields = fields
@@ -20,3 +30,13 @@ class RegisterSerializer(serializers.ModelSerializer):
def create(self, validated_data):
return User.objects.create_user(**validated_data)
+
+
+class E621CredentialsSerializer(serializers.Serializer):
+ username = serializers.CharField(max_length=100, allow_blank=True)
+ api_key = serializers.CharField(
+ max_length=100, allow_blank=True, trim_whitespace=True
+ )
+ base_url = serializers.URLField(
+ max_length=200, required=False, allow_blank=True
+ )
diff --git a/backend/apps/accounts/urls.py b/backend/apps/accounts/urls.py
index 2d538f6..0f4597a 100644
--- a/backend/apps/accounts/urls.py
+++ b/backend/apps/accounts/urls.py
@@ -1,11 +1,12 @@
from django.urls import path
from rest_framework.authtoken.views import obtain_auth_token
-from .views import LogoutView, MeView, RegisterView
+from .views import E621CredentialsView, LogoutView, MeView, RegisterView
urlpatterns = [
path("register/", RegisterView.as_view(), name="register"),
path("token/", obtain_auth_token, name="login"),
path("logout/", LogoutView.as_view(), name="logout"),
path("me/", MeView.as_view(), name="me"),
+ path("e621/", E621CredentialsView.as_view(), name="e621_credentials"),
]
diff --git a/backend/apps/accounts/views.py b/backend/apps/accounts/views.py
index 0c284db..ad92d31 100644
--- a/backend/apps/accounts/views.py
+++ b/backend/apps/accounts/views.py
@@ -4,7 +4,11 @@ from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
-from .serializers import RegisterSerializer, UserSerializer
+from .serializers import (
+ E621CredentialsSerializer,
+ RegisterSerializer,
+ UserSerializer,
+)
class RegisterView(APIView):
@@ -35,3 +39,35 @@ class LogoutView(APIView):
if request.auth:
request.auth.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
+
+
+class E621CredentialsView(APIView):
+ """Store the e621 credentials used by the user's browser to call e621 directly."""
+
+ permission_classes = [IsAuthenticated]
+
+ def _payload(self, user):
+ return {
+ "username": user.e621_username,
+ "api_key": user.e621_api_key,
+ "base_url": user.e621_base_url,
+ "configured": user.e621_configured,
+ }
+
+ def get(self, request):
+ return Response(self._payload(request.user))
+
+ def put(self, request):
+ serializer = E621CredentialsSerializer(data=request.data)
+ serializer.is_valid(raise_exception=True)
+ data = serializer.validated_data
+ user = request.user
+ user.e621_username = data["username"].strip()
+ user.e621_api_key = data["api_key"].strip()
+ user.e621_base_url = (
+ (data.get("base_url") or "https://e621.net").strip().rstrip("/")
+ )
+ user.save(
+ update_fields=["e621_username", "e621_api_key", "e621_base_url"]
+ )
+ return Response(self._payload(user))
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 2fcffa4..fbf738c 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -3,6 +3,7 @@ import { Navigate, Route, Routes, useLocation } from "react-router-dom";
import { AppShell } from "@/components/AppShell";
import { FullPageSpinner } from "@/components/ui";
+import AccountPage from "@/features/account/AccountPage";
import LoginPage from "@/features/auth/LoginPage";
import RegisterPage from "@/features/auth/RegisterPage";
import FileDetailPage from "@/features/library/FileDetailPage";
@@ -44,6 +45,14 @@ export default function App() {
}
/>
+
+
+
+ }
+ />
} />
} />
diff --git a/frontend/src/components/AppShell.tsx b/frontend/src/components/AppShell.tsx
index d523c60..14c600f 100644
--- a/frontend/src/components/AppShell.tsx
+++ b/frontend/src/components/AppShell.tsx
@@ -100,14 +100,18 @@ export function AppShell() {
{user ? (
<>
-
+
{user.username.slice(0, 1).toUpperCase()}
{user.username}
-
+