diff --git a/api/resolvers/user.js b/api/resolvers/user.js index 1ff9f6e9..ea6b143c 100644 --- a/api/resolvers/user.js +++ b/api/resolvers/user.js @@ -46,7 +46,8 @@ async function authMethods (user, args, { models, me }) { email: user.emailVerified && user.email, twitter: oauth.indexOf('twitter') >= 0, github: oauth.indexOf('github') >= 0, - nostr: !!user.nostrAuthPubkey + nostr: !!user.nostrAuthPubkey, + apiKey: user.apiKeyEnabled ? user.apiKey : null } } @@ -521,6 +522,26 @@ export default { return await models.user.findUnique({ where: { id: me.id } }) }, + generateApiKey: async (parent, { id }, { models, me }) => { + if (!me) { + throw new GraphQLError('you must be logged in', { extensions: { code: 'UNAUTHENTICATED' } }) + } + + const user = await models.user.findUnique({ where: { id: me.id } }) + if (!user.apiKeyEnabled) { + throw new GraphQLError('you are not allowed to generate api keys', { extensions: { code: 'FORBIDDEN' } }) + } + + const [{ apiKey }] = await models.$queryRaw`UPDATE users SET "apiKey" = encode(gen_random_bytes(32), 'base64')::CHAR(32) WHERE id = ${me.id} RETURNING "apiKey"` + return apiKey + }, + deleteApiKey: async (parent, { id }, { models, me }) => { + if (!me) { + throw new GraphQLError('you must be logged in', { extensions: { code: 'UNAUTHENTICATED' } }) + } + + return await models.user.update({ where: { id: me.id }, data: { apiKey: null } }) + }, unlinkAuth: async (parent, { authType }, { models, me }) => { if (!me) { throw new GraphQLError('you must be logged in', { extensions: { code: 'UNAUTHENTICATED' } }) diff --git a/api/typeDefs/user.js b/api/typeDefs/user.js index aa054913..e7145a16 100644 --- a/api/typeDefs/user.js +++ b/api/typeDefs/user.js @@ -36,6 +36,8 @@ export default gql` subscribeUserPosts(id: ID): User subscribeUserComments(id: ID): User toggleMute(id: ID): User + generateApiKey(id: ID!): String + deleteApiKey(id: ID!): User } type User { @@ -100,6 +102,7 @@ export default gql` github: Boolean! twitter: Boolean! email: String + apiKey: String } type UserPrivates { @@ -118,6 +121,7 @@ export default gql` tipPopover: Boolean! upvotePopover: Boolean! hasInvites: Boolean! + apiKeyEnabled: Boolean! """ mirrors SettingsInput diff --git a/components/discussion-form.js b/components/discussion-form.js index 0860b60e..97aa7a77 100644 --- a/components/discussion-form.js +++ b/components/discussion-form.js @@ -27,6 +27,7 @@ export function DiscussionForm ({ const schema = discussionSchema({ client, me, existingBoost: item?.boost }) // if Web Share Target API was used const shareTitle = router.query.title + const shareText = router.query.text ? decodeURI(router.query.text) : undefined const crossposter = useCrossposter() const toaster = useToast() @@ -88,7 +89,7 @@ export function DiscussionForm ({
) } + +export function ApiKey ({ enabled, apiKey }) { + const me = useMe() + const [generateApiKey] = useMutation( + gql` + mutation generateApiKey($id: ID!) { + generateApiKey(id: $id) + }`, + { + update (cache, { data: { generateApiKey } }) { + cache.modify({ + id: 'ROOT_QUERY', + fields: { + settings (existing) { + return { + ...existing, + privates: { + ...existing.privates, + apiKey: generateApiKey, + authMethods: { ...existing.privates.authMethods, apiKey: generateApiKey } + } + } + } + } + }) + } + } + ) + const [deleteApiKey] = useMutation( + gql` + mutation deleteApiKey($id: ID!) { + deleteApiKey(id: $id) { + id + } + }`, + { + update (cache, { data: { deleteApiKey } }) { + cache.modify({ + id: 'ROOT_QUERY', + fields: { + settings (existing) { + return { + ...existing, + privates: { + ...existing.privates, + authMethods: { ...existing.privates.authMethods, apiKey: null } + } + } + } + } + }) + } + } + ) + + const subject = '[API Key Request]