Commit 6fe49309 authored by Nick's avatar Nick

feat: admin auth + config ref + modules sidebar ui + GQL upload (wip)

parent 59683318
...@@ -5,11 +5,10 @@ import VueRouter from 'vue-router' ...@@ -5,11 +5,10 @@ import VueRouter from 'vue-router'
import VueClipboards from 'vue-clipboards' import VueClipboards from 'vue-clipboards'
import VeeValidate from 'vee-validate' import VeeValidate from 'vee-validate'
import { ApolloClient } from 'apollo-client' import { ApolloClient } from 'apollo-client'
import { createPersistedQueryLink } from 'apollo-link-persisted-queries'
import { BatchHttpLink } from 'apollo-link-batch-http' import { BatchHttpLink } from 'apollo-link-batch-http'
import { ApolloLink, split } from 'apollo-link' import { ApolloLink, split } from 'apollo-link'
// import { createHttpLink } from 'apollo-link-http'
import { WebSocketLink } from 'apollo-link-ws' import { WebSocketLink } from 'apollo-link-ws'
import { createUploadLink } from 'apollo-upload-client'
import { ErrorLink } from 'apollo-link-error' import { ErrorLink } from 'apollo-link-error'
import { InMemoryCache } from 'apollo-cache-inmemory' import { InMemoryCache } from 'apollo-cache-inmemory'
import { getMainDefinition } from 'apollo-utilities' import { getMainDefinition } from 'apollo-utilities'
...@@ -56,6 +55,33 @@ store.commit('user/REFRESH_AUTH') ...@@ -56,6 +55,33 @@ store.commit('user/REFRESH_AUTH')
const graphQLEndpoint = window.location.protocol + '//' + window.location.host + '/graphql' const graphQLEndpoint = window.location.protocol + '//' + window.location.host + '/graphql'
const graphQLWSEndpoint = ((window.location.protocol === 'https:') ? 'wss:' : 'ws:') + '//' + window.location.host + '/graphql-subscriptions' const graphQLWSEndpoint = ((window.location.protocol === 'https:') ? 'wss:' : 'ws:') + '//' + window.location.host + '/graphql-subscriptions'
const graphQLFetch = async (uri, options) => {
// Strip __typename fields from variables
let body = JSON.parse(options.body)
body = body.map(bd => {
return ({
...bd,
variables: JSON.parse(JSON.stringify(bd.variables), (key, value) => { return key === '__typename' ? undefined : value })
})
})
options.body = JSON.stringify(body)
// Inject authentication token
const jwtToken = Cookies.get('jwt')
if (jwtToken) {
options.headers.Authorization = `Bearer ${jwtToken}`
}
const resp = await fetch(uri, options)
// Handle renewed JWT
const newJWT = resp.headers.get('new-jwt')
if (newJWT) {
Cookies.set('jwt', newJWT, { expires: 365 })
}
return resp
}
const graphQLLink = ApolloLink.from([ const graphQLLink = ApolloLink.from([
new ErrorLink(({ graphQLErrors, networkError }) => { new ErrorLink(({ graphQLErrors, networkError }) => {
if (graphQLErrors) { if (graphQLErrors) {
...@@ -79,7 +105,6 @@ const graphQLLink = ApolloLink.from([ ...@@ -79,7 +105,6 @@ const graphQLLink = ApolloLink.from([
}) })
} }
}), }),
// createPersistedQueryLink(),
new BatchHttpLink({ new BatchHttpLink({
includeExtensions: true, includeExtensions: true,
uri: graphQLEndpoint, uri: graphQLEndpoint,
...@@ -93,10 +118,6 @@ const graphQLLink = ApolloLink.from([ ...@@ -93,10 +118,6 @@ const graphQLLink = ApolloLink.from([
variables: JSON.parse(JSON.stringify(bd.variables), (key, value) => { return key === '__typename' ? undefined : value }) variables: JSON.parse(JSON.stringify(bd.variables), (key, value) => { return key === '__typename' ? undefined : value })
}) })
}) })
// body = {
// ...body,
// variables: JSON.parse(JSON.stringify(body.variables), (key, value) => { return key === '__typename' ? undefined : value })
// }
options.body = JSON.stringify(body) options.body = JSON.stringify(body)
// Inject authentication token // Inject authentication token
...@@ -117,6 +138,28 @@ const graphQLLink = ApolloLink.from([ ...@@ -117,6 +138,28 @@ const graphQLLink = ApolloLink.from([
}) })
]) ])
const graphQLUploadLink = createUploadLink({
includeExtensions: true,
uri: graphQLEndpoint,
credentials: 'include',
fetch: async (uri, options) => {
// Inject authentication token
const jwtToken = Cookies.get('jwt')
if (jwtToken) {
options.headers.Authorization = `Bearer ${jwtToken}`
}
const resp = await fetch(uri, options)
// Handle renewed JWT
const newJWT = resp.headers.get('new-jwt')
if (newJWT) {
Cookies.set('jwt', newJWT, { expires: 365 })
}
return resp
}
})
const graphQLWSLink = new WebSocketLink({ const graphQLWSLink = new WebSocketLink({
uri: graphQLWSEndpoint, uri: graphQLWSEndpoint,
options: { options: {
...@@ -129,7 +172,7 @@ window.graphQL = new ApolloClient({ ...@@ -129,7 +172,7 @@ window.graphQL = new ApolloClient({
link: split(({ query }) => { link: split(({ query }) => {
const { kind, operation } = getMainDefinition(query) const { kind, operation } = getMainDefinition(query)
return kind === 'OperationDefinition' && operation === 'subscription' return kind === 'OperationDefinition' && operation === 'subscription'
}, graphQLWSLink, graphQLLink), }, graphQLWSLink, split(operation => operation.getContext().hasUpload, graphQLUploadLink, graphQLLink)),
cache: new InMemoryCache(), cache: new InMemoryCache(),
connectToDevTools: (process.env.node_env === 'development') connectToDevTools: (process.env.node_env === 'development')
}) })
......
...@@ -49,7 +49,7 @@ ...@@ -49,7 +49,7 @@
v-list-tile(to='/auth') v-list-tile(to='/auth')
v-list-tile-avatar: v-icon lock_outline v-list-tile-avatar: v-icon lock_outline
v-list-tile-title {{ $t('admin:auth.title') }} v-list-tile-title {{ $t('admin:auth.title') }}
v-list-tile(to='/editor') v-list-tile(to='/editor', disabled)
v-list-tile-avatar: v-icon transform v-list-tile-avatar: v-icon transform
v-list-tile-title {{ $t('admin:editor.title') }} v-list-tile-title {{ $t('admin:editor.title') }}
v-list-tile(to='/logging') v-list-tile(to='/logging')
......
...@@ -21,17 +21,19 @@ ...@@ -21,17 +21,19 @@
v-card.animated.fadeInUp v-card.animated.fadeInUp
v-toolbar(flat, color='primary', dark, dense) v-toolbar(flat, color='primary', dark, dense)
.subheading Search Engine .subheading Search Engine
v-card-text v-list.py-0(two-line, dense)
v-radio-group.my-0(v-model='selectedEngine') template(v-for='(eng, idx) in engines')
v-radio.my-1( v-list-tile(:key='eng.key', @click='selectedEngine = eng.key', :disabled='!eng.isAvailable')
v-for='(engine, n) in engines' v-list-tile-avatar
:key='engine.key' v-icon(color='grey', v-if='!eng.isAvailable') cancel
:label='engine.title' v-icon(color='primary', v-else-if='eng.key === selectedEngine') radio_button_checked
:value='engine.key' v-icon(color='grey', v-else) radio_button_unchecked
:disabled='!engine.isAvailable' v-list-tile-content
color='primary' v-list-tile-title.body-2(:class='!eng.isAvailable ? `grey--text` : (selectedEngine === eng.key ? `primary--text` : ``)') {{ eng.title }}
hide-details v-list-tile-sub-title.caption(:class='!eng.isAvailable ? `grey--text text--lighten-1` : (selectedEngine === eng.key ? `blue--text ` : ``)') {{ eng.description }}
) v-list-tile-avatar(v-if='selectedEngine === eng.key')
v-icon.animated.fadeInLeft(color='primary') arrow_forward_ios
v-divider(v-if='idx < engines.length - 1')
v-flex(lg9, xs12) v-flex(lg9, xs12)
v-card.wiki-form.animated.fadeInUp.wait-p2s v-card.wiki-form.animated.fadeInUp.wait-p2s
......
...@@ -9,7 +9,7 @@ ...@@ -9,7 +9,7 @@
v-toolbar.radius-7(color='teal lighten-5', dense, flat, height='44') v-toolbar.radius-7(color='teal lighten-5', dense, flat, height='44')
.body-2.teal--text Images .body-2.teal--text Images
v-btn.ml-3.my-0.radius-7(outline, large, color='teal', disabled) v-btn.ml-3.my-0.radius-7(outline, large, color='teal', disabled)
v-icon(left) keyboard_backspace v-icon(left) keyboard_arrow_up
span Parent Folder span Parent Folder
v-btn.my-0.radius-7(outline, large, color='teal') v-btn.my-0.radius-7(outline, large, color='teal')
v-icon(left) add v-icon(left) add
...@@ -48,14 +48,15 @@ ...@@ -48,14 +48,15 @@
ref='pond' ref='pond'
label-idle='Browse or Drop files here...' label-idle='Browse or Drop files here...'
allow-multiple='true' allow-multiple='true'
accepted-file-types='image/jpeg, image/png' accepted-file-types='image/jpeg, image/png, image/gif, image/svg'
:files='files' :files='files'
max-files='10'
) )
v-divider v-divider
v-card-actions.pa-3 v-card-actions.pa-3
.caption.grey--text.text-darken-2 Max 20 files, 5 MB each .caption.grey--text.text-darken-2 Max 10 files, 5 MB each
v-spacer v-spacer
v-btn(color='teal', dark) Upload v-btn(color='teal', dark, @click='upload') Upload
v-card.mt-3.radius-7.animated.fadeInRight.wait-p4s(light) v-card.mt-3.radius-7.animated.fadeInRight.wait-p4s(light)
v-card-text.pb-0 v-card-text.pb-0
...@@ -96,11 +97,11 @@ import { sync } from 'vuex-pathify' ...@@ -96,11 +97,11 @@ import { sync } from 'vuex-pathify'
import vueFilePond from 'vue-filepond' import vueFilePond from 'vue-filepond'
import 'filepond/dist/filepond.min.css' import 'filepond/dist/filepond.min.css'
import 'filepond-plugin-image-preview/dist/filepond-plugin-image-preview.min.css'
import FilePondPluginFileValidateType from 'filepond-plugin-file-validate-type' import FilePondPluginFileValidateType from 'filepond-plugin-file-validate-type'
import FilePondPluginImagePreview from 'filepond-plugin-image-preview'
const FilePond = vueFilePond(FilePondPluginFileValidateType, FilePondPluginImagePreview) import uploadFileMutation from 'gql/editor/upload.gql'
const FilePond = vueFilePond(FilePondPluginFileValidateType)
export default { export default {
components: { components: {
...@@ -135,6 +136,21 @@ export default { ...@@ -135,6 +136,21 @@ export default {
methods: { methods: {
insert () { insert () {
this.activeModal = '' this.activeModal = ''
},
async upload () {
const files = this.$refs.pond.getFiles()
for (let fl of files) {
const resp = await this.$apollo.mutate({
mutation: uploadFileMutation,
variables: {
data: fl.file
},
context: {
hasUpload: true
}
})
console.info(resp)
}
} }
} }
} }
......
...@@ -5,6 +5,7 @@ query { ...@@ -5,6 +5,7 @@ query {
key key
title title
description description
isAvailable
useForm useForm
logo logo
website website
......
mutation ($file: Upload!) {
assets {
upload(data:$file) {
responseResult {
succeeded
message
}
}
}
}
...@@ -76,6 +76,7 @@ ...@@ -76,6 +76,7 @@
"graphql-rate-limit-directive": "0.1.0", "graphql-rate-limit-directive": "0.1.0",
"graphql-subscriptions": "1.0.0", "graphql-subscriptions": "1.0.0",
"graphql-tools": "4.0.4", "graphql-tools": "4.0.4",
"graphql-upload": "8.0.5",
"highlight.js": "9.14.2", "highlight.js": "9.14.2",
"i18next": "14.0.1", "i18next": "14.0.1",
"i18next-express-middleware": "1.7.1", "i18next-express-middleware": "1.7.1",
...@@ -192,6 +193,7 @@ ...@@ -192,6 +193,7 @@
"apollo-link-http": "1.5.11", "apollo-link-http": "1.5.11",
"apollo-link-persisted-queries": "0.2.2", "apollo-link-persisted-queries": "0.2.2",
"apollo-link-ws": "1.0.14", "apollo-link-ws": "1.0.14",
"apollo-upload-client": "10.0.0",
"apollo-utilities": "1.1.2", "apollo-utilities": "1.1.2",
"autoprefixer": "9.4.7", "autoprefixer": "9.4.7",
"babel-eslint": "10.0.1", "babel-eslint": "10.0.1",
...@@ -221,7 +223,6 @@ ...@@ -221,7 +223,6 @@
"file-loader": "3.0.1", "file-loader": "3.0.1",
"filepond": "4.2.0", "filepond": "4.2.0",
"filepond-plugin-file-validate-type": "1.2.2", "filepond-plugin-file-validate-type": "1.2.2",
"filepond-plugin-image-preview": "4.0.3",
"filesize.js": "1.0.2", "filesize.js": "1.0.2",
"grapesjs": "0.14.52", "grapesjs": "0.14.52",
"graphiql": "0.12.0", "graphiql": "0.12.0",
......
exports.up = knex => {
const dbCompat = {
charset: (WIKI.config.db.type === `mysql` || WIKI.config.db.type === `mariadb`)
}
return knex.schema
.createTable('assetData', table => {
if (dbCompat.charset) { table.charset('utf8mb4') }
table.integer('id').primary()
table.binary('data').notNullable()
})
}
exports.down = knex => {
return knex.schema
.dropTableIfExists('assetData')
}
exports.up = knex => {
const dbCompat = {
charset: (WIKI.config.db.type === `mysql` || WIKI.config.db.type === `mariadb`)
}
return knex.schema
.createTable('assetData', table => {
if (dbCompat.charset) { table.charset('utf8mb4') }
table.integer('id').primary()
table.binary('data').notNullable()
})
}
exports.down = knex => {
return knex.schema
.dropTableIfExists('assetData')
}
...@@ -7,6 +7,7 @@ const PubSub = require('graphql-subscriptions').PubSub ...@@ -7,6 +7,7 @@ const PubSub = require('graphql-subscriptions').PubSub
const { LEVEL, MESSAGE } = require('triple-beam') const { LEVEL, MESSAGE } = require('triple-beam')
const Transport = require('winston-transport') const Transport = require('winston-transport')
const { createRateLimitTypeDef } = require('graphql-rate-limit-directive') const { createRateLimitTypeDef } = require('graphql-rate-limit-directive')
const { GraphQLUpload } = require('graphql-upload')
/* global WIKI */ /* global WIKI */
...@@ -26,7 +27,9 @@ schemas.forEach(schema => { ...@@ -26,7 +27,9 @@ schemas.forEach(schema => {
// Resolvers // Resolvers
let resolvers = {} let resolvers = {
Upload: GraphQLUpload
}
const resolversObj = _.values(autoload(path.join(WIKI.SERVERPATH, 'graph/resolvers'))) const resolversObj = _.values(autoload(path.join(WIKI.SERVERPATH, 'graph/resolvers')))
resolversObj.forEach(resolver => { resolversObj.forEach(resolver => {
_.merge(resolvers, resolver) _.merge(resolvers, resolver)
......
# ===============================================
# ASSETS
# ===============================================
extend type Query {
assets: AssetQuery
}
extend type Mutation {
assets: AssetMutation
}
# -----------------------------------------------
# QUERIES
# -----------------------------------------------
type AssetQuery {
list(
root: String
kind: [AssetKind]
): [AssetItem]
}
# -----------------------------------------------
# MUTATIONS
# -----------------------------------------------
type AssetMutation {
upload(
data: Upload!
): DefaultResponse
}
# -----------------------------------------------
# TYPES
# -----------------------------------------------
type AssetItem {
id: Int!
}
enum AssetKind {
IMAGE
BINARY
}
...@@ -58,6 +58,7 @@ type AuthenticationStrategy { ...@@ -58,6 +58,7 @@ type AuthenticationStrategy {
props: [String] props: [String]
title: String! title: String!
description: String description: String
isAvailable: Boolean
useForm: Boolean! useForm: Boolean!
logo: String logo: String
color: String color: String
......
...@@ -11,7 +11,6 @@ const https = require('https') ...@@ -11,7 +11,6 @@ const https = require('https')
const path = require('path') const path = require('path')
const _ = require('lodash') const _ = require('lodash')
const { ApolloServer } = require('apollo-server-express') const { ApolloServer } = require('apollo-server-express')
// const oauth2orize = require('oauth2orize')
/* global WIKI */ /* global WIKI */
...@@ -62,12 +61,6 @@ module.exports = async () => { ...@@ -62,12 +61,6 @@ module.exports = async () => {
})) }))
// ---------------------------------------- // ----------------------------------------
// OAuth2 Server
// ----------------------------------------
// const OAuth2Server = oauth2orize.createServer()
// ----------------------------------------
// Passport Authentication // Passport Authentication
// ---------------------------------------- // ----------------------------------------
...@@ -137,6 +130,7 @@ module.exports = async () => { ...@@ -137,6 +130,7 @@ module.exports = async () => {
path: '/graphql-subscriptions' path: '/graphql-subscriptions'
} }
}) })
app.use('/graphql', mw.upload)
apolloServer.applyMiddleware({ app }) apolloServer.applyMiddleware({ app })
// ---------------------------------------- // ----------------------------------------
......
const { graphqlUploadExpress } = require('graphql-upload')
/* global WIKI */
/**
* GraphQL File Upload Middleware
*/
module.exports = graphqlUploadExpress({ maxFileSize: 5000000, maxFiles: 20 })
...@@ -5,8 +5,21 @@ author: requarks.io ...@@ -5,8 +5,21 @@ author: requarks.io
logo: https://static.requarks.io/logo/auth0.svg logo: https://static.requarks.io/logo/auth0.svg
color: deep-orange color: deep-orange
website: https://auth0.com/ website: https://auth0.com/
isAvailable: true
useForm: false useForm: false
props: props:
domain: String domain:
clientId: String type: String
clientSecret: String title: Domain
hint: Your Auth0 domain (e.g. something.auth0.com)
order: 1
clientId:
type: String
title: Client ID
hint: Application Client ID
order: 2
clientSecret:
type: String
title: Client Secret
hint: Application Client Secret
order: 3
...@@ -5,5 +5,6 @@ author: requarks.io ...@@ -5,5 +5,6 @@ author: requarks.io
logo: https://static.requarks.io/logo/wikijs.svg logo: https://static.requarks.io/logo/wikijs.svg
color: yellow darken-3 color: yellow darken-3
website: https://wiki.js.org website: https://wiki.js.org
isAvailable: true
useForm: true useForm: true
props: {} props: {}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment