mirror of
https://github.com/graphql-hive/console
synced 2026-04-21 14:37:17 +00:00
Move SuperTokens-node to GraphQL server (#4288)
This commit is contained in:
parent
0db0580949
commit
f44fdd474a
100 changed files with 1244 additions and 1323 deletions
|
|
@ -14,7 +14,7 @@ describe('basic user flow', () => {
|
|||
|
||||
it('should redirect anon to auth', () => {
|
||||
cy.visit('/');
|
||||
cy.url().should('include', '/auth?redirectToPath=%2F');
|
||||
cy.url().should('include', '/auth?redirectToPath=');
|
||||
});
|
||||
|
||||
it('should sign up', () => {
|
||||
|
|
@ -34,7 +34,7 @@ describe('basic user flow', () => {
|
|||
// Logout
|
||||
cy.get('[data-cy="user-menu-trigger"]').click();
|
||||
cy.get('[data-cy="user-menu-logout"]').click();
|
||||
cy.url().should('include', '/auth?redirectToPath=%2F');
|
||||
cy.url().should('include', '/auth?redirectToPath=');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -223,8 +223,6 @@ const app = deployApp({
|
|||
dbMigrations,
|
||||
image: docker.factory.getImageId('app', imagesTag),
|
||||
docker,
|
||||
supertokens,
|
||||
emails,
|
||||
zendesk,
|
||||
billing,
|
||||
github: githubApp,
|
||||
|
|
|
|||
|
|
@ -1,33 +1,23 @@
|
|||
import * as pulumi from '@pulumi/pulumi';
|
||||
import { serviceLocalEndpoint } from '../utils/local-endpoint';
|
||||
import { ServiceSecret } from '../utils/secrets';
|
||||
import { ServiceDeployment } from '../utils/service-deployment';
|
||||
import { StripeBilling } from './billing';
|
||||
import { DbMigrations } from './db-migrations';
|
||||
import { Docker } from './docker';
|
||||
import { Emails } from './emails';
|
||||
import { Environment } from './environment';
|
||||
import { GitHubApp } from './github';
|
||||
import { GraphQL } from './graphql';
|
||||
import { Sentry } from './sentry';
|
||||
import { SlackApp } from './slack-app';
|
||||
import { Supertokens } from './supertokens';
|
||||
import { Zendesk } from './zendesk';
|
||||
|
||||
export type App = ReturnType<typeof deployApp>;
|
||||
|
||||
class AppOAuthSecret extends ServiceSecret<{
|
||||
clientId: string | pulumi.Output<string>;
|
||||
clientSecret: string | pulumi.Output<string>;
|
||||
}> {}
|
||||
|
||||
export function deployApp({
|
||||
graphql,
|
||||
dbMigrations,
|
||||
image,
|
||||
supertokens,
|
||||
docker,
|
||||
emails,
|
||||
zendesk,
|
||||
github,
|
||||
slackApp,
|
||||
|
|
@ -40,8 +30,6 @@ export function deployApp({
|
|||
graphql: GraphQL;
|
||||
dbMigrations: DbMigrations;
|
||||
docker: Docker;
|
||||
supertokens: Supertokens;
|
||||
emails: Emails;
|
||||
zendesk: Zendesk;
|
||||
github: GitHubApp;
|
||||
slackApp: SlackApp;
|
||||
|
|
@ -51,16 +39,6 @@ export function deployApp({
|
|||
const appConfig = new pulumi.Config('app');
|
||||
const appEnv = appConfig.requireObject<Record<string, string>>('env');
|
||||
|
||||
const oauthConfig = new pulumi.Config('oauth');
|
||||
const githubOAuthSecret = new AppOAuthSecret('oauth-github', {
|
||||
clientId: oauthConfig.requireSecret('githubClient'),
|
||||
clientSecret: oauthConfig.requireSecret('githubSecret'),
|
||||
});
|
||||
const googleOAuthSecret = new AppOAuthSecret('oauth-google', {
|
||||
clientId: oauthConfig.requireSecret('googleClient'),
|
||||
clientSecret: oauthConfig.requireSecret('googleSecret'),
|
||||
});
|
||||
|
||||
return new ServiceDeployment(
|
||||
'app',
|
||||
{
|
||||
|
|
@ -80,7 +58,8 @@ export function deployApp({
|
|||
env: {
|
||||
...environment.envVars,
|
||||
SENTRY: sentry.enabled ? '1' : '0',
|
||||
GRAPHQL_ENDPOINT: serviceLocalEndpoint(graphql.service).apply(s => `${s}/graphql`),
|
||||
GRAPHQL_PUBLIC_ENDPOINT: `https://${environment.appDns}/graphql`,
|
||||
GRAPHQL_PUBLIC_ORIGIN: `https://${environment.appDns}`,
|
||||
SERVER_ENDPOINT: serviceLocalEndpoint(graphql.service),
|
||||
APP_BASE_URL: `https://${environment.appDns}/`,
|
||||
INTEGRATION_SLACK: '1',
|
||||
|
|
@ -89,8 +68,6 @@ export function deployApp({
|
|||
DOCS_URL: 'https://the-guild.dev/graphql/hive/docs',
|
||||
GRAPHQL_PERSISTED_OPERATIONS: '1',
|
||||
ZENDESK_SUPPORT: zendesk.enabled ? '1' : '0',
|
||||
SUPERTOKENS_CONNECTION_URI: supertokens.localEndpoint,
|
||||
EMAILS_ENDPOINT: serviceLocalEndpoint(emails.service),
|
||||
AUTH_GITHUB: '1',
|
||||
AUTH_GOOGLE: '1',
|
||||
AUTH_REQUIRE_EMAIL_VERIFICATION: '1',
|
||||
|
|
@ -104,11 +81,6 @@ export function deployApp({
|
|||
.withSecret('INTEGRATION_SLACK_CLIENT_ID', slackApp.secret, 'clientId')
|
||||
.withSecret('INTEGRATION_SLACK_CLIENT_SECRET', slackApp.secret, 'clientSecret')
|
||||
.withSecret('STRIPE_PUBLIC_KEY', billing.secret, 'stripePublicKey')
|
||||
.withSecret('SUPERTOKENS_API_KEY', supertokens.secret, 'apiKey')
|
||||
.withSecret('AUTH_GITHUB_CLIENT_ID', githubOAuthSecret, 'clientId')
|
||||
.withSecret('AUTH_GITHUB_CLIENT_SECRET', githubOAuthSecret, 'clientSecret')
|
||||
.withSecret('AUTH_GOOGLE_CLIENT_ID', googleOAuthSecret, 'clientId')
|
||||
.withSecret('AUTH_GOOGLE_CLIENT_SECRET', googleOAuthSecret, 'clientSecret')
|
||||
.withConditionalSecret(sentry.enabled, 'SENTRY_DSN', sentry.secret, 'dsn')
|
||||
.deploy();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import * as pulumi from '@pulumi/pulumi';
|
||||
import { serviceLocalEndpoint } from '../utils/local-endpoint';
|
||||
import { ServiceSecret } from '../utils/secrets';
|
||||
import { ServiceDeployment } from '../utils/service-deployment';
|
||||
import { StripeBillingService } from './billing';
|
||||
import { CDN } from './cf-cdn';
|
||||
|
|
@ -25,6 +26,11 @@ import { Zendesk } from './zendesk';
|
|||
|
||||
export type GraphQL = ReturnType<typeof deployGraphQL>;
|
||||
|
||||
class AppOAuthSecret extends ServiceSecret<{
|
||||
clientId: string | pulumi.Output<string>;
|
||||
clientSecret: string | pulumi.Output<string>;
|
||||
}> {}
|
||||
|
||||
export function deployGraphQL({
|
||||
clickhouse,
|
||||
image,
|
||||
|
|
@ -75,6 +81,16 @@ export function deployGraphQL({
|
|||
const apiConfig = new pulumi.Config('api');
|
||||
const apiEnv = apiConfig.requireObject<Record<string, string>>('env');
|
||||
|
||||
const oauthConfig = new pulumi.Config('oauth');
|
||||
const githubOAuthSecret = new AppOAuthSecret('oauth-github', {
|
||||
clientId: oauthConfig.requireSecret('githubClient'),
|
||||
clientSecret: oauthConfig.requireSecret('githubSecret'),
|
||||
});
|
||||
const googleOAuthSecret = new AppOAuthSecret('oauth-google', {
|
||||
clientId: oauthConfig.requireSecret('googleClient'),
|
||||
clientSecret: oauthConfig.requireSecret('googleSecret'),
|
||||
});
|
||||
|
||||
return (
|
||||
new ServiceDeployment(
|
||||
'graphql-api',
|
||||
|
|
@ -108,6 +124,7 @@ export function deployGraphQL({
|
|||
EMAILS_ENDPOINT: serviceLocalEndpoint(emails.service),
|
||||
USAGE_ESTIMATOR_ENDPOINT: serviceLocalEndpoint(usageEstimator.service),
|
||||
WEB_APP_URL: `https://${environment.appDns}`,
|
||||
GRAPHQL_PUBLIC_ORIGIN: `https://${environment.appDns}`,
|
||||
CDN_CF: '1',
|
||||
HIVE: '1',
|
||||
HIVE_REPORTING: '1',
|
||||
|
|
@ -115,9 +132,13 @@ export function deployGraphQL({
|
|||
HIVE_REPORTING_ENDPOINT: 'http://0.0.0.0:4000/graphql',
|
||||
ZENDESK_SUPPORT: zendesk.enabled ? '1' : '0',
|
||||
INTEGRATION_GITHUB: '1',
|
||||
SUPERTOKENS_CONNECTION_URI: supertokens.localEndpoint,
|
||||
AUTH_ORGANIZATION_OIDC: '1',
|
||||
GRAPHQL_PERSISTED_OPERATIONS_PATH: './persisted-operations.json',
|
||||
// Auth
|
||||
SUPERTOKENS_CONNECTION_URI: supertokens.localEndpoint,
|
||||
AUTH_GITHUB: '1',
|
||||
AUTH_GOOGLE: '1',
|
||||
AUTH_ORGANIZATION_OIDC: '1',
|
||||
AUTH_REQUIRE_EMAIL_VERIFICATION: '1',
|
||||
},
|
||||
exposesMetrics: true,
|
||||
port: 4000,
|
||||
|
|
@ -160,8 +181,12 @@ export function deployGraphQL({
|
|||
.withSecret('S3_SECRET_ACCESS_KEY', s3.secret, 'secretAccessKey')
|
||||
.withSecret('S3_BUCKET_NAME', s3.secret, 'bucket')
|
||||
.withSecret('S3_ENDPOINT', s3.secret, 'endpoint')
|
||||
// Supertokens
|
||||
// Auth
|
||||
.withSecret('SUPERTOKENS_API_KEY', supertokens.secret, 'apiKey')
|
||||
.withSecret('AUTH_GITHUB_CLIENT_ID', githubOAuthSecret, 'clientId')
|
||||
.withSecret('AUTH_GITHUB_CLIENT_SECRET', githubOAuthSecret, 'clientSecret')
|
||||
.withSecret('AUTH_GOOGLE_CLIENT_ID', googleOAuthSecret, 'clientId')
|
||||
.withSecret('AUTH_GOOGLE_CLIENT_SECRET', googleOAuthSecret, 'clientSecret')
|
||||
// Zendesk
|
||||
.withConditionalSecret(zendesk.enabled, 'ZENDESK_SUBDOMAIN', zendesk.secret, 'subdomain')
|
||||
.withConditionalSecret(zendesk.enabled, 'ZENDESK_USERNAME', zendesk.secret, 'username')
|
||||
|
|
|
|||
|
|
@ -66,6 +66,14 @@ export function deployProxy({
|
|||
timeoutInSeconds: 60,
|
||||
retriable: true,
|
||||
},
|
||||
{
|
||||
name: 'auth',
|
||||
path: '/auth-api',
|
||||
customRewrite: '/auth-api',
|
||||
service: graphql.service,
|
||||
timeoutInSeconds: 60,
|
||||
retriable: true,
|
||||
},
|
||||
{
|
||||
name: 'usage',
|
||||
path: '/usage',
|
||||
|
|
|
|||
|
|
@ -243,8 +243,6 @@ services:
|
|||
ENCRYPTION_SECRET: '${HIVE_ENCRYPTION_SECRET}'
|
||||
WEB_APP_URL: '${HIVE_APP_BASE_URL}'
|
||||
PORT: 3001
|
||||
SUPERTOKENS_CONNECTION_URI: http://supertokens:3567
|
||||
SUPERTOKENS_API_KEY: '${SUPERTOKENS_API_KEY}'
|
||||
S3_ENDPOINT: 'http://s3:9000'
|
||||
S3_ACCESS_KEY_ID: ${MINIO_ROOT_USER}
|
||||
S3_SECRET_ACCESS_KEY: ${MINIO_ROOT_PASSWORD}
|
||||
|
|
@ -253,8 +251,13 @@ services:
|
|||
CDN_AUTH_PRIVATE_KEY: ${CDN_AUTH_PRIVATE_KEY}
|
||||
CDN_API: '1'
|
||||
CDN_API_BASE_URL: 'http://localhost:8082'
|
||||
AUTH_ORGANIZATION_OIDC: '1'
|
||||
LOG_LEVEL: '${LOG_LEVEL:-info}'
|
||||
# Auth
|
||||
AUTH_ORGANIZATION_OIDC: '1'
|
||||
AUTH_REQUIRE_EMAIL_VERIFICATION: '0'
|
||||
SUPERTOKENS_CONNECTION_URI: http://supertokens:3567
|
||||
SUPERTOKENS_API_KEY: '${SUPERTOKENS_API_KEY}'
|
||||
GRAPHQL_PUBLIC_ORIGIN: http://localhost:8082
|
||||
|
||||
policy:
|
||||
image: '${DOCKER_REGISTRY}policy${DOCKER_TAG}'
|
||||
|
|
@ -390,11 +393,8 @@ services:
|
|||
PORT: 3000
|
||||
NODE_ENV: production
|
||||
APP_BASE_URL: '${HIVE_APP_BASE_URL}'
|
||||
SUPERTOKENS_CONNECTION_URI: http://supertokens:3567
|
||||
SUPERTOKENS_API_KEY: '${SUPERTOKENS_API_KEY}'
|
||||
EMAILS_ENDPOINT: http://emails:3011
|
||||
GRAPHQL_ENDPOINT: http://server:3001/graphql
|
||||
SERVER_ENDPOINT: http://server:3001
|
||||
GRAPHQL_PUBLIC_ENDPOINT: http://localhost:8082/graphql
|
||||
GRAPHQL_PUBLIC_ORIGIN: http://localhost:8082
|
||||
AUTH_REQUIRE_EMAIL_VERIFICATION: '0'
|
||||
AUTH_ORGANIZATION_OIDC: '1'
|
||||
LOG_LEVEL: '${LOG_LEVEL:-info}'
|
||||
|
|
|
|||
|
|
@ -169,12 +169,18 @@ services:
|
|||
GITHUB_APP_PRIVATE_KEY: 5f938d51a065476c4dc1b04aeba13afb
|
||||
FEEDBACK_SLACK_TOKEN: ''
|
||||
FEEDBACK_SLACK_CHANNEL: '#hive'
|
||||
AUTH_ORGANIZATION_OIDC: '1'
|
||||
S3_PUBLIC_URL: 'http://localhost:8083'
|
||||
USAGE_ESTIMATOR_ENDPOINT: '${USAGE_ESTIMATOR_ENDPOINT}'
|
||||
RATE_LIMIT_ENDPOINT: '${RATE_LIMIT_ENDPOINT}'
|
||||
EMAIL_PROVIDER: '${EMAIL_PROVIDER}'
|
||||
LOG_LEVEL: debug
|
||||
# Auth
|
||||
WEB_APP_URL: '${HIVE_APP_BASE_URL}'
|
||||
AUTH_ORGANIZATION_OIDC: '1'
|
||||
AUTH_REQUIRE_EMAIL_VERIFICATION: '0'
|
||||
SUPERTOKENS_CONNECTION_URI: http://supertokens:3567
|
||||
SUPERTOKENS_API_KEY: '${SUPERTOKENS_API_KEY}'
|
||||
GRAPHQL_PUBLIC_ORIGIN: http://localhost:8082
|
||||
|
||||
broker:
|
||||
image: vectorized/redpanda:latest
|
||||
|
|
|
|||
|
|
@ -188,7 +188,7 @@ export class AuthManager {
|
|||
|
||||
getCurrentUser: () => Promise<(User & { isAdmin: boolean }) | never> = share(async () => {
|
||||
if (!this.session) {
|
||||
throw new AccessError('Authorization token is missing');
|
||||
throw new AccessError('Authorization token is missing', 'UNAUTHENTICATED');
|
||||
}
|
||||
|
||||
const user = await this.storage.getUserBySuperTokenId({
|
||||
|
|
|
|||
|
|
@ -55,11 +55,16 @@ export class GitHubIntegrationManager {
|
|||
installationId: string;
|
||||
},
|
||||
): Promise<void> {
|
||||
this.logger.debug('Registering GitHub integration (organization=%s)', input.organization);
|
||||
this.logger.debug(
|
||||
'Registering GitHub integration (organization=%s, installationId:%s)',
|
||||
input.organization,
|
||||
input.installationId,
|
||||
);
|
||||
await this.authManager.ensureOrganizationAccess({
|
||||
...input,
|
||||
scope: OrganizationAccessScope.INTEGRATIONS,
|
||||
});
|
||||
this.logger.debug('Updating organization');
|
||||
await this.storage.addGitHubIntegration({
|
||||
organization: input.organization,
|
||||
installationId: input.installationId,
|
||||
|
|
@ -72,6 +77,7 @@ export class GitHubIntegrationManager {
|
|||
...input,
|
||||
scope: OrganizationAccessScope.INTEGRATIONS,
|
||||
});
|
||||
this.logger.debug('Updating organization');
|
||||
await this.storage.deleteGitHubIntegration({
|
||||
organization: input.organization,
|
||||
});
|
||||
|
|
@ -80,7 +86,8 @@ export class GitHubIntegrationManager {
|
|||
async isAvailable(selector: OrganizationSelector): Promise<boolean> {
|
||||
this.logger.debug('Checking GitHub integration (organization=%s)', selector.organization);
|
||||
|
||||
if (this.isEnabled()) {
|
||||
if (!this.isEnabled()) {
|
||||
this.logger.debug('GitHub integration is disabled.');
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ export class SlackIntegrationManager {
|
|||
...input,
|
||||
scope: OrganizationAccessScope.INTEGRATIONS,
|
||||
});
|
||||
this.logger.debug('Updating organization');
|
||||
await this.storage.addSlackIntegration({
|
||||
organization: input.organization,
|
||||
token: this.crypto.encrypt(input.token),
|
||||
|
|
@ -54,6 +55,7 @@ export class SlackIntegrationManager {
|
|||
...input,
|
||||
scope: OrganizationAccessScope.INTEGRATIONS,
|
||||
});
|
||||
this.logger.debug('Updating organization');
|
||||
await this.storage.deleteSlackIntegration({
|
||||
organization: input.organization,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -278,6 +278,31 @@ export class OIDCIntegrationsProvider {
|
|||
organizationId: integration.linkedOrganizationId,
|
||||
} as const;
|
||||
}
|
||||
|
||||
async getOIDCIntegrationById(args: { oidcIntegrationId: string }) {
|
||||
if (this.isEnabled() === false) {
|
||||
return {
|
||||
type: 'error',
|
||||
message: 'OIDC integrations are disabled.',
|
||||
} as const;
|
||||
}
|
||||
|
||||
const integration = await this.storage.getOIDCIntegrationById({
|
||||
oidcIntegrationId: args.oidcIntegrationId,
|
||||
});
|
||||
|
||||
if (integration === null) {
|
||||
return {
|
||||
type: 'error',
|
||||
message: 'Integration not found.',
|
||||
} as const;
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'ok',
|
||||
organizationId: integration.linkedOrganizationId,
|
||||
} as const;
|
||||
}
|
||||
}
|
||||
|
||||
const OIDCIntegrationClientIdModel = zod
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ export default gql`
|
|||
organizationTransferRequest(
|
||||
selector: OrganizationTransferRequestSelector!
|
||||
): OrganizationTransfer
|
||||
myDefaultOrganization(previouslyVisitedOrganizationId: ID): OrganizationPayload
|
||||
}
|
||||
|
||||
extend type Mutation {
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import {
|
|||
} from '../auth/providers/organization-access';
|
||||
import { isProjectScope, ProjectAccessScope } from '../auth/providers/project-access';
|
||||
import { isTargetScope, TargetAccessScope } from '../auth/providers/target-access';
|
||||
import { OIDCIntegrationsProvider } from '../oidc-integrations/providers/oidc-integrations.provider';
|
||||
import { InMemoryRateLimiter } from '../rate-limit/providers/in-memory-rate-limiter';
|
||||
import { IdTranslator } from '../shared/providers/id-translator';
|
||||
import { Logger } from '../shared/providers/logger';
|
||||
|
|
@ -54,6 +55,71 @@ export const resolvers: OrganizationModule.Resolvers = {
|
|||
async organizations(_, __, { injector }) {
|
||||
return injector.get(OrganizationManager).getOrganizations();
|
||||
},
|
||||
async myDefaultOrganization(_, { previouslyVisitedOrganizationId }, { injector }) {
|
||||
const user = await injector.get(AuthManager).getCurrentUser();
|
||||
const organizationManager = injector.get(OrganizationManager);
|
||||
|
||||
// For an OIDC Integration User we want to return the linked organization
|
||||
if (user?.oidcIntegrationId) {
|
||||
const oidcIntegration = await injector
|
||||
.get(OIDCIntegrationsProvider)
|
||||
.getOIDCIntegrationById({
|
||||
oidcIntegrationId: user.oidcIntegrationId,
|
||||
});
|
||||
if (oidcIntegration.type === 'ok') {
|
||||
const org = await organizationManager.getOrganization({
|
||||
organization: oidcIntegration.organizationId,
|
||||
});
|
||||
|
||||
return {
|
||||
selector: {
|
||||
organization: org.cleanId,
|
||||
},
|
||||
organization: org,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// This is the organization that got stored as an cookie
|
||||
// We make sure it actually exists before directing to it.
|
||||
if (previouslyVisitedOrganizationId) {
|
||||
const orgId = await injector.get(IdTranslator).translateOrganizationId({
|
||||
organization: previouslyVisitedOrganizationId,
|
||||
});
|
||||
|
||||
const org = await organizationManager.getOrganization({
|
||||
organization: orgId,
|
||||
});
|
||||
|
||||
if (org) {
|
||||
return {
|
||||
selector: {
|
||||
organization: org.cleanId,
|
||||
},
|
||||
organization: org,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (user?.id) {
|
||||
const allOrganizations = await organizationManager.getOrganizations();
|
||||
|
||||
if (allOrganizations.length > 0) {
|
||||
const firstOrg = allOrganizations[0];
|
||||
|
||||
return {
|
||||
selector: {
|
||||
organization: firstOrg.cleanId,
|
||||
},
|
||||
organization: firstOrg,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
async organizationByInviteCode(_, { code }, { injector }) {
|
||||
const organization = await injector.get(OrganizationManager).getOrganizationByInviteCode({
|
||||
code,
|
||||
|
|
|
|||
|
|
@ -27,7 +27,11 @@ export function isGraphQLError(error: unknown): error is GraphQLError {
|
|||
export const HiveError = GraphQLError;
|
||||
|
||||
export class AccessError extends HiveError {
|
||||
constructor(reason: string) {
|
||||
super(`No access (reason: "${reason}")`);
|
||||
constructor(reason: string, code: string = 'UNAUTHORISED') {
|
||||
super(`No access (reason: "${reason}")`, {
|
||||
extensions: {
|
||||
code,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
"license": "MIT",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "BUILD=1 tsx ../../../../scripts/runify.ts",
|
||||
"build": "tsx ../../../../scripts/runify.ts",
|
||||
"dev": "tsup-node --config ../../../configs/tsup/dev.config.node.ts src/index.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
|||
|
|
@ -57,3 +57,23 @@ SUPERTOKENS_API_KEY=bubatzbieber6942096420
|
|||
# Organization level Open ID Connect Authentication
|
||||
AUTH_ORGANIZATION_OIDC=0
|
||||
|
||||
## Enable GitHub login
|
||||
AUTH_GITHUB="<sync>"
|
||||
AUTH_GITHUB_CLIENT_ID="<sync>"
|
||||
AUTH_GITHUB_CLIENT_SECRET="<sync>"
|
||||
|
||||
## Enable Google login
|
||||
AUTH_GOOGLE="<sync>"
|
||||
AUTH_GOOGLE_CLIENT_ID="<sync>"
|
||||
AUTH_GOOGLE_CLIENT_SECRET="<sync>"
|
||||
|
||||
WEB_APP_URL="http://localhost:3000"
|
||||
GRAPHQL_PUBLIC_ORIGIN="http://localhost:3001"
|
||||
|
||||
## Enable Okta login
|
||||
AUTH_OKTA=0
|
||||
AUTH_OKTA_HIDDEN=0
|
||||
AUTH_OKTA_ENDPOINT="<sync>"
|
||||
AUTH_OKTA_CLIENT_ID="<sync>"
|
||||
AUTH_OKTA_CLIENT_SECRET="<sync>"
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ The GraphQL API for GraphQL Hive.
|
|||
| `PORT` | **Yes** | The port this service is running on. | `4013` |
|
||||
| `ENCRYPTION_SECRET` | **Yes** | Secret for encrypting stuff. | `8ebe95cg21c1fee355e9fa32c8c33141` |
|
||||
| `WEB_APP_URL` | **Yes** | The url of the web app. | `http://127.0.0.1:3000` |
|
||||
| `GRAPHQL_PUBLIC_ORIGIN` | **Yes** | The origin of the GraphQL server. | `http://127.0.0.1:4013` |
|
||||
| `RATE_LIMIT_ENDPOINT` | **Yes** | The endpoint of the rate limiting service. | `http://127.0.0.1:4012` |
|
||||
| `EMAILS_ENDPOINT` | **Yes** | The endpoint of the GraphQL Hive Email service. | `http://127.0.0.1:6260` |
|
||||
| `TOKENS_ENDPOINT` | **Yes** | The endpoint of the tokens service. | `http://127.0.0.1:6001` |
|
||||
|
|
@ -40,10 +41,22 @@ The GraphQL API for GraphQL Hive.
|
|||
| `CDN_API_BASE_URL` | No (Yes if `CDN_API` is set to `1`) | The public base url of the API service. | `http://localhost:8082` |
|
||||
| `SUPERTOKENS_CONNECTION_URI` | **Yes** | The URI of the SuperTokens instance. | `http://127.0.0.1:3567` |
|
||||
| `SUPERTOKENS_API_KEY` | **Yes** | The API KEY of the SuperTokens instance. | `iliketurtlesandicannotlie` |
|
||||
| `AUTH_GITHUB` | No | Whether login via GitHub should be allowed | `1` (enabled) or `0` (disabled) |
|
||||
| `AUTH_GITHUB_CLIENT_ID` | No (**Yes** if `AUTH_GITHUB` is set) | The GitHub client ID. | `g6aff8102efda5e1d12e` |
|
||||
| `AUTH_GITHUB_CLIENT_SECRET` | No (**Yes** if `AUTH_GITHUB` is set) | The GitHub client secret. | `g12e552xx54xx2b127821dc4abc4491dxxxa6b187` |
|
||||
| `AUTH_GOOGLE` | No | Whether login via Google should be allowed | `1` (enabled) or `0` (disabled) |
|
||||
| `AUTH_GOOGLE_CLIENT_ID` | No (**Yes** if `AUTH_GOOGLE` is set) | The Google client ID. | `g6aff8102efda5e1d12e` |
|
||||
| `AUTH_GOOGLE_CLIENT_SECRET` | No (**Yes** if `AUTH_GOOGLE` is set) | The Google client secret. | `g12e552xx54xx2b127821dc4abc4491dxxxa6b187` |
|
||||
| `AUTH_ORGANIZATION_OIDC` | No | Whether linking a Hive organization to an Open ID Connect provider is allowed. (Default: `0`) | `1` (enabled) or `0` (disabled) |
|
||||
| `AUTH_OKTA` | No | Whether login via Okta should be allowed | `1` (enabled) or `0` (disabled) |
|
||||
| `AUTH_OKTA_CLIENT_ENDPOINT` | No (**Yes** if `AUTH_OKTA` is set) | The Okta endpoint. | `https://dev-1234567.okta.com` |
|
||||
| `AUTH_OKTA_HIDDEN` | No | Whether the Okta login button should be hidden. (Default: `0`) | `1` (enabled) or `0` (disabled) |
|
||||
| `AUTH_OKTA_CLIENT_ID` | No (**Yes** if `AUTH_OKTA` is set) | The Okta client ID. | `g6aff8102efda5e1d12e` |
|
||||
| `AUTH_OKTA_CLIENT_SECRET` | No (**Yes** if `AUTH_OKTA` is set) | The Okta client secret. | `g12e552xx54xx2b127821dc4abc4491dxxxa6b187` |
|
||||
| `AUTH_REQUIRE_EMAIL_VERIFICATION` | No | Whether verifying the email address is mandatory. | `1` (enabled) or `0` (disabled) |
|
||||
| `INTEGRATION_GITHUB` | No | Whether the GitHub integration is enabled | `1` (enabled) or `0` (disabled) |
|
||||
| `INTEGRATION_GITHUB_GITHUB_APP_ID` | No (Yes if `INTEGRATION_GITHUB` is set to `1`) | The GitHub app id. | `123` |
|
||||
| `INTEGRATION_GITHUB_GITHUB_APP_PRIVATE_KEY` | No (Yes if `INTEGRATION_GITHUB` is set to `1`) | The GitHub app private key. | `letmein1` |
|
||||
| `AUTH_ORGANIZATION_OIDC` | No | Whether linking a Hive organization to an Open ID Connect provider is allowed. (Default: `0`) | `1` (enabled) or `0` (disabled) |
|
||||
| `ENVIRONMENT` | No | The environment of your Hive app. (**Note:** This will be used for Sentry reporting.) | `staging` |
|
||||
| `SENTRY` | No | Whether Sentry error reporting should be enabled. | `1` (enabled) or `0` (disabled) |
|
||||
| `SENTRY_DSN` | No | The DSN for reporting errors to Sentry. | `https://dooobars@o557896.ingest.sentry.io/12121212` |
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@
|
|||
"@escape.tech/graphql-armor-max-depth": "2.2.0",
|
||||
"@escape.tech/graphql-armor-max-directives": "2.1.0",
|
||||
"@escape.tech/graphql-armor-max-tokens": "2.3.0",
|
||||
"@fastify/cors": "9.0.1",
|
||||
"@fastify/formbody": "7.4.0",
|
||||
"@graphql-hive/client": "workspace:*",
|
||||
"@graphql-yoga/plugin-persisted-operations": "3.2.0",
|
||||
"@graphql-yoga/plugin-response-cache": "3.4.0",
|
||||
|
|
@ -30,6 +32,7 @@
|
|||
"@sentry/integrations": "7.108.0",
|
||||
"@sentry/node": "7.108.0",
|
||||
"@swc/core": "1.4.8",
|
||||
"@trpc/client": "10.45.2",
|
||||
"@trpc/server": "10.45.2",
|
||||
"@whatwg-node/server": "0.9.32",
|
||||
"dotenv": "16.4.5",
|
||||
|
|
@ -41,6 +44,8 @@
|
|||
"pino-pretty": "11.0.0",
|
||||
"prom-client": "15.1.0",
|
||||
"reflect-metadata": "0.2.1",
|
||||
"supertokens-js-override": "0.0.4",
|
||||
"supertokens-node": "15.2.1",
|
||||
"tslib": "2.6.2",
|
||||
"zod": "3.22.4"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -2,23 +2,19 @@ import { CryptoProvider } from 'packages/services/api/src/modules/shared/provide
|
|||
import { z } from 'zod';
|
||||
import type { Storage } from '@hive/api';
|
||||
import { OrganizationAccessScope, ProjectAccessScope, TargetAccessScope } from '@hive/api';
|
||||
import { FastifyRequest, handleTRPCError } from '@hive/service-common';
|
||||
import type { inferAsyncReturnType } from '@trpc/server';
|
||||
import { initTRPC } from '@trpc/server';
|
||||
|
||||
export async function createContext({
|
||||
storage,
|
||||
crypto,
|
||||
req,
|
||||
}: {
|
||||
storage: Storage;
|
||||
crypto: CryptoProvider;
|
||||
req: FastifyRequest;
|
||||
}) {
|
||||
return {
|
||||
storage,
|
||||
crypto,
|
||||
req,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -32,10 +28,9 @@ const oidcDefaultScopes = [
|
|||
];
|
||||
|
||||
const t = initTRPC.context<Context>().create();
|
||||
const procedure = t.procedure.use(handleTRPCError);
|
||||
|
||||
export const internalApiRouter = t.router({
|
||||
ensureUser: procedure
|
||||
ensureUser: t.procedure
|
||||
.input(
|
||||
z
|
||||
.object({
|
||||
|
|
@ -58,64 +53,8 @@ export const internalApiRouter = t.router({
|
|||
|
||||
return result;
|
||||
}),
|
||||
getDefaultOrgForUser: procedure
|
||||
.input(
|
||||
z.object({
|
||||
superTokensUserId: z.string(),
|
||||
lastOrgId: z.union([z.string(), z.null()]),
|
||||
}),
|
||||
)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const user = await ctx.storage.getUserBySuperTokenId({
|
||||
superTokensUserId: input.superTokensUserId,
|
||||
});
|
||||
|
||||
// For an OIDC Integration User we want to return the linked organization
|
||||
if (user?.oidcIntegrationId) {
|
||||
const oidcIntegration = await ctx.storage.getOIDCIntegrationById({
|
||||
oidcIntegrationId: user.oidcIntegrationId,
|
||||
});
|
||||
if (oidcIntegration) {
|
||||
const org = await ctx.storage.getOrganization({
|
||||
organization: oidcIntegration.linkedOrganizationId,
|
||||
});
|
||||
|
||||
return {
|
||||
id: org.id,
|
||||
cleanId: org.cleanId,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// This is the organizaton that got stored as an cookie
|
||||
// We make sure it actually exists before directing to it.
|
||||
if (input.lastOrgId) {
|
||||
const org = await ctx.storage.getOrganizationByCleanId({ cleanId: input.lastOrgId });
|
||||
|
||||
if (org) {
|
||||
return {
|
||||
id: org.id,
|
||||
cleanId: org.cleanId,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (user?.id) {
|
||||
const allAllOraganizations = await ctx.storage.getOrganizations({ user: user.id });
|
||||
|
||||
if (allAllOraganizations.length > 0) {
|
||||
const someOrg = allAllOraganizations[0];
|
||||
|
||||
return {
|
||||
id: someOrg.id,
|
||||
cleanId: someOrg.cleanId,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}),
|
||||
getOIDCIntegrationById: procedure
|
||||
getOIDCIntegrationById: t.procedure
|
||||
.input(
|
||||
z.object({
|
||||
oidcIntegrationId: z.string().min(1),
|
||||
|
|
@ -145,4 +84,6 @@ export const internalApiRouter = t.router({
|
|||
}),
|
||||
});
|
||||
|
||||
export const createInternalApiCaller = t.createCallerFactory(internalApiRouter);
|
||||
|
||||
export type InternalApi = typeof internalApiRouter;
|
||||
|
|
|
|||
|
|
@ -22,7 +22,13 @@ const EnvironmentModel = zod.object({
|
|||
ENVIRONMENT: emptyString(zod.string().optional()),
|
||||
RELEASE: emptyString(zod.string().optional()),
|
||||
ENCRYPTION_SECRET: emptyString(zod.string()),
|
||||
WEB_APP_URL: emptyString(zod.string().url().optional()),
|
||||
WEB_APP_URL: zod.string().url(),
|
||||
GRAPHQL_PUBLIC_ORIGIN: zod
|
||||
.string({
|
||||
required_error:
|
||||
'GRAPHQL_PUBLIC_ORIGIN is required (see: https://github.com/kamilkisiela/graphql-hive/pull/4288#issue-2195509699)',
|
||||
})
|
||||
.url(),
|
||||
RATE_LIMIT_ENDPOINT: emptyString(zod.string().url().optional()),
|
||||
SCHEMA_POLICY_ENDPOINT: emptyString(zod.string().url().optional()),
|
||||
TOKENS_ENDPOINT: zod.string().url(),
|
||||
|
|
@ -34,6 +40,9 @@ const EnvironmentModel = zod.object({
|
|||
SCHEMA_ENDPOINT: zod.string().url(),
|
||||
AUTH_ORGANIZATION_OIDC: emptyString(zod.union([zod.literal('1'), zod.literal('0')]).optional()),
|
||||
GRAPHQL_PERSISTED_OPERATIONS_PATH: emptyString(zod.string().optional()),
|
||||
AUTH_REQUIRE_EMAIL_VERIFICATION: emptyString(
|
||||
zod.union([zod.literal('1'), zod.literal('0')]).optional(),
|
||||
),
|
||||
});
|
||||
|
||||
const SentryModel = zod.union([
|
||||
|
|
@ -146,6 +155,45 @@ const S3Model = zod.object({
|
|||
S3_PUBLIC_URL: emptyString(zod.string().url().optional()),
|
||||
});
|
||||
|
||||
const AuthGitHubConfigSchema = zod.union([
|
||||
zod.object({
|
||||
AUTH_GITHUB: zod.union([zod.void(), zod.literal('0'), zod.literal('')]),
|
||||
}),
|
||||
zod.object({
|
||||
AUTH_GITHUB: zod.literal('1'),
|
||||
AUTH_GITHUB_CLIENT_ID: zod.string(),
|
||||
AUTH_GITHUB_CLIENT_SECRET: zod.string(),
|
||||
}),
|
||||
]);
|
||||
|
||||
const AuthGoogleConfigSchema = zod.union([
|
||||
zod.object({
|
||||
AUTH_GOOGLE: zod.union([zod.void(), zod.literal('0'), zod.literal('')]),
|
||||
}),
|
||||
zod.object({
|
||||
AUTH_GOOGLE: zod.literal('1'),
|
||||
AUTH_GOOGLE_CLIENT_ID: zod.string(),
|
||||
AUTH_GOOGLE_CLIENT_SECRET: zod.string(),
|
||||
}),
|
||||
]);
|
||||
|
||||
const AuthOktaConfigSchema = zod.union([
|
||||
zod.object({
|
||||
AUTH_OKTA: zod.union([zod.void(), zod.literal('0')]),
|
||||
}),
|
||||
zod.object({
|
||||
AUTH_OKTA: zod.literal('1'),
|
||||
AUTH_OKTA_HIDDEN: emptyString(zod.union([zod.literal('1'), zod.literal('0')]).optional()),
|
||||
AUTH_OKTA_ENDPOINT: zod.string().url(),
|
||||
AUTH_OKTA_CLIENT_ID: zod.string(),
|
||||
AUTH_OKTA_CLIENT_SECRET: zod.string(),
|
||||
}),
|
||||
]);
|
||||
|
||||
const AuthOktaMultiTenantSchema = zod.object({
|
||||
AUTH_ORGANIZATION_OIDC: emptyString(zod.union([zod.literal('1'), zod.literal('0')]).optional()),
|
||||
});
|
||||
|
||||
const LogModel = zod.object({
|
||||
LOG_LEVEL: emptyString(
|
||||
zod
|
||||
|
|
@ -165,35 +213,28 @@ const LogModel = zod.object({
|
|||
),
|
||||
});
|
||||
|
||||
// eslint-disable-next-line no-process-env
|
||||
const processEnv = process.env;
|
||||
|
||||
const configs = {
|
||||
// eslint-disable-next-line no-process-env
|
||||
base: EnvironmentModel.safeParse(process.env),
|
||||
// eslint-disable-next-line no-process-env
|
||||
sentry: SentryModel.safeParse(process.env),
|
||||
// eslint-disable-next-line no-process-env
|
||||
postgres: PostgresModel.safeParse(process.env),
|
||||
// eslint-disable-next-line no-process-env
|
||||
clickhouse: ClickHouseModel.safeParse(process.env),
|
||||
// eslint-disable-next-line no-process-env
|
||||
redis: RedisModel.safeParse(process.env),
|
||||
// eslint-disable-next-line no-process-env
|
||||
supertokens: SuperTokensModel.safeParse(process.env),
|
||||
// eslint-disable-next-line no-process-env
|
||||
github: GitHubModel.safeParse(process.env),
|
||||
// eslint-disable-next-line no-process-env
|
||||
cdnCf: CdnCFModel.safeParse(process.env),
|
||||
// eslint-disable-next-line no-process-env
|
||||
cdnApi: CdnApiModel.safeParse(process.env),
|
||||
// eslint-disable-next-line no-process-env
|
||||
prometheus: PrometheusModel.safeParse(process.env),
|
||||
// eslint-disable-next-line no-process-env
|
||||
hive: HiveModel.safeParse(process.env),
|
||||
// eslint-disable-next-line no-process-env
|
||||
s3: S3Model.safeParse(process.env),
|
||||
// eslint-disable-next-line no-process-env
|
||||
log: LogModel.safeParse(process.env),
|
||||
// eslint-disable-next-line no-process-env
|
||||
zendeskSupport: ZendeskSupportModel.safeParse(process.env),
|
||||
base: EnvironmentModel.safeParse(processEnv),
|
||||
sentry: SentryModel.safeParse(processEnv),
|
||||
postgres: PostgresModel.safeParse(processEnv),
|
||||
clickhouse: ClickHouseModel.safeParse(processEnv),
|
||||
redis: RedisModel.safeParse(processEnv),
|
||||
supertokens: SuperTokensModel.safeParse(processEnv),
|
||||
authGithub: AuthGitHubConfigSchema.safeParse(processEnv),
|
||||
authGoogle: AuthGoogleConfigSchema.safeParse(processEnv),
|
||||
authOkta: AuthOktaConfigSchema.safeParse(processEnv),
|
||||
authOktaMultiTenant: AuthOktaMultiTenantSchema.safeParse(processEnv),
|
||||
github: GitHubModel.safeParse(processEnv),
|
||||
cdnCf: CdnCFModel.safeParse(processEnv),
|
||||
cdnApi: CdnApiModel.safeParse(processEnv),
|
||||
prometheus: PrometheusModel.safeParse(processEnv),
|
||||
hive: HiveModel.safeParse(processEnv),
|
||||
s3: S3Model.safeParse(processEnv),
|
||||
log: LogModel.safeParse(processEnv),
|
||||
zendeskSupport: ZendeskSupportModel.safeParse(processEnv),
|
||||
};
|
||||
|
||||
const environmentErrors: Array<string> = [];
|
||||
|
|
@ -223,6 +264,10 @@ const sentry = extractConfig(configs.sentry);
|
|||
const clickhouse = extractConfig(configs.clickhouse);
|
||||
const redis = extractConfig(configs.redis);
|
||||
const supertokens = extractConfig(configs.supertokens);
|
||||
const authGithub = extractConfig(configs.authGithub);
|
||||
const authGoogle = extractConfig(configs.authGoogle);
|
||||
const authOkta = extractConfig(configs.authOkta);
|
||||
const authOktaMultiTenant = extractConfig(configs.authOktaMultiTenant);
|
||||
const github = extractConfig(configs.github);
|
||||
const prometheus = extractConfig(configs.prometheus);
|
||||
const log = extractConfig(configs.log);
|
||||
|
|
@ -254,11 +299,9 @@ export const env = {
|
|||
release: base.RELEASE ?? 'local',
|
||||
encryptionSecret: base.ENCRYPTION_SECRET,
|
||||
hiveServices: {
|
||||
webApp: base.WEB_APP_URL
|
||||
? {
|
||||
url: base.WEB_APP_URL,
|
||||
}
|
||||
: null,
|
||||
webApp: {
|
||||
url: base.WEB_APP_URL,
|
||||
},
|
||||
tokens: {
|
||||
endpoint: base.TOKENS_ENDPOINT,
|
||||
},
|
||||
|
|
@ -311,6 +354,33 @@ export const env = {
|
|||
connectionURI: supertokens.SUPERTOKENS_CONNECTION_URI,
|
||||
apiKey: supertokens.SUPERTOKENS_API_KEY,
|
||||
},
|
||||
auth: {
|
||||
github:
|
||||
authGithub.AUTH_GITHUB === '1'
|
||||
? {
|
||||
clientId: authGithub.AUTH_GITHUB_CLIENT_ID,
|
||||
clientSecret: authGithub.AUTH_GITHUB_CLIENT_SECRET,
|
||||
}
|
||||
: null,
|
||||
google:
|
||||
authGoogle.AUTH_GOOGLE === '1'
|
||||
? {
|
||||
clientId: authGoogle.AUTH_GOOGLE_CLIENT_ID,
|
||||
clientSecret: authGoogle.AUTH_GOOGLE_CLIENT_SECRET,
|
||||
}
|
||||
: null,
|
||||
okta:
|
||||
authOkta.AUTH_OKTA === '1'
|
||||
? {
|
||||
endpoint: authOkta.AUTH_OKTA_ENDPOINT,
|
||||
hidden: authOkta.AUTH_OKTA_HIDDEN === '1',
|
||||
clientId: authOkta.AUTH_OKTA_CLIENT_ID,
|
||||
clientSecret: authOkta.AUTH_OKTA_CLIENT_SECRET,
|
||||
}
|
||||
: null,
|
||||
organizationOIDC: authOktaMultiTenant.AUTH_ORGANIZATION_OIDC === '1',
|
||||
requireEmailVerification: base.AUTH_REQUIRE_EMAIL_VERIFICATION === '1',
|
||||
},
|
||||
github:
|
||||
github.INTEGRATION_GITHUB === '1'
|
||||
? {
|
||||
|
|
@ -357,6 +427,7 @@ export const env = {
|
|||
hive: hiveConfig,
|
||||
graphql: {
|
||||
persistedOperationsPath: base.GRAPHQL_PERSISTED_OPERATIONS_PATH ?? null,
|
||||
origin: base.GRAPHQL_PUBLIC_ORIGIN,
|
||||
},
|
||||
zendeskSupport:
|
||||
zendeskSupport.ZENDESK_SUPPORT === '1'
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ import {
|
|||
} from 'graphql';
|
||||
import { createYoga, Plugin, useErrorHandler } from 'graphql-yoga';
|
||||
import hyperid from 'hyperid';
|
||||
import zod from 'zod';
|
||||
import { isGraphQLError } from '@envelop/core';
|
||||
import { useGenericAuth } from '@envelop/generic-auth';
|
||||
import { useGraphQlJit } from '@envelop/graphql-jit';
|
||||
|
|
@ -21,11 +20,12 @@ import { useSentry } from '@envelop/sentry';
|
|||
import { useYogaHive } from '@graphql-hive/client';
|
||||
import { usePersistedOperations } from '@graphql-yoga/plugin-persisted-operations';
|
||||
import { useResponseCache } from '@graphql-yoga/plugin-response-cache';
|
||||
import { HiveError, Registry, RegistryContext } from '@hive/api';
|
||||
import { Registry, RegistryContext } from '@hive/api';
|
||||
import { cleanRequestId } from '@hive/service-common';
|
||||
import { runWithAsyncContext } from '@sentry/node';
|
||||
import { asyncStorage } from './async-storage';
|
||||
import type { HiveConfig } from './environment';
|
||||
import { resolveUser, type SupertokensSession } from './supertokens';
|
||||
import { useArmor } from './use-armor';
|
||||
import { extractUserId, useSentryUser } from './use-sentry-user';
|
||||
|
||||
|
|
@ -37,23 +37,6 @@ function hashSessionId(sessionId: string): string {
|
|||
return createHash('sha256').update(sessionId).digest('hex');
|
||||
}
|
||||
|
||||
const SuperTokenAccessTokenModel = zod.object({
|
||||
version: zod.literal('1'),
|
||||
superTokensUserId: zod.string(),
|
||||
/**
|
||||
* Supertokens for some reason omits externalUserId from the access token payload if it is null.
|
||||
*/
|
||||
externalUserId: zod.optional(zod.union([zod.string(), zod.null()])),
|
||||
email: zod.string(),
|
||||
});
|
||||
|
||||
const SuperTokenSessionVerifyModel = zod.object({
|
||||
status: zod.literal('OK'),
|
||||
session: zod.object({
|
||||
userDataInJWT: SuperTokenAccessTokenModel,
|
||||
}),
|
||||
});
|
||||
|
||||
export interface GraphQLHandlerOptions {
|
||||
graphiqlEndpoint: string;
|
||||
registry: Registry;
|
||||
|
|
@ -69,12 +52,10 @@ export interface GraphQLHandlerOptions {
|
|||
persistedOperations: Record<string, DocumentNode | string> | null;
|
||||
}
|
||||
|
||||
export type SuperTokenSessionPayload = zod.TypeOf<typeof SuperTokenAccessTokenModel>;
|
||||
|
||||
interface Context extends RegistryContext {
|
||||
req: FastifyRequest;
|
||||
reply: FastifyReply;
|
||||
session: SuperTokenSessionPayload | null;
|
||||
session: SupertokensSession | null;
|
||||
}
|
||||
|
||||
const NoIntrospection: ValidationRule = (context: ValidationContext) => ({
|
||||
|
|
@ -204,27 +185,8 @@ export const graphqlHandler = (options: GraphQLHandlerOptions): RouteHandlerMeth
|
|||
useGenericAuth({
|
||||
mode: 'resolve-only',
|
||||
contextFieldName: 'session',
|
||||
resolveUserFn: async (ctx: Context) => {
|
||||
if (ctx.headers['authorization']) {
|
||||
let authHeader = ctx.headers['authorization'];
|
||||
authHeader = Array.isArray(authHeader) ? authHeader[0] : authHeader;
|
||||
|
||||
const authHeaderParts = authHeader.split(' ');
|
||||
if (authHeaderParts.length === 2 && authHeaderParts[0] === 'Bearer') {
|
||||
const accessToken = authHeaderParts[1];
|
||||
// The token issued by Hive is always 32 characters long.
|
||||
// Everything longer should be treated as a supertokens token (JWT).
|
||||
if (accessToken.length > 32) {
|
||||
return await verifySuperTokensSession(
|
||||
options.supertokens.connectionUri,
|
||||
options.supertokens.apiKey,
|
||||
accessToken,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
async resolveUserFn(ctx: Context) {
|
||||
return resolveUser(ctx);
|
||||
},
|
||||
}),
|
||||
useYogaHive({
|
||||
|
|
@ -358,52 +320,6 @@ export const graphqlHandler = (options: GraphQLHandlerOptions): RouteHandlerMeth
|
|||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Verify whether a SuperTokens access token session is valid.
|
||||
* https://app.swaggerhub.com/apis/supertokens/CDI/2.20#/Session%20Recipe/verifySession
|
||||
*/
|
||||
async function verifySuperTokensSession(
|
||||
connectionUri: string,
|
||||
apiKey: string,
|
||||
accessToken: string,
|
||||
): Promise<SuperTokenSessionPayload> {
|
||||
const response = await fetch(connectionUri + '/appid-public/public/recipe/session/verify', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
'api-key': apiKey,
|
||||
rid: 'session',
|
||||
'cdi-version': '4.0',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
accessToken,
|
||||
enableAntiCsrf: false,
|
||||
doAntiCsrfCheck: false,
|
||||
checkDatabase: true,
|
||||
}),
|
||||
});
|
||||
const body = await response.text();
|
||||
if (response.status !== 200) {
|
||||
console.error(
|
||||
`SuperTokens session verification failed with status ${response.status}.\n` + body,
|
||||
);
|
||||
throw new Error(`SuperTokens instance returned an unexpected error.`);
|
||||
}
|
||||
|
||||
const result = SuperTokenSessionVerifyModel.safeParse(JSON.parse(body));
|
||||
|
||||
if (result.success === false) {
|
||||
console.error(`SuperTokens session verification failed.\n` + body);
|
||||
throw new HiveError(`Invalid token provided`);
|
||||
}
|
||||
|
||||
// ensure externalUserId is a string or null
|
||||
return {
|
||||
...result.data.session.userDataInJWT,
|
||||
externalUserId: result.data.session.userDataInJWT.externalUserId ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
function isOperationDefinitionNode(def: DefinitionNode): def is OperationDefinitionNode {
|
||||
return def.kind === Kind.OPERATION_DEFINITION;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,8 +2,16 @@
|
|||
import * as fs from 'fs';
|
||||
import got from 'got';
|
||||
import { DocumentNode, GraphQLError, stripIgnoredCharacters } from 'graphql';
|
||||
import supertokens from 'supertokens-node';
|
||||
import {
|
||||
errorHandler as supertokensErrorHandler,
|
||||
plugin as supertokensFastifyPlugin,
|
||||
} from 'supertokens-node/framework/fastify/index.js';
|
||||
import cors from '@fastify/cors';
|
||||
import type { FastifyCorsOptionsDelegateCallback } from '@fastify/cors';
|
||||
import 'reflect-metadata';
|
||||
import { hostname } from 'os';
|
||||
import formDataPlugin from '@fastify/formbody';
|
||||
import { createRegistry, createTaskRunner, CryptoProvider, LogFn, Logger } from '@hive/api';
|
||||
import { createArtifactRequestHandler } from '@hive/cdn-script/artifact-handler';
|
||||
import { ArtifactStorageReader } from '@hive/cdn-script/artifact-storage-reader';
|
||||
|
|
@ -36,6 +44,7 @@ import { asyncStorage } from './async-storage';
|
|||
import { env } from './environment';
|
||||
import { graphqlHandler } from './graphql-handler';
|
||||
import { clickHouseElapsedDuration, clickHouseReadDuration } from './metrics';
|
||||
import { initSupertokens } from './supertokens';
|
||||
|
||||
export async function main() {
|
||||
init({
|
||||
|
|
@ -69,6 +78,7 @@ export async function main() {
|
|||
const server = await createServer({
|
||||
name: 'graphql-api',
|
||||
tracing: true,
|
||||
cors: false,
|
||||
log: {
|
||||
level: env.log.level,
|
||||
requests: env.log.requests,
|
||||
|
|
@ -93,6 +103,31 @@ export async function main() {
|
|||
},
|
||||
);
|
||||
|
||||
server.setErrorHandler(supertokensErrorHandler());
|
||||
await server.register(cors, (_: unknown): FastifyCorsOptionsDelegateCallback => {
|
||||
return (req, callback) => {
|
||||
if (req.headers.origin?.startsWith(env.hiveServices.webApp.url)) {
|
||||
// We need to treat requests from the web app a bit differently than others.
|
||||
// The web app requires to define the `Access-Control-Allow-Origin` header (not *).
|
||||
callback(null, {
|
||||
origin: env.hiveServices.webApp.url,
|
||||
credentials: true,
|
||||
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
||||
allowedHeaders: [
|
||||
'Content-Type',
|
||||
'graphql-client-version',
|
||||
'graphql-client-name',
|
||||
'x-request-id',
|
||||
...supertokens.getAllCORSHeaders(),
|
||||
],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
callback(null, {});
|
||||
};
|
||||
});
|
||||
|
||||
const storage = await createPostgreSQLStorage(createConnectionString(env.postgres), 10);
|
||||
|
||||
let dbPurgeTaskRunner: null | ReturnType<typeof createTaskRunner> = null;
|
||||
|
|
@ -292,9 +327,7 @@ export async function main() {
|
|||
schemaConfig: env.hiveServices.webApp
|
||||
? {
|
||||
schemaPublishLink(input) {
|
||||
let url = `${env.hiveServices.webApp!.url}/${input.organization.cleanId}/${
|
||||
input.project.cleanId
|
||||
}/${input.target.cleanId}`;
|
||||
let url = `${env.hiveServices.webApp.url}/${input.organization.cleanId}/${input.project.cleanId}/${input.target.cleanId}`;
|
||||
|
||||
if (input.version) {
|
||||
url += `/history/${input.version.id}`;
|
||||
|
|
@ -303,9 +336,7 @@ export async function main() {
|
|||
return url;
|
||||
},
|
||||
schemaCheckLink(input) {
|
||||
return `${env.hiveServices.webApp!.url}/${input.organization.cleanId}/${
|
||||
input.project.cleanId
|
||||
}/${input.target.cleanId}/checks/${input.schemaCheckId}`;
|
||||
return `${env.hiveServices.webApp.url}/${input.organization.cleanId}/${input.project.cleanId}/${input.target.cleanId}/checks/${input.schemaCheckId}`;
|
||||
},
|
||||
}
|
||||
: {},
|
||||
|
|
@ -359,17 +390,26 @@ export async function main() {
|
|||
|
||||
const crypto = new CryptoProvider(env.encryptionSecret);
|
||||
|
||||
initSupertokens({
|
||||
storage,
|
||||
crypto,
|
||||
logger: server.log,
|
||||
});
|
||||
|
||||
await server.register(formDataPlugin);
|
||||
await server.register(supertokensFastifyPlugin);
|
||||
|
||||
await registerTRPC(server, {
|
||||
router: internalApiRouter,
|
||||
createContext({ req }) {
|
||||
return createContext({ storage, crypto, req });
|
||||
createContext() {
|
||||
return createContext({ storage, crypto });
|
||||
},
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: ['GET', 'HEAD'],
|
||||
url: '/_health',
|
||||
async handler(req, res) {
|
||||
async handler(_, res) {
|
||||
res.status(200).send(); // eslint-disable-line @typescript-eslint/no-floating-promises -- false positive, FastifyReply.then returns void
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,27 +1,50 @@
|
|||
import { OverrideableBuilder } from 'supertokens-js-override/lib/build';
|
||||
import EmailVerification from 'supertokens-node/recipe/emailverification';
|
||||
import SessionNode from 'supertokens-node/recipe/session';
|
||||
import type { FastifyBaseLogger, FastifyReply, FastifyRequest } from 'fastify';
|
||||
import { CryptoProvider } from 'packages/services/api/src/modules/shared/providers/crypto';
|
||||
import { OverrideableBuilder } from 'supertokens-js-override/lib/build/index.js';
|
||||
import supertokens from 'supertokens-node';
|
||||
import EmailVerification from 'supertokens-node/recipe/emailverification/index.js';
|
||||
import SessionNode from 'supertokens-node/recipe/session/index.js';
|
||||
import type { ProviderInput } from 'supertokens-node/recipe/thirdparty/types';
|
||||
import ThirdPartyEmailPasswordNode from 'supertokens-node/recipe/thirdpartyemailpassword';
|
||||
import { TypeInput as ThirdPartEmailPasswordTypeInput } from 'supertokens-node/recipe/thirdpartyemailpassword/types';
|
||||
import { TypeInput } from 'supertokens-node/types';
|
||||
import { env } from '@/env/backend';
|
||||
import { appInfo } from '@/lib/supertokens/app-info';
|
||||
import ThirdPartyEmailPasswordNode from 'supertokens-node/recipe/thirdpartyemailpassword/index.js';
|
||||
import type { TypeInput as ThirdPartEmailPasswordTypeInput } from 'supertokens-node/recipe/thirdpartyemailpassword/types';
|
||||
import type { TypeInput } from 'supertokens-node/types';
|
||||
import zod from 'zod';
|
||||
import { HiveError, type Storage } from '@hive/api';
|
||||
import type { EmailsApi } from '@hive/emails';
|
||||
import { captureException } from '@sentry/node';
|
||||
import { createTRPCProxyClient, httpLink } from '@trpc/client';
|
||||
import { createInternalApiCaller } from './api';
|
||||
import { env } from './environment';
|
||||
import {
|
||||
createOIDCSuperTokensProvider,
|
||||
getOIDCSuperTokensOverrides,
|
||||
} from '@/lib/supertokens/third-party-email-password-node-oidc-provider';
|
||||
import { createThirdPartyEmailPasswordNodeOktaProvider } from '@/lib/supertokens/third-party-email-password-node-okta-provider';
|
||||
import type { EmailsApi } from '@hive/emails';
|
||||
import type { InternalApi } from '@hive/server';
|
||||
import { createTRPCProxyClient, CreateTRPCProxyClient, httpLink } from '@trpc/client';
|
||||
} from './supertokens/oidc-provider';
|
||||
import { createThirdPartyEmailPasswordNodeOktaProvider } from './supertokens/okta-provider';
|
||||
|
||||
export const backendConfig = (): TypeInput => {
|
||||
const SuperTokenAccessTokenModel = zod.object({
|
||||
version: zod.literal('1'),
|
||||
superTokensUserId: zod.string(),
|
||||
/**
|
||||
* Supertokens for some reason omits externalUserId from the access token payload if it is null.
|
||||
*/
|
||||
externalUserId: zod.optional(zod.union([zod.string(), zod.null()])),
|
||||
email: zod.string(),
|
||||
});
|
||||
|
||||
export type SupertokensSession = zod.TypeOf<typeof SuperTokenAccessTokenModel>;
|
||||
|
||||
export const backendConfig = (requirements: {
|
||||
storage: Storage;
|
||||
crypto: CryptoProvider;
|
||||
logger: FastifyBaseLogger;
|
||||
}): TypeInput => {
|
||||
const { logger } = requirements;
|
||||
const emailsService = createTRPCProxyClient<EmailsApi>({
|
||||
links: [httpLink({ url: `${env.emailsEndpoint}/trpc` })],
|
||||
links: [httpLink({ url: `${env.hiveServices.emails?.endpoint}/trpc` })],
|
||||
});
|
||||
const internalApi = createTRPCProxyClient<InternalApi>({
|
||||
links: [httpLink({ url: `${env.serverEndpoint}/trpc` })],
|
||||
const internalApi = createInternalApiCaller({
|
||||
storage: requirements.storage,
|
||||
crypto: requirements.crypto,
|
||||
});
|
||||
const providers: ProviderInput[] = [];
|
||||
|
||||
|
|
@ -70,12 +93,24 @@ export const backendConfig = (): TypeInput => {
|
|||
);
|
||||
}
|
||||
|
||||
logger.info('SuperTokens providers: %s', providers.map(p => p.config.thirdPartyId).join(', '));
|
||||
logger.info('SuperTokens websiteDomain: %s', env.hiveServices.webApp.url);
|
||||
logger.info('SuperTokens apiDomain: %s', env.graphql.origin);
|
||||
|
||||
return {
|
||||
framework: 'fastify',
|
||||
supertokens: {
|
||||
connectionURI: env.supertokens.connectionUri,
|
||||
connectionURI: env.supertokens.connectionURI,
|
||||
apiKey: env.supertokens.apiKey,
|
||||
},
|
||||
appInfo: appInfo(),
|
||||
appInfo: {
|
||||
// learn more about this on https://supertokens.com/docs/thirdpartyemailpassword/appinfo
|
||||
appName: 'GraphQL Hive',
|
||||
apiDomain: env.graphql.origin,
|
||||
websiteDomain: env.hiveServices.webApp.url,
|
||||
apiBasePath: '/auth-api',
|
||||
websiteBasePath: '/auth',
|
||||
},
|
||||
recipeList: [
|
||||
ThirdPartyEmailPasswordNode.init({
|
||||
providers,
|
||||
|
|
@ -166,7 +201,7 @@ export const backendConfig = (): TypeInput => {
|
|||
};
|
||||
|
||||
const getEnsureUserOverrides = (
|
||||
internalApi: CreateTRPCProxyClient<InternalApi>,
|
||||
internalApi: ReturnType<typeof createInternalApiCaller>,
|
||||
): ThirdPartEmailPasswordTypeInput['override'] => ({
|
||||
apis: originalImplementation => ({
|
||||
...originalImplementation,
|
||||
|
|
@ -178,7 +213,7 @@ const getEnsureUserOverrides = (
|
|||
const response = await originalImplementation.emailPasswordSignUpPOST(input);
|
||||
|
||||
if (response.status === 'OK') {
|
||||
await internalApi.ensureUser.mutate({
|
||||
await internalApi.ensureUser({
|
||||
superTokensUserId: response.user.id,
|
||||
email: response.user.email,
|
||||
oidcIntegrationId: null,
|
||||
|
|
@ -195,7 +230,7 @@ const getEnsureUserOverrides = (
|
|||
const response = await originalImplementation.emailPasswordSignInPOST(input);
|
||||
|
||||
if (response.status === 'OK') {
|
||||
await internalApi.ensureUser.mutate({
|
||||
await internalApi.ensureUser({
|
||||
superTokensUserId: response.user.id,
|
||||
email: response.user.email,
|
||||
oidcIntegrationId: null,
|
||||
|
|
@ -211,7 +246,6 @@ const getEnsureUserOverrides = (
|
|||
|
||||
function extractOidcId(args: typeof input) {
|
||||
if (input.provider.id === 'oidc') {
|
||||
// eslint-disable-next-line prefer-destructuring
|
||||
const oidcId: unknown = args.userContext['oidcId'];
|
||||
if (typeof oidcId === 'string') {
|
||||
return oidcId;
|
||||
|
|
@ -223,7 +257,7 @@ const getEnsureUserOverrides = (
|
|||
const response = await originalImplementation.thirdPartySignInUpPOST(input);
|
||||
|
||||
if (response.status === 'OK') {
|
||||
await internalApi.ensureUser.mutate({
|
||||
await internalApi.ensureUser({
|
||||
superTokensUserId: response.user.id,
|
||||
email: response.user.email,
|
||||
oidcIntegrationId: extractOidcId(input),
|
||||
|
|
@ -291,3 +325,64 @@ const composeSuperTokensOverrides = (
|
|||
return impl;
|
||||
},
|
||||
});
|
||||
|
||||
export function initSupertokens(requirements: {
|
||||
storage: Storage;
|
||||
crypto: CryptoProvider;
|
||||
logger: FastifyBaseLogger;
|
||||
}) {
|
||||
supertokens.init(backendConfig(requirements));
|
||||
}
|
||||
|
||||
export async function resolveUser(ctx: { req: FastifyRequest; reply: FastifyReply }) {
|
||||
let session: SessionNode.SessionContainer | undefined;
|
||||
|
||||
try {
|
||||
session = await SessionNode.getSession(ctx.req, ctx.reply, {
|
||||
sessionRequired: false,
|
||||
antiCsrfCheck: false,
|
||||
checkDatabase: true,
|
||||
});
|
||||
} catch (error) {
|
||||
if (SessionNode.Error.isErrorFromSuperTokens(error)) {
|
||||
// Check whether the email is already verified.
|
||||
// If it is not then we need to redirect to the email verification page - which will trigger the email sending.
|
||||
if (error.type === SessionNode.Error.INVALID_CLAIMS) {
|
||||
throw new HiveError('Your account is not verified. Please verify your email address.', {
|
||||
extensions: {
|
||||
code: 'VERIFY_EMAIL',
|
||||
},
|
||||
});
|
||||
} else if (
|
||||
error.type === SessionNode.Error.TRY_REFRESH_TOKEN ||
|
||||
error.type === SessionNode.Error.UNAUTHORISED
|
||||
) {
|
||||
throw new HiveError('Invalid session', {
|
||||
extensions: {
|
||||
code: 'NEEDS_REFRESH',
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
ctx.req.log.error(error, 'Error while resolving user');
|
||||
captureException(error);
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
const payload = session?.getAccessTokenPayload();
|
||||
|
||||
if (!payload) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const result = SuperTokenAccessTokenModel.safeParse(payload);
|
||||
|
||||
if (result.success === false) {
|
||||
console.error(`SuperTokens session verification failed.\n` + JSON.stringify(payload));
|
||||
throw new HiveError(`Invalid token provided`);
|
||||
}
|
||||
|
||||
return result.data;
|
||||
}
|
||||
|
|
@ -1,13 +1,13 @@
|
|||
import type { ExpressRequest } from 'supertokens-node/lib/build/framework/express/framework';
|
||||
import type { FastifyRequest } from 'supertokens-node/lib/build/framework/fastify/framework';
|
||||
import type { ProviderInput } from 'supertokens-node/recipe/thirdparty/types';
|
||||
import type { TypeInput as ThirdPartEmailPasswordTypeInput } from 'supertokens-node/recipe/thirdpartyemailpassword/types';
|
||||
import zod from 'zod';
|
||||
import { getLogger } from '@/server-logger';
|
||||
import type { InternalApi } from '@hive/server';
|
||||
import type { CreateTRPCProxyClient } from '@trpc/client';
|
||||
import { createInternalApiCaller } from '../api';
|
||||
|
||||
const couldNotResolveOidcIntegrationSymbol = Symbol('could_not_resolve_oidc_integration');
|
||||
|
||||
type InternalApiCaller = ReturnType<typeof createInternalApiCaller>;
|
||||
|
||||
export const getOIDCSuperTokensOverrides = (): ThirdPartEmailPasswordTypeInput['override'] => ({
|
||||
apis(originalImplementation) {
|
||||
return {
|
||||
|
|
@ -27,18 +27,17 @@ export const getOIDCSuperTokensOverrides = (): ThirdPartEmailPasswordTypeInput['
|
|||
});
|
||||
|
||||
export const createOIDCSuperTokensProvider = (args: {
|
||||
internalApi: CreateTRPCProxyClient<InternalApi>;
|
||||
internalApi: InternalApiCaller;
|
||||
}): ProviderInput => ({
|
||||
config: {
|
||||
thirdPartyId: 'oidc',
|
||||
},
|
||||
override(originalImplementation) {
|
||||
const logger = getLogger();
|
||||
return {
|
||||
...originalImplementation,
|
||||
|
||||
async getConfigForClientType(input) {
|
||||
logger.info('resolve config for OIDC provider.');
|
||||
console.info('resolve config for OIDC provider.');
|
||||
const config = await getOIDCConfigFromInput(args.internalApi, input);
|
||||
if (!config) {
|
||||
// In the next step the override `authorisationUrlGET` from `getOIDCSuperTokensOverrides` is called.
|
||||
|
|
@ -66,7 +65,7 @@ export const createOIDCSuperTokensProvider = (args: {
|
|||
},
|
||||
|
||||
async getAuthorisationRedirectURL(input) {
|
||||
logger.info('resolve config for OIDC provider.');
|
||||
console.info('resolve config for OIDC provider.');
|
||||
const oidcConfig = await getOIDCConfigFromInput(args.internalApi, input);
|
||||
|
||||
if (!oidcConfig) {
|
||||
|
|
@ -88,7 +87,7 @@ export const createOIDCSuperTokensProvider = (args: {
|
|||
},
|
||||
|
||||
async getUserInfo(input) {
|
||||
logger.info('retrieve profile info from OIDC provider');
|
||||
console.info('retrieve profile info from OIDC provider');
|
||||
const config = await getOIDCConfigFromInput(args.internalApi, input);
|
||||
if (!config) {
|
||||
// This case should never be reached (guarded by getConfigForClientType).
|
||||
|
|
@ -96,7 +95,7 @@ export const createOIDCSuperTokensProvider = (args: {
|
|||
throw new Error('Could not find OIDC integration.');
|
||||
}
|
||||
|
||||
logger.info('fetch info for OIDC provider (oidcId=%s)', config.id);
|
||||
console.info('fetch info for OIDC provider (oidcId=%s)', config.id);
|
||||
|
||||
const tokenResponse = OIDCTokenSchema.parse(input.oAuthTokens);
|
||||
const rawData: unknown = await fetch(config.userinfoEndpoint, {
|
||||
|
|
@ -107,16 +106,16 @@ export const createOIDCSuperTokensProvider = (args: {
|
|||
},
|
||||
}).then(res => res.json());
|
||||
|
||||
logger.info('retrieved profile info for provider (oidcId=%s)', config.id);
|
||||
console.info('retrieved profile info for provider (oidcId=%s)', config.id);
|
||||
|
||||
const dataParseResult = OIDCProfileInfoSchema.safeParse(rawData);
|
||||
|
||||
if (!dataParseResult.success) {
|
||||
logger.error('Could not parse profile info for OIDC provider (oidcId=%s)', config.id);
|
||||
logger.error('Raw data: %s', JSON.stringify(rawData));
|
||||
logger.error('Error: %s', JSON.stringify(dataParseResult.error));
|
||||
console.error('Could not parse profile info for OIDC provider (oidcId=%s)', config.id);
|
||||
console.error('Raw data: %s', JSON.stringify(rawData));
|
||||
console.error('Error: %s', JSON.stringify(dataParseResult.error));
|
||||
for (const issue of dataParseResult.error.issues) {
|
||||
logger.debug('Issue: %s', JSON.stringify(issue));
|
||||
console.debug('Issue: %s', JSON.stringify(issue));
|
||||
}
|
||||
throw new Error('Could not parse profile info.');
|
||||
}
|
||||
|
|
@ -159,45 +158,40 @@ const OIDCProfileInfoSchema = zod.object({
|
|||
const OIDCTokenSchema = zod.object({ access_token: zod.string() });
|
||||
|
||||
const getOIDCIdFromInput = (input: { userContext: any }): string => {
|
||||
const expressRequest = input.userContext._default.request as ExpressRequest;
|
||||
const originalUrl = 'http://localhost' + expressRequest.getOriginalURL();
|
||||
const fastifyRequest = input.userContext._default.request as FastifyRequest;
|
||||
const originalUrl = 'http://localhost' + fastifyRequest.getOriginalURL();
|
||||
const oidcId = new URL(originalUrl).searchParams.get('oidc_id');
|
||||
|
||||
if (typeof oidcId !== 'string') {
|
||||
const logger = getLogger();
|
||||
logger.error('Invalid OIDC ID sent from client: %s', oidcId);
|
||||
console.error('Invalid OIDC ID sent from client: %s', oidcId);
|
||||
throw new Error('Invalid OIDC ID sent from client.');
|
||||
}
|
||||
|
||||
return oidcId;
|
||||
};
|
||||
|
||||
const configCache = new WeakMap<ExpressRequest, OIDCCOnfig | null>();
|
||||
const configCache = new WeakMap<FastifyRequest, OIDCCOnfig | null>();
|
||||
|
||||
/**
|
||||
* Get cached OIDC config from the supertokens input.
|
||||
*/
|
||||
async function getOIDCConfigFromInput(
|
||||
internalApi: CreateTRPCProxyClient<InternalApi>,
|
||||
input: { userContext: any },
|
||||
) {
|
||||
const expressRequest = input.userContext._default.request as ExpressRequest;
|
||||
if (configCache.has(expressRequest)) {
|
||||
return configCache.get(expressRequest) ?? null;
|
||||
async function getOIDCConfigFromInput(internalApi: InternalApiCaller, input: { userContext: any }) {
|
||||
const fastifyRequest = input.userContext._default.request as FastifyRequest;
|
||||
if (configCache.has(fastifyRequest)) {
|
||||
return configCache.get(fastifyRequest) ?? null;
|
||||
}
|
||||
|
||||
const oidcId = getOIDCIdFromInput(input);
|
||||
const config = await fetchOIDCConfig(internalApi, oidcId);
|
||||
configCache.set(expressRequest, config);
|
||||
configCache.set(fastifyRequest, config);
|
||||
if (!config) {
|
||||
const logger = getLogger();
|
||||
logger.error('Could not find OIDC integration (oidcId: %s)', oidcId);
|
||||
console.error('Could not find OIDC integration (oidcId: %s)', oidcId);
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
const fetchOIDCConfig = async (
|
||||
internalApi: CreateTRPCProxyClient<InternalApi>,
|
||||
internalApi: InternalApiCaller,
|
||||
oidcIntegrationId: string,
|
||||
): Promise<{
|
||||
id: string;
|
||||
|
|
@ -207,10 +201,10 @@ const fetchOIDCConfig = async (
|
|||
userinfoEndpoint: string;
|
||||
authorizationEndpoint: string;
|
||||
} | null> => {
|
||||
const result = await internalApi.getOIDCIntegrationById.query({ oidcIntegrationId });
|
||||
const result = await internalApi.getOIDCIntegrationById({ oidcIntegrationId });
|
||||
if (result === null) {
|
||||
const logger = getLogger();
|
||||
logger.error('OIDC integration not found. (oidcId=%s)', oidcIntegrationId);
|
||||
// TODO: replace console.error with req.log.error
|
||||
console.error('OIDC integration not found. (oidcId=%s)', oidcIntegrationId);
|
||||
return null;
|
||||
}
|
||||
return result;
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import type { ProviderInput } from 'supertokens-node/recipe/thirdparty/types';
|
||||
import zod from 'zod';
|
||||
import { env } from '@/env/backend';
|
||||
import { env } from '../environment';
|
||||
|
||||
type OktaConfig = Exclude<(typeof env)['auth']['okta'], null>;
|
||||
|
||||
|
|
@ -58,7 +58,7 @@ const OktaProfileModel = zod.object({
|
|||
}),
|
||||
});
|
||||
|
||||
const fetchOktaProfile = async (config: OktaConfig, accessToken: string) => {
|
||||
async function fetchOktaProfile(config: OktaConfig, accessToken: string) {
|
||||
const response = await fetch(`${config.endpoint}/api/v1/users/me`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
|
|
@ -74,4 +74,4 @@ const fetchOktaProfile = async (config: OktaConfig, accessToken: string) => {
|
|||
|
||||
const json = await response.json();
|
||||
return OktaProfileModel.parse(json);
|
||||
};
|
||||
}
|
||||
|
|
@ -13,6 +13,7 @@ export async function createServer(options: {
|
|||
requests: boolean;
|
||||
level: string;
|
||||
};
|
||||
cors?: boolean;
|
||||
bodyLimit?: number;
|
||||
}) {
|
||||
const server = fastify({
|
||||
|
|
@ -48,7 +49,9 @@ export async function createServer(options: {
|
|||
await useRequestLogging(server);
|
||||
}
|
||||
|
||||
await server.register(cors);
|
||||
if (options.cors !== false) {
|
||||
await server.register(cors);
|
||||
}
|
||||
|
||||
return server;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,31 +1,21 @@
|
|||
GRAPHQL_ENDPOINT="http://localhost:3001/graphql"
|
||||
APP_BASE_URL="http://localhost:3000"
|
||||
SERVER_ENDPOINT="http://localhost:3001"
|
||||
ENVIRONMENT="development"
|
||||
|
||||
# Supertokens
|
||||
|
||||
SUPERTOKENS_CONNECTION_URI="http://localhost:3567"
|
||||
SUPERTOKENS_API_KEY="bubatzbieber6942096420"
|
||||
# Public GraphQL endpoint
|
||||
GRAPHQL_PUBLIC_ENDPOINT="http://localhost:3001/graphql"
|
||||
GRAPHQL_PUBLIC_ORIGIN="http://localhost:3001"
|
||||
|
||||
# Auth Provider
|
||||
|
||||
## Enable GitHub login
|
||||
AUTH_GITHUB="<sync>"
|
||||
AUTH_GITHUB_CLIENT_ID="<sync>"
|
||||
AUTH_GITHUB_CLIENT_SECRET="<sync>"
|
||||
|
||||
## Enable Google login
|
||||
AUTH_GOOGLE="<sync>"
|
||||
AUTH_GOOGLE_CLIENT_ID="<sync>"
|
||||
AUTH_GOOGLE_CLIENT_SECRET="<sync>"
|
||||
|
||||
## Enable Okta login
|
||||
AUTH_OKTA=0
|
||||
AUTH_OKTA_HIDDEN=0
|
||||
AUTH_OKTA_ENDPOINT="<sync>"
|
||||
AUTH_OKTA_CLIENT_ID="<sync>"
|
||||
AUTH_OKTA_CLIENT_SECRET="<sync>"
|
||||
|
||||
## Organization level Open ID Connect Authentication
|
||||
AUTH_ORGANIZATION_OIDC=0
|
||||
|
|
@ -41,7 +31,3 @@ INTEGRATION_GITHUB_APP_NAME="<sync>"
|
|||
|
||||
# Stripe
|
||||
STRIPE_PUBLIC_KEY="<sync>"
|
||||
|
||||
# Emails Service
|
||||
|
||||
EMAILS_ENDPOINT=http://localhost:6260
|
||||
|
|
|
|||
3
packages/web/app/.gitignore
vendored
3
packages/web/app/.gitignore
vendored
|
|
@ -30,6 +30,9 @@ npm-debug.log*
|
|||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# next-env-runtime
|
||||
public/__ENV.js
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
|
|
|
|||
|
|
@ -9,27 +9,17 @@ The following environment variables configure the application.
|
|||
| Name | Required | Description | Example Value |
|
||||
| --------------------------------------- | ------------------------------------------ | --------------------------------------------------------------------------------------------- | ---------------------------------------------------- |
|
||||
| `APP_BASE_URL` | **Yes** | The base url of the app, | `https://app.graphql-hive.com` |
|
||||
| `SERVER_ENDPOINT` | **Yes** | The endpoint of the Hive server. | `http://127.0.0.1:4000` |
|
||||
| `GRAPHQL_ENDPOINT` | **Yes** | The endpoint of the Hive GraphQL API. | `http://127.0.0.1:4000/graphql` |
|
||||
| `EMAILS_ENDPOINT` | **Yes** | The endpoint of the GraphQL Hive Email service. | `http://127.0.0.1:6260` |
|
||||
| `SUPERTOKENS_CONNECTION_URI` | **Yes** | The URI of the SuperTokens instance. | `http://127.0.0.1:3567` |
|
||||
| `SUPERTOKENS_API_KEY` | **Yes** | The API KEY of the SuperTokens instance. | `iliketurtlesandicannotlie` |
|
||||
| `GRAPHQL_PUBLIC_ENDPOINT` | **Yes** | The public endpoint of the Hive GraphQL API. | `http://127.0.0.1:4000/graphql` |
|
||||
| `GRAPHQL_PUBLIC_ORIGIN` | **Yes** | The http address origin of the Hive GraphQL server. | `http://127.0.0.1:4000/` |
|
||||
| `INTEGRATION_SLACK` | No | Whether the Slack integration is enabled or disabled. | `1` (enabled) or `0` (disabled) |
|
||||
| `INTEGRATION_SLACK_SLACK_CLIENT_ID` | No (**Yes** if `INTEGRATION_SLACK` is set) | The Slack client ID. | `g6aff8102efda5e1d12e` |
|
||||
| `INTEGRATION_SLACK_SLACK_CLIENT_SECRET` | No (**Yes** if `INTEGRATION_SLACK` is set) | The Slack client secret. | `g12e552xx54xx2b127821dc4abc4491dxxxa6b187` |
|
||||
| `INTEGRATION_GITHUB_APP_NAME` | No | The GitHub application name. | `graphql-hive-self-hosted` |
|
||||
| `AUTH_GITHUB` | No | Whether login via GitHub should be allowed | `1` (enabled) or `0` (disabled) |
|
||||
| `AUTH_GITHUB_CLIENT_ID` | No (**Yes** if `AUTH_GITHUB` is set) | The GitHub client ID. | `g6aff8102efda5e1d12e` |
|
||||
| `AUTH_GITHUB_CLIENT_SECRET` | No (**Yes** if `AUTH_GITHUB` is set) | The GitHub client secret. | `g12e552xx54xx2b127821dc4abc4491dxxxa6b187` |
|
||||
| `AUTH_GOOGLE` | No | Whether login via Google should be allowed | `1` (enabled) or `0` (disabled) |
|
||||
| `AUTH_GOOGLE_CLIENT_ID` | No (**Yes** if `AUTH_GOOGLE` is set) | The Google client ID. | `g6aff8102efda5e1d12e` |
|
||||
| `AUTH_GOOGLE_CLIENT_SECRET` | No (**Yes** if `AUTH_GOOGLE` is set) | The Google client secret. | `g12e552xx54xx2b127821dc4abc4491dxxxa6b187` |
|
||||
| `AUTH_ORGANIZATION_OIDC` | No | Whether linking a Hive organization to an Open ID Connect provider is allowed. (Default: `0`) | `1` (enabled) or `0` (disabled) |
|
||||
| `AUTH_OKTA` | No | Whether login via Okta should be allowed | `1` (enabled) or `0` (disabled) |
|
||||
| `AUTH_OKTA_CLIENT_ENDPOINT` | No (**Yes** if `AUTH_OKTA` is set) | The Okta endpoint. | `https://dev-1234567.okta.com` |
|
||||
| `AUTH_OKTA_HIDDEN` | No | Whether the Okta login button should be hidden. (Default: `0`) | `1` (enabled) or `0` (disabled) |
|
||||
| `AUTH_OKTA_CLIENT_ID` | No (**Yes** if `AUTH_OKTA` is set) | The Okta client ID. | `g6aff8102efda5e1d12e` |
|
||||
| `AUTH_OKTA_CLIENT_SECRET` | No (**Yes** if `AUTH_OKTA` is set) | The Okta client secret. | `g12e552xx54xx2b127821dc4abc4491dxxxa6b187` |
|
||||
| `AUTH_REQUIRE_EMAIL_VERIFICATION` | No | Whether verifying the email address is mandatory. | `1` (enabled) or `0` (disabled) |
|
||||
| `ENVIRONMENT` | No | The environment of your Hive app. (**Note:** This will be used for Sentry reporting.) | `staging` |
|
||||
| `SENTRY_DSN` | No | The DSN for reporting errors to Sentry. | `https://dooobars@o557896.ingest.sentry.io/12121212` |
|
||||
|
|
@ -37,7 +27,7 @@ The following environment variables configure the application.
|
|||
| `DOCS_URL` | No | The URL of the Hive Docs | `https://the-guild.dev/graphql/hive/docs` |
|
||||
| `NODE_ENV` | No | The `NODE_ENV` value. | `production` |
|
||||
| `GA_TRACKING_ID` | No | The token for Google Analytics in order to track user actions. | `g6aff8102efda5e1d12e` |
|
||||
| `GRAPHQL_PERSISTED_OPERATIONS` | No | Send persisted oepration hashes instead of documents to the |
|
||||
| `GRAPHQL_PERSISTED_OPERATIONS` | No | Send persisted operation hashes instead of documents to the |
|
||||
| server. | `1` (enabled) or `0` (disabled) |
|
||||
|
||||
## Hive Hosted Configuration
|
||||
|
|
|
|||
|
|
@ -1,250 +1,33 @@
|
|||
import zod from 'zod';
|
||||
import * as Sentry from '@sentry/nextjs';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { ALLOWED_ENVIRONMENT_VARIABLES } from './src/env/frontend-public-variables';
|
||||
|
||||
console.log('🌲 Loading environment variables...');
|
||||
//
|
||||
// Runtime environment in Next.js
|
||||
//
|
||||
|
||||
// Weird hacky way of getting the Sentry.Integrations object
|
||||
// When the nextjs config is loaded by Next CLI Sentry has `Integrations` property.
|
||||
// When nextjs starts and the `environment.js` is loaded, the Sentry object doesn't have the `Integrations` property, it' under `Sentry.default` property.
|
||||
// Dealing with esm/cjs/default exports is a pain, we all feel that pain...
|
||||
const Integrations =
|
||||
'default' in Sentry
|
||||
? ((Sentry as any).default as typeof Sentry).Integrations
|
||||
: Sentry.Integrations;
|
||||
configureRuntimeEnv(ALLOWED_ENVIRONMENT_VARIABLES);
|
||||
|
||||
// treat an empty string `''` as `undefined`
|
||||
const emptyString = <T extends zod.ZodType>(input: T) => {
|
||||
return zod.preprocess((value: unknown) => {
|
||||
if (value === '') return undefined;
|
||||
return value;
|
||||
}, input);
|
||||
};
|
||||
|
||||
const BaseSchema = zod.object({
|
||||
NODE_ENV: zod.string(),
|
||||
ENVIRONMENT: zod.string(),
|
||||
APP_BASE_URL: zod.string().url(),
|
||||
GRAPHQL_ENDPOINT: zod.string().url(),
|
||||
SERVER_ENDPOINT: zod.string().url(),
|
||||
EMAILS_ENDPOINT: zod.string().url(),
|
||||
SUPERTOKENS_CONNECTION_URI: zod.string().url(),
|
||||
SUPERTOKENS_API_KEY: zod.string(),
|
||||
INTEGRATION_GITHUB_APP_NAME: emptyString(zod.string().optional()),
|
||||
GA_TRACKING_ID: emptyString(zod.string().optional()),
|
||||
DOCS_URL: emptyString(zod.string().url().optional()),
|
||||
STRIPE_PUBLIC_KEY: emptyString(zod.string().optional()),
|
||||
RELEASE: emptyString(zod.string().optional()),
|
||||
AUTH_REQUIRE_EMAIL_VERIFICATION: emptyString(
|
||||
zod.union([zod.literal('1'), zod.literal('0')]).optional(),
|
||||
),
|
||||
GRAPHQL_PERSISTED_OPERATIONS: emptyString(
|
||||
zod.union([zod.literal('1'), zod.literal('0')]).optional(),
|
||||
),
|
||||
ZENDESK_SUPPORT: emptyString(zod.union([zod.literal('1'), zod.literal('0')]).optional()),
|
||||
});
|
||||
|
||||
const IntegrationSlackSchema = zod.union([
|
||||
zod.object({
|
||||
INTEGRATION_SLACK: emptyString(zod.literal('0').optional()),
|
||||
}),
|
||||
zod.object({
|
||||
INTEGRATION_SLACK: zod.literal('1'),
|
||||
INTEGRATION_SLACK_CLIENT_ID: zod.string(),
|
||||
INTEGRATION_SLACK_CLIENT_SECRET: zod.string(),
|
||||
}),
|
||||
]);
|
||||
|
||||
const AuthGitHubConfigSchema = zod.union([
|
||||
zod.object({
|
||||
AUTH_GITHUB: zod.union([zod.void(), zod.literal('0'), zod.literal('')]),
|
||||
}),
|
||||
zod.object({
|
||||
AUTH_GITHUB: zod.literal('1'),
|
||||
AUTH_GITHUB_CLIENT_ID: zod.string(),
|
||||
AUTH_GITHUB_CLIENT_SECRET: zod.string(),
|
||||
}),
|
||||
]);
|
||||
|
||||
const AuthGoogleConfigSchema = zod.union([
|
||||
zod.object({
|
||||
AUTH_GOOGLE: zod.union([zod.void(), zod.literal('0'), zod.literal('')]),
|
||||
}),
|
||||
zod.object({
|
||||
AUTH_GOOGLE: zod.literal('1'),
|
||||
AUTH_GOOGLE_CLIENT_ID: zod.string(),
|
||||
AUTH_GOOGLE_CLIENT_SECRET: zod.string(),
|
||||
}),
|
||||
]);
|
||||
|
||||
const AuthOktaConfigSchema = zod.union([
|
||||
zod.object({
|
||||
AUTH_OKTA: zod.union([zod.void(), zod.literal('0')]),
|
||||
}),
|
||||
zod.object({
|
||||
AUTH_OKTA: zod.literal('1'),
|
||||
AUTH_OKTA_HIDDEN: emptyString(zod.union([zod.literal('1'), zod.literal('0')]).optional()),
|
||||
AUTH_OKTA_ENDPOINT: zod.string().url(),
|
||||
AUTH_OKTA_CLIENT_ID: zod.string(),
|
||||
AUTH_OKTA_CLIENT_SECRET: zod.string(),
|
||||
}),
|
||||
]);
|
||||
|
||||
const AuthOktaMultiTenantSchema = zod.object({
|
||||
AUTH_ORGANIZATION_OIDC: emptyString(zod.union([zod.literal('1'), zod.literal('0')]).optional()),
|
||||
});
|
||||
|
||||
const SentryConfigSchema = zod.union([
|
||||
zod.object({
|
||||
SENTRY: zod.union([zod.void(), zod.literal('0')]),
|
||||
}),
|
||||
zod.object({
|
||||
SENTRY: zod.literal('1'),
|
||||
SENTRY_DSN: zod.string(),
|
||||
}),
|
||||
]);
|
||||
|
||||
const MigrationsSchema = zod.object({
|
||||
MEMBER_ROLES_DEADLINE: emptyString(
|
||||
zod
|
||||
.date({
|
||||
coerce: true,
|
||||
})
|
||||
.optional(),
|
||||
),
|
||||
});
|
||||
|
||||
const configs = {
|
||||
// Writes the environment variables to public/__ENV.js file and make them accessible under `window.__ENV`
|
||||
function configureRuntimeEnv(publicEnvVars: readonly string[]) {
|
||||
const envObject: Record<string, unknown> = {};
|
||||
// eslint-disable-next-line no-process-env
|
||||
base: BaseSchema.safeParse(process.env),
|
||||
// eslint-disable-next-line no-process-env
|
||||
integrationSlack: IntegrationSlackSchema.safeParse(process.env),
|
||||
// eslint-disable-next-line no-process-env
|
||||
sentry: SentryConfigSchema.safeParse(process.env),
|
||||
// eslint-disable-next-line no-process-env
|
||||
authGithub: AuthGitHubConfigSchema.safeParse(process.env),
|
||||
// eslint-disable-next-line no-process-env
|
||||
authGoogle: AuthGoogleConfigSchema.safeParse(process.env),
|
||||
// eslint-disable-next-line no-process-env
|
||||
authOkta: AuthOktaConfigSchema.safeParse(process.env),
|
||||
// eslint-disable-next-line no-process-env
|
||||
authOktaMultiTenant: AuthOktaMultiTenantSchema.safeParse(process.env),
|
||||
// eslint-disable-next-line no-process-env
|
||||
migrations: MigrationsSchema.safeParse(process.env),
|
||||
};
|
||||
const processEnv = process.env;
|
||||
|
||||
const environmentErrors: Array<string> = [];
|
||||
|
||||
for (const config of Object.values(configs)) {
|
||||
if (config.success === false) {
|
||||
environmentErrors.push(JSON.stringify(config.error.format(), null, 4));
|
||||
for (const key in processEnv) {
|
||||
if (publicEnvVars.includes(key)) {
|
||||
envObject[key] = processEnv[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (environmentErrors.length) {
|
||||
const fullError = environmentErrors.join('\n');
|
||||
console.error('❌ Invalid environment variables:', fullError);
|
||||
process.exit(1);
|
||||
}
|
||||
const base = fs.realpathSync(process.cwd());
|
||||
const file = `${base}/public/__ENV.js`;
|
||||
const content = `window.__ENV = ${JSON.stringify(envObject)};`;
|
||||
const dirname = path.dirname(file);
|
||||
|
||||
function extractConfig<Input, Output>(config: zod.SafeParseReturnType<Input, Output>): Output {
|
||||
if (!config.success) {
|
||||
throw new Error('Something went wrong.');
|
||||
if (!fs.existsSync(dirname)) {
|
||||
fs.mkdirSync(dirname, { recursive: true });
|
||||
}
|
||||
return config.data;
|
||||
|
||||
fs.writeFileSync(file, content);
|
||||
}
|
||||
|
||||
const base = extractConfig(configs.base);
|
||||
const integrationSlack = extractConfig(configs.integrationSlack);
|
||||
const sentry = extractConfig(configs.sentry);
|
||||
const authGithub = extractConfig(configs.authGithub);
|
||||
const authGoogle = extractConfig(configs.authGoogle);
|
||||
const authOkta = extractConfig(configs.authOkta);
|
||||
const authOktaMultiTenant = extractConfig(configs.authOktaMultiTenant);
|
||||
const migrations = extractConfig(configs.migrations);
|
||||
|
||||
const config = {
|
||||
release: base.RELEASE ?? 'local',
|
||||
nodeEnv: base.NODE_ENV,
|
||||
environment: base.ENVIRONMENT,
|
||||
appBaseUrl: base.APP_BASE_URL.replace(/\/$/, ''),
|
||||
graphqlEndpoint: base.GRAPHQL_ENDPOINT,
|
||||
serverEndpoint: base.SERVER_ENDPOINT,
|
||||
emailsEndpoint: base.EMAILS_ENDPOINT,
|
||||
supertokens: {
|
||||
connectionUri: base.SUPERTOKENS_CONNECTION_URI,
|
||||
apiKey: base.SUPERTOKENS_API_KEY,
|
||||
},
|
||||
slack:
|
||||
integrationSlack.INTEGRATION_SLACK === '1'
|
||||
? {
|
||||
clientId: integrationSlack.INTEGRATION_SLACK_CLIENT_ID,
|
||||
clientSecret: integrationSlack.INTEGRATION_SLACK_CLIENT_SECRET,
|
||||
}
|
||||
: null,
|
||||
github: base.INTEGRATION_GITHUB_APP_NAME ? { appName: base.INTEGRATION_GITHUB_APP_NAME } : null,
|
||||
analytics: {
|
||||
googleAnalyticsTrackingId: base.GA_TRACKING_ID,
|
||||
},
|
||||
docsUrl: base.DOCS_URL,
|
||||
auth: {
|
||||
github:
|
||||
authGithub.AUTH_GITHUB === '1'
|
||||
? {
|
||||
clientId: authGithub.AUTH_GITHUB_CLIENT_ID,
|
||||
clientSecret: authGithub.AUTH_GITHUB_CLIENT_SECRET,
|
||||
}
|
||||
: null,
|
||||
google:
|
||||
authGoogle.AUTH_GOOGLE === '1'
|
||||
? {
|
||||
clientId: authGoogle.AUTH_GOOGLE_CLIENT_ID,
|
||||
clientSecret: authGoogle.AUTH_GOOGLE_CLIENT_SECRET,
|
||||
}
|
||||
: null,
|
||||
okta:
|
||||
authOkta.AUTH_OKTA === '1'
|
||||
? {
|
||||
endpoint: authOkta.AUTH_OKTA_ENDPOINT,
|
||||
hidden: authOkta.AUTH_OKTA_HIDDEN === '1',
|
||||
clientId: authOkta.AUTH_OKTA_CLIENT_ID,
|
||||
clientSecret: authOkta.AUTH_OKTA_CLIENT_SECRET,
|
||||
}
|
||||
: null,
|
||||
organizationOIDC: authOktaMultiTenant.AUTH_ORGANIZATION_OIDC === '1',
|
||||
requireEmailVerification: base.AUTH_REQUIRE_EMAIL_VERIFICATION === '1',
|
||||
},
|
||||
sentry: sentry.SENTRY === '1' ? { dsn: sentry.SENTRY_DSN } : null,
|
||||
stripePublicKey: base.STRIPE_PUBLIC_KEY ?? null,
|
||||
graphql: {
|
||||
persistedOperations: base.GRAPHQL_PERSISTED_OPERATIONS === '1',
|
||||
},
|
||||
zendeskSupport: base.ZENDESK_SUPPORT === '1',
|
||||
migrations: {
|
||||
member_roles_deadline: migrations.MEMBER_ROLES_DEADLINE ?? null,
|
||||
},
|
||||
} as const;
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line no-var
|
||||
var __backend_env: typeof config;
|
||||
}
|
||||
|
||||
globalThis['__backend_env'] = config;
|
||||
|
||||
// TODO: I don't like this here, but it seems like it makes most sense here :)
|
||||
Sentry.init({
|
||||
serverName: 'app',
|
||||
dist: 'app',
|
||||
enabled: !!config.sentry,
|
||||
enableTracing: false,
|
||||
tracesSampleRate: 1,
|
||||
dsn: config.sentry?.dsn,
|
||||
release: config.release,
|
||||
environment: config.environment,
|
||||
integrations: [
|
||||
// HTTP integration is only available on the server
|
||||
new Integrations.Http({
|
||||
tracing: false,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,10 +1,5 @@
|
|||
import bundleAnalyzer from '@next/bundle-analyzer';
|
||||
|
||||
// For the dev server we want to make sure that the correct environment variables are set :)
|
||||
// during build we don't need environment variables!
|
||||
if (globalThis.process.env.BUILD !== '1') {
|
||||
await import('./environment');
|
||||
}
|
||||
import './environment';
|
||||
|
||||
const withBundleAnalyzer = bundleAnalyzer({
|
||||
enabled: globalThis.process.env.ANALYZE === '1',
|
||||
|
|
|
|||
|
|
@ -4,12 +4,12 @@
|
|||
"private": true,
|
||||
"scripts": {
|
||||
"analyze": "pnpm build:config && ANALYZE=1 next build",
|
||||
"build": "pnpm generate-changelog && pnpm build:config && BUILD=1 next build && tsx ../../../scripts/runify.ts",
|
||||
"build": "pnpm generate-changelog && pnpm build:config && next build && tsx ../../../scripts/runify.ts",
|
||||
"build-storybook": "storybook build",
|
||||
"build:config": "tsup-node --no-splitting --out-dir . --loader \".mts=ts\" --format esm --target node21 next.config.mts",
|
||||
"dev": "pnpm generate-changelog && pnpm build:config && next dev | pino-pretty",
|
||||
"generate-changelog": "node ../../../scripts/generate-changelog.js",
|
||||
"postbuild": "pnpm --filter @hive/app deploy --prod --no-optional deploy-tmp/web && rimraf dist/node_modules && mv deploy-tmp/web/node_modules dist && rimraf deploy-tmp/web",
|
||||
"postbuild": "rimraf deploy-tmp/web && pnpm --filter @hive/app deploy --prod --no-optional deploy-tmp/web && rimraf dist/node_modules && mv deploy-tmp/web/node_modules dist && rimraf deploy-tmp/web",
|
||||
"postinstall": "pnpm generate-changelog",
|
||||
"start": "node dist/index.js",
|
||||
"storybook": "storybook dev -p 6006",
|
||||
|
|
@ -53,6 +53,7 @@
|
|||
"@trpc/client": "10.45.2",
|
||||
"@trpc/server": "10.45.2",
|
||||
"@urql/core": "4.1.4",
|
||||
"@urql/exchange-auth": "2.1.6",
|
||||
"@urql/exchange-graphcache": "6.3.3",
|
||||
"@urql/exchange-persisted": "4.1.0",
|
||||
"class-variance-authority": "0.7.0",
|
||||
|
|
@ -93,8 +94,6 @@
|
|||
"regenerator-runtime": "0.14.1",
|
||||
"snarkdown": "2.0.0",
|
||||
"supertokens-auth-react": "0.35.6",
|
||||
"supertokens-js-override": "0.0.4",
|
||||
"supertokens-node": "15.2.1",
|
||||
"supertokens-web-js": "0.8.0",
|
||||
"tailwind-merge": "2.2.2",
|
||||
"tslib": "2.6.2",
|
||||
|
|
|
|||
|
|
@ -37,7 +37,6 @@ import { FragmentType, graphql, useFragment } from '@/gql';
|
|||
import { CriticalityLevel } from '@/gql/graphql';
|
||||
import { ProjectType } from '@/graphql';
|
||||
import { useRouteSelector } from '@/lib/hooks';
|
||||
import { withSessionProtection } from '@/lib/supertokens/guard';
|
||||
import { cn } from '@/lib/utils';
|
||||
import {
|
||||
CheckIcon,
|
||||
|
|
@ -166,7 +165,7 @@ const Navigation = (props: {
|
|||
</NextLink>
|
||||
{edge.node.githubRepository && edge.node.meta ? (
|
||||
<a
|
||||
className="ml-[-1px] text-xs font-medium text-gray-500 hover:text-gray-400"
|
||||
className="-ml-px text-xs font-medium text-gray-500 hover:text-gray-400"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
href={`https://github.com/${edge.node.githubRepository}/commit/${edge.node.meta.commit}`}
|
||||
|
|
@ -1492,6 +1491,4 @@ function ChecksPage() {
|
|||
);
|
||||
}
|
||||
|
||||
export const getServerSideProps = withSessionProtection();
|
||||
|
||||
export default authenticated(ChecksPage);
|
||||
|
|
|
|||
|
|
@ -1,5 +1 @@
|
|||
import { withSessionProtection } from '@/lib/supertokens/guard';
|
||||
|
||||
export { default } from '../checks';
|
||||
|
||||
export const getServerSideProps = withSessionProtection();
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ import { MetaTitle } from '@/components/v2';
|
|||
import { noSchemaVersion, noValidSchemaVersion } from '@/components/v2/empty-list';
|
||||
import { FragmentType, graphql, useFragment } from '@/gql';
|
||||
import { useRouteSelector } from '@/lib/hooks';
|
||||
import { withSessionProtection } from '@/lib/supertokens/guard';
|
||||
|
||||
const ExplorerPage_SchemaExplorerFragment = graphql(`
|
||||
fragment ExplorerPage_SchemaExplorerFragment on SchemaExplorer {
|
||||
|
|
@ -161,6 +160,9 @@ function ExplorerPageContent() {
|
|||
}
|
||||
}, [setDataRetentionInDays, retentionInDays]);
|
||||
|
||||
/* to avoid janky behaviour we keep track if the version has a successful explorer once, and in that case always show the filter bar. */
|
||||
const isFilterVisible = useRef(false);
|
||||
|
||||
if (query.error) {
|
||||
return <QueryError error={query.error} />;
|
||||
}
|
||||
|
|
@ -173,9 +175,6 @@ function ExplorerPageContent() {
|
|||
const latestSchemaVersion = currentTarget?.latestSchemaVersion;
|
||||
const latestValidSchemaVersion = currentTarget?.latestValidSchemaVersion;
|
||||
|
||||
/* to avoid janky behaviour we keep track if the version has a successful explorer once, and in that case always show the filter bar. */
|
||||
const isFilterVisible = useRef(false);
|
||||
|
||||
if (latestValidSchemaVersion?.explorer) {
|
||||
isFilterVisible.current = true;
|
||||
}
|
||||
|
|
@ -280,6 +279,4 @@ function ExplorerPage(): ReactElement {
|
|||
);
|
||||
}
|
||||
|
||||
export const getServerSideProps = withSessionProtection();
|
||||
|
||||
export default authenticated(ExplorerPage);
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ import { MetaTitle } from '@/components/v2';
|
|||
import { noSchemaVersion } from '@/components/v2/empty-list';
|
||||
import { FragmentType, graphql, useFragment } from '@/gql';
|
||||
import { useRouteSelector } from '@/lib/hooks/use-route-selector';
|
||||
import { withSessionProtection } from '@/lib/supertokens/guard';
|
||||
|
||||
export const TypeRenderFragment = graphql(`
|
||||
fragment TypeRenderFragment on GraphQLNamedType {
|
||||
|
|
@ -256,6 +255,4 @@ function TypeExplorerPage() {
|
|||
);
|
||||
}
|
||||
|
||||
export const getServerSideProps = withSessionProtection();
|
||||
|
||||
export default authenticated(TypeExplorerPage);
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ import { EmptyList, noSchemaVersion } from '@/components/v2/empty-list';
|
|||
import { FragmentType, graphql, useFragment } from '@/gql';
|
||||
import { useRouteSelector } from '@/lib/hooks';
|
||||
import { useDateRangeController } from '@/lib/hooks/use-date-range-controller';
|
||||
import { withSessionProtection } from '@/lib/supertokens/guard';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { TypeRenderer, TypeRenderFragment } from './[typename]';
|
||||
|
||||
|
|
@ -382,6 +381,4 @@ function UnusedSchemaExplorerPage(): ReactElement {
|
|||
);
|
||||
}
|
||||
|
||||
export const getServerSideProps = withSessionProtection();
|
||||
|
||||
export default authenticated(UnusedSchemaExplorerPage);
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ import { DiffIcon } from '@/components/v2/icon';
|
|||
import { FragmentType, graphql, useFragment } from '@/gql';
|
||||
import { CriticalityLevel, ProjectType } from '@/graphql';
|
||||
import { useRouteSelector } from '@/lib/hooks/use-route-selector';
|
||||
import { withSessionProtection } from '@/lib/supertokens/guard';
|
||||
import { cn } from '@/lib/utils';
|
||||
import {
|
||||
CheckCircledIcon,
|
||||
|
|
@ -164,7 +163,7 @@ function ListPage({
|
|||
</NextLink>
|
||||
{version.githubMetadata ? (
|
||||
<a
|
||||
className="ml-[-1px] text-xs font-medium text-gray-500 hover:text-gray-400"
|
||||
className="-ml-px text-xs font-medium text-gray-500 hover:text-gray-400"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
href={`https://github.com/${version.githubMetadata.repository}/commit/${version.githubMetadata.commit}`}
|
||||
|
|
@ -884,6 +883,4 @@ function HistoryPage(): ReactElement {
|
|||
);
|
||||
}
|
||||
|
||||
export const getServerSideProps = withSessionProtection();
|
||||
|
||||
export default authenticated(HistoryPage);
|
||||
|
|
|
|||
|
|
@ -1,5 +1 @@
|
|||
import { withSessionProtection } from '@/lib/supertokens/guard';
|
||||
|
||||
export { default } from '../history';
|
||||
|
||||
export const getServerSideProps = withSessionProtection();
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ import { DocumentType, FragmentType, graphql, useFragment } from '@/gql';
|
|||
import { ProjectType, RegistryModel } from '@/graphql';
|
||||
import { TargetAccessScope, useTargetAccess } from '@/lib/access/target';
|
||||
import { useRouteSelector } from '@/lib/hooks';
|
||||
import { withSessionProtection } from '@/lib/supertokens/guard';
|
||||
|
||||
type CompositeSchema = Extract<
|
||||
DocumentType<typeof SchemaView_SchemaFragment>,
|
||||
|
|
@ -361,6 +360,4 @@ function SchemaPage(): ReactElement {
|
|||
);
|
||||
}
|
||||
|
||||
export const getServerSideProps = withSessionProtection();
|
||||
|
||||
export default authenticated(SchemaPage);
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ import { EmptyList, MetaTitle } from '@/components/v2';
|
|||
import { graphql } from '@/gql';
|
||||
import { useRouteSelector } from '@/lib/hooks';
|
||||
import { useDateRangeController } from '@/lib/hooks/use-date-range-controller';
|
||||
import { withSessionProtection } from '@/lib/supertokens/guard';
|
||||
|
||||
function OperationsView({
|
||||
organizationCleanId,
|
||||
|
|
@ -187,6 +186,4 @@ function InsightsPage(): ReactElement {
|
|||
);
|
||||
}
|
||||
|
||||
export const getServerSideProps = withSessionProtection();
|
||||
|
||||
export default authenticated(InsightsPage);
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ import { EmptyList, Link, MetaTitle } from '@/components/v2';
|
|||
import { FragmentType, graphql, useFragment } from '@/gql';
|
||||
import { useRouteSelector } from '@/lib/hooks';
|
||||
import { useDateRangeController } from '@/lib/hooks/use-date-range-controller';
|
||||
import { withSessionProtection } from '@/lib/supertokens/guard';
|
||||
|
||||
const GraphQLOperationBody_OperationFragment = graphql(`
|
||||
fragment GraphQLOperationBody_OperationFragment on Operation {
|
||||
|
|
@ -265,6 +264,4 @@ function OperationInsightsPage(): ReactElement {
|
|||
);
|
||||
}
|
||||
|
||||
export const getServerSideProps = withSessionProtection();
|
||||
|
||||
export default authenticated(OperationInsightsPage);
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ import { CHART_PRIMARY_COLOR } from '@/constants';
|
|||
import { graphql } from '@/gql';
|
||||
import { formatNumber, formatThroughput, toDecimal, useRouteSelector } from '@/lib/hooks';
|
||||
import { useDateRangeController } from '@/lib/hooks/use-date-range-controller';
|
||||
import { withSessionProtection } from '@/lib/supertokens/guard';
|
||||
import { useChartStyles } from '@/utils';
|
||||
|
||||
const ClientView_ClientStatsQuery = graphql(`
|
||||
|
|
@ -442,6 +441,4 @@ function ClientInsightsPage(): ReactElement {
|
|||
);
|
||||
}
|
||||
|
||||
export const getServerSideProps = withSessionProtection();
|
||||
|
||||
export default authenticated(ClientInsightsPage);
|
||||
|
|
|
|||
|
|
@ -26,7 +26,6 @@ import { CHART_PRIMARY_COLOR } from '@/constants';
|
|||
import { graphql } from '@/gql';
|
||||
import { formatNumber, formatThroughput, toDecimal, useRouteSelector } from '@/lib/hooks';
|
||||
import { useDateRangeController } from '@/lib/hooks/use-date-range-controller';
|
||||
import { withSessionProtection } from '@/lib/supertokens/guard';
|
||||
import { useChartStyles } from '@/utils';
|
||||
|
||||
const SchemaCoordinateView_SchemaCoordinateStatsQuery = graphql(`
|
||||
|
|
@ -496,6 +495,4 @@ function SchemaCoordinatePage(): ReactElement {
|
|||
);
|
||||
}
|
||||
|
||||
export const getServerSideProps = withSessionProtection();
|
||||
|
||||
export default authenticated(SchemaCoordinatePage);
|
||||
|
|
|
|||
|
|
@ -28,7 +28,6 @@ import { graphql } from '@/gql';
|
|||
import { TargetAccessScope } from '@/gql/graphql';
|
||||
import { canAccessTarget } from '@/lib/access/target';
|
||||
import { useClipboard, useNotifications, useRouteSelector, useToggle } from '@/lib/hooks';
|
||||
import { withSessionProtection } from '@/lib/supertokens/guard';
|
||||
import { cn } from '@/lib/utils';
|
||||
import {
|
||||
Button as GraphiQLButton,
|
||||
|
|
@ -824,6 +823,7 @@ function LaboratoryPageContent() {
|
|||
(actualSelectedApiEndpoint === 'linkedApi'
|
||||
? query.data?.target?.graphqlEndpointUrl
|
||||
: undefined) ?? mockEndpoint,
|
||||
fetch,
|
||||
});
|
||||
|
||||
const result = await fetcher(params, opts);
|
||||
|
|
@ -1052,8 +1052,6 @@ function LaboratoryPage(): ReactElement {
|
|||
);
|
||||
}
|
||||
|
||||
export const getServerSideProps = withSessionProtection();
|
||||
|
||||
export default authenticated(LaboratoryPage);
|
||||
|
||||
function useApiTabValueState(graphqlEndpointUrl: string | null) {
|
||||
|
|
|
|||
|
|
@ -42,7 +42,6 @@ import { ProjectType } from '@/gql/graphql';
|
|||
import { canAccessTarget, TargetAccessScope } from '@/lib/access/target';
|
||||
import { subDays } from '@/lib/date-time';
|
||||
import { useRouteSelector, useToggle } from '@/lib/hooks';
|
||||
import { withSessionProtection } from '@/lib/supertokens/guard';
|
||||
|
||||
const SetTargetValidationMutation = graphql(`
|
||||
mutation Settings_SetTargetValidation($input: SetTargetValidationInput!) {
|
||||
|
|
@ -1060,6 +1059,4 @@ function SettingsPage(): ReactElement {
|
|||
);
|
||||
}
|
||||
|
||||
export const getServerSideProps = withSessionProtection();
|
||||
|
||||
export default authenticated(SettingsPage);
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ import { FragmentType, graphql, useFragment } from '@/gql';
|
|||
import { subDays } from '@/lib/date-time';
|
||||
import { useFormattedNumber } from '@/lib/hooks';
|
||||
import { useRouteSelector } from '@/lib/hooks/use-route-selector';
|
||||
import { withSessionProtection } from '@/lib/supertokens/guard';
|
||||
import { cn, pluralize } from '@/lib/utils';
|
||||
|
||||
function floorDate(date: Date): Date {
|
||||
|
|
@ -383,6 +382,4 @@ function ProjectsPage(): ReactElement {
|
|||
);
|
||||
}
|
||||
|
||||
export const getServerSideProps = withSessionProtection();
|
||||
|
||||
export default authenticated(ProjectsPage);
|
||||
|
|
|
|||
|
|
@ -30,7 +30,6 @@ import { DocsLink, MetaTitle } from '@/components/v2';
|
|||
import { FragmentType, graphql, useFragment } from '@/gql';
|
||||
import { ProjectAccessScope, useProjectAccess } from '@/lib/access/project';
|
||||
import { useRouteSelector, useToggle } from '@/lib/hooks';
|
||||
import { withSessionProtection } from '@/lib/supertokens/guard';
|
||||
|
||||
function Channels(props: {
|
||||
channels: FragmentType<typeof ChannelsTable_AlertChannelFragment>[];
|
||||
|
|
@ -265,6 +264,4 @@ function AlertsPage(): ReactElement {
|
|||
);
|
||||
}
|
||||
|
||||
export const getServerSideProps = withSessionProtection();
|
||||
|
||||
export default authenticated(AlertsPage);
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ import { ProjectAccessScope } from '@/gql/graphql';
|
|||
import { RegistryModel } from '@/graphql';
|
||||
import { useProjectAccess } from '@/lib/access/project';
|
||||
import { useRouteSelector } from '@/lib/hooks';
|
||||
import { withSessionProtection } from '@/lib/supertokens/guard';
|
||||
|
||||
const ProjectPolicyPageQuery = graphql(`
|
||||
query ProjectPolicyPageQuery($organizationId: ID!, $projectId: ID!) {
|
||||
|
|
@ -206,6 +205,4 @@ function ProjectPolicyPage(): ReactElement {
|
|||
);
|
||||
}
|
||||
|
||||
export const getServerSideProps = withSessionProtection();
|
||||
|
||||
export default authenticated(ProjectPolicyPage);
|
||||
|
|
|
|||
|
|
@ -28,7 +28,6 @@ import { ProjectType } from '@/graphql';
|
|||
import { canAccessProject, ProjectAccessScope, useProjectAccess } from '@/lib/access/project';
|
||||
import { getDocsUrl } from '@/lib/docs-url';
|
||||
import { useNotifications, useRouteSelector, useToggle } from '@/lib/hooks';
|
||||
import { withSessionProtection } from '@/lib/supertokens/guard';
|
||||
|
||||
const GithubIntegration_GithubIntegrationDetailsQuery = graphql(`
|
||||
query getGitHubIntegrationDetails($selector: OrganizationSelectorInput!) {
|
||||
|
|
@ -107,7 +106,7 @@ function GitHubIntegration(props: {
|
|||
<div className="flex items-center gap-x-2 pl-1">
|
||||
<CheckIcon className="size-4 text-emerald-500" />
|
||||
<div className="flex size-6 items-center justify-center rounded-sm bg-white">
|
||||
<HiveLogo className="size-[80%]" />
|
||||
<HiveLogo className="size-4/5" />
|
||||
</div>
|
||||
|
||||
<div className="font-semibold text-[#adbac7]">
|
||||
|
|
@ -123,7 +122,7 @@ function GitHubIntegration(props: {
|
|||
<div className="flex items-center gap-x-2 pl-1">
|
||||
<CheckIcon className="size-4 text-emerald-500" />
|
||||
<div className="flex size-6 items-center justify-center rounded-sm bg-white">
|
||||
<HiveLogo className="size-[80%]" />
|
||||
<HiveLogo className="size-4/5" />
|
||||
</div>
|
||||
|
||||
<div className="font-semibold text-[#adbac7]">
|
||||
|
|
@ -414,6 +413,4 @@ function SettingsPage() {
|
|||
);
|
||||
}
|
||||
|
||||
export const getServerSideProps = withSessionProtection();
|
||||
|
||||
export default authenticated(SettingsPage);
|
||||
|
|
|
|||
|
|
@ -19,11 +19,9 @@ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/comp
|
|||
import { Activities, Card, EmptyList, MetaTitle } from '@/components/v2';
|
||||
import { FragmentType, graphql, useFragment } from '@/gql';
|
||||
import { ProjectType } from '@/gql/graphql';
|
||||
import { writeLastVisitedOrganization } from '@/lib/cookies';
|
||||
import { subDays } from '@/lib/date-time';
|
||||
import { useFormattedNumber } from '@/lib/hooks';
|
||||
import { useRouteSelector } from '@/lib/hooks/use-route-selector';
|
||||
import { withSessionProtection } from '@/lib/supertokens/guard';
|
||||
import { pluralize } from '@/lib/utils';
|
||||
|
||||
function floorDate(date: Date): Date {
|
||||
|
|
@ -403,9 +401,4 @@ function OrganizationPage(): ReactElement {
|
|||
);
|
||||
}
|
||||
|
||||
export const getServerSideProps = withSessionProtection(async ({ req, res, resolvedUrl }) => {
|
||||
writeLastVisitedOrganization(req, res, resolvedUrl.substring(1));
|
||||
return { props: {} };
|
||||
});
|
||||
|
||||
export default authenticated(OrganizationPage);
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ import { BillingPlanType } from '@/gql/graphql';
|
|||
import { OrganizationAccessScope, useOrganizationAccess } from '@/lib/access/organization';
|
||||
import { getIsStripeEnabled } from '@/lib/billing/stripe-public-key';
|
||||
import { useRouteSelector } from '@/lib/hooks';
|
||||
import { withSessionProtection } from '@/lib/supertokens/guard';
|
||||
import { CardElement, useElements, useStripe } from '@stripe/react-stripe-js';
|
||||
|
||||
const ManageSubscriptionInner_OrganizationFragment = graphql(`
|
||||
|
|
@ -168,6 +167,16 @@ function Inner(props: {
|
|||
[setOperationsRateLimit],
|
||||
);
|
||||
|
||||
const isFetching =
|
||||
updateOrgRateLimitMutationState.fetching ||
|
||||
downgradeToHobbyMutationState.fetching ||
|
||||
upgradeToProMutationState.fetching;
|
||||
|
||||
const billingPlans = useFragment(
|
||||
ManageSubscriptionInner_BillingPlansFragment,
|
||||
props.billingPlans,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (query.data?.billingPlans?.length) {
|
||||
if (organization.plan === plan) {
|
||||
|
|
@ -182,15 +191,6 @@ function Inner(props: {
|
|||
}
|
||||
}, [organization.plan, organization.rateLimit.operations, plan, query.data?.billingPlans]);
|
||||
|
||||
if (!canAccess) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isFetching =
|
||||
updateOrgRateLimitMutationState.fetching ||
|
||||
downgradeToHobbyMutationState.fetching ||
|
||||
upgradeToProMutationState.fetching;
|
||||
|
||||
const upgrade = useCallback(async () => {
|
||||
if (isFetching) {
|
||||
return;
|
||||
|
|
@ -262,6 +262,10 @@ function Inner(props: {
|
|||
});
|
||||
}, [organization.cleanId, operationsRateLimit, updateOrgRateLimitMutation, isFetching]);
|
||||
|
||||
if (!canAccess) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const renderActions = () => {
|
||||
if (plan === organization.plan) {
|
||||
if (organization.rateLimit.operations !== operationsRateLimit * 1_000_000) {
|
||||
|
|
@ -312,11 +316,6 @@ function Inner(props: {
|
|||
downgradeToHobbyMutationState.error ||
|
||||
updateOrgRateLimitMutationState.error;
|
||||
|
||||
const billingPlans = useFragment(
|
||||
ManageSubscriptionInner_BillingPlansFragment,
|
||||
props.billingPlans,
|
||||
);
|
||||
|
||||
// TODO: this is also not safe as billingPlans might be an empty list.
|
||||
const selectedPlan = billingPlans.find(v => v.planType === plan) ?? billingPlans[0];
|
||||
|
||||
|
|
@ -441,6 +440,20 @@ const ManageSubscriptionPageQuery = graphql(`
|
|||
|
||||
function ManageSubscriptionPageContent() {
|
||||
const router = useRouteSelector();
|
||||
|
||||
/**
|
||||
* If Stripe is not enabled we redirect the user to the organization.
|
||||
*/
|
||||
if (!getIsStripeEnabled()) {
|
||||
void router.push({
|
||||
pathname: '/[organizationId]',
|
||||
query: {
|
||||
organizationId: router.organizationId,
|
||||
},
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
const [query] = useQuery({
|
||||
query: ManageSubscriptionPageQuery,
|
||||
variables: {
|
||||
|
|
@ -523,22 +536,4 @@ function ManageSubscriptionPage(): ReactElement {
|
|||
);
|
||||
}
|
||||
|
||||
export const getServerSideProps = withSessionProtection(async context => {
|
||||
/**
|
||||
* If Stripe is not enabled we redirect the user to the organization.
|
||||
*/
|
||||
const isStripeEnabled = getIsStripeEnabled();
|
||||
if (!isStripeEnabled) {
|
||||
const parts = String(context.resolvedUrl).split('/');
|
||||
parts.pop();
|
||||
return {
|
||||
redirect: {
|
||||
destination: parts.join('/'),
|
||||
permanent: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
return { props: {} };
|
||||
});
|
||||
|
||||
export default authenticated(ManageSubscriptionPage);
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ import { MetaTitle } from '@/components/v2';
|
|||
import { FragmentType, graphql, useFragment } from '@/gql';
|
||||
import { OrganizationAccessScope, useOrganizationAccess } from '@/lib/access/organization';
|
||||
import { useRouteSelector } from '@/lib/hooks/use-route-selector';
|
||||
import { withSessionProtection } from '@/lib/supertokens/guard';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const OrganizationMembersPage_OrganizationFragment = graphql(`
|
||||
|
|
@ -225,6 +224,4 @@ function OrganizationMembersPage() {
|
|||
);
|
||||
}
|
||||
|
||||
export const getServerSideProps = withSessionProtection();
|
||||
|
||||
export default authenticated(OrganizationMembersPage);
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ import { OrganizationAccessScope } from '@/gql/graphql';
|
|||
import { RegistryModel } from '@/graphql';
|
||||
import { useOrganizationAccess } from '@/lib/access/organization';
|
||||
import { useRouteSelector } from '@/lib/hooks';
|
||||
import { withSessionProtection } from '@/lib/supertokens/guard';
|
||||
|
||||
const OrganizationPolicyPageQuery = graphql(`
|
||||
query OrganizationPolicyPageQuery($selector: OrganizationSelectorInput!) {
|
||||
|
|
@ -223,6 +222,4 @@ function OrganizationPolicyPage(): ReactElement {
|
|||
);
|
||||
}
|
||||
|
||||
export const getServerSideProps = withSessionProtection();
|
||||
|
||||
export default authenticated(OrganizationPolicyPage);
|
||||
|
|
|
|||
|
|
@ -31,7 +31,6 @@ import {
|
|||
useOrganizationAccess,
|
||||
} from '@/lib/access/organization';
|
||||
import { useRouteSelector, useToggle } from '@/lib/hooks';
|
||||
import { withSessionProtection } from '@/lib/supertokens/guard';
|
||||
|
||||
const Integrations_CheckIntegrationsQuery = graphql(`
|
||||
query Integrations_CheckIntegrationsQuery($selector: OrganizationSelectorInput!) {
|
||||
|
|
@ -479,6 +478,4 @@ function OrganizationSettingsPage(): ReactElement {
|
|||
);
|
||||
}
|
||||
|
||||
export const getServerSideProps = withSessionProtection();
|
||||
|
||||
export default authenticated(OrganizationSettingsPage);
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ import { graphql, useFragment } from '@/gql';
|
|||
import { OrganizationAccessScope, useOrganizationAccess } from '@/lib/access/organization';
|
||||
import { getIsStripeEnabled } from '@/lib/billing/stripe-public-key';
|
||||
import { useRouteSelector } from '@/lib/hooks';
|
||||
import { withSessionProtection } from '@/lib/supertokens/guard';
|
||||
|
||||
const DateFormatter = Intl.DateTimeFormat('en-US', {
|
||||
month: 'short',
|
||||
|
|
@ -75,6 +74,17 @@ const SubscriptionPageQuery = graphql(`
|
|||
|
||||
function SubscriptionPageContent() {
|
||||
const router = useRouteSelector();
|
||||
|
||||
if (!getIsStripeEnabled()) {
|
||||
void router.push({
|
||||
pathname: '/[organizationId]',
|
||||
query: {
|
||||
organizationId: router.organizationId,
|
||||
},
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
const [query] = useQuery({
|
||||
query: SubscriptionPageQuery,
|
||||
variables: {
|
||||
|
|
@ -200,22 +210,4 @@ function SubscriptionPage(): ReactElement {
|
|||
);
|
||||
}
|
||||
|
||||
export const getServerSideProps = withSessionProtection(async context => {
|
||||
/**
|
||||
* If Stripe is not enabled we redirect the user to the organization.
|
||||
*/
|
||||
const isStripeEnabled = getIsStripeEnabled();
|
||||
if (!isStripeEnabled) {
|
||||
const parts = String(context.resolvedUrl).split('/');
|
||||
parts.pop();
|
||||
return {
|
||||
redirect: {
|
||||
destination: parts.join('/'),
|
||||
permanent: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
return { props: {} };
|
||||
});
|
||||
|
||||
export default authenticated(SubscriptionPage);
|
||||
|
|
|
|||
|
|
@ -45,7 +45,6 @@ import { FragmentType, graphql, useFragment } from '@/gql';
|
|||
import { SupportTicketPriority, SupportTicketStatus } from '@/gql/graphql';
|
||||
import { OrganizationAccessScope, useOrganizationAccess } from '@/lib/access/organization';
|
||||
import { useNotifications, useRouteSelector, useToggle } from '@/lib/hooks';
|
||||
import { withSessionProtection } from '@/lib/supertokens/guard';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
|
||||
|
|
@ -457,6 +456,4 @@ function SupportPage() {
|
|||
);
|
||||
}
|
||||
|
||||
export const getServerSideProps = withSessionProtection();
|
||||
|
||||
export default authenticated(SupportPage);
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ import { MetaTitle } from '@/components/v2';
|
|||
import { FragmentType, graphql, useFragment } from '@/gql';
|
||||
import { OrganizationAccessScope, useOrganizationAccess } from '@/lib/access/organization';
|
||||
import { useNotifications, useRouteSelector } from '@/lib/hooks';
|
||||
import { withSessionProtection } from '@/lib/supertokens/guard';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
|
||||
|
|
@ -366,6 +365,4 @@ function SupportTicketPage() {
|
|||
);
|
||||
}
|
||||
|
||||
export const getServerSideProps = withSessionProtection();
|
||||
|
||||
export default authenticated(SupportTicketPage);
|
||||
|
|
|
|||
|
|
@ -1,25 +1,17 @@
|
|||
import Document, { DocumentContext, Head, Html, Main, NextScript } from 'next/document';
|
||||
import 'regenerator-runtime/runtime';
|
||||
// don't remove this import ; it will break the built app ; but not the dev app :)
|
||||
import '@/config/frontend-env';
|
||||
|
||||
export default class MyDocument extends Document<{
|
||||
ids: Array<string>;
|
||||
css: string;
|
||||
frontendEnv: (typeof import('@/config/frontend-env'))['env'];
|
||||
}> {
|
||||
static async getInitialProps(ctx: DocumentContext) {
|
||||
if (globalThis.process.env.BUILD !== '1') {
|
||||
await import('../environment');
|
||||
}
|
||||
const { env: frontendEnv } = await import('@/config/frontend-env');
|
||||
const initialProps = await Document.getInitialProps(ctx);
|
||||
const page = await ctx.renderPage();
|
||||
|
||||
return {
|
||||
...initialProps,
|
||||
...page,
|
||||
frontendEnv,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -37,14 +29,8 @@ export default class MyDocument extends Document<{
|
|||
<link rel="preconnect" href="https://rsms.me/" />
|
||||
<link rel="stylesheet" href="https://rsms.me/inter/inter.css" />
|
||||
<link rel="icon" href="/just-logo.svg" type="image/svg+xml" />
|
||||
<script
|
||||
type="module"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `globalThis.__frontend_env = ${JSON.stringify(
|
||||
(this.props as any).frontendEnv,
|
||||
)}`,
|
||||
}}
|
||||
/>
|
||||
{/* eslint-disable-next-line @next/next/no-sync-scripts -- if it's not sync, then env variables are not present) */}
|
||||
<script src="/__ENV.js" />
|
||||
</Head>
|
||||
<body className="bg-transparent font-sans text-white">
|
||||
<Main />
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import { Button, DataWrapper } from '@/components/v2';
|
|||
import { graphql } from '@/gql';
|
||||
import { useNotifications } from '@/lib/hooks/use-notifications';
|
||||
import { useRouteSelector } from '@/lib/hooks/use-route-selector';
|
||||
import { withSessionProtection } from '@/lib/supertokens/guard';
|
||||
|
||||
const classes = {
|
||||
title: clsx('sm:text-4xl text-3xl mb-4 font-medium text-white'),
|
||||
|
|
@ -148,6 +147,4 @@ function OrganizationTransferPage() {
|
|||
);
|
||||
}
|
||||
|
||||
export const getServerSideProps = withSessionProtection();
|
||||
|
||||
export default authenticated(OrganizationTransferPage);
|
||||
|
|
|
|||
|
|
@ -1,38 +0,0 @@
|
|||
import { NextApiRequest, NextApiResponse } from 'next';
|
||||
import type { Request, Response } from 'express';
|
||||
import NextCors from 'nextjs-cors';
|
||||
import supertokens from 'supertokens-node';
|
||||
import { middleware } from 'supertokens-node/framework/express';
|
||||
import { superTokensNextWrapper } from 'supertokens-node/nextjs';
|
||||
import { backendConfig } from '@/config/supertokens/backend';
|
||||
import { env } from '@/env/backend';
|
||||
|
||||
supertokens.init(backendConfig());
|
||||
|
||||
/**
|
||||
* Route for proxying to the underlying SuperTokens backend.
|
||||
*/
|
||||
export default async function superTokens(
|
||||
req: NextApiRequest & Request,
|
||||
res: NextApiResponse & Response,
|
||||
) {
|
||||
// NOTE: We need CORS only if we are querying the APIs from a different origin
|
||||
await NextCors(req, res, {
|
||||
methods: ['GET', 'HEAD', 'PUT', 'PATCH', 'POST', 'DELETE'],
|
||||
origin: env.appBaseUrl,
|
||||
credentials: true,
|
||||
allowedHeaders: ['content-type', ...supertokens.getAllCORSHeaders()],
|
||||
});
|
||||
|
||||
await superTokensNextWrapper(
|
||||
async next => {
|
||||
await middleware()(req, res, next);
|
||||
},
|
||||
req,
|
||||
res,
|
||||
);
|
||||
|
||||
if (!res.writableEnded) {
|
||||
res.status(404).send('Not found');
|
||||
}
|
||||
}
|
||||
|
|
@ -11,7 +11,7 @@ export async function ensureGithubIntegration(
|
|||
) {
|
||||
const { orgId, installationId } = input;
|
||||
await graphql({
|
||||
url: `${env.appBaseUrl}/api/proxy`,
|
||||
url: env.graphqlPublicEndpoint,
|
||||
headers: {
|
||||
...req.headers,
|
||||
'content-type': 'application/json',
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ export default async function githubSetupCallback(req: NextApiRequest, res: Next
|
|||
cleanId: string;
|
||||
};
|
||||
}>({
|
||||
url: `${env.appBaseUrl}/api/proxy`,
|
||||
url: env.graphqlPublicEndpoint,
|
||||
headers: {
|
||||
...req.headers,
|
||||
'content-type': 'application/json',
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { NextApiRequest, NextApiResponse } from 'next';
|
||||
|
||||
export default async function graphql(req: NextApiRequest, res: NextApiResponse) {
|
||||
export default async function health(req: NextApiRequest, res: NextApiResponse) {
|
||||
res.status(200).json({});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,48 +0,0 @@
|
|||
import { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { getLogger } from '@/server-logger';
|
||||
import { captureException } from '@sentry/nextjs';
|
||||
|
||||
async function joinWaitingList(req: NextApiRequest, res: NextApiResponse) {
|
||||
const logger = getLogger(req);
|
||||
|
||||
function success(message: string) {
|
||||
res.status(200).json({
|
||||
ok: true,
|
||||
message,
|
||||
});
|
||||
}
|
||||
|
||||
function failure(message: string) {
|
||||
res.status(200).json({
|
||||
ok: false,
|
||||
message,
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
logger.info('Joining the waiting list (input=%o)', req.body);
|
||||
|
||||
if (req.body.email) {
|
||||
await fetch(`https://guild-ms-slack-bot.vercel.app/api/hive?email=${req.body.email}`, {
|
||||
method: 'GET',
|
||||
});
|
||||
} else {
|
||||
return failure('Missing email');
|
||||
}
|
||||
|
||||
return success('Thank you for joining our waiting list!');
|
||||
} catch (error) {
|
||||
captureException(error);
|
||||
logger.error(`Failed to join waiting list: ${error}`);
|
||||
logger.error(error);
|
||||
return failure('Failed to join. Try again or contact@the-guild.dev');
|
||||
}
|
||||
}
|
||||
|
||||
export default joinWaitingList;
|
||||
|
||||
export const config = {
|
||||
api: {
|
||||
externalResolver: true,
|
||||
},
|
||||
};
|
||||
|
|
@ -1,13 +1,13 @@
|
|||
import { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { buildSchema, execute, GraphQLError, parse } from 'graphql';
|
||||
import { env } from '@/env/backend';
|
||||
import { extractAccessTokenFromRequest } from '@/lib/api/extract-access-token-from-request';
|
||||
import { getLogger } from '@/server-logger';
|
||||
import { addMocksToSchema } from '@graphql-tools/mock';
|
||||
|
||||
// TODO: check if lab is working
|
||||
async function lab(req: NextApiRequest, res: NextApiResponse) {
|
||||
const logger = getLogger(req);
|
||||
const url = env.graphqlEndpoint;
|
||||
const url = env.graphqlPublicEndpoint;
|
||||
const labParams = req.query.lab || [];
|
||||
|
||||
if (labParams.length < 3) {
|
||||
|
|
@ -22,24 +22,9 @@ async function lab(req: NextApiRequest, res: NextApiResponse) {
|
|||
const headers: Record<string, string> = {};
|
||||
|
||||
if (req.headers['x-hive-key']) {
|
||||
// TODO: change that to Authorization: Bearer
|
||||
headers['X-API-Token'] = req.headers['x-hive-key'] as string;
|
||||
headers['Authorization'] = `Bearer ${req.headers['x-hive-key'] as string}`;
|
||||
} else {
|
||||
try {
|
||||
const accessToken = await extractAccessTokenFromRequest(req, res);
|
||||
|
||||
if (!accessToken) {
|
||||
throw 'Invalid Token!';
|
||||
}
|
||||
|
||||
headers['Authorization'] = `Bearer ${accessToken}`;
|
||||
} catch (error) {
|
||||
console.warn('Lab auth failed:', error);
|
||||
res.status(200).send({
|
||||
errors: [new GraphQLError('Invalid or missing X-Hive-Key authentication')],
|
||||
});
|
||||
return;
|
||||
}
|
||||
headers['Cookie'] = req.headers.cookie as string;
|
||||
}
|
||||
|
||||
const body = {
|
||||
|
|
@ -72,6 +57,7 @@ async function lab(req: NextApiRequest, res: NextApiResponse) {
|
|||
'graphql-client-version': env.release,
|
||||
...headers,
|
||||
},
|
||||
credentials: 'include',
|
||||
method: 'POST',
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,106 +0,0 @@
|
|||
import { NextApiRequest, NextApiResponse } from 'next';
|
||||
import hyperid from 'hyperid';
|
||||
import { env } from '@/env/backend';
|
||||
import { extractAccessTokenFromRequest } from '@/lib/api/extract-access-token-from-request';
|
||||
import { getLogger } from '@/server-logger';
|
||||
import { captureException } from '@sentry/nextjs';
|
||||
|
||||
const reqIdGenerate = hyperid({ fixedLength: true });
|
||||
async function graphql(req: NextApiRequest, res: NextApiResponse) {
|
||||
const logger = getLogger(req);
|
||||
const url = env.graphqlEndpoint;
|
||||
|
||||
const requestIdHeader = req.headers['x-request-id'];
|
||||
const requestId = Array.isArray(requestIdHeader)
|
||||
? requestIdHeader[0]
|
||||
: requestIdHeader ?? reqIdGenerate();
|
||||
|
||||
if (req.method === 'GET') {
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
'content-type': req.headers['content-type'],
|
||||
accept: req.headers['accept'],
|
||||
'accept-encoding': req.headers['accept-encoding'],
|
||||
'x-request-id': requestId,
|
||||
authorization: req.headers.authorization,
|
||||
// We need that to be backwards compatible with the new Authorization header format
|
||||
'X-API-Token': req.headers['x-api-token'] ?? '',
|
||||
'graphql-client-name': 'Hive App',
|
||||
'x-use-proxy': '/api/proxy',
|
||||
'graphql-client-version': env.release,
|
||||
},
|
||||
method: 'GET',
|
||||
} as any);
|
||||
return res.send(await response.text());
|
||||
}
|
||||
|
||||
let accessToken: string | undefined;
|
||||
|
||||
try {
|
||||
accessToken = await extractAccessTokenFromRequest(req, res);
|
||||
} catch (error) {
|
||||
captureException(error);
|
||||
logger.error(error);
|
||||
}
|
||||
|
||||
// For convenience, we allow to pass the access token in the Authorization header
|
||||
// in development mode to avoid spinning up multiple proxy servers when testing integrations
|
||||
if (env.nodeEnv === 'development' && !accessToken && req.headers['authorization']) {
|
||||
accessToken = req.headers['authorization'].replace('Bearer ', '');
|
||||
}
|
||||
|
||||
if (!accessToken) {
|
||||
res.status(401).json({});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
'content-type': req.headers['content-type'],
|
||||
accept: req.headers['accept'],
|
||||
'accept-encoding': req.headers['accept-encoding'],
|
||||
'x-request-id': requestId,
|
||||
'X-API-Token': req.headers['x-api-token'] ?? '',
|
||||
'graphql-client-name': 'Hive App',
|
||||
'graphql-client-version': env.release,
|
||||
},
|
||||
method: 'POST',
|
||||
body: JSON.stringify(req.body || {}),
|
||||
} as any);
|
||||
|
||||
const xRequestId = response.headers.get('x-request-id');
|
||||
if (xRequestId) {
|
||||
res.setHeader('x-request-id', xRequestId);
|
||||
}
|
||||
const parsedData = await response.json();
|
||||
|
||||
res.status(200).json(parsedData);
|
||||
} catch (error) {
|
||||
captureException(error);
|
||||
logger.error(error);
|
||||
|
||||
// TODO: better type narrowing of the error
|
||||
const status = (error as Record<string, number | undefined>)?.['status'] ?? 500;
|
||||
const code = (error as Record<string, unknown | undefined>)?.['code'] ?? '';
|
||||
const message = (error as Record<string, unknown | undefined>)?.['message'] ?? '';
|
||||
|
||||
res.setHeader('x-request-id', requestId);
|
||||
res.status(status).json({
|
||||
code,
|
||||
error: message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default graphql;
|
||||
|
||||
export const config = {
|
||||
api: {
|
||||
bodyParser: {
|
||||
sizeLimit: '10mb',
|
||||
},
|
||||
externalResolver: true,
|
||||
},
|
||||
};
|
||||
|
|
@ -49,7 +49,7 @@ export default async function slackCallback(req: NextApiRequest, res: NextApiRes
|
|||
const token = slackResponse.access_token;
|
||||
|
||||
await graphql({
|
||||
url: `${env.appBaseUrl}/api/proxy`,
|
||||
url: env.graphqlPublicEndpoint,
|
||||
headers: {
|
||||
...req.headers,
|
||||
'content-type': 'application/json',
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import { ReactElement } from 'react';
|
||||
import { ReactElement, useEffect, useState } from 'react';
|
||||
import { GraphiQL } from 'graphiql';
|
||||
import { HiveLogo } from '@/components/v2/icon';
|
||||
import { createGraphiQLFetcher } from '@graphiql/toolkit';
|
||||
import 'graphiql/graphiql.css';
|
||||
import { env } from '@/env/frontend';
|
||||
|
||||
export default function DevPage(): ReactElement {
|
||||
return (
|
||||
|
|
@ -13,13 +14,27 @@ export default function DevPage(): ReactElement {
|
|||
--color-primary: 40, 89%, 60%;
|
||||
}
|
||||
`}</style>
|
||||
{process.browser && (
|
||||
<GraphiQL fetcher={createGraphiQLFetcher({ url: `${location.origin}/api/proxy` })}>
|
||||
<GraphiQL.Logo>
|
||||
<HiveLogo className="size-6" />
|
||||
</GraphiQL.Logo>
|
||||
</GraphiQL>
|
||||
)}
|
||||
<Editor />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Editor() {
|
||||
const [isClient, setIsClient] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setIsClient(true);
|
||||
}, []);
|
||||
|
||||
if (!isClient) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<GraphiQL fetcher={createGraphiQLFetcher({ url: env.graphqlPublicEndpoint })}>
|
||||
<GraphiQL.Logo>
|
||||
<HiveLogo className="size-6" />
|
||||
</GraphiQL.Logo>
|
||||
</GraphiQL>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,104 +1,50 @@
|
|||
import { ReactElement, useEffect } from 'react';
|
||||
import { NextApiRequest, NextApiResponse } from 'next';
|
||||
import Cookies from 'cookies';
|
||||
import Session from 'supertokens-node/recipe/session';
|
||||
import { useQuery } from 'urql';
|
||||
import { authenticated } from '@/components/authenticated-container';
|
||||
import { Title } from '@/components/common';
|
||||
import { DataWrapper } from '@/components/v2';
|
||||
import { LAST_VISITED_ORG_KEY } from '@/constants';
|
||||
import { env } from '@/env/backend';
|
||||
import { QueryError } from '@/components/ui/query-error';
|
||||
import { HiveLogo } from '@/components/v2/icon';
|
||||
import { graphql } from '@/gql';
|
||||
import { writeLastVisitedOrganization } from '@/lib/cookies';
|
||||
import { useRouteSelector } from '@/lib/hooks/use-route-selector';
|
||||
import { withSessionProtection } from '@/lib/supertokens/guard';
|
||||
import { getLogger } from '@/server-logger';
|
||||
import { type InternalApi } from '@hive/server';
|
||||
import { createTRPCProxyClient, httpLink } from '@trpc/client';
|
||||
import { useLastVisitedOrganizationWriter } from '@/lib/last-visited-org';
|
||||
|
||||
async function getSuperTokensUserIdFromRequest(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse,
|
||||
): Promise<string | null> {
|
||||
const session = await Session.getSession(req, res, { sessionRequired: false });
|
||||
return session?.getUserId() ?? null;
|
||||
}
|
||||
|
||||
export const getServerSideProps = withSessionProtection(async ({ req, res }) => {
|
||||
const logger = getLogger(req);
|
||||
const internalApi = createTRPCProxyClient<InternalApi>({
|
||||
links: [httpLink({ url: `${env.serverEndpoint}/trpc` })],
|
||||
});
|
||||
|
||||
const superTokensId = await getSuperTokensUserIdFromRequest(req as any, res as any);
|
||||
try {
|
||||
const cookies = new Cookies(req, res);
|
||||
const lastOrgIdInCookies = cookies.get(LAST_VISITED_ORG_KEY) ?? null;
|
||||
|
||||
if (superTokensId) {
|
||||
const defaultOrganization = await internalApi.getDefaultOrgForUser.query({
|
||||
superTokensUserId: superTokensId,
|
||||
lastOrgId: lastOrgIdInCookies,
|
||||
});
|
||||
|
||||
if (defaultOrganization) {
|
||||
writeLastVisitedOrganization(req, res, defaultOrganization.cleanId);
|
||||
|
||||
return {
|
||||
redirect: {
|
||||
destination: `/${defaultOrganization.cleanId}`,
|
||||
permanent: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
redirect: {
|
||||
destination: '/org/new',
|
||||
permanent: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
}
|
||||
|
||||
return {
|
||||
props: {},
|
||||
};
|
||||
});
|
||||
|
||||
export const OrganizationsQuery = graphql(`
|
||||
query organizations {
|
||||
organizations {
|
||||
nodes {
|
||||
...OrganizationFields
|
||||
export const DefaultOrganizationQuery = graphql(`
|
||||
query myDefaultOrganization($previouslyVisitedOrganizationId: ID) {
|
||||
myDefaultOrganization(previouslyVisitedOrganizationId: $previouslyVisitedOrganizationId) {
|
||||
organization {
|
||||
id
|
||||
cleanId
|
||||
}
|
||||
total
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
||||
function Home(): ReactElement {
|
||||
const [query] = useQuery({ query: OrganizationsQuery });
|
||||
const [query] = useQuery({ query: DefaultOrganizationQuery });
|
||||
const router = useRouteSelector();
|
||||
const result = query.data?.myDefaultOrganization;
|
||||
|
||||
useLastVisitedOrganizationWriter(result?.organization?.cleanId);
|
||||
useEffect(() => {
|
||||
// Just in case server-side redirect wasn't working
|
||||
if (query.data) {
|
||||
const org = query.data.organizations.nodes[0];
|
||||
if (result === null) {
|
||||
// No organization, redirect to create one
|
||||
void router.push('/org/new');
|
||||
} else if (result?.organization.cleanId) {
|
||||
// Redirect to the organization
|
||||
void router.visitOrganization({ organizationId: result.organization.cleanId });
|
||||
} // else, still loading
|
||||
}, [router, result]);
|
||||
|
||||
if (org) {
|
||||
void router.visitOrganization({ organizationId: org.cleanId });
|
||||
}
|
||||
}
|
||||
}, [router, query.data]);
|
||||
if (query.error) {
|
||||
return <QueryError error={query.error} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Title title="Home" />
|
||||
<DataWrapper query={query}>{() => <></>}</DataWrapper>
|
||||
<div className="flex size-full flex-row items-center justify-center">
|
||||
<HiveLogo className="size-16 animate-pulse" />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import { Button, DataWrapper } from '@/components/v2';
|
|||
import { graphql } from '@/gql';
|
||||
import { useNotifications } from '@/lib/hooks/use-notifications';
|
||||
import { useRouteSelector } from '@/lib/hooks/use-route-selector';
|
||||
import { withSessionProtection } from '@/lib/supertokens/guard';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const classes = {
|
||||
|
|
@ -131,6 +130,4 @@ function JoinOrganizationPage() {
|
|||
);
|
||||
}
|
||||
|
||||
export const getServerSideProps = withSessionProtection();
|
||||
|
||||
export default authenticated(JoinOrganizationPage);
|
||||
|
|
|
|||
|
|
@ -2,12 +2,6 @@ import React from 'react';
|
|||
import { useRouter } from 'next/router';
|
||||
import { signOut } from 'supertokens-auth-react/recipe/thirdpartyemailpassword';
|
||||
|
||||
export function getServerSideProps() {
|
||||
return {
|
||||
props: {},
|
||||
};
|
||||
}
|
||||
|
||||
export default function LogOutPage() {
|
||||
const router = useRouter();
|
||||
React.useEffect(() => {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import { Page } from '@/components/common';
|
|||
import { DATE_RANGE_OPTIONS, floorToMinute } from '@/components/common/TimeFilter';
|
||||
import { Checkbox as RadixCheckbox, RadixSelect, Tooltip } from '@/components/v2';
|
||||
import { subDays } from '@/lib/date-time';
|
||||
import { withSessionProtection } from '@/lib/supertokens/guard';
|
||||
|
||||
type DateRangeOptions = Exclude<(typeof DATE_RANGE_OPTIONS)[number], { key: 'all' }>;
|
||||
|
||||
|
|
@ -122,6 +121,4 @@ function Manage() {
|
|||
);
|
||||
}
|
||||
|
||||
export const getServerSideProps = withSessionProtection();
|
||||
|
||||
export default authenticated(Manage);
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import { authenticated } from '@/components/authenticated-container';
|
|||
import { Button } from '@/components/ui/button';
|
||||
import { MetaTitle } from '@/components/v2';
|
||||
import { CreateOrganizationForm } from '@/components/v2/modals/create-organization';
|
||||
import { withSessionProtection } from '@/lib/supertokens/guard';
|
||||
|
||||
function CreateOrgPage(): ReactElement {
|
||||
const router = useRouter();
|
||||
|
|
@ -28,6 +27,4 @@ function CreateOrgPage(): ReactElement {
|
|||
);
|
||||
}
|
||||
|
||||
export const getServerSideProps = withSessionProtection();
|
||||
|
||||
export default authenticated(CreateOrgPage);
|
||||
|
|
|
|||
|
|
@ -1,36 +1,13 @@
|
|||
import { ReactElement, useEffect } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import Session, { SessionAuth } from 'supertokens-auth-react/recipe/session';
|
||||
import { ReactElement } from 'react';
|
||||
import { SessionAuth } from 'supertokens-auth-react/recipe/session';
|
||||
import { HiveStripeWrapper } from '@/lib/billing/stripe';
|
||||
|
||||
/**
|
||||
* Utility for wrapping a component with an authenticated container that has the default application layout.
|
||||
*/
|
||||
export const authenticated =
|
||||
<TProps extends { fromSupertokens?: 'needs-refresh' }>(
|
||||
Component: (props: Omit<TProps, 'fromSupertokens'>) => ReactElement | null,
|
||||
) =>
|
||||
<TProps extends {}>(Component: (props: TProps) => ReactElement | null) =>
|
||||
(props: TProps) => {
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
async function doRefresh() {
|
||||
if (props.fromSupertokens === 'needs-refresh') {
|
||||
if (await Session.attemptRefreshingSession()) {
|
||||
location.reload();
|
||||
} else {
|
||||
void router.replace(`/auth?redirectToPath=${router.asPath}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void doRefresh();
|
||||
}, []);
|
||||
|
||||
if (props.fromSupertokens) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<SessionAuth>
|
||||
<HiveStripeWrapper>
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import {
|
|||
} from '@/lib/access/organization';
|
||||
import { getIsStripeEnabled } from '@/lib/billing/stripe-public-key';
|
||||
import { useRouteSelector, useToggle } from '@/lib/hooks';
|
||||
import { useLastVisitedOrganizationWriter } from '@/lib/last-visited-org';
|
||||
import { ProPlanBilling } from '../organization/billing/ProPlanBillingWarm';
|
||||
import { RateLimitWarn } from '../organization/billing/RateLimitWarn';
|
||||
|
||||
|
|
@ -86,6 +87,8 @@ export function OrganizationLayout({
|
|||
redirect: true,
|
||||
});
|
||||
|
||||
useLastVisitedOrganizationWriter(currentOrganization?.cleanId);
|
||||
|
||||
const meInCurrentOrg = currentOrganization?.me;
|
||||
const me = useFragment(OrganizationLayout_MeFragment, props.me);
|
||||
const organizationConnection = useFragment(
|
||||
|
|
@ -226,7 +229,7 @@ export function OrganizationLayout({
|
|||
</Tabs.List>
|
||||
</Tabs>
|
||||
) : (
|
||||
<div className="flex flex-row gap-x-8 border-b-[2px] border-b-transparent px-4 py-3">
|
||||
<div className="flex flex-row gap-x-8 border-b-2 border-b-transparent px-4 py-3">
|
||||
<div className="h-5 w-12 animate-pulse rounded-full bg-gray-800" />
|
||||
<div className="h-5 w-12 animate-pulse rounded-full bg-gray-800" />
|
||||
<div className="h-5 w-12 animate-pulse rounded-full bg-gray-800" />
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { CreateTargetModal } from '@/components/v2/modals';
|
|||
import { FragmentType, graphql, useFragment } from '@/gql';
|
||||
import { canAccessProject, ProjectAccessScope, useProjectAccess } from '@/lib/access/project';
|
||||
import { useRouteSelector, useToggle } from '@/lib/hooks';
|
||||
import { useLastVisitedOrganizationWriter } from '@/lib/last-visited-org';
|
||||
import { ProjectMigrationToast } from '../project/migration-toast';
|
||||
|
||||
export enum Page {
|
||||
|
|
@ -101,6 +102,8 @@ export function ProjectLayout({
|
|||
redirect: true,
|
||||
});
|
||||
|
||||
useLastVisitedOrganizationWriter(currentOrganization?.cleanId);
|
||||
|
||||
const me = useFragment(ProjectLayout_MeFragment, props.me);
|
||||
const organizationConnection = useFragment(
|
||||
ProjectLayout_OrganizationConnectionFragment,
|
||||
|
|
@ -239,7 +242,7 @@ export function ProjectLayout({
|
|||
</Tabs.List>
|
||||
</Tabs>
|
||||
) : (
|
||||
<div className="flex flex-row gap-x-8 border-b-[2px] border-b-transparent px-4 py-3">
|
||||
<div className="flex flex-row gap-x-8 border-b-2 border-b-transparent px-4 py-3">
|
||||
<div className="h-5 w-12 animate-pulse rounded-full bg-gray-800" />
|
||||
<div className="h-5 w-12 animate-pulse rounded-full bg-gray-800" />
|
||||
<div className="h-5 w-12 animate-pulse rounded-full bg-gray-800" />
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { ConnectSchemaModal } from '@/components/v2/modals';
|
|||
import { FragmentType, graphql, useFragment } from '@/gql';
|
||||
import { canAccessTarget, TargetAccessScope, useTargetAccess } from '@/lib/access/target';
|
||||
import { useRouteSelector, useToggle } from '@/lib/hooks';
|
||||
import { useLastVisitedOrganizationWriter } from '@/lib/last-visited-org';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { ProjectMigrationToast } from '../project/migration-toast';
|
||||
|
||||
|
|
@ -131,6 +132,8 @@ export const TargetLayout = ({
|
|||
redirect: true,
|
||||
});
|
||||
|
||||
useLastVisitedOrganizationWriter(currentOrganization?.cleanId);
|
||||
|
||||
const canAccessSchema = canAccessTarget(
|
||||
TargetAccessScope.RegistryRead,
|
||||
currentOrganization?.me ?? null,
|
||||
|
|
@ -333,7 +336,7 @@ export const TargetLayout = ({
|
|||
</Tabs.List>
|
||||
</Tabs>
|
||||
) : (
|
||||
<div className="flex flex-row gap-x-8 border-b-[2px] border-b-transparent px-4 py-3">
|
||||
<div className="flex flex-row gap-x-8 border-b-2 border-b-transparent px-4 py-3">
|
||||
<div className="h-5 w-12 animate-pulse rounded-full bg-gray-800" />
|
||||
<div className="h-5 w-12 animate-pulse rounded-full bg-gray-800" />
|
||||
<div className="h-5 w-12 animate-pulse rounded-full bg-gray-800" />
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ import { Textarea } from '@/components/ui/textarea';
|
|||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
import { useToast } from '@/components/ui/use-toast';
|
||||
import { ProductUpdatesLink } from '@/components/v2/docs-note';
|
||||
import { env } from '@/env/frontend';
|
||||
import { FragmentType, graphql, useFragment } from '@/gql';
|
||||
import { OrganizationAccessScope, ProjectAccessScope, TargetAccessScope } from '@/gql/graphql';
|
||||
import { Scope, scopes } from '@/lib/access/common';
|
||||
|
|
@ -69,7 +70,7 @@ export function MemberRoleMigrationStickyNote(props: {
|
|||
|
||||
const isAdmin = organization?.me.isAdmin;
|
||||
const unassignedMembersToMigrateCount = organization?.unassignedMembersToMigrate.length;
|
||||
const migrationDeadline = __frontend_env.migrations.member_roles_deadline;
|
||||
const migrationDeadline = env.migrations.member_roles_deadline;
|
||||
const daysLeft = useRef<number>();
|
||||
|
||||
if (typeof daysLeft.current !== 'number') {
|
||||
|
|
@ -205,7 +206,7 @@ function SimilarRoles(props: {
|
|||
<p className="text-xs text-gray-400">
|
||||
Maybe some of the existing roles are similar to the one you are about to create?
|
||||
</p>
|
||||
<div className="my-4 h-[1px] w-full bg-gray-900" />
|
||||
<div className="my-4 h-px w-full bg-gray-900" />
|
||||
<div className="space-y-4 text-sm">
|
||||
{props.roles.map(role => {
|
||||
const downgrade = {
|
||||
|
|
|
|||
|
|
@ -86,6 +86,7 @@ export function PolicyListItem(props: {
|
|||
<div>
|
||||
<SeverityLevelToggle canTurnOff={props.overridingParentRule} rule={ruleInfo.id} />
|
||||
</div>
|
||||
{/* eslint-disable-next-line tailwindcss/no-unnecessary-arbitrary-value */}
|
||||
<div className="grid grow grid-cols-4 align-middle [&>*]:min-h-[40px] [&>*]:border-l-[1px] [&>*]:border-l-gray-800">
|
||||
{shouldShowRuleConfig && (
|
||||
<PolicyRuleConfig
|
||||
|
|
|
|||
|
|
@ -361,7 +361,7 @@ export function DateRangePicker(props: DateRangePickerProps): JSX.Element {
|
|||
</div>
|
||||
</div>
|
||||
{showCalendar && (
|
||||
<div className="absolute left-0 top-0 translate-x-[-100%]">
|
||||
<div className="absolute left-0 top-0 -translate-x-full">
|
||||
<div className="bg-popover mr-1 rounded-md border p-4">
|
||||
<Button
|
||||
variant="ghost"
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ export const DropdownMenuContent = React.forwardRef<
|
|||
flex-col
|
||||
gap-1
|
||||
rounded-md
|
||||
bg-[#0b0d11]
|
||||
bg-black
|
||||
p-[13px]
|
||||
text-sm
|
||||
font-normal
|
||||
|
|
@ -110,7 +110,7 @@ export const DropdownMenuSubContent = React.forwardRef<
|
|||
flex-col
|
||||
gap-1
|
||||
rounded-md
|
||||
bg-[#0b0d11]
|
||||
bg-black
|
||||
p-[13px]
|
||||
text-sm
|
||||
font-normal
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ const Trigger = forwardRef<any, Omit<TabsTriggerProps, 'className'> & { hasBorde
|
|||
? `
|
||||
radix-state-active:border-b-orange-500
|
||||
cursor-pointer
|
||||
border-b-[2px]
|
||||
border-b-2
|
||||
border-b-transparent
|
||||
px-4
|
||||
py-3
|
||||
|
|
|
|||
|
|
@ -1,48 +0,0 @@
|
|||
import { env as backendEnv } from '@/env/backend';
|
||||
|
||||
/**
|
||||
* The configuration for the frontend environment.
|
||||
* Don't add any sensitive values here!
|
||||
* Everything here must be serializable.
|
||||
*
|
||||
* **NOTE** Don't import this file in your frontend code. Instead import `@/env/frontend`.
|
||||
*
|
||||
* You might wonder why there is an optional chaining is used here?
|
||||
* It is because Next.js tries to prerender the page lol and during that time we don't have the environment variables. :)
|
||||
*/
|
||||
export const env = {
|
||||
appBaseUrl: backendEnv.appBaseUrl,
|
||||
docsUrl: backendEnv.docsUrl,
|
||||
stripePublicKey: backendEnv?.stripePublicKey,
|
||||
auth: {
|
||||
github: !!backendEnv.auth.github,
|
||||
google: !!backendEnv.auth.google,
|
||||
okta: backendEnv.auth.okta ? { hidden: backendEnv.auth.okta.hidden } : null,
|
||||
requireEmailVerification: backendEnv.auth.requireEmailVerification,
|
||||
organizationOIDC: backendEnv.auth.organizationOIDC,
|
||||
},
|
||||
analytics: {
|
||||
googleAnalyticsTrackingId: backendEnv?.analytics.googleAnalyticsTrackingId,
|
||||
},
|
||||
integrations: {
|
||||
slack: !!backendEnv.slack,
|
||||
},
|
||||
sentry: backendEnv.sentry,
|
||||
release: backendEnv.release,
|
||||
environment: backendEnv.environment,
|
||||
nodeEnv: backendEnv.nodeEnv,
|
||||
graphql: {
|
||||
persistedOperations: backendEnv.graphql.persistedOperations,
|
||||
},
|
||||
zendeskSupport: backendEnv.zendeskSupport,
|
||||
migrations: {
|
||||
member_roles_deadline: backendEnv.migrations.member_roles_deadline,
|
||||
},
|
||||
} as const;
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line no-var
|
||||
var __frontend_env: typeof env;
|
||||
}
|
||||
|
||||
globalThis['__frontend_env'] = env;
|
||||
205
packages/web/app/src/env/backend.ts
vendored
205
packages/web/app/src/env/backend.ts
vendored
|
|
@ -1,7 +1,204 @@
|
|||
/**
|
||||
* The environment as available on the !!!BACKEND!!!
|
||||
*/
|
||||
export const env = globalThis.__backend_env ?? noop();
|
||||
import { PHASE_PRODUCTION_BUILD } from 'next/constants';
|
||||
import zod from 'zod';
|
||||
import * as Sentry from '@sentry/nextjs';
|
||||
import { getAllEnv } from './read';
|
||||
|
||||
// Weird hacky way of getting the Sentry.Integrations object
|
||||
// When the nextjs config is loaded by Next CLI Sentry has `Integrations` property.
|
||||
// When nextjs starts and the `environment.js` is loaded, the Sentry object doesn't have the `Integrations` property, it' under `Sentry.default` property.
|
||||
// Dealing with esm/cjs/default exports is a pain, we all feel that pain...
|
||||
const Integrations =
|
||||
'default' in Sentry
|
||||
? ((Sentry as any).default as typeof Sentry).Integrations
|
||||
: Sentry.Integrations;
|
||||
|
||||
// treat an empty string `''` as `undefined`
|
||||
const emptyString = <T extends zod.ZodType>(input: T) => {
|
||||
return zod.preprocess((value: unknown) => {
|
||||
if (value === '') return undefined;
|
||||
return value;
|
||||
}, input);
|
||||
};
|
||||
|
||||
const BaseSchema = zod.object({
|
||||
NODE_ENV: zod.string(),
|
||||
ENVIRONMENT: zod.string(),
|
||||
APP_BASE_URL: zod.string().url(),
|
||||
GRAPHQL_PUBLIC_ENDPOINT: zod.string().url(),
|
||||
GRAPHQL_PUBLIC_ORIGIN: zod.string().url(),
|
||||
INTEGRATION_GITHUB_APP_NAME: emptyString(zod.string().optional()),
|
||||
GA_TRACKING_ID: emptyString(zod.string().optional()),
|
||||
DOCS_URL: emptyString(zod.string().url().optional()),
|
||||
STRIPE_PUBLIC_KEY: emptyString(zod.string().optional()),
|
||||
RELEASE: emptyString(zod.string().optional()),
|
||||
AUTH_REQUIRE_EMAIL_VERIFICATION: emptyString(
|
||||
zod.union([zod.literal('1'), zod.literal('0')]).optional(),
|
||||
),
|
||||
GRAPHQL_PERSISTED_OPERATIONS: emptyString(
|
||||
zod.union([zod.literal('1'), zod.literal('0')]).optional(),
|
||||
),
|
||||
ZENDESK_SUPPORT: emptyString(zod.union([zod.literal('1'), zod.literal('0')]).optional()),
|
||||
});
|
||||
|
||||
const IntegrationSlackSchema = zod.union([
|
||||
zod.object({
|
||||
INTEGRATION_SLACK: emptyString(zod.literal('0').optional()),
|
||||
}),
|
||||
zod.object({
|
||||
INTEGRATION_SLACK: zod.literal('1'),
|
||||
INTEGRATION_SLACK_CLIENT_ID: zod.string(),
|
||||
INTEGRATION_SLACK_CLIENT_SECRET: zod.string(),
|
||||
}),
|
||||
]);
|
||||
|
||||
const AuthGitHubConfigSchema = zod.object({
|
||||
AUTH_GITHUB: emptyString(zod.union([zod.literal('1'), zod.literal('0')]).optional()),
|
||||
});
|
||||
|
||||
const AuthGoogleConfigSchema = zod.object({
|
||||
AUTH_GOOGLE: emptyString(zod.union([zod.literal('1'), zod.literal('0')]).optional()),
|
||||
});
|
||||
|
||||
const AuthOktaConfigSchema = zod.object({
|
||||
AUTH_OKTA: emptyString(zod.union([zod.literal('1'), zod.literal('0')]).optional()),
|
||||
AUTH_OKTA_HIDDEN: emptyString(zod.union([zod.literal('1'), zod.literal('0')]).optional()),
|
||||
});
|
||||
|
||||
const AuthOktaMultiTenantSchema = zod.object({
|
||||
AUTH_ORGANIZATION_OIDC: emptyString(zod.union([zod.literal('1'), zod.literal('0')]).optional()),
|
||||
});
|
||||
|
||||
const SentryConfigSchema = zod.union([
|
||||
zod.object({
|
||||
SENTRY: zod.union([zod.void(), zod.literal('0')]),
|
||||
}),
|
||||
zod.object({
|
||||
SENTRY: zod.literal('1'),
|
||||
SENTRY_DSN: zod.string(),
|
||||
}),
|
||||
]);
|
||||
|
||||
const MigrationsSchema = zod.object({
|
||||
MEMBER_ROLES_DEADLINE: emptyString(
|
||||
zod
|
||||
.date({
|
||||
coerce: true,
|
||||
})
|
||||
.optional(),
|
||||
),
|
||||
});
|
||||
|
||||
const processEnv = getAllEnv();
|
||||
|
||||
function buildConfig() {
|
||||
const configs = {
|
||||
base: BaseSchema.safeParse(processEnv),
|
||||
integrationSlack: IntegrationSlackSchema.safeParse(processEnv),
|
||||
sentry: SentryConfigSchema.safeParse(processEnv),
|
||||
authGithub: AuthGitHubConfigSchema.safeParse(processEnv),
|
||||
authGoogle: AuthGoogleConfigSchema.safeParse(processEnv),
|
||||
authOkta: AuthOktaConfigSchema.safeParse(processEnv),
|
||||
authOktaMultiTenant: AuthOktaMultiTenantSchema.safeParse(processEnv),
|
||||
migrations: MigrationsSchema.safeParse(processEnv),
|
||||
};
|
||||
|
||||
const environmentErrors: Array<string> = [];
|
||||
|
||||
for (const config of Object.values(configs)) {
|
||||
if (config.success === false) {
|
||||
environmentErrors.push(JSON.stringify(config.error.format(), null, 4));
|
||||
}
|
||||
}
|
||||
|
||||
if (environmentErrors.length) {
|
||||
const fullError = environmentErrors.join('\n');
|
||||
console.error('❌ Invalid (backend) environment variables:', fullError);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function extractConfig<Input, Output>(config: zod.SafeParseReturnType<Input, Output>): Output {
|
||||
if (!config.success) {
|
||||
throw new Error('Something went wrong.');
|
||||
}
|
||||
return config.data;
|
||||
}
|
||||
|
||||
const base = extractConfig(configs.base);
|
||||
const integrationSlack = extractConfig(configs.integrationSlack);
|
||||
const sentry = extractConfig(configs.sentry);
|
||||
const authGithub = extractConfig(configs.authGithub);
|
||||
const authGoogle = extractConfig(configs.authGoogle);
|
||||
const authOkta = extractConfig(configs.authOkta);
|
||||
const authOktaMultiTenant = extractConfig(configs.authOktaMultiTenant);
|
||||
const migrations = extractConfig(configs.migrations);
|
||||
|
||||
const config = {
|
||||
release: base.RELEASE ?? 'local',
|
||||
nodeEnv: base.NODE_ENV,
|
||||
environment: base.ENVIRONMENT,
|
||||
appBaseUrl: base.APP_BASE_URL.replace(/\/$/, ''),
|
||||
graphqlPublicEndpoint: base.GRAPHQL_PUBLIC_ENDPOINT,
|
||||
graphqlPublicOrigin: base.GRAPHQL_PUBLIC_ORIGIN,
|
||||
slack:
|
||||
integrationSlack.INTEGRATION_SLACK === '1'
|
||||
? {
|
||||
clientId: integrationSlack.INTEGRATION_SLACK_CLIENT_ID,
|
||||
clientSecret: integrationSlack.INTEGRATION_SLACK_CLIENT_SECRET,
|
||||
}
|
||||
: null,
|
||||
github: base.INTEGRATION_GITHUB_APP_NAME ? { appName: base.INTEGRATION_GITHUB_APP_NAME } : null,
|
||||
analytics: {
|
||||
googleAnalyticsTrackingId: base.GA_TRACKING_ID,
|
||||
},
|
||||
docsUrl: base.DOCS_URL,
|
||||
auth: {
|
||||
github: {
|
||||
enabled: authGithub.AUTH_GITHUB === '1',
|
||||
},
|
||||
google: {
|
||||
enabled: authGoogle.AUTH_GOOGLE === '1',
|
||||
},
|
||||
okta: {
|
||||
enabled: authOkta.AUTH_OKTA === '1',
|
||||
hidden: authOkta.AUTH_OKTA_HIDDEN === '1',
|
||||
},
|
||||
organizationOIDC: authOktaMultiTenant.AUTH_ORGANIZATION_OIDC === '1',
|
||||
requireEmailVerification: base.AUTH_REQUIRE_EMAIL_VERIFICATION === '1',
|
||||
},
|
||||
sentry: sentry.SENTRY === '1' ? { dsn: sentry.SENTRY_DSN } : null,
|
||||
stripePublicKey: base.STRIPE_PUBLIC_KEY ?? null,
|
||||
graphql: {
|
||||
persistedOperations: base.GRAPHQL_PERSISTED_OPERATIONS === '1',
|
||||
},
|
||||
zendeskSupport: base.ZENDESK_SUPPORT === '1',
|
||||
migrations: {
|
||||
member_roles_deadline: migrations.MEMBER_ROLES_DEADLINE ?? null,
|
||||
},
|
||||
} as const;
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
const isNextBuilding = processEnv['NEXT_PHASE'] === PHASE_PRODUCTION_BUILD;
|
||||
export const env = !isNextBuilding ? buildConfig() : noop();
|
||||
|
||||
// TODO: I don't like this here, but it seems like it makes most sense here :)
|
||||
Sentry.init({
|
||||
serverName: 'app',
|
||||
dist: 'app',
|
||||
enabled: !!env.sentry,
|
||||
enableTracing: false,
|
||||
tracesSampleRate: 1,
|
||||
dsn: env.sentry?.dsn,
|
||||
release: env.release,
|
||||
environment: env.environment,
|
||||
integrations: [
|
||||
// HTTP integration is only available on the server
|
||||
new Integrations.Http({
|
||||
tracing: false,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
/**
|
||||
* Next.js is so kind and tries to pre-render our page without the environment information being available... :)
|
||||
|
|
|
|||
25
packages/web/app/src/env/frontend-public-variables.ts
vendored
Normal file
25
packages/web/app/src/env/frontend-public-variables.ts
vendored
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
export const ALLOWED_ENVIRONMENT_VARIABLES = [
|
||||
'NODE_ENV',
|
||||
'ENVIRONMENT',
|
||||
'APP_BASE_URL',
|
||||
'GRAPHQL_PUBLIC_ENDPOINT',
|
||||
'GRAPHQL_PUBLIC_ORIGIN',
|
||||
'GA_TRACKING_ID',
|
||||
'DOCS_URL',
|
||||
'STRIPE_PUBLIC_KEY',
|
||||
'RELEASE',
|
||||
'AUTH_REQUIRE_EMAIL_VERIFICATION',
|
||||
'GRAPHQL_PERSISTED_OPERATIONS',
|
||||
'ZENDESK_SUPPORT',
|
||||
'INTEGRATION_SLACK',
|
||||
'AUTH_GITHUB',
|
||||
'AUTH_GOOGLE',
|
||||
'AUTH_OKTA',
|
||||
'AUTH_OKTA_HIDDEN',
|
||||
'AUTH_ORGANIZATION_OIDC',
|
||||
'SENTRY',
|
||||
'SENTRY_DSN',
|
||||
'MEMBER_ROLES_DEADLINE',
|
||||
] as const;
|
||||
|
||||
export type AllowedEnvironmentVariables = (typeof ALLOWED_ENVIRONMENT_VARIABLES)[number];
|
||||
182
packages/web/app/src/env/frontend.ts
vendored
182
packages/web/app/src/env/frontend.ts
vendored
|
|
@ -1,7 +1,181 @@
|
|||
/**
|
||||
* The environment as available on the !!!FRONT END!!!
|
||||
*/
|
||||
export const env = globalThis.__frontend_env ?? noop();
|
||||
import { PHASE_PRODUCTION_BUILD } from 'next/constants';
|
||||
import zod from 'zod';
|
||||
import type { AllowedEnvironmentVariables } from './frontend-public-variables';
|
||||
import { getAllEnv } from './read';
|
||||
|
||||
type RestrictKeys<T, K> = {
|
||||
[P in keyof T]: P extends K ? T[P] : never;
|
||||
};
|
||||
|
||||
// Makes sure the list of available environment variables (ALLOWED_ENVIRONMENT_VARIABLES in 'frontend-public-variables.ts') is in sync with the zod schema.
|
||||
// If you add/remove an environment variable, make sure to modify it there as well
|
||||
// Example: If `NODE_ENV` is not one of the allowed values, TypeScript will detect a type error.
|
||||
//
|
||||
// {
|
||||
// NOT_FOUND: zod.string(),
|
||||
// ^ Type 'ZodString' is not assignable to type 'never'.
|
||||
// NODE_ENV: zod.string(),
|
||||
// }
|
||||
//
|
||||
function protectedObject<
|
||||
T extends {
|
||||
[K in keyof T]: zod.ZodTypeAny;
|
||||
},
|
||||
>(shape: RestrictKeys<T, AllowedEnvironmentVariables>) {
|
||||
return zod.object(shape);
|
||||
}
|
||||
|
||||
// treat an empty string `''` as `undefined`
|
||||
const emptyString = <T extends zod.ZodType>(input: T) => {
|
||||
return zod.preprocess((value: unknown) => {
|
||||
if (value === '') return undefined;
|
||||
return value;
|
||||
}, input);
|
||||
};
|
||||
|
||||
const enabledOrDisabled = emptyString(zod.union([zod.literal('1'), zod.literal('0')]).optional());
|
||||
|
||||
// todo: reuse backend schema
|
||||
|
||||
const BaseSchema = protectedObject({
|
||||
NODE_ENV: zod.string(),
|
||||
ENVIRONMENT: zod.string(),
|
||||
APP_BASE_URL: zod.string().url(),
|
||||
GRAPHQL_PUBLIC_ENDPOINT: zod.string().url(),
|
||||
GRAPHQL_PUBLIC_ORIGIN: zod.string().url(),
|
||||
GA_TRACKING_ID: emptyString(zod.string().optional()),
|
||||
DOCS_URL: emptyString(zod.string().url().optional()),
|
||||
STRIPE_PUBLIC_KEY: emptyString(zod.string().optional()),
|
||||
RELEASE: emptyString(zod.string().optional()),
|
||||
AUTH_REQUIRE_EMAIL_VERIFICATION: emptyString(
|
||||
zod.union([zod.literal('1'), zod.literal('0')]).optional(),
|
||||
),
|
||||
GRAPHQL_PERSISTED_OPERATIONS: emptyString(
|
||||
zod.union([zod.literal('1'), zod.literal('0')]).optional(),
|
||||
),
|
||||
ZENDESK_SUPPORT: enabledOrDisabled,
|
||||
});
|
||||
|
||||
const IntegrationSlackSchema = protectedObject({
|
||||
INTEGRATION_SLACK: enabledOrDisabled,
|
||||
});
|
||||
|
||||
const AuthGitHubConfigSchema = protectedObject({
|
||||
AUTH_GITHUB: enabledOrDisabled,
|
||||
});
|
||||
|
||||
const AuthGoogleConfigSchema = protectedObject({
|
||||
AUTH_GOOGLE: enabledOrDisabled,
|
||||
});
|
||||
|
||||
const AuthOktaConfigSchema = protectedObject({
|
||||
AUTH_OKTA: enabledOrDisabled,
|
||||
AUTH_OKTA_HIDDEN: enabledOrDisabled,
|
||||
});
|
||||
|
||||
const AuthOktaMultiTenantSchema = protectedObject({
|
||||
AUTH_ORGANIZATION_OIDC: enabledOrDisabled,
|
||||
});
|
||||
|
||||
const SentryConfigSchema = zod.union([
|
||||
protectedObject({
|
||||
SENTRY: zod.union([zod.void(), zod.literal('0')]),
|
||||
}),
|
||||
protectedObject({
|
||||
SENTRY: zod.literal('1'),
|
||||
SENTRY_DSN: zod.string(),
|
||||
}),
|
||||
]);
|
||||
|
||||
const MigrationsSchema = protectedObject({
|
||||
MEMBER_ROLES_DEADLINE: emptyString(
|
||||
zod
|
||||
.date({
|
||||
coerce: true,
|
||||
})
|
||||
.optional(),
|
||||
),
|
||||
});
|
||||
|
||||
const envValues = getAllEnv();
|
||||
|
||||
function buildConfig() {
|
||||
const configs = {
|
||||
base: BaseSchema.safeParse(envValues),
|
||||
integrationSlack: IntegrationSlackSchema.safeParse(envValues),
|
||||
sentry: SentryConfigSchema.safeParse(envValues),
|
||||
authGithub: AuthGitHubConfigSchema.safeParse(envValues),
|
||||
authGoogle: AuthGoogleConfigSchema.safeParse(envValues),
|
||||
authOkta: AuthOktaConfigSchema.safeParse(envValues),
|
||||
authOktaMultiTenant: AuthOktaMultiTenantSchema.safeParse(envValues),
|
||||
migrations: MigrationsSchema.safeParse(envValues),
|
||||
};
|
||||
|
||||
const environmentErrors: Array<string> = [];
|
||||
|
||||
for (const config of Object.values(configs)) {
|
||||
if (config.success === false) {
|
||||
environmentErrors.push(JSON.stringify(config.error.format(), null, 4));
|
||||
}
|
||||
}
|
||||
|
||||
if (environmentErrors.length) {
|
||||
const fullError = environmentErrors.join('\n');
|
||||
console.error('❌ Invalid (frontend) environment variables:', fullError);
|
||||
throw new Error('Invalid environment variables.');
|
||||
}
|
||||
|
||||
function extractConfig<Input, Output>(config: zod.SafeParseReturnType<Input, Output>): Output {
|
||||
if (!config.success) {
|
||||
throw new Error('Something went wrong.');
|
||||
}
|
||||
return config.data;
|
||||
}
|
||||
|
||||
const base = extractConfig(configs.base);
|
||||
const integrationSlack = extractConfig(configs.integrationSlack);
|
||||
const sentry = extractConfig(configs.sentry);
|
||||
const authGithub = extractConfig(configs.authGithub);
|
||||
const authGoogle = extractConfig(configs.authGoogle);
|
||||
const authOkta = extractConfig(configs.authOkta);
|
||||
const authOktaMultiTenant = extractConfig(configs.authOktaMultiTenant);
|
||||
const migrations = extractConfig(configs.migrations);
|
||||
|
||||
return {
|
||||
appBaseUrl: base.APP_BASE_URL.replace(/\/$/, ''),
|
||||
graphqlPublicEndpoint: base.GRAPHQL_PUBLIC_ENDPOINT,
|
||||
graphqlPublicOrigin: base.GRAPHQL_PUBLIC_ORIGIN,
|
||||
docsUrl: base.DOCS_URL,
|
||||
stripePublicKey: base.STRIPE_PUBLIC_KEY ?? null,
|
||||
auth: {
|
||||
github: authGithub.AUTH_GITHUB === '1',
|
||||
google: authGoogle.AUTH_GOOGLE === '1',
|
||||
okta: authOkta.AUTH_OKTA === '1' ? { hidden: authOkta.AUTH_OKTA_HIDDEN === '1' } : null,
|
||||
requireEmailVerification: base.AUTH_REQUIRE_EMAIL_VERIFICATION === '1',
|
||||
organizationOIDC: authOktaMultiTenant.AUTH_ORGANIZATION_OIDC === '1',
|
||||
},
|
||||
analytics: {
|
||||
googleAnalyticsTrackingId: base.GA_TRACKING_ID,
|
||||
},
|
||||
integrations: {
|
||||
slack: integrationSlack.INTEGRATION_SLACK === '1',
|
||||
},
|
||||
sentry: sentry.SENTRY === '1' ? { dsn: sentry.SENTRY_DSN } : null,
|
||||
release: base.RELEASE ?? 'local',
|
||||
environment: base.ENVIRONMENT,
|
||||
nodeEnv: base.NODE_ENV,
|
||||
graphql: {
|
||||
persistedOperations: base.GRAPHQL_PERSISTED_OPERATIONS === '1',
|
||||
},
|
||||
zendeskSupport: base.ZENDESK_SUPPORT === '1',
|
||||
migrations: {
|
||||
member_roles_deadline: migrations.MEMBER_ROLES_DEADLINE ?? null,
|
||||
},
|
||||
} as const;
|
||||
}
|
||||
|
||||
const isNextBuilding = envValues['NEXT_PHASE'] === PHASE_PRODUCTION_BUILD;
|
||||
export const env = !isNextBuilding ? buildConfig() : noop();
|
||||
|
||||
/**
|
||||
* Next.js is so kind and tries to pre-render our page without the environment information being available... :)
|
||||
|
|
|
|||
15
packages/web/app/src/env/read.ts
vendored
Normal file
15
packages/web/app/src/env/read.ts
vendored
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
function isBrowser() {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
return Boolean(
|
||||
typeof window !== 'undefined' && '__ENV' in window && window['__ENV'] !== undefined,
|
||||
);
|
||||
}
|
||||
|
||||
export function getAllEnv(): Record<string, string | undefined> {
|
||||
if (isBrowser()) {
|
||||
return (window as any)['__ENV'] ?? {};
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-process-env
|
||||
return process.env;
|
||||
}
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
import { NextApiRequest, NextApiResponse } from 'next';
|
||||
import supertokens from 'supertokens-node';
|
||||
import { SessionContainerInterface } from 'supertokens-node/lib/build/recipe/session/types';
|
||||
import { superTokensNextWrapper } from 'supertokens-node/nextjs';
|
||||
import { verifySession } from 'supertokens-node/recipe/session/framework/express';
|
||||
import { backendConfig } from '@/config/supertokens/backend';
|
||||
|
||||
supertokens.init(backendConfig());
|
||||
|
||||
export async function extractAccessTokenFromRequest(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse,
|
||||
): Promise<string> {
|
||||
await superTokensNextWrapper(
|
||||
async next =>
|
||||
await verifySession({
|
||||
sessionRequired: false,
|
||||
checkDatabase: true,
|
||||
})(req as any, res as any, next),
|
||||
req,
|
||||
res,
|
||||
);
|
||||
const { session } = req as { session?: SessionContainerInterface };
|
||||
// Session can be undefined in case no access token was sent.
|
||||
const accessToken = session?.getAccessToken() ?? null;
|
||||
|
||||
if (accessToken === null) {
|
||||
throw new Error('Missing access token.');
|
||||
}
|
||||
|
||||
return accessToken;
|
||||
}
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
import Cookies from 'cookies';
|
||||
import { LAST_VISITED_ORG_KEY } from '@/constants';
|
||||
|
||||
export function writeLastVisitedOrganization(req: any, res: any, orgId: string): void {
|
||||
const cookies = new Cookies(req, res);
|
||||
cookies.set(LAST_VISITED_ORG_KEY, orgId, {
|
||||
httpOnly: false,
|
||||
});
|
||||
}
|
||||
11
packages/web/app/src/lib/last-visited-org.ts
Normal file
11
packages/web/app/src/lib/last-visited-org.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import { useEffect } from 'react';
|
||||
import Cookies from 'js-cookie';
|
||||
import { LAST_VISITED_ORG_KEY } from '@/constants';
|
||||
|
||||
export function useLastVisitedOrganizationWriter(orgId?: string | null) {
|
||||
useEffect(() => {
|
||||
if (orgId) {
|
||||
Cookies.set(LAST_VISITED_ORG_KEY, orgId);
|
||||
}
|
||||
}, [orgId]);
|
||||
}
|
||||
|
|
@ -1,14 +1,14 @@
|
|||
import { env } from '@/env/frontend';
|
||||
|
||||
export const appInfo = () => {
|
||||
const { appBaseUrl } = env;
|
||||
const { appBaseUrl, graphqlPublicOrigin } = env;
|
||||
|
||||
return {
|
||||
// learn more about this on https://supertokens.com/docs/thirdpartyemailpassword/appinfo
|
||||
appName: 'GraphQL Hive',
|
||||
apiDomain: appBaseUrl,
|
||||
apiDomain: graphqlPublicOrigin,
|
||||
websiteDomain: appBaseUrl,
|
||||
apiBasePath: '/api/auth',
|
||||
apiBasePath: '/auth-api',
|
||||
websiteBasePath: '/auth',
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -7,13 +7,13 @@ const supertokenRoutes = new Set([
|
|||
'/auth',
|
||||
]);
|
||||
|
||||
if (env.auth.github) {
|
||||
if (env.auth.github.enabled) {
|
||||
supertokenRoutes.add('/auth/callback/github');
|
||||
}
|
||||
if (env.auth.google) {
|
||||
if (env.auth.google.enabled) {
|
||||
supertokenRoutes.add('/auth/callback/google');
|
||||
}
|
||||
if (env.auth.okta) {
|
||||
if (env.auth.okta.enabled) {
|
||||
supertokenRoutes.add('/auth/callback/okta');
|
||||
}
|
||||
if (env.auth.organizationOIDC) {
|
||||
|
|
|
|||
|
|
@ -1,67 +0,0 @@
|
|||
import { GetServerSideProps } from 'next';
|
||||
import { SessionContainerInterface } from 'supertokens-node/lib/build/recipe/session/types';
|
||||
import { captureException } from '@sentry/nextjs';
|
||||
|
||||
const serverSidePropsSessionHandling = async (context: Parameters<GetServerSideProps>[0]) => {
|
||||
const { backendConfig } = await import('@/config/supertokens/backend');
|
||||
const SupertokensNode = await import('supertokens-node');
|
||||
const Session = await import('supertokens-node/recipe/session');
|
||||
SupertokensNode.init(backendConfig());
|
||||
let session: SessionContainerInterface | undefined;
|
||||
|
||||
try {
|
||||
session = await Session.getSession(context.req, context.res, { sessionRequired: false });
|
||||
// TODO: better error decoding :)
|
||||
} catch (err: any) {
|
||||
// Check whether the email is already verified.
|
||||
// If it is not then we need to redirect to the email verification page - which will trigger the email sending.
|
||||
if (err.type === Session.Error.INVALID_CLAIMS) {
|
||||
return {
|
||||
redirect: {
|
||||
destination: '/auth/verify-email',
|
||||
permanent: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (err.type === Session.Error.TRY_REFRESH_TOKEN || err.type === Session.Error.UNAUTHORISED) {
|
||||
return { props: { fromSupertokens: 'needs-refresh' } };
|
||||
}
|
||||
|
||||
captureException(err);
|
||||
throw err;
|
||||
}
|
||||
|
||||
if (session === undefined) {
|
||||
return {
|
||||
redirect: {
|
||||
destination: `/auth?redirectToPath=${encodeURIComponent(context.resolvedUrl)}`,
|
||||
permanent: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
function defaultHandler() {
|
||||
return Promise.resolve({ props: {} });
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility for protecting a server side props function with session handling.
|
||||
* Redirects user to the login page in case there is no session.
|
||||
*/
|
||||
export function withSessionProtection(handlerFn: GetServerSideProps = defaultHandler) {
|
||||
const getServerSideProps: GetServerSideProps = async context => {
|
||||
const result = await serverSidePropsSessionHandling(context);
|
||||
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
|
||||
return handlerFn(context);
|
||||
};
|
||||
|
||||
return getServerSideProps;
|
||||
}
|
||||
|
|
@ -10,7 +10,6 @@ import type { CreateOperationMutationType } from '@/components/target/laboratory
|
|||
import type { DeleteCollectionMutationType } from '@/components/target/laboratory/delete-collection-modal';
|
||||
import type { DeleteOperationMutationType } from '@/components/target/laboratory/delete-operation-modal';
|
||||
import type { CreateAccessToken_CreateTokenMutation } from '@/components/v2/modals/create-access-token';
|
||||
import type { CreateOrganizationMutation } from '@/components/v2/modals/create-organization';
|
||||
import type { CreateProjectMutation } from '@/components/v2/modals/create-project';
|
||||
import type { CreateTarget_CreateTargetMutation } from '@/components/v2/modals/create-target';
|
||||
import type { DeleteOrganizationDocument } from '@/components/v2/modals/delete-organization';
|
||||
|
|
@ -19,7 +18,6 @@ import { type DeleteTargetMutation } from '@/components/v2/modals/delete-target'
|
|||
import { graphql } from '@/gql';
|
||||
import { ResultOf, VariablesOf } from '@graphql-typed-document-node/core';
|
||||
import { Cache, QueryInput, UpdateResolver } from '@urql/exchange-graphcache';
|
||||
import { OrganizationsQuery } from '../../pages';
|
||||
import { CollectionsQuery } from '../../pages/[organizationId]/[projectId]/[targetId]/laboratory';
|
||||
import {
|
||||
TokensDocument,
|
||||
|
|
@ -74,28 +72,6 @@ const deleteAlerts: TypedDocumentNodeUpdateResolver<
|
|||
}
|
||||
};
|
||||
|
||||
const createOrganization: TypedDocumentNodeUpdateResolver<typeof CreateOrganizationMutation> = (
|
||||
{ createOrganization },
|
||||
_args,
|
||||
cache,
|
||||
) => {
|
||||
updateQuery(
|
||||
cache,
|
||||
{
|
||||
query: OrganizationsQuery,
|
||||
},
|
||||
data => {
|
||||
if (createOrganization.ok) {
|
||||
data.organizations.nodes.unshift(
|
||||
// TODO: figure out masking
|
||||
createOrganization.ok.createdOrganizationPayload.organization as any,
|
||||
);
|
||||
data.organizations.total += 1;
|
||||
}
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const deleteOrganization: TypedDocumentNodeUpdateResolver<typeof DeleteOrganizationDocument> = (
|
||||
{ deleteOrganization },
|
||||
_args,
|
||||
|
|
@ -362,7 +338,6 @@ const createOperationInDocumentCollection: TypedDocumentNodeUpdateResolver<
|
|||
|
||||
// UpdateResolver
|
||||
export const Mutation = {
|
||||
createOrganization,
|
||||
deleteOrganization,
|
||||
createProject,
|
||||
deleteProject,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import { createClient, errorExchange, fetchExchange } from 'urql';
|
||||
import Session from 'supertokens-auth-react/recipe/session';
|
||||
import { createClient, fetchExchange } from 'urql';
|
||||
import { env } from '@/env/frontend';
|
||||
import schema from '@/gql/schema';
|
||||
import { authExchange } from '@urql/exchange-auth';
|
||||
import { cacheExchange } from '@urql/exchange-graphcache';
|
||||
import { persistedExchange } from '@urql/exchange-persisted';
|
||||
import { Mutation } from './urql-cache';
|
||||
|
|
@ -8,12 +10,18 @@ import { networkStatusExchange } from './urql-exchanges/state';
|
|||
|
||||
const noKey = (): null => null;
|
||||
|
||||
const SERVER_BASE_PATH = '/api/proxy';
|
||||
const SERVER_BASE_PATH = env.graphqlPublicEndpoint;
|
||||
|
||||
const isSome = <T>(value: T | null | undefined): value is T => value != null;
|
||||
|
||||
export const urqlClient = createClient({
|
||||
url: SERVER_BASE_PATH,
|
||||
fetchOptions: {
|
||||
headers: {
|
||||
'graphql-client-name': 'Hive App',
|
||||
'graphql-client-version': env.release,
|
||||
},
|
||||
},
|
||||
exchanges: [
|
||||
cacheExchange({
|
||||
schema,
|
||||
|
|
@ -54,12 +62,44 @@ export const urqlClient = createClient({
|
|||
globalIDs: ['SuccessfulSchemaCheck', 'FailedSchemaCheck'],
|
||||
}),
|
||||
networkStatusExchange,
|
||||
errorExchange({
|
||||
onError(error) {
|
||||
if (error.response?.status === 401) {
|
||||
window.location.href = '/logout';
|
||||
}
|
||||
},
|
||||
authExchange(async () => {
|
||||
let action: 'NEEDS_REFRESH' | 'VERIFY_EMAIL' | 'UNAUTHENTICATED' = 'UNAUTHENTICATED';
|
||||
|
||||
return {
|
||||
addAuthToOperation(operation) {
|
||||
return operation;
|
||||
},
|
||||
willAuthError() {
|
||||
return false;
|
||||
},
|
||||
didAuthError(error) {
|
||||
if (error.graphQLErrors.some(e => e.extensions?.code === 'UNAUTHENTICATED')) {
|
||||
action = 'UNAUTHENTICATED';
|
||||
return true;
|
||||
}
|
||||
|
||||
if (error.graphQLErrors.some(e => e.extensions?.code === 'VERIFY_EMAIL')) {
|
||||
action = 'VERIFY_EMAIL';
|
||||
return true;
|
||||
}
|
||||
|
||||
if (error.graphQLErrors.some(e => e.extensions?.code === 'NEEDS_REFRESH')) {
|
||||
action = 'NEEDS_REFRESH';
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
async refreshAuth() {
|
||||
if (action === 'NEEDS_REFRESH' && (await Session.attemptRefreshingSession())) {
|
||||
location.reload();
|
||||
} else if (action === 'VERIFY_EMAIL') {
|
||||
window.location.href = '/auth/verify-email';
|
||||
} else {
|
||||
window.location.href = `/auth?redirectToPath=${encodeURIComponent(window.location.pathname)}`;
|
||||
}
|
||||
},
|
||||
};
|
||||
}),
|
||||
env.graphql.persistedOperations
|
||||
? persistedExchange({
|
||||
|
|
|
|||
|
|
@ -16,6 +16,6 @@
|
|||
"jsx": "preserve",
|
||||
"incremental": true
|
||||
},
|
||||
"include": ["next-env.d.ts", "modules.d.ts", "src", "pages", "environment.ts"],
|
||||
"include": ["next-env.d.ts", "modules.d.ts", "src", "pages"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -186,19 +186,19 @@ function Feature(props: {
|
|||
<div className={cn(classes.feature, 'relative overflow-hidden')}>
|
||||
<div>
|
||||
<div
|
||||
className="absolute top-0 h-[1px] w-full opacity-25"
|
||||
className="absolute top-0 h-px w-full opacity-25"
|
||||
style={{
|
||||
backgroundImage: `linear-gradient(90deg, ${end}, ${start})`,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className="absolute left-[-200px] top-[-200px] h-[255px] w-[60vw] opacity-[0.15] blur-3xl"
|
||||
className="absolute left-[-200px] top-[-200px] h-[255px] w-[60vw] opacity-15 blur-3xl"
|
||||
style={{
|
||||
backgroundImage: `linear-gradient(180deg, ${end}, ${start})`,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className="absolute right-[-200px] top-[-200px] h-[255px] w-[60vw] opacity-[0.15] blur-3xl"
|
||||
className="absolute right-[-200px] top-[-200px] h-[255px] w-[60vw] opacity-15 blur-3xl"
|
||||
style={{
|
||||
backgroundImage: `linear-gradient(180deg, ${start}, ${end})`,
|
||||
}}
|
||||
|
|
@ -364,7 +364,7 @@ export function IndexPage(): ReactElement {
|
|||
</Hero>
|
||||
<div className="relative even:bg-gray-50">
|
||||
<div>
|
||||
<div className="absolute top-0 h-[1px] w-full bg-gradient-to-r from-gray-300 via-gray-500 to-gray-300 opacity-25" />
|
||||
<div className="absolute top-0 h-px w-full bg-gradient-to-r from-gray-300 via-gray-500 to-gray-300 opacity-25" />
|
||||
</div>
|
||||
<StatsList>
|
||||
<StatsItem label="Happy users" value={4.1} suffix="K" decimal />
|
||||
|
|
@ -407,9 +407,9 @@ export function IndexPage(): ReactElement {
|
|||
/>
|
||||
<div className={cn(classes.feature, 'relative overflow-hidden')}>
|
||||
<div>
|
||||
<div className="absolute top-0 h-[1px] w-full bg-gradient-to-r from-gray-300 via-gray-500 to-gray-300 opacity-25" />
|
||||
<div className="absolute left-[-200px] top-[-200px] h-[255px] w-[60vw] bg-gradient-to-b from-gray-50 to-gray-300 opacity-[0.15] blur-3xl" />
|
||||
<div className="absolute right-[-200px] top-[-200px] h-[255px] w-[60vw] bg-gradient-to-b from-gray-300 to-gray-50 opacity-[0.15] blur-3xl" />
|
||||
<div className="absolute top-0 h-px w-full bg-gradient-to-r from-gray-300 via-gray-500 to-gray-300 opacity-25" />
|
||||
<div className="absolute left-[-200px] top-[-200px] h-[255px] w-[60vw] bg-gradient-to-b from-gray-50 to-gray-300 opacity-15 blur-3xl" />
|
||||
<div className="absolute right-[-200px] top-[-200px] h-[255px] w-[60vw] bg-gradient-to-b from-gray-300 to-gray-50 opacity-15 blur-3xl" />
|
||||
</div>
|
||||
<div className="py-24">
|
||||
<h2 className="base:mr-1 mb-12 ml-1 text-center text-3xl font-semibold leading-normal tracking-tight text-black">
|
||||
|
|
@ -517,7 +517,7 @@ export function IndexPage(): ReactElement {
|
|||
}}
|
||||
>
|
||||
<div>
|
||||
<div className="absolute top-0 h-[1px] w-full bg-blue-900 opacity-25" />
|
||||
<div className="absolute top-0 h-px w-full bg-blue-900 opacity-25" />
|
||||
</div>
|
||||
<div className="py-24">
|
||||
<div className="mx-auto max-w-lg text-center text-white">
|
||||
|
|
@ -543,9 +543,9 @@ export function IndexPage(): ReactElement {
|
|||
</div>
|
||||
<div className={cn(classes.feature, 'relative overflow-hidden')}>
|
||||
<div>
|
||||
<div className="absolute top-0 h-[1px] w-full bg-gradient-to-r from-gray-300 via-gray-500 to-gray-300 opacity-25" />
|
||||
<div className="absolute left-[-200px] top-[-200px] h-[255px] w-[60vw] bg-gradient-to-b from-gray-600 to-gray-900 opacity-[0.15] blur-3xl" />
|
||||
<div className="absolute right-[-200px] top-[-200px] h-[255px] w-[60vw] bg-gradient-to-b from-gray-900 to-gray-600 opacity-[0.15] blur-3xl" />
|
||||
<div className="absolute top-0 h-px w-full bg-gradient-to-r from-gray-300 via-gray-500 to-gray-300 opacity-25" />
|
||||
<div className="absolute left-[-200px] top-[-200px] h-[255px] w-[60vw] bg-gradient-to-b from-gray-600 to-gray-900 opacity-15 blur-3xl" />
|
||||
<div className="absolute right-[-200px] top-[-200px] h-[255px] w-[60vw] bg-gradient-to-b from-gray-900 to-gray-600 opacity-15 blur-3xl" />
|
||||
</div>
|
||||
<div className="py-24">
|
||||
<h2 className="mb-12 text-center text-3xl font-semibold leading-normal tracking-tight text-black">
|
||||
|
|
@ -579,19 +579,19 @@ export function IndexPage(): ReactElement {
|
|||
<div className={cn(classes.feature, 'relative overflow-hidden')}>
|
||||
<div>
|
||||
<div
|
||||
className="absolute top-0 h-[1px] w-full opacity-25"
|
||||
className="absolute top-0 h-px w-full opacity-25"
|
||||
style={{
|
||||
backgroundImage: `linear-gradient(90deg, ${gradients[3][1]}, ${gradients[3][0]})`,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className="absolute left-[-200px] top-[-200px] h-[255px] w-[60vw] opacity-[0.15] blur-3xl"
|
||||
className="absolute left-[-200px] top-[-200px] h-[255px] w-[60vw] opacity-15 blur-3xl"
|
||||
style={{
|
||||
backgroundImage: `linear-gradient(180deg, ${gradients[3][0]}, ${gradients[3][1]})`,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className="absolute right-[-200px] top-[-200px] h-[255px] w-[60vw] opacity-[0.15] blur-3xl"
|
||||
className="absolute right-[-200px] top-[-200px] h-[255px] w-[60vw] opacity-15 blur-3xl"
|
||||
style={{
|
||||
backgroundImage: `linear-gradient(180deg, ${gradients[3][1]}, ${gradients[3][0]})`,
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -105,7 +105,7 @@ const OPERATIONS_EXPLAINER = 'GraphQL operations reported to GraphQL Hive';
|
|||
export function Pricing({ gradient }: { gradient: [string, string] }): ReactElement {
|
||||
return (
|
||||
<div className="w-full bg-neutral-900">
|
||||
<div className="mx-auto my-12 box-border w-full max-w-[1024px] px-6">
|
||||
<div className="mx-auto my-12 box-border w-full max-w-screen-lg px-6">
|
||||
<a id="pricing">
|
||||
<h2
|
||||
className="bg-clip-text text-2xl font-bold leading-normal text-transparent md:text-3xl"
|
||||
|
|
|
|||
|
|
@ -1054,6 +1054,12 @@ importers:
|
|||
'@escape.tech/graphql-armor-max-tokens':
|
||||
specifier: 2.3.0
|
||||
version: 2.3.0
|
||||
'@fastify/cors':
|
||||
specifier: 9.0.1
|
||||
version: 9.0.1
|
||||
'@fastify/formbody':
|
||||
specifier: 7.4.0
|
||||
version: 7.4.0
|
||||
'@graphql-hive/client':
|
||||
specifier: workspace:*
|
||||
version: link:../../libraries/client/dist
|
||||
|
|
@ -1084,6 +1090,9 @@ importers:
|
|||
'@swc/core':
|
||||
specifier: 1.4.8
|
||||
version: 1.4.8
|
||||
'@trpc/client':
|
||||
specifier: 10.45.2
|
||||
version: 10.45.2(@trpc/server@10.45.2)
|
||||
'@trpc/server':
|
||||
specifier: 10.45.2
|
||||
version: 10.45.2
|
||||
|
|
@ -1117,6 +1126,12 @@ importers:
|
|||
reflect-metadata:
|
||||
specifier: 0.2.1
|
||||
version: 0.2.1
|
||||
supertokens-js-override:
|
||||
specifier: 0.0.4
|
||||
version: 0.0.4
|
||||
supertokens-node:
|
||||
specifier: 15.2.1
|
||||
version: 15.2.1
|
||||
tslib:
|
||||
specifier: 2.6.2
|
||||
version: 2.6.2
|
||||
|
|
@ -1586,6 +1601,9 @@ importers:
|
|||
'@urql/core':
|
||||
specifier: 4.1.4
|
||||
version: 4.1.4(graphql@16.8.1)
|
||||
'@urql/exchange-auth':
|
||||
specifier: 2.1.6
|
||||
version: 2.1.6(graphql@16.8.1)
|
||||
'@urql/exchange-graphcache':
|
||||
specifier: 6.3.3
|
||||
version: 6.3.3(graphql@16.8.1)
|
||||
|
|
@ -1706,12 +1724,6 @@ importers:
|
|||
supertokens-auth-react:
|
||||
specifier: 0.35.6
|
||||
version: 0.35.6(react-dom@18.2.0)(react@18.2.0)(supertokens-web-js@0.8.0)
|
||||
supertokens-js-override:
|
||||
specifier: 0.0.4
|
||||
version: 0.0.4
|
||||
supertokens-node:
|
||||
specifier: 15.2.1
|
||||
version: 15.2.1
|
||||
supertokens-web-js:
|
||||
specifier: 0.8.0
|
||||
version: 0.8.0
|
||||
|
|
@ -5715,6 +5727,13 @@ packages:
|
|||
fast-json-stringify: 5.12.0
|
||||
dev: true
|
||||
|
||||
/@fastify/formbody@7.4.0:
|
||||
resolution: {integrity: sha512-H3C6h1GN56/SMrZS8N2vCT2cZr7mIHzBHzOBa5OPpjfB/D6FzP9mMpE02ZzrFX0ANeh0BAJdoXKOF2e7IbV+Og==}
|
||||
dependencies:
|
||||
fast-querystring: 1.1.2
|
||||
fastify-plugin: 4.5.1
|
||||
dev: true
|
||||
|
||||
/@fastify/merge-json-schemas@0.1.1:
|
||||
resolution: {integrity: sha512-fERDVz7topgNjtXsJTTW1JKLy0rhuLRcquYqNR9rF7OcVpCa2OVW49ZPDIhaRRCaUuvVxI+N416xUoF76HNSXA==}
|
||||
dependencies:
|
||||
|
|
@ -13994,6 +14013,15 @@ packages:
|
|||
- graphql
|
||||
dev: false
|
||||
|
||||
/@urql/exchange-auth@2.1.6(graphql@16.8.1):
|
||||
resolution: {integrity: sha512-snOlt7p5kYq0KnPDuXkKe2qW3/BucQZOElvTeo3svLQuk9JiNJVnm6ffQ6QGiGO+G3AtMrctnno1+X44fLtDuQ==}
|
||||
dependencies:
|
||||
'@urql/core': 4.1.4(graphql@16.8.1)
|
||||
wonka: 6.3.4
|
||||
transitivePeerDependencies:
|
||||
- graphql
|
||||
dev: false
|
||||
|
||||
/@urql/exchange-graphcache@6.3.3(graphql@16.8.1):
|
||||
resolution: {integrity: sha512-uD8zzNIrxQHYCSgfIwYxzEmU1Ml4nJ6NTKwrDlpKmTLJa3aYuG3AoiO138HZBK1XGJ2QzV5yQPfcZsmbVFH8Yg==}
|
||||
dependencies:
|
||||
|
|
@ -15077,6 +15105,7 @@ packages:
|
|||
follow-redirects: 1.15.4(debug@4.3.4)
|
||||
transitivePeerDependencies:
|
||||
- debug
|
||||
dev: true
|
||||
|
||||
/axios@1.6.2:
|
||||
resolution: {integrity: sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==}
|
||||
|
|
@ -15544,6 +15573,7 @@ packages:
|
|||
|
||||
/buffer-equal-constant-time@1.0.1:
|
||||
resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==}
|
||||
dev: true
|
||||
|
||||
/buffer-from@1.1.2:
|
||||
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
|
||||
|
|
@ -16680,6 +16710,7 @@ packages:
|
|||
/cookie@0.4.0:
|
||||
resolution: {integrity: sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
dev: true
|
||||
|
||||
/cookie@0.5.0:
|
||||
resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==}
|
||||
|
|
@ -16891,6 +16922,7 @@ packages:
|
|||
node-fetch: 2.6.12
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
dev: true
|
||||
|
||||
/cross-inspect@1.0.0:
|
||||
resolution: {integrity: sha512-4PFfn4b5ZN6FMNGSZlyb7wUhuN8wvj8t/VQHZdM4JsDcruGJ8L2kf9zao98QIrBPFCpdk27qst/AGTl7pL3ypQ==}
|
||||
|
|
@ -16944,6 +16976,7 @@ packages:
|
|||
|
||||
/crypto-js@4.2.0:
|
||||
resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==}
|
||||
dev: true
|
||||
|
||||
/crypto-random-string@2.0.0:
|
||||
resolution: {integrity: sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==}
|
||||
|
|
@ -18051,6 +18084,7 @@ packages:
|
|||
resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==}
|
||||
dependencies:
|
||||
safe-buffer: 5.2.1
|
||||
dev: true
|
||||
|
||||
/echarts-for-react@3.0.2(echarts@5.5.0)(react@18.2.0):
|
||||
resolution: {integrity: sha512-DRwIiTzx8JfwPOVgGttDytBqdp5VzCSyMRIxubgU/g2n9y3VLUmF2FK7Icmg/sNVkv4+rktmrLN9w22U2yy3fA==}
|
||||
|
|
@ -19796,6 +19830,7 @@ packages:
|
|||
optional: true
|
||||
dependencies:
|
||||
debug: 4.3.4(supports-color@8.1.1)
|
||||
dev: true
|
||||
|
||||
/for-each@0.3.3:
|
||||
resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==}
|
||||
|
|
@ -21572,6 +21607,7 @@ packages:
|
|||
/inflation@2.0.0:
|
||||
resolution: {integrity: sha512-m3xv4hJYR2oXw4o4Y5l6P5P16WYmazYof+el6Al3f+YlggGj6qT9kImBAnzDelRALnP5d3h4jGBPKzYCizjZZw==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
dev: true
|
||||
|
||||
/inflight@1.0.6:
|
||||
resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==}
|
||||
|
|
@ -22397,6 +22433,7 @@ packages:
|
|||
|
||||
/jose@4.14.4:
|
||||
resolution: {integrity: sha512-j8GhLiKmUAh+dsFXlX1aJCbt5KMibuKb+d7j1JaOJG6s2UjX1PQlW+OKB/sD4a/5ZYF4RcmYmLSndOoU3Lt/3g==}
|
||||
dev: true
|
||||
|
||||
/joycon@3.1.1:
|
||||
resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==}
|
||||
|
|
@ -22640,6 +22677,7 @@ packages:
|
|||
lodash: 4.17.21
|
||||
ms: 2.1.3
|
||||
semver: 7.5.4
|
||||
dev: true
|
||||
|
||||
/jsox@1.2.118:
|
||||
resolution: {integrity: sha512-ubYWn4WOc7HA7icvcQuIni1I7Xx4bI4KbRXbXzlr5e48hvdizeAbflBx97B629ZNH5RZnQ657Z5Z8dFgxFVrSQ==}
|
||||
|
|
@ -22697,12 +22735,14 @@ packages:
|
|||
buffer-equal-constant-time: 1.0.1
|
||||
ecdsa-sig-formatter: 1.0.11
|
||||
safe-buffer: 5.2.1
|
||||
dev: true
|
||||
|
||||
/jws@3.2.2:
|
||||
resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==}
|
||||
dependencies:
|
||||
jwa: 1.4.1
|
||||
safe-buffer: 5.2.1
|
||||
dev: true
|
||||
|
||||
/kafkajs@2.2.4:
|
||||
resolution: {integrity: sha512-j/YeapB1vfPT2iOIUn/vxdyKEuhuY2PxMBvf5JWux6iSaukAccrMtXEY/Lb7OvavDhOWME589bpLrEdnVHjfjA==}
|
||||
|
|
@ -22862,6 +22902,7 @@ packages:
|
|||
|
||||
/libphonenumber-js@1.10.14:
|
||||
resolution: {integrity: sha512-McGS7GV/WjJ2KjfOGhJU1oJn29RYeo7Q+RpANRbUNMQ9gj5XArpbjurSuyYPTejFwbaUojstQ4XyWCrAzGOUXw==}
|
||||
dev: true
|
||||
|
||||
/libqp@1.1.0:
|
||||
resolution: {integrity: sha512-4Rgfa0hZpG++t1Vi2IiqXG9Ad1ig4QTmtuZF946QJP4bPqOYC78ixUXgz5TW/wE7lNaNKlplSYTxQ+fR2KZ0EA==}
|
||||
|
|
@ -25731,6 +25772,7 @@ packages:
|
|||
/nodemailer@6.9.7:
|
||||
resolution: {integrity: sha512-rUtR77ksqex/eZRLmQ21LKVH5nAAsVicAtAYudK7JgwenEDZ0UIQ1adUGqErz7sMkWYxWTTU1aeP2Jga6WQyJw==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
dev: true
|
||||
|
||||
/noms@0.0.0:
|
||||
resolution: {integrity: sha512-lNDU9VJaOPxUmXcLb+HQFeUgQQPtMI24Gt6hgfuMHRJgMRHMF/qZ4HJD3GDru4sSw9IQl2jPjAYnQrdIeLbwow==}
|
||||
|
|
@ -27066,6 +27108,7 @@ packages:
|
|||
resolution: {integrity: sha512-bQ/0XPZZ7eX+cdAkd61uYWpfMhakH3NeteUF1R8GNa+LMqX8QFAkbCLqq+AYAns1/ueACBu/BMWhrlKGrdvGZg==}
|
||||
dependencies:
|
||||
crypto-js: 4.2.0
|
||||
dev: true
|
||||
|
||||
/pkg-dir@3.0.0:
|
||||
resolution: {integrity: sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==}
|
||||
|
|
@ -27987,6 +28030,7 @@ packages:
|
|||
|
||||
/psl@1.8.0:
|
||||
resolution: {integrity: sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==}
|
||||
dev: true
|
||||
|
||||
/public-encrypt@4.0.3:
|
||||
resolution: {integrity: sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==}
|
||||
|
|
@ -28068,6 +28112,7 @@ packages:
|
|||
|
||||
/querystringify@2.2.0:
|
||||
resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==}
|
||||
dev: true
|
||||
|
||||
/queue-microtask@1.2.3:
|
||||
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
|
||||
|
|
@ -29006,6 +29051,7 @@ packages:
|
|||
|
||||
/requires-port@1.0.0:
|
||||
resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==}
|
||||
dev: true
|
||||
|
||||
/resolve-alpn@1.2.1:
|
||||
resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==}
|
||||
|
|
@ -29406,6 +29452,7 @@ packages:
|
|||
|
||||
/scmp@2.1.0:
|
||||
resolution: {integrity: sha512-o/mRQGk9Rcer/jEEw/yw4mwo3EU/NvYvp577/Btqrym9Qy5/MdWGBqipbALgd2lrdWTJ5/gqDusxfnQBxOxT2Q==}
|
||||
dev: true
|
||||
|
||||
/scoped-regex@2.1.0:
|
||||
resolution: {integrity: sha512-g3WxHrqSWCZHGHlSrF51VXFdjImhwvH8ZO/pryFH56Qi0cDsZfylQa/t0jCzVQFNbNvM00HfHjkDPEuarKDSWQ==}
|
||||
|
|
@ -30607,6 +30654,7 @@ packages:
|
|||
transitivePeerDependencies:
|
||||
- encoding
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/supertokens-web-js@0.8.0:
|
||||
resolution: {integrity: sha512-kkvuPbdy1I0e7nejVJAhpPJhR5h7EUHdn7Fh1YqfSjZfSJh46J3JU2qWGCgSDVZuD1hSuUKb6skF9CQnltHWrQ==}
|
||||
|
|
@ -31475,6 +31523,7 @@ packages:
|
|||
transitivePeerDependencies:
|
||||
- debug
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/twoslash-protocol@0.2.4:
|
||||
resolution: {integrity: sha512-AEGTJj4mFGfvQc/M6qi0+s82Zq+mxLcjWZU+EUHGG8LQElyHDs+uDR+/3+m1l+WP7WL+QmWrVzFXgFX+hBg+bg==}
|
||||
|
|
@ -32010,6 +32059,7 @@ packages:
|
|||
dependencies:
|
||||
querystringify: 2.2.0
|
||||
requires-port: 1.0.0
|
||||
dev: true
|
||||
|
||||
/url@0.10.3:
|
||||
resolution: {integrity: sha512-hzSUW2q06EqL1gKM/a+obYHLIO6ct2hwPuviqTTOcfFVc61UbfJ2Q32+uGL/HCPxKqrdGB5QUwIe7UqlDgwsOQ==}
|
||||
|
|
@ -32924,6 +32974,7 @@ packages:
|
|||
/xmlbuilder@13.0.2:
|
||||
resolution: {integrity: sha512-Eux0i2QdDYKbdbA6AM6xE4m6ZTZr4G4xF9kahI2ukSEMCzwce2eX9WlTI5J3s+NU7hpasFsr8hWIONae7LluAQ==}
|
||||
engines: {node: '>=6.0'}
|
||||
dev: true
|
||||
|
||||
/xmlbuilder@9.0.7:
|
||||
resolution: {integrity: sha512-7YXTQc3P2l9+0rjaUbLwMKRhtmwg1M1eDf6nag7urC7pIPYLD9W/jmzQ4ptRSUbodw5S0jfoGTflLemQibSpeQ==}
|
||||
|
|
|
|||
Loading…
Reference in a new issue