From 8eef7498d85468512259c8dcb5ea492f402bef1d Mon Sep 17 00:00:00 2001 From: Dan Trickey Date: Sat, 10 Feb 2024 14:17:41 +0000 Subject: [PATCH] Remove Discord Integration We can build this in a separate service, let's keep the scope small --- .../0003_remove_discord_integration.py | 26 +++++ kmicms/accounts/models.py | 32 ------- kmicms/accounts/oauth.py | 12 --- .../accounts/discord_account_disconnect.html | 14 --- .../templates/accounts/user_profile.html | 50 ---------- kmicms/accounts/urls.py | 20 +--- kmicms/accounts/{views/auth.py => views.py} | 20 +++- kmicms/accounts/views/__init__.py | 3 - kmicms/accounts/views/discord.py | 94 ------------------- kmicms/accounts/views/profile.py | 17 ---- kmicms/accounts/wagtail_hooks.py | 8 -- kmicms/kmicms/configuration.prod.py | 3 - kmicms/kmicms/settings.py | 12 --- kmicms/templates/components/navbar.html | 3 - kmicms/templates/layouts/page.html | 1 + 15 files changed, 46 insertions(+), 269 deletions(-) create mode 100644 kmicms/accounts/migrations/0003_remove_discord_integration.py delete mode 100644 kmicms/accounts/templates/accounts/discord_account_disconnect.html delete mode 100644 kmicms/accounts/templates/accounts/user_profile.html rename kmicms/accounts/{views/auth.py => views.py} (84%) delete mode 100644 kmicms/accounts/views/__init__.py delete mode 100644 kmicms/accounts/views/discord.py delete mode 100644 kmicms/accounts/views/profile.py delete mode 100644 kmicms/accounts/wagtail_hooks.py diff --git a/kmicms/accounts/migrations/0003_remove_discord_integration.py b/kmicms/accounts/migrations/0003_remove_discord_integration.py new file mode 100644 index 0000000..19de163 --- /dev/null +++ b/kmicms/accounts/migrations/0003_remove_discord_integration.py @@ -0,0 +1,26 @@ +# Generated by Django 4.2.10 on 2024-02-10 14:15 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("accounts", "0002_discord_accounts"), + ] + + operations = [ + migrations.RemoveField( + model_name="discordconnection", + name="discord_account", + ), + migrations.RemoveField( + model_name="discordconnection", + name="user", + ), + migrations.DeleteModel( + name="DiscordAccount", + ), + migrations.DeleteModel( + name="DiscordConnection", + ), + ] diff --git a/kmicms/accounts/models.py b/kmicms/accounts/models.py index 1c53abb..94684ac 100644 --- a/kmicms/accounts/models.py +++ b/kmicms/accounts/models.py @@ -1,39 +1,7 @@ from __future__ import annotations from django.contrib.auth.models import AbstractUser -from django.db import models class User(AbstractUser): pass - - -class DiscordAccount(models.Model): - discord_id = models.PositiveBigIntegerField(unique=True) - username = models.CharField(max_length=255, unique=True) - created_at = models.DateTimeField(auto_now_add=True) - updated_at = models.DateTimeField(auto_now=True) - - def __str__(self) -> str: - return f"@{self.username}" - - -class DiscordConnection(models.Model): - user = models.OneToOneField( - User, on_delete=models.CASCADE, related_name="discord_connection", related_query_name="discord_connection" - ) - discord_account = models.OneToOneField( - DiscordAccount, - on_delete=models.CASCADE, - related_name="discord_connection", - related_query_name="discord_connection", - ) - access_token = models.CharField(max_length=30) - refresh_token = models.CharField(max_length=30) - access_token_expires_at = models.DateTimeField() - - created_at = models.DateTimeField(auto_now_add=True) - updated_at = models.DateTimeField(auto_now=True) - - def __str__(self) -> str: - return f"Discord Connection for {self.user} to {self.discord_account}" diff --git a/kmicms/accounts/oauth.py b/kmicms/accounts/oauth.py index 10f916d..2c5a278 100644 --- a/kmicms/accounts/oauth.py +++ b/kmicms/accounts/oauth.py @@ -3,18 +3,6 @@ oauth_config = OAuth() -# Discord Integration -oauth_config.register( - "discord", - client_id=settings.DISCORD_APP_CLIENT_ID, - client_secret=settings.DISCORD_APP_CLIENT_SECRET, - access_token_url=settings.DISCORD_ACCESS_TOKEN_URL, - authorize_url=settings.DISCORD_AUTHORIZE_URL, - revocation_url=settings.DISCORD_REVOCATION_URL, - userinfo_endpoint=settings.DISCORD_USERINFO_ENDPOINT, - client_kwargs=settings.DISCORD_CLIENT_KWARGS, -) - # SOWN SSO oauth_config.register( "sown", diff --git a/kmicms/accounts/templates/accounts/discord_account_disconnect.html b/kmicms/accounts/templates/accounts/discord_account_disconnect.html deleted file mode 100644 index b06f5e1..0000000 --- a/kmicms/accounts/templates/accounts/discord_account_disconnect.html +++ /dev/null @@ -1,14 +0,0 @@ -{% extends "layouts/page.html" %} -{% load auth_tags crispy_forms_tags %} - -{% block title %}Disconnect Discord Account{% endblock %} - -{% block content %} -
-

