From 08ad7cf0db158d6677a6c9678a8f48702b5799b9 Mon Sep 17 00:00:00 2001
From: Jonathan Weth <git@jonathanweth.de>
Date: Sun, 23 Apr 2023 14:10:39 +0200
Subject: [PATCH] Rewrite page for authorized OAuth applications with (Vue)tify

---
 aleksis/core/frontend/app/dateTimeFormats.js  | 14 ++++
 .../AuthorizedApplication.vue                 | 77 +++++++++++++++++++
 .../AuthorizedApplications.vue                | 40 ++++++++++
 .../accessTokens.graphql                      | 21 +++++
 aleksis/core/frontend/messages/en.json        | 11 ++-
 aleksis/core/frontend/routes.js               | 10 +--
 aleksis/core/urls.py                          |  4 +
 7 files changed, 170 insertions(+), 7 deletions(-)
 create mode 100644 aleksis/core/frontend/components/authorized_oauth_applications/AuthorizedApplication.vue
 create mode 100644 aleksis/core/frontend/components/authorized_oauth_applications/AuthorizedApplications.vue
 create mode 100644 aleksis/core/frontend/components/authorized_oauth_applications/accessTokens.graphql

diff --git a/aleksis/core/frontend/app/dateTimeFormats.js b/aleksis/core/frontend/app/dateTimeFormats.js
index a835b238c..6a980fc8d 100644
--- a/aleksis/core/frontend/app/dateTimeFormats.js
+++ b/aleksis/core/frontend/app/dateTimeFormats.js
@@ -19,6 +19,13 @@ const dateTimeFormats = {
       minute: "numeric",
       second: "numeric",
     },
+    longNumeric: {
+      year: "numeric",
+      month: "numeric",
+      day: "numeric",
+      hour: "numeric",
+      minute: "numeric",
+    },
   },
   de: {
     short: {
@@ -39,6 +46,13 @@ const dateTimeFormats = {
       minute: "numeric",
       second: "numeric",
     },
+    longNumeric: {
+      year: "numeric",
+      month: "numeric",
+      day: "numeric",
+      hour: "numeric",
+      minute: "numeric",
+    },
   },
 };
 
diff --git a/aleksis/core/frontend/components/authorized_oauth_applications/AuthorizedApplication.vue b/aleksis/core/frontend/components/authorized_oauth_applications/AuthorizedApplication.vue
new file mode 100644
index 000000000..0fe07455e
--- /dev/null
+++ b/aleksis/core/frontend/components/authorized_oauth_applications/AuthorizedApplication.vue
@@ -0,0 +1,77 @@
+<template>
+  <v-expansion-panel>
+    <v-expansion-panel-header v-slot="{ open }">
+      <div class="d-flex justify-start align-center">
+        <v-avatar
+          x-large
+          v-if="accessToken.application.icon.absoluteUrl"
+          class="mr-4"
+        >
+          <img
+            :src="accessToken.application.icon.absoluteUrl"
+            :alt="accessToken.application.name"
+          />
+        </v-avatar>
+        <v-avatar x-large v-else class="mr-4" color="secondary">
+          <v-icon color="white">mdi-apps</v-icon>
+        </v-avatar>
+        <div class="subtitle-1 font-weight-medium">
+          {{ accessToken.application.name }}
+        </div>
+      </div>
+    </v-expansion-panel-header>
+    <v-expansion-panel-content>
+      <v-list dense class="pa-0">
+        <v-list-item>
+          <v-list-item-content class="body-2">
+            {{
+              $t("oauth.authorized_application.access_since", {
+                date: $d(new Date(accessToken.created), "longNumeric"),
+              })
+            }}
+            ·
+            {{
+              $t("oauth.authorized_application.valid_until", {
+                date: $d(new Date(accessToken.expires), "longNumeric"),
+              })
+            }}
+          </v-list-item-content>
+          <v-list-item-action>
+            <v-btn color="primary">
+              {{ $t("oauth.authorized_application.revoke") }}
+            </v-btn>
+          </v-list-item-action>
+        </v-list-item>
+        <v-list-item v-if="accessToken.scopes && accessToken.scopes.length > 0">
+          <div class="pr-4">
+            <v-list-item-content class="body-2">
+              {{ $t("oauth.authorized_application.has_access_to") }}
+            </v-list-item-content>
+          </div>
+          <v-list dense class="pa-0 flex-grow-1">
+            <div v-for="(scope, idx) in accessToken.scopes" :key="scope.name">
+              <v-list-item>
+                <v-list-item-content class="body-2">
+                  {{ scope.description }}
+                </v-list-item-content>
+              </v-list-item>
+              <v-divider v-if="idx < accessToken.scopes.length - 1" />
+            </div>
+          </v-list>
+        </v-list-item>
+      </v-list>
+    </v-expansion-panel-content>
+  </v-expansion-panel>
+</template>
+
+<script>
+export default {
+  name: "AuthorizedApplication",
+  props: {
+    accessToken: {
+      type: Object,
+      required: true,
+    },
+  },
+};
+</script>
diff --git a/aleksis/core/frontend/components/authorized_oauth_applications/AuthorizedApplications.vue b/aleksis/core/frontend/components/authorized_oauth_applications/AuthorizedApplications.vue
new file mode 100644
index 000000000..5a6b42102
--- /dev/null
+++ b/aleksis/core/frontend/components/authorized_oauth_applications/AuthorizedApplications.vue
@@ -0,0 +1,40 @@
+<template>
+  <div>
+    <h1 class="mb-4">{{ $t("oauth.authorized_application.title") }}</h1>
+    <div v-if="$apollo.queries.oauth.loading">
+      <v-skeleton-loader type="card"></v-skeleton-loader>
+    </div>
+    <div v-else-if="oauth">
+      <v-card class="mb-4">
+        <v-card-title>
+          {{ $t("oauth.authorized_application.subtitle") }}
+        </v-card-title>
+        <v-card-text>
+          {{ $t("oauth.authorized_application.description") }}
+        </v-card-text>
+        <v-expansion-panels flat>
+          <authorized-application
+            v-for="(accessToken, index) in oauth.accessTokens"
+            :key="accessToken.id"
+            :access-token="accessToken"
+          />
+        </v-expansion-panels>
+      </v-card>
+    </div>
+  </div>
+</template>
+
+<script>
+import gqlAccessTokens from "./accessTokens.graphql";
+import AuthorizedApplication from "./AuthorizedApplication.vue";
+
+export default {
+  name: "AuthorizedApplications",
+  components: { AuthorizedApplication },
+  apollo: {
+    oauth: {
+      query: gqlAccessTokens,
+    },
+  },
+};
+</script>
diff --git a/aleksis/core/frontend/components/authorized_oauth_applications/accessTokens.graphql b/aleksis/core/frontend/components/authorized_oauth_applications/accessTokens.graphql
new file mode 100644
index 000000000..3c40c63bd
--- /dev/null
+++ b/aleksis/core/frontend/components/authorized_oauth_applications/accessTokens.graphql
@@ -0,0 +1,21 @@
+{
+  oauth {
+    accessTokens {
+      id
+      created
+      updated
+      expires
+      scopes {
+        name
+        description
+      }
+      application {
+        id
+        name
+        icon {
+          absoluteUrl
+        }
+      }
+    }
+  }
+}
diff --git a/aleksis/core/frontend/messages/en.json b/aleksis/core/frontend/messages/en.json
index 5235e2326..63af5033d 100644
--- a/aleksis/core/frontend/messages/en.json
+++ b/aleksis/core/frontend/messages/en.json
@@ -167,8 +167,15 @@
       "title": "OAuth Application",
       "title_plural": "OAuth Applications"
     },
-    "authorized_token": {
-      "menu_title": "Authorized Applications"
+    "authorized_application": {
+      "menu_title": "Third-party Applications",
+      "title": "Third-party Applications",
+      "subtitle": "Third-party Applications With Access to Your Account",
+      "description": "The following third-party applications have access to your account. You can revoke access at any time for those you don't need or trust anymore.",
+      "valid_until": "Valid until {date}",
+      "access_since": "Access since {date}",
+      "has_access_to": "Has access to:",
+      "revoke": "Revoke Access"
     }
   },
   "people": "People",
diff --git a/aleksis/core/frontend/routes.js b/aleksis/core/frontend/routes.js
index 215f4d148..e7fc1e4fe 100644
--- a/aleksis/core/frontend/routes.js
+++ b/aleksis/core/frontend/routes.js
@@ -920,14 +920,14 @@ const routes = [
   },
   {
     path: "/oauth/authorized_tokens/",
-    component: () => import("./components/LegacyBaseTemplate.vue"),
-    props: {
-      byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
-    },
+    component: () =>
+      import(
+        "./components/authorized_oauth_applications/AuthorizedApplications.vue"
+      ),
     name: "core.oauth.authorizedTokens",
     meta: {
       inAccountMenu: true,
-      titleKey: "oauth.authorized_token.menu_title",
+      titleKey: "oauth.authorized_application.menu_title",
       icon: "mdi-gesture-tap-hold",
       permission: "core.manage_authorized_tokens_rule",
     },
diff --git a/aleksis/core/urls.py b/aleksis/core/urls.py
index f5e589906..29fe9dcb2 100644
--- a/aleksis/core/urls.py
+++ b/aleksis/core/urls.py
@@ -33,6 +33,10 @@ urlpatterns = [
         ConnectDiscoveryInfoView.as_view(),
         name="oidc_configuration",
     ),
+    path("oauth/applications/", views.TemplateView.as_view(template_name="core/vue_index.html")),
+    path(
+        "oauth/authorized_tokens/", views.TemplateView.as_view(template_name="core/vue_index.html")
+    ),
     path("oauth/", include("oauth2_provider.urls", namespace="oauth2_provider")),
     path(
         "django/",
-- 
GitLab