Browse Source

feat: admin auth UI + refs

NGPixel 1 year ago
parent
commit
349f4e5730

+ 23 - 0
server/db/migrations/3.0.0.mjs

@@ -414,6 +414,7 @@ export async function up (knex) {
   // -> GENERATE IDS
 
   const groupAdminId = uuid()
+  const groupUserId = uuid()
   const groupGuestId = '10000000-0000-4000-8000-000000000001'
   const siteId = uuid()
   const authModuleId = uuid()
@@ -658,6 +659,24 @@ export async function up (knex) {
       rules: JSON.stringify([]),
       isSystem: true
     },
+    {
+      id: groupUserId,
+      name: 'Users',
+      permissions: JSON.stringify(['read:pages', 'read:assets', 'read:comments']),
+      rules: JSON.stringify([
+        {
+          id: uuid(),
+          name: 'Default Rule',
+          roles: ['read:pages', 'read:assets', 'read:comments'],
+          match: 'START',
+          mode: 'ALLOW',
+          path: '',
+          locales: [],
+          sites: []
+        }
+      ]),
+      isSystem: true
+    },
     {
       id: groupGuestId,
       name: 'Guests',
@@ -744,6 +763,10 @@ export async function up (knex) {
       userId: userAdminId,
       groupId: groupAdminId
     },
+    {
+      userId: userAdminId,
+      groupId: groupUserId
+    },
     {
       userId: userGuestId,
       groupId: groupGuestId

+ 10 - 1
server/graph/resolvers/authentication.mjs

@@ -40,7 +40,16 @@ export default {
      * Fetch active authentication strategies
      */
     async authActiveStrategies (obj, args, context) {
-      return WIKI.db.authentication.getStrategies({ enabledOnly: args.enabledOnly })
+      const strategies = await WIKI.db.authentication.getStrategies({ enabledOnly: args.enabledOnly })
+      return strategies.map(a => {
+        const str = _.find(WIKI.data.authentication, ['key', a.module]) || {}
+        return {
+          ...a,
+          config: _.transform(str.props, (r, v, k) => {
+            r[k] = v.sensitive ? a.config[k] : '********'
+          }, {})
+        }
+      })
     },
     /**
      * Fetch site authentication strategies

+ 1 - 0
server/graph/schemas/authentication.graphql

@@ -86,6 +86,7 @@ extend type Mutation {
 type AuthenticationStrategy {
   key: String
   props: JSON
+  refs: JSON
   title: String
   description: String
   isAvailable: Boolean

+ 2 - 1
server/helpers/common.mjs

@@ -115,7 +115,8 @@ export function parseModuleProps (props) {
       multiline: value.multiline || false,
       sensitive: value.sensitive || false,
       icon: value.icon || 'rename',
-      order: value.order || 100
+      order: value.order || 100,
+      if: value.if ?? []
     })
     return result
   }, {})

+ 9 - 5
server/locales/en.json

@@ -67,28 +67,32 @@
   "admin.auth.activeStrategies": "Active Strategies",
   "admin.auth.addStrategy": "Add Strategy",
   "admin.auth.allowedWebOrigins": "Allowed Web Origins",
-  "admin.auth.autoEnrollGroups": "Assign to group",
-  "admin.auth.autoEnrollGroupsHint": "Automatically assign new users to these groups.",
+  "admin.auth.autoEnrollGroups": "Assign to group(s)",
+  "admin.auth.autoEnrollGroupsHint": "Automatically assign new users to these groups. New users are always added to the Users group regardless of this setting.",
   "admin.auth.callbackUrl": "Callback URL / Redirect URI",
   "admin.auth.configReference": "Configuration Reference",
   "admin.auth.configReferenceSubtitle": "Some strategies may require some configuration values to be set on your provider. These are provided for reference only and may not be needed by the current strategy.",
   "admin.auth.displayName": "Display Name",
   "admin.auth.displayNameHint": "The title shown to the end user for this authentication strategy.",
-  "admin.auth.domainsWhitelist": "Limit to specific email domains",
-  "admin.auth.domainsWhitelistHint": "A list of domains authorized to register. The user email address domain must match one of these to gain access.",
+  "admin.auth.domainsWhitelist": "Email Address Allowlist",
+  "admin.auth.domainsWhitelistHint": "Only allow users to register with an email address that matches the regex expression.",
   "admin.auth.enabled": "Enabled",
   "admin.auth.enabledForced": "This strategy cannot be disabled.",
   "admin.auth.enabledHint": "Should this strategy be available to sites for login.",
   "admin.auth.force2fa": "Force all users to use Two-Factor Authentication (2FA)",
   "admin.auth.force2faHint": "Users will be required to setup 2FA the first time they login and cannot be disabled by the user.",
   "admin.auth.globalAdvSettings": "Global Advanced Settings",
+  "admin.auth.info": "Info",
+  "admin.auth.infoName": "Name",
+  "admin.auth.infoNameHint": "Display name for this strategy.",
   "admin.auth.loginUrl": "Login URL",
   "admin.auth.logoutUrl": "Logout URL",
+  "admin.auth.noConfigOption": "This strategy has no configuration options you can modify.",
   "admin.auth.refreshSuccess": "List of strategies has been refreshed.",
   "admin.auth.registration": "Registration",
   "admin.auth.saveSuccess": "Authentication configuration saved successfully.",
   "admin.auth.security": "Security",
-  "admin.auth.selfRegistration": "Allow self-registration",
+  "admin.auth.selfRegistration": "Allow Self-Registration",
   "admin.auth.selfRegistrationHint": "Allow any user successfully authorized by the strategy to access the wiki.",
   "admin.auth.siteUrlNotSetup": "You must set a valid {siteUrl} first! Click on {general} in the left sidebar.",
   "admin.auth.status": "Status",

+ 7 - 0
server/modules/authentication/auth0/definition.yml

@@ -27,4 +27,11 @@ props:
     type: String
     title: Client Secret
     hint: Application Client Secret
+    sensitive: true
     order: 3
+refs:
+  callbackUrl:
+    title: Authorization Callback URL
+    hint: The callback endpoint to input on Auth0.
+    icon: back
+    value: '{host}/login/{id}/callback'

+ 1 - 1
server/modules/authentication/azure/definition.yml

@@ -26,5 +26,5 @@ props:
   cookieEncryptionKeyString:
     type: String
     title: Cookie Encryption Key String
-    hint: Random string with 44-character length.  Setting this enables workaround for Chrome's SameSite cookies.
+    hint: Random string with 44-character length. Setting this enables workaround for Chrome's SameSite cookies.
     order: 3

+ 7 - 0
server/modules/authentication/discord/definition.yml

@@ -18,9 +18,16 @@ props:
     type: String
     title: Client Secret
     hint: Application Client Secret
+    sensitive: true
     order: 2
   guildId:
     type: String
     title: Server ID
     hint: Optional - Your unique server identifier, such that only members are authorized
     order: 3
+refs:
+  callbackUrl:
+    title: Authorization Callback URL
+    hint: The callback endpoint to input on Discord.
+    icon: back
+    value: '{host}/login/{id}/callback'

+ 7 - 0
server/modules/authentication/dropbox/definition.yml

@@ -18,4 +18,11 @@ props:
     type: String
     title: App Secret
     hint: Application Client Secret
+    sensitive: true
     order: 2
+refs:
+  callbackUrl:
+    title: Authorization Callback URL
+    hint: The callback endpoint to input on Dropbox.
+    icon: back
+    value: '{host}/login/{id}/callback'

+ 7 - 0
server/modules/authentication/facebook/definition.yml

@@ -20,4 +20,11 @@ props:
     type: String
     title: App Secret
     hint: Application Secret
+    sensitive: true
     order: 2
+refs:
+  callbackUrl:
+    title: Authorization Callback URL
+    hint: The callback endpoint to input on Facebook.
+    icon: back
+    value: '{host}/login/{id}/callback'

+ 11 - 0
server/modules/authentication/github/definition.yml

@@ -18,6 +18,7 @@ props:
     type: String
     title: Client Secret
     hint: Application Client Secret
+    sensitive: true
     order: 2
   useEnterprise:
     type: Boolean
@@ -31,10 +32,20 @@ props:
     hint: GitHub Enterprise Only - Domain of your installation (e.g. github.company.com). Leave blank otherwise.
     default: ''
     order: 4
+    if:
+      - { key: 'useEnterprise', eq: true }
   enterpriseUserEndpoint:
     type: String
     title: GitHub Enterprise User Endpoint
     hint: GitHub Enterprise Only - Endpoint to fetch user details (e.g. https://api.github.com/user). Leave blank otherwise.
     default: 'https://api.github.com/user'
     order: 5
+    if:
+      - { key: 'useEnterprise', eq: true }
+refs:
+  callbackUrl:
+    title: Authorization Callback URL
+    hint: The callback endpoint to input on GitHub.
+    icon: back
+    value: '{host}/login/{id}/callback'
 

+ 7 - 0
server/modules/authentication/gitlab/definition.yml

@@ -18,6 +18,7 @@ props:
     type: String
     title: Client Secret
     hint: Application Client Secret
+    sensitive: true
     order: 2
   baseUrl:
     type: String
@@ -25,3 +26,9 @@ props:
     hint: For self-managed GitLab instances, define the base URL (e.g. https://gitlab.example.com). Leave default for GitLab.com SaaS (https://gitlab.com).
     default: https://gitlab.com
     order: 3
+refs:
+  callbackUrl:
+    title: Authorization Callback URL
+    hint: The callback endpoint to input on GitLab.
+    icon: back
+    value: '{host}/login/{id}/callback'

+ 7 - 0
server/modules/authentication/google/definition.yml

@@ -22,9 +22,16 @@ props:
     type: String
     title: Client Secret
     hint: Application Client Secret
+    sensitive: true
     order: 2
   hostedDomain:
     type: String
     title: Hosted Domain
     hint: (optional) Only for G Suite hosted domain. Leave empty otherwise.
     order: 3
+refs:
+  callbackUrl:
+    title: Authorization Callback URL
+    hint: The callback endpoint to input on Google.
+    icon: back
+    value: '{host}/login/{id}/callback'

+ 7 - 1
server/modules/authentication/keycloak/definition.yml

@@ -28,6 +28,7 @@ props:
     type: String
     title: Client Secret
     hint: Application Client Secret
+    sensitive: true
     order: 4
   authorizationURL:
     type: String
@@ -54,4 +55,9 @@ props:
     title: Logout Endpoint URL
     hint: e.g. https://KEYCLOAK-HOST/auth/realms/YOUR-REALM/protocol/openid-connect/logout
     order: 9
-
+refs:
+  callbackUrl:
+    title: Authorization Callback URL
+    hint: The callback endpoint to input on Keycloak.
+    icon: back
+    value: '{host}/login/{id}/callback'

+ 1 - 0
server/modules/authentication/ldap/definition.yml

@@ -29,6 +29,7 @@ props:
     type: String
     hint: The password of the account used above for binding.
     maxWidth: 600
+    sensitive: true
     order: 3
   searchBase:
     title: Search Base

+ 1 - 0
server/modules/authentication/microsoft/definition.yml

@@ -22,4 +22,5 @@ props:
     type: String
     title: Client Secret
     hint: Application Client Secret
+    sensitive: true
     order: 2

+ 7 - 0
server/modules/authentication/oauth2/definition.yml

@@ -18,6 +18,7 @@ props:
     type: String
     title: Client Secret
     hint: Application Client Secret
+    sensitive: true
     order: 2
   authorizationURL:
     type: String
@@ -71,3 +72,9 @@ props:
     title: Pass access token via GET query string to User Info Endpoint
     hint: (optional) Pass the access token in an `access_token` parameter attached to the GET query string of the User Info Endpoint URL. Otherwise the access token will be passed in the Authorization header.
     order: 11
+refs:
+  callbackUrl:
+    title: Authorization Callback URL
+    hint: The callback endpoint to input on the oauth2 server.
+    icon: back
+    value: '{host}/login/{id}/callback'

+ 7 - 0
server/modules/authentication/oidc/definition.yml

@@ -22,6 +22,7 @@ props:
     type: String
     title: Client Secret
     hint: Application Client Secret
+    sensitive: true
     order: 2
   authorizationURL:
     type: String
@@ -55,3 +56,9 @@ props:
     title: Logout URL
     hint: (optional) Logout URL on the OAuth2 provider where the user will be redirected to complete the logout process.
     order: 8
+refs:
+  callbackUrl:
+    title: Authorization Callback URL
+    hint: The callback endpoint to input on OIDC server.
+    icon: back
+    value: '{host}/login/{id}/callback'

+ 7 - 0
server/modules/authentication/okta/definition.yml

@@ -29,6 +29,7 @@ props:
     type: String
     hint: 40 chars alphanumeric string with a hyphen(s)
     maxWidth: 600
+    sensitive: true
     order: 3
   idp:
     title: Identity Provider ID (idp)
@@ -36,3 +37,9 @@ props:
     hint: (Optional) - 20 chars alphanumeric string
     maxWidth: 400
     order: 4
+refs:
+  callbackUrl:
+    title: Authorization Callback URL
+    hint: The callback endpoint to input on Okta.
+    icon: back
+    value: '{host}/login/{id}/callback'

+ 7 - 0
server/modules/authentication/rocketchat/definition.yml

@@ -22,9 +22,16 @@ props:
     type: String
     title: Client Secret
     hint: Application Client Secret
+    sensitive: true
     order: 2
   siteURL:
     type: String
     title: Rocket.chat Site URL
     hint: The base URL of your Rocket.chat site (e.g. https://example.rocket.chat)
     order: 3
+refs:
+  callbackUrl:
+    title: Authorization Callback URL
+    hint: The callback endpoint to input on Rocket.chat.
+    icon: back
+    value: '{host}/login/{id}/callback'

+ 7 - 0
server/modules/authentication/slack/definition.yml

@@ -22,9 +22,16 @@ props:
     type: String
     title: Client Secret
     hint: Application Client Secret
+    sensitive: true
     order: 2
   team:
     type: String
     title: Team / Workspace ID
     hint: Optional - Your unique team (workspace) identifier
     order: 3
+refs:
+  callbackUrl:
+    title: Authorization Callback URL
+    hint: The callback endpoint to input on Slack.
+    icon: back
+    value: '{host}/login/{id}/callback'

+ 1 - 0
ux/public/_assets/icons/ultraviolet-back.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg"  viewBox="0 0 40 40" width="80px" height="80px"><path fill="#dff0fe" d="M19.5 30.5H22.5V34.5H19.5z"/><path fill="#4788c7" d="M22,31v3h-2v-3H22 M23,30h-4v5h4V30L23,30z"/><path fill="#dff0fe" d="M12.5 30.5H15.5V34.5H12.5z"/><path fill="#4788c7" d="M15,31v3h-2v-3H15 M16,30h-4v5h4V30L16,30z"/><g><path fill="#dff0fe" d="M5.5 30.5H8.5V34.5H5.5z"/><path fill="#4788c7" d="M8,31v3H6v-3H8 M9,30H5v5h4V30L9,30z"/></g><g><path fill="#dff0fe" d="M26.5,34.5v-4H27c4.136,0,7.5-3.364,7.5-7.5s-3.364-7.5-7.5-7.5H10.5v6.335L1.726,13.5L10.5,5.165 V11.5H27c6.341,0,11.5,5.159,11.5,11.5S33.341,34.5,27,34.5H26.5z"/><path fill="#4788c7" d="M10,6.329V11v1h1h16c6.065,0,11,4.935,11,11s-4.935,11-11,11h0v-3h0c4.411,0,8-3.589,8-8 s-3.589-8-8-8H11h-1v1v4.671L2.452,13.5L10,6.329 M11,4L1,13.5L11,23v-7h16c3.86,0,7,3.14,7,7s-3.14,7-7,7h-1v5h1 c6.617,0,12-5.383,12-12s-5.383-12-12-12H11V4L11,4z"/></g></svg>

+ 1 - 0
ux/public/_assets/icons/ultraviolet-private.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg"  viewBox="0 0 40 40" width="80px" height="80px"><path fill="#98ccfd" d="M20,38.5C9.799,38.5,1.5,30.201,1.5,20S9.799,1.5,20,1.5S38.5,9.799,38.5,20S30.201,38.5,20,38.5z"/><path fill="#4788c7" d="M20,2c9.925,0,18,8.075,18,18s-8.075,18-18,18S2,29.925,2,20S10.075,2,20,2 M20,1 C9.507,1,1,9.507,1,20s8.507,19,19,19s19-8.507,19-19S30.493,1,20,1L20,1z"/><path fill="#fff" d="M23 20h3v-9.5C26 9.672 25.328 9 24.5 9h0C23.672 9 23 9.672 23 10.5V20zM15 20h3V8.5C18 7.672 17.328 7 16.5 7h0C15.672 7 15 7.672 15 8.5V20zM11 23h3V12.5c0-.828-.672-1.5-1.5-1.5h0c-.828 0-1.5.672-1.5 1.5V23zM19 20h3V7.5C22 6.672 21.328 6 20.5 6h0C19.672 6 19 6.672 19 7.5V20zM21.554 27.567l2.879 2.879 6.971-6.971c.795-.795.795-2.084 0-2.879l0 0c-.795-.795-2.084-.795-2.879 0L21.554 27.567z"/><path fill="#fff" d="M20.678,32H16c-2.761,0-5-2.239-5-5v-9h15v8.678C26,29.617,23.617,32,20.678,32z"/></svg>

File diff suppressed because it is too large
+ 0 - 0
ux/public/_assets/icons/ultraviolet-register.svg


+ 1 - 0
ux/public/_assets/icons/ultraviolet-shutdown.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg"  viewBox="0 0 40 40" width="80px" height="80px"><path fill="#98ccfd" d="M20,38.5C9.799,38.5,1.5,30.201,1.5,20S9.799,1.5,20,1.5S38.5,9.799,38.5,20S30.201,38.5,20,38.5z"/><path fill="#4788c7" d="M20,2c9.925,0,18,8.075,18,18s-8.075,18-18,18S2,29.925,2,20S10.075,2,20,2 M20,1 C9.507,1,1,9.507,1,20s8.507,19,19,19s19-8.507,19-19S30.493,1,20,1L20,1z"/><path fill="#fff" d="M20.002,31.001L19.84,31C13.866,30.916,9,25.983,9,20.004l0.001-0.154 c0.04-2.825,1.144-5.493,3.109-7.515c0.384-0.396,1.017-0.405,1.414-0.02c0.396,0.385,0.405,1.018,0.02,1.414 c-1.608,1.654-2.511,3.836-2.543,6.145L11,20.004c0,4.891,3.981,8.926,8.874,8.996l0.128,0.001c4.893,0,8.928-3.981,8.997-8.874 L29,19.996c0-2.354-0.904-4.581-2.547-6.27c-0.385-0.396-0.376-1.029,0.02-1.414c0.396-0.385,1.028-0.376,1.414,0.02 C29.895,14.397,31,17.119,31,19.996l-0.001,0.165C30.916,26.136,25.982,31.001,20.002,31.001z"/><path fill="#fff" d="M20,19L20,19c-0.55,0-1-0.45-1-1V8c0-0.55,0.45-1,1-1l0,0c0.55,0,1,0.45,1,1v10 C21,18.55,20.55,19,20,19z"/></svg>

+ 239 - 235
ux/src/pages/AdminAuth.vue

@@ -27,7 +27,7 @@ q-page.admin-mail
       )
   q-separator(inset)
   .row.q-pa-md.q-col-gutter-md
-    .col-auto
+    .col-12.col-lg-auto
       q-card.rounded-borders.bg-dark
         q-list(
           style='min-width: 350px;'
@@ -35,8 +35,8 @@ q-page.admin-mail
           dark
           )
           q-item(
-            v-for='str of combinedActiveStrategies'
-            :key='str.key'
+            v-for='str of state.activeStrategies'
+            :key='str.id'
             active-class='bg-primary text-white'
             :active='state.selectedStrategy === str.id'
             @click='state.selectedStrategy = str.id'
@@ -77,20 +77,124 @@ q-page.admin-mail
     .col
       q-card.q-pb-sm
         q-card-section
-          .text-subtitle1 {{t('admin.storage.contentTypes')}}
-          .text-body2.text-grey {{ t('admin.storage.contentTypesHint') }}
+          .text-subtitle1 {{t('admin.auth.info')}}
+        q-item
+          blueprint-icon(icon='information')
+          q-item-section
+            q-item-label {{t(`admin.auth.infoName`)}}
+            q-item-label(caption) {{t(`admin.auth.infoNameHint`)}}
+          q-item-section
+            q-input(
+              outlined
+              v-model='state.strategy.displayName'
+              dense
+              hide-bottom-space
+              :aria-label='t(`admin.auth.infoName`)'
+              )
+        q-separator.q-my-sm(inset)
+        q-item(tag='label')
+          blueprint-icon(icon='shutdown')
+          q-item-section
+            q-item-label {{t(`admin.auth.enabled`)}}
+            q-item-label(caption) {{t(`admin.auth.enabledHint`)}}
+            q-item-label.text-deep-orange(v-if='state.strategy.strategy.key === `local`', caption) {{t(`admin.auth.enabledForced`)}}
+          q-item-section(avatar)
+            q-toggle(
+              v-model='state.strategy.isEnabled'
+              :disable='state.strategy.strategy.key === `local`'
+              color='primary'
+              checked-icon='las la-check'
+              unchecked-icon='las la-times'
+              :aria-label='t(`admin.auth.enabled`)'
+              )
+        q-separator.q-my-sm(inset)
+        q-item(tag='label')
+          blueprint-icon(icon='register')
+          q-item-section
+            q-item-label {{t(`admin.auth.selfRegistration`)}}
+            q-item-label(caption) {{t(`admin.auth.selfRegistrationHint`)}}
+          q-item-section(avatar)
+            q-toggle(
+              v-model='state.strategy.selfRegistration'
+              color='primary'
+              checked-icon='las la-check'
+              unchecked-icon='las la-times'
+              :aria-label='t(`admin.auth.selfRegistration`)'
+              )
+        template(v-if='state.strategy.selfRegistration')
+          q-separator.q-my-sm(inset)
+          q-item
+            blueprint-icon(icon='team')
+            q-item-section
+              q-item-label {{t(`admin.auth.autoEnrollGroups`)}}
+              q-item-label(caption) {{t(`admin.auth.autoEnrollGroupsHint`)}}
+            q-item-section
+              q-select(
+                outlined
+                :options='state.groups'
+                v-model='state.strategy.autoEnrollGroups'
+                multiple
+                map-options
+                emit-value
+                option-value='id'
+                option-label='name'
+                options-dense
+                dense
+                hide-bottom-space
+                :aria-label='t(`admin.users.groups`)'
+                :loading='state.loadingGroups'
+                )
+                template(v-slot:selected)
+                  .text-caption(v-if='state.strategy.autoEnrollGroups?.length > 1')
+                    i18n-t(keypath='admin.users.groupsSelected')
+                      template(#count)
+                        strong {{ state.strategy.autoEnrollGroups?.length }}
+                  .text-caption(v-else-if='state.strategy.autoEnrollGroups?.length === 1')
+                    i18n-t(keypath='admin.users.groupSelected')
+                      template(#group)
+                        strong {{ selectedGroupName }}
+                  span(v-else)
+                template(v-slot:option='{ itemProps, opt, selected, toggleOption }')
+                  q-item(
+                    v-bind='itemProps'
+                    )
+                    q-item-section(side)
+                      q-checkbox(
+                        size='sm'
+                        :model-value='selected'
+                        @update:model-value='toggleOption(opt)'
+                        )
+                    q-item-section
+                      q-item-label {{opt.name}}
+
+          q-separator.q-my-sm(inset)
+          q-item
+            blueprint-icon(icon='private')
+            q-item-section
+              q-item-label {{t(`admin.auth.domainsWhitelist`)}}
+              q-item-label(caption) {{t(`admin.auth.domainsWhitelistHint`)}}
+            q-item-section
+              q-input(
+                outlined
+                v-model='state.strategy.domainWhitelist'
+                dense
+                hide-bottom-space
+                :aria-label='t(`admin.auth.domainsWhitelist`)'
+                prefix='/'
+                suffix='/'
+                )
 
       //- -----------------------
       //- Configuration
       //- -----------------------
       q-card.q-pb-sm.q-mt-md
         q-card-section
-          .text-subtitle1 {{t('admin.storage.config')}}
+          .text-subtitle1 {{t('admin.auth.strategyConfiguration')}}
           q-banner.q-mt-md(
             v-if='!state.strategy.config || Object.keys(state.strategy.config).length < 1'
             rounded
             :class='$q.dark.isActive ? `bg-negative text-white` : `bg-grey-2 text-grey-7`'
-            ) {{t('admin.storage.noConfigOption')}}
+            ) {{t('admin.auth.noConfigOption')}}
         template(
           v-for='(cfg, cfgKey, idx) in state.strategy.config'
           )
@@ -148,228 +252,62 @@ q-page.admin-mail
                   outlined
                   v-model='cfg.value'
                   dense
-                  :type='cfg.multiline ? `textarea` : `input`'
+                  :type='cfg.multiline ? `textarea` : (cfg.sensitive ? `password` : `input`)'
                   :aria-label='cfg.title'
                   :disable='cfg.readOnly'
                   )
 
-    .col-auto(v-if='state.selectedStrategy && state.strategy')
       //- -----------------------
-      //- Infobox
+      //- References
       //- -----------------------
-      q-card.rounded-borders.q-pb-md(style='width: 350px;')
+      q-card.q-pb-sm.q-mt-md(v-if='strategyRefs.length > 0')
         q-card-section
-          .text-subtitle1 {{state.strategy.strategy.title}}
-          q-img.q-mt-sm.rounded-borders(
-            :src='state.strategy.strategy.logo'
-            fit='contain'
-            no-spinner
-            style='height: 100px;'
-          )
-          .text-body2.q-mt-md {{state.strategy.strategy.description}}
-        q-separator.q-mb-sm(inset)
-        q-item
+          .text-subtitle1 {{t('admin.auth.configReference')}}
+        q-item(v-for='strRef of strategyRefs', :key='strRef.key')
+          blueprint-icon(:icon='strRef.icon', :hue-rotate='-45')
           q-item-section
-            q-item-label.text-grey {{t(`admin.auth.vendor`)}}
-            q-item-label {{state.strategy.strategy.vendor}}
-        q-separator.q-my-sm(inset)
-        q-item
+            q-item-label {{strRef.title}}
+            q-item-label(caption) {{strRef.hint}}
           q-item-section
-            q-item-label.text-grey {{t(`admin.auth.vendorWebsite`)}}
-            q-item-label: a(:href='state.strategy.strategy.website', target='_blank', rel='noreferrer') {{state.strategy.strategy.website}}
-
+            q-input(
+              outlined
+              v-model='strRef.value'
+              dense
+              :aria-label='strRef.title'
+              readonly
+            )
       //- -----------------------
-      //- Status
+      //- Infobox
       //- -----------------------
-      q-card.rounded-borders.q-pb-md.q-mt-md(style='width: 350px;')
-        q-card-section
-          .text-subtitle1 {{t(`admin.auth.status`)}}
-        q-item(tag='label')
-          q-item-section
-            q-item-label {{t(`admin.auth.enabled`)}}
-            q-item-label(caption) {{t(`admin.auth.enabledHint`)}}
-            q-item-label.text-deep-orange(v-if='state.strategy.strategy.key === `local`', caption) {{t(`admin.auth.enabledForced`)}}
-          q-item-section(avatar)
-            q-toggle(
-              v-model='state.strategy.isEnabled'
-              :disable='state.strategy.strategy.key === `local`'
-              color='primary'
-              checked-icon='las la-check'
-              unchecked-icon='las la-times'
-              :aria-label='t(`admin.auth.enabled`)'
-              )
-        q-separator.q-my-sm(inset)
-        q-item
-          q-item-section
-            q-btn.acrylic-btn(
-              icon='las la-trash-alt'
-              flat
-              color='negative'
-              :disable='state.strategy.strategy.key === `local`'
-              label='Delete Strategy'
-              )
-
-    //- v-flex(xs12, lg9)
-    //-   v-card.animated.fadeInUp.wait-p2s
-    //-     v-toolbar(color='primary', dense, flat, dark)
-    //-       .subtitle-1 {{strategy.displayName}} #[em ({{strategy.strategy.title}})]
-    //-       v-spacer
-    //-       v-btn(small, outlined, dark, color='white', :disabled='strategy.key === `local`', @click='deleteStrategy()')
-    //-         v-icon(left) mdi-close
-    //-         span {{$t('common.actions.delete')}}
-    //-     v-card-info(color='blue')
-    //-       div
-    //-         span {{strategy.strategy.description}}
-    //-         .caption: a(:href='strategy.strategy.website') {{strategy.strategy.website}}
-    //-       v-spacer
-    //-       .admin-providerlogo
-    //-         img(:src='strategy.strategy.logo', :alt='strategy.strategy.title')
-    //-     v-card-text
-    //-       .row
-    //-         .col-8
-    //-           v-text-field(
-    //-             outlined
-    //-             :label='$t(`admin.auth.displayName`)'
-    //-             v-model='strategy.displayName'
-    //-             prepend-icon='mdi-format-title'
-    //-             :hint='$t(`admin.auth.displayNameHint`)'
-    //-             persistent-hint
-    //-             )
-    //-         .col-4
-    //-           v-switch.mt-1(
-    //-             :label='$t(`admin.auth.strategyIsEnabled`)'
-    //-             v-model='strategy.isEnabled'
-    //-             color='primary'
-    //-             prepend-icon='mdi-power'
-    //-             :hint='$t(`admin.auth.strategyIsEnabledHint`)'
-    //-             persistent-hint
-    //-             inset
-    //-             :disabled='strategy.key === `local`'
-    //-             )
-    //-       template(v-if='strategy.config && Object.keys(strategy.config).length > 0')
-    //-         v-divider
-    //-         .overline.my-5 {{$t('admin.auth.strategyConfiguration')}}
-    //-         .pr-3
-    //-           template(v-for='cfg in strategy.config')
-    //-             v-select.mb-3(
-    //-               v-if='cfg.value.type === "string" && cfg.value.enum'
-    //-               outlined
-    //-               :items='cfg.value.enum'
-    //-               :key='cfg.key'
-    //-               :label='cfg.value.title'
-    //-               v-model='cfg.value.value'
-    //-               prepend-icon='mdi-cog-box'
-    //-               :hint='cfg.value.hint ? cfg.value.hint : ""'
-    //-               persistent-hint
-    //-               :class='cfg.value.hint ? "mb-2" : ""'
-    //-               :style='cfg.value.maxWidth > 0 ? `max-width:` + cfg.value.maxWidth + `px;` : ``'
-    //-             )
-    //-             v-switch.mb-6(
-    //-               v-else-if='cfg.value.type === "boolean"'
-    //-               :key='cfg.key'
-    //-               :label='cfg.value.title'
-    //-               v-model='cfg.value.value'
-    //-               color='primary'
-    //-               prepend-icon='mdi-cog-box'
-    //-               :hint='cfg.value.hint ? cfg.value.hint : ""'
-    //-               persistent-hint
-    //-               inset
-    //-               )
-    //-             v-textarea.mb-3(
-    //-               v-else-if='cfg.value.type === "string" && cfg.value.multiline'
-    //-               outlined
-    //-               :key='cfg.key'
-    //-               :label='cfg.value.title'
-    //-               v-model='cfg.value.value'
-    //-               prepend-icon='mdi-cog-box'
-    //-               :hint='cfg.value.hint ? cfg.value.hint : ""'
-    //-               persistent-hint
-    //-               :class='cfg.value.hint ? "mb-2" : ""'
-    //-               )
-    //-             v-text-field.mb-3(
-    //-               v-else
-    //-               outlined
-    //-               :key='cfg.key'
-    //-               :label='cfg.value.title'
-    //-               v-model='cfg.value.value'
-    //-               prepend-icon='mdi-cog-box'
-    //-               :hint='cfg.value.hint ? cfg.value.hint : ""'
-    //-               persistent-hint
-    //-               :class='cfg.value.hint ? "mb-2" : ""'
-    //-               :style='cfg.value.maxWidth > 0 ? `max-width:` + cfg.value.maxWidth + `px;` : ``'
-    //-               )
-    //-       v-divider
-    //-       .overline.my-5 {{$t('admin.auth.registration')}}
-    //-       .pr-3
-    //-         v-switch.ml-3(
-    //-           v-model='strategy.selfRegistration'
-    //-           :label='$t(`admin.auth.selfRegistration`)'
-    //-           color='primary'
-    //-           :hint='$t(`admin.auth.selfRegistrationHint`)'
-    //-           persistent-hint
-    //-           inset
-    //-         )
-    //-         v-combobox.ml-3.mt-5(
-    //-           :label='$t(`admin.auth.domainsWhitelist`)'
-    //-           v-model='strategy.domainWhitelist'
-    //-           prepend-icon='mdi-email-check-outline'
-    //-           outlined
-    //-           :disabled='!strategy.selfRegistration'
-    //-           :hint='$t(`admin.auth.domainsWhitelistHint`)'
-    //-           persistent-hint
-    //-           small-chips
-    //-           deletable-chips
-    //-           clearable
-    //-           multiple
-    //-           chips
-    //-           )
-    //-         v-autocomplete.mt-3.ml-3(
-    //-           outlined
-    //-           :disabled='!strategy.selfRegistration'
-    //-           :items='groups'
-    //-           item-text='name'
-    //-           item-value='id'
-    //-           :label='$t(`admin.auth.autoEnrollGroups`)'
-    //-           v-model='strategy.autoEnrollGroups'
-    //-           prepend-icon='mdi-account-group'
-    //-           :hint='$t(`admin.auth.autoEnrollGroupsHint`)'
-    //-           small-chips
-    //-           persistent-hint
-    //-           deletable-chips
-    //-           clearable
-    //-           multiple
-    //-           chips
-    //-           )
+      q-card.q-mt-md
+        q-card-section.text-center
+          q-img.rounded-borders(
+            :src='state.strategy.strategy.logo'
+            fit='contain'
+            no-spinner
+            style='height: 100px; max-width: 300px;'
+          )
+          .text-subtitle2.q-mt-sm {{state.strategy.strategy.title}}
+          .text-caption.q-mt-sm {{state.strategy.strategy.description}}
+          .text-caption.q-mt-sm: strong {{state.strategy.strategy.vendor}}
+          .text-caption: a(:href='state.strategy.strategy.website', target='_blank', rel='noreferrer') {{state.strategy.strategy.website}}
 
-    //-   v-card.mt-4.wiki-form.animated.fadeInUp.wait-p4s(v-if='selectedStrategy !== `local`')
-    //-     v-toolbar(color='primary', dense, flat, dark)
-    //-       .subtitle-1 {{$t('admin.auth.configReference')}}
-    //-     v-card-text
-    //-       .body-2 {{$t('admin.auth.configReferenceSubtitle')}}
-    //-       v-alert.mt-3.radius-7(v-if='host.length < 8', color='red', outlined, :value='true', icon='mdi-alert')
-    //-         i18next(path='admin.auth.siteUrlNotSetup', tag='span')
-    //-           strong(place='siteUrl') {{$t('admin.general.siteUrl')}}
-    //-           strong(place='general') {{$t('admin.general.title')}}
-    //-       .pa-3.mt-3.radius-7.grey(v-else, :class='$vuetify.theme.dark ? `darken-3-d5` : `lighten-3`')
-    //-         .body-2: strong {{$t('admin.auth.allowedWebOrigins')}}
-    //-         .body-2 {{host}}
-    //-         v-divider.my-3
-    //-         .body-2: strong {{$t('admin.auth.callbackUrl')}}
-    //-         .body-2 {{host}}/login/{{strategy.key}}/callback
-    //-         v-divider.my-3
-    //-         .body-2: strong {{$t('admin.auth.loginUrl')}}
-    //-         .body-2 {{host}}/login
-    //-         v-divider.my-3
-    //-         .body-2: strong {{$t('admin.auth.logoutUrl')}}
-    //-         .body-2 {{host}}
-    //-         v-divider.my-3
-    //-         .body-2: strong {{$t('admin.auth.tokenEndpointAuthMethod')}}
-    //-         .body-2 HTTP-POST
+      .flex.q-mt-md
+        .text-caption.text-grey ID: {{ state.strategy.id }}
+        q-space
+        q-btn.acrylic-btn(
+          icon='las la-trash-alt'
+          flat
+          color='negative'
+          :disable='state.strategy.strategy.key === `local`'
+          label='Delete Strategy'
+          @click='deleteStrategy(state.strategy.id)'
+          )
 </template>
 
 <script setup>
 import gql from 'graphql-tag'
-import { find, reject, transform } from 'lodash-es'
+import { cloneDeep, find, reject, transform } from 'lodash-es'
 import { v4 as uuid } from 'uuid'
 
 import { useI18n } from 'vue-i18n'
@@ -402,6 +340,7 @@ useMeta({
 
 const state = reactive({
   loading: 0,
+  loadingGroups: true,
   groups: [],
   strategies: [],
   activeStrategies: [],
@@ -417,25 +356,26 @@ const state = reactive({
 const availableStrategies = computed(() => {
   return state.strategies.filter(str => str.key !== 'local')
 })
-
-const combinedActiveStrategies = computed(() => {
-  return state.activeStrategies.map(str => ({
-    ...str,
-    strategy: find(state.strategies, ['key', str.strategy?.key]) || {}
-  }))
+const selectedGroupName = computed(() => {
+  return state.groups.filter(g => g.id === state.strategy?.autoEnrollGroups?.[0])[0]?.name
+})
+const strategyRefs = computed(() => {
+  if (!state.selectedStrategy) { return [] }
+  const str = find(state.strategies, ['key', state.strategy?.strategy.key])
+  if (!str || !str.refs) { return [] }
+  return Object.entries(str.refs).map(([k, v]) => {
+    return {
+      ...v,
+      key: k,
+      value: v.value.replaceAll('{host}', window.location.origin).replaceAll('{id}', state.selectedStrategy)
+    }
+  }) ?? []
 })
 
 // WATCHERS
 
 watch(() => state.selectedStrategy, (newValue, oldValue) => {
-  const str = find(combinedActiveStrategies.value, ['id', newValue]) || {}
-  str.config = transform(str.strategy.props, (cfg, v, k) => {
-    cfg[k] = {
-      ...v,
-      value: str.config[k]
-    }
-  }, {})
-  state.strategy = str
+  state.strategy = find(state.activeStrategies, ['id', newValue]) || {}
 })
 watch(() => state.activeStrategies, (newValue, oldValue) => {
   state.selectedStrategy = newValue[0]?.id
@@ -443,6 +383,25 @@ watch(() => state.activeStrategies, (newValue, oldValue) => {
 
 // METHODS
 
+async function loadGroups () {
+  state.loading++
+  state.loadingGroups = true
+  const resp = await APOLLO_CLIENT.query({
+    query: gql`
+      query getGroupsForAdminAuth {
+        groups {
+          id
+          name
+        }
+      }
+    `,
+    fetchPolicy: 'network-only'
+  })
+  state.groups = cloneDeep(resp?.data?.groups?.filter(g => g.id !== '10000000-0000-4000-8000-000000000001') ?? [])
+  state.loadingGroups = false
+  state.loading--
+}
+
 async function load () {
   state.loading++
   $q.loading.show()
@@ -452,6 +411,7 @@ async function load () {
         authStrategies {
           key
           props
+          refs
           title
           description
           isAvailable
@@ -480,7 +440,33 @@ async function load () {
     fetchPolicy: 'network-only'
   })
   state.strategies = resp?.data?.authStrategies || []
-  state.activeStrategies = resp?.data?.authActiveStrategies || []
+  state.activeStrategies = (cloneDeep(resp?.data?.authActiveStrategies) || []).map(a => {
+    const str = cloneDeep(find(state.strategies, ['key', a.strategy.key])) || {}
+    a.strategy = str
+    a.config = transform(str.props, (r, v, k) => {
+      r[k] = {
+        ...v,
+        value: a.config?.[k],
+        ...v.enum && {
+          enum: v.enum.map(o => {
+            if (o.indexOf('|') > 0) {
+              const oParsed = o.split('|')
+              return {
+                value: oParsed[0],
+                label: oParsed[1]
+              }
+            } else {
+              return {
+                value: o,
+                label: o
+              }
+            }
+          })
+        }
+      }
+    }, {})
+    return a
+  })
   $q.loading.hide()
   state.loading--
 }
@@ -493,15 +479,32 @@ function configIfCheck (ifs) {
 function addStrategy (str) {
   const newStr = {
     id: uuid(),
-    strategy: {
-      key: str.key
-    },
-    config: transform(str.props, (cfg, v, k) => {
-      cfg[k] = v.default
+    strategy: str,
+    config: transform(str.props, (r, v, k) => {
+      r[k] = {
+        ...v,
+        value: v.default,
+        ...v.enum && {
+          enum: v.enum.map(o => {
+            if (o.indexOf('|') > 0) {
+              const oParsed = o.split('|')
+              return {
+                value: oParsed[0],
+                label: oParsed[1]
+              }
+            } else {
+              return {
+                value: o,
+                label: o
+              }
+            }
+          })
+        }
+      }
     }, {}),
-    isEnabled: false,
+    isEnabled: true,
     displayName: str.title,
-    selfRegistration: false,
+    selfRegistration: true,
     domainWhitelist: [],
     autoEnrollGroups: []
   }
@@ -660,5 +663,6 @@ async function save () {
 
 onMounted(() => {
   load()
+  loadGroups()
 })
 </script>

Some files were not shown because too many files changed in this diff