From 48535b2381afd6ed3fdf6854549d148b7256659a Mon Sep 17 00:00:00 2001 From: Jonas Natten Date: Mon, 20 Nov 2023 06:36:41 +0100 Subject: [PATCH 1/2] Add mutation to create new topics in arena --- src/api/arenaApi.ts | 37 +++++++++++++++++++++++++++++++++ src/resolvers/arenaResolvers.ts | 13 ++++++++++++ src/resolvers/index.ts | 6 +++++- src/schema.ts | 5 +++++ src/types/schema.d.ts | 11 +++++++++- 5 files changed, 70 insertions(+), 2 deletions(-) diff --git a/src/api/arenaApi.ts b/src/api/arenaApi.ts index 35e6beeb..f23eadb5 100644 --- a/src/api/arenaApi.ts +++ b/src/api/arenaApi.ts @@ -11,6 +11,7 @@ import { GQLArenaPost, GQLArenaTopic, GQLArenaUser, + GQLMutationNewArenaTopicArgs, GQLQueryArenaCategoryArgs, GQLQueryArenaTopicArgs, GQLQueryArenaTopicsByUserArgs, @@ -68,6 +69,24 @@ const toCategory = (category: any): GQLArenaCategory => { }; }; +export const fetchCsrfTokenForSession = async ( + context: Context, +): Promise<{ cookie: string; 'x-csrf-token': string }> => { + const incomingCookie = context.req.headers.cookie; + const incomingCsrfToken = context.req.headers['x-csrf-token']; + if (incomingCookie !== undefined && typeof incomingCsrfToken === 'string') { + return { cookie: incomingCookie, 'x-csrf-token': incomingCsrfToken }; + } + + const response = await fetch('/groups/api/config', context); + const resolved: any = await resolveJson(response); + const token = resolved.csrf_token; + return { + 'x-csrf-token': token, + cookie: response.headers.get('set-cookie')!, + }; +}; + export const fetchArenaUser = async ( { username }: GQLQueryArenaUserArgs, context: Context, @@ -128,3 +147,21 @@ export const fetchArenaTopicsByUser = async ( const resolved = await resolveJson(response); return resolved.topics.map(toTopic); }; + +export const newTopic = async ( + { title, content, categoryId }: GQLMutationNewArenaTopicArgs, + context: Context, +): Promise => { + const csrfHeaders = await fetchCsrfTokenForSession(context); + const response = await fetch(`/groups/api/v3/topics`, context, { + method: 'POST', + headers: { 'content-type': 'application/json', ...csrfHeaders }, + body: JSON.stringify({ + cid: categoryId, + title, + content, + }), + }); + const resolved = await resolveJson(response); + return toTopic(resolved.response); +}; diff --git a/src/resolvers/arenaResolvers.ts b/src/resolvers/arenaResolvers.ts index 9b00a7af..15f96c38 100644 --- a/src/resolvers/arenaResolvers.ts +++ b/src/resolvers/arenaResolvers.ts @@ -13,6 +13,8 @@ import { fetchArenaUser, fetchArenaTopic, fetchArenaTopicsByUser, + fetchCsrfTokenForSession, + newTopic, } from '../api/arenaApi'; import { GQLArenaCategory, @@ -23,6 +25,7 @@ import { GQLQueryArenaTopicArgs, GQLQueryArenaTopicsByUserArgs, GQLQueryResolvers, + GQLMutationResolvers, } from '../types/schema'; export const Query: Pick< @@ -77,3 +80,13 @@ export const Query: Pick< return fetchArenaTopicsByUser(params, context); }, }; + +export const Mutations: Pick = { + async newArenaTopic( + _: any, + params, + context: ContextWithLoaders, + ): Promise { + return newTopic(params, context); + }, +}; diff --git a/src/resolvers/index.ts b/src/resolvers/index.ts index 209efa3c..5d3d4a55 100644 --- a/src/resolvers/index.ts +++ b/src/resolvers/index.ts @@ -69,7 +69,10 @@ import { resolvers as ProgrammeResolvers, } from './programmeResolvers'; -import { Query as ArenaQuery } from './arenaResolvers'; +import { + Query as ArenaQuery, + Mutations as ArenaMutations, +} from './arenaResolvers'; export const resolvers = { Query: { @@ -93,6 +96,7 @@ export const resolvers = { Mutation: { ...FolderMutations, ...TransformArticleMutations, + ...ArenaMutations, }, ...folderResolvers, ...articleResolvers, diff --git a/src/schema.ts b/src/schema.ts index b7525092..bbb8e02d 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -1393,6 +1393,11 @@ export const typeDefs = gql` draftConcept: Boolean absoluteUrl: Boolean ): String! + newArenaTopic( + categoryId: Int! + title: String! + content: String! + ): ArenaTopic! } `; diff --git a/src/types/schema.d.ts b/src/types/schema.d.ts index 921a8113..2664937c 100644 --- a/src/types/schema.d.ts +++ b/src/types/schema.d.ts @@ -876,6 +876,7 @@ export type GQLMutation = { deleteFolder: Scalars['String']; deleteFolderResource: Scalars['String']; deletePersonalData: Scalars['Boolean']; + newArenaTopic: GQLArenaTopic; sortFolders: GQLSortResult; sortResources: GQLSortResult; transformArticleContent: Scalars['String']; @@ -920,6 +921,13 @@ export type GQLMutationDeleteFolderResourceArgs = { }; +export type GQLMutationNewArenaTopicArgs = { + categoryId: Scalars['Int']; + content: Scalars['String']; + title: Scalars['String']; +}; + + export type GQLMutationSortFoldersArgs = { parentId?: InputMaybe; sortedIds: Array; @@ -2221,7 +2229,7 @@ export type GQLArenaTopicResolvers = { displayName?: Resolver; - groupTitleArray?: Resolver>>, ParentType, ContextType>; + groupTitleArray?: Resolver, ParentType, ContextType>; id?: Resolver; profilePicture?: Resolver, ParentType, ContextType>; slug?: Resolver; @@ -3018,6 +3026,7 @@ export type GQLMutationResolvers>; deleteFolderResource?: Resolver>; deletePersonalData?: Resolver; + newArenaTopic?: Resolver>; sortFolders?: Resolver>; sortResources?: Resolver>; transformArticleContent?: Resolver>; From f7b140c3354e4194eadd8ab08f988ad860eefbae Mon Sep 17 00:00:00 2001 From: Jonas Natten Date: Mon, 20 Nov 2023 12:09:26 +0100 Subject: [PATCH 2/2] Add mutation to allow replying to a topic --- src/api/arenaApi.ts | 29 ++++++++++++++++++++++++++++- src/resolvers/arenaResolvers.ts | 14 +++++++++++++- src/schema.ts | 1 + src/types/schema.d.ts | 8 ++++++++ 4 files changed, 50 insertions(+), 2 deletions(-) diff --git a/src/api/arenaApi.ts b/src/api/arenaApi.ts index f23eadb5..3a1850cc 100644 --- a/src/api/arenaApi.ts +++ b/src/api/arenaApi.ts @@ -12,6 +12,7 @@ import { GQLArenaTopic, GQLArenaUser, GQLMutationNewArenaTopicArgs, + GQLMutationReplyToTopicArgs, GQLQueryArenaCategoryArgs, GQLQueryArenaTopicArgs, GQLQueryArenaTopicsByUserArgs, @@ -80,10 +81,17 @@ export const fetchCsrfTokenForSession = async ( const response = await fetch('/groups/api/config', context); const resolved: any = await resolveJson(response); + const responseCookie = response.headers.get('set-cookie'); + + if (!responseCookie) + throw new Error( + 'Did not get set-cookie header from /groups/api/config endpoint to use together with csrf token.', + ); + const token = resolved.csrf_token; return { 'x-csrf-token': token, - cookie: response.headers.get('set-cookie')!, + cookie: responseCookie, }; }; @@ -165,3 +173,22 @@ export const newTopic = async ( const resolved = await resolveJson(response); return toTopic(resolved.response); }; + +export const replyToTopic = async ( + { topicId, content }: GQLMutationReplyToTopicArgs, + context: Context, +) => { + const csrfHeaders = await fetchCsrfTokenForSession(context); + const response = await fetch(`/groups/api/v3/topics/${topicId}`, context, { + method: 'POST', + headers: { + 'content-type': 'application/json', + ...csrfHeaders, + }, + body: JSON.stringify({ + content, + }), + }); + const resolved = await resolveJson(response); + return toArenaPost(resolved.response); +}; diff --git a/src/resolvers/arenaResolvers.ts b/src/resolvers/arenaResolvers.ts index 15f96c38..05d4d80b 100644 --- a/src/resolvers/arenaResolvers.ts +++ b/src/resolvers/arenaResolvers.ts @@ -15,6 +15,7 @@ import { fetchArenaTopicsByUser, fetchCsrfTokenForSession, newTopic, + replyToTopic, } from '../api/arenaApi'; import { GQLArenaCategory, @@ -26,6 +27,7 @@ import { GQLQueryArenaTopicsByUserArgs, GQLQueryResolvers, GQLMutationResolvers, + GQLArenaPost, } from '../types/schema'; export const Query: Pick< @@ -81,7 +83,10 @@ export const Query: Pick< }, }; -export const Mutations: Pick = { +export const Mutations: Pick< + GQLMutationResolvers, + 'newArenaTopic' | 'replyToTopic' +> = { async newArenaTopic( _: any, params, @@ -89,4 +94,11 @@ export const Mutations: Pick = { ): Promise { return newTopic(params, context); }, + async replyToTopic( + _: any, + params, + context: ContextWithLoaders, + ): Promise { + return replyToTopic(params, context); + }, }; diff --git a/src/schema.ts b/src/schema.ts index bbb8e02d..6dc8dd05 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -1398,6 +1398,7 @@ export const typeDefs = gql` title: String! content: String! ): ArenaTopic! + replyToTopic(topicId: Int!, content: String!): ArenaPost! } `; diff --git a/src/types/schema.d.ts b/src/types/schema.d.ts index 2664937c..c5dbde83 100644 --- a/src/types/schema.d.ts +++ b/src/types/schema.d.ts @@ -877,6 +877,7 @@ export type GQLMutation = { deleteFolderResource: Scalars['String']; deletePersonalData: Scalars['Boolean']; newArenaTopic: GQLArenaTopic; + replyToTopic: GQLArenaPost; sortFolders: GQLSortResult; sortResources: GQLSortResult; transformArticleContent: Scalars['String']; @@ -928,6 +929,12 @@ export type GQLMutationNewArenaTopicArgs = { }; +export type GQLMutationReplyToTopicArgs = { + content: Scalars['String']; + topicId: Scalars['Int']; +}; + + export type GQLMutationSortFoldersArgs = { parentId?: InputMaybe; sortedIds: Array; @@ -3027,6 +3034,7 @@ export type GQLMutationResolvers>; deletePersonalData?: Resolver; newArenaTopic?: Resolver>; + replyToTopic?: Resolver>; sortFolders?: Resolver>; sortResources?: Resolver>; transformArticleContent?: Resolver>;