mirror of
https://github.com/graphql-hive/console
synced 2026-05-22 00:28:46 +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 {
|
||||
getUserBySuperTokenId(_: { superTokensUserId: string }): Promise<User | null>;
|
||||
setSuperTokensUserId(_: { auth0UserId: string; superTokensUserId: string; externalUserId: string }): Promise<void>;
|
||||
getUserWithoutAssociatedSuperTokenIdByAuth0Email(_: { email: string }): Promise<User | null>;
|
||||
getUserById(_: { id: string }): Promise<User | null>;
|
||||
|
||||
createUser(_: {
|
||||
|
|
|
|||
|
|
@ -169,6 +169,7 @@ export interface User {
|
|||
provider: AuthProvider;
|
||||
superTokensUserId: string | null;
|
||||
isAdmin: boolean;
|
||||
externalAuthUserId: string | null;
|
||||
}
|
||||
|
||||
export interface Member {
|
||||
|
|
|
|||
|
|
@ -25,6 +25,10 @@ const LegacySetUserIdMappingPayloadModel = zod.object({
|
|||
superTokensUserId: zod.string(),
|
||||
});
|
||||
|
||||
const LegacyCheckAuth0EmailUserExistsPayloadModel = zod.object({
|
||||
email: zod.string(),
|
||||
});
|
||||
|
||||
export async function main() {
|
||||
Sentry.init({
|
||||
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
|
||||
},
|
||||
});
|
||||
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') {
|
||||
|
|
|
|||
|
|
@ -84,6 +84,7 @@ export async function createStorage(connection: string): Promise<Storage> {
|
|||
fullName: user.full_name,
|
||||
displayName: user.display_name,
|
||||
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;
|
||||
},
|
||||
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 }) {
|
||||
await pool.query(sql`
|
||||
UPDATE
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import type { TypeInput as ThirdPartEmailPasswordTypeInput } from 'supertokens-n
|
|||
import { fetch } from 'cross-undici-fetch';
|
||||
import { appInfo } from './app-info';
|
||||
import zod from 'zod';
|
||||
import * as crypto from 'crypto';
|
||||
import { createTRPCClient } from '@trpc/client';
|
||||
import type { EmailsApi } from '@hive/emails';
|
||||
|
||||
|
|
@ -180,6 +181,43 @@ export const backendConfig = (): TypeInput => {
|
|||
|
||||
const getAuth0Overrides = (config: LegacyAuth0ConfigEnabled) => {
|
||||
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) {
|
||||
return {
|
||||
...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.
|
||||
*/
|
||||
|
|
@ -381,3 +458,15 @@ const generateAuth0AccessToken = async (config: LegacyAuth0ConfigEnabled): Promi
|
|||
|
||||
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