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 ? ( + {clientName} + ) : ( + 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 (