Disconnect Discord Account

-

Are your sure you want to disconnect your discord account?

-
- {% crispy form %} -
-
-{% endblock %} \ No newline at end of file diff --git a/kmicms/accounts/templates/accounts/user_profile.html b/kmicms/accounts/templates/accounts/user_profile.html deleted file mode 100644 index 41b67cb..0000000 --- a/kmicms/accounts/templates/accounts/user_profile.html +++ /dev/null @@ -1,50 +0,0 @@ -{% extends "layouts/page.html" %} - -{% block title %}My Profile{% endblock %} - -{% block content %} -
-

My Profile

- {% include "components/messages.html" %} -

Your SUWS / SOWN profile holds data in accordance with our privacy policy. Please contact a member of the committee for further details.

-
-
-

Personal Details

-
-
Name
-
{{ request.user.first_name }} {{ request.user.last_name }}
- -
Username
-
{{ request.user.username }}
- -
Email Address
-
{{ request.user.email }}
-
- - Update my details - -
-
-

Discord

- {% if request.user.discord_connection %} -

Your account is connected to Discord.

-
-
Username
-
@{{ request.user.discord_connection.discord_account.username }}
- -
ID
-
{{ request.user.discord_connection.discord_account.discord_id }}
-
- - Disconnect - - {% else %} -

Your account is not connected to Discord.

