fix: assetById permissions + set min pg requirement to 16

vega
NGPixel 2 months ago
parent c3f562b315
commit 7a3d78bbac
No known key found for this signature in database
GPG Key ID: B755FB6870B30F63

@ -117,8 +117,8 @@ The server **dev** should already be available under **Servers**. If that's not
### Requirements
- PostgreSQL **12** or later *(**16** or later recommended)*
- Node.js **20.x** or later
- PostgreSQL **16** or later
- Node.js **24.x** or later
- [pnpm](https://pnpm.io/installation#using-corepack)
### Usage

@ -3,7 +3,7 @@ import { generateError, generateSuccess } from '../../helpers/graph.mjs'
export default {
Query: {
async analyticsProviders(obj, args, context, info) {
async analyticsProviders (obj, args, context, info) {
if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) {
throw new Error('ERR_FORBIDDEN')
}
@ -30,13 +30,13 @@ export default {
}
},
Mutation: {
async updateAnalyticsProviders(obj, args, context) {
async updateAnalyticsProviders (obj, args, context) {
try {
if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) {
throw new Error('ERR_FORBIDDEN')
}
for (let str of args.providers) {
for (const str of args.providers) {
await WIKI.db.analytics.query().patch({
isEnabled: str.isEnabled,
config: reduce(str.config, (result, value, key) => {

@ -1,7 +1,7 @@
import _ from 'lodash-es'
import sanitize from 'sanitize-filename'
import { generateError, generateSuccess } from '../../helpers/graph.mjs'
import { decodeFolderPath, decodeTreePath, generateHash } from '../../helpers/common.mjs'
import { decodeTreePath, generateHash } from '../../helpers/common.mjs'
import path from 'node:path'
import fs from 'fs-extra'
import { v4 as uuid } from 'uuid'
@ -9,10 +9,13 @@ import { pipeline } from 'node:stream/promises'
export default {
Query: {
async assetById(obj, args, context) {
// FIXME: Perm
const asset = await WIKI.db.assets.query().findById(args.id)
async assetById (obj, args, context) {
const asset = await WIKI.db.assets.query().findById(args.id).withGraphFetched('tree')
if (asset) {
const assetPath = asset.tree.folderPath ? `${decodeTreePath(asset.tree.folderPath)}/${asset.tree.fileName}` : asset.tree.fileName
if (!WIKI.auth.checkAccess(context.req.user, ['read:assets'], { path: assetPath })) {
throw new Error('ERR_FORBIDDEN')
}
return asset
} else {
throw new Error('ERR_ASSET_NOT_FOUND')
@ -23,7 +26,7 @@ export default {
/**
* Rename an Asset
*/
async renameAsset(obj, args, context) {
async renameAsset (obj, args, context) {
try {
const filename = sanitize(args.fileName).toLowerCase()
@ -50,13 +53,13 @@ export default {
}
// Check source asset permissions
const assetSourcePath = (treeItem.folderPath) ? decodeTreePath(decodeFolderPath(treeItem.folderPath)) + `/${treeItem.fileName}` : treeItem.fileName
const assetSourcePath = (treeItem.folderPath) ? decodeTreePath(treeItem.folderPath) + `/${treeItem.fileName}` : treeItem.fileName
if (!WIKI.auth.checkAccess(context.req.user, ['manage:assets'], { path: assetSourcePath })) {
throw new Error('ERR_FORBIDDEN')
}
// Check target asset permissions
const assetTargetPath = (treeItem.folderPath) ? decodeTreePath(decodeFolderPath(treeItem.folderPath)) + `/${filename}` : filename
const assetTargetPath = (treeItem.folderPath) ? decodeTreePath(treeItem.folderPath) + `/${filename}` : filename
if (!WIKI.auth.checkAccess(context.req.user, ['write:assets'], { path: assetTargetPath })) {
throw new Error('ERR_TARGET_FORBIDDEN')
}
@ -102,12 +105,12 @@ export default {
/**
* Delete an Asset
*/
async deleteAsset(obj, args, context) {
async deleteAsset (obj, args, context) {
try {
const treeItem = await WIKI.db.tree.query().findById(args.id)
if (treeItem) {
// Check permissions
const assetPath = (treeItem.folderPath) ? decodeTreePath(decodeFolderPath(treeItem.folderPath)) + `/${treeItem.fileName}` : treeItem.fileName
const assetPath = (treeItem.folderPath) ? decodeTreePath(treeItem.folderPath) + `/${treeItem.fileName}` : treeItem.fileName
if (!WIKI.auth.checkAccess(context.req.user, ['manage:assets'], { path: assetPath })) {
throw new Error('ERR_FORBIDDEN')
}
@ -144,7 +147,7 @@ export default {
/**
* Upload Assets
*/
async uploadAssets(obj, args, context) {
async uploadAssets (obj, args, context) {
try {
// FIXME: Perm
// -> Get Folder
@ -354,7 +357,7 @@ export default {
const failedResults = results.filter(r => r.status === 'rejected')
if (failedResults.length > 0) {
// -> One or more thrown errors
WIKI.logger.warn(`Failed to upload one or more assets:`)
WIKI.logger.warn('Failed to upload one or more assets:')
for (const failedResult of failedResults) {
WIKI.logger.warn(failedResult.reason)
}
@ -380,7 +383,7 @@ export default {
/**
* Flush Temporary Uploads
*/
async flushTempUploads(obj, args, context) {
async flushTempUploads (obj, args, context) {
try {
if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) {
throw new Error('ERR_FORBIDDEN')

@ -1,7 +1,5 @@
import _ from 'lodash-es'
import {
decodeFolderPath,
encodeFolderPath,
decodeTreePath,
encodeTreePath
} from '../../helpers/common.mjs'
@ -50,12 +48,12 @@ export default {
if (args.parentId) {
const parent = await WIKI.db.knex('tree').where('id', args.parentId).first()
if (parent) {
parentPath = (parent.folderPath ? `${decodeFolderPath(parent.folderPath)}.${parent.fileName}` : parent.fileName)
parentPath = (parent.folderPath ? `${parent.folderPath}.${parent.fileName}` : parent.fileName)
}
} else if (args.parentPath) {
parentPath = encodeTreePath(args.parentPath)
}
const folderPathCondition = parentPath ? `${encodeFolderPath(parentPath)}.${depthCondition}` : depthCondition
const folderPathCondition = parentPath ? `${parentPath}.${depthCondition}` : depthCondition
// Fetch Items
const items = await WIKI.db.knex('tree')
@ -67,7 +65,7 @@ export default {
const parentPathParts = parentPath.split('.')
for (let i = 0; i <= parentPathParts.length; i++) {
builder.orWhere({
folderPath: encodeFolderPath(_.dropRight(parentPathParts, i).join('.')),
folderPath: _.dropRight(parentPathParts, i).join('.'),
fileName: _.nth(parentPathParts, i * -1),
type: 'folder'
})
@ -103,7 +101,7 @@ export default {
id: item.id,
depth: item.depth,
type: item.type,
folderPath: decodeTreePath(decodeFolderPath(item.folderPath)),
folderPath: decodeTreePath(item.folderPath),
fileName: item.fileName,
title: item.title,
tags: item.tags ?? [],

@ -50,26 +50,6 @@ export function encodeTreePath (str) {
return str?.toLowerCase()?.replaceAll('/', '.') || ''
}
/**
* Encode a folder path (to support legacy PostgresSQL ltree)
*
* @param {string} val String to encode
* @returns Encoded folder path
*/
export function encodeFolderPath (val) {
return WIKI.db.LEGACY ? val?.replaceAll('-', '_') : val
}
/**
* Decode a folder path (to support legacy PostgresSQL ltree)
*
* @param {string} val String to decode
* @returns Decoded folder path
*/
export function decodeFolderPath (val) {
return WIKI.db.LEGACY ? val?.replaceAll('_', '-') : val
}
/**
* Generate SHA-1 Hash of a string
*

@ -4,34 +4,35 @@ import fse from 'fs-extra'
import { startsWith } from 'lodash-es'
import { generateHash } from '../helpers/common.mjs'
import { Tree } from './tree.mjs'
import { User } from './users.mjs'
/**
* Users model
*/
export class Asset extends Model {
static get tableName() { return 'assets' }
static get tableName () { return 'assets' }
static get jsonSchema () {
return {
type: 'object',
properties: {
id: {type: 'string'},
filename: {type: 'string'},
hash: {type: 'string'},
ext: {type: 'string'},
kind: {type: 'string'},
mime: {type: 'string'},
fileSize: {type: 'integer'},
metadata: {type: 'object'},
createdAt: {type: 'string'},
updatedAt: {type: 'string'}
id: { type: 'string' },
filename: { type: 'string' },
hash: { type: 'string' },
ext: { type: 'string' },
kind: { type: 'string' },
mime: { type: 'string' },
fileSize: { type: 'integer' },
metadata: { type: 'object' },
createdAt: { type: 'string' },
updatedAt: { type: 'string' }
}
}
}
static get relationMappings() {
static get relationMappings () {
return {
author: {
relation: Model.BelongsToOneRelation,
@ -40,23 +41,32 @@ export class Asset extends Model {
from: 'assets.authorId',
to: 'users.id'
}
},
tree: {
relation: Model.HasOneRelation,
modelClass: Tree,
join: {
from: 'assets.id',
to: 'tree.id'
}
}
}
}
async $beforeUpdate(opt, context) {
async $beforeUpdate (opt, context) {
await super.$beforeUpdate(opt, context)
this.updatedAt = new Date().toISOString()
}
async $beforeInsert(context) {
async $beforeInsert (context) {
await super.$beforeInsert(context)
this.createdAt = new Date().toISOString()
this.updatedAt = new Date().toISOString()
}
async getAssetPath() {
async getAssetPath () {
let hierarchy = []
if (this.folderId) {
hierarchy = await WIKI.db.assetFolders.getHierarchy(this.folderId)
@ -64,11 +74,11 @@ export class Asset extends Model {
return (this.folderId) ? hierarchy.map(h => h.slug).join('/') + `/${this.filename}` : this.filename
}
async deleteAssetCache() {
async deleteAssetCache () {
await fse.remove(path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, `cache/${this.hash}.dat`))
}
static async upload(opts) {
static async upload (opts) {
const fileInfo = path.parse(opts.originalname)
// Check for existing asset
@ -78,7 +88,7 @@ export class Asset extends Model {
}).first()
// Build Object
let assetRow = {
const assetRow = {
filename: opts.originalname,
ext: fileInfo.ext,
kind: startsWith(opts.mimetype, 'image/') ? 'image' : 'binary',
@ -158,7 +168,9 @@ export class Asset extends Model {
return WIKI.db.tree.query()
.select('tree.*', 'assets.preview', 'assets.previewState')
.innerJoin('assets', 'tree.id', 'assets.id')
.where(id ? { 'tree.id': id } : {
.where(id
? { 'tree.id': id }
: {
'tree.hash': generateHash(path),
'tree.locale': locale,
'tree.siteId': siteId
@ -166,7 +178,7 @@ export class Asset extends Model {
.first()
}
static async getAsset({ pathArgs, siteId }, res) {
static async getAsset ({ pathArgs, siteId }, res) {
try {
const fileInfo = path.parse(pathArgs.path.toLowerCase())
const fileHash = generateHash(pathArgs.path)
@ -185,7 +197,7 @@ export class Asset extends Model {
// }
await WIKI.db.assets.getAssetFromDb({ pathArgs, fileHash, cachePath, siteId }, res)
} catch (err) {
if (err.code === `ECONNABORTED` || err.code === `EPIPE`) {
if (err.code === 'ECONNABORTED' || err.code === 'EPIPE') {
return
}
WIKI.logger.error(err)
@ -193,7 +205,7 @@ export class Asset extends Model {
}
}
static async getAssetFromCache({ cachePath, extName }, res) {
static async getAssetFromCache ({ cachePath, extName }, res) {
try {
await fse.access(cachePath, fse.constants.R_OK)
} catch (err) {
@ -204,13 +216,13 @@ export class Asset extends Model {
return true
}
static async getAssetFromStorage(assetPath, res) {
static async getAssetFromStorage (assetPath, res) {
const localLocations = await WIKI.db.storage.getLocalLocations({
asset: {
path: assetPath
}
})
for (let location of localLocations.filter(location => Boolean(location.path))) {
for (const location of localLocations.filter(location => Boolean(location.path))) {
const assetExists = await WIKI.db.assets.getAssetFromCache(assetPath, location.path, res)
if (assetExists) {
return true
@ -219,7 +231,7 @@ export class Asset extends Model {
return false
}
static async getAssetFromDb({ pathArgs, fileHash, cachePath, siteId }, res) {
static async getAssetFromDb ({ pathArgs, fileHash, cachePath, siteId }, res) {
const asset = await WIKI.db.knex('tree').where({
siteId,
hash: fileHash
@ -234,7 +246,7 @@ export class Asset extends Model {
}
}
static async flushTempUploads() {
return fse.emptyDir(path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, `uploads`))
static async flushTempUploads () {
return fse.emptyDir(path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, 'uploads'))
}
}

@ -1,9 +1,7 @@
import { Model } from 'objection'
import { differenceWith, dropRight, last, nth } from 'lodash-es'
import {
decodeFolderPath,
decodeTreePath,
encodeFolderPath,
encodeTreePath,
generateHash
} from '../helpers/common.mjs'
@ -85,7 +83,7 @@ export class Tree extends Model {
const parentPath = encodeTreePath(path)
const parentPathParts = parentPath.split('.')
const parentFilter = {
folderPath: encodeFolderPath(dropRight(parentPathParts).join('.')),
folderPath: dropRight(parentPathParts).join('.'),
fileName: last(parentPathParts)
}
const parent = await WIKI.db.knex('tree').where({
@ -143,7 +141,7 @@ export class Tree extends Model {
const pageEntry = await WIKI.db.knex('tree').insert({
id,
folderPath: encodeFolderPath(folderPath),
folderPath,
fileName,
type: 'page',
title,
@ -191,7 +189,7 @@ export class Tree extends Model {
const assetEntry = await WIKI.db.knex('tree').insert({
id,
folderPath: encodeFolderPath(folderPath),
folderPath,
fileName,
type: 'asset',
title,
@ -231,7 +229,7 @@ export class Tree extends Model {
WIKI.logger.debug(`Creating new folder ${pathName}...`)
const parentPathParts = parentPath.split('.')
const parentFilter = {
folderPath: encodeFolderPath(dropRight(parentPathParts).join('.')),
folderPath: dropRight(parentPathParts).join('.'),
fileName: last(parentPathParts)
}
@ -242,7 +240,7 @@ export class Tree extends Model {
if (!parent) {
throw new Error('ERR_FOLDER_PARENT_INVALID')
}
parentPath = parent.folderPath ? `${decodeFolderPath(parent.folderPath)}.${parent.fileName}` : parent.fileName
parentPath = parent.folderPath ? `${parent.folderPath}.${parent.fileName}` : parent.fileName
} else if (parentPath) {
parent = await WIKI.db.knex('tree').where(parentFilter).first()
} else {
@ -253,7 +251,7 @@ export class Tree extends Model {
const existingFolder = await WIKI.db.knex('tree').select('id').where({
siteId,
locale,
folderPath: encodeFolderPath(parentPath),
folderPath: parentPath,
fileName: pathName,
type: 'folder'
}).first()
@ -268,7 +266,7 @@ export class Tree extends Model {
const parentPathParts = parentPath.split('.')
for (let i = 1; i <= parentPathParts.length; i++) {
const ancestor = {
folderPath: encodeFolderPath(dropRight(parentPathParts, i).join('.')),
folderPath: dropRight(parentPathParts, i).join('.'),
fileName: nth(parentPathParts, i * -1)
}
expectedAncestors.push(ancestor)
@ -303,7 +301,7 @@ export class Tree extends Model {
// Create folder
const fullPath = parentPath ? `${decodeTreePath(parentPath)}/${pathName}` : pathName
const folder = await WIKI.db.knex('tree').insert({
folderPath: encodeFolderPath(parentPath),
folderPath: parentPath,
fileName: pathName,
type: 'folder',
title,
@ -372,8 +370,8 @@ export class Tree extends Model {
}
// Build new paths
const oldFolderPath = encodeFolderPath(folder.folderPath ? `${folder.folderPath}.${folder.fileName}` : folder.fileName)
const newFolderPath = encodeFolderPath(folder.folderPath ? `${folder.folderPath}.${pathName}` : pathName)
const oldFolderPath = folder.folderPath ? `${folder.folderPath}.${folder.fileName}` : folder.fileName
const newFolderPath = folder.folderPath ? `${folder.folderPath}.${pathName}` : pathName
// Update children nodes
WIKI.logger.debug(`Updating parent path of children nodes from ${oldFolderPath} to ${newFolderPath} ...`)
@ -385,7 +383,7 @@ export class Tree extends Model {
})
// Rename the folder itself
const fullPath = folder.folderPath ? `${decodeFolderPath(folder.folderPath)}/${pathName}` : pathName
const fullPath = folder.folderPath ? `${folder.folderPath}/${pathName}` : pathName
await WIKI.db.knex('tree').where('id', folder.id).update({
fileName: pathName,
title,
@ -416,7 +414,7 @@ export class Tree extends Model {
WIKI.logger.debug(`Deleting folder ${folder.id} at path ${folderPath}...`)
// Delete all children
const deletedNodes = await WIKI.db.knex('tree').where('folderPath', '<@', encodeFolderPath(folderPath)).del().returning(['id', 'type'])
const deletedNodes = await WIKI.db.knex('tree').where('folderPath', '<@', folderPath).del().returning(['id', 'type'])
// Delete folders
const deletedFolders = deletedNodes.filter(n => n.type === 'folder').map(n => n.id)
@ -447,7 +445,7 @@ export class Tree extends Model {
if (folder.folderPath) {
const parentPathParts = folder.folderPath.split('.')
const parent = await WIKI.db.knex('tree').where({
folderPath: encodeFolderPath(dropRight(parentPathParts).join('.')),
folderPath: dropRight(parentPathParts).join('.'),
fileName: last(parentPathParts)
}).first()
await WIKI.db.knex('tree').where('id', parent.id).update({

@ -112,18 +112,18 @@
"@quasar/app-vite": "2.3.0",
"@quasar/vite-plugin": "1.10.0",
"@types/lodash": "4.17.20",
"@vue/devtools": "7.7.7",
"@vue/language-plugin-pug": "3.0.4",
"autoprefixer": "10.4.21",
"browserlist": "latest",
"eslint": "9.32.0",
"eslint-plugin-import": "2.32.0",
"eslint-plugin-n": "17.21.0",
"eslint-plugin-n": "17.21.2",
"eslint-plugin-promise": "7.2.1",
"eslint-plugin-vue": "10.3.0",
"eslint-plugin-vue-pug": "1.0.0-alpha.3",
"neostandard": "0.12.2",
"sass": "1.89.2"
"sass": "1.89.2",
"vite-plugin-vue-devtools": "8.0.0"
},
"engines": {
"node": ">= 18.0",

File diff suppressed because it is too large Load Diff

@ -4,6 +4,7 @@ import yaml from 'js-yaml'
import fs from 'node:fs'
import { fileURLToPath } from 'node:url'
import { quasar, transformAssetUrls } from '@quasar/vite-plugin'
import vueDevTools from 'vite-plugin-vue-devtools'
// https://vitejs.dev/config/
export default defineConfig(({ mode }) => {
@ -55,7 +56,8 @@ export default defineConfig(({ mode }) => {
quasar({
autoImportComponentCase: 'kebab',
sassVariables: '@/css/_theme.scss'
})
}),
vueDevTools()
],
resolve: {
alias: {

Loading…
Cancel
Save