mirror of
https://github.com/Ocelot-Social-Community/Ocelot-Social.git
synced 2025-12-13 07:46:06 +00:00
Merge branch 'master' of github.com:Human-Connection/Human-Connection into 236-list-social-media-accounts
This commit is contained in:
commit
3f9916404e
@ -4,7 +4,7 @@
|
|||||||
"description": "GraphQL Backend for Human Connection",
|
"description": "GraphQL Backend for Human Connection",
|
||||||
"main": "src/index.js",
|
"main": "src/index.js",
|
||||||
"config": {
|
"config": {
|
||||||
"no_auth": "cross-env GRAPHQL_URI=http://localhost:4001 GRAPHQL_PORT=4001 PERMISSIONS=disabled"
|
"no_auth": "cross-env GRAPHQL_URI=http://localhost:4001 GRAPHQL_PORT=4001 DISABLED_MIDDLEWARES=permissions"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "babel src/ -d dist/ --copy-files",
|
"build": "babel src/ -d dist/ --copy-files",
|
||||||
@ -15,7 +15,7 @@
|
|||||||
"test": "nyc --reporter=text-lcov yarn test:jest",
|
"test": "nyc --reporter=text-lcov yarn test:jest",
|
||||||
"test:cypress": "run-p --race test:before:*",
|
"test:cypress": "run-p --race test:before:*",
|
||||||
"test:before:server": "cross-env CLIENT_URI=http://localhost:4123 GRAPHQL_URI=http://localhost:4123 GRAPHQL_PORT=4123 babel-node src/ 2> /dev/null",
|
"test:before:server": "cross-env CLIENT_URI=http://localhost:4123 GRAPHQL_URI=http://localhost:4123 GRAPHQL_PORT=4123 babel-node src/ 2> /dev/null",
|
||||||
"test:before:seeder": "cross-env GRAPHQL_URI=http://localhost:4001 GRAPHQL_PORT=4001 PERMISSIONS=disabled babel-node src/ 2> /dev/null",
|
"test:before:seeder": "cross-env GRAPHQL_URI=http://localhost:4001 GRAPHQL_PORT=4001 DISABLED_MIDDLEWARES=permissions,activityPub babel-node src/ 2> /dev/null",
|
||||||
"test:jest:cmd": "wait-on tcp:4001 tcp:4123 && jest --forceExit --detectOpenHandles --runInBand",
|
"test:jest:cmd": "wait-on tcp:4001 tcp:4123 && jest --forceExit --detectOpenHandles --runInBand",
|
||||||
"test:cucumber:cmd": "wait-on tcp:4001 tcp:4123 && cucumber-js --require-module @babel/register --exit test/",
|
"test:cucumber:cmd": "wait-on tcp:4001 tcp:4123 && cucumber-js --require-module @babel/register --exit test/",
|
||||||
"test:jest:cmd:debug": "wait-on tcp:4001 tcp:4123 && node --inspect-brk ./node_modules/.bin/jest -i --forceExit --detectOpenHandles --runInBand",
|
"test:jest:cmd:debug": "wait-on tcp:4001 tcp:4123 && node --inspect-brk ./node_modules/.bin/jest -i --forceExit --detectOpenHandles --runInBand",
|
||||||
|
|||||||
@ -22,21 +22,19 @@ let activityPub = null
|
|||||||
export { activityPub }
|
export { activityPub }
|
||||||
|
|
||||||
export default class ActivityPub {
|
export default class ActivityPub {
|
||||||
constructor (host, uri) {
|
constructor (activityPubEndpointUri, internalGraphQlUri) {
|
||||||
this.host = host
|
this.endpoint = activityPubEndpointUri
|
||||||
this.dataSource = new NitroDataSource(uri)
|
this.dataSource = new NitroDataSource(internalGraphQlUri)
|
||||||
this.collections = new Collections(this.dataSource)
|
this.collections = new Collections(this.dataSource)
|
||||||
}
|
}
|
||||||
|
|
||||||
static init (server) {
|
static init (server) {
|
||||||
if (!activityPub) {
|
if (!activityPub) {
|
||||||
dotenv.config()
|
dotenv.config()
|
||||||
const url = new URL(process.env.CLIENT_URI)
|
activityPub = new ActivityPub(process.env.CLIENT_URI || 'http://localhost:3000', process.env.GRAPHQL_URI || 'http://localhost:4000')
|
||||||
activityPub = new ActivityPub(url.host || 'localhost:4000', url.origin)
|
|
||||||
|
|
||||||
// integrate into running graphql express server
|
// integrate into running graphql express server
|
||||||
server.express.set('ap', activityPub)
|
server.express.set('ap', activityPub)
|
||||||
server.express.set('port', url.port)
|
|
||||||
server.express.use(router)
|
server.express.use(router)
|
||||||
console.log('-> ActivityPub middleware added to the graphql express server')
|
console.log('-> ActivityPub middleware added to the graphql express server')
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -12,7 +12,9 @@ router.get('/', async function (req, res) {
|
|||||||
const nameAndDomain = resource.replace('acct:', '')
|
const nameAndDomain = resource.replace('acct:', '')
|
||||||
const name = nameAndDomain.split('@')[0]
|
const name = nameAndDomain.split('@')[0]
|
||||||
|
|
||||||
const result = await req.app.get('ap').dataSource.client.query({
|
let result
|
||||||
|
try {
|
||||||
|
result = await req.app.get('ap').dataSource.client.query({
|
||||||
query: gql`
|
query: gql`
|
||||||
query {
|
query {
|
||||||
User(slug: "${name}") {
|
User(slug: "${name}") {
|
||||||
@ -21,6 +23,9 @@ router.get('/', async function (req, res) {
|
|||||||
}
|
}
|
||||||
`
|
`
|
||||||
})
|
})
|
||||||
|
} catch (error) {
|
||||||
|
return res.status(500).json({ error })
|
||||||
|
}
|
||||||
|
|
||||||
if (result.data && result.data.User.length > 0) {
|
if (result.data && result.data.User.length > 0) {
|
||||||
const webFinger = createWebFinger(name)
|
const webFinger = createWebFinger(name)
|
||||||
|
|||||||
@ -11,14 +11,14 @@ export function createNoteObject (text, name, id, published) {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||||
'id': `https://${activityPub.host}/activitypub/users/${name}/status/${createUuid}`,
|
'id': `${activityPub.endpoint}/activitypub/users/${name}/status/${createUuid}`,
|
||||||
'type': 'Create',
|
'type': 'Create',
|
||||||
'actor': `https://${activityPub.host}/activitypub/users/${name}`,
|
'actor': `${activityPub.endpoint}/activitypub/users/${name}`,
|
||||||
'object': {
|
'object': {
|
||||||
'id': `https://${activityPub.host}/activitypub/users/${name}/status/${id}`,
|
'id': `${activityPub.endpoint}/activitypub/users/${name}/status/${id}`,
|
||||||
'type': 'Note',
|
'type': 'Note',
|
||||||
'published': published,
|
'published': published,
|
||||||
'attributedTo': `https://${activityPub.host}/activitypub/users/${name}`,
|
'attributedTo': `${activityPub.endpoint}/activitypub/users/${name}`,
|
||||||
'content': text,
|
'content': text,
|
||||||
'to': 'https://www.w3.org/ns/activitystreams#Public'
|
'to': 'https://www.w3.org/ns/activitystreams#Public'
|
||||||
}
|
}
|
||||||
@ -64,8 +64,8 @@ export async function getActorId (name) {
|
|||||||
|
|
||||||
export function sendAcceptActivity (theBody, name, targetDomain, url) {
|
export function sendAcceptActivity (theBody, name, targetDomain, url) {
|
||||||
as.accept()
|
as.accept()
|
||||||
.id(`https://${activityPub.host}/activitypub/users/${name}/status/` + crypto.randomBytes(16).toString('hex'))
|
.id(`${activityPub.endpoint}/activitypub/users/${name}/status/` + crypto.randomBytes(16).toString('hex'))
|
||||||
.actor(`https://${activityPub.host}/activitypub/users/${name}`)
|
.actor(`${activityPub.endpoint}/activitypub/users/${name}`)
|
||||||
.object(theBody)
|
.object(theBody)
|
||||||
.prettyWrite((err, doc) => {
|
.prettyWrite((err, doc) => {
|
||||||
if (!err) {
|
if (!err) {
|
||||||
@ -79,8 +79,8 @@ export function sendAcceptActivity (theBody, name, targetDomain, url) {
|
|||||||
|
|
||||||
export function sendRejectActivity (theBody, name, targetDomain, url) {
|
export function sendRejectActivity (theBody, name, targetDomain, url) {
|
||||||
as.reject()
|
as.reject()
|
||||||
.id(`https://${activityPub.host}/activitypub/users/${name}/status/` + crypto.randomBytes(16).toString('hex'))
|
.id(`${activityPub.endpoint}/activitypub/users/${name}/status/` + crypto.randomBytes(16).toString('hex'))
|
||||||
.actor(`https://${activityPub.host}/activitypub/users/${name}`)
|
.actor(`${activityPub.endpoint}/activitypub/users/${name}`)
|
||||||
.object(theBody)
|
.object(theBody)
|
||||||
.prettyWrite((err, doc) => {
|
.prettyWrite((err, doc) => {
|
||||||
if (!err) {
|
if (!err) {
|
||||||
|
|||||||
@ -6,34 +6,35 @@ export function createActor (name, pubkey) {
|
|||||||
'https://www.w3.org/ns/activitystreams',
|
'https://www.w3.org/ns/activitystreams',
|
||||||
'https://w3id.org/security/v1'
|
'https://w3id.org/security/v1'
|
||||||
],
|
],
|
||||||
'id': `https://${activityPub.host}/activitypub/users/${name}`,
|
'id': `${activityPub.endpoint}/activitypub/users/${name}`,
|
||||||
'type': 'Person',
|
'type': 'Person',
|
||||||
'preferredUsername': `${name}`,
|
'preferredUsername': `${name}`,
|
||||||
'name': `${name}`,
|
'name': `${name}`,
|
||||||
'following': `https://${activityPub.host}/activitypub/users/${name}/following`,
|
'following': `${activityPub.endpoint}/activitypub/users/${name}/following`,
|
||||||
'followers': `https://${activityPub.host}/activitypub/users/${name}/followers`,
|
'followers': `${activityPub.endpoint}/activitypub/users/${name}/followers`,
|
||||||
'inbox': `https://${activityPub.host}/activitypub/users/${name}/inbox`,
|
'inbox': `${activityPub.endpoint}/activitypub/users/${name}/inbox`,
|
||||||
'outbox': `https://${activityPub.host}/activitypub/users/${name}/outbox`,
|
'outbox': `${activityPub.endpoint}/activitypub/users/${name}/outbox`,
|
||||||
'url': `https://${activityPub.host}/activitypub/@${name}`,
|
'url': `${activityPub.endpoint}/activitypub/@${name}`,
|
||||||
'endpoints': {
|
'endpoints': {
|
||||||
'sharedInbox': `https://${activityPub.host}/activitypub/inbox`
|
'sharedInbox': `${activityPub.endpoint}/activitypub/inbox`
|
||||||
},
|
},
|
||||||
'publicKey': {
|
'publicKey': {
|
||||||
'id': `https://${activityPub.host}/activitypub/users/${name}#main-key`,
|
'id': `${activityPub.endpoint}/activitypub/users/${name}#main-key`,
|
||||||
'owner': `https://${activityPub.host}/activitypub/users/${name}`,
|
'owner': `${activityPub.endpoint}/activitypub/users/${name}`,
|
||||||
'publicKeyPem': pubkey
|
'publicKeyPem': pubkey
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createWebFinger (name) {
|
export function createWebFinger (name) {
|
||||||
|
const { host } = new URL(activityPub.endpoint)
|
||||||
return {
|
return {
|
||||||
'subject': `acct:${name}@${activityPub.host}`,
|
'subject': `acct:${name}@${host}`,
|
||||||
'links': [
|
'links': [
|
||||||
{
|
{
|
||||||
'rel': 'self',
|
'rel': 'self',
|
||||||
'type': 'application/activity+json',
|
'type': 'application/activity+json',
|
||||||
'href': `https://${activityPub.host}/activitypub/users/${name}`
|
'href': `${activityPub.endpoint}/activitypub/users/${name}`
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,10 +5,10 @@ const debug = require('debug')('ea:utils:collections')
|
|||||||
export function createOrderedCollection (name, collectionName) {
|
export function createOrderedCollection (name, collectionName) {
|
||||||
return {
|
return {
|
||||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||||
'id': `https://${activityPub.host}/activitypub/users/${name}/${collectionName}`,
|
'id': `${activityPub.endpoint}/activitypub/users/${name}/${collectionName}`,
|
||||||
'summary': `${name}s ${collectionName} collection`,
|
'summary': `${name}s ${collectionName} collection`,
|
||||||
'type': 'OrderedCollection',
|
'type': 'OrderedCollection',
|
||||||
'first': `https://${activityPub.host}/activitypub/users/${name}/${collectionName}?page=true`,
|
'first': `${activityPub.endpoint}/activitypub/users/${name}/${collectionName}?page=true`,
|
||||||
'totalItems': 0
|
'totalItems': 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -16,11 +16,11 @@ export function createOrderedCollection (name, collectionName) {
|
|||||||
export function createOrderedCollectionPage (name, collectionName) {
|
export function createOrderedCollectionPage (name, collectionName) {
|
||||||
return {
|
return {
|
||||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||||
'id': `https://${activityPub.host}/activitypub/users/${name}/${collectionName}?page=true`,
|
'id': `${activityPub.endpoint}/activitypub/users/${name}/${collectionName}?page=true`,
|
||||||
'summary': `${name}s ${collectionName} collection`,
|
'summary': `${name}s ${collectionName} collection`,
|
||||||
'type': 'OrderedCollectionPage',
|
'type': 'OrderedCollectionPage',
|
||||||
'totalItems': 0,
|
'totalItems': 0,
|
||||||
'partOf': `https://${activityPub.host}/activitypub/users/${name}/${collectionName}`,
|
'partOf': `${activityPub.endpoint}/activitypub/users/${name}/${collectionName}`,
|
||||||
'orderedItems': []
|
'orderedItems': []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,8 +20,8 @@ export function extractIdFromActivityId (uri) {
|
|||||||
return splitted[splitted.indexOf('status') + 1]
|
return splitted[splitted.indexOf('status') + 1]
|
||||||
}
|
}
|
||||||
|
|
||||||
export function constructIdFromName (name, fromDomain = activityPub.host) {
|
export function constructIdFromName (name, fromDomain = activityPub.endpoint) {
|
||||||
return `http://${fromDomain}/activitypub/users/${name}`
|
return `${fromDomain}/activitypub/users/${name}`
|
||||||
}
|
}
|
||||||
|
|
||||||
export function extractDomainFromUrl (url) {
|
export function extractDomainFromUrl (url) {
|
||||||
@ -76,7 +76,7 @@ export function signAndSend (activity, fromName, targetDomain, url) {
|
|||||||
'Host': targetDomain,
|
'Host': targetDomain,
|
||||||
'Date': date,
|
'Date': date,
|
||||||
'Signature': createSignature({ privateKey,
|
'Signature': createSignature({ privateKey,
|
||||||
keyId: `http://${activityPub.host}/activitypub/users/${fromName}#main-key`,
|
keyId: `${activityPub.endpoint}/activitypub/users/${fromName}#main-key`,
|
||||||
url,
|
url,
|
||||||
headers: {
|
headers: {
|
||||||
'Host': targetDomain,
|
'Host': targetDomain,
|
||||||
|
|||||||
@ -1,44 +1,21 @@
|
|||||||
|
const setCreatedAt = (resolve, root, args, context, info) => {
|
||||||
|
args.createdAt = (new Date()).toISOString()
|
||||||
|
return resolve(root, args, context, info)
|
||||||
|
}
|
||||||
|
const setUpdatedAt = (resolve, root, args, context, info) => {
|
||||||
|
args.updatedAt = (new Date()).toISOString()
|
||||||
|
return resolve(root, args, context, info)
|
||||||
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
Mutation: {
|
Mutation: {
|
||||||
CreateUser: async (resolve, root, args, context, info) => {
|
CreateUser: setCreatedAt,
|
||||||
args.createdAt = (new Date()).toISOString()
|
CreatePost: setCreatedAt,
|
||||||
const result = await resolve(root, args, context, info)
|
CreateComment: setCreatedAt,
|
||||||
return result
|
CreateOrganization: setCreatedAt,
|
||||||
},
|
UpdateUser: setUpdatedAt,
|
||||||
CreatePost: async (resolve, root, args, context, info) => {
|
UpdatePost: setUpdatedAt,
|
||||||
args.createdAt = (new Date()).toISOString()
|
UpdateComment: setUpdatedAt,
|
||||||
const result = await resolve(root, args, context, info)
|
UpdateOrganization: setUpdatedAt
|
||||||
return result
|
|
||||||
},
|
|
||||||
CreateComment: async (resolve, root, args, context, info) => {
|
|
||||||
args.createdAt = (new Date()).toISOString()
|
|
||||||
const result = await resolve(root, args, context, info)
|
|
||||||
return result
|
|
||||||
},
|
|
||||||
CreateOrganization: async (resolve, root, args, context, info) => {
|
|
||||||
args.createdAt = (new Date()).toISOString()
|
|
||||||
const result = await resolve(root, args, context, info)
|
|
||||||
return result
|
|
||||||
},
|
|
||||||
UpdateUser: async (resolve, root, args, context, info) => {
|
|
||||||
args.updatedAt = (new Date()).toISOString()
|
|
||||||
const result = await resolve(root, args, context, info)
|
|
||||||
return result
|
|
||||||
},
|
|
||||||
UpdatePost: async (resolve, root, args, context, info) => {
|
|
||||||
args.updatedAt = (new Date()).toISOString()
|
|
||||||
const result = await resolve(root, args, context, info)
|
|
||||||
return result
|
|
||||||
},
|
|
||||||
UpdateComment: async (resolve, root, args, context, info) => {
|
|
||||||
args.updatedAt = (new Date()).toISOString()
|
|
||||||
const result = await resolve(root, args, context, info)
|
|
||||||
return result
|
|
||||||
},
|
|
||||||
UpdateOrganization: async (resolve, root, args, context, info) => {
|
|
||||||
args.updatedAt = (new Date()).toISOString()
|
|
||||||
const result = await resolve(root, args, context, info)
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -24,6 +24,6 @@ const includeFieldsRecursively = (includedFields) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
Query: includeFieldsRecursively(['id', 'disabled', 'deleted']),
|
Query: includeFieldsRecursively(['id', 'createdAt', 'disabled', 'deleted']),
|
||||||
Mutation: includeFieldsRecursively(['id', 'disabled', 'deleted'])
|
Mutation: includeFieldsRecursively(['id', 'createdAt', 'disabled', 'deleted'])
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import xssMiddleware from './xssMiddleware'
|
|||||||
import permissionsMiddleware from './permissionsMiddleware'
|
import permissionsMiddleware from './permissionsMiddleware'
|
||||||
import userMiddleware from './userMiddleware'
|
import userMiddleware from './userMiddleware'
|
||||||
import includedFieldsMiddleware from './includedFieldsMiddleware'
|
import includedFieldsMiddleware from './includedFieldsMiddleware'
|
||||||
|
import orderByMiddleware from './orderByMiddleware'
|
||||||
|
|
||||||
export default schema => {
|
export default schema => {
|
||||||
let middleware = [
|
let middleware = [
|
||||||
@ -20,14 +21,17 @@ export default schema => {
|
|||||||
fixImageUrlsMiddleware,
|
fixImageUrlsMiddleware,
|
||||||
softDeleteMiddleware,
|
softDeleteMiddleware,
|
||||||
userMiddleware,
|
userMiddleware,
|
||||||
includedFieldsMiddleware
|
includedFieldsMiddleware,
|
||||||
|
orderByMiddleware
|
||||||
]
|
]
|
||||||
|
|
||||||
// add permisions middleware at the first position (unless we're seeding)
|
// add permisions middleware at the first position (unless we're seeding)
|
||||||
// NOTE: DO NOT SET THE PERMISSION FLAT YOUR SELF
|
// NOTE: DO NOT SET THE PERMISSION FLAT YOUR SELF
|
||||||
if (process.env.PERMISSIONS !== 'disabled' && process.env.NODE_ENV !== 'production') {
|
if (process.env.NODE_ENV !== 'production') {
|
||||||
middleware.unshift(activityPubMiddleware)
|
const DISABLED_MIDDLEWARES = process.env.DISABLED_MIDDLEWARES || ''
|
||||||
middleware.unshift(permissionsMiddleware.generate(schema))
|
const disabled = DISABLED_MIDDLEWARES.split(',')
|
||||||
|
if (!disabled.includes('activityPub')) middleware.unshift(activityPubMiddleware)
|
||||||
|
if (!disabled.includes('permissions')) middleware.unshift(permissionsMiddleware.generate(schema))
|
||||||
}
|
}
|
||||||
return middleware
|
return middleware
|
||||||
}
|
}
|
||||||
|
|||||||
19
backend/src/middleware/orderByMiddleware.js
Normal file
19
backend/src/middleware/orderByMiddleware.js
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import cloneDeep from 'lodash/cloneDeep'
|
||||||
|
|
||||||
|
const defaultOrderBy = (resolve, root, args, context, resolveInfo) => {
|
||||||
|
const copy = cloneDeep(resolveInfo)
|
||||||
|
const newestFirst = {
|
||||||
|
kind: 'Argument',
|
||||||
|
name: { kind: 'Name', value: 'orderBy' },
|
||||||
|
value: { kind: 'EnumValue', value: 'createdAt_desc' }
|
||||||
|
}
|
||||||
|
const [fieldNode] = copy.fieldNodes
|
||||||
|
if (fieldNode) fieldNode.arguments.push(newestFirst)
|
||||||
|
return resolve(root, args, context, copy)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
Query: {
|
||||||
|
Post: defaultOrderBy
|
||||||
|
}
|
||||||
|
}
|
||||||
62
backend/src/middleware/orderByMiddleware.spec.js
Normal file
62
backend/src/middleware/orderByMiddleware.spec.js
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import Factory from '../seed/factories'
|
||||||
|
import { host } from '../jest/helpers'
|
||||||
|
import { GraphQLClient } from 'graphql-request'
|
||||||
|
|
||||||
|
let client
|
||||||
|
let headers
|
||||||
|
let query
|
||||||
|
const factory = Factory()
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const userParams = { name: 'Author', email: 'author@example.org', password: '1234' }
|
||||||
|
await factory.create('User', userParams)
|
||||||
|
await factory.authenticateAs(userParams)
|
||||||
|
await factory.create('Post', { title: 'first' })
|
||||||
|
await factory.create('Post', { title: 'second' })
|
||||||
|
await factory.create('Post', { title: 'third' })
|
||||||
|
await factory.create('Post', { title: 'last' })
|
||||||
|
headers = {}
|
||||||
|
client = new GraphQLClient(host, { headers })
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await factory.cleanDatabase()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Query', () => {
|
||||||
|
describe('Post', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
query = '{ Post { title } }'
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('orderBy', () => {
|
||||||
|
it('createdAt descending is default', async () => {
|
||||||
|
const posts = [
|
||||||
|
{ title: 'last' },
|
||||||
|
{ title: 'third' },
|
||||||
|
{ title: 'second' },
|
||||||
|
{ title: 'first' }
|
||||||
|
]
|
||||||
|
const expected = { Post: posts }
|
||||||
|
await expect(client.request(query)).resolves.toEqual(expected)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('(orderBy: createdAt_asc)', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
query = '{ Post(orderBy: createdAt_asc) { title } }'
|
||||||
|
})
|
||||||
|
|
||||||
|
it('orders by createdAt ascending', async () => {
|
||||||
|
const posts = [
|
||||||
|
{ title: 'first' },
|
||||||
|
{ title: 'second' },
|
||||||
|
{ title: 'third' },
|
||||||
|
{ title: 'last' }
|
||||||
|
]
|
||||||
|
const expected = { Post: posts }
|
||||||
|
await expect(client.request(query)).resolves.toEqual(expected)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -4,6 +4,7 @@ import uuid from 'uuid/v4'
|
|||||||
export default function (params) {
|
export default function (params) {
|
||||||
const {
|
const {
|
||||||
id = uuid(),
|
id = uuid(),
|
||||||
|
slug = '',
|
||||||
title = faker.lorem.sentence(),
|
title = faker.lorem.sentence(),
|
||||||
content = [
|
content = [
|
||||||
faker.lorem.sentence(),
|
faker.lorem.sentence(),
|
||||||
@ -21,6 +22,7 @@ export default function (params) {
|
|||||||
mutation {
|
mutation {
|
||||||
CreatePost(
|
CreatePost(
|
||||||
id: "${id}",
|
id: "${id}",
|
||||||
|
slug: "${slug}",
|
||||||
title: "${title}",
|
title: "${title}",
|
||||||
content: "${content}",
|
content: "${content}",
|
||||||
image: "${image}",
|
image: "${image}",
|
||||||
|
|||||||
@ -5,6 +5,7 @@ export default function create (params) {
|
|||||||
const {
|
const {
|
||||||
id = uuid(),
|
id = uuid(),
|
||||||
name = faker.name.findName(),
|
name = faker.name.findName(),
|
||||||
|
slug = '',
|
||||||
email = faker.internet.email(),
|
email = faker.internet.email(),
|
||||||
password = '1234',
|
password = '1234',
|
||||||
role = 'user',
|
role = 'user',
|
||||||
@ -19,6 +20,7 @@ export default function create (params) {
|
|||||||
CreateUser(
|
CreateUser(
|
||||||
id: "${id}",
|
id: "${id}",
|
||||||
name: "${name}",
|
name: "${name}",
|
||||||
|
slug: "${slug}",
|
||||||
password: "${password}",
|
password: "${password}",
|
||||||
email: "${email}",
|
email: "${email}",
|
||||||
avatar: "${avatar}",
|
avatar: "${avatar}",
|
||||||
@ -30,6 +32,7 @@ export default function create (params) {
|
|||||||
) {
|
) {
|
||||||
id
|
id
|
||||||
name
|
name
|
||||||
|
slug
|
||||||
email
|
email
|
||||||
avatar
|
avatar
|
||||||
role
|
role
|
||||||
|
|||||||
@ -29,7 +29,7 @@ Feature: Delete an object
|
|||||||
"""
|
"""
|
||||||
{
|
{
|
||||||
"@context": "https://www.w3.org/ns/activitystreams",
|
"@context": "https://www.w3.org/ns/activitystreams",
|
||||||
"id": "https://localhost:4123/activitypub/users/karl-heinz/status/a4DJ2afdg323v32641vna42lkj685kasd2",
|
"id": "http://localhost:4123/activitypub/users/karl-heinz/status/a4DJ2afdg323v32641vna42lkj685kasd2",
|
||||||
"type": "Delete",
|
"type": "Delete",
|
||||||
"object": {
|
"object": {
|
||||||
"id": "https://aronda.org/activitypub/users/bernd-das-brot/status/kljsdfg9843jknsdf234",
|
"id": "https://aronda.org/activitypub/users/bernd-das-brot/status/kljsdfg9843jknsdf234",
|
||||||
|
|||||||
@ -15,7 +15,7 @@ Feature: Follow a user
|
|||||||
"""
|
"""
|
||||||
{
|
{
|
||||||
"@context": "https://www.w3.org/ns/activitystreams",
|
"@context": "https://www.w3.org/ns/activitystreams",
|
||||||
"id": "https://localhost:4123/activitypub/users/stuart-little/status/83J23549sda1k72fsa4567na42312455kad83",
|
"id": "http://localhost:4123/activitypub/users/stuart-little/status/83J23549sda1k72fsa4567na42312455kad83",
|
||||||
"type": "Follow",
|
"type": "Follow",
|
||||||
"actor": "http://localhost:4123/activitypub/users/stuart-little",
|
"actor": "http://localhost:4123/activitypub/users/stuart-little",
|
||||||
"object": "http://localhost:4123/activitypub/users/tero-vota"
|
"object": "http://localhost:4123/activitypub/users/tero-vota"
|
||||||
@ -32,11 +32,11 @@ Feature: Follow a user
|
|||||||
"""
|
"""
|
||||||
{
|
{
|
||||||
"@context": "https://www.w3.org/ns/activitystreams",
|
"@context": "https://www.w3.org/ns/activitystreams",
|
||||||
"id": "https://localhost:4123/activitypub/users/tero-vota/status/a4DJ2afdg323v32641vna42lkj685kasd2",
|
"id": "http://localhost:4123/activitypub/users/tero-vota/status/a4DJ2afdg323v32641vna42lkj685kasd2",
|
||||||
"type": "Undo",
|
"type": "Undo",
|
||||||
"actor": "http://localhost:4123/activitypub/users/tero-vota",
|
"actor": "http://localhost:4123/activitypub/users/tero-vota",
|
||||||
"object": {
|
"object": {
|
||||||
"id": "https://localhost:4123/activitypub/users/stuart-little/status/83J23549sda1k72fsa4567na42312455kad83",
|
"id": "http://localhost:4123/activitypub/users/stuart-little/status/83J23549sda1k72fsa4567na42312455kad83",
|
||||||
"type": "Follow",
|
"type": "Follow",
|
||||||
"actor": "http://localhost:4123/activitypub/users/stuart-little",
|
"actor": "http://localhost:4123/activitypub/users/stuart-little",
|
||||||
"object": "http://localhost:4123/activitypub/users/tero-vota"
|
"object": "http://localhost:4123/activitypub/users/tero-vota"
|
||||||
|
|||||||
@ -13,14 +13,14 @@ Feature: Like an object like an article or note
|
|||||||
"""
|
"""
|
||||||
{
|
{
|
||||||
"@context": "https://www.w3.org/ns/activitystreams",
|
"@context": "https://www.w3.org/ns/activitystreams",
|
||||||
"id": "https://localhost:4123/activitypub/users/karl-heinz/status/faslkasa7dasfzkjn2398hsfd",
|
"id": "http://localhost:4123/activitypub/users/karl-heinz/status/faslkasa7dasfzkjn2398hsfd",
|
||||||
"type": "Create",
|
"type": "Create",
|
||||||
"actor": "https://localhost:4123/activitypub/users/karl-heinz",
|
"actor": "http://localhost:4123/activitypub/users/karl-heinz",
|
||||||
"object": {
|
"object": {
|
||||||
"id": "https://localhost:4123/activitypub/users/karl-heinz/status/dkasfljsdfaafg9843jknsdf",
|
"id": "http://localhost:4123/activitypub/users/karl-heinz/status/dkasfljsdfaafg9843jknsdf",
|
||||||
"type": "Article",
|
"type": "Article",
|
||||||
"published": "2019-02-07T19:37:55.002Z",
|
"published": "2019-02-07T19:37:55.002Z",
|
||||||
"attributedTo": "https://localhost:4123/activitypub/users/karl-heinz",
|
"attributedTo": "http://localhost:4123/activitypub/users/karl-heinz",
|
||||||
"content": "Hi Max, how are you?",
|
"content": "Hi Max, how are you?",
|
||||||
"to": "https://www.w3.org/ns/activitystreams#Public"
|
"to": "https://www.w3.org/ns/activitystreams#Public"
|
||||||
}
|
}
|
||||||
@ -32,7 +32,7 @@ Feature: Like an object like an article or note
|
|||||||
"""
|
"""
|
||||||
{
|
{
|
||||||
"@context": "https://www.w3.org/ns/activitystreams",
|
"@context": "https://www.w3.org/ns/activitystreams",
|
||||||
"id": "https://localhost:4123/activitypub/users/peter-lustiger/status/83J23549sda1k72fsa4567na42312455kad83",
|
"id": "http://localhost:4123/activitypub/users/peter-lustiger/status/83J23549sda1k72fsa4567na42312455kad83",
|
||||||
"type": "Like",
|
"type": "Like",
|
||||||
"actor": "http://localhost:4123/activitypub/users/peter-lustiger",
|
"actor": "http://localhost:4123/activitypub/users/peter-lustiger",
|
||||||
"object": "http://localhost:4123/activitypub/users/karl-heinz/status/dkasfljsdfaafg9843jknsdf"
|
"object": "http://localhost:4123/activitypub/users/karl-heinz/status/dkasfljsdfaafg9843jknsdf"
|
||||||
|
|||||||
@ -14,10 +14,10 @@ Feature: Receiving collections
|
|||||||
"""
|
"""
|
||||||
{
|
{
|
||||||
"@context": "https://www.w3.org/ns/activitystreams",
|
"@context": "https://www.w3.org/ns/activitystreams",
|
||||||
"id": "https://localhost:4123/activitypub/users/renate-oberdorfer/outbox",
|
"id": "http://localhost:4123/activitypub/users/renate-oberdorfer/outbox",
|
||||||
"summary": "renate-oberdorfers outbox collection",
|
"summary": "renate-oberdorfers outbox collection",
|
||||||
"type": "OrderedCollection",
|
"type": "OrderedCollection",
|
||||||
"first": "https://localhost:4123/activitypub/users/renate-oberdorfer/outbox?page=true",
|
"first": "http://localhost:4123/activitypub/users/renate-oberdorfer/outbox?page=true",
|
||||||
"totalItems": 0
|
"totalItems": 0
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
@ -29,10 +29,10 @@ Feature: Receiving collections
|
|||||||
"""
|
"""
|
||||||
{
|
{
|
||||||
"@context": "https://www.w3.org/ns/activitystreams",
|
"@context": "https://www.w3.org/ns/activitystreams",
|
||||||
"id": "https://localhost:4123/activitypub/users/renate-oberdorfer/following",
|
"id": "http://localhost:4123/activitypub/users/renate-oberdorfer/following",
|
||||||
"summary": "renate-oberdorfers following collection",
|
"summary": "renate-oberdorfers following collection",
|
||||||
"type": "OrderedCollection",
|
"type": "OrderedCollection",
|
||||||
"first": "https://localhost:4123/activitypub/users/renate-oberdorfer/following?page=true",
|
"first": "http://localhost:4123/activitypub/users/renate-oberdorfer/following?page=true",
|
||||||
"totalItems": 0
|
"totalItems": 0
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
@ -44,10 +44,10 @@ Feature: Receiving collections
|
|||||||
"""
|
"""
|
||||||
{
|
{
|
||||||
"@context": "https://www.w3.org/ns/activitystreams",
|
"@context": "https://www.w3.org/ns/activitystreams",
|
||||||
"id": "https://localhost:4123/activitypub/users/renate-oberdorfer/followers",
|
"id": "http://localhost:4123/activitypub/users/renate-oberdorfer/followers",
|
||||||
"summary": "renate-oberdorfers followers collection",
|
"summary": "renate-oberdorfers followers collection",
|
||||||
"type": "OrderedCollection",
|
"type": "OrderedCollection",
|
||||||
"first": "https://localhost:4123/activitypub/users/renate-oberdorfer/followers?page=true",
|
"first": "http://localhost:4123/activitypub/users/renate-oberdorfer/followers?page=true",
|
||||||
"totalItems": 0
|
"totalItems": 0
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
@ -59,11 +59,11 @@ Feature: Receiving collections
|
|||||||
"""
|
"""
|
||||||
{
|
{
|
||||||
"@context": "https://www.w3.org/ns/activitystreams",
|
"@context": "https://www.w3.org/ns/activitystreams",
|
||||||
"id": "https://localhost:4123/activitypub/users/renate-oberdorfer/outbox?page=true",
|
"id": "http://localhost:4123/activitypub/users/renate-oberdorfer/outbox?page=true",
|
||||||
"summary": "renate-oberdorfers outbox collection",
|
"summary": "renate-oberdorfers outbox collection",
|
||||||
"type": "OrderedCollectionPage",
|
"type": "OrderedCollectionPage",
|
||||||
"totalItems": 0,
|
"totalItems": 0,
|
||||||
"partOf": "https://localhost:4123/activitypub/users/renate-oberdorfer/outbox",
|
"partOf": "http://localhost:4123/activitypub/users/renate-oberdorfer/outbox",
|
||||||
"orderedItems": []
|
"orderedItems": []
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
@ -75,11 +75,11 @@ Feature: Receiving collections
|
|||||||
"""
|
"""
|
||||||
{
|
{
|
||||||
"@context": "https://www.w3.org/ns/activitystreams",
|
"@context": "https://www.w3.org/ns/activitystreams",
|
||||||
"id": "https://localhost:4123/activitypub/users/renate-oberdorfer/following?page=true",
|
"id": "http://localhost:4123/activitypub/users/renate-oberdorfer/following?page=true",
|
||||||
"summary": "renate-oberdorfers following collection",
|
"summary": "renate-oberdorfers following collection",
|
||||||
"type": "OrderedCollectionPage",
|
"type": "OrderedCollectionPage",
|
||||||
"totalItems": 0,
|
"totalItems": 0,
|
||||||
"partOf": "https://localhost:4123/activitypub/users/renate-oberdorfer/following",
|
"partOf": "http://localhost:4123/activitypub/users/renate-oberdorfer/following",
|
||||||
"orderedItems": []
|
"orderedItems": []
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
@ -91,11 +91,11 @@ Feature: Receiving collections
|
|||||||
"""
|
"""
|
||||||
{
|
{
|
||||||
"@context": "https://www.w3.org/ns/activitystreams",
|
"@context": "https://www.w3.org/ns/activitystreams",
|
||||||
"id": "https://localhost:4123/activitypub/users/renate-oberdorfer/followers?page=true",
|
"id": "http://localhost:4123/activitypub/users/renate-oberdorfer/followers?page=true",
|
||||||
"summary": "renate-oberdorfers followers collection",
|
"summary": "renate-oberdorfers followers collection",
|
||||||
"type": "OrderedCollectionPage",
|
"type": "OrderedCollectionPage",
|
||||||
"totalItems": 0,
|
"totalItems": 0,
|
||||||
"partOf": "https://localhost:4123/activitypub/users/renate-oberdorfer/followers",
|
"partOf": "http://localhost:4123/activitypub/users/renate-oberdorfer/followers",
|
||||||
"orderedItems": []
|
"orderedItems": []
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
|
|||||||
@ -4,7 +4,7 @@ Feature: Webfinger discovery
|
|||||||
In order to follow the actor
|
In order to follow the actor
|
||||||
|
|
||||||
Background:
|
Background:
|
||||||
Given our own server runs at "http://localhost:4100"
|
Given our own server runs at "http://localhost:4123"
|
||||||
And we have the following users in our database:
|
And we have the following users in our database:
|
||||||
| Slug |
|
| Slug |
|
||||||
| peter-lustiger |
|
| peter-lustiger |
|
||||||
@ -19,7 +19,7 @@ Feature: Webfinger discovery
|
|||||||
{
|
{
|
||||||
"rel": "self",
|
"rel": "self",
|
||||||
"type": "application/activity+json",
|
"type": "application/activity+json",
|
||||||
"href": "https://localhost:4123/activitypub/users/peter-lustiger"
|
"href": "http://localhost:4123/activitypub/users/peter-lustiger"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@ -44,21 +44,21 @@ Feature: Webfinger discovery
|
|||||||
"https://www.w3.org/ns/activitystreams",
|
"https://www.w3.org/ns/activitystreams",
|
||||||
"https://w3id.org/security/v1"
|
"https://w3id.org/security/v1"
|
||||||
],
|
],
|
||||||
"id": "https://localhost:4123/activitypub/users/peter-lustiger",
|
"id": "http://localhost:4123/activitypub/users/peter-lustiger",
|
||||||
"type": "Person",
|
"type": "Person",
|
||||||
"preferredUsername": "peter-lustiger",
|
"preferredUsername": "peter-lustiger",
|
||||||
"name": "peter-lustiger",
|
"name": "peter-lustiger",
|
||||||
"following": "https://localhost:4123/activitypub/users/peter-lustiger/following",
|
"following": "http://localhost:4123/activitypub/users/peter-lustiger/following",
|
||||||
"followers": "https://localhost:4123/activitypub/users/peter-lustiger/followers",
|
"followers": "http://localhost:4123/activitypub/users/peter-lustiger/followers",
|
||||||
"inbox": "https://localhost:4123/activitypub/users/peter-lustiger/inbox",
|
"inbox": "http://localhost:4123/activitypub/users/peter-lustiger/inbox",
|
||||||
"outbox": "https://localhost:4123/activitypub/users/peter-lustiger/outbox",
|
"outbox": "http://localhost:4123/activitypub/users/peter-lustiger/outbox",
|
||||||
"url": "https://localhost:4123/activitypub/@peter-lustiger",
|
"url": "http://localhost:4123/activitypub/@peter-lustiger",
|
||||||
"endpoints": {
|
"endpoints": {
|
||||||
"sharedInbox": "https://localhost:4123/activitypub/inbox"
|
"sharedInbox": "http://localhost:4123/activitypub/inbox"
|
||||||
},
|
},
|
||||||
"publicKey": {
|
"publicKey": {
|
||||||
"id": "https://localhost:4123/activitypub/users/peter-lustiger#main-key",
|
"id": "http://localhost:4123/activitypub/users/peter-lustiger#main-key",
|
||||||
"owner": "https://localhost:4123/activitypub/users/peter-lustiger",
|
"owner": "http://localhost:4123/activitypub/users/peter-lustiger",
|
||||||
"publicKeyPem": "adglkjlk89235kjn8obn2384f89z5bv9..."
|
"publicKeyPem": "adglkjlk89235kjn8obn2384f89z5bv9..."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,7 +8,7 @@ Feature: Search
|
|||||||
And we have the following posts in our database:
|
And we have the following posts in our database:
|
||||||
| Author | id | title | content |
|
| Author | id | title | content |
|
||||||
| Brianna Wiest | p1 | 101 Essays that will change the way you think | 101 Essays, of course! |
|
| Brianna Wiest | p1 | 101 Essays that will change the way you think | 101 Essays, of course! |
|
||||||
| Brianna Wiest | p1 | No searched for content | will be found in this post, I guarantee |
|
| Brianna Wiest | p2 | No searched for content | will be found in this post, I guarantee |
|
||||||
Given I am logged in
|
Given I am logged in
|
||||||
|
|
||||||
Scenario: Search for specific words
|
Scenario: Search for specific words
|
||||||
|
|||||||
@ -17,7 +17,7 @@ Feature: Create a post
|
|||||||
for active citizenship.
|
for active citizenship.
|
||||||
"""
|
"""
|
||||||
And I click on "Save"
|
And I click on "Save"
|
||||||
Then I get redirected to "/post/my-first-post/"
|
Then I get redirected to ".../my-first-post"
|
||||||
And the post was saved successfully
|
And the post was saved successfully
|
||||||
|
|
||||||
Scenario: See a post on the landing page
|
Scenario: See a post on the landing page
|
||||||
|
|||||||
@ -71,7 +71,7 @@ When('I click on the author', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
When('I report the author', () => {
|
When('I report the author', () => {
|
||||||
cy.get('.page-name-profile-slug').then(() => {
|
cy.get('.page-name-profile-id-slug').then(() => {
|
||||||
invokeReportOnElement('.ds-card').then(() => {
|
invokeReportOnElement('.ds-card').then(() => {
|
||||||
cy.get('button')
|
cy.get('button')
|
||||||
.contains('Send')
|
.contains('Send')
|
||||||
|
|||||||
@ -42,9 +42,13 @@ When('I select an entry', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
Then("I should be on the post's page", () => {
|
Then("I should be on the post's page", () => {
|
||||||
|
cy.location('pathname').should(
|
||||||
|
'contain',
|
||||||
|
'/post/'
|
||||||
|
)
|
||||||
cy.location('pathname').should(
|
cy.location('pathname').should(
|
||||||
'eq',
|
'eq',
|
||||||
'/post/101-essays-that-will-change-the-way-you-think/'
|
'/post/p1/101-essays-that-will-change-the-way-you-think'
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -86,6 +86,10 @@ Given('my user account has the role {string}', role => {
|
|||||||
|
|
||||||
When('I log out', cy.logout)
|
When('I log out', cy.logout)
|
||||||
|
|
||||||
|
When('I visit {string}', page => {
|
||||||
|
cy.openPage(page)
|
||||||
|
})
|
||||||
|
|
||||||
When('I visit the {string} page', page => {
|
When('I visit the {string} page', page => {
|
||||||
cy.openPage(page)
|
cy.openPage(page)
|
||||||
})
|
})
|
||||||
@ -220,7 +224,7 @@ Then('the post shows up on the landing page at position {int}', index => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
Then('I get redirected to {string}', route => {
|
Then('I get redirected to {string}', route => {
|
||||||
cy.location('pathname').should('contain', route)
|
cy.location('pathname').should('contain', route.replace('...', ''))
|
||||||
})
|
})
|
||||||
|
|
||||||
Then('the post was saved successfully', () => {
|
Then('the post was saved successfully', () => {
|
||||||
|
|||||||
41
cypress/integration/identifier/PersistentLinks.feature
Normal file
41
cypress/integration/identifier/PersistentLinks.feature
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
Feature: Persistent Links
|
||||||
|
As a user
|
||||||
|
I want all links to carry permanent information that identifies the linked resource
|
||||||
|
In order to have persistent links even if a part of the URL might change
|
||||||
|
|
||||||
|
| | Modifiable | Referenceable | Unique | Purpose |
|
||||||
|
| -- | -- | -- | -- | -- |
|
||||||
|
| ID | no | yes | yes | Identity, Traceability, Links |
|
||||||
|
| Slug | yes | yes | yes | @-Mentions, SEO-friendly URL |
|
||||||
|
| Name | yes | no | no | Search, self-description |
|
||||||
|
|
||||||
|
|
||||||
|
Background:
|
||||||
|
Given we have the following user accounts:
|
||||||
|
| id | name | slug |
|
||||||
|
| MHNqce98y1 | Stephen Hawking | thehawk |
|
||||||
|
And we have the following posts in our database:
|
||||||
|
| id | title | slug |
|
||||||
|
| bWBjpkTKZp | 101 Essays that will change the way you think | 101-essays |
|
||||||
|
And I have a user account
|
||||||
|
And I am logged in
|
||||||
|
|
||||||
|
Scenario Outline: Link with slug only is valid and gets auto-completed
|
||||||
|
When I visit "<url>"
|
||||||
|
Then I get redirected to "<redirectUrl>"
|
||||||
|
Examples:
|
||||||
|
| url | redirectUrl |
|
||||||
|
| /profile/thehawk | /profile/MHNqce98y1/thehawk |
|
||||||
|
| /post/101-essays | /post/bWBjpkTKZp/101-essays |
|
||||||
|
|
||||||
|
Scenario: Link with id only will always point to the same user
|
||||||
|
When I visit "/profile/MHNqce98y1"
|
||||||
|
Then I get redirected to "/profile/MHNqce98y1/thehawk"
|
||||||
|
|
||||||
|
Scenario Outline: ID takes precedence over slug
|
||||||
|
When I visit "<url>"
|
||||||
|
Then I get redirected to "<redirectUrl>"
|
||||||
|
Examples:
|
||||||
|
| url | redirectUrl |
|
||||||
|
| /profile/MHNqce98y1/stephen-hawking | /profile/MHNqce98y1/thehawk |
|
||||||
|
| /post/bWBjpkTKZp/the-way-you-think | /post/bWBjpkTKZp/101-essays |
|
||||||
@ -111,8 +111,8 @@ export default {
|
|||||||
const result = res.data[this.id ? 'UpdatePost' : 'CreatePost']
|
const result = res.data[this.id ? 'UpdatePost' : 'CreatePost']
|
||||||
|
|
||||||
this.$router.push({
|
this.$router.push({
|
||||||
name: 'post-slug',
|
name: 'post-id-slug',
|
||||||
params: { slug: result.slug }
|
params: { id: result.id, slug: result.slug }
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
|
|||||||
@ -106,8 +106,8 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
href(post) {
|
href(post) {
|
||||||
return this.$router.resolve({
|
return this.$router.resolve({
|
||||||
name: 'post-slug',
|
name: 'post-id-slug',
|
||||||
params: { slug: post.slug }
|
params: { id: post.id, slug: post.slug }
|
||||||
}).href
|
}).href
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -153,9 +153,9 @@ export default {
|
|||||||
return count
|
return count
|
||||||
},
|
},
|
||||||
userLink() {
|
userLink() {
|
||||||
const { slug } = this.user
|
const { id, slug } = this.user
|
||||||
if (!slug) return ''
|
if (!(id && slug)) return ''
|
||||||
return { name: 'profile-slug', params: { slug } }
|
return { name: 'profile-id-slug', params: { slug, id } }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,58 +9,69 @@ export default app => {
|
|||||||
type
|
type
|
||||||
createdAt
|
createdAt
|
||||||
submitter {
|
submitter {
|
||||||
|
id
|
||||||
|
slug
|
||||||
|
name
|
||||||
disabled
|
disabled
|
||||||
deleted
|
deleted
|
||||||
name
|
|
||||||
slug
|
|
||||||
}
|
}
|
||||||
user {
|
user {
|
||||||
name
|
id
|
||||||
slug
|
slug
|
||||||
|
name
|
||||||
disabled
|
disabled
|
||||||
deleted
|
deleted
|
||||||
disabledBy {
|
disabledBy {
|
||||||
|
id
|
||||||
slug
|
slug
|
||||||
name
|
name
|
||||||
|
disabled
|
||||||
|
deleted
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
comment {
|
comment {
|
||||||
contentExcerpt
|
contentExcerpt
|
||||||
author {
|
author {
|
||||||
name
|
id
|
||||||
slug
|
slug
|
||||||
|
name
|
||||||
disabled
|
disabled
|
||||||
deleted
|
deleted
|
||||||
}
|
}
|
||||||
post {
|
post {
|
||||||
|
id
|
||||||
|
slug
|
||||||
|
title
|
||||||
disabled
|
disabled
|
||||||
deleted
|
deleted
|
||||||
title
|
|
||||||
slug
|
|
||||||
}
|
}
|
||||||
disabledBy {
|
disabledBy {
|
||||||
disabled
|
id
|
||||||
deleted
|
|
||||||
slug
|
slug
|
||||||
name
|
name
|
||||||
|
disabled
|
||||||
|
deleted
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
post {
|
post {
|
||||||
title
|
id
|
||||||
slug
|
slug
|
||||||
|
title
|
||||||
disabled
|
disabled
|
||||||
deleted
|
deleted
|
||||||
author {
|
author {
|
||||||
|
id
|
||||||
|
slug
|
||||||
|
name
|
||||||
disabled
|
disabled
|
||||||
deleted
|
deleted
|
||||||
name
|
|
||||||
slug
|
|
||||||
}
|
}
|
||||||
disabledBy {
|
disabledBy {
|
||||||
disabled
|
id
|
||||||
deleted
|
|
||||||
slug
|
slug
|
||||||
name
|
name
|
||||||
|
disabled
|
||||||
|
deleted
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,6 +6,7 @@ export default app => {
|
|||||||
query User($slug: String!, $first: Int, $offset: Int) {
|
query User($slug: String!, $first: Int, $offset: Int) {
|
||||||
User(slug: $slug) {
|
User(slug: $slug) {
|
||||||
id
|
id
|
||||||
|
slug
|
||||||
name
|
name
|
||||||
avatar
|
avatar
|
||||||
about
|
about
|
||||||
@ -27,8 +28,8 @@ export default app => {
|
|||||||
followingCount
|
followingCount
|
||||||
following(first: 7) {
|
following(first: 7) {
|
||||||
id
|
id
|
||||||
name
|
|
||||||
slug
|
slug
|
||||||
|
name
|
||||||
avatar
|
avatar
|
||||||
disabled
|
disabled
|
||||||
deleted
|
deleted
|
||||||
@ -49,10 +50,10 @@ export default app => {
|
|||||||
followedByCurrentUser
|
followedByCurrentUser
|
||||||
followedBy(first: 7) {
|
followedBy(first: 7) {
|
||||||
id
|
id
|
||||||
|
slug
|
||||||
name
|
name
|
||||||
disabled
|
disabled
|
||||||
deleted
|
deleted
|
||||||
slug
|
|
||||||
avatar
|
avatar
|
||||||
followedByCount
|
followedByCount
|
||||||
followedByCurrentUser
|
followedByCurrentUser
|
||||||
@ -87,6 +88,7 @@ export default app => {
|
|||||||
}
|
}
|
||||||
author {
|
author {
|
||||||
id
|
id
|
||||||
|
slug
|
||||||
avatar
|
avatar
|
||||||
name
|
name
|
||||||
disabled
|
disabled
|
||||||
|
|||||||
@ -39,7 +39,7 @@
|
|||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
class="avatar-menu-trigger"
|
class="avatar-menu-trigger"
|
||||||
:href="$router.resolve({name: 'profile-slug', params: {slug: user.slug}}).href"
|
:href="$router.resolve({name: 'profile-id-slug', params: {id: user.id, slug: user.slug}}).href"
|
||||||
@click.prevent="toggleMenu"
|
@click.prevent="toggleMenu"
|
||||||
>
|
>
|
||||||
<ds-avatar
|
<ds-avatar
|
||||||
@ -182,8 +182,8 @@ export default {
|
|||||||
goToPost(item) {
|
goToPost(item) {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.$router.push({
|
this.$router.push({
|
||||||
name: 'post-slug',
|
name: 'post-id-slug',
|
||||||
params: { slug: item.slug }
|
params: { id: item.id, slug: item.slug }
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|||||||
32
webapp/mixins/persistentLinks.js
Normal file
32
webapp/mixins/persistentLinks.js
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
export default function(options = {}) {
|
||||||
|
const { queryId, querySlug, path, message = 'Page not found.' } = options
|
||||||
|
return {
|
||||||
|
asyncData: async context => {
|
||||||
|
const {
|
||||||
|
params: { id, slug },
|
||||||
|
redirect,
|
||||||
|
error,
|
||||||
|
app: { apolloProvider }
|
||||||
|
} = context
|
||||||
|
const idOrSlug = id || slug
|
||||||
|
|
||||||
|
const variables = { idOrSlug }
|
||||||
|
const client = apolloProvider.defaultClient
|
||||||
|
|
||||||
|
let response
|
||||||
|
let resource
|
||||||
|
response = await client.query({ query: queryId, variables })
|
||||||
|
resource = response.data[Object.keys(response.data)[0]][0]
|
||||||
|
if (resource && resource.slug === slug) return // all good
|
||||||
|
if (resource && resource.slug !== slug) {
|
||||||
|
return redirect(`/${path}/${resource.id}/${resource.slug}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
response = await client.query({ query: querySlug, variables })
|
||||||
|
resource = response.data[Object.keys(response.data)[0]][0]
|
||||||
|
if (resource) return redirect(`/${path}/${resource.id}/${resource.slug}`)
|
||||||
|
|
||||||
|
return error({ statusCode: 404, message })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -123,13 +123,22 @@ module.exports = {
|
|||||||
proxy: {
|
proxy: {
|
||||||
'/.well-known/webfinger': {
|
'/.well-known/webfinger': {
|
||||||
target: process.env.GRAPHQL_URI || 'http://localhost:4000',
|
target: process.env.GRAPHQL_URI || 'http://localhost:4000',
|
||||||
toProxy: true // cloudflare needs that
|
toProxy: true, // cloudflare needs that
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
'X-UI-Request': true,
|
||||||
|
'X-API-TOKEN': process.env.BACKEND_TOKEN || 'NULL'
|
||||||
|
}
|
||||||
},
|
},
|
||||||
'/activityPub': {
|
'/activitypub': {
|
||||||
// make this configurable (nuxt-dotenv)
|
// make this configurable (nuxt-dotenv)
|
||||||
target: process.env.GRAPHQL_URI || 'http://localhost:4000',
|
target: process.env.GRAPHQL_URI || 'http://localhost:4000',
|
||||||
pathRewrite: { '^/activityPub': '' },
|
toProxy: true, // cloudflare needs that
|
||||||
toProxy: true // cloudflare needs that
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
'X-UI-Request': true,
|
||||||
|
'X-API-TOKEN': process.env.BACKEND_TOKEN || 'NULL'
|
||||||
|
}
|
||||||
},
|
},
|
||||||
'/api': {
|
'/api': {
|
||||||
// make this configurable (nuxt-dotenv)
|
// make this configurable (nuxt-dotenv)
|
||||||
|
|||||||
@ -64,8 +64,8 @@ export default {
|
|||||||
},
|
},
|
||||||
href(post) {
|
href(post) {
|
||||||
return this.$router.resolve({
|
return this.$router.resolve({
|
||||||
name: 'post-slug',
|
name: 'post-id-slug',
|
||||||
params: { slug: post.slug }
|
params: { id: post.id, slug: post.slug }
|
||||||
}).href
|
}).href
|
||||||
},
|
},
|
||||||
showMoreContributions() {
|
showMoreContributions() {
|
||||||
|
|||||||
@ -14,7 +14,7 @@
|
|||||||
slot-scope="scope"
|
slot-scope="scope"
|
||||||
>
|
>
|
||||||
<div v-if="scope.row.type === 'Post'">
|
<div v-if="scope.row.type === 'Post'">
|
||||||
<nuxt-link :to="{ name: 'post-slug', params: { slug: scope.row.post.slug } }">
|
<nuxt-link :to="{ name: 'post-id-slug', params: { id: scope.row.post.id, slug: scope.row.post.slug } }">
|
||||||
<b>{{ scope.row.post.title | truncate(50) }}</b>
|
<b>{{ scope.row.post.title | truncate(50) }}</b>
|
||||||
</nuxt-link><br>
|
</nuxt-link><br>
|
||||||
<ds-text
|
<ds-text
|
||||||
@ -25,7 +25,7 @@
|
|||||||
</ds-text>
|
</ds-text>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="scope.row.type === 'Comment'">
|
<div v-else-if="scope.row.type === 'Comment'">
|
||||||
<nuxt-link :to="{ name: 'post-slug', params: { slug: scope.row.comment.post.slug } }">
|
<nuxt-link :to="{ name: 'post-id-slug', params: { id: scope.row.comment.post.id, slug: scope.row.comment.post.slug } }">
|
||||||
<b>{{ scope.row.comment.contentExcerpt | truncate(50) }}</b>
|
<b>{{ scope.row.comment.contentExcerpt | truncate(50) }}</b>
|
||||||
</nuxt-link><br>
|
</nuxt-link><br>
|
||||||
<ds-text
|
<ds-text
|
||||||
@ -36,7 +36,7 @@
|
|||||||
</ds-text>
|
</ds-text>
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<nuxt-link :to="{ name: 'profile-slug', params: { slug: scope.row.user.slug } }">
|
<nuxt-link :to="{ name: 'profile-id-slug', params: { id: scope.row.user.id, slug: scope.row.user.slug } }">
|
||||||
<b>{{ scope.row.user.name | truncate(50) }}</b>
|
<b>{{ scope.row.user.name | truncate(50) }}</b>
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
</div>
|
</div>
|
||||||
@ -69,7 +69,7 @@
|
|||||||
slot="submitter"
|
slot="submitter"
|
||||||
slot-scope="scope"
|
slot-scope="scope"
|
||||||
>
|
>
|
||||||
<nuxt-link :to="{ name: 'profile-slug', params: { slug: scope.row.submitter.slug } }">
|
<nuxt-link :to="{ name: 'profile-id-slug', params: { id: scope.row.submitter.id, slug: scope.row.submitter.slug } }">
|
||||||
{{ scope.row.submitter.name }}
|
{{ scope.row.submitter.name }}
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
</template>
|
</template>
|
||||||
@ -79,19 +79,19 @@
|
|||||||
>
|
>
|
||||||
<nuxt-link
|
<nuxt-link
|
||||||
v-if="scope.row.type === 'Post' && scope.row.post.disabledBy"
|
v-if="scope.row.type === 'Post' && scope.row.post.disabledBy"
|
||||||
:to="{ name: 'profile-slug', params: { slug: scope.row.post.disabledBy.slug } }"
|
:to="{ name: 'profile-id-slug', params: { id: scope.row.post.disabledBy.id, slug: scope.row.post.disabledBy.slug } }"
|
||||||
>
|
>
|
||||||
<b>{{ scope.row.post.disabledBy.name | truncate(50) }}</b>
|
<b>{{ scope.row.post.disabledBy.name | truncate(50) }}</b>
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
<nuxt-link
|
<nuxt-link
|
||||||
v-else-if="scope.row.type === 'Comment' && scope.row.comment.disabledBy"
|
v-else-if="scope.row.type === 'Comment' && scope.row.comment.disabledBy"
|
||||||
:to="{ name: 'profile-slug', params: { slug: scope.row.comment.disabledBy.slug } }"
|
:to="{ name: 'profile-id-slug', params: { id: scope.row.comment.disabledBy.id, slug: scope.row.comment.disabledBy.slug } }"
|
||||||
>
|
>
|
||||||
<b>{{ scope.row.comment.disabledBy.name | truncate(50) }}</b>
|
<b>{{ scope.row.comment.disabledBy.name | truncate(50) }}</b>
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
<nuxt-link
|
<nuxt-link
|
||||||
v-else-if="scope.row.type === 'User' && scope.row.user.disabledBy"
|
v-else-if="scope.row.type === 'User' && scope.row.user.disabledBy"
|
||||||
:to="{ name: 'profile-slug', params: { slug: scope.row.user.disabledBy.slug } }"
|
:to="{ name: 'profile-id-slug', params: { id: scope.row.user.disabledBy.id, slug: scope.row.user.disabledBy.slug } }"
|
||||||
>
|
>
|
||||||
<b>{{ scope.row.user.disabledBy.name | truncate(50) }}</b>
|
<b>{{ scope.row.user.disabledBy.name | truncate(50) }}</b>
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
|
|||||||
@ -17,35 +17,62 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import gql from 'graphql-tag'
|
||||||
|
import PersistentLinks from '~/mixins/persistentLinks.js'
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
queryId: gql`
|
||||||
|
query($idOrSlug: ID) {
|
||||||
|
Post(id: $idOrSlug) {
|
||||||
|
id
|
||||||
|
slug
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
querySlug: gql`
|
||||||
|
query($idOrSlug: String) {
|
||||||
|
Post(slug: $idOrSlug) {
|
||||||
|
id
|
||||||
|
slug
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
path: 'post',
|
||||||
|
message: 'This post could not be found'
|
||||||
|
}
|
||||||
|
const persistentLinks = PersistentLinks(options)
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
mixins: [persistentLinks],
|
||||||
computed: {
|
computed: {
|
||||||
routes() {
|
routes() {
|
||||||
|
const { slug, id } = this.$route.params
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
name: this.$t('common.post', null, 1),
|
name: this.$t('common.post', null, 1),
|
||||||
path: `/post/${this.$route.params.slug}`,
|
path: `/post/${id}/${slug}`,
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
name: this.$t('common.comment', null, 2),
|
name: this.$t('common.comment', null, 2),
|
||||||
path: `/post/${this.$route.params.slug}#comments`
|
path: `/post/${id}/${slug}#comments`
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: this.$t('common.letsTalk'),
|
name: this.$t('common.letsTalk'),
|
||||||
path: `/post/${this.$route.params.slug}#lets-talk`
|
path: `/post/${id}/${slug}#lets-talk`
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: this.$t('common.versus'),
|
name: this.$t('common.versus'),
|
||||||
path: `/post/${this.$route.params.slug}#versus`
|
path: `/post/${id}/${slug}#versus`
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: this.$t('common.moreInfo'),
|
name: this.$t('common.moreInfo'),
|
||||||
path: `/post/${this.$route.params.slug}/more-info`
|
path: `/post/${id}/${slug}/more-info`
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: this.$t('common.takeAction'),
|
name: this.$t('common.takeAction'),
|
||||||
path: `/post/${this.$route.params.slug}/take-action`
|
path: `/post/${id}/${slug}/take-action`
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@ -263,7 +263,7 @@ export default {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.page-name-post-slug {
|
.page-name-post-id-slug {
|
||||||
.content-menu {
|
.content-menu {
|
||||||
float: right;
|
float: right;
|
||||||
margin-right: -$space-x-small;
|
margin-right: -$space-x-small;
|
||||||
34
webapp/pages/profile/_id.vue
Normal file
34
webapp/pages/profile/_id.vue
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
<template>
|
||||||
|
<nuxt-child />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import gql from 'graphql-tag'
|
||||||
|
import PersistentLinks from '~/mixins/persistentLinks.js'
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
queryId: gql`
|
||||||
|
query($idOrSlug: ID) {
|
||||||
|
User(id: $idOrSlug) {
|
||||||
|
id
|
||||||
|
slug
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
querySlug: gql`
|
||||||
|
query($idOrSlug: String) {
|
||||||
|
User(slug: $idOrSlug) {
|
||||||
|
id
|
||||||
|
slug
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
message: 'This user could not be found',
|
||||||
|
path: 'profile'
|
||||||
|
}
|
||||||
|
const persistentLinks = PersistentLinks(options)
|
||||||
|
|
||||||
|
export default {
|
||||||
|
mixins: [persistentLinks]
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@ -423,7 +423,7 @@ export default {
|
|||||||
border: #fff 5px solid;
|
border: #fff 5px solid;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-name-profile-slug {
|
.page-name-profile-id-slug {
|
||||||
.ds-flex-item:first-child .content-menu {
|
.ds-flex-item:first-child .content-menu {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: $space-x-small;
|
top: $space-x-small;
|
||||||
@ -1,8 +0,0 @@
|
|||||||
<script>
|
|
||||||
export default {
|
|
||||||
layout: 'blank',
|
|
||||||
asyncData({ error }) {
|
|
||||||
error({ statusCode: 404, message: 'Profile slug missing' })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
Loading…
x
Reference in New Issue
Block a user