- -  Connect to Discord - - {% endif %} -
-
-
-{% endblock %} \ No newline at end of file diff --git a/kmicms/accounts/urls.py b/kmicms/accounts/urls.py index f2f04a7..5335815 100644 --- a/kmicms/accounts/urls.py +++ b/kmicms/accounts/urls.py @@ -6,27 +6,11 @@ app_name = "accounts" urlpatterns = [ - path("login/", views.auth.LoginView.as_view(), name="login"), + path("login/", views.LoginView.as_view(), name="login"), path("logout/", LogoutView.as_view(), name="logout"), - path("profile/", views.profile.UserProfileView.as_view(), name="profile"), - path( - "integrations/discord/setup/", - views.discord.DiscordOAuthProfileAuthorizeView.as_view(), - name="discord_oauth_setup", - ), - path( - "integrations/discord/redirect/", - views.discord.DiscordOAuthProfileRedirectView.as_view(), - name="discord_oauth_redirect", - ), - path( - "integrations/discord/disconnect/", - views.discord.DiscordAccountProfileDisconnectView.as_view(), - name="discord_oauth_disconnect", - ), path( "oidc/redirect/", - views.auth.SSOOIDCRedirectView.as_view(), + views.SSOOIDCRedirectView.as_view(), name="sso_oidc_redirect", ), ] diff --git a/kmicms/accounts/views/auth.py b/kmicms/accounts/views.py similarity index 84% rename from kmicms/accounts/views/auth.py rename to kmicms/accounts/views.py index 93750b4..5714479 100644 --- a/kmicms/accounts/views/auth.py +++ b/kmicms/accounts/views.py @@ -11,9 +11,10 @@ from django.shortcuts import redirect from django.urls import reverse from django.views import View +from wagtail.models import Site -from accounts.models import User -from accounts.oauth import oauth_config +from .models import User +from .oauth import oauth_config class LoginView(auth_views.LoginView): @@ -31,11 +32,17 @@ def dispatch(self, request: http.HttpRequest, *args: Any, **kwargs: Any) -> http # If SSO is enabled, redirect immediately. if not settings.USE_CONVENTIONAL_AUTH: + request.session["sso_next"] = self.get_redirect_url() + redirect_uri = request.build_absolute_uri(reverse("accounts:sso_oidc_redirect")) return oauth_config.sown.authorize_redirect(request, redirect_uri) return super().dispatch(request, *args, **kwargs) + def get_default_redirect_url(self) -> str: + site = Site.find_for_request(self.request) + return site.root_page.get_url() + class SSOOIDCRedirectView(View): def get(self, request: http.HttpRequest) -> http.HttpResponseRedirect: @@ -54,8 +61,15 @@ def get(self, request: http.HttpRequest) -> http.HttpResponseRedirect: login(request, user) + from_session = request.session.pop("sso_next", None) + + redirect_to = from_session or self.get_default_redirect_url() messages.info(request, f"Signed in via SOWN SSO. Welcome {user.get_short_name()}") - return redirect("accounts:profile") + return redirect(redirect_to) + + def get_default_redirect_url(self) -> str: + site = Site.find_for_request(self.request) + return site.root_page.get_url() def update_user(self, user: User, claims: dict[str, bool | str | list[str]]) -> User: full_name = claims.get("given_name", "") diff --git a/kmicms/accounts/views/__init__.py b/kmicms/accounts/views/__init__.py deleted file mode 100644 index a4b3513..0000000 --- a/kmicms/accounts/views/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from . import auth, discord, profile - -__all__ = ["auth", "discord", "profile"] diff --git a/kmicms/accounts/views/discord.py b/kmicms/accounts/views/discord.py deleted file mode 100644 index 96cf3e5..0000000 --- a/kmicms/accounts/views/discord.py +++ /dev/null @@ -1,94 +0,0 @@ -from __future__ import annotations - -from datetime import datetime -from zoneinfo import ZoneInfo - -from crispy_forms import helper, layout -from django.conf import settings -from django.contrib import messages -from django.contrib.auth.mixins import LoginRequiredMixin -from django.forms import BaseModelForm, Form -from django.http import HttpRequest, HttpResponseRedirect -from django.shortcuts import get_object_or_404, redirect -from django.urls import reverse, reverse_lazy -from django.views.generic import DeleteView, View - -from accounts.models import DiscordAccount, DiscordConnection -from accounts.oauth import oauth_config - - -class DiscordOAuthProfileAuthorizeView(LoginRequiredMixin, View): - def get(self, request: HttpRequest) -> HttpResponseRedirect: - if DiscordConnection.objects.filter(user=request.user).exists(): - messages.warning(request, "Your account is already connected to Discord.") - return redirect("accounts:profile") - - redirect_uri = request.build_absolute_uri(reverse("accounts:discord_oauth_redirect")) - return oauth_config.discord.authorize_redirect(request, redirect_uri) - - -class DiscordOAuthProfileRedirectView(LoginRequiredMixin, View): - def get(self, request: HttpRequest) -> HttpResponseRedirect: - if DiscordConnection.objects.filter(user=request.user).exists(): - messages.warning(request, "Your account is already connected to Discord.") - return redirect("accounts:profile") - - token = oauth_config.discord.authorize_access_token(request) - userinfo = oauth_config.discord.userinfo(token=token) - - account, created = DiscordAccount.objects.get_or_create( - discord_id=int(userinfo["id"]), - defaults={ - "username": userinfo["username"], - }, - ) - - # If the discord account was already known, update the username - if not created: - account.username = userinfo["username"] - account.save() - - if DiscordConnection.objects.filter(discord_account=account).exists(): - messages.error( - request, "That Discord account is connected to another user. Please disconnect it and try again." - ) - return redirect("accounts:profile") - - expires_at = datetime.fromtimestamp(token["expires_at"], ZoneInfo("UTC")) - DiscordConnection.objects.create( - user=request.user, - discord_account=account, - access_token=token["access_token"], - refresh_token=token["refresh_token"], - access_token_expires_at=expires_at, - ) - - messages.success(request, f"Successfully connected to Discord account: {account}") - return redirect("accounts:profile") - - -class DiscordAccountProfileDisconnectView(LoginRequiredMixin, DeleteView): - template_name = "accounts/discord_account_disconnect.html" - success_url = reverse_lazy("accounts:profile") - - def get_object(self) -> DiscordConnection: - return get_object_or_404(DiscordConnection, user=self.request.user) - - def get_form(self, form_class: type[BaseModelForm] | None = None) -> BaseModelForm: - form = super().get_form(form_class) - form.helper = helper.FormHelper() - form.helper.add_input(layout.Submit("submit", "Disconnect", css_class="btn-danger")) - return form - - def form_valid(self, form: Form) -> HttpResponseRedirect: - resp = super().form_valid(form) - - # Revoke the refresh token - oauth_config.discord._get_oauth_client().revoke_token( - settings.DISCORD_REVOCATION_URL, - token=self.object.refresh_token, - token_type_hint="refresh_token", # noqa: S106 - ) - - messages.success(self.request, "Successfully disconnected from Discord") - return resp diff --git a/kmicms/accounts/views/profile.py b/kmicms/accounts/views/profile.py deleted file mode 100644 index b2f9d21..0000000 --- a/kmicms/accounts/views/profile.py +++ /dev/null @@ -1,17 +0,0 @@ -from __future__ import annotations - -from typing import Any - -from django.conf import settings -from django.contrib.auth.mixins import LoginRequiredMixin -from django.views.generic import TemplateView - - -class UserProfileView(LoginRequiredMixin, TemplateView): - template_name = "accounts/user_profile.html" - - def get_context_data(self, **kwargs: Any) -> dict[str, Any]: - return super().get_context_data( - sso_user_settings_url=settings.SSO_USER_SETTINGS_URL, - **kwargs, - ) diff --git a/kmicms/accounts/wagtail_hooks.py b/kmicms/accounts/wagtail_hooks.py deleted file mode 100644 index 2952f90..0000000 --- a/kmicms/accounts/wagtail_hooks.py +++ /dev/null @@ -1,8 +0,0 @@ -from django.http import HttpRequest -from django.urls import reverse -from wagtail import hooks - - -@hooks.register("register_account_menu_item") -def register_account_profile(request: HttpRequest) -> dict[str, str]: - return {"url": reverse("accounts:profile"), "label": "View profile", "help_text": "View your SUWS / SOWN profile."} diff --git a/kmicms/kmicms/configuration.prod.py b/kmicms/kmicms/configuration.prod.py index f0266c4..25ab415 100644 --- a/kmicms/kmicms/configuration.prod.py +++ b/kmicms/kmicms/configuration.prod.py @@ -37,9 +37,6 @@ SSO_STAFF_GROUP_NAME = os.environ.get("SSO_STAFF_GROUP_NAME") SSO_SUPERUSER_GROUP_NAME = os.environ.get("SSO_SUPERUSER_GROUP_NAME") -DISCORD_APP_CLIENT_ID = os.environ.get("DISCORD_APP_CLIENT_ID") -DISCORD_APP_CLIENT_SECRET = os.environ.get("DISCORD_APP_CLIENT_SECRET") - RECAPTCHA_PUBLIC_KEY = os.environ.get("RECAPTCHA_PUBLIC_KEY") RECAPTCHA_PRIVATE_KEY = os.environ.get("RECAPTCHA_PRIVATE_KEY") diff --git a/kmicms/kmicms/settings.py b/kmicms/kmicms/settings.py index 2eb0d15..e6cd547 100644 --- a/kmicms/kmicms/settings.py +++ b/kmicms/kmicms/settings.py @@ -207,18 +207,6 @@ USE_X_FORWARDED_HOST = True SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") -# Discord Integration -DISCORD_APP_CLIENT_ID = getattr(configuration, "DISCORD_APP_CLIENT_ID") -DISCORD_APP_CLIENT_SECRET = getattr(configuration, "DISCORD_APP_CLIENT_SECRET") -DISCORD_ACCESS_TOKEN_URL = "https://discordapp.com/api/oauth2/token" # noqa: S105 -DISCORD_AUTHORIZE_URL = "https://discordapp.com/api/oauth2/authorize" -DISCORD_REVOCATION_URL = "https://discord.com/api/oauth2/token/revoke" -DISCORD_USERINFO_ENDPOINT = "https://discordapp.com/api/users/@me" -DISCORD_CLIENT_KWARGS = { - "token_endpoint_auth_method": "client_secret_post", - "scope": "identify", -} - # Internationalization # https://docs.djangoproject.com/en/4.2/topics/i18n/ diff --git a/kmicms/templates/components/navbar.html b/kmicms/templates/components/navbar.html index 4f7dcf6..cb6284e 100644 --- a/kmicms/templates/components/navbar.html +++ b/kmicms/templates/components/navbar.html @@ -35,7 +35,6 @@  {{ request.user.get_full_name }} - {% else %} - Log in {% endif %} diff --git a/kmicms/templates/layouts/page.html b/kmicms/templates/layouts/page.html index 8b986d0..f6d0645 100644 --- a/kmicms/templates/layouts/page.html +++ b/kmicms/templates/layouts/page.html @@ -7,6 +7,7 @@ {% include "components/navbar.html" %}
+ {% include "components/messages.html" %} {% block content %}{% endblock %}
{% include "components/footer.html" %}