Ensure user and personal org creation after successful sign up/in (#518)

This commit is contained in:
Kamil Kisiela 2022-10-25 16:12:43 +02:00 committed by GitHub
parent 40a4cd39ff
commit e85d8220a7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
45 changed files with 775 additions and 524 deletions

View file

@ -79,6 +79,10 @@ export function deployApp({
name: 'GRAPHQL_ENDPOINT',
value: serviceLocalEndpoint(graphql.service).apply(s => `${s}/graphql`),
},
{
name: 'SERVER_ENDPOINT',
value: serviceLocalEndpoint(graphql.service),
},
{
name: 'APP_BASE_URL',
value: `https://${deploymentEnv.DEPLOYED_DNS}/`,

View file

@ -311,4 +311,5 @@ services:
SUPERTOKENS_API_KEY: '${SUPERTOKENS_API_KEY}'
EMAILS_ENDPOINT: http://emails:3011
GRAPHQL_ENDPOINT: http://server:3001/graphql
SERVER_ENDPOINT: http://server:3001
AUTH_REQUIRE_EMAIL_VERIFICATION: '0'

View file

@ -1,4 +1,4 @@
import { Dockest, logLevel } from 'dockest';
import { Dockest, logLevel } from '@n1ru4l/dockest';
import { cleanDockerContainers, createServices } from './testkit/dockest';
import dotenv from 'dotenv';

View file

@ -1,5 +1,5 @@
import { createPool } from 'slonik';
import * as utils from 'dockest/test-helper';
import * as utils from '@n1ru4l/dockest/test-helper';
import { resetDb } from './testkit/db';
import { resetClickHouse } from './testkit/clickhouse';
import { resetRedis } from './testkit/redis';

View file

@ -11,13 +11,17 @@
"dotenv": "10.0.0",
"date-fns": "2.25.0",
"dependency-graph": "0.11.0",
"dockest": "npm:@n1ru4l/dockest@2.1.0-rc.6",
"@n1ru4l/dockest": "2.1.0-rc.6",
"rxjs": "^6.5.4",
"slonik": "30.1.2",
"tsup": "5.12.7",
"yaml": "2.1.0",
"@whatwg-node/fetch": "0.4.7",
"zod": "3.15.1"
"zod": "3.15.1",
"@trpc/client": "9.23.2"
},
"devDependencies": {
"@hive/server": "*"
},
"scripts": {
"build-and-pack": "(cd ../ && yarn build:services && yarn build:libraries && yarn build:local) && node ./scripts/pack.mjs",

View file

@ -1,16 +1,30 @@
import { fetch } from '@whatwg-node/fetch';
import zod from 'zod';
import * as utils from '@n1ru4l/dockest/test-helper';
import { createFetch } from '@whatwg-node/fetch';
import { createTRPCClient } from '@trpc/client';
import type { InternalApi } from '@hive/server';
import { z } from 'zod';
import { ensureEnv } from './env';
const SignUpSignInUserResponseModel = zod.object({
status: zod.literal('OK'),
user: zod.object({ email: zod.string(), id: zod.string(), timeJoined: zod.number() }),
const graphqlAddress = utils.getServiceAddress('server', 3001);
const { fetch } = createFetch({
useNodeFetch: true,
});
const internalApi = createTRPCClient<InternalApi>({
url: `http://${graphqlAddress}/trpc`,
fetch,
});
const SignUpSignInUserResponseModel = z.object({
status: z.literal('OK'),
user: z.object({ email: z.string(), id: z.string(), timeJoined: z.number() }),
});
const signUpUserViaEmail = async (
email: string,
password: string
): Promise<zod.TypeOf<typeof SignUpSignInUserResponseModel>> => {
): Promise<z.TypeOf<typeof SignUpSignInUserResponseModel>> => {
const response = await fetch(`${ensureEnv('SUPERTOKENS_CONNECTION_URI')}/recipe/signup`, {
method: 'POST',
headers: {
@ -37,19 +51,27 @@ const createSessionPayload = (superTokensUserId: string, email: string) => ({
email,
});
const CreateSessionModel = zod.object({
accessToken: zod.object({
token: zod.string(),
const CreateSessionModel = z.object({
accessToken: z.object({
token: z.string(),
}),
refreshToken: zod.object({
token: zod.string(),
refreshToken: z.object({
token: z.string(),
}),
idRefreshToken: zod.object({
token: zod.string(),
idRefreshToken: z.object({
token: z.string(),
}),
});
const createSession = async (superTokensUserId: string, email: string) => {
// I failed to make the TypeScript work here...
// It shows that the input type is `undefined`...
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
await internalApi.mutation('ensureUser', {
superTokensUserId,
email,
});
const sessionData = createSessionPayload(superTokensUserId, email);
const payload = {
enableAntiCsrf: false,
@ -91,21 +113,16 @@ export const userEmails: Record<UserID, string> = {
admin: 'admin@localhost.localhost',
};
const tokenResponsePromise: Record<UserID, Promise<{ access_token: string }> | null> = {
const tokenResponsePromise: Record<UserID, Promise<z.TypeOf<typeof SignUpSignInUserResponseModel>> | null> = {
main: null,
extra: null,
admin: null,
};
async function signUpAndSignInViaEmail(email: string, password: string): Promise<{ access_token: string }> {
const data = await signUpUserViaEmail(email, password);
return await createSession(data.user.id, data.user.email);
}
export function authenticate(userId: UserID): Promise<{ access_token: string }> {
if (!tokenResponsePromise[userId]) {
tokenResponsePromise[userId] = signUpAndSignInViaEmail(userEmails[userId], password);
tokenResponsePromise[userId] = signUpUserViaEmail(userEmails[userId], password);
}
return tokenResponsePromise[userId]!;
return tokenResponsePromise[userId]!.then(data => createSession(data.user.id, data.user.email));
}

View file

@ -1,5 +1,5 @@
import * as utils from 'dockest/test-helper';
import { execa } from 'dockest';
import * as utils from '@n1ru4l/dockest/test-helper';
import { execa } from '@n1ru4l/dockest';
import { resolve } from 'path';
const registryAddress = utils.getServiceAddress('server', 3001);

View file

@ -1,4 +1,4 @@
import * as utils from 'dockest/test-helper';
import * as utils from '@n1ru4l/dockest/test-helper';
import { fetch } from '@whatwg-node/fetch';
const clickhouseAddress = utils.getServiceAddress('clickhouse', 8123);

View file

@ -1,6 +1,9 @@
import { DockestService, execa } from 'dockest';
import { containerIsHealthyReadinessCheck, zeroExitCodeReadinessCheck } from 'dockest/dist/readiness-check/index.js';
import { ReadinessCheck } from 'dockest/dist/@types.js';
import { DockestService, execa } from '@n1ru4l/dockest';
import {
containerIsHealthyReadinessCheck,
zeroExitCodeReadinessCheck,
} from '@n1ru4l/dockest/dist/readiness-check/index.js';
import { ReadinessCheck } from '@n1ru4l/dockest/dist/@types.js';
import { mapTo, take, filter } from 'rxjs/operators/index.js';
import { DepGraph } from 'dependency-graph';
import { readFileSync } from 'fs';

View file

@ -1,4 +1,4 @@
import * as utils from 'dockest/test-helper';
import * as utils from '@n1ru4l/dockest/test-helper';
import { fetch } from '@whatwg-node/fetch';
const emailsAddress = utils.getServiceAddress('emails', 3011);

View file

@ -1,4 +1,4 @@
import * as utils from 'dockest/test-helper';
import * as utils from '@n1ru4l/dockest/test-helper';
import { fetch } from '@whatwg-node/fetch';
const port = 3012;

View file

@ -341,7 +341,6 @@ export function readTokenInfo(token: string) {
}
`),
token,
variables: undefined,
});
}
@ -602,7 +601,6 @@ export function fetchLatestSchema(token: string) {
}
`),
token,
variables: undefined,
});
}
@ -624,7 +622,6 @@ export function fetchLatestValidSchema(token: string) {
}
`),
token,
variables: undefined,
});
}

View file

@ -1,4 +1,4 @@
import * as utils from 'dockest/test-helper';
import * as utils from '@n1ru4l/dockest/test-helper';
// eslint-disable-next-line import/no-extraneous-dependencies
import { ExecutionResult, print } from 'graphql';
import { TypedDocumentNode } from '@graphql-typed-document-node/core';
@ -45,8 +45,11 @@ export async function execute<TResult, TVariables>(
: {}),
},
});
const body = (await response.json()) as ExecutionResult<TResult>;
return {
body: (await response.json()) as ExecutionResult<TResult>,
body,
status: response.status,
};
}

View file

@ -1,4 +1,4 @@
import * as utils from 'dockest/test-helper';
import * as utils from '@n1ru4l/dockest/test-helper';
import { fetch } from '@whatwg-node/fetch';
const usageAddress = utils.getServiceAddress('usage', 3006);

View file

@ -0,0 +1,12 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"baseUrl": ".",
"esModuleInterop": true,
"paths": {
"@hive/server": ["../packages/services/server/src/api.ts"],
"@hive/storage": ["../packages/services/storage/src/index.ts"]
}
},
"include": ["testkit", "tests", "../node_modules/dockest/dist/@types.d.ts"]
}

View file

@ -1 +1 @@
export const version = '0.19.0';
export const version = '0.20.0';

View file

@ -24,7 +24,6 @@
"abort-controller": "3.0.0",
"dataloader": "2.0.0",
"date-fns": "2.25.0",
"emittery": "0.10.0",
"got": "12.5.1",
"graphql-modules": "2.0.0",
"graphql-parse-resolve-info": "4.12.0",

View file

@ -13,7 +13,6 @@ import { HttpClient } from './modules/shared/providers/http-client';
import { IdTranslator } from './modules/shared/providers/id-translator';
import { IdempotentRunner } from './modules/shared/providers/idempotent-runner';
import { Logger } from './modules/shared/providers/logger';
import { MessageBus } from './modules/shared/providers/message-bus';
import { CryptoProvider, encryptionSecretProvider } from './modules/shared/providers/crypto';
import { RedisConfig, REDIS_CONFIG, RedisProvider } from './modules/shared/providers/redis';
import { Storage } from './modules/shared/providers/storage';
@ -108,7 +107,6 @@ export function createRegistry({
const providers = [
HttpClient,
IdTranslator,
MessageBus,
RedisProvider,
IdempotentRunner,
CryptoProvider,

View file

@ -29,3 +29,7 @@ export { HttpClient } from './modules/shared/providers/http-client';
export { OperationsManager } from './modules/operations/providers/operations-manager';
export { OperationsReader } from './modules/operations/providers/operations-reader';
export { ClickHouse } from './modules/operations/providers/clickhouse-client';
export {
organizationAdminScopes,
reservedOrganizationNames,
} from './modules/organization/providers/organization-config';

View file

@ -4,13 +4,7 @@ import type { Listify, MapToArray } from '../../../shared/helpers';
import { AccessError } from '../../../shared/errors';
import { share } from '../../../shared/helpers';
import { Storage } from '../../shared/providers/storage';
import { MessageBus } from '../../shared/providers/message-bus';
import { IdempotentRunner } from '../../shared/providers/idempotent-runner';
import { TokenStorage } from '../../token/providers/token-storage';
import {
ENSURE_PERSONAL_ORGANIZATION_EVENT,
EnsurePersonalOrganizationEventPayload,
} from '../../organization/providers/events';
import { ApiToken } from './tokens';
import { OrganizationAccess, OrganizationAccessScope, OrganizationUserScopesSelector } from './organization-access';
import { ProjectAccess, ProjectAccessScope, ProjectUserScopesSelector } from './project-access';
@ -61,9 +55,7 @@ export class AuthManager {
private targetAccess: TargetAccess,
private userManager: UserManager,
private tokenStorage: TokenStorage,
private messageBus: MessageBus,
private storage: Storage,
private idempotentRunner: IdempotentRunner
private storage: Storage
) {
this.session = context.session;
}
@ -183,47 +175,16 @@ export class AuthManager {
throw new AccessError('Authorization token is missing');
}
const { session } = this;
return await this.idempotentRunner.run({
identifier: `user:create:${session.superTokensUserId}`,
executor: () =>
this.ensureInternalUser({
superTokensUserId: session.superTokensUserId,
email: session.email,
externalAuthUserId: session.externalUserId,
}),
ttl: 60,
});
});
private async ensureInternalUser(input: {
superTokensUserId: string;
email: string;
externalAuthUserId: string | null;
}): Promise<User> {
let internalUser = await this.storage.getUserBySuperTokenId({
superTokensUserId: input.superTokensUserId,
const user = await this.storage.getUserBySuperTokenId({
superTokensUserId: this.session.superTokensUserId,
});
if (!internalUser) {
internalUser = await this.userManager.createUser({
superTokensUserId: input.superTokensUserId,
externalAuthUserId: input.externalAuthUserId,
email: input.email,
});
if (!user) {
throw new AccessError('User not found');
}
await this.messageBus.emit<EnsurePersonalOrganizationEventPayload>(ENSURE_PERSONAL_ORGANIZATION_EVENT, {
name: internalUser.displayName,
user: {
id: internalUser.id,
superTokensUserId: input.superTokensUserId,
},
});
return internalUser;
}
return user;
});
async updateCurrentUser(input: { displayName: string; fullName: string }): Promise<User> {
const user = await this.getCurrentUser();

View file

@ -6,8 +6,10 @@ import { Token } from '../../../shared/entities';
import { AccessError } from '../../../shared/errors';
import DataLoader from 'dataloader';
import { TokenStorage, TokenSelector } from '../../token/providers/token-storage';
import { OrganizationAccessScope } from './scopes';
import type { ProjectAccessScope } from './project-access';
import type { TargetAccessScope } from './target-access';
export { OrganizationAccessScope } from './scopes';
export interface OrganizationUserScopesSelector {
user: string;
@ -26,29 +28,6 @@ interface OrganizationTokenAccessSelector {
scope: OrganizationAccessScope;
}
export enum OrganizationAccessScope {
/**
* Read organization data (projects, targets, etc.)
*/
READ = 'organization:read',
/**
* Who can delete the organization
*/
DELETE = 'organization:delete',
/**
* Who can modify organization's settings
*/
SETTINGS = 'organization:settings',
/**
* Who can add/remove 3rd-party integrations (Slack, etc.)
*/
INTEGRATIONS = 'organization:integrations',
/**
* Who can manage members
*/
MEMBERS = 'organization:members',
}
const organizationAccessScopeValues = Object.values(OrganizationAccessScope);
function isOrganizationScope(scope: any): scope is OrganizationAccessScope {

View file

@ -2,7 +2,9 @@ import { Injectable, Scope } from 'graphql-modules';
import Dataloader from 'dataloader';
import { Logger } from '../../shared/providers/logger';
import { AccessError } from '../../../shared/errors';
import { ProjectAccessScope } from './scopes';
import { OrganizationAccess } from './organization-access';
export { ProjectAccessScope } from './scopes';
export interface ProjectUserAccessSelector {
user: string;
@ -23,33 +25,6 @@ interface ProjectTokenAccessSelector {
scope: ProjectAccessScope;
}
export enum ProjectAccessScope {
/**
* Read project data (targets, etc.)
*/
READ = 'project:read',
/**
* Who can delete the project
*/
DELETE = 'project:delete',
/**
* Who can modify projects's name
*/
SETTINGS = 'project:settings',
/**
* Who can manage alerts
*/
ALERTS = 'project:alerts',
/**
* Who can read Operations Store
*/
OPERATIONS_STORE_READ = 'project:operations-store:read',
/**
* Who can write to Operations Store
*/
OPERATIONS_STORE_WRITE = 'project:operations-store:write',
}
const projectAccessScopeValues = Object.values(ProjectAccessScope);
function isProjectScope(scope: any): scope is ProjectAccessScope {

View file

@ -0,0 +1,80 @@
export enum OrganizationAccessScope {
/**
* Read organization data (projects, targets, etc.)
*/
READ = 'organization:read',
/**
* Who can delete the organization
*/
DELETE = 'organization:delete',
/**
* Who can modify organization's settings
*/
SETTINGS = 'organization:settings',
/**
* Who can add/remove 3rd-party integrations (Slack, etc.)
*/
INTEGRATIONS = 'organization:integrations',
/**
* Who can manage members
*/
MEMBERS = 'organization:members',
}
export enum ProjectAccessScope {
/**
* Read project data (targets, etc.)
*/
READ = 'project:read',
/**
* Who can delete the project
*/
DELETE = 'project:delete',
/**
* Who can modify projects's name
*/
SETTINGS = 'project:settings',
/**
* Who can manage alerts
*/
ALERTS = 'project:alerts',
/**
* Who can read Operations Store
*/
OPERATIONS_STORE_READ = 'project:operations-store:read',
/**
* Who can write to Operations Store
*/
OPERATIONS_STORE_WRITE = 'project:operations-store:write',
}
export enum TargetAccessScope {
/**
* Read target data
*/
READ = 'target:read',
/**
* Who can delete the target
*/
DELETE = 'target:delete',
/**
* Who can modify targets's name etc
*/
SETTINGS = 'target:settings',
/**
* Who can read registry
*/
REGISTRY_READ = 'target:registry:read',
/**
* Who can manage registry
*/
REGISTRY_WRITE = 'target:registry:write',
/**
* Who can read tokens
*/
TOKENS_READ = 'target:tokens:read',
/**
* Who can manage tokens
*/
TOKENS_WRITE = 'target:tokens:write',
}

View file

@ -2,7 +2,9 @@ import { Injectable, Scope } from 'graphql-modules';
import Dataloader from 'dataloader';
import { Logger } from '../../shared/providers/logger';
import { AccessError } from '../../../shared/errors';
import { TargetAccessScope } from './scopes';
import { OrganizationAccess } from './organization-access';
export { TargetAccessScope } from './scopes';
export interface TargetUserAccessSelector {
user: string;
@ -25,37 +27,6 @@ interface TargetTokenAccessSelector {
scope: TargetAccessScope;
}
export enum TargetAccessScope {
/**
* Read target data
*/
READ = 'target:read',
/**
* Who can delete the target
*/
DELETE = 'target:delete',
/**
* Who can modify targets's name etc
*/
SETTINGS = 'target:settings',
/**
* Who can read registry
*/
REGISTRY_READ = 'target:registry:read',
/**
* Who can manage registry
*/
REGISTRY_WRITE = 'target:registry:write',
/**
* Who can read tokens
*/
TOKENS_READ = 'target:tokens:read',
/**
* Who can manage tokens
*/
TOKENS_WRITE = 'target:tokens:write',
}
const targetAccessScopeValues = Object.values(TargetAccessScope);
function isTargetScope(scope: any): scope is TargetAccessScope {

View file

@ -1,8 +0,0 @@
export const ENSURE_PERSONAL_ORGANIZATION_EVENT = 'ensure-personal-organization-event';
export interface EnsurePersonalOrganizationEventPayload {
name: string;
user: {
id: string;
superTokensUserId: string;
};
}

View file

@ -0,0 +1,41 @@
import { OrganizationAccessScope, ProjectAccessScope, TargetAccessScope } from '../../auth/providers/scopes';
export const reservedOrganizationNames = [
'registry',
'server',
'usage',
'graphql',
'api',
'auth',
'home',
'register',
'login',
'logout',
'signup',
'signin',
'signout',
'sign-up',
'sign-in',
'sign-out',
'manage',
'admin',
'stats',
'internal',
'general',
'dashboard',
'index',
'contact',
'docs',
'documentation',
'help',
'support',
'faq',
'knowledge',
'internal',
];
export const organizationAdminScopes = [
...Object.values(OrganizationAccessScope),
...Object.values(ProjectAccessScope),
...Object.values(TargetAccessScope),
];

View file

@ -8,7 +8,6 @@ import { Logger } from '../../shared/providers/logger';
import { Storage } from '../../shared/providers/storage';
import type { OrganizationSelector } from '../../shared/providers/storage';
import { share, cache, uuid, diffArrays, pushIfMissing } from '../../../shared/helpers';
import { MessageBus } from '../../shared/providers/message-bus';
import { ActivityManager } from '../../activity/providers/activity-manager';
import { BillingProvider } from '../../billing/providers/billing.provider';
import { TokenStorage } from '../../token/providers/token-storage';
@ -16,41 +15,7 @@ import { Emails } from '../../shared/providers/emails';
import { OrganizationAccessScope } from '../../auth/providers/organization-access';
import { ProjectAccessScope } from '../../auth/providers/project-access';
import { TargetAccessScope } from '../../auth/providers/target-access';
import { EnsurePersonalOrganizationEventPayload, ENSURE_PERSONAL_ORGANIZATION_EVENT } from './events';
const reservedNames = [
'registry',
'server',
'usage',
'graphql',
'api',
'auth',
'home',
'register',
'login',
'logout',
'signup',
'signin',
'signout',
'sign-up',
'sign-in',
'sign-out',
'manage',
'admin',
'stats',
'internal',
'general',
'dashboard',
'index',
'contact',
'docs',
'documentation',
'help',
'support',
'faq',
'knowledge',
'internal',
];
import { reservedOrganizationNames, organizationAdminScopes } from './organization-config';
/**
* Responsible for auth checks.
@ -68,15 +33,11 @@ export class OrganizationManager {
private storage: Storage,
private authManager: AuthManager,
private tokenStorage: TokenStorage,
private messageBus: MessageBus,
private activityManager: ActivityManager,
private billingProvider: BillingProvider,
private emails: Emails
) {
this.logger = logger.child({ source: 'OrganizationManager' });
this.messageBus.on<EnsurePersonalOrganizationEventPayload>(ENSURE_PERSONAL_ORGANIZATION_EVENT, data =>
this.ensurePersonalOrganization(data)
);
}
getOrganizationFromToken: () => Promise<Organization | never> = share(async () => {
@ -175,22 +136,14 @@ export class OrganizationManager {
}): Promise<Organization> {
const { name, type, user } = input;
this.logger.info('Creating an organization (input=%o)', input);
let cleanId = paramCase(name);
if (reservedNames.includes(cleanId) || (await this.storage.getOrganizationByCleanId({ cleanId }))) {
cleanId = paramCase(`${name}-${uuid(4)}`);
}
const organization = await this.storage.createOrganization({
name,
cleanId,
cleanId: paramCase(name),
type,
user: user.id,
scopes: [
...Object.values(OrganizationAccessScope),
...Object.values(ProjectAccessScope),
...Object.values(TargetAccessScope),
],
scopes: organizationAdminScopes,
reservedNames: reservedOrganizationNames,
});
await this.activityManager.create({
@ -587,19 +540,4 @@ export class OrganizationManager {
organization: input.organization,
});
}
async ensurePersonalOrganization(payload: EnsurePersonalOrganizationEventPayload) {
const myOrg = await this.storage.getMyOrganization({
user: payload.user.id,
});
if (!myOrg) {
this.logger.info('Detected missing personal organization (user=%s)', payload.user.id);
await this.createOrganization({
name: payload.name,
user: payload.user,
type: OrganizationType.PERSONAL,
});
}
}
}

View file

@ -1,17 +0,0 @@
import { Injectable, Scope } from 'graphql-modules';
import Emittery from 'emittery';
@Injectable({
scope: Scope.Operation,
})
export class MessageBus {
private emitter = new Emittery();
on<TPayload>(event: string, listener: (payload: TPayload) => Promise<void>): void {
this.emitter.on(event, listener);
}
emit<TPayload>(event: string, payload: TPayload) {
return this.emitter.emitSerial(event, payload);
}
}

View file

@ -45,6 +45,14 @@ export interface PersistedOperationSelector extends ProjectSelector {
export interface Storage {
destroy(): Promise<void>;
ensureUserExists(_: {
superTokensUserId: string;
externalAuthUserId?: string | null;
email: string;
reservedOrgNames: string[];
scopes: ReadonlyArray<OrganizationAccessScope | ProjectAccessScope | TargetAccessScope>;
}): Promise<'created' | 'no_action'>;
getUserBySuperTokenId(_: { superTokensUserId: string }): Promise<User | null>;
setSuperTokensUserId(_: { auth0UserId: string; superTokensUserId: string; externalUserId: string }): Promise<void>;
getUserWithoutAssociatedSuperTokenIdByAuth0Email(_: { email: string }): Promise<User | null>;
@ -70,6 +78,7 @@ export interface Storage {
_: Pick<Organization, 'cleanId' | 'name' | 'type'> & {
user: string;
scopes: ReadonlyArray<OrganizationAccessScope | ProjectAccessScope | TargetAccessScope>;
reservedNames: string[];
}
): Promise<Organization | never>;
deleteOrganization(_: OrganizationSelector): Promise<Organization | never>;

View file

@ -25,6 +25,7 @@
"graphql": "16.5.0",
"hyperid": "2.3.1",
"reflect-metadata": "0.1.13",
"@trpc/server": "9.23.2",
"zod": "3.15.1",
"@whatwg-node/fetch": "0.4.7"
},

View file

@ -0,0 +1,31 @@
import { router } from '@trpc/server';
import type { inferAsyncReturnType } from '@trpc/server';
import { reservedOrganizationNames, organizationAdminScopes } from '@hive/api';
import type { Storage } from '@hive/api';
import { z } from 'zod';
export async function createContext({ storage }: { storage: Storage }) {
return {
storage,
};
}
export type Context = inferAsyncReturnType<typeof createContext>;
export const internalApiRouter = router<Context>().mutation('ensureUser', {
input: z
.object({
superTokensUserId: z.string().min(1),
email: z.string().min(1),
})
.required(),
resolve({ input, ctx }) {
return ctx.storage.ensureUserExists({
...input,
reservedOrgNames: reservedOrganizationNames,
scopes: organizationAdminScopes,
});
},
});
export type InternalApi = typeof internalApiRouter;

View file

@ -2,12 +2,14 @@
import 'reflect-metadata';
import { createServer, startMetrics, registerShutdown, reportReadiness } from '@hive/service-common';
import { fastifyTRPCPlugin } from '@trpc/server/adapters/fastify/dist/trpc-server-adapters-fastify.cjs.js';
import { createRegistry, LogFn, Logger } from '@hive/api';
import { createStorage as createPostgreSQLStorage, createConnectionString } from '@hive/storage';
import got from 'got';
import { stripIgnoredCharacters } from 'graphql';
import * as Sentry from '@sentry/node';
import { Dedupe, ExtraErrorData } from '@sentry/integrations';
import { internalApiRouter, createContext } from './api';
import { asyncStorage } from './async-storage';
import { graphqlHandler } from './graphql-handler';
import { clickHouseReadDuration, clickHouseElapsedDuration } from './metrics';
@ -225,6 +227,16 @@ export async function main() {
operationName: 'readiness',
});
await server.register(fastifyTRPCPlugin, {
prefix: '/trpc',
trpcOptions: {
router: internalApiRouter,
createContext() {
return createContext({ storage });
},
},
});
server.route({
method: ['GET', 'HEAD'],
url: '/_health',

View file

@ -25,6 +25,7 @@
"@theguild/buddy": "0.1.0",
"dotenv": "10.0.0",
"got": "12.5.1",
"param-case": "3.0.4",
"slonik": "30.1.2",
"slonik-interceptor-query-logging": "1.4.7",
"slonik-utilities": "1.9.4",

View file

@ -18,8 +18,9 @@ import type {
OrganizationType,
OrganizationInvitation,
} from '@hive/api';
import { sql, TaggedTemplateLiteralInvocation } from 'slonik';
import { DatabasePool, DatabaseTransactionConnection, sql, TaggedTemplateLiteralInvocation } from 'slonik';
import { update } from 'slonik-utilities';
import { paramCase } from 'param-case';
import {
commits,
getPool,
@ -51,6 +52,8 @@ export type WithMaybeMetadata<T> = T & {
metadata?: string | null;
};
type Connection = DatabasePool | DatabaseTransactionConnection;
const organizationGetStartedMapping: Record<Exclude<keyof Organization['getStarted'], 'id'>, keyof organizations> = {
creatingProject: 'get_started_creating_project',
publishingSchema: 'get_started_publishing_schema',
@ -282,12 +285,9 @@ export async function createStorage(connection: string, maximumPoolSize: number)
};
}
const storage: Storage = {
destroy() {
return pool.end();
},
async getUserBySuperTokenId({ superTokensUserId }) {
const user = await pool.maybeOne<Slonik<users>>(sql`
const shared = {
async getUserBySuperTokenId({ superTokensUserId }: { superTokensUserId: string }, connection: Connection) {
const user = await connection.maybeOne<Slonik<users>>(sql`
SELECT
*
FROM
@ -303,6 +303,163 @@ export async function createStorage(connection: string, maximumPoolSize: number)
return null;
},
async createUser(
{
superTokensUserId,
email,
fullName,
displayName,
externalAuthUserId,
}: {
superTokensUserId: string;
email: string;
fullName: string;
displayName: string;
externalAuthUserId: string | null;
},
connection: Connection
) {
return transformUser(
await connection.one<Slonik<users>>(
sql`
INSERT INTO public.users
("email", "supertoken_user_id", "full_name", "display_name", "external_auth_user_id")
VALUES
(${email}, ${superTokensUserId}, ${fullName}, ${displayName}, ${externalAuthUserId})
RETURNING *
`
)
);
},
async getOrganization(userId: string, connection: Connection) {
const org = await connection.maybeOne<Slonik<organizations>>(
sql`SELECT * FROM public.organizations WHERE user_id = ${userId} AND type = ${'PERSONAL'} LIMIT 1`
);
return org ? transformOrganization(org) : null;
},
async createOrganization(
{
name,
user,
cleanId,
type,
scopes,
reservedNames,
}: Parameters<Storage['createOrganization']>[0] & {
reservedNames: string[];
},
connection: Connection
) {
function addRandomHashToId(id: string) {
return `${id}-${Math.random().toString(16).substring(2, 6)}`;
}
async function ensureFreeCleanId(id: string, originalId: string | null): Promise<string> {
if (reservedNames.includes(id)) {
return ensureFreeCleanId(addRandomHashToId(id), originalId);
}
const orgCleanIdExists = await connection.exists(
sql`SELECT 1 FROM public.organizations WHERE clean_id = ${id} LIMIT 1`
);
if (orgCleanIdExists) {
return ensureFreeCleanId(addRandomHashToId(id), originalId);
}
return id;
}
const availableCleanId = await ensureFreeCleanId(cleanId, null);
const org = await connection.one<Slonik<organizations>>(
sql`
INSERT INTO public.organizations
("name", "clean_id", "type", "user_id")
VALUES
(${name}, ${availableCleanId}, ${type}, ${user})
RETURNING *
`
);
await connection.query<Slonik<organization_member>>(
sql`
INSERT INTO public.organization_member
("organization_id", "user_id", "scopes")
VALUES
(${org.id}, ${user}, ${sql.array(scopes, 'text')})
`
);
return transformOrganization(org);
},
};
function buildUserData(input: { superTokensUserId: string; email: string; externalAuthUserId: string | null }) {
const displayName = input.email.split('@')[0].slice(0, 25).padEnd(2, '1');
const fullName = input.email.split('@')[0].slice(0, 25).padEnd(2, '1');
return {
superTokensUserId: input.superTokensUserId,
email: input.email,
displayName,
fullName,
externalAuthUserId: input.externalAuthUserId,
};
}
const storage: Storage = {
destroy() {
return pool.end();
},
async ensureUserExists({
superTokensUserId,
externalAuthUserId,
email,
scopes,
reservedOrgNames,
}: {
superTokensUserId: string;
externalAuthUserId?: string | null;
email: string;
reservedOrgNames: string[];
scopes: Parameters<Storage['createOrganization']>[0]['scopes'];
}) {
return pool.transaction(async t => {
let action: 'created' | 'no_action' = 'no_action';
let internalUser = await shared.getUserBySuperTokenId({ superTokensUserId }, t);
if (!internalUser) {
internalUser = await shared.createUser(
buildUserData({ superTokensUserId, email, externalAuthUserId: externalAuthUserId ?? null }),
t
);
action = 'created';
}
const personalOrg = await shared.getOrganization(internalUser.id, t);
if (!personalOrg) {
await shared.createOrganization(
{
name: internalUser.displayName,
user: internalUser.id,
cleanId: paramCase(internalUser.displayName),
type: 'PERSONAL' as OrganizationType,
scopes,
reservedNames: reservedOrgNames,
},
t
);
action = 'created';
}
return action;
});
},
async getUserBySuperTokenId({ superTokensUserId }) {
return shared.getUserBySuperTokenId({ superTokensUserId }, pool);
},
async getUserWithoutAssociatedSuperTokenIdByAuth0Email({ email }) {
const user = await pool.maybeOne<Slonik<users>>(sql`
SELECT
@ -342,18 +499,8 @@ export async function createStorage(connection: string, maximumPoolSize: number)
return null;
},
async createUser({ superTokensUserId, email, fullName, displayName, externalAuthUserId }) {
return transformUser(
await pool.one<Slonik<users>>(
sql`
INSERT INTO public.users
("email", "supertoken_user_id", "full_name", "display_name", "external_auth_user_id")
VALUES
(${email}, ${superTokensUserId}, ${fullName}, ${displayName}, ${externalAuthUserId})
RETURNING *
`
)
);
createUser(input) {
return shared.createUser(input, pool);
},
async updateUser({ id, displayName, fullName }) {
return transformUser(
@ -365,29 +512,8 @@ export async function createStorage(connection: string, maximumPoolSize: number)
`)
);
},
async createOrganization({ name, cleanId, type, user, scopes }) {
const org = transformOrganization(
await pool.one<Slonik<organizations>>(
sql`
INSERT INTO public.organizations
("name", "clean_id", "type", "user_id")
VALUES
(${name}, ${cleanId}, ${type}, ${user})
RETURNING *
`
)
);
await pool.query<Slonik<organization_member>>(
sql`
INSERT INTO public.organization_member
("organization_id", "user_id", "scopes")
VALUES
(${org.id}, ${user}, ${sql.array(scopes, 'text')})
`
);
return org;
createOrganization(input) {
return pool.transaction(t => shared.createOrganization(input, t));
},
async deleteOrganization({ organization }) {
const result = transformOrganization(

View file

@ -10,6 +10,7 @@ The following environment variables configure the application.
| --------------------------------------- | ------------------------------------------ | ------------------------------------------------------------------------------------- | ---------------------------------------------------- |
| `APP_BASE_URL` | **Yes** | The base url of the app, | `https://app.graphql-hive.com` |
| `GRAPHQL_ENDPOINT` | **Yes** | The endpoint of the Hive GraphQL API. | `http://127.0.0.1:4000/graphql` |
| `SERVER_ENDPOINT` | **Yes** | The endpoint of the Hive server. | `http://127.0.0.1:4000` |
| `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` |

View file

@ -6,6 +6,7 @@ const BaseSchema = zod.object({
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(),
@ -125,6 +126,7 @@ const config = {
environment: base.ENVIRONMENT,
appBaseUrl: base.APP_BASE_URL,
graphqlEndpoint: base.GRAPHQL_ENDPOINT,
serverEndpoint: base.SERVER_ENDPOINT,
emailsEndpoint: base.EMAILS_ENDPOINT,
supertokens: {
connectionUri: base.SUPERTOKENS_CONNECTION_URI,

View file

@ -56,7 +56,7 @@
"js-cookie": "3.0.1",
"monaco-editor": "0.27.0",
"monaco-themes": "0.3.3",
"next": "12.3.0",
"next": "12.3.1",
"node-crisp-api": "1.12.3",
"react": "17.0.2",
"react-children-utilities": "2.7.1",
@ -79,6 +79,7 @@
"yup": "0.32.11",
"supertokens-auth-react": "0.26.3",
"supertokens-node": "12.0.2",
"supertokens-js-override": "0.0.4",
"nextjs-cors": "2.1.1",
"@whatwg-node/fetch": "0.4.7",
"zod": "3.15.1",
@ -103,7 +104,8 @@
"postcss": "8.4.13",
"tailwindcss": "2.2.19",
"tailwindcss-radix": "2.2.0",
"@hive/emails": "*"
"@hive/emails": "*",
"@hive/server": "*"
},
"babelMacros": {
"twin": {

View file

@ -2,6 +2,7 @@ import ThirdPartyEmailPasswordNode from 'supertokens-node/recipe/thirdpartyemail
import SessionNode from 'supertokens-node/recipe/session';
import type { TypeInput } from 'supertokens-node/types';
import EmailVerification from 'supertokens-node/recipe/emailverification';
import { OverrideableBuilder } from 'supertokens-js-override/lib/build';
import type { TypeProvider } from 'supertokens-node/recipe/thirdparty/types';
import type { TypeInput as ThirdPartEmailPasswordTypeInput } from 'supertokens-node/recipe/thirdpartyemailpassword/types';
import { fetch } from '@whatwg-node/fetch';
@ -10,12 +11,16 @@ import zod from 'zod';
import * as crypto from 'crypto';
import { createTRPCClient } from '@trpc/client';
import type { EmailsApi } from '@hive/emails';
import type { InternalApi } from '@hive/server';
import { env } from '@/env/backend';
export const backendConfig = (): TypeInput => {
const trpcService = createTRPCClient<EmailsApi>({
const emailsService = createTRPCClient<EmailsApi>({
url: `${env.emailsEndpoint}/trpc`,
});
const internalApi = createTRPCClient<InternalApi>({
url: `${env.serverEndpoint}/trpc`,
});
const providers: Array<TypeProvider> = [];
if (env.auth.github) {
@ -49,7 +54,7 @@ export const backendConfig = (): TypeInput => {
...originalImplementation,
async sendEmail(input) {
if (input.type === 'PASSWORD_RESET') {
await trpcService.mutation('sendPasswordResetEmail', {
await emailsService.mutation('sendPasswordResetEmail', {
user: {
id: input.user.id,
email: input.user.email,
@ -63,11 +68,10 @@ export const backendConfig = (): TypeInput => {
},
}),
},
override:
/**
* These overrides are only relevant for the legacy Auth0 -> SuperTokens migration (period).
*/
env.auth.legacyAuth0 ? getAuth0Overrides(env.auth.legacyAuth0) : undefined,
override: composeSuperTokensOverrides([
getEnsureUserOverrides(internalApi),
env.auth.legacyAuth0 ? getAuth0Overrides(env.auth.legacyAuth0) : null,
]),
}),
EmailVerification.init({
mode: env.auth.requireEmailVerification ? 'REQUIRED' : 'OPTIONAL',
@ -76,7 +80,7 @@ export const backendConfig = (): TypeInput => {
...originalImplementation,
sendEmail: async input => {
if (input.type === 'EMAIL_VERIFICATION') {
await trpcService.mutation('sendEmailVerificationEmail', {
await emailsService.mutation('sendEmailVerificationEmail', {
user: {
id: input.user.id,
email: input.user.email,
@ -131,125 +135,197 @@ export const backendConfig = (): TypeInput => {
};
};
function getEnsureUserOverrides(internalApi: ReturnType<typeof createTRPCClient<InternalApi>>) {
const override: ThirdPartEmailPasswordTypeInput['override'] = {
// here: https://supertokens.com/docs/thirdpartyemailpassword/common-customizations/handling-signinup-success
apis: originalImplementation => {
// override the email password sign up API
const emailPasswordSignUpPOST: typeof originalImplementation['emailPasswordSignUpPOST'] = async input => {
if (!originalImplementation.emailPasswordSignUpPOST) {
throw Error('emailPasswordSignUpPOST is not available');
}
const response = await originalImplementation.emailPasswordSignUpPOST(input);
if (response.status === 'OK') {
await internalApi.mutation('ensureUser', {
superTokensUserId: response.user.id,
email: response.user.email,
});
}
return response;
};
// override the email password sign in API
const emailPasswordSignInPOST: typeof originalImplementation['emailPasswordSignInPOST'] = async input => {
if (originalImplementation.emailPasswordSignInPOST === undefined) {
throw Error('Should never come here');
}
const response = await originalImplementation.emailPasswordSignInPOST(input);
if (response.status === 'OK') {
await internalApi.mutation('ensureUser', {
superTokensUserId: response.user.id,
email: response.user.email,
});
}
return response;
};
// override the third party sign in API
const thirdPartySignInUpPOST: typeof originalImplementation['thirdPartySignInUpPOST'] = async input => {
if (originalImplementation.thirdPartySignInUpPOST === undefined) {
throw Error('Should never come here');
}
const response = await originalImplementation.thirdPartySignInUpPOST(input);
if (response.status === 'OK') {
await internalApi.mutation('ensureUser', {
superTokensUserId: response.user.id,
email: response.user.email,
});
}
return response;
};
return {
...originalImplementation,
emailPasswordSignUpPOST,
emailPasswordSignInPOST,
thirdPartySignInUpPOST,
};
},
};
return override;
}
//
// LEGACY Auth0 Utilities
// These are only required for the Auth0 -> SuperTokens migrations and can be removed once the migration (period) is complete.
//
const getAuth0Overrides = (config: Exclude<typeof env.auth.legacyAuth0, null>) => {
const override: ThirdPartEmailPasswordTypeInput['override'] = {
apis(originalImplementation) {
return {
...originalImplementation,
async generatePasswordResetTokenPOST(input) {
const email = input.formFields.find(formField => formField.id === 'email')?.value;
const apis: NonNullable<ThirdPartEmailPasswordTypeInput['override']>['apis'] = originalImplementation => {
return {
...originalImplementation,
async generatePasswordResetTokenPOST(input) {
const email = input.formFields.find(formField => formField.id === 'email')?.value;
if (email) {
// We first use the existing implementation for looking for users within supertokens.
const users = await ThirdPartyEmailPasswordNode.getUsersByEmail(email);
if (email) {
// We first use the existing implementation for looking for users within supertokens.
const users = await ThirdPartyEmailPasswordNode.getUsersByEmail(email);
// If there is no email/password SuperTokens user yet, we need to check if there is an Auth0 user for this email.
if (users.some(user => user.thirdParty == null) === false) {
// RPC call to check if email/password user exists in Auth0
const dbUser = await checkWhetherAuth0EmailUserWithoutAssociatedSuperTokensIdExists(config, { email });
// If there is no email/password SuperTokens user yet, we need to check if there is an Auth0 user for this email.
if (users.some(user => user.thirdParty == null) === false) {
// RPC call to check if email/password user exists in Auth0
const dbUser = await checkWhetherAuth0EmailUserWithoutAssociatedSuperTokensIdExists(config, { email });
if (dbUser) {
// If we have this user within our database we create our new supertokens user
const newUserResult = await ThirdPartyEmailPasswordNode.emailPasswordSignUp(
dbUser.email,
await generateRandomPassword()
);
if (newUserResult.status === 'OK') {
// link the db record to the new supertokens user
await setUserIdMapping(config, {
auth0UserId: dbUser.auth0UserId,
supertokensUserId: newUserResult.user.id,
});
}
}
}
}
return await originalImplementation.generatePasswordResetTokenPOST!(input);
},
};
},
functions(originalImplementation) {
return {
...originalImplementation,
async emailPasswordSignIn(input) {
if (await doesUserExistInAuth0(config, input.email)) {
// check if user exists in SuperTokens
const superTokensUsers = await this.getUsersByEmail({
email: input.email,
userContext: input.userContext,
});
const emailPasswordUser =
// if the thirdParty field in the user object is undefined, then the user is an EmailPassword account.
superTokensUsers.find(superTokensUser => superTokensUser.thirdParty === undefined) ?? null;
// EmailPassword user does not exist in SuperTokens
// We first need to verify whether the password is legit,then if so, create a new user in SuperTokens with the same password.
if (emailPasswordUser === null) {
const auth0UserData = await trySignIntoAuth0WithUserCredentialsAndRetrieveUserInfo(
config,
input.email,
input.password
if (dbUser) {
// If we have this user within our database we create our new supertokens user
const newUserResult = await ThirdPartyEmailPasswordNode.emailPasswordSignUp(
dbUser.email,
await generateRandomPassword()
);
if (auth0UserData === null) {
// Invalid credentials -> Sent this to the client.
return {
status: 'WRONG_CREDENTIALS_ERROR',
};
if (newUserResult.status === 'OK') {
// link the db record to the new supertokens user
await setUserIdMapping(config, {
auth0UserId: dbUser.auth0UserId,
supertokensUserId: newUserResult.user.id,
});
}
// If the Auth0 credentials are correct we can successfully create the user in supertokens.
const response = await this.emailPasswordSignUp(input);
if (response.status !== 'OK') {
return {
status: 'WRONG_CREDENTIALS_ERROR',
};
}
await setUserIdMapping(config, {
auth0UserId: auth0UserData.sub,
supertokensUserId: response.user.id,
});
return response;
}
}
}
return originalImplementation.emailPasswordSignIn(input);
},
async thirdPartySignInUp(input) {
const externalUserId = `${input.thirdPartyId}|${input.thirdPartyUserId}`;
// Sign up the user with SuperTokens.
const response = await originalImplementation.thirdPartySignInUp(input);
return await originalImplementation.generatePasswordResetTokenPOST!(input);
},
};
};
// Auth0 user exists
if (response.status === 'OK') {
// We always make sure that we set the user mapping between Auth0 and SuperTokens.
const functions: NonNullable<ThirdPartEmailPasswordTypeInput['override']>['functions'] = originalImplementation => {
return {
...originalImplementation,
async emailPasswordSignIn(input) {
if (await doesUserExistInAuth0(config, input.email)) {
// check if user exists in SuperTokens
const superTokensUsers = await this.getUsersByEmail({
email: input.email,
userContext: input.userContext,
});
const emailPasswordUser =
// if the thirdParty field in the user object is undefined, then the user is an EmailPassword account.
superTokensUsers.find(superTokensUser => superTokensUser.thirdParty === undefined) ?? null;
// EmailPassword user does not exist in SuperTokens
// We first need to verify whether the password is legit,then if so, create a new user in SuperTokens with the same password.
if (emailPasswordUser === null) {
const auth0UserData = await trySignIntoAuth0WithUserCredentialsAndRetrieveUserInfo(
config,
input.email,
input.password
);
if (auth0UserData === null) {
// Invalid credentials -> Sent this to the client.
return {
status: 'WRONG_CREDENTIALS_ERROR',
};
}
// If the Auth0 credentials are correct we can successfully create the user in supertokens.
const response = await this.emailPasswordSignUp(input);
if (response.status !== 'OK') {
return {
status: 'WRONG_CREDENTIALS_ERROR',
};
}
await setUserIdMapping(config, {
auth0UserId: externalUserId,
auth0UserId: auth0UserData.sub,
supertokensUserId: response.user.id,
});
response.createdNewUser = false;
return response;
}
}
// Auth0 user does not exist
return await originalImplementation.thirdPartySignInUp(input);
},
};
},
return originalImplementation.emailPasswordSignIn(input);
},
async thirdPartySignInUp(input) {
const externalUserId = `${input.thirdPartyId}|${input.thirdPartyUserId}`;
// Sign up the user with SuperTokens.
const response = await originalImplementation.thirdPartySignInUp(input);
// Auth0 user exists
if (response.status === 'OK') {
// We always make sure that we set the user mapping between Auth0 and SuperTokens.
await setUserIdMapping(config, {
auth0UserId: externalUserId,
supertokensUserId: response.user.id,
});
response.createdNewUser = false;
return response;
}
// Auth0 user does not exist
return await originalImplementation.thirdPartySignInUp(input);
},
};
};
return override;
return {
apis,
functions,
};
};
/**
@ -427,3 +503,34 @@ async function generateRandomPassword(): Promise<string> {
})
);
}
/** * Utility function for composing multiple (dynamic SuperTokens overrides). */
const composeSuperTokensOverrides = (overrides: Array<ThirdPartEmailPasswordTypeInput['override'] | null>) => ({
apis: (
originalImplementation: ReturnType<
Exclude<Exclude<ThirdPartEmailPasswordTypeInput['override'], undefined>['apis'], undefined>
>,
builder: OverrideableBuilder<ThirdPartyEmailPasswordNode.APIInterface> | undefined
) => {
let impl = originalImplementation;
for (const override of overrides) {
if (typeof override?.apis === 'function') {
impl = override.apis(impl, builder);
}
}
return impl;
},
functions: (
originalImplementation: ReturnType<
Exclude<Exclude<ThirdPartEmailPasswordTypeInput['override'], undefined>['functions'], undefined>
>
) => {
let impl = originalImplementation;
for (const override of overrides) {
if (typeof override?.functions === 'function') {
impl = override.functions(impl);
}
}
return impl;
},
});

View file

@ -70,6 +70,8 @@ export function useRouteSelector() {
[router, push]
);
const replace = useCallback((url: string) => router.replace(url), [router.replace]);
// useMemo is necessary because we return new object and on every rerender `router` object will be different
return useMemo(
() => ({
@ -78,7 +80,7 @@ export function useRouteSelector() {
query: router.query,
update,
push,
replace: router.replace,
replace,
visitHome,
organizationId: router.query.orgId as string,
visitOrganization,

View file

@ -1,13 +1,12 @@
{
"extends": "../../../tsconfig.json",
"compilerOptions": {
"baseUrl": ".",
"target": "es5",
"target": "esnext",
"lib": ["dom", "dom.iterable", "esnext"],
"downlevelIteration": true,
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
@ -17,11 +16,6 @@
"isolatedModules": true,
"jsx": "preserve",
"jsxImportSource": "@emotion/react",
"paths": {
"@hive/emails": ["../../services/emails/src/api.ts"],
"@hive/service-common": ["../../services/service-common/src/index.ts"],
"@/*": ["src/*"]
},
"incremental": true
},
"include": ["next-env.d.ts", "modules.d.ts", "twin.d.ts", "src", "pages", "environment.ts"],

View file

@ -8,7 +8,7 @@
"build": "bob runify --single"
},
"dependencies": {
"next": "12.3.0",
"next": "12.3.1",
"nextra": "2.0.0-beta.5",
"nextra-theme-docs": "2.0.0-beta.5",
"react": "17.0.2",

View file

@ -10,7 +10,7 @@
"@radix-ui/react-tooltip": "0.1.7",
"@theguild/components": "1.12.1-alpha-bbdd479.0",
"framer-motion": "^6.3.3",
"next": "12.3.0",
"next": "12.3.1",
"react": "17.0.2",
"react-dom": "17.0.2",
"react-icons": "4.4.0",

View file

@ -1,7 +1,7 @@
diff --git a/node_modules/dockest/dist/run/bootstrap/getParsedComposeFile.js b/node_modules/dockest/dist/run/bootstrap/getParsedComposeFile.js
diff --git a/node_modules/@n1ru4l/dockest/dist/run/bootstrap/getParsedComposeFile.js b/node_modules/@n1ru4l/dockest/dist/run/bootstrap/getParsedComposeFile.js
index 9734743..ae3c5c8 100644
--- a/node_modules/dockest/dist/run/bootstrap/getParsedComposeFile.js
+++ b/node_modules/dockest/dist/run/bootstrap/getParsedComposeFile.js
--- a/node_modules/@n1ru4l/dockest/dist/run/bootstrap/getParsedComposeFile.js
+++ b/node_modules/@n1ru4l/dockest/dist/run/bootstrap/getParsedComposeFile.js
@@ -18,6 +18,22 @@ const PortBinding = io.type({
published: io.number,
target: io.number,

View file

@ -25,7 +25,7 @@
"paths": {
"@hive/api": ["./packages/services/api/src/index.ts"],
"@hive/service-common": ["./packages/services/service-common/src/index.ts"],
"@hive/server": ["./packages/services/server/src/index.ts"],
"@hive/server": ["./packages/services/server/src/api.ts"],
"@hive/stripe-billing": ["./packages/services/stripe-billing/src/api.ts"],
"@hive/schema": ["./packages/services/schema/src/api.ts"],
"@hive/usage-common": ["./packages/services/usage-common/src/index.ts"],
@ -39,7 +39,8 @@
"@hive/storage": ["./packages/services/storage/src/index.ts"],
"@graphql-hive/client": ["./packages/libraries/client/src/index.ts"],
"@graphql-hive/external-composition": ["./packages/libraries/external-composition/src/index.ts"],
"@graphql-hive/core": ["./packages/libraries/core/src/index.ts"]
"@graphql-hive/core": ["./packages/libraries/core/src/index.ts"],
"@/*": ["./packages/web/app/src/*"]
}
},
"include": ["packages"],

200
yarn.lock
View file

@ -3407,6 +3407,20 @@
"@monaco-editor/loader" "^1.2.0"
prop-types "^15.7.2"
"@n1ru4l/dockest@2.1.0-rc.6":
version "2.1.0-rc.6"
resolved "https://registry.yarnpkg.com/@n1ru4l/dockest/-/dockest-2.1.0-rc.6.tgz#b345eea2638c6d8da089f5538f160a4c38f4d3e5"
integrity sha512-EC1NcAovOwtptfw3T0y05FuTCSwH7CV6RKDaeJsldr6j3gnRpPQ/7NL3ssKcXx/wt+5uWq7/qPMuzNQVOPhfoQ==
dependencies:
chalk "^3.0.0"
execa "^4.0.0"
fp-ts "^2.8.3"
io-ts "^2.2.10"
is-docker "^2.0.0"
js-yaml "^3.13.1"
rxjs "^6.5.4"
toposort "^2.0.2"
"@n1ru4l/graphql-live-query@^0.9.0":
version "0.9.0"
resolved "https://registry.yarnpkg.com/@n1ru4l/graphql-live-query/-/graphql-live-query-0.9.0.tgz#defaebdd31f625bee49e6745934f36312532b2bc"
@ -3425,10 +3439,10 @@
prop-types "^15.6.0"
zen-observable "^0.8.6"
"@next/env@12.3.0":
version "12.3.0"
resolved "https://registry.yarnpkg.com/@next/env/-/env-12.3.0.tgz#85f971fdc668cc312342761057c59cb8ab1abadf"
integrity sha512-PTJpjAFVbzBQ9xXpzMTroShvD5YDIIy46jQ7d4LrWpY+/5a8H90Tm8hE3Hvkc5RBRspVo7kvEOnqQms0A+2Q6w==
"@next/env@12.3.1":
version "12.3.1"
resolved "https://registry.yarnpkg.com/@next/env/-/env-12.3.1.tgz#18266bd92de3b4aa4037b1927aa59e6f11879260"
integrity sha512-9P9THmRFVKGKt9DYqeC2aKIxm8rlvkK38V1P1sRE7qyoPBIs8l9oo79QoSdPtOWfzkbDAVUqvbQGgTMsb8BtJg==
"@next/eslint-plugin-next@12.1.6":
version "12.1.6"
@ -3437,70 +3451,70 @@
dependencies:
glob "7.1.7"
"@next/swc-android-arm-eabi@12.3.0":
version "12.3.0"
resolved "https://registry.yarnpkg.com/@next/swc-android-arm-eabi/-/swc-android-arm-eabi-12.3.0.tgz#9a934904643591cb6f66eb09803a92d2b10ada13"
integrity sha512-/PuirPnAKsYBw93w/7Q9hqy+KGOU9mjYprZ/faxMUJh/dc6v3rYLxkZKNG9nFPIW4QKNTCnhP40xF9hLnxO+xg==
"@next/swc-android-arm-eabi@12.3.1":
version "12.3.1"
resolved "https://registry.yarnpkg.com/@next/swc-android-arm-eabi/-/swc-android-arm-eabi-12.3.1.tgz#b15ce8ad376102a3b8c0f3c017dde050a22bb1a3"
integrity sha512-i+BvKA8tB//srVPPQxIQN5lvfROcfv4OB23/L1nXznP+N/TyKL8lql3l7oo2LNhnH66zWhfoemg3Q4VJZSruzQ==
"@next/swc-android-arm64@12.3.0":
version "12.3.0"
resolved "https://registry.yarnpkg.com/@next/swc-android-arm64/-/swc-android-arm64-12.3.0.tgz#c1e3e24d0625efe88f45a2135c8f5c4dff594749"
integrity sha512-OaI+FhAM6P9B6Ybwbn0Zl8YwWido0lLwhDBi9WiYCh4RQmIXAyVIoIJPHo4fP05+mXaJ/k1trvDvuURvHOq2qw==
"@next/swc-android-arm64@12.3.1":
version "12.3.1"
resolved "https://registry.yarnpkg.com/@next/swc-android-arm64/-/swc-android-arm64-12.3.1.tgz#85d205f568a790a137cb3c3f720d961a2436ac9c"
integrity sha512-CmgU2ZNyBP0rkugOOqLnjl3+eRpXBzB/I2sjwcGZ7/Z6RcUJXK5Evz+N0ucOxqE4cZ3gkTeXtSzRrMK2mGYV8Q==
"@next/swc-darwin-arm64@12.3.0":
version "12.3.0"
resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-12.3.0.tgz#37a9f971b9ad620184af69f38243a36757126fb9"
integrity sha512-9s4d3Mhii+WFce8o8Jok7WC3Bawkr9wEUU++SJRptjU1L5tsfYJMrSYCACHLhZujziNDLyExe4Hwwsccps1sfg==
"@next/swc-darwin-arm64@12.3.1":
version "12.3.1"
resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-12.3.1.tgz#b105457d6760a7916b27e46c97cb1a40547114ae"
integrity sha512-hT/EBGNcu0ITiuWDYU9ur57Oa4LybD5DOQp4f22T6zLfpoBMfBibPtR8XktXmOyFHrL/6FC2p9ojdLZhWhvBHg==
"@next/swc-darwin-x64@12.3.0":
version "12.3.0"
resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-12.3.0.tgz#fb017f1066c8cf2b8da49ef3588c8731d8bf1bf3"
integrity sha512-2scC4MqUTwGwok+wpVxP+zWp7WcCAVOtutki2E1n99rBOTnUOX6qXkgxSy083yBN6GqwuC/dzHeN7hIKjavfRA==
"@next/swc-darwin-x64@12.3.1":
version "12.3.1"
resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-12.3.1.tgz#6947b39082271378896b095b6696a7791c6e32b1"
integrity sha512-9S6EVueCVCyGf2vuiLiGEHZCJcPAxglyckTZcEwLdJwozLqN0gtS0Eq0bQlGS3dH49Py/rQYpZ3KVWZ9BUf/WA==
"@next/swc-freebsd-x64@12.3.0":
version "12.3.0"
resolved "https://registry.yarnpkg.com/@next/swc-freebsd-x64/-/swc-freebsd-x64-12.3.0.tgz#e7955b016f41e0f95088e3459ff4197027871fbf"
integrity sha512-xAlruUREij/bFa+qsE1tmsP28t7vz02N4ZDHt2lh3uJUniE0Ne9idyIDLc1Ed0IF2RjfgOp4ZVunuS3OM0sngw==
"@next/swc-freebsd-x64@12.3.1":
version "12.3.1"
resolved "https://registry.yarnpkg.com/@next/swc-freebsd-x64/-/swc-freebsd-x64-12.3.1.tgz#2b6c36a4d84aae8b0ea0e0da9bafc696ae27085a"
integrity sha512-qcuUQkaBZWqzM0F1N4AkAh88lLzzpfE6ImOcI1P6YeyJSsBmpBIV8o70zV+Wxpc26yV9vpzb+e5gCyxNjKJg5Q==
"@next/swc-linux-arm-gnueabihf@12.3.0":
version "12.3.0"
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm-gnueabihf/-/swc-linux-arm-gnueabihf-12.3.0.tgz#d2233267bffaa24378245b328f2f8a01a37eab29"
integrity sha512-jin2S4VT/cugc2dSZEUIabhYDJNgrUh7fufbdsaAezgcQzqfdfJqfxl4E9GuafzB4cbRPTaqA0V5uqbp0IyGkQ==
"@next/swc-linux-arm-gnueabihf@12.3.1":
version "12.3.1"
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm-gnueabihf/-/swc-linux-arm-gnueabihf-12.3.1.tgz#6e421c44285cfedac1f4631d5de330dd60b86298"
integrity sha512-diL9MSYrEI5nY2wc/h/DBewEDUzr/DqBjIgHJ3RUNtETAOB3spMNHvJk2XKUDjnQuluLmFMloet9tpEqU2TT9w==
"@next/swc-linux-arm64-gnu@12.3.0":
version "12.3.0"
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-12.3.0.tgz#149a0cb877352ab63e81cf1dd53b37f382929d2a"
integrity sha512-RqJHDKe0WImeUrdR0kayTkRWgp4vD/MS7g0r6Xuf8+ellOFH7JAAJffDW3ayuVZeMYOa7RvgNFcOoWnrTUl9Nw==
"@next/swc-linux-arm64-gnu@12.3.1":
version "12.3.1"
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-12.3.1.tgz#8863f08a81f422f910af126159d2cbb9552ef717"
integrity sha512-o/xB2nztoaC7jnXU3Q36vGgOolJpsGG8ETNjxM1VAPxRwM7FyGCPHOMk1XavG88QZSQf+1r+POBW0tLxQOJ9DQ==
"@next/swc-linux-arm64-musl@12.3.0":
version "12.3.0"
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-12.3.0.tgz#73ec7f121f56fd7cf99cf2b00cf41f62c4560e90"
integrity sha512-nvNWoUieMjvDjpYJ/4SQe9lQs2xMj6ZRs8N+bmTrVu9leY2Fg3WD6W9p/1uU9hGO8u+OdF13wc4iRShu/WYIHg==
"@next/swc-linux-arm64-musl@12.3.1":
version "12.3.1"
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-12.3.1.tgz#0038f07cf0b259d70ae0c80890d826dfc775d9f3"
integrity sha512-2WEasRxJzgAmP43glFNhADpe8zB7kJofhEAVNbDJZANp+H4+wq+/cW1CdDi8DqjkShPEA6/ejJw+xnEyDID2jg==
"@next/swc-linux-x64-gnu@12.3.0":
version "12.3.0"
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-12.3.0.tgz#6812e52ef21bfd091810f271dd61da11d82b66b9"
integrity sha512-4ajhIuVU9PeQCMMhdDgZTLrHmjbOUFuIyg6J19hZqwEwDTSqQyrSLkbJs2Nd7IRiM6Ul/XyrtEFCpk4k+xD2+w==
"@next/swc-linux-x64-gnu@12.3.1":
version "12.3.1"
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-12.3.1.tgz#c66468f5e8181ffb096c537f0dbfb589baa6a9c1"
integrity sha512-JWEaMyvNrXuM3dyy9Pp5cFPuSSvG82+yABqsWugjWlvfmnlnx9HOQZY23bFq3cNghy5V/t0iPb6cffzRWylgsA==
"@next/swc-linux-x64-musl@12.3.0":
version "12.3.0"
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-12.3.0.tgz#c9e7ffb6d44da330961c1ce651c5b03a1becfe22"
integrity sha512-U092RBYbaGxoMAwpauePJEu2PuZSEoUCGJBvsptQr2/2XIMwAJDYM4c/M5NfYEsBr+yjvsYNsOpYfeQ88D82Yg==
"@next/swc-linux-x64-musl@12.3.1":
version "12.3.1"
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-12.3.1.tgz#c6269f3e96ac0395bc722ad97ce410ea5101d305"
integrity sha512-xoEWQQ71waWc4BZcOjmatuvPUXKTv6MbIFzpm4LFeCHsg2iwai0ILmNXf81rJR+L1Wb9ifEke2sQpZSPNz1Iyg==
"@next/swc-win32-arm64-msvc@12.3.0":
version "12.3.0"
resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-12.3.0.tgz#e0d9d26297f52b0d3b3c2f5138ddcce30601bc98"
integrity sha512-pzSzaxjDEJe67bUok9Nxf9rykbJfHXW0owICFsPBsqHyc+cr8vpF7g9e2APTCddtVhvjkga9ILoZJ9NxWS7Yiw==
"@next/swc-win32-arm64-msvc@12.3.1":
version "12.3.1"
resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-12.3.1.tgz#83c639ee969cee36ce247c3abd1d9df97b5ecade"
integrity sha512-hswVFYQYIeGHE2JYaBVtvqmBQ1CppplQbZJS/JgrVI3x2CurNhEkmds/yqvDONfwfbttTtH4+q9Dzf/WVl3Opw==
"@next/swc-win32-ia32-msvc@12.3.0":
version "12.3.0"
resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-12.3.0.tgz#37daeac1acc68537b8e76cd81fde96dce11f78b4"
integrity sha512-MQGUpMbYhQmTZ06a9e0hPQJnxFMwETo2WtyAotY3GEzbNCQVbCGhsvqEKcl+ZEHgShlHXUWvSffq1ZscY6gK7A==
"@next/swc-win32-ia32-msvc@12.3.1":
version "12.3.1"
resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-12.3.1.tgz#52995748b92aa8ad053440301bc2c0d9fbcf27c2"
integrity sha512-Kny5JBehkTbKPmqulr5i+iKntO5YMP+bVM8Hf8UAmjSMVo3wehyLVc9IZkNmcbxi+vwETnQvJaT5ynYBkJ9dWA==
"@next/swc-win32-x64-msvc@12.3.0":
version "12.3.0"
resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-12.3.0.tgz#c1b983316307f8f55fee491942b5d244bd2036e2"
integrity sha512-C/nw6OgQpEULWqs+wgMHXGvlJLguPRFFGqR2TAqWBerQ8J+Sg3z1ZTqwelkSi4FoqStGuZ2UdFHIDN1ySmR1xA==
"@next/swc-win32-x64-msvc@12.3.1":
version "12.3.1"
resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-12.3.1.tgz#27d71a95247a9eaee03d47adee7e3bd594514136"
integrity sha512-W1ijvzzg+kPEX6LAc+50EYYSEo0FVu7dmTE+t+DM4iOLqgGHoW9uYSz9wCVdkXOEEMP9xhXfGpcSxsfDucyPkA==
"@nodelib/fs.scandir@2.1.4":
version "2.1.4"
@ -7624,11 +7638,16 @@ camelize@^1.0.0:
resolved "https://registry.yarnpkg.com/camelize/-/camelize-1.0.0.tgz#164a5483e630fa4321e5af07020e531831b2609b"
integrity sha1-FkpUg+Yw+kMh5a8HAg5TGDGyYJs=
caniuse-lite@^1.0.30001332, caniuse-lite@^1.0.30001335, caniuse-lite@^1.0.30001400:
caniuse-lite@^1.0.30001335, caniuse-lite@^1.0.30001400:
version "1.0.30001409"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001409.tgz#6135da9dcab34cd9761d9cdb12a68e6740c5e96e"
integrity sha512-V0mnJ5dwarmhYv8/MzhJ//aW68UpvnQBXv8lJ2QUsvn2pHcmAuNtu8hQEDz37XnA1iE+lRR9CIfGWWpgJ5QedQ==
caniuse-lite@^1.0.30001406:
version "1.0.30001425"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001425.tgz#52917791a453eb3265143d2cd08d80629e82c735"
integrity sha512-/pzFv0OmNG6W0ym80P3NtapU0QEiDS3VuYAZMGoLLqiC7f6FJFe1MjpQDREGApeenD9wloeytmVDj+JLXPC6qw==
capital-case@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/capital-case/-/capital-case-1.0.4.tgz#9d130292353c9249f6b00fa5852bee38a717e669"
@ -9049,20 +9068,6 @@ dlv@^1.1.3:
resolved "https://registry.yarnpkg.com/dlv/-/dlv-1.1.3.tgz#5c198a8a11453596e751494d49874bc7732f2e79"
integrity sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==
"dockest@npm:@n1ru4l/dockest@2.1.0-rc.6":
version "2.1.0-rc.6"
resolved "https://registry.yarnpkg.com/@n1ru4l/dockest/-/dockest-2.1.0-rc.6.tgz#b345eea2638c6d8da089f5538f160a4c38f4d3e5"
integrity sha512-EC1NcAovOwtptfw3T0y05FuTCSwH7CV6RKDaeJsldr6j3gnRpPQ/7NL3ssKcXx/wt+5uWq7/qPMuzNQVOPhfoQ==
dependencies:
chalk "^3.0.0"
execa "^4.0.0"
fp-ts "^2.8.3"
io-ts "^2.2.10"
is-docker "^2.0.0"
js-yaml "^3.13.1"
rxjs "^6.5.4"
toposort "^2.0.2"
doctrine@^2.1.0:
version "2.1.0"
resolved "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d"
@ -9246,11 +9251,6 @@ electron-to-chromium@^1.4.251:
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.257.tgz#895dc73c6bb58d1235dc80879ecbca0bcba96e2c"
integrity sha512-C65sIwHqNnPC2ADMfse/jWTtmhZMII+x6ADI9gENzrOiI7BpxmfKFE84WkIEl5wEg+7+SfIkwChDlsd1Erju2A==
emittery@0.10.0:
version "0.10.0"
resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.10.0.tgz#bb373c660a9d421bb44706ec4967ed50c02a8026"
integrity sha512-AGvFfs+d0JKCJQ4o01ASQLGPmSCxgfU9RFXvzPvZdjKK8oscynksuJhWrSTSw7j7Ep/sZct5b5ZhYCi8S/t0HQ==
emittery@^0.10.2:
version "0.10.2"
resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.10.2.tgz#902eec8aedb8c41938c46e9385e9db7e03182933"
@ -15335,31 +15335,31 @@ next-themes@^0.0.8:
resolved "https://registry.yarnpkg.com/next-themes/-/next-themes-0.0.8.tgz#2a1748317085afbc2509e2c32bd04af4f0f6cb7d"
integrity sha512-dyrh+/bZW4hkecFEg2rfwOLLzU2UnE7KfiwcV0mIwkPrO+1n1WvwkC8nabgKA5Eoi8stkYfjmA72FxTaWEOHtg==
next@12.3.0:
version "12.3.0"
resolved "https://registry.yarnpkg.com/next/-/next-12.3.0.tgz#0e4c1ed0092544c7e8f4c998ca57cf6529e286cb"
integrity sha512-GpzI6me9V1+XYtfK0Ae9WD0mKqHyzQlGq1xH1rzNIYMASo4Tkl4rTe9jSqtBpXFhOS33KohXs9ZY38Akkhdciw==
next@12.3.1:
version "12.3.1"
resolved "https://registry.yarnpkg.com/next/-/next-12.3.1.tgz#127b825ad2207faf869b33393ec8c75fe61e50f1"
integrity sha512-l7bvmSeIwX5lp07WtIiP9u2ytZMv7jIeB8iacR28PuUEFG5j0HGAPnMqyG5kbZNBG2H7tRsrQ4HCjuMOPnANZw==
dependencies:
"@next/env" "12.3.0"
"@next/env" "12.3.1"
"@swc/helpers" "0.4.11"
caniuse-lite "^1.0.30001332"
caniuse-lite "^1.0.30001406"
postcss "8.4.14"
styled-jsx "5.0.6"
styled-jsx "5.0.7"
use-sync-external-store "1.2.0"
optionalDependencies:
"@next/swc-android-arm-eabi" "12.3.0"
"@next/swc-android-arm64" "12.3.0"
"@next/swc-darwin-arm64" "12.3.0"
"@next/swc-darwin-x64" "12.3.0"
"@next/swc-freebsd-x64" "12.3.0"
"@next/swc-linux-arm-gnueabihf" "12.3.0"
"@next/swc-linux-arm64-gnu" "12.3.0"
"@next/swc-linux-arm64-musl" "12.3.0"
"@next/swc-linux-x64-gnu" "12.3.0"
"@next/swc-linux-x64-musl" "12.3.0"
"@next/swc-win32-arm64-msvc" "12.3.0"
"@next/swc-win32-ia32-msvc" "12.3.0"
"@next/swc-win32-x64-msvc" "12.3.0"
"@next/swc-android-arm-eabi" "12.3.1"
"@next/swc-android-arm64" "12.3.1"
"@next/swc-darwin-arm64" "12.3.1"
"@next/swc-darwin-x64" "12.3.1"
"@next/swc-freebsd-x64" "12.3.1"
"@next/swc-linux-arm-gnueabihf" "12.3.1"
"@next/swc-linux-arm64-gnu" "12.3.1"
"@next/swc-linux-arm64-musl" "12.3.1"
"@next/swc-linux-x64-gnu" "12.3.1"
"@next/swc-linux-x64-musl" "12.3.1"
"@next/swc-win32-arm64-msvc" "12.3.1"
"@next/swc-win32-ia32-msvc" "12.3.1"
"@next/swc-win32-x64-msvc" "12.3.1"
nextjs-cors@2.1.1:
version "2.1.1"
@ -19077,10 +19077,10 @@ styled-components@5.3.5:
shallowequal "^1.1.0"
supports-color "^5.5.0"
styled-jsx@5.0.6:
version "5.0.6"
resolved "https://registry.yarnpkg.com/styled-jsx/-/styled-jsx-5.0.6.tgz#fa684790a9cc3badded14badea163418fe568f77"
integrity sha512-xOeROtkK5MGMDimBQ3J6iPId8q0t/BDoG5XN6oKkZClVz9ISF/hihN8OCn2LggMU6N32aXnrXBdn3auSqNS9fA==
styled-jsx@5.0.7:
version "5.0.7"
resolved "https://registry.yarnpkg.com/styled-jsx/-/styled-jsx-5.0.7.tgz#be44afc53771b983769ac654d355ca8d019dff48"
integrity sha512-b3sUzamS086YLRuvnaDigdAewz1/EFYlHpYBP5mZovKEdQQOIIYq8lApylub3HHZ6xFjV051kkGU7cudJmrXEA==
stylis@4.0.13, stylis@^4.0.6:
version "4.0.13"