refactor(backend): user graphql (#8354)

* refactor user graphql

- remove isLoggedIn query
- currentUser query only for authenticated, currenUser always returns a
User
- currentUser query implementation uses neo4jgraphql with id parameter
- remove custom email field from user
- fix bug in frontend when there is no categories

* remove comment

* remove unused filter

* fix currentuser test

* fixedswitchUserRole mutation

* fix categories
This commit is contained in:
Ulf Gebhardt 2025-04-12 02:50:16 +02:00 committed by GitHub
parent fa0280f9e9
commit 117c0d75e7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 37 additions and 147 deletions

View File

@ -384,7 +384,7 @@ export default shield(
Tag: allow,
reports: isModerator,
statistics: allow,
currentUser: allow,
currentUser: isAuthenticated,
Group: isAuthenticated,
GroupMembers: isAllowedSeeingGroupMembers,
GroupCount: isAuthenticated,
@ -392,7 +392,6 @@ export default shield(
profilePagePosts: allow,
Comment: allow,
User: or(noEmailFilter, isAdmin),
isLoggedIn: allow,
Badge: allow,
PostsEmotionsCountByEmotion: allow,
PostsEmotionsByCurrentUser: isAuthenticated,

View File

@ -64,55 +64,6 @@ afterEach(async () => {
await cleanDatabase()
})
describe('isLoggedIn', () => {
const isLoggedInQuery = gql`
{
isLoggedIn
}
`
const respondsWith = async (expected) => {
await expect(query({ query: isLoggedInQuery })).resolves.toMatchObject(expected)
}
describe('unauthenticated', () => {
it('returns false', async () => {
await respondsWith({ data: { isLoggedIn: false } })
})
})
describe('authenticated', () => {
beforeEach(async () => {
user = await Factory.build('user', { id: 'u3' })
const userBearerToken = encode({ id: 'u3' })
req = { headers: { authorization: `Bearer ${userBearerToken}` } }
})
it('returns true', async () => {
await respondsWith({ data: { isLoggedIn: true } })
})
describe('but user is disabled', () => {
beforeEach(async () => {
await disable('u3')
})
it('returns false', async () => {
await respondsWith({ data: { isLoggedIn: false } })
})
})
describe('but user is deleted', () => {
beforeEach(async () => {
await user.update({ updatedAt: new Date().toISOString(), deleted: true })
})
it('returns false', async () => {
await respondsWith({ data: { isLoggedIn: false } })
})
})
})
})
describe('currentUser', () => {
const currentUserQuery = gql`
{
@ -135,8 +86,8 @@ describe('currentUser', () => {
}
describe('unauthenticated', () => {
it('returns null', async () => {
await respondsWith({ data: { currentUser: null } })
it('throws "Not Authorized!"', async () => {
await respondsWith({ errors: [{ message: 'Not Authorized!' }] })
})
})
@ -200,10 +151,32 @@ describe('currentUser', () => {
)
})
it('returns empty array for all categories', async () => {
it('returns all categories by default', async () => {
await respondsWith({
data: {
currentUser: expect.objectContaining({ activeCategories: [] }),
currentUser: expect.objectContaining({
activeCategories: [
'cat1',
'cat10',
'cat11',
'cat12',
'cat13',
'cat14',
'cat15',
'cat16',
'cat17',
'cat18',
'cat19',
'cat2',
'cat3',
'cat4',
'cat5',
'cat6',
'cat7',
'cat8',
'cat9',
],
}),
},
})
})

View File

@ -1,5 +1,6 @@
import { AuthenticationError } from 'apollo-server'
import bcrypt from 'bcryptjs'
import { neo4jgraphql } from 'neo4j-graphql-js'
import { getNeode } from '@db/neo4j'
import encode from '@jwt/encode'
@ -11,38 +12,8 @@ const neode = getNeode()
export default {
Query: {
isLoggedIn: (_, args, { driver, user }) => {
return Boolean(user && user.id)
},
currentUser: async (object, params, context, resolveInfo) => {
const { user, driver } = context
if (!user) return null
const session = driver.session()
const currentUserTransactionPromise = session.readTransaction(async (transaction) => {
const result = await transaction.run(
`
MATCH (user:User {id: $id})
OPTIONAL MATCH (category:Category) WHERE NOT ((user)-[:NOT_INTERESTED_IN]->(category))
OPTIONAL MATCH (cats:Category)
WITH user, [(user)<-[:OWNED_BY]-(medium:SocialMedia) | properties(medium) ] AS media, category, toString(COUNT(cats)) AS categoryCount
RETURN user {.*, socialMedia: media, activeCategories: collect(category.id) } AS user, categoryCount
`,
{ id: user.id },
)
const [categoryCount] = result.records.map((record) => record.get('categoryCount'))
const [currentUser] = result.records.map((record) => record.get('user'))
// frontend expects empty array when all categories are selected
if (currentUser.activeCategories.length === parseInt(categoryCount))
currentUser.activeCategories = []
return currentUser
})
try {
const currentUser = await currentUserTransactionPromise
return currentUser
} finally {
session.close()
}
},
currentUser: async (object, params, context, resolveInfo) =>
neo4jgraphql(object, { id: context.user.id }, context, resolveInfo),
},
Mutation: {
login: async (_, { email, password }, { driver, req, user }) => {

View File

@ -66,12 +66,12 @@ export default {
const result = txc.run(
`
MATCH (user:User)-[:PRIMARY_EMAIL]->(e:EmailAddress {email: $args.email})
RETURN user`,
RETURN user {.*, email: e.email}`,
{ args },
)
return result
})
return readTxResult.records.map((r) => r.get('user').properties)
return readTxResult.records.map((r) => r.get('user'))
} finally {
session.close()
}
@ -291,14 +291,14 @@ export default {
const switchUserRoleResponse = await transaction.run(
`
MATCH (user:User {id: $id})
OPTIONAL MATCH (user)-[:PRIMARY_EMAIL]->(e:EmailAddress)
SET user.role = $role
SET user.updatedAt = toString(datetime())
RETURN user {.*}
RETURN user {.*, email: e.email}
`,
{ id, role },
)
const [user] = switchUserRoleResponse.records.map((record) => record.get('user'))
return user
return switchUserRoleResponse.records.map((record) => record.get('user'))[0]
})
try {
const user = await writeTxResultPromise
@ -383,14 +383,6 @@ export default {
},
},
User: {
email: async (parent, params, context, resolveInfo) => {
if (typeof parent.email !== 'undefined') return parent.email
const { id } = parent
const statement = `MATCH(u:User {id: $id})-[:PRIMARY_EMAIL]->(e:EmailAddress) RETURN e`
const result = await neode.cypher(statement, { id })
const [{ email }] = result.records.map((r) => r.get('e').properties)
return email
},
emailNotificationSettings: async (parent, params, context, resolveInfo) => {
return [
{

View File

@ -199,8 +199,7 @@ type Query {
availableRoles: [UserRole]!
mutedUsers: [User]
blockedUsers: [User]
isLoggedIn: Boolean!
currentUser: User
currentUser: User!
}
enum Deletable {

View File

@ -1,44 +0,0 @@
import { createTestClient } from 'apollo-server-testing'
import createServer from './server'
/**
* This file is for demonstration purposes. It does not really test the
* `isLoggedIn` query but demonstrates how we can use `apollo-server-testing`.
* All we need to do is to get an instance of `ApolloServer` and maybe we want
* stub out `context` as shown below.
*
*/
let user
let action
describe('isLoggedIn', () => {
beforeEach(() => {
action = async () => {
const { server } = createServer({
context: () => {
return {
user,
}
},
})
const { query } = createTestClient(server)
const isLoggedIn = `{ isLoggedIn }`
return query({ query: isLoggedIn })
}
})
it('returns false', async () => {
const expected = expect.objectContaining({ data: { isLoggedIn: false } })
await expect(action()).resolves.toEqual(expected)
})
describe('when authenticated', () => {
it('returns true', async () => {
user = { id: '123' }
const expected = expect.objectContaining({ data: { isLoggedIn: true } })
await expect(action()).resolves.toEqual(expected)
})
})
})

View File

@ -100,7 +100,7 @@ export default {
await this.$store.dispatch('auth/login', { email, password })
if (this.currentUser && this.currentUser.activeCategories) {
this.resetCategories()
if (this.currentUser.activeCategories.length > 0) {
if (this.currentUser.activeCategories.length < 19) {
this.currentUser.activeCategories.forEach((categoryId) => {
this.toggleCategory(categoryId)
})