From d77dcf62b4005a0f36ef2fa7ba6d3651d2ef38d7 Mon Sep 17 00:00:00 2001 From: Andrew Godwin Date: Sat, 5 Nov 2022 14:17:27 -0600 Subject: Initial commit (users and statuses) --- users/models/__init__.py | 3 ++ users/models/identity.py | 79 ++++++++++++++++++++++++++++++++++++++++++++++ users/models/user.py | 58 ++++++++++++++++++++++++++++++++++ users/models/user_event.py | 22 +++++++++++++ 4 files changed, 162 insertions(+) create mode 100644 users/models/__init__.py create mode 100644 users/models/identity.py create mode 100644 users/models/user.py create mode 100644 users/models/user_event.py (limited to 'users/models') diff --git a/users/models/__init__.py b/users/models/__init__.py new file mode 100644 index 0000000..7032a81 --- /dev/null +++ b/users/models/__init__.py @@ -0,0 +1,3 @@ +from .identity import Identity # noqa +from .user import User # noqa +from .user_event import UserEvent # noqa diff --git a/users/models/identity.py b/users/models/identity.py new file mode 100644 index 0000000..495b4a4 --- /dev/null +++ b/users/models/identity.py @@ -0,0 +1,79 @@ +import base64 +import uuid +from functools import partial + +import urlman +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric import rsa +from django.conf import settings +from django.db import models +from django.utils import timezone + + +def upload_namer(prefix, instance, filename): + """ + Names uploaded images etc. + """ + now = timezone.now() + filename = base64.b32encode(uuid.uuid4().bytes).decode("ascii") + return f"{prefix}/{now.year}/{now.month}/{now.day}/{filename}" + + +class Identity(models.Model): + """ + Represents both local and remote Fediverse identities (actors) + """ + + # The handle includes the domain! + handle = models.CharField(max_length=500, unique=True) + name = models.CharField(max_length=500, blank=True, null=True) + bio = models.TextField(blank=True, null=True) + + profile_image = models.ImageField(upload_to=partial(upload_namer, "profile_images")) + background_image = models.ImageField( + upload_to=partial(upload_namer, "background_images") + ) + + local = models.BooleanField() + users = models.ManyToManyField("users.User", related_name="identities") + private_key = models.TextField(null=True, blank=True) + public_key = models.TextField(null=True, blank=True) + + created = models.DateTimeField(auto_now_add=True) + updated = models.DateTimeField(auto_now=True) + deleted = models.DateTimeField(null=True, blank=True) + + @property + def short_handle(self): + if self.handle.endswith("@" + settings.DEFAULT_DOMAIN): + return self.handle.split("@", 1)[0] + return self.handle + + @property + def domain(self): + return self.handle.split("@", 1)[1] + + def generate_keypair(self): + private_key = rsa.generate_private_key( + public_exponent=65537, + key_size=2048, + ) + self.private_key = private_key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.PKCS8, + encryption_algorithm=serialization.NoEncryption(), + ) + self.public_key = private_key.public_key().public_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PublicFormat.SubjectPublicKeyInfo, + ) + self.save() + + def __str__(self): + return self.name or self.handle + + class urls(urlman.Urls): + view = "/@{self.short_handle}/" + actor = "{view}actor/" + inbox = "{actor}inbox/" + activate = "{view}activate/" diff --git a/users/models/user.py b/users/models/user.py new file mode 100644 index 0000000..de51380 --- /dev/null +++ b/users/models/user.py @@ -0,0 +1,58 @@ +from typing import List + +from django.contrib.auth.models import AbstractBaseUser, BaseUserManager +from django.db import models + + +class UserManager(BaseUserManager): + """ + Custom user manager that understands emails + """ + + def create_user(self, email, password=None): + user = self.create(email=email) + if password: + user.set_password(password) + user.save() + return user + + def create_superuser(self, email, password=None): + user = self.create(email=email, admin=True) + if password: + user.set_password(password) + user.save() + return user + + +class User(AbstractBaseUser): + """ + Custom user model that only needs an email + """ + + email = models.EmailField(unique=True) + + admin = models.BooleanField(default=False) + moderator = models.BooleanField(default=False) + banned = models.BooleanField(default=False) + deleted = models.BooleanField(default=False) + + created = models.DateTimeField(auto_now_add=True) + updated = models.DateTimeField(auto_now=True) + + USERNAME_FIELD = "email" + EMAIL_FIELD = "email" + REQUIRED_FIELDS: List[str] = [] + + objects = UserManager() + + @property + def is_active(self): + return not (self.deleted or self.banned) + + @property + def is_superuser(self): + return self.admin + + @property + def is_staff(self): + return self.admin diff --git a/users/models/user_event.py b/users/models/user_event.py new file mode 100644 index 0000000..858f334 --- /dev/null +++ b/users/models/user_event.py @@ -0,0 +1,22 @@ +from django.db import models + + +class UserEvent(models.Model): + """ + Tracks major events that happen to users + """ + + class EventType(models.TextChoices): + created = "created" + reset_password = "reset_password" + banned = "banned" + + user = models.ForeignKey( + "users.User", + on_delete=models.CASCADE, + related_name="events", + ) + + date = models.DateTimeField(auto_now_add=True) + type = models.CharField(max_length=100, choices=EventType.choices) + data = models.JSONField(blank=True, null=True) -- cgit v1.2.3