Skip to content
Closed
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
a3ceef9
Add google libs
ahmedxgouda Aug 6, 2025
af62c47
Add model
ahmedxgouda Aug 7, 2025
811dc49
Merge branch 'main' into nestbot-calendar/initial-google-auth
ahmedxgouda Aug 9, 2025
90874de
Update logic and add tests
ahmedxgouda Aug 9, 2025
97ffd25
Update tests, apply make-check and suggestions
ahmedxgouda Aug 9, 2025
0f3b823
Skip sonar and apply rabbit suggestion
ahmedxgouda Aug 9, 2025
4edeced
Update poetry lock
ahmedxgouda Aug 9, 2025
2067d79
Merge branch 'main' into nestbot-calendar/initial-google-auth
ahmedxgouda Aug 10, 2025
8e86e73
Merge branch 'main' into nestbot-calendar/initial-google-auth
ahmedxgouda Aug 11, 2025
e9b6b24
Add migrations
ahmedxgouda Aug 11, 2025
9e24628
Change user to member
ahmedxgouda Aug 11, 2025
f57e542
Update env.example
ahmedxgouda Aug 12, 2025
579c0ff
Merge branch 'main' into nestbot-calendar/initial-google-auth
ahmedxgouda Aug 12, 2025
a77d901
Update poetry.lock
ahmedxgouda Aug 12, 2025
675d58f
Update tokens to binary field
ahmedxgouda Aug 12, 2025
e68f177
Improve auth logic
ahmedxgouda Aug 13, 2025
5bf0a79
Apply migrations
ahmedxgouda Aug 13, 2025
e7bb5cf
Apply rabbit's suggestions
ahmedxgouda Aug 13, 2025
24ff1d0
Separate google client from slack.GoogleAuth model
ahmedxgouda Aug 13, 2025
58a6b29
Convert singleton to factory
ahmedxgouda Aug 13, 2025
318f771
Update auth_uri
ahmedxgouda Aug 13, 2025
f22b935
Apply suggestions
ahmedxgouda Aug 13, 2025
b997b9e
Add meta class
ahmedxgouda Aug 16, 2025
0a18917
Update refresh logic and tests
ahmedxgouda Aug 16, 2025
54839ef
Make google auth credentials secret
ahmedxgouda Aug 16, 2025
f07770e
Merge branch 'main' into nestbot-calendar/initial-google-auth
ahmedxgouda Aug 19, 2025
5820460
Update code
arkid15r Aug 20, 2025
52443fe
Merge branch 'main' into nestbot-calendar/initial-google-auth
ahmedxgouda Aug 20, 2025
943e939
Apply suggestions
ahmedxgouda Aug 20, 2025
68df7de
Update tests and member related name
ahmedxgouda Aug 20, 2025
e1f52d5
Merge branch 'main' into nestbot-calendar/initial-google-auth
ahmedxgouda Aug 20, 2025
cf136a4
Clean up migrations
ahmedxgouda Aug 21, 2025
8fcc676
Merge branch 'main' into nestbot-calendar/initial-google-auth
ahmedxgouda Aug 21, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions backend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,6 @@ DJANGO_SENTRY_DSN=None
DJANGO_SLACK_BOT_TOKEN=None
DJANGO_SLACK_SIGNING_SECRET=None
GITHUB_TOKEN=None
GOOGLE_AUTH_CLIENT_ID=None
GOOGLE_AUTH_CLIENT_SECRET=None
GOOGLE_AUTH_REDIRECT_URI=http://localhost:8000/integrations/slack/oauth2/callback/
39 changes: 39 additions & 0 deletions backend/apps/slack/migrations/0019_googleauth.py
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")),
(
"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",
),
),
],
),
]
1 change: 1 addition & 0 deletions backend/apps/slack/models/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from .conversation import Conversation
from .event import Event
from .google_auth import GoogleAuth
from .member import Member
from .message import Message
from .workspace import Workspace
107 changes: 107 additions & 0 deletions backend/apps/slack/models/google_auth.py
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's move this to nest app. It's only related to Slack due to the current task scope.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But it has slack.Member as a user not nest.User

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not just linked user what matters here. This model has very weak tie to slack app. In future slack.Member relation can be optional. We may expand google auth to a wider scope not related to Slack at all. Consider this: tomorrow I change the project requirements to notify not only in Slack channel but also via email or Nest UI (e.g. pop up messages for logged in users).

For solid long term solution you should implement short-term ones keeping in mind their extensibility.

Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
"""Slack Google OAuth Authentication Model."""

from django.conf import settings
from django.db import models
from django.utils import timezone
from google_auth_oauthlib.flow import Flow

AUTH_ERROR_MESSAGE = (
"Google OAuth client ID, secret, and redirect URI must be set in environment variables."
)


class GoogleAuth(models.Model):
"""Model to store Google OAuth tokens for Slack integration."""

member = models.OneToOneField(
"slack.Member",
on_delete=models.CASCADE,
related_name="google_auth",
verbose_name="Slack Member",
)
access_token = models.TextField(
verbose_name="Access Token",
blank=True,
)
refresh_token = models.TextField(
verbose_name="Refresh Token",
blank=True,
)
expires_at = models.DateTimeField(
verbose_name="Token Expiry",
blank=True,
null=True,
)

@staticmethod
def get_flow():
"""Create a Google OAuth flow instance."""
if not settings.IS_GOOGLE_AUTH_ENABLED:
raise ValueError(AUTH_ERROR_MESSAGE)
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": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
}
},
scopes=["https://www.googleapis.com/auth/calendar.readonly"],
)

@staticmethod
def authenticate(auth_url, member):
"""Authenticate a member and return a GoogleAuth instance."""
if not settings.IS_GOOGLE_AUTH_ENABLED:
raise ValueError(AUTH_ERROR_MESSAGE)
auth = GoogleAuth.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
GoogleAuth.refresh_access_token(auth)
return auth
# This is the first time authentication, so we need to fetch a new token
flow = GoogleAuth.get_flow()
flow.redirect_uri = settings.GOOGLE_AUTH_REDIRECT_URI
flow.fetch_token(authorization_response=auth_url)
auth.access_token = flow.credentials.token
auth.refresh_token = flow.credentials.refresh_token
auth.expires_at = flow.credentials.expiry
auth.save()
return auth

@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 ValueError(refresh_error)

flow = GoogleAuth.get_flow()
flow.fetch_token(
refresh_token=auth.refresh_token,
client_id=settings.GOOGLE_AUTH_CLIENT_ID,
client_secret=settings.GOOGLE_AUTH_CLIENT_SECRET,
)

credentials = flow.credentials
auth.access_token = credentials.token
auth.refresh_token = credentials.refresh_token
auth.expires_at = credentials.expiry
auth.save()

def __str__(self):
"""Return a string representation of the GoogleAuth instance."""
return f"GoogleAuth(member={self.member})"
Loading