diff --git a/i18n/locales/de/settings.json b/i18n/locales/de/settings.json
index 94d92f8..f1dd946 100644
--- a/i18n/locales/de/settings.json
+++ b/i18n/locales/de/settings.json
@@ -18,5 +18,18 @@
"save": "Speichern",
"overview": {
"remainingTime": "Verbleibende Zeit vor der automatischen Abmeldung"
+ },
+ "hydraRevoke": {
+ "title": "Verbundene Anwendungen",
+ "description": "Apps, die Sie per OAuth2 autorisiert haben. Sie können den Zugriff pro App widerrufen; eine erneute Anmeldung kann nötig sein.",
+ "loading": "Verbundene Anwendungen werden geladen…",
+ "empty": "Sie haben keine verbundenen Anwendungen.",
+ "loadError": "Verbundene Anwendungen konnten nicht geladen werden.",
+ "noScopes": "Für diese Verbindung sind keine Berechtigungen (Scopes) hinterlegt.",
+ "revoke": "Zugriff widerrufen",
+ "revoking": "Wird widerrufen…",
+ "revokeSuccess": "Der Zugriff für „{{name}}“ wurde widerrufen.",
+ "revokeError": "Der Zugriff konnte nicht widerrufen werden. Bitte versuchen Sie es erneut.",
+ "retry": "Erneut versuchen"
}
}
\ No newline at end of file
diff --git a/i18n/locales/en/settings.json b/i18n/locales/en/settings.json
index d727d67..2c30089 100644
--- a/i18n/locales/en/settings.json
+++ b/i18n/locales/en/settings.json
@@ -18,5 +18,18 @@
"save": "Save",
"overview": {
"remainingTime": "Remaining time before auto logout"
+ },
+ "hydraRevoke": {
+ "title": "Connected applications",
+ "description": "Apps you authorized with OAuth2. You can revoke access for each app; you may need to sign in again if you use it later.",
+ "loading": "Loading connected applications…",
+ "empty": "You have no connected applications.",
+ "loadError": "Could not load connected applications.",
+ "noScopes": "No permissions (scopes) recorded for this connection.",
+ "revoke": "Revoke access",
+ "revoking": "Revoking…",
+ "revokeSuccess": "Access for “{{name}}” has been revoked.",
+ "revokeError": "Could not revoke access. Please try again.",
+ "retry": "Retry"
}
}
\ No newline at end of file
diff --git a/i18n/locales/nl/settings.json b/i18n/locales/nl/settings.json
index 0baf998..6945e22 100644
--- a/i18n/locales/nl/settings.json
+++ b/i18n/locales/nl/settings.json
@@ -18,5 +18,18 @@
"save": "Redden",
"overview": {
"remainingTime": "Verbleibende tijd voor automatische afmelding"
+ },
+ "hydraRevoke": {
+ "title": "Gekoppelde applicaties",
+ "description": "Apps die u met OAuth2 hebt gemachtigd. U kunt de toegang per app intrekken; u moet zich mogelijk later opnieuw aanmelden.",
+ "loading": "Gekoppelde applicaties laden…",
+ "empty": "U hebt geen gekoppelde applicaties.",
+ "loadError": "Gekoppelde applicaties konden niet worden geladen.",
+ "noScopes": "Geen rechten (scopes) vastgelegd voor deze verbinding.",
+ "revoke": "Toegang intrekken",
+ "revoking": "Bezig met intrekken…",
+ "revokeSuccess": "Toegang voor „{{name}}“ is ingetrokken.",
+ "revokeError": "Toegang kon niet worden ingetrokken. Probeer het opnieuw.",
+ "retry": "Opnieuw proberen"
}
}
\ No newline at end of file
diff --git a/next.config.js b/next.config.js
index 951a8da..22dd8c1 100644
--- a/next.config.js
+++ b/next.config.js
@@ -4,8 +4,7 @@ const nextConfig = {
swcMinify: true,
env: {
IS_DEV: process.env.IS_DEV,
- }
-}
+ },
+};
-
-module.exports = nextConfig
+module.exports = nextConfig;
diff --git a/pages/SettingsConnectedApplications.tsx b/pages/SettingsConnectedApplications.tsx
new file mode 100644
index 0000000..2a607bc
--- /dev/null
+++ b/pages/SettingsConnectedApplications.tsx
@@ -0,0 +1,156 @@
+import { memo, useCallback } from "react"
+import { useTranslation } from "react-i18next"
+
+import { combineClassNames } from "@/submodules/javascript-functions/general"
+import {
+ oauth2ScopeLabel,
+ HydraConnectedApplication,
+} from "@/util/hydra-connected-applications.helper"
+
+export enum ConnectedApplicationsLoadStateEnum {
+ IDLE = "idle",
+ LOADING = "loading",
+ SUCCESS = "success",
+ ERROR = "error"
+}
+
+export interface SettingsConnectedApplicationsProps {
+ readonly applications: HydraConnectedApplication[];
+ readonly loadState: ConnectedApplicationsLoadStateEnum;
+ readonly loadError: string;
+ readonly revokingClientId: string | null;
+ readonly revokeNotice: { type: "success" | "error"; message: string } | null;
+ readonly onRevoke: (clientId: string, clientName: string) => void;
+ readonly onRetryLoad: () => void;
+}
+
+interface RowProps {
+ readonly app: HydraConnectedApplication;
+ readonly isRevoking: boolean;
+ readonly onRevoke: (clientId: string, clientName: string) => void;
+}
+
+const SettingsConnectedApplicationRow = memo(function SettingsConnectedApplicationRow({
+ app,
+ isRevoking,
+ onRevoke,
+}: RowProps) {
+ const { t } = useTranslation("settings")
+ const clientName = app.clientName
+ const clientInitial = clientName.charAt(0).toUpperCase()
+ const handleRevokeClick = useCallback(() => {
+ onRevoke(app.clientId, clientName)
+ }, [app.clientId, clientName, onRevoke])
+
+ return (
+
+
+
+ {app.logoUri ? (
+
+ ) : (
+ clientInitial
+ )}
+
+
+ {clientName}
+
+
+ {app.grantedScope.length > 0 ? (
+
+ {app.grantedScope.map(scope => (
+
+ {oauth2ScopeLabel(scope)}
+
+ ))}
+
+ ) : (
+
{t("hydraRevoke.noScopes")}
+ )}
+
+
+
+
+ )
+})
+
+function SettingsConnectedApplications(props: SettingsConnectedApplicationsProps) {
+ const { t } = useTranslation("settings")
+ const {
+ applications,
+ loadState,
+ loadError,
+ revokingClientId,
+ revokeNotice,
+ onRevoke,
+ onRetryLoad,
+ } = props
+
+ const handleRetry = useCallback(() => {
+ onRetryLoad()
+ }, [onRetryLoad])
+
+ const showLoading = loadState === ConnectedApplicationsLoadStateEnum.IDLE || loadState === ConnectedApplicationsLoadStateEnum.LOADING
+
+ return (
+
+
{t("hydraRevoke.title")}
+
{t("hydraRevoke.description")}
+
+ {revokeNotice && (
+
+ {revokeNotice.message}
+
+ )}
+
+ {showLoading && (
+
{t("hydraRevoke.loading")}
+ )}
+
+ {loadState === "error" && (
+
+
{loadError || t("hydraRevoke.loadError")}
+
+
+ )}
+
+ {loadState === "success" && applications.length === 0 && (
+
{t("hydraRevoke.empty")}
+ )}
+
+ {loadState === "success" && applications.length > 0 && (
+
+ {applications.map(app => (
+
+ ))}
+
+ )}
+
+ )
+}
+
+export default memo(SettingsConnectedApplications)
diff --git a/pages/consent.tsx b/pages/consent.tsx
index ad19eb4..8dcda93 100644
--- a/pages/consent.tsx
+++ b/pages/consent.tsx
@@ -250,15 +250,18 @@ const Consent: NextPage = () => {
{consentRequest.requested_scope.map(scope => {
const meta = getScopeMeta(scope)
const isChecked = selectedScopes.includes(scope)
+ const isDisabled = scope.toLowerCase().includes('offline') || scope.toLowerCase().includes('openid')
return (