-
-
Notifications
You must be signed in to change notification settings - Fork 202
Implement Initial Setup for Google OAuth #2021
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 31 commits
a3ceef9
af62c47
811dc49
90874de
97ffd25
0f3b823
4edeced
2067d79
8e86e73
e9b6b24
9e24628
f57e542
579c0ff
a77d901
675d58f
e68f177
5bf0a79
e7bb5cf
24ff1d0
58a6b29
318f771
f22b935
b997b9e
0a18917
54839ef
f07770e
5820460
52443fe
943e939
68df7de
e1f52d5
cf136a4
8fcc676
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
"""Common API Clients.""" | ||
|
||
from django.conf import settings | ||
from google_auth_oauthlib.flow import Flow | ||
|
||
|
||
def get_google_auth_client(): | ||
"""Get a Google OAuth client.""" | ||
return Flow.from_client_config( | ||
client_config={ | ||
"web": { | ||
"client_id": settings.GOOGLE_AUTH_CLIENT_ID, | ||
"client_secret": settings.GOOGLE_AUTH_CLIENT_SECRET, | ||
"redirect_uris": [settings.GOOGLE_AUTH_REDIRECT_URI], | ||
"auth_uri": settings.GOOGLE_AUTH_AUTH_URI, | ||
"token_uri": settings.GOOGLE_AUTH_TOKEN_URI, | ||
} | ||
}, | ||
scopes=settings.GOOGLE_AUTH_SCOPES, | ||
) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
# Generated by Django 5.2.4 on 2025-08-20 18:53 | ||
|
||
import django.db.models.deletion | ||
from django.db import migrations, models | ||
|
||
|
||
class Migration(migrations.Migration): | ||
dependencies = [ | ||
("nest", "0003_badge"), | ||
("slack", "0023_delete_googleauth"), | ||
] | ||
|
||
operations = [ | ||
migrations.CreateModel( | ||
name="MemberGoogleCredentials", | ||
fields=[ | ||
( | ||
"id", | ||
models.BigAutoField( | ||
auto_created=True, primary_key=True, serialize=False, verbose_name="ID" | ||
), | ||
), | ||
("access_token", models.BinaryField(null=True, verbose_name="Access Token")), | ||
("refresh_token", models.BinaryField(null=True, verbose_name="Refresh Token")), | ||
("expires_at", models.DateTimeField(null=True, verbose_name="Token Expiry")), | ||
( | ||
"member", | ||
models.OneToOneField( | ||
on_delete=django.db.models.deletion.CASCADE, | ||
related_name="google_auth", | ||
to="slack.member", | ||
verbose_name="Slack Member", | ||
), | ||
), | ||
], | ||
options={ | ||
"verbose_name_plural": "Google Auths", | ||
"db_table": "slack_google_auths", | ||
}, | ||
), | ||
] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
# Generated by Django 5.2.4 on 2025-08-20 18:55 | ||
|
||
from django.db import migrations | ||
|
||
|
||
class Migration(migrations.Migration): | ||
dependencies = [ | ||
("nest", "0004_membergooglecredentials"), | ||
] | ||
|
||
operations = [ | ||
migrations.AlterModelOptions( | ||
name="membergooglecredentials", | ||
options={"verbose_name_plural": "Member's Google Credentials"}, | ||
), | ||
migrations.AlterModelTable( | ||
name="membergooglecredentials", | ||
table="nest_member_google_credentials", | ||
), | ||
] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
# Generated by Django 5.2.4 on 2025-08-20 19:08 | ||
|
||
import django.db.models.deletion | ||
from django.db import migrations, models | ||
|
||
|
||
class Migration(migrations.Migration): | ||
dependencies = [ | ||
("nest", "0005_alter_membergooglecredentials_options_and_more"), | ||
("slack", "0023_delete_googleauth"), | ||
] | ||
|
||
operations = [ | ||
migrations.AlterField( | ||
model_name="membergooglecredentials", | ||
name="member", | ||
field=models.OneToOneField( | ||
on_delete=django.db.models.deletion.CASCADE, | ||
related_name="member_google_credentials", | ||
to="slack.member", | ||
verbose_name="Slack Member", | ||
), | ||
), | ||
] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,4 @@ | ||
from .api_key import ApiKey | ||
from .badge import Badge | ||
from .member_google_credentials import MemberGoogleCredentials | ||
from .user import User |
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,131 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
"""Slack Google OAuth Authentication Model.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
from django.conf import settings | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
from django.core.exceptions import ValidationError | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
from django.db import models | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
from django.utils import timezone | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
from google.auth.transport.requests import Request | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
from google.oauth2.credentials import Credentials | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
from apps.common.clients import get_google_auth_client | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
from apps.slack.models.member import Member | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
AUTH_ERROR_MESSAGE = ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
"Google OAuth client ID, secret, and redirect URI must be set in environment variables." | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
class MemberGoogleCredentials(models.Model): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
"""Model to store Google OAuth tokens for Slack integration.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
class Meta: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
db_table = "nest_member_google_credentials" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
verbose_name_plural = "Member's Google Credentials" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
member = models.OneToOneField( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
"slack.Member", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
on_delete=models.CASCADE, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
related_name="member_google_credentials", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
verbose_name="Slack Member", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
access_token = models.BinaryField(verbose_name="Access Token", null=True) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
refresh_token = models.BinaryField(verbose_name="Refresh Token", null=True) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
expires_at = models.DateTimeField( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
verbose_name="Token Expiry", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
null=True, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
@staticmethod | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
def authenticate(member): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
"""Authenticate a member. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Returns: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
- MemberGoogleCredentials instance if a valid/refreshable token exists, or | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
- (authorization_url, state) tuple to complete the OAuth flow. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
if not settings.IS_GOOGLE_AUTH_ENABLED: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
raise ValueError(AUTH_ERROR_MESSAGE) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
auth = MemberGoogleCredentials.objects.get_or_create(member=member)[0] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
if auth.access_token and not auth.is_token_expired: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return auth | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
if auth.access_token: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
# If the access token is present but expired, refresh it | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
MemberGoogleCredentials.refresh_access_token(auth) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return auth | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
# If no access token is present, redirect to Google OAuth | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
flow = MemberGoogleCredentials.get_flow() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
flow.redirect_uri = settings.GOOGLE_AUTH_REDIRECT_URI | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
state = member.slack_user_id | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return flow.authorization_url( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
access_type="offline", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
prompt="consent", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
state=state, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
@staticmethod | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
def authenticate_callback(auth_response, member_id): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
"""Authenticate a member and return a MemberGoogleCredentials instance.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
if not settings.IS_GOOGLE_AUTH_ENABLED: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
raise ValueError(AUTH_ERROR_MESSAGE) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
member = None | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
try: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
member = Member.objects.get(slack_user_id=member_id) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
except Member.DoesNotExist as e: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
error_message = f"Member with Slack ID {member_id} does not exist." | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
raise ValidationError(error_message) from e | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
auth = MemberGoogleCredentials.objects.get_or_create(member=member)[0] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
# This is the first time authentication, so we need to fetch a new token | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
flow = MemberGoogleCredentials.get_flow() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
flow.redirect_uri = settings.GOOGLE_AUTH_REDIRECT_URI | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
flow.fetch_token(authorization_response=auth_response) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
auth.access_token = flow.credentials.token | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
auth.refresh_token = flow.credentials.refresh_token | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
expires_at = flow.credentials.expiry | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
if expires_at and timezone.is_naive(expires_at): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
expires_at = timezone.make_aware(expires_at) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
auth.expires_at = expires_at | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
auth.save() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return auth | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+84
to
+91
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion BinaryField tokens vs Google SDK strings: encode/decode at boundaries Google’s Flow/Credentials return tokens as str, but the model stores tokens in BinaryField. Assigning str to BinaryField and later passing bytes back into Credentials will cause type errors in real usage (the tests use bytes, masking this). Encode on write and decode on use. Apply this diff: - auth.access_token = flow.credentials.token
- auth.refresh_token = flow.credentials.refresh_token
+ access = flow.credentials.token
+ refresh = flow.credentials.refresh_token
+ if isinstance(access, str):
+ access = access.encode("utf-8")
+ if isinstance(refresh, str) and refresh is not None:
+ refresh = refresh.encode("utf-8")
+ auth.access_token = access
+ auth.refresh_token = refresh 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
@staticmethod | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
def get_flow(): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
"""Create a Google OAuth flow instance.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
if not settings.IS_GOOGLE_AUTH_ENABLED: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
raise ValueError(AUTH_ERROR_MESSAGE) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return get_google_auth_client() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
@property | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
def is_token_expired(self): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
"""Check if the access token is expired.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return self.expires_at is None or self.expires_at <= timezone.now() + timezone.timedelta( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
seconds=60 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
@staticmethod | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
def refresh_access_token(auth): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
"""Refresh the access token using the refresh token.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
if not settings.IS_GOOGLE_AUTH_ENABLED: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
raise ValueError(AUTH_ERROR_MESSAGE) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
refresh_error = "Google OAuth refresh token is not set or expired." | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
if not auth.refresh_token: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
raise ValidationError(refresh_error) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
credentials = Credentials( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
token=auth.access_token, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
refresh_token=auth.refresh_token, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
token_uri=settings.GOOGLE_AUTH_TOKEN_URI, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
client_id=settings.GOOGLE_AUTH_CLIENT_ID, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
client_secret=settings.GOOGLE_AUTH_CLIENT_SECRET, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
credentials.refresh(Request()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
auth.access_token = credentials.token | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
auth.refresh_token = credentials.refresh_token | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
auth.expires_at = credentials.expiry | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
auth.save() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+115
to
+127
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Fix: ensure Credentials() receives strings; normalize expiry to aware datetime Two issues:
Apply this diff: - credentials = Credentials(
- token=auth.access_token,
- refresh_token=auth.refresh_token,
+ token_str = (
+ auth.access_token.decode("utf-8")
+ if isinstance(auth.access_token, (bytes, memoryview))
+ else auth.access_token
+ )
+ refresh_str = (
+ auth.refresh_token.decode("utf-8")
+ if isinstance(auth.refresh_token, (bytes, memoryview))
+ else auth.refresh_token
+ )
+ credentials = Credentials(
+ token=token_str,
+ refresh_token=refresh_str,
token_uri=settings.GOOGLE_AUTH_TOKEN_URI,
client_id=settings.GOOGLE_AUTH_CLIENT_ID,
client_secret=settings.GOOGLE_AUTH_CLIENT_SECRET,
)
credentials.refresh(Request())
- auth.access_token = credentials.token
- auth.refresh_token = credentials.refresh_token
- auth.expires_at = credentials.expiry
+ # Store refreshed tokens back as bytes
+ new_token = credentials.token
+ new_refresh = credentials.refresh_token
+ if isinstance(new_token, str):
+ new_token = new_token.encode("utf-8")
+ if isinstance(new_refresh, str) and new_refresh is not None:
+ new_refresh = new_refresh.encode("utf-8")
+ auth.access_token = new_token
+ auth.refresh_token = new_refresh
+ expires_at = credentials.expiry
+ if expires_at and timezone.is_naive(expires_at):
+ expires_at = timezone.make_aware(expires_at)
+ auth.expires_at = expires_at
auth.save() 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
def __str__(self): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
"""Return a string representation of the MemberGoogleCredentials instance.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return f"MemberGoogleCredentials(member={self.member})" |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
# Generated by Django 5.2.4 on 2025-08-11 04:45 | ||
|
||
import django.db.models.deletion | ||
from django.db import migrations, models | ||
|
||
|
||
class Migration(migrations.Migration): | ||
dependencies = [ | ||
("slack", "0018_conversation_sync_messages"), | ||
] | ||
|
||
operations = [ | ||
migrations.CreateModel( | ||
name="GoogleAuth", | ||
fields=[ | ||
( | ||
"id", | ||
models.BigAutoField( | ||
auto_created=True, primary_key=True, serialize=False, verbose_name="ID" | ||
), | ||
), | ||
("access_token", models.TextField(blank=True, verbose_name="Access Token")), | ||
("refresh_token", models.TextField(blank=True, verbose_name="Refresh Token")), | ||
( | ||
ahmedxgouda marked this conversation as resolved.
Show resolved
Hide resolved
|
||
"expires_at", | ||
models.DateTimeField(blank=True, null=True, verbose_name="Token Expiry"), | ||
), | ||
( | ||
"member", | ||
models.OneToOneField( | ||
on_delete=django.db.models.deletion.CASCADE, | ||
related_name="google_auth", | ||
to="slack.member", | ||
verbose_name="Slack Member", | ||
), | ||
), | ||
], | ||
), | ||
] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
# Generated by Django 5.2.4 on 2025-08-12 17:31 | ||
|
||
from django.db import migrations, models | ||
|
||
|
||
class Migration(migrations.Migration): | ||
dependencies = [ | ||
("slack", "0019_googleauth"), | ||
] | ||
|
||
operations = [ | ||
migrations.AlterField( | ||
model_name="googleauth", | ||
name="access_token", | ||
field=models.BinaryField(blank=True, verbose_name="Access Token"), | ||
), | ||
migrations.AlterField( | ||
model_name="googleauth", | ||
name="refresh_token", | ||
field=models.BinaryField(blank=True, verbose_name="Refresh Token"), | ||
), | ||
ahmedxgouda marked this conversation as resolved.
Show resolved
Hide resolved
|
||
] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
# Generated by Django 5.2.4 on 2025-08-13 16:01 | ||
|
||
from django.db import migrations, models | ||
|
||
|
||
class Migration(migrations.Migration): | ||
dependencies = [ | ||
("slack", "0020_alter_googleauth_access_token_and_more"), | ||
] | ||
ahmedxgouda marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
operations = [ | ||
migrations.AlterField( | ||
model_name="googleauth", | ||
name="access_token", | ||
field=models.BinaryField(null=True, verbose_name="Access Token"), | ||
), | ||
migrations.AlterField( | ||
model_name="googleauth", | ||
name="expires_at", | ||
field=models.DateTimeField(null=True, verbose_name="Token Expiry"), | ||
), | ||
migrations.AlterField( | ||
model_name="googleauth", | ||
name="refresh_token", | ||
field=models.BinaryField(null=True, verbose_name="Refresh Token"), | ||
), | ||
] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
# Generated by Django 5.2.4 on 2025-08-16 12:19 | ||
|
||
from django.db import migrations | ||
|
||
|
||
class Migration(migrations.Migration): | ||
dependencies = [ | ||
("slack", "0021_alter_googleauth_access_token_and_more"), | ||
] | ||
|
||
operations = [ | ||
migrations.AlterModelOptions( | ||
name="googleauth", | ||
options={"verbose_name_plural": "Google Auths"}, | ||
), | ||
migrations.AlterModelTable( | ||
name="googleauth", | ||
table="slack_google_auths", | ||
), | ||
] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
# Generated by Django 5.2.4 on 2025-08-20 18:53 | ||
|
||
from django.db import migrations | ||
|
||
|
||
class Migration(migrations.Migration): | ||
dependencies = [ | ||
("slack", "0022_alter_googleauth_options_alter_googleauth_table"), | ||
] | ||
|
||
operations = [ | ||
migrations.DeleteModel( | ||
name="GoogleAuth", | ||
), | ||
] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This one should be at
apps/nest/auth/clients/google.py