aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorstuppie2026-10-27 12:02:01 -0600
committerstuppie2026-10-27 12:02:01 -0600
commit1c73e7f2338dadc327b88c580a57298568bc254c (patch)
tree0c23cbf1030ae55f74e1c95e827754c12abe765f
parenta0e32d620ec91cd5e5a877c3220eeb176de8b07b (diff)
downloadgeneralresearch-1c73e7f2338dadc327b88c580a57298568bc254c.tar.gz
generalresearch-1c73e7f2338dadc327b88c580a57298568bc254c.zip
gr user: add name and avatar_url. sub nullable and remove sub/claims stuff from managers
-rw-r--r--generalresearch/gr_django/migrations/0004_user_avatar_url_user_name_alter_user_sub.py28
-rw-r--r--generalresearch/gr_django/models.py8
-rw-r--r--generalresearch/managers/gr/authentication.py125
-rw-r--r--generalresearch/models/gr/authentication.py103
4 files changed, 75 insertions, 189 deletions
diff --git a/generalresearch/gr_django/migrations/0004_user_avatar_url_user_name_alter_user_sub.py b/generalresearch/gr_django/migrations/0004_user_avatar_url_user_name_alter_user_sub.py
new file mode 100644
index 0000000..586b5f6
--- /dev/null
+++ b/generalresearch/gr_django/migrations/0004_user_avatar_url_user_name_alter_user_sub.py
@@ -0,0 +1,28 @@
+# Generated by Django 6.1.1 on 2026-10-27 18:01
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('common', '0003_user_email_alter_membership_owner_and_more'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='user',
+ name='avatar_url',
+ field=models.URLField(null=True),
+ ),
+ migrations.AddField(
+ model_name='user',
+ name='name',
+ field=models.CharField(max_length=200, null=True),
+ ),
+ migrations.AlterField(
+ model_name='user',
+ name='sub',
+ field=models.CharField(max_length=200, null=True),
+ ),
+ ]
diff --git a/generalresearch/gr_django/models.py b/generalresearch/gr_django/models.py
index 7f26eaf..7d2b7b1 100644
--- a/generalresearch/gr_django/models.py
+++ b/generalresearch/gr_django/models.py
@@ -5,8 +5,10 @@ from django.db import models
class User(models.Model):
id = models.BigAutoField(primary_key=True)
- sub = models.CharField(max_length=200, blank=False, null=False)
+ sub = models.CharField(max_length=200, blank=False, null=True)
email = models.EmailField(null=True, unique=True)
+ name = models.CharField(max_length=200, null=True)
+ avatar_url = models.URLField(null=True)
is_superuser = models.BooleanField(default=False)
date_joined = models.DateTimeField(auto_now_add=True)
@@ -50,11 +52,11 @@ class Membership(models.Model):
class Meta:
constraints = [
models.UniqueConstraint(
- fields=['user_id', 'team_id'],
- name='uniq_membership_user_team'
+ fields=["user_id", "team_id"], name="uniq_membership_user_team"
)
]
+
class Team(models.Model):
"""
This is a collection of users / accounts that work as a team and share
diff --git a/generalresearch/managers/gr/authentication.py b/generalresearch/managers/gr/authentication.py
index 18c8ba4..8e3b84e 100644
--- a/generalresearch/managers/gr/authentication.py
+++ b/generalresearch/managers/gr/authentication.py
@@ -4,29 +4,27 @@ import binascii
import logging
import os
from datetime import UTC, datetime
-from typing import TYPE_CHECKING, Any
+from typing import TYPE_CHECKING
from psycopg import sql
-from pydantic import AnyHttpUrl, PositiveInt
+from pydantic import PositiveInt
from generalresearch.managers.base import PostgresManager, PostgresManagerWithRedis
from generalresearch.models.custom_types import UUIDStr
if TYPE_CHECKING:
-
from generalresearch.models.gr.authentication import GRToken, GRUser
from generalresearch.pg_helper import PostgresConfig
- from generalresearch.redis_helper import RedisConfig
LOG = logging.getLogger("gr")
class GRUserManager(PostgresManagerWithRedis):
-
def create(
self,
- sub: str,
- email: str | None = None,
+ email: str,
+ name: str,
+ avatar_url: str | None = None,
is_superuser: bool = False,
) -> GRUser:
from generalresearch.models.gr.authentication import GRUser
@@ -35,8 +33,9 @@ class GRUserManager(PostgresManagerWithRedis):
instance = GRUser.model_validate(
{
- "sub": sub,
"email": email,
+ "name": name,
+ "avatar_url": avatar_url,
"is_superuser": is_superuser,
"date_joined": now,
}
@@ -46,10 +45,12 @@ class GRUserManager(PostgresManagerWithRedis):
with self.pg_config.make_connection() as conn:
with conn.cursor() as c:
query = sql.SQL("""
- INSERT INTO gr_user
- (sub, email, is_superuser, date_joined)
- VALUES (%(sub)s, %(email)s, %(is_superuser)s, %(date_joined)s)
- RETURNING id
+ INSERT INTO gr_user (
+ email, name, is_superuser, date_joined, avatar_url
+ ) VALUES (
+ %(email)s, %(name)s, %(is_superuser)s,
+ %(date_joined)s, %(avatar_url)s
+ ) RETURNING id;
""")
c.execute(query=query, params=data)
gr_user_id: int = c.fetchone()["id"]
@@ -62,7 +63,6 @@ class GRUserManager(PostgresManagerWithRedis):
self,
*,
gr_user_id: int | None = None,
- sub: str | None = None,
email: str | None = None,
) -> list[GRUser]:
from generalresearch.models.gr.authentication import GRUser
@@ -72,19 +72,16 @@ class GRUserManager(PostgresManagerWithRedis):
if gr_user_id is not None:
conditions.append("u.id = %(gr_user_id)s")
- params['gr_user_id'] = gr_user_id
- if sub is not None:
- conditions.append("u.sub = %(sub)s")
- params['sub'] = sub
+ params["gr_user_id"] = gr_user_id
if email is not None:
conditions.append("u.email = %(email)s")
- params['email'] = email
+ params["email"] = email
filter_str = " AND ".join(conditions)
filter_str = "WHERE " + filter_str if filter_str else ""
query = f"""
- SELECT u.*
+ SELECT u.id, u.name, u.email, u.is_superuser, u.date_joined, u.avatar_url
FROM gr_user AS u
{filter_str}
"""
@@ -108,26 +105,11 @@ class GRUserManager(PostgresManagerWithRedis):
def get_by_id(self, gr_user_id: int) -> GRUser | None:
return self._get_one(gr_user_id=gr_user_id)
- def get_by_sub(self, sub: str, raises=True) -> GRUser | None:
- return self._get_one(sub=sub, raises=raises)
-
def get_by_email(self, email: str, raises=True) -> GRUser | None:
return self._get_one(email=email, raises=raises)
- def get_by_sub_or_create(self, sub: str) -> GRUser:
- return self.get_by_sub(sub=sub, raises=False) or self.create(sub=sub)
-
def get_all(self) -> list[GRUser]:
- from generalresearch.models.gr.authentication import GRUser
-
- with self.pg_config.make_connection() as conn, conn.cursor() as c:
- c.execute(query="""
- SELECT u.*
- FROM gr_user AS u
- """)
- res = c.fetchall()
-
- return [GRUser.from_postgresql(i) for i in res]
+ return self.filter()
def get_by_team(self, team_id: PositiveInt) -> list[GRUser]:
from generalresearch.models.gr.authentication import GRUser
@@ -135,21 +117,17 @@ class GRUserManager(PostgresManagerWithRedis):
with self.pg_config.make_connection() as conn, conn.cursor() as c:
c.execute(
query="""
- SELECT gru.*
+ SELECT u.id, u.name, u.email, u.avatar_url,
+ u.is_superuser, u.date_joined
FROM common_membership AS membership
- INNER JOIN gr_user AS gru
- ON gru.id = membership.user_id
+ INNER JOIN gr_user AS u
+ ON u.id = membership.user_id
WHERE membership.team_id = %s
""",
params=(team_id,),
)
res = c.fetchall()
- for item in res:
- for k, v in item.items():
- if isinstance(v, datetime):
- item[k] = item[k].replace(tzinfo=UTC)
-
return [GRUser.model_validate(item) for item in res]
def list_product_uuids(
@@ -171,14 +149,9 @@ class GRUserManager(PostgresManagerWithRedis):
class GRTokenManager(PostgresManager):
-
def get_by_key(
self,
api_key: str,
- jwks: dict[str, Any] | None = None,
- audience: str | None = None,
- issuer: AnyHttpUrl | str | None = None,
- gr_redis_config: RedisConfig | None = None,
) -> GRToken:
"""Return the GRToken for this API Token.
@@ -191,46 +164,16 @@ class GRTokenManager(PostgresManager):
:return GRToken instance (minified version, no relationships)
:raises NotFoundException
"""
- from generalresearch.models.gr.authentication import Claims, GRToken
-
- # SSO Key
- if GRToken.is_sso(api_key):
- from jose import jwt
-
- payload = jwt.decode(
- token=api_key,
- key=jwks,
- algorithms=["RS256"],
- audience=audience,
- issuer=issuer,
- )
- claims = Claims.model_validate(payload)
-
- gr_um = GRUserManager(
- pg_config=self.pg_config, redis_config=gr_redis_config
- )
- gr_user = gr_um.get_by_sub_or_create(sub=claims.subject)
- gr_user.claims = claims
-
- gr_token = GRToken.model_validate(
- {
- "key": api_key,
- "user_id": gr_user.id,
- "user": gr_user,
- "created": datetime.now(tz=UTC),
- }
- )
-
- return gr_token
+ from generalresearch.models.gr.authentication import GRToken
# API Key
with self.pg_config.make_connection() as conn, conn.cursor() as c:
query = sql.SQL("""
- SELECT grk.*
- FROM gr_token AS grk
- WHERE grk.key = %s
- LIMIT 1
- """)
+ SELECT grk.*
+ FROM gr_token AS grk
+ WHERE grk.key = %s
+ LIMIT 2
+ """)
c.execute(query=query, params=(api_key,))
res = c.fetchall()
@@ -240,9 +183,7 @@ class GRTokenManager(PostgresManager):
if len(res) > 1:
raise ValueError(f"Too many GRUsers found with token of '{api_key}'")
- item = res[0]
-
- return GRToken.model_validate(item)
+ return GRToken.model_validate(res[0])
def create(self, user_id: PositiveInt) -> None:
# Taken directly from the DRF Token
@@ -285,18 +226,10 @@ class GRTokenManager(PostgresManager):
WHERE u.id = %s
LIMIT 1;
""")
-
c.execute(query=query, params=(user_id,))
-
result = c.fetchall()
if not result:
return None
- res = result[0]
-
- for k in res:
- if isinstance(res[k], datetime):
- res[k] = res[k].replace(tzinfo=UTC)
-
- return GRToken.model_validate(res)
+ return GRToken.model_validate(result[0])
diff --git a/generalresearch/models/gr/authentication.py b/generalresearch/models/gr/authentication.py
index 5366db8..3c7c461 100644
--- a/generalresearch/models/gr/authentication.py
+++ b/generalresearch/models/gr/authentication.py
@@ -7,17 +7,15 @@ from datetime import UTC, datetime
from typing import TYPE_CHECKING, Any
from pydantic import (
- AnyHttpUrl,
BaseModel,
ConfigDict,
EmailStr,
Field,
- NonNegativeInt,
PositiveInt,
field_validator,
)
-from generalresearch.models.custom_types import AwareDatetimeISO, UUIDStr
+from generalresearch.models.custom_types import AwareDatetimeISO, HttpsUrlStr, UUIDStr
if TYPE_CHECKING:
from generalresearch.managers.gr.business import BusinessManager
@@ -30,76 +28,10 @@ if TYPE_CHECKING:
from generalresearch.redis_helper import RedisConfig
-class Claims(BaseModel):
- iss: str | None = Field(
- default=None,
- description="Issuer: https://www.rfc-editor.org/rfc/rfc7519.html#section-4.1.1",
- )
-
- sub: str | None = Field(
- default=None,
- description="Subject: https://www.rfc-editor.org/rfc/rfc7519.html#section-4.1.2",
- )
-
- aud: str | None = Field(
- default=None,
- description="Audience: https://www.rfc-editor.org/rfc/rfc7519.html#section-4.1.3",
- )
-
- exp: NonNegativeInt | None = Field(
- default=None,
- description="Expiration time: https://www.rfc-editor.org/rfc/rfc7519.html#section-4.1.4",
- )
-
- iat: NonNegativeInt | None = Field(
- default=None,
- description="Issued at: https://www.rfc-editor.org/rfc/rfc7519.html#section-4.1.6",
- )
-
- auth_time: NonNegativeInt | None = Field(
- default=None,
- description="When authentication occured: https://openid.net/specs/openid-connect-core-1_0.html#IDToken",
- )
-
- acr: str | None = Field(
- default=None,
- description="Authentication Context Class Reference: https://openid.net/specs/openid-connect-core-1_0.html#IDToken",
- )
-
- amr: list[str] | None = Field(
- default=None,
- description="Authentication Methods References: https://openid.net/specs/openid-connect-core-1_0.html#IDToken",
- )
-
- c_hash: str | None = Field(
- default=None,
- description="Code hash value: http://openid.net/specs/openid-connect-core-1_0.html",
- )
-
- nonce: str | None = Field(
- default=None,
- description="Value used to associate a Client session with an ID Token: http://openid.net/specs/openid-connect-core-1_0.html",
- )
-
- at_hash: str | None = Field(
- default=None,
- description="Access Token hash value: http://openid.net/specs/openid-connect-core-1_0.html",
- )
-
- sid: str | None = Field(
- default=None,
- description="Session ID: https://openid.net/specs/openid-connect-frontchannel-1_0.html#ClaimsContents",
- )
-
- # --- Properties ---
-
- @property
- def subject(self):
- return self.sub
-
-
class GRUserRef(BaseModel):
email: EmailStr | None = Field(default=None)
+ name: str | None = Field(default=None)
+ avatar_url: HttpsUrlStr | None = Field(default=None)
date_joined: AwareDatetimeISO = Field(
description="When the GR User account signed up."
)
@@ -113,8 +45,10 @@ class GRUser(BaseModel):
)
id: PositiveInt | None = Field(default=None)
- sub: str | None = Field(max_length=200)
email: EmailStr | None = Field(default=None)
+ name: str | None = Field(default=None)
+ avatar_url: HttpsUrlStr | None = Field(default=None)
+
is_superuser: bool = Field(default=False)
date_joined: AwareDatetimeISO = Field(
@@ -126,28 +60,15 @@ class GRUser(BaseModel):
teams: list[Team] | None = Field(default=None)
products: list[Product] | None = Field(default=None)
token: GRToken | None = Field(default=None)
- claims: Claims | None = Field(default=None)
def to_user_ref(self) -> GRUserRef:
return GRUserRef(
email=self.email,
+ name=self.name,
+ avatar_url=self.avatar_url,
date_joined=self.date_joined,
)
- def prefetch_claims(
- self, token: str, key: dict[str, Any], audience: str, issuer: AnyHttpUrl
- ) -> None:
- from jose import jwt
-
- payload = jwt.decode(
- token=token,
- key=key,
- algorithms=["RS256"],
- audience=audience,
- issuer=issuer,
- )
- self.claims = Claims.model_validate(payload)
-
def prefetch_businesses(
self, pg_config: PostgresConfig, redis_config: RedisConfig
) -> None:
@@ -158,6 +79,7 @@ class GRUser(BaseModel):
if self.is_superuser:
self.businesses = bm.get_all()
else:
+ assert self.id is not None
self.businesses = bm.get_by_user_id(user_id=self.id)
def prefetch_teams(
@@ -206,6 +128,7 @@ class GRUser(BaseModel):
)
tm = GRTokenManager(pg_config=pg_config)
+ assert self.id is not None
self.token = tm.get_by_user_id(user_id=self.id)
def __eq__(self, other: GRUser) -> bool:
@@ -242,9 +165,9 @@ class GRUser(BaseModel):
team_manager: TeamManager | None,
product_manager: ProductManager,
) -> list[UUIDStr]:
- assert (
- business_manager is not None or team_manager is not None
- ), "At least one of business_manager or team_manager is required"
+ assert business_manager is not None or team_manager is not None, (
+ "At least one of business_manager or team_manager is required"
+ )
return product_manager.get_uuids(
business_uuids=(