diff --git a/api/resolvers/user.js b/api/resolvers/user.js
index efbfe642..0e5e04ad 100644
--- a/api/resolvers/user.js
+++ b/api/resolvers/user.js
@@ -47,7 +47,7 @@ async function authMethods (user, args, { models, me }) {
twitter: oauth.indexOf('twitter') >= 0,
github: oauth.indexOf('github') >= 0,
nostr: !!user.nostrAuthPubkey,
- apiKey: user.apiKeyEnabled ? user.apiKey : null
+ apiKey: user.apiKeyEnabled ? !!user.apiKeyHash : null
}
}
@@ -541,7 +541,14 @@ export default {
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"`
+ // I trust postgres CSPRNG more than the one from JS
+ const [{ apiKey, apiKeyHash }] = await models.$queryRaw`
+ SELECT "apiKey", encode(digest("apiKey", 'sha256'), 'hex') AS "apiKeyHash"
+ FROM (
+ SELECT encode(gen_random_bytes(32), 'base64')::CHAR(32) as "apiKey"
+ ) rng`
+ await models.user.update({ where: { id: me.id }, data: { apiKeyHash } })
+
return apiKey
},
deleteApiKey: async (parent, { id }, { models, me }) => {
@@ -549,7 +556,7 @@ export default {
throw new GraphQLError('you must be logged in', { extensions: { code: 'UNAUTHENTICATED' } })
}
- return await models.user.update({ where: { id: me.id }, data: { apiKey: null } })
+ return await models.user.update({ where: { id: me.id }, data: { apiKeyHash: null } })
},
unlinkAuth: async (parent, { authType }, { models, me }) => {
if (!me) {
diff --git a/api/typeDefs/user.js b/api/typeDefs/user.js
index 2300fdc3..cd2e31bd 100644
--- a/api/typeDefs/user.js
+++ b/api/typeDefs/user.js
@@ -104,7 +104,7 @@ export default gql`
github: Boolean!
twitter: Boolean!
email: String
- apiKey: String
+ apiKey: Boolean!
}
type UserPrivates {
diff --git a/components/modal.js b/components/modal.js
index 7ae025c5..19aee78e 100644
--- a/components/modal.js
+++ b/components/modal.js
@@ -61,7 +61,7 @@ export default function useModal () {
dialogClassName={className}
contentClassName={className}
>
-
+
{modalOptions?.overflow &&
diff --git a/pages/api/graphql.js b/pages/api/graphql.js
index 14cfba3b..e19723ea 100644
--- a/pages/api/graphql.js
+++ b/pages/api/graphql.js
@@ -56,8 +56,11 @@ export default startServerAndCreateNextHandler(apolloServer, {
const apiKey = req.headers['x-api-key']
let session
if (apiKey) {
- const sessionFieldSelect = { name: true, id: true, email: true }
- const user = await models.user.findUnique({ where: { apiKey }, select: { ...sessionFieldSelect, apiKeyEnabled: true } })
+ const [user] = await models.$queryRaw`
+ SELECT id, name, email, "apiKeyEnabled"
+ FROM users
+ WHERE "apiKeyHash" = encode(digest(${apiKey}, 'sha256'), 'hex')
+ LIMIT 1`
if (user?.apiKeyEnabled) {
const { apiKeyEnabled, ...sessionFields } = user
session = { user: { ...sessionFields, apiKey: true } }
diff --git a/pages/settings/index.js b/pages/settings/index.js
index 117675c0..4bc596f6 100644
--- a/pages/settings/index.js
+++ b/pages/settings/index.js
@@ -767,7 +767,8 @@ export function EmailLinkForm ({ callbackUrl }) {
)
}
-export function ApiKey ({ enabled, apiKey }) {
+function ApiKey ({ enabled, apiKey }) {
+ const showModal = useShowModal()
const me = useMe()
const [generateApiKey] = useMutation(
gql`
@@ -785,33 +786,7 @@ export function ApiKey ({ enabled, apiKey }) {
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 }
+ authMethods: { ...existing.privates.authMethods, apiKey: true }
}
}
}
@@ -820,6 +795,7 @@ export function ApiKey ({ enabled, apiKey }) {
}
}
)
+ const toaster = useToast()
const subject = '[API Key Request] '
const body =
@@ -844,44 +820,42 @@ I estimate that I will call the GraphQL API this many times (rough estimate is f
// link to DM with ek on SimpleX
const simplexLink = 'https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2F6iIcWT_dF2zN_w5xzZEY7HI2Prbh3ldP07YTyDexPjE%3D%40smp10.simplex.im%2FxNnPk9DkTbQJ6NckWom9mi5vheo_VPLm%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAnFUiU0M8jS1JY34LxUoPr7mdJlFZwf3pFkjRrhprdQs%253D%26srv%3Drb2pbttocvnbrngnwziclp2f4ckjq65kebafws6g4hy22cdaiv5dwjqd.onion'
- const disabled = !enabled
+ const disabled = !enabled || apiKey
return (
<>
api key
- {apiKey &&
- <>
-
- >}
request access to API keys in ~meta : <>>}
+ overlay={disabled ? {apiKey ? 'you can have only one API key at a time' : 'request access to API keys in ~meta'} : <>>}
trigger={['hover', 'focus']}
>
- {apiKey
- ?
{
- await deleteApiKey({ variables: { id: me.id } })
- }}
- />
- : (
- {
- await generateApiKey({ variables: { id: me.id } })
- }}
- >Generate API key
-
- )}
+ {
+ try {
+ const { data } = await generateApiKey({ variables: { id: me.id } })
+ const { generateApiKey: apiKey } = data
+ showModal(() => , { keepOpen: true })
+ } catch (err) {
+ console.error(err)
+ toaster.danger('error generating api key')
+ }
+ }}
+ >Generate API key
+
+ {apiKey &&
+
{
+ showModal((onClose) => )
+ }}
+ />}