mirror of
https://github.com/graphql-hive/console
synced 2026-05-24 09:38:26 +00:00
feat: import auth0 accounts into supertokens when doing a password reset (#349)
* feat: import auth0 accounts into supertokens when doing a password reset * refactor: more detailed error messages
This commit is contained in:
parent
45946f0c5e
commit
7b9c2ec90a
5 changed files with 141 additions and 0 deletions
|
|
@ -46,6 +46,7 @@ export interface PersistedOperationSelector extends ProjectSelector {
|
||||||
export interface Storage {
|
export interface Storage {
|
||||||
getUserBySuperTokenId(_: { superTokensUserId: string }): Promise<User | null>;
|
getUserBySuperTokenId(_: { superTokensUserId: string }): Promise<User | null>;
|
||||||
setSuperTokensUserId(_: { auth0UserId: string; superTokensUserId: string; externalUserId: string }): Promise<void>;
|
setSuperTokensUserId(_: { auth0UserId: string; superTokensUserId: string; externalUserId: string }): Promise<void>;
|
||||||
|
getUserWithoutAssociatedSuperTokenIdByAuth0Email(_: { email: string }): Promise<User | null>;
|
||||||
getUserById(_: { id: string }): Promise<User | null>;
|
getUserById(_: { id: string }): Promise<User | null>;
|
||||||
|
|
||||||
createUser(_: {
|
createUser(_: {
|
||||||
|
|
|
||||||
|
|
@ -169,6 +169,7 @@ export interface User {
|
||||||
provider: AuthProvider;
|
provider: AuthProvider;
|
||||||
superTokensUserId: string | null;
|
superTokensUserId: string | null;
|
||||||
isAdmin: boolean;
|
isAdmin: boolean;
|
||||||
|
externalAuthUserId: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Member {
|
export interface Member {
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,10 @@ const LegacySetUserIdMappingPayloadModel = zod.object({
|
||||||
superTokensUserId: zod.string(),
|
superTokensUserId: zod.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const LegacyCheckAuth0EmailUserExistsPayloadModel = zod.object({
|
||||||
|
email: zod.string(),
|
||||||
|
});
|
||||||
|
|
||||||
export async function main() {
|
export async function main() {
|
||||||
Sentry.init({
|
Sentry.init({
|
||||||
serverName: 'api',
|
serverName: 'api',
|
||||||
|
|
@ -302,6 +306,32 @@ export async function main() {
|
||||||
reply.status(200).send(); // eslint-disable-line @typescript-eslint/no-floating-promises -- false positive, FastifyReply.then returns void
|
reply.status(200).send(); // eslint-disable-line @typescript-eslint/no-floating-promises -- false positive, FastifyReply.then returns void
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
server.route({
|
||||||
|
method: 'POST',
|
||||||
|
url: '/__legacy/check_auth0_email_user_without_associated_supertoken_id_exists',
|
||||||
|
async handler(req, reply) {
|
||||||
|
if (req.headers['x-authorization'] !== authLegacyAPIKey) {
|
||||||
|
reply.status(401).send({ error: 'Invalid update user id mapping key.', code: 'ERR_INVALID_KEY' }); // eslint-disable-line @typescript-eslint/no-floating-promises -- false positive, FastifyReply.then returns void
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { email } = LegacyCheckAuth0EmailUserExistsPayloadModel.parse(req.body);
|
||||||
|
|
||||||
|
const user = await storage.getUserWithoutAssociatedSuperTokenIdByAuth0Email({
|
||||||
|
email,
|
||||||
|
});
|
||||||
|
|
||||||
|
await reply.status(200).send({
|
||||||
|
user: user
|
||||||
|
? {
|
||||||
|
id: user.id,
|
||||||
|
email: user?.email,
|
||||||
|
auth0UserId: user.externalAuthUserId,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (process.env.METRICS_ENABLED === 'true') {
|
if (process.env.METRICS_ENABLED === 'true') {
|
||||||
|
|
|
||||||
|
|
@ -84,6 +84,7 @@ export async function createStorage(connection: string): Promise<Storage> {
|
||||||
fullName: user.full_name,
|
fullName: user.full_name,
|
||||||
displayName: user.display_name,
|
displayName: user.display_name,
|
||||||
isAdmin: user.is_admin ?? false,
|
isAdmin: user.is_admin ?? false,
|
||||||
|
externalAuthUserId: user.external_auth_user_id ?? null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -294,6 +295,25 @@ export async function createStorage(connection: string): Promise<Storage> {
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
|
async getUserWithoutAssociatedSuperTokenIdByAuth0Email({ email }) {
|
||||||
|
const user = await pool.maybeOne<Slonik<users>>(sql`
|
||||||
|
SELECT
|
||||||
|
*
|
||||||
|
FROM
|
||||||
|
public."users"
|
||||||
|
WHERE
|
||||||
|
"email" = ${email}
|
||||||
|
AND "supertoken_user_id" IS NULL
|
||||||
|
AND "external_auth_user_id" LIKE 'auth0|%'
|
||||||
|
LIMIT 1
|
||||||
|
`);
|
||||||
|
|
||||||
|
if (user) {
|
||||||
|
return transformUser(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
},
|
||||||
async setSuperTokensUserId({ auth0UserId, superTokensUserId, externalUserId }) {
|
async setSuperTokensUserId({ auth0UserId, superTokensUserId, externalUserId }) {
|
||||||
await pool.query(sql`
|
await pool.query(sql`
|
||||||
UPDATE
|
UPDATE
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import type { TypeInput as ThirdPartEmailPasswordTypeInput } from 'supertokens-n
|
||||||
import { fetch } from 'cross-undici-fetch';
|
import { fetch } from 'cross-undici-fetch';
|
||||||
import { appInfo } from './app-info';
|
import { appInfo } from './app-info';
|
||||||
import zod from 'zod';
|
import zod from 'zod';
|
||||||
|
import * as crypto from 'crypto';
|
||||||
import { createTRPCClient } from '@trpc/client';
|
import { createTRPCClient } from '@trpc/client';
|
||||||
import type { EmailsApi } from '@hive/emails';
|
import type { EmailsApi } from '@hive/emails';
|
||||||
|
|
||||||
|
|
@ -180,6 +181,43 @@ export const backendConfig = (): TypeInput => {
|
||||||
|
|
||||||
const getAuth0Overrides = (config: LegacyAuth0ConfigEnabled) => {
|
const getAuth0Overrides = (config: LegacyAuth0ConfigEnabled) => {
|
||||||
const override: ThirdPartEmailPasswordTypeInput['override'] = {
|
const override: ThirdPartEmailPasswordTypeInput['override'] = {
|
||||||
|
apis(originalImplementation) {
|
||||||
|
return {
|
||||||
|
...originalImplementation,
|
||||||
|
async generatePasswordResetTokenPOST(input) {
|
||||||
|
const email = input.formFields.find(formField => formField.id === 'email')?.value;
|
||||||
|
|
||||||
|
if (email) {
|
||||||
|
// We first use the existing implementation for looking for users within supertokens.
|
||||||
|
const users = await ThirdPartyEmailPasswordNode.getUsersByEmail(email);
|
||||||
|
|
||||||
|
// If there is no email/password SuperTokens user yet, we need to check if there is an Auth0 user for this email.
|
||||||
|
if (users.some(user => user.thirdParty == null) === false) {
|
||||||
|
// RPC call to check if email/password user exists in Auth0
|
||||||
|
const dbUser = await checkWhetherAuth0EmailUserWithoutAssociatedSuperTokensIdExists(config, { email });
|
||||||
|
|
||||||
|
if (dbUser) {
|
||||||
|
// If we have this user within our database we create our new supertokens user
|
||||||
|
const newUserResult = await ThirdPartyEmailPasswordNode.emailPasswordSignUp(
|
||||||
|
dbUser.email,
|
||||||
|
await generateRandomPassword()
|
||||||
|
);
|
||||||
|
|
||||||
|
if (newUserResult.status === 'OK') {
|
||||||
|
// link the db record to the new supertokens user
|
||||||
|
await setUserIdMapping(config, {
|
||||||
|
auth0UserId: dbUser.auth0UserId,
|
||||||
|
supertokensUserId: newUserResult.user.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return await originalImplementation.generatePasswordResetTokenPOST!(input);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
functions(originalImplementation) {
|
functions(originalImplementation) {
|
||||||
return {
|
return {
|
||||||
...originalImplementation,
|
...originalImplementation,
|
||||||
|
|
@ -358,6 +396,45 @@ async function setUserIdMapping(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const CheckAuth0EmailUserExistsResponseModel = zod.object({
|
||||||
|
user: zod.nullable(zod.object({ id: zod.string(), email: zod.string(), auth0UserId: zod.string() })),
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether a specific user that SIGNED UP VIA EMAIL and password THAT DOES NOT YET EXIST IN SUPER TOKENS exists in the database as an Auth0 user.
|
||||||
|
*/
|
||||||
|
async function checkWhetherAuth0EmailUserWithoutAssociatedSuperTokensIdExists(
|
||||||
|
config: LegacyAuth0ConfigEnabled,
|
||||||
|
params: { email: string }
|
||||||
|
): Promise<zod.TypeOf<typeof CheckAuth0EmailUserExistsResponseModel>['user']> {
|
||||||
|
const response = await fetch(
|
||||||
|
config['AUTH_LEGACY_AUTH0_INTERNAL_API_ENDPOINT'] +
|
||||||
|
'/check_auth0_email_user_without_associated_supertoken_id_exists',
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'content-type': 'application/json',
|
||||||
|
'x-authorization': config['AUTH_LEGACY_AUTH0_INTERNAL_API_KEY'],
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
email: params.email,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const body = await response.text();
|
||||||
|
|
||||||
|
if (response.status !== 200) {
|
||||||
|
throw new Error(
|
||||||
|
`Failed to check whether the Auth0 email user without an associated supertokenId exists. Status: ${response.status}. Body: ${body}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { user } = CheckAuth0EmailUserExistsResponseModel.parse(JSON.parse(body));
|
||||||
|
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate a Auth0 access token that is required for making API calls to Auth0.
|
* Generate a Auth0 access token that is required for making API calls to Auth0.
|
||||||
*/
|
*/
|
||||||
|
|
@ -381,3 +458,15 @@ const generateAuth0AccessToken = async (config: LegacyAuth0ConfigEnabled): Promi
|
||||||
|
|
||||||
return JSON.parse(body).access_token;
|
return JSON.parse(body).access_token;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
async function generateRandomPassword(): Promise<string> {
|
||||||
|
return await new Promise<string>((resolve, reject) =>
|
||||||
|
crypto.randomBytes(20, (err, buf) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resolve(buf.toString('hex'));
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue