Browse Source

Implement the ability to use up invites

Getty Ritter 4 years ago
parent
commit
326cae6bcc
3 changed files with 76 additions and 4 deletions
  1. 24 2
      lc/error.py
  2. 18 1
      lc/model.py
  3. 34 1
      tests/model.py

+ 24 - 2
lc/error.py

@@ -62,7 +62,7 @@ class BadPermissions(LCException):
         return f"Insufficient permissions."
 
     def http_code(self) -> int:
-        return 400
+        return 403
 
 
 @dataclass
@@ -73,4 +73,26 @@ class BadContentType(LCException):
         return f"Bad content type for request: {self.content_type}"
 
     def http_code(self) -> int:
-        return 500
+        return 403
+
+
+@dataclass
+class NoSuchInvite(LCException):
+    invite: str
+
+    def __str__(self):
+        return f"No such invite code: {self.invite}."
+
+    def http_code(self) -> int:
+        return 404
+
+
+@dataclass
+class AlreadyUsedInvite(LCException):
+    invite: str
+
+    def __str__(self):
+        return f"Invite code {self.invite} already taken."
+
+    def http_code(self) -> int:
+        return 403

+ 18 - 1
lc/model.py

@@ -56,6 +56,17 @@ class User(Model):
         except peewee.IntegrityError:
             raise e.UserExists(name=user.name)
 
+    @staticmethod
+    def from_invite(user: r.User, token: str) -> "User":
+        invite = UserInvite.by_code(token)
+        if invite.claimed_by is not None or invite.claimed_at is not None:
+            raise e.AlreadyUsedInvite(invite=token)
+        u = User.from_request(user)
+        invite.claimed_at = datetime.datetime.now()
+        invite.claimed_by = u
+        invite.save()
+        return u
+
     def authenticate(self, password: str) -> bool:
         return pwd.verify(password, self.passhash)
 
@@ -185,7 +196,7 @@ class HasTag(Model):
 
 
 class UserInvite(Model):
-    token = peewee.TextField()
+    token = peewee.TextField(unique=True)
 
     created_by = peewee.ForeignKeyField(User, backref="invites")
     created_at = peewee.DateTimeField()
@@ -193,6 +204,12 @@ class UserInvite(Model):
     claimed_by = peewee.ForeignKeyField(User, null=True)
     claimed_at = peewee.DateTimeField(null=True)
 
+    @staticmethod
+    def by_code(token: str) -> "UserInvite":
+        if (u := UserInvite.get_or_none(token=token)):
+            return u
+        raise e.NoSuchInvite(invite=token)
+
     @staticmethod
     def manufacture(creator: User) -> "UserInvite":
         now = datetime.datetime.now()

+ 34 - 1
tests/model.py

@@ -95,8 +95,41 @@ class Testdb:
         u = self.mk_user()
         invite = m.UserInvite.manufacture(u)
 
+        # the invite should reference the user and be unclaimed
         assert invite.created_by.id == u.id
+        assert invite.created_at is not None
+        assert invite.claimed_by is None
+        assert invite.claimed_at is None
 
+        # deserializing the unique token should reveal the encrypted data
         raw_data = c.serializer.loads(invite.token)
-
         assert(raw_data["created_by"] == u.name)
+
+    def test_use_invite(self):
+        u = self.mk_user()
+        initial_invite = m.UserInvite.manufacture(u)
+
+        assert initial_invite.claimed_by is None
+        assert initial_invite.claimed_at is None
+
+        u2 = m.User.from_invite(r.User(name="u2", password="u2"), initial_invite.token)
+
+        invite = m.UserInvite.by_code(initial_invite.token)
+        assert invite.token == initial_invite.token
+        assert invite.created_by.id == u.id
+        assert invite.claimed_by.id == u2.id
+        assert invite.created_at is not None
+        assert invite.claimed_at is not None
+
+    def bad_use_invite(self):
+        initial_invite = m.UserInvite.manufacture(self.mk_user())
+
+        # creating this user claims the invite
+        m.User.from_invite(r.User(name="u2", password="u2"), initial_invite.token)
+
+        # using the invite again raise an error
+        with pytest.raises(e.AlreadyUsedInvite):
+            m.User.from_invite(r.User(name="u3", password="u3"), initial_invite.token)
+
+        with pytest.raises(e.NoSuchInvite):
+            m.User.from_invite(r.User(name="u4", password="u4"), "a-non-existent-token")