mirror of
https://github.com/graphql-hive/console
synced 2026-04-21 14:37:17 +00:00
368 lines
10 KiB
TypeScript
368 lines
10 KiB
TypeScript
import type { AddressInfo } from 'node:net';
|
|
import humanId from 'human-id';
|
|
import setCookie from 'set-cookie-parser';
|
|
import { sql, type DatabasePool } from 'slonik';
|
|
import z from 'zod';
|
|
import formDataPlugin from '@fastify/formbody';
|
|
import { createServer, type FastifyReply, type FastifyRequest } from '@hive/service-common';
|
|
import { graphql } from './gql';
|
|
import { execute } from './graphql';
|
|
import { getServiceHost, pollForEmailVerificationLink } from './utils';
|
|
|
|
const apiAddress = await getServiceHost('server', 8082);
|
|
|
|
async function createMockOIDCServer() {
|
|
const host =
|
|
process.env.RUN_AGAINST_LOCAL_SERVICES === '1' ? 'localhost' : 'host.docker.internal';
|
|
const server = await createServer({
|
|
sentryErrorHandler: false,
|
|
log: {
|
|
requests: false,
|
|
level: 'silent',
|
|
},
|
|
name: '',
|
|
});
|
|
await server.register(formDataPlugin);
|
|
|
|
let registeredHandler: typeof handler;
|
|
|
|
async function handler(request: FastifyRequest, reply: FastifyReply): Promise<void> {
|
|
if (!handler) {
|
|
throw new Error('No handler registered');
|
|
}
|
|
return await registeredHandler(request, reply);
|
|
}
|
|
|
|
server.route({
|
|
method: 'POST',
|
|
url: '/token',
|
|
handler,
|
|
});
|
|
|
|
server.route({
|
|
method: 'GET',
|
|
url: '/userinfo',
|
|
handler,
|
|
});
|
|
|
|
await server.listen({
|
|
port: 0,
|
|
host: '0.0.0.0',
|
|
});
|
|
|
|
return {
|
|
url: 'http://' + host + ':' + (server.server.address() as AddressInfo).port,
|
|
setHandler(newHandler: typeof handler) {
|
|
registeredHandler = newHandler;
|
|
},
|
|
[Symbol.asyncDispose]: () => {
|
|
server.close();
|
|
},
|
|
};
|
|
}
|
|
|
|
const CreateOIDCIntegrationMutation = graphql(`
|
|
mutation TestKit_OIDCIntegration_CreateOIDCIntegrationMutation(
|
|
$input: CreateOIDCIntegrationInput!
|
|
) {
|
|
createOIDCIntegration(input: $input) {
|
|
ok {
|
|
createdOIDCIntegration {
|
|
id
|
|
clientId
|
|
clientSecretPreview
|
|
tokenEndpoint
|
|
userinfoEndpoint
|
|
authorizationEndpoint
|
|
additionalScopes
|
|
oidcUserJoinOnly
|
|
oidcUserAccessOnly
|
|
}
|
|
}
|
|
error {
|
|
message
|
|
details {
|
|
clientId
|
|
clientSecret
|
|
tokenEndpoint
|
|
userinfoEndpoint
|
|
authorizationEndpoint
|
|
additionalScopes
|
|
}
|
|
}
|
|
}
|
|
}
|
|
`);
|
|
|
|
const UpdateOIDCIntegrationMutation = graphql(`
|
|
mutation TestKit_OIDCIntegration_UpdateOIDCIntegrationMutation(
|
|
$input: UpdateOIDCIntegrationInput!
|
|
) {
|
|
updateOIDCIntegration(input: $input) {
|
|
ok {
|
|
updatedOIDCIntegration {
|
|
id
|
|
tokenEndpoint
|
|
userinfoEndpoint
|
|
authorizationEndpoint
|
|
clientId
|
|
clientSecretPreview
|
|
additionalScopes
|
|
}
|
|
}
|
|
error {
|
|
message
|
|
details {
|
|
clientId
|
|
clientSecret
|
|
tokenEndpoint
|
|
userinfoEndpoint
|
|
authorizationEndpoint
|
|
additionalScopes
|
|
}
|
|
}
|
|
}
|
|
}
|
|
`);
|
|
|
|
const SendVerificationEmailMutation = graphql(`
|
|
mutation TestKit_OIDCIntegration_SendVerificationEmailMutation(
|
|
$input: SendVerificationEmailInput!
|
|
) {
|
|
sendVerificationEmail(input: $input) {
|
|
ok {
|
|
expiresAt
|
|
}
|
|
error {
|
|
message
|
|
emailAlreadyVerified
|
|
}
|
|
}
|
|
}
|
|
`);
|
|
|
|
const VerifyEmailMutation = graphql(`
|
|
mutation TestKit_OIDCIntegration_VerifyEmailMutation($input: VerifyEmailInput!) {
|
|
verifyEmail(input: $input) {
|
|
ok {
|
|
verified
|
|
}
|
|
error {
|
|
message
|
|
}
|
|
}
|
|
}
|
|
`);
|
|
|
|
export async function createOIDCIntegration(args: {
|
|
organizationId: string;
|
|
accessToken: string;
|
|
getPool: () => Promise<DatabasePool>;
|
|
}) {
|
|
const { accessToken: authToken, getPool } = args;
|
|
const result = await execute({
|
|
document: CreateOIDCIntegrationMutation,
|
|
variables: {
|
|
input: {
|
|
organizationId: args.organizationId,
|
|
additionalScopes: [],
|
|
authorizationEndpoint: 'http://localhost:6666/noop/authoriation',
|
|
tokenEndpoint: 'http://localhost:6666/noop/token',
|
|
userinfoEndpoint: 'http://localhost:666/noop/userinfo',
|
|
clientId: 'noop',
|
|
clientSecret: 'noop',
|
|
},
|
|
},
|
|
authToken,
|
|
}).then(r => r.expectNoGraphQLErrors());
|
|
|
|
if (!result.createOIDCIntegration.ok) {
|
|
throw new Error(result.createOIDCIntegration.error?.message ?? 'Unexpected error.');
|
|
}
|
|
|
|
const oidcIntegration = result.createOIDCIntegration.ok.createdOIDCIntegration;
|
|
|
|
return {
|
|
oidcIntegration,
|
|
async registerFakeDomain() {
|
|
const randomDomain =
|
|
humanId({
|
|
separator: '',
|
|
capitalize: false,
|
|
}) + '.local';
|
|
|
|
const pool = await getPool();
|
|
const query = sql`
|
|
INSERT INTO "oidc_integration_domains" (
|
|
"organization_id"
|
|
, "oidc_integration_id"
|
|
, "domain_name"
|
|
, "verified_at"
|
|
) VALUES (
|
|
${args.organizationId}
|
|
, ${oidcIntegration.id}
|
|
, ${randomDomain}
|
|
, NOW()
|
|
)
|
|
`;
|
|
|
|
await pool.query(query);
|
|
return randomDomain;
|
|
},
|
|
async createMockServerAndUpdateIntegrationEndpoints(args?: {
|
|
additionalScopes?: Array<string>;
|
|
clientId?: string;
|
|
clientSecret?: string;
|
|
}) {
|
|
const server = await createMockOIDCServer();
|
|
|
|
const result = await execute({
|
|
document: UpdateOIDCIntegrationMutation,
|
|
variables: {
|
|
input: {
|
|
oidcIntegrationId: oidcIntegration.id,
|
|
authorizationEndpoint: server.url + '/authorize',
|
|
tokenEndpoint: server.url + '/token',
|
|
userinfoEndpoint: server.url + '/userinfo',
|
|
additionalScopes: args?.additionalScopes,
|
|
clientId: args?.clientId,
|
|
clientSecret: args?.clientSecret,
|
|
},
|
|
},
|
|
authToken,
|
|
}).then(r => r.expectNoGraphQLErrors());
|
|
|
|
if (!result.updateOIDCIntegration.ok) {
|
|
throw new Error(result.updateOIDCIntegration.error?.message ?? 'Unexpected error.');
|
|
}
|
|
|
|
return {
|
|
setHandler: server.setHandler,
|
|
setUser(args: { email: string; sub: string }) {
|
|
server.setHandler(async (req, res) => {
|
|
if (req.routeOptions.url === '/token') {
|
|
return res.status(200).send({
|
|
access_token: 'yolo',
|
|
});
|
|
}
|
|
|
|
if (req.routeOptions.url === '/userinfo') {
|
|
return res.status(200).send({
|
|
sub: args.sub,
|
|
email: args.email,
|
|
});
|
|
}
|
|
|
|
console.log('unhandled', req.routeOptions.url);
|
|
return res.status(404).send();
|
|
});
|
|
},
|
|
async runGetAuthorizationUrl() {
|
|
const baseUrl = 'http://' + apiAddress;
|
|
const url = new URL('http://' + apiAddress + '/auth-api/authorisationurl');
|
|
url.searchParams.set('thirdPartyId', 'oidc');
|
|
url.searchParams.set('redirectURIOnProviderDashboard', baseUrl + '/');
|
|
url.searchParams.set('oidc_id', oidcIntegration.id);
|
|
const result = await fetch(url).then(res => res.json());
|
|
|
|
const urlWithQueryParams = new URL(result.urlWithQueryParams);
|
|
return {
|
|
codeChallenge: urlWithQueryParams.searchParams.get('code_challenge') ?? '',
|
|
state: urlWithQueryParams.searchParams.get('state') ?? '',
|
|
};
|
|
},
|
|
async runSignInUp(args: { state: string; code?: string }) {
|
|
const url = new URL('http://' + apiAddress + '/auth-api/signinup');
|
|
url.searchParams.set('oidc_id', oidcIntegration.id);
|
|
|
|
const result = await fetch(url, {
|
|
method: 'POST',
|
|
body: JSON.stringify({
|
|
thirdPartyId: 'oidc',
|
|
redirectURIInfo: {
|
|
redirectURIOnProviderDashboard: '/',
|
|
redirectURIQueryParams: {
|
|
state: args.state,
|
|
code: args.code ?? 'noop',
|
|
},
|
|
},
|
|
}),
|
|
headers: {
|
|
'content-type': 'application/json',
|
|
'st-auth-mode': 'cookie',
|
|
},
|
|
});
|
|
|
|
if (result.status !== 200) {
|
|
throw new Error('Failed ' + result.status + (await result.text()));
|
|
}
|
|
|
|
const rawBody = await result.json();
|
|
|
|
const body = z
|
|
.object({
|
|
user: z.object({
|
|
id: z.string(),
|
|
emails: z.array(z.string()),
|
|
loginMethods: z.array(
|
|
z.object({
|
|
recipeUserId: z.string(),
|
|
}),
|
|
),
|
|
}),
|
|
})
|
|
.parse(rawBody);
|
|
const cookies = setCookie.parse(result.headers.getSetCookie());
|
|
return {
|
|
accessToken: cookies.find(c => c.name === 'sAccessToken')?.value ?? '',
|
|
user: {
|
|
id: body.user.id,
|
|
email: body.user.emails[0],
|
|
userIdentityId: body.user.loginMethods[0]?.recipeUserId,
|
|
},
|
|
};
|
|
},
|
|
async confirmEmail(args: { userIdentityId: string; email: string }) {
|
|
const now = Date.now();
|
|
const sendMail = await execute({
|
|
document: SendVerificationEmailMutation,
|
|
variables: {
|
|
input: {
|
|
userIdentityId: args.userIdentityId,
|
|
resend: true,
|
|
},
|
|
},
|
|
authToken,
|
|
}).then(e => e.expectNoGraphQLErrors());
|
|
|
|
if (!sendMail.sendVerificationEmail.ok) {
|
|
throw new Error(sendMail.sendVerificationEmail.error?.message ?? 'Unknown error.');
|
|
}
|
|
|
|
const url = await pollForEmailVerificationLink({
|
|
email: args.email,
|
|
now,
|
|
});
|
|
|
|
const token = url.searchParams.get('token') ?? '';
|
|
|
|
const confirmMail = await execute({
|
|
document: VerifyEmailMutation,
|
|
variables: {
|
|
input: {
|
|
userIdentityId: args.userIdentityId,
|
|
email: args.email,
|
|
token,
|
|
},
|
|
},
|
|
authToken,
|
|
}).then(e => e.expectNoGraphQLErrors());
|
|
|
|
if (!confirmMail.verifyEmail.ok) {
|
|
throw new Error(confirmMail.verifyEmail.error?.message ?? 'Unknown error.');
|
|
}
|
|
},
|
|
};
|
|
},
|
|
};
|
|
}
|