From 3aa94f88dcdd3b99bf8df9e53417879bfd37998b Mon Sep 17 00:00:00 2001 From: Yannick Marcon Date: Sat, 4 Jan 2025 11:33:50 +0100 Subject: [PATCH] feat: new settings UI (#578) * feat: settings page * feat: sysem properties dialog --- agate-ui/src/components/ApplicationDialog.vue | 29 ++- agate-ui/src/components/ApplicationsTable.vue | 1 - agate-ui/src/components/MainDrawer.vue | 8 + agate-ui/src/components/SystemProperties.vue | 152 ++++++++++++ .../src/components/SystemPropertiesDialog.vue | 223 ++++++++++++++++++ agate-ui/src/components/UserDialog.vue | 2 +- agate-ui/src/css/app.scss | 13 - agate-ui/src/i18n/en/index.js | 91 ++++--- agate-ui/src/i18n/fr/index.js | 91 ++++--- agate-ui/src/models/Agate.ts | 18 +- agate-ui/src/pages/SettingsPage.vue | 10 +- agate-ui/src/stores/system.ts | 8 + 12 files changed, 553 insertions(+), 93 deletions(-) create mode 100644 agate-ui/src/components/SystemProperties.vue create mode 100644 agate-ui/src/components/SystemPropertiesDialog.vue diff --git a/agate-ui/src/components/ApplicationDialog.vue b/agate-ui/src/components/ApplicationDialog.vue index 8e0d2da5..8280e646 100644 --- a/agate-ui/src/components/ApplicationDialog.vue +++ b/agate-ui/src/components/ApplicationDialog.vue @@ -93,7 +93,8 @@ size="sm" :label="t('application.add_scope')" @click="onAddScope" - class="q-mt-md" /> + class="q-mt-md" + />
{{ t('application.realms_groups') }}
@@ -108,7 +109,8 @@ :options="realmOptions" emit-value map-options - dense /> + dense + /> + style="min-width: 200px" + />
- + @@ -133,7 +143,8 @@ size="sm" :label="t('application.add_realm_groups')" @click="onAddRealmGroups" - class="q-mt-md" /> + class="q-mt-md" + /> @@ -172,8 +183,8 @@ const editMode = ref(false); const key = ref(''); const groupOptions = computed(() => groupStore.groups?.map((group) => ({ label: group.name, value: group.id })) ?? []); -const realmOptions = computed(() => - realmStore.realms?.map((realm) => ({ label: realm.name, value: realm.id || '' })) ?? [] +const realmOptions = computed( + () => realmStore.realms?.map((realm) => ({ label: realm.name, value: realm.id || '' })) ?? [], ); const isValid = computed( () => @@ -258,7 +269,9 @@ function onAddRealmGroups() { if (!selected.value.realmGroups) { selected.value.realmGroups = []; } - const realm = realmOptions.value.find((rlm) => !selected.value.realmGroups.map((rlmGrps) => rlmGrps.realm).includes(rlm.value))?.value || ''; + const realm = + realmOptions.value.find((rlm) => !selected.value.realmGroups.map((rlmGrps) => rlmGrps.realm).includes(rlm.value)) + ?.value || ''; if (!realm) { return; } diff --git a/agate-ui/src/components/ApplicationsTable.vue b/agate-ui/src/components/ApplicationsTable.vue index e584b66b..7b64dab4 100644 --- a/agate-ui/src/components/ApplicationsTable.vue +++ b/agate-ui/src/components/ApplicationsTable.vue @@ -143,5 +143,4 @@ function onAdd() { function onSaved() { refresh(); } - diff --git a/agate-ui/src/components/MainDrawer.vue b/agate-ui/src/components/MainDrawer.vue index d3c225cd..9608ceb9 100644 --- a/agate-ui/src/components/MainDrawer.vue +++ b/agate-ui/src/components/MainDrawer.vue @@ -53,6 +53,14 @@ {{ t('realms') }} + + + + + + {{ t('tickets') }} + + diff --git a/agate-ui/src/components/SystemProperties.vue b/agate-ui/src/components/SystemProperties.vue new file mode 100644 index 00000000..4ddb8b19 --- /dev/null +++ b/agate-ui/src/components/SystemProperties.vue @@ -0,0 +1,152 @@ + + + diff --git a/agate-ui/src/components/SystemPropertiesDialog.vue b/agate-ui/src/components/SystemPropertiesDialog.vue new file mode 100644 index 00000000..319a2f23 --- /dev/null +++ b/agate-ui/src/components/SystemPropertiesDialog.vue @@ -0,0 +1,223 @@ + + + diff --git a/agate-ui/src/components/UserDialog.vue b/agate-ui/src/components/UserDialog.vue index 43d670fa..7ba43443 100644 --- a/agate-ui/src/components/UserDialog.vue +++ b/agate-ui/src/components/UserDialog.vue @@ -228,7 +228,7 @@ const realmOptions = computed(() => [ ...(realmStore.realms?.map((realm) => ({ label: realm.name, value: realm.id })) ?? []), ]); const languageOptions = computed( - () => systemStore.configurationPublic.languages?.map((lang) => ({ label: lang.toUpperCase(), value: lang })) ?? [], + () => systemStore.configurationPublic.languages?.map((lang) => ({ label: lang, value: lang })) ?? [], ); const showPassword = computed(() => !editMode.value && selected.value.realm === 'agate-user-realm'); const isValid = computed( diff --git a/agate-ui/src/css/app.scss b/agate-ui/src/css/app.scss index 448f48cd..736512da 100644 --- a/agate-ui/src/css/app.scss +++ b/agate-ui/src/css/app.scss @@ -110,19 +110,6 @@ code { } } -.ace_editor { - font-size: 16px; -} - -.ace_gutter { - padding-top: 10px; - padding-bottom: 10px; -} - -.ace_scroller { - padding: 10px; -} - .dialog-sm { width: 500px !important; max-width: 80vw !important; diff --git a/agate-ui/src/i18n/en/index.js b/agate-ui/src/i18n/en/index.js index 6fd3a30e..b94a0b2d 100644 --- a/agate-ui/src/i18n/en/index.js +++ b/agate-ui/src/i18n/en/index.js @@ -6,46 +6,46 @@ export default { powered_by: 'Powered by', }, application: { + add_realm_groups: 'Add realm groups', + add_scope: 'Add permission', add: 'Add application', - edit: 'Edit application', - remove: 'Remove application', - remove_confirm: 'Please confirm application removal: {name}', - saved: 'Application saved', - save_failed: 'Failed to save application', - key: 'Key', + auto_approval_hint: 'Automatically approve a user who signed up through the application. Otherwise the user will be in "Pending" state, requiring manual approval.', + auto_approval: 'User approved on sign up', copy_key: 'Copy key', + edit: 'Edit application', generate_key: 'Generate key', - redirect_uris: 'Redirect URIs', - redirect_uris_hint: "Callback URL to the application's server, required in the OAuth context. Use commas to separate multiple allowed callback URLs.", - key_required: 'Key is required', - key_hint: 'This key is used to authenticate the application with the API.', + key_copied: 'Key copied', key_hint_edit: "Leave blank to not modify application's secret key.", + key_hint: 'This key is used to authenticate the application with the API.', key_min_length: 'Key must be at least {min} characters', - key_copied: 'Key copied', - auto_approval: 'User approved on sign up', - auto_approval_hint: 'Automatically approve a user who signed up through the application. Otherwise the user will be in "Pending" state, requiring manual approval.', - scopes: 'Permissions', - scopes_hint: 'Permissions allow to qualify the authorization access to the application that is granted in the OAuth context. Permissions are optional.', - add_scope: 'Add permission', - realms_groups: 'Realms and groups', + key_required: 'Key is required', + key: 'Key', realms_groups_hint: 'Mapping between realm and group names. When defined, corresponding realms will be proposed for user signin/signup from the application. When a user joins though an application, using a realm, the corresponding group(s) will be automatically applied.', - add_realm_groups: 'Add realm groups', + realms_groups: 'Realms and groups', + redirect_uris_hint: "Callback URL to the application's server, required in the OAuth context. Use commas to separate multiple allowed callback URLs.", + redirect_uris: 'Redirect URIs', + remove_confirm: 'Please confirm application removal: {name}', + remove: 'Remove application', + save_failed: 'Failed to save application', + saved: 'Application saved', + scopes_hint: 'Permissions allow to qualify the authorization access to the application that is granted in the OAuth context. Permissions are optional.', + scopes: 'Permissions', }, group: { add: 'Add group', applications_hint: 'Members of a group get access to the applications associated to this group.', edit: 'Edit group', - remove: 'Remove group', remove_confirm: 'Please confirm group removal: {name}. User members of this group will not be removed.', - saved: 'Group saved', + remove: 'Remove group', save_failed: 'Failed to save group', + saved: 'Group saved', }, user: { add: 'Add user', applications_hint: 'Users can be directly granted application access.', + approve_error: 'Failed to approve user', approve: 'Approve user', approved: 'User approved', - approve_error: 'Failed to approve user', copy_password: 'Copy password', disable_2fa: 'Disable 2FA', edit: 'Edit user', @@ -54,24 +54,24 @@ export default { groups_hint: 'Users can be members of groups to access applications associated to these groups.', language: 'Language', lastName: 'Last Name', - otp_disabled: '2FA disabled', otp_disable_error: 'Failed to disable 2FA', - password: 'Password', + otp_disabled: '2FA disabled', password_copied: 'Password copied', password_hint: 'Password must contain at least one digit, one upper case alphabet, one lower case alphabet, one special character (which includes @#$%^&+=!) and no white spaces.', password_min_length: 'Password must be at least {min} characters', password_required: 'Password is required', + password: 'Password', realm_hint: 'Realm in which user authenticates.', - reject: 'Reject user', reject_confirm: 'Please confirm user rejection: {name}', - remove: 'Remove user', + reject: 'Reject user', remove_confirm: 'Please confirm user removal: {name}', - removed: 'User removed', remove_error: 'Failed to remove user', - reset_password: 'Reset password', + remove: 'Remove user', + removed: 'User removed', reset_password_confirm: 'Please confirm that you want to send a password reset notification to this user.', reset_password_error: 'Failed to reset user password', reset_password_success: 'Password reset notification sent', + reset_password: 'Reset password', role: { 'agate-user': 'User', 'agate-administrator': 'Administrator', @@ -97,6 +97,40 @@ export default { }, update_password: 'Update password', }, + system: { + inactive_timeout_hint: 'User account expiration timeout in days.', + inactive_timeout: 'Inactive timeout (days)', + languages_hint: 'Possible notification email languages.', + languages: 'Languages', + long_timeout_hint: 'Ticket expiration timeout in hours when "remember me" option is selected.', + long_timeout: 'Long timeout (hours)', + name_hint: 'Name of your organization.', + otp_strategy_hint: 'Enforce users to use two-factor authentication (depending on the strategy chosen, an email can be sent if the secret is not stored in an authenticator app).', + otp_strategy: '2FA strategy', + portal_url_hint: 'Public base URL of the organisation portal.', + portal_url: 'Portal URL', + public_url_hint: 'Public base URL of the server that will be used when sending notification emails and building an OpenID Connect callback URL.', + public_url: 'Public URL', + short_timeout_hint: 'Ticket expiration timeout in hours.', + short_timeout: 'Short timeout (hours)', + signup_blacklist_hint: 'User allowed to sign up must not have an email address in the black listed domains.', + signup_blacklist_hint_form: 'User allowed to sign up must not have an email address in the black listed domains. The domain names are space or comma separated (ex: "gmail.com yahoo.com").', + signup_blacklist: 'Signup blacklist', + signup_enabled_hint: 'Agate sign up page is accessible. This does not affect the user join service offered to applications.', + signup_enabled: 'Signup enabled', + signup_username_hint: 'Allow users to choose their username when signing up, otherwise email will be used.', + signup_username: 'Signup with username', + signup_whitelist_hint: 'User allowed to sign up must have an email address in the white listed domains.', + signup_whitelist_hint_form: 'User allowed to sign up must have an email address in the white listed domains. The domain names are space or comma separated (ex: "institute.org who.int").', + signup_whitelist: 'Signup whitelist', + sso_domain_hint: 'Single sign-on domain.', + sso_domain: 'SSO Domain', + otp_strategies: { + NONE: 'None', + APP: 'Mobile app', + ANY: 'Email or mobile app', + }, + }, add: 'Add', administration: 'Administration', applications_caption: 'Manage applications, identifications and identity providers', @@ -123,8 +157,10 @@ export default { name_min_length: 'Name must be at least {min} characters', name_required: 'Name is required', name: 'Name', + number_invalid: 'Invalid number', other_links: 'Other links', otpEnabled: '2FA', + properties: 'Properties', realm: 'Realm', realms_caption: 'Manage realms, federate external identity providers', realms: 'Realms', @@ -134,6 +170,7 @@ export default { settings: 'Settings', source_code: 'Source Code', status: 'Status', + tickets: 'Tickets', users_caption: 'Manage users, assign role and groups to grant applications access', users: 'Users', }; diff --git a/agate-ui/src/i18n/fr/index.js b/agate-ui/src/i18n/fr/index.js index 5b13df1f..ce77549b 100644 --- a/agate-ui/src/i18n/fr/index.js +++ b/agate-ui/src/i18n/fr/index.js @@ -6,46 +6,46 @@ export default { powered_by: 'Propulsé par', }, application: { + add_realm_groups: 'Ajouter un domaine', + add_scope: 'Ajouter une permission', add: 'Ajouter une application', - edit: "Modifier l'application", - remove: "Suppression de l'application", - remove_confirm: "Veuillez confirmer la suppression de l'application: {name}", - saved: 'Application enregistrée', - save_failed: "Échec de l'enregistrement de l'application", - key: 'Clé', + auto_approval_hint: "Approuver automatiquement un usager qui s'est inscrit via l'application. Sinon, l'usager sera dans l'état \"En attente\", nécessitant une approbation manuelle.", + auto_approval: "Usager approuvé à l'inscription", copy_key: 'Copier la clé', + edit: "Modifier l'application", generate_key: 'Générer une clé', - redirect_uris: 'URLs de redirection', - redirect_uris_hint: "URL de rappel vers le serveur de l'application, requis dans le contexte OAuth. Utilisez des virgules pour séparer plusieurs URLs de rappel autorisées.", - key_required: 'La clé est requise', - key_hint: "Cette clé est utilisée pour authentifier l'application avec l'API.", + key_copied: 'Clé copiée', key_hint_edit: "Laisser vide pour ne pas modifier la clé secrète de l'application.", + key_hint: "Cette clé est utilisée pour authentifier l'application avec l'API.", key_min_length: 'La clé doit comporter au moins {min} caractères', - key_copied: 'Clé copiée', - auto_approval: "Usager approuvé à l'inscription", - auto_approval_hint: "Approuver automatiquement un usager qui s'est inscrit via l'application. Sinon, l'usager sera dans l'état \"En attente\", nécessitant une approbation manuelle.", - scopes: 'Permissions', - scopes_hint: 'Les permissions permettent de qualifier l\'accès d\'autorisation à l\'application qui est accordé dans le contexte OAuth. Les permissions sont optionnelles.', - add_scope: 'Ajouter une permission', + key_required: 'La clé est requise', + key: 'Clé', + realms_groups_hint: "Mapping entre des domaines et des groupes. Lorsque défini, les domaines correspondants seront proposés pour la connexion/inscription de l'usager à partir de l'application. Lorsqu'un usager rejoint une application, en utilisant un domaine, le(s) groupe(s) correspondant(s) seront automatiquement appliqués.", realms_groups: 'Domaines et groupes', - realms_groups_hint: 'Mapping entre des domaines et des groupes. Lorsque défini, les domaines correspondants seront proposés pour la connexion/inscription de l\'usager à partir de l\'application. Lorsqu\'un usager rejoint une application, en utilisant un domaine, le(s) groupe(s) correspondant(s) seront automatiquement appliqués.', - add_realm_groups: 'Ajouter un domaine', + redirect_uris_hint: "URL de rappel vers le serveur de l'application, requis dans le contexte OAuth. Utilisez des virgules pour séparer plusieurs URLs de rappel autorisées.", + redirect_uris: 'URLs de redirection', + remove_confirm: "Veuillez confirmer la suppression de l'application: {name}", + remove: "Suppression de l'application", + save_failed: "Échec de l'enregistrement de l'application", + saved: 'Application enregistrée', + scopes_hint: "Les permissions permettent de qualifier l'accès d'autorisation à l'application qui est accordé dans le contexte OAuth. Les permissions sont optionnelles.", + scopes: 'Permissions', }, group: { add: 'Ajouter un groupe', applications_hint: "Les membres d'un groupe ont accès aux applications associées à ce groupe.", edit: 'Modifier le groupe', - remove: 'Suppression du groupe', remove_confirm: 'Veuillez confirmer la suppression du groupe: {name}. Les usagers membres de ce groupe ne seront pas affectés.', - saved: 'Groupe enregistré', + remove: 'Suppression du groupe', save_failed: "Échec de l'enregistrement du groupe", + saved: 'Groupe enregistré', }, user: { add: 'Ajouter un usager', applications_hint: 'Les usagers peuvent être directement accordés un accès aux applications.', + approve_error: "Échec de l'approbation de l'usager", approve: "Approuver l'usager", approved: 'Usager approuvé', - approve_error: "Échec de l'approbation de l'usager", copy_password: 'Copier le mot de passe', disable_2fa: 'Désactiver le 2FA', edit: "Modifier l'usager", @@ -54,24 +54,24 @@ export default { groups_hint: 'Les usagers peuvent être membres de groupes pour accéder aux applications associées à ces groupes.', language: 'Langue', lastName: 'Nom de famille', - otp_disabled: '2FA désactivé', otp_disable_error: 'Échec de la désactivation du 2FA', - password: 'Mot de passe', + otp_disabled: '2FA désactivé', password_copied: 'Mot de passe copié', password_hint: 'Le mot de passe doit contenir au moins un chiffre, une lettre majuscule, une lettre minuscule, un caractère spécial (qui inclut @#$%^&+=!) et aucun espace blanc.', password_min_length: 'Le mot de passe doit comporter au moins {min} caractères', password_required: 'Le mot de passe est requis', + password: 'Mot de passe', realm_hint: "Domaine dans lequel l'usager est authentifié.", - reject: "Rejeter l'usager", reject_confirm: "Veuillez confirmer le rejet de l'usager: {name}", - remove: "Suppression de l'usager", + reject: "Rejeter l'usager", remove_confirm: "Veuillez confirmer la suppression de l'usager: {name}", - removed: 'Usager supprimé', remove_error: "Échec de la suppression de l'usager", - reset_password: 'Réinitialiser le mot de passe', + remove: "Suppression de l'usager", + removed: 'Usager supprimé', reset_password_confirm: 'Veuillez confirmer que vous souhaitez envoyer une notification de réinitialisation du mot de passe à cet usager.', reset_password_error: "Échec de la réinitialisation du mot de passe de l'usager", reset_password_success: 'Notification de réinitialisation du mot de passe envoyée', + reset_password: 'Réinitialiser le mot de passe', role: { 'agate-user': 'Usager', 'agate-administrator': 'Administrateur', @@ -97,6 +97,40 @@ export default { }, update_password: 'Mettre à jour le mot de passe', }, + system: { + inactive_timeout_hint: "Délai d'expiration du compte utilisateur en jours.", + inactive_timeout: "Délai d'expiration inactif (jours)", + languages_hint: 'Langues possibles pour les courriels de notification.', + languages: 'Langues', + long_timeout_hint: 'Délai d\'expiration du ticket en heures lorsque l\'option "se souvenir de moi" est sélectionnée.', + long_timeout: 'Délai long (heures)', + name_hint: 'Le nom de votre organisation.', + otp_strategy_hint: "Forcer l'activation du 2FA pour tous les usagers (selon la stratégie le code temporaire sera envoyé par courriel ou en utilisant une app mobile d'authentification).", + otp_strategy: 'Stratégie 2FA', + portal_url_hint: "URL de base publique du portail de l'organisation.", + portal_url: 'URL du portail', + public_url_hint: "URL de base publique du portail de l'organisation qui sera utilisée pour les liens dans les courriels de notification et les redirections OpenID Connect.", + public_url: 'URL publique', + short_timeout_hint: "Délai d'expiration du ticket en heures.", + short_timeout: 'Délai court (heures)', + signup_blacklist_hint: "Les usagers autorisés à s'inscrire ne doivent pas avoir d'adresse courriel dans les domaines de la liste noire.", + signup_blacklist_hint_form: "Les usagers autorisés à s'inscrire ne doivent pas avoir d'adresse courriel dans les domaines de la liste noire. Utilisez des virgules ou des espaces pour séparer plusieurs domaines.", + signup_blacklist: "Liste noire d'inscription", + signup_enabled_hint: "Permettre aux usagers de s'inscrire directement par Agate. Ceci n'affect pas le service offert aux applications.", + signup_enabled: 'Inscription activée', + signup_username_hint: "Permettre aux usagers de choisir leur nom d'usager lors de l'inscription, sinon le courriel sera utilisé.", + signup_username: "Inscription avec nom d'usager", + signup_whitelist_hint: "Les usagers autorisés à s'inscrire doivent avoir une adresse courriel dans les domaines de la liste blanche.", + signup_whitelist_hint_form: "Les usagers autorisés à s'inscrire doivent avoir une adresse courriel dans les domaines de la liste blanche. Utilisez des virgules ou des espaces pour séparer plusieurs domaines.", + signup_whitelist: "Liste blanche d'inscription", + sso_domain_hint: 'Domaine SSO utilisé.', + sso_domain: 'Domaine SSO', + otp_strategies: { + NONE: 'Aucun', + APP: 'Application mobile', + ANY: 'Courriel ou application mobile', + }, + }, add: 'ajouter', administration: 'Administration', applications_caption: "Gérer les applications, les identifications et les fournisseurs d'identité", @@ -123,8 +157,10 @@ export default { name_min_length: 'Le nom doit comporter au moins {min} caractères', name_required: 'Le nom est requis', name: 'Nom', + number_invalid: 'Nombre invalide', other_links: 'Autres liens', otpEnabled: '2FA', + properties: 'Propriétés', realm: 'Domaine', realms_caption: "Gérer les domaines, fédérer les fournisseurs d'identité externes", realms: 'Domaines', @@ -134,6 +170,7 @@ export default { settings: 'Paramètres', source_code: 'Code source', status: 'Statut', + tickets: 'Tickets', users_caption: 'Gérer les utilisateurs, les rôles et les accès aux applications', users: 'Utilisateurs', }; diff --git a/agate-ui/src/models/Agate.ts b/agate-ui/src/models/Agate.ts index 9ce18cc2..cfadfb95 100644 --- a/agate-ui/src/models/Agate.ts +++ b/agate-ui/src/models/Agate.ts @@ -4,18 +4,18 @@ // protoc v3.21.12 // source: Agate.proto -export const protobufPackage = "obiba.agate"; +export const protobufPackage = 'obiba.agate'; export enum KeyType { - KEY_PAIR = "KEY_PAIR", - CERTIFICATE = "CERTIFICATE", - UNRECOGNIZED = "UNRECOGNIZED", + KEY_PAIR = 'KEY_PAIR', + CERTIFICATE = 'CERTIFICATE', + UNRECOGNIZED = 'UNRECOGNIZED', } export enum RealmStatus { - INACTIVE = "INACTIVE", - ACTIVE = "ACTIVE", - UNRECOGNIZED = "UNRECOGNIZED", + INACTIVE = 'INACTIVE', + ACTIVE = 'ACTIVE', + UNRECOGNIZED = 'UNRECOGNIZED', } export interface SessionDto { @@ -71,9 +71,7 @@ export interface LocalizedStringDto { export interface ConfigurationDto { name: string; - domain?: - | string - | undefined; + domain?: string | undefined; /** hours */ shortTimeout: number; /** hours */ diff --git a/agate-ui/src/pages/SettingsPage.vue b/agate-ui/src/pages/SettingsPage.vue index cbb7545a..a7e2a340 100644 --- a/agate-ui/src/pages/SettingsPage.vue +++ b/agate-ui/src/pages/SettingsPage.vue @@ -7,16 +7,14 @@ -
{{ systemStore.configuration }}
+
{{ t('properties') }}
+
diff --git a/agate-ui/src/stores/system.ts b/agate-ui/src/stores/system.ts index 6b74ebb2..662142e3 100644 --- a/agate-ui/src/stores/system.ts +++ b/agate-ui/src/stores/system.ts @@ -24,10 +24,18 @@ export const useSystemStore = defineStore('system', () => { }); } + async function save(config: ConfigurationDto) { + return api.put('/config', config).then((response) => { + configuration.value = { ...config }; + return response; + }); + } + return { configuration, configurationPublic, init, initPub, + save, }; });