浏览代码

fix: various auth improvements + other fixes

NGPixel 1 年之前
父节点
当前提交
5a60fb11b5

+ 1 - 1
.devcontainer/docker-compose.yml

@@ -28,7 +28,7 @@ services:
     # (Adding the "ports" property to this file will not forward from a Codespace.)
 
   db:
-    image: postgres:16beta1
+    image: postgres:16rc1
     restart: unless-stopped
     volumes:
       - postgres-data:/var/lib/postgresql/data

+ 3 - 5
config.sample.yml

@@ -2,7 +2,7 @@
 # Wiki.js - CONFIGURATION                                             #
 #######################################################################
 # Full documentation + examples:
-# https://js.wiki/docs/install
+# https://next.js.wiki/docs/install
 
 # ---------------------------------------------------------------------
 # Port the server should listen to
@@ -13,7 +13,7 @@ port: 3000
 # ---------------------------------------------------------------------
 # Database
 # ---------------------------------------------------------------------
-# PostgreSQL 11 or later required
+# PostgreSQL 12 or later required
 
 db:
   host: localhost
@@ -21,9 +21,7 @@ db:
   user: postgres
   pass: postgres
   db: postgres
-  schemas:
-    wiki: wiki
-    scheduler: scheduler
+  schema: wiki
   ssl: false
 
   # Optional

+ 1 - 3
server/app/data.yml

@@ -16,9 +16,7 @@ defaults:
       ssl: false
       sslOptions:
         auto: true
-      schemas:
-        wiki: wiki
-        scheduler: scheduler
+      schema: wiki
     ssl:
       enabled: false
     pool:

+ 3 - 3
server/core/db.mjs

@@ -86,7 +86,7 @@ export default {
       useNullAsDefault: true,
       asyncStackTraces: WIKI.IS_DEBUG,
       connection: this.config,
-      searchPath: [WIKI.config.db.schemas.wiki],
+      searchPath: [WIKI.config.db.schema],
       pool: {
         ...workerMode ? { min: 0, max: 1 } : WIKI.config.pool,
         async afterCreate(conn, done) {
@@ -223,12 +223,12 @@ export default {
    */
   async syncSchemas () {
     WIKI.logger.info('Ensuring DB schema exists...')
-    await this.knex.raw(`CREATE SCHEMA IF NOT EXISTS ${WIKI.config.db.schemas.wiki}`)
+    await this.knex.raw(`CREATE SCHEMA IF NOT EXISTS ${WIKI.config.db.schema}`)
     WIKI.logger.info('Ensuring DB migrations have been applied...')
     return this.knex.migrate.latest({
       tableName: 'migrations',
       migrationSource,
-      schemaName: WIKI.config.db.schemas.wiki
+      schemaName: WIKI.config.db.schema
     })
   },
   /**

+ 2 - 2
server/db/migrations/3.0.0.mjs

@@ -67,8 +67,8 @@ export async function up (knex) {
       table.string('displayName').notNullable().defaultTo('')
       table.jsonb('config').notNullable().defaultTo('{}')
       table.boolean('selfRegistration').notNullable().defaultTo(false)
-      table.jsonb('domainWhitelist').notNullable().defaultTo('[]')
-      table.jsonb('autoEnrollGroups').notNullable().defaultTo('[]')
+      table.string('allowedEmailRegex')
+      table.specificType('autoEnrollGroups', 'uuid[]')
     })
     .createTable('commentProviders', table => {
       table.uuid('id').notNullable().primary().defaultTo(knex.raw('gen_random_uuid()'))

+ 44 - 3
server/graph/resolvers/authentication.mjs

@@ -57,7 +57,7 @@ export default {
     async authSiteStrategies (obj, args, context, info) {
       const site = await WIKI.db.sites.query().findById(args.siteId)
       const activeStrategies = await WIKI.db.authentication.getStrategies({ enabledOnly: true })
-      return activeStrategies.map(str => {
+      const siteStrategies = _.sortBy(activeStrategies.map(str => {
         const siteAuth = _.find(site.config.authStrategies, ['id', str.id]) || {}
         return {
           id: str.id,
@@ -65,7 +65,8 @@ export default {
           order: siteAuth.order ?? 0,
           isVisible: siteAuth.isVisible ?? false
         }
-      })
+      }), ['order'])
+      return args.visibleOnly ? siteStrategies.filter(s => s.isVisible) : siteStrategies
     }
   },
   Mutation: {
@@ -196,6 +197,10 @@ export default {
      */
     async setApiState (obj, args, context) {
       try {
+        if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) {
+          throw new Error('ERR_FORBIDDEN')
+        }
+
         WIKI.config.api.isEnabled = args.enabled
         await WIKI.configSvc.saveToDb(['api'])
         return {
@@ -210,6 +215,10 @@ export default {
      */
     async revokeApiKey (obj, args, context) {
       try {
+        if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) {
+          throw new Error('ERR_FORBIDDEN')
+        }
+
         await WIKI.db.apiKeys.query().findById(args.id).patch({
           isRevoked: true
         })
@@ -227,11 +236,14 @@ export default {
      */
     async updateAuthStrategies (obj, args, context) {
       try {
+        if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) {
+          throw new Error('ERR_FORBIDDEN')
+        }
+
         const previousStrategies = await WIKI.db.authentication.getStrategies()
         for (const str of args.strategies) {
           const newStr = {
             displayName: str.displayName,
-            order: str.order,
             isEnabled: str.isEnabled,
             config: _.reduce(str.config, (result, value, key) => {
               _.set(result, `${value.key}`, _.get(JSON.parse(value.value), 'v', null))
@@ -280,6 +292,10 @@ export default {
      */
     async regenerateCertificates (obj, args, context) {
       try {
+        if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) {
+          throw new Error('ERR_FORBIDDEN')
+        }
+
         await WIKI.auth.regenerateCertificates()
         return {
           responseResult: generateSuccess('Certificates have been regenerated successfully.')
@@ -293,6 +309,10 @@ export default {
      */
     async resetGuestUser (obj, args, context) {
       try {
+        if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) {
+          throw new Error('ERR_FORBIDDEN')
+        }
+
         await WIKI.auth.resetGuestUser()
         return {
           responseResult: generateSuccess('Guest user has been reset successfully.')
@@ -302,7 +322,28 @@ export default {
       }
     }
   },
+  // ------------------------------------------------------------------
+  // TYPE: AuthenticationActiveStrategy
+  // ------------------------------------------------------------------
   AuthenticationActiveStrategy: {
+    config (obj, args, context) {
+      if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) {
+        throw new Error('ERR_FORBIDDEN')
+      }
+      return obj.config ?? {}
+    },
+    allowedEmailRegex (obj, args, context) {
+      if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) {
+        throw new Error('ERR_FORBIDDEN')
+      }
+      return obj.allowedEmailRegex ?? ''
+    },
+    autoEnrollGroups (obj, args, context) {
+      if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) {
+        throw new Error('ERR_FORBIDDEN')
+      }
+      return obj.autoEnrollGroups ?? []
+    },
     strategy (obj, args, context) {
       return _.find(WIKI.data.authentication, ['key', obj.module])
     }

+ 11 - 11
server/graph/resolvers/page.mjs

@@ -483,7 +483,7 @@ export default {
           user: context.req.user
         })
         return {
-          responseResult: generateSuccess('Page has been converted.')
+          operation: generateSuccess('Page has been converted.')
         }
       } catch (err) {
         return generateError(err)
@@ -499,7 +499,7 @@ export default {
           user: context.req.user
         })
         return {
-          responseResult: generateSuccess('Page has been moved.')
+          operation: generateSuccess('Page has been moved.')
         }
       } catch (err) {
         return generateError(err)
@@ -515,7 +515,7 @@ export default {
           user: context.req.user
         })
         return {
-          responseResult: generateSuccess('Page has been deleted.')
+          operation: generateSuccess('Page has been deleted.')
         }
       } catch (err) {
         return generateError(err)
@@ -534,7 +534,7 @@ export default {
           throw new Error('This tag does not exist.')
         }
         return {
-          responseResult: generateSuccess('Tag has been deleted.')
+          operation: generateSuccess('Tag has been deleted.')
         }
       } catch (err) {
         return generateError(err)
@@ -555,7 +555,7 @@ export default {
           throw new Error('This tag does not exist.')
         }
         return {
-          responseResult: generateSuccess('Tag has been updated successfully.')
+          operation: generateSuccess('Tag has been updated successfully.')
         }
       } catch (err) {
         return generateError(err)
@@ -569,7 +569,7 @@ export default {
         await WIKI.db.pages.flushCache()
         WIKI.events.outbound.emit('flushCache')
         return {
-          responseResult: generateSuccess('Pages Cache has been flushed successfully.')
+          operation: generateSuccess('Pages Cache has been flushed successfully.')
         }
       } catch (err) {
         return generateError(err)
@@ -582,7 +582,7 @@ export default {
       try {
         const count = await WIKI.db.pages.migrateToLocale(args)
         return {
-          responseResult: generateSuccess('Migrated content to target locale successfully.'),
+          operation: generateSuccess('Migrated content to target locale successfully.'),
           count
         }
       } catch (err) {
@@ -596,7 +596,7 @@ export default {
       try {
         await WIKI.db.pages.rebuildTree()
         return {
-          responseResult: generateSuccess('Page tree rebuilt successfully.')
+          operation: generateSuccess('Page tree rebuilt successfully.')
         }
       } catch (err) {
         return generateError(err)
@@ -613,7 +613,7 @@ export default {
         }
         await WIKI.db.pages.renderPage(page)
         return {
-          responseResult: generateSuccess('Page rendered successfully.')
+          operation: generateSuccess('Page rendered successfully.')
         }
       } catch (err) {
         return generateError(err)
@@ -649,7 +649,7 @@ export default {
         })
 
         return {
-          responseResult: generateSuccess('Page version restored successfully.')
+          operation: generateSuccess('Page version restored successfully.')
         }
       } catch (err) {
         return generateError(err)
@@ -662,7 +662,7 @@ export default {
       try {
         await WIKI.db.pageHistory.purge(args.olderThan)
         return {
-          responseResult: generateSuccess('Page history purged successfully.')
+          operation: generateSuccess('Page history purged successfully.')
         }
       } catch (err) {
         return generateError(err)

+ 4 - 5
server/graph/schemas/authentication.graphql

@@ -106,14 +106,13 @@ type AuthenticationActiveStrategy {
   isEnabled: Boolean
   config: JSON
   selfRegistration: Boolean
-  domainWhitelist: [String]
-  autoEnrollGroups: [Int]
+  allowedEmailRegex: String
+  autoEnrollGroups: [UUID]
 }
 
 type AuthenticationSiteStrategy {
   id: UUID
   activeStrategy: AuthenticationActiveStrategy
-  order: Int
   isVisible: Boolean
 }
 
@@ -146,8 +145,8 @@ input AuthenticationStrategyInput {
   order: Int!
   isEnabled: Boolean!
   selfRegistration: Boolean!
-  domainWhitelist: [String]!
-  autoEnrollGroups: [Int]!
+  allowedEmailRegex: String!
+  autoEnrollGroups: [UUID]!
 }
 
 type AuthenticationApiKey {

+ 3 - 3
server/locales/en.json

@@ -66,16 +66,16 @@
   "admin.audit.title": "Audit Log",
   "admin.auth.activeStrategies": "Active Strategies",
   "admin.auth.addStrategy": "Add Strategy",
+  "admin.auth.allowedEmailRegex": "Allowed Email Address Regex",
+  "admin.auth.allowedEmailRegexHint": "(optional) Only allow users to register with an email address that matches the regex expression.",
   "admin.auth.allowedWebOrigins": "Allowed Web Origins",
   "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.autoEnrollGroupsHint": "(optional) 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": "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.",

+ 1 - 6
server/models/authentication.mjs

@@ -34,12 +34,7 @@ export class Authentication extends Model {
   }
 
   static async getStrategies({ enabledOnly = false } = {}) {
-    const strategies = await WIKI.db.authentication.query().where(enabledOnly ? { isEnabled: true } : {})
-    return strategies.map(str => ({
-      ...str,
-      domainWhitelist: get(str.domainWhitelist, 'v', []),
-      autoEnrollGroups: get(str.autoEnrollGroups, 'v', [])
-    }))
+    return WIKI.db.authentication.query().where(enabledOnly ? { isEnabled: true } : {})
   }
 
   static async refreshStrategiesFromDisk() {

+ 6 - 21
server/models/pages.mjs

@@ -1045,15 +1045,12 @@ export class Page extends Model {
     await WIKI.db.pages.deletePageFromCache(page.hash)
     WIKI.events.outbound.emit('deletePageFromCache', page.hash)
 
-    // -> Delete from Search Index
-    await WIKI.data.searchEngine.deleted(page)
-
     // -> Delete from Storage
     if (!opts.skipStorage) {
-      await WIKI.db.storage.pageEvent({
-        event: 'deleted',
-        page
-      })
+      // await WIKI.db.storage.pageEvent({
+      //   event: 'deleted',
+      //   page
+      // })
     }
 
     // -> Reconnect Links
@@ -1076,6 +1073,8 @@ export class Page extends Model {
    * @returns {Promise} Promise with no value
    */
   static async reconnectLinks (opts) {
+    return
+    // TODO: fix this
     const pageHref = `/${opts.locale}/${opts.path}`
     let replaceArgs = {
       from: '',
@@ -1142,20 +1141,6 @@ export class Page extends Model {
     }
   }
 
-  /**
-   * Rebuild page tree for new/updated/deleted page
-   *
-   * @returns {Promise} Promise with no value
-   */
-  static async rebuildTree() {
-    const rebuildJob = await WIKI.scheduler.registerJob({
-      name: 'rebuild-tree',
-      immediate: true,
-      worker: true
-    })
-    return rebuildJob.finished
-  }
-
   /**
    * Trigger the rendering of a page
    *

+ 0 - 1
ux/src/components/AuthLoginPanel.vue

@@ -461,7 +461,6 @@ async function fetchStrategies (showAll = false) {
             }
             selfRegistration
           }
-          order
         }
       }
     `,

+ 9 - 9
ux/src/pages/AdminAuth.vue

@@ -171,15 +171,15 @@ q-page.admin-mail
           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-label {{t(`admin.auth.allowedEmailRegex`)}}
+              q-item-label(caption) {{t(`admin.auth.allowedEmailRegexHint`)}}
             q-item-section
               q-input(
                 outlined
-                v-model='state.strategy.domainWhitelist'
+                v-model='state.strategy.allowedEmailRegex'
                 dense
                 hide-bottom-space
-                :aria-label='t(`admin.auth.domainsWhitelist`)'
+                :aria-label='t(`admin.auth.allowedEmailRegex`)'
                 prefix='/'
                 suffix='/'
                 )
@@ -193,8 +193,8 @@ q-page.admin-mail
           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.auth.noConfigOption')}}
+            :class='$q.dark.isActive ? `bg-dark-4 text-grey-5` : `bg-grey-2 text-grey-7`'
+            ): em {{t('admin.auth.noConfigOption')}}
         template(
           v-for='(cfg, cfgKey, idx) in state.strategy.config'
           )
@@ -213,7 +213,7 @@ q-page.admin-mail
                   color='primary'
                   checked-icon='las la-check'
                   unchecked-icon='las la-times'
-                  :aria-label='t(`admin.general.allowComments`)'
+                  :aria-label='cfg.title'
                   :disable='cfg.readOnly'
                   )
             q-item(v-else)
@@ -432,7 +432,7 @@ async function load () {
           isEnabled
           config
           selfRegistration
-          domainWhitelist
+          allowedEmailRegex
           autoEnrollGroups
         }
       }
@@ -505,7 +505,7 @@ function addStrategy (str) {
     isEnabled: true,
     displayName: str.title,
     selfRegistration: true,
-    domainWhitelist: [],
+    allowedEmailRegex: '',
     autoEnrollGroups: []
   }
   state.activeStrategies = [...state.activeStrategies, newStr]

+ 57 - 35
ux/src/pages/AdminLogin.vue

@@ -145,30 +145,30 @@ q-page.admin-login
         q-card-section
           .text-subtitle1 {{t('admin.login.providers')}}
         q-card-section.admin-login-providers.q-pt-none
-          draggable(
-            class='q-list rounded-borders'
+          sortable(
+            class='q-list'
             :list='state.providers'
-            :animation='150'
-            handle='.handle'
-            @end='dragStarted = false'
             item-key='id'
+            :options='sortableOptions'
+            @end='updateAuthPosition'
             )
             template(#item='{element}')
               q-item
                 q-item-section(side)
-                  q-icon.handle(name='las la-bars')
-                blueprint-icon(:icon='element.icon')
+                  q-icon.handle(name='mdi-drag-horizontal')
+                q-item-section(side)
+                  q-icon(:name='`img:` + element.activeStrategy.strategy.icon')
                 q-item-section
-                  q-item-label {{element.label}}
-                  q-item-label(caption) {{element.provider}}
+                  q-item-label {{element.activeStrategy.displayName}}
+                  q-item-label(caption) {{element.activeStrategy.strategy.title}}
                 q-item-section(side)
                   q-toggle(
-                    v-model='element.isActive'
+                    v-model='element.isVisible'
                     color='primary'
                     checked-icon='las la-check'
                     unchecked-icon='las la-times'
                     label='Visible'
-                    :aria-label='element.label'
+                    :aria-label='element.activeStrategy.displayName'
                   )
         q-item.q-pt-none
           q-item-section
@@ -183,7 +183,7 @@ q-page.admin-login
 <script setup>
 import { cloneDeep } from 'lodash-es'
 import gql from 'graphql-tag'
-import draggable from 'vuedraggable'
+import { Sortable } from 'sortablejs-vue3'
 
 import { useI18n } from 'vue-i18n'
 import { useMeta, useQuasar } from 'quasar'
@@ -224,11 +224,18 @@ const state = reactive({
     welcomeRedirect: '/',
     logoutRedirect: '/'
   },
-  providers: [
-    { id: 'local', label: 'Local Authentication', provider: 'Username-Password', icon: 'database', isActive: true },
-    { id: 'google', label: 'Google', provider: 'Google', icon: 'google', isActive: true },
-    { id: 'slack', label: 'Slack', provider: 'Slack', icon: 'slack', isActive: false }
-  ]
+  providers: []
+})
+
+const sortableOptions = {
+  handle: '.handle',
+  animation: 150
+}
+
+// WATCHERS
+
+watch(() => adminStore.currentSiteId, (newValue) => {
+  load()
 })
 
 // METHODS
@@ -236,24 +243,34 @@ const state = reactive({
 async function load () {
   state.loading++
   $q.loading.show()
-  // const resp = await APOLLO_CLIENT.query({
-  //   query: gql`
-  //     query getSite (
-  //       $id: UUID!
-  //     ) {
-  //       siteById(
-  //         id: $id
-  //       ) {
-  //         id
-  //       }
-  //     }
-  //   `,
-  //   variables: {
-  //     id: adminStore.currentSiteId
-  //   },
-  //   fetchPolicy: 'network-only'
-  // })
-  // this.config = cloneDeep(resp?.data?.siteById)
+  const resp = await APOLLO_CLIENT.query({
+    query: gql`
+      query getSiteAuthStrategies (
+        $siteId: UUID!
+      ) {
+        authSiteStrategies(
+          siteId: $siteId
+          visibleOnly: false
+        ) {
+          id
+          activeStrategy {
+            displayName
+            strategy {
+              key
+              title
+              icon
+            }
+          }
+          isVisible
+        }
+      }
+    `,
+    variables: {
+      siteId: adminStore.currentSiteId
+    },
+    fetchPolicy: 'network-only'
+  })
+  state.providers = cloneDeep(resp?.data?.authSiteStrategies)
   $q.loading.hide()
   state.loading--
 }
@@ -300,6 +317,11 @@ async function save () {
   state.loading--
 }
 
+function updateAuthPosition (ev) {
+  const item = state.providers.splice(ev.oldIndex, 1)[0]
+  state.providers.splice(ev.newIndex, 0, item)
+}
+
 async function uploadBg () {
   const input = document.createElement('input')
   input.type = 'file'