Create core.workspace at signup

This commit is contained in:
martmull
2024-02-01 17:22:38 +01:00
parent b801eddf7d
commit fa7480f3bb
13 changed files with 62 additions and 73 deletions
@@ -546,7 +546,7 @@ export type User = {
canImpersonate: Scalars['Boolean'];
createdAt: Scalars['DateTime'];
defaultAvatarUrl?: Maybe<Scalars['String']>;
defaultWorkspace?: Maybe<Workspace>;
defaultWorkspace: Workspace;
deletedAt?: Maybe<Scalars['DateTime']>;
disabled?: Maybe<Scalars['Boolean']>;
email: Scalars['String'];
@@ -790,7 +790,7 @@ export type ImpersonateMutationVariables = Exact<{
}>;
export type ImpersonateMutation = { __typename?: 'Mutation', impersonate: { __typename?: 'Verify', user: { __typename?: 'User', id: string, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: string, colorScheme: string, avatarUrl?: string | null, locale: string, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, defaultWorkspace?: { __typename?: 'Workspace', id: string, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, subscriptionStatus: string, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: string, key: string, value: boolean, workspaceId: string }> | null } | null }, tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } };
export type ImpersonateMutation = { __typename?: 'Mutation', impersonate: { __typename?: 'Verify', user: { __typename?: 'User', id: string, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: string, colorScheme: string, avatarUrl?: string | null, locale: string, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, defaultWorkspace: { __typename?: 'Workspace', id: string, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, subscriptionStatus: string, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: string, key: string, value: boolean, workspaceId: string }> | null } }, tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } };
export type RenewTokenMutationVariables = Exact<{
refreshToken: Scalars['String'];
@@ -821,7 +821,7 @@ export type VerifyMutationVariables = Exact<{
}>;
export type VerifyMutation = { __typename?: 'Mutation', verify: { __typename?: 'Verify', user: { __typename?: 'User', id: string, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: string, colorScheme: string, avatarUrl?: string | null, locale: string, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, defaultWorkspace?: { __typename?: 'Workspace', id: string, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, subscriptionStatus: string, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: string, key: string, value: boolean, workspaceId: string }> | null } | null }, tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } };
export type VerifyMutation = { __typename?: 'Mutation', verify: { __typename?: 'Verify', user: { __typename?: 'User', id: string, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: string, colorScheme: string, avatarUrl?: string | null, locale: string, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, defaultWorkspace: { __typename?: 'Workspace', id: string, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, subscriptionStatus: string, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: string, key: string, value: boolean, workspaceId: string }> | null } }, tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } };
export type CheckUserExistsQueryVariables = Exact<{
email: Scalars['String'];
@@ -858,7 +858,7 @@ export type UploadImageMutationVariables = Exact<{
export type UploadImageMutation = { __typename?: 'Mutation', uploadImage: string };
export type UserQueryFragmentFragment = { __typename?: 'User', id: string, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: string, colorScheme: string, avatarUrl?: string | null, locale: string, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, defaultWorkspace?: { __typename?: 'Workspace', id: string, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, subscriptionStatus: string, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: string, key: string, value: boolean, workspaceId: string }> | null } | null };
export type UserQueryFragmentFragment = { __typename?: 'User', id: string, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: string, colorScheme: string, avatarUrl?: string | null, locale: string, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, defaultWorkspace: { __typename?: 'Workspace', id: string, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, subscriptionStatus: string, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: string, key: string, value: boolean, workspaceId: string }> | null } };
export type DeleteUserAccountMutationVariables = Exact<{ [key: string]: never; }>;
@@ -877,7 +877,7 @@ export type CreateWorkspaceMutationVariables = Exact<{
}>;
export type CreateWorkspaceMutation = { __typename?: 'Mutation', createWorkspace: { __typename?: 'User', id: string, workspaceMember?: { __typename?: 'WorkspaceMember', id: string, colorScheme: string, avatarUrl?: string | null, locale: string, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, defaultWorkspace?: { __typename?: 'Workspace', id: string, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, subscriptionStatus: string, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: string, key: string, value: boolean, workspaceId: string }> | null } | null } };
export type CreateWorkspaceMutation = { __typename?: 'Mutation', createWorkspace: { __typename?: 'User', id: string, workspaceMember?: { __typename?: 'WorkspaceMember', id: string, colorScheme: string, avatarUrl?: string | null, locale: string, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, defaultWorkspace: { __typename?: 'Workspace', id: string, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, subscriptionStatus: string, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: string, key: string, value: boolean, workspaceId: string }> | null } } };
export type DeleteCurrentWorkspaceMutationVariables = Exact<{ [key: string]: never; }>;
@@ -141,13 +141,12 @@ export const useSignInUp = () => {
if (
billing?.isBillingEnabled &&
(!currentWorkspace ||
currentWorkspace.subscriptionStatus !== 'active')
currentWorkspace.subscriptionStatus !== 'active'
) {
navigate(AppPath.PlanRequired);
return;
}
if (currentWorkspace?.displayName) {
if (currentWorkspace.displayName) {
navigate(AppPath.Index);
return;
}
@@ -27,14 +27,6 @@ export const getOnboardingStatus = ({
if (!isLoggedIn) {
return OnboardingStatus.OngoingUserCreation;
}
// if the user has not been fetched yet, we can't know the onboarding status
if (currentWorkspace && !currentWorkspaceMember) {
return undefined;
}
if (!currentWorkspace && isBillingEnabled) {
return OnboardingStatus.Incomplete;
}
if (
isBillingEnabled &&
@@ -50,9 +42,12 @@ export const getOnboardingStatus = ({
if (!currentWorkspace?.displayName) {
return OnboardingStatus.OngoingWorkspaceCreation;
}
if (!currentWorkspaceMember) {
return undefined;
}
if (
!currentWorkspaceMember?.name.firstName ||
!currentWorkspaceMember?.name.lastName
!currentWorkspaceMember.name.firstName ||
!currentWorkspaceMember.name.lastName
) {
return OnboardingStatus.OngoingProfileCreation;
}
@@ -157,13 +157,13 @@ export const PasswordReset = () => {
if (
billing?.isBillingEnabled &&
(!currentWorkspace || currentWorkspace.subscriptionStatus !== 'active')
currentWorkspace.subscriptionStatus !== 'active'
) {
navigate(AppPath.PlanRequired);
return;
}
if (currentWorkspace?.displayName) {
if (currentWorkspace.displayName) {
navigate(AppPath.Index);
return;
}
@@ -1,8 +1,4 @@
import {
BadRequestException,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { BadRequestException, Injectable } from '@nestjs/common';
import { Request } from 'express';
@@ -46,10 +42,6 @@ export class ApiRestQueryBuilderFactory {
}> {
const workspace = await this.tokenService.validateToken(request);
if (!workspace) {
throw new UnauthorizedException();
}
const objectMetadataItems =
await this.objectMetadataService.findManyWithinWorkspace(workspace.id);
@@ -108,7 +108,7 @@ export class AuthService {
}
const passwordHash = password ? await hashPassword(password) : undefined;
let workspace: Workspace | null = null;
let workspace: Workspace | null;
if (workspaceInviteHash) {
workspace = await this.workspaceRepository.findOneBy({
@@ -126,6 +126,15 @@ export class AuthService {
'Sign up is disabled',
ForbiddenException,
);
const workspaceToCreate = this.workspaceRepository.create({
displayName: '',
domainName: '',
inviteHash: v4(),
subscriptionStatus: 'incomplete',
});
workspace = await this.workspaceRepository.save(workspaceToCreate);
}
let imagePath: string | undefined = undefined;
@@ -155,7 +164,7 @@ export class AuthService {
defaultAvatarUrl: imagePath,
canImpersonate: false,
passwordHash,
defaultWorkspace: workspace || undefined,
defaultWorkspace: workspace,
});
return await this.userRepository.save(userToCreate);
@@ -166,12 +175,24 @@ export class AuthService {
where: {
email,
},
relations: ['defaultWorkspace'],
});
assert(user, "This user doesn't exist", NotFoundException);
assert(
user.defaultWorkspace,
'User has no default workspace',
NotFoundException,
);
// passwordHash is hidden for security reasons
user.passwordHash = '';
const workspaceMember = await this.userService.loadWorkspaceMember(user);
if (workspaceMember) {
user.workspaceMember = workspaceMember;
}
const accessToken = await this.tokenService.generateAccessToken(user.id);
const refreshToken = await this.tokenService.generateRefreshToken(user.id);
@@ -68,9 +68,13 @@ export class TokenService {
throw new NotFoundException('User is not found');
}
if (!user.defaultWorkspace) {
throw new NotFoundException('User does not have a default workspace');
}
const jwtPayload: JwtPayload = {
sub: user.id,
workspaceId: user.defaultWorkspace?.id || undefined,
workspaceId: user.defaultWorkspace.id,
};
return {
@@ -188,7 +192,7 @@ export class TokenService {
return !!token;
}
async validateToken(request: Request): Promise<Workspace | null | undefined> {
async validateToken(request: Request): Promise<Workspace> {
const token = ExtractJwt.fromAuthHeaderAsBearerToken()(request);
if (!token) {
@@ -16,12 +16,8 @@ import { User } from 'src/core/user/user.entity';
import { TypeORMService } from 'src/database/typeorm/typeorm.service';
import { DataSourceService } from 'src/metadata/data-source/data-source.service';
export type JwtPayload = {
sub: string;
workspaceId?: string;
jti?: string;
};
export type PassportUser = { user?: User; workspace?: Workspace | null };
export type JwtPayload = { sub: string; workspaceId: string; jti?: string };
export type PassportUser = { user?: User; workspace: Workspace };
@Injectable()
export class JwtAuthStrategy extends PassportStrategy(Strategy, 'jwt') {
@@ -46,7 +42,11 @@ export class JwtAuthStrategy extends PassportStrategy(Strategy, 'jwt') {
id: payload.workspaceId ?? payload.sub,
});
if (workspace && payload.jti) {
if (!workspace) {
throw new UnauthorizedException();
}
if (payload.jti) {
const dataSourceMetadata =
await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceIdOrFail(
workspace.id,
@@ -68,7 +68,7 @@ export class JwtAuthStrategy extends PassportStrategy(Strategy, 'jwt') {
let user;
if (payload.workspaceId || !workspace) {
if (payload.workspaceId) {
user = await this.userRepository.findOne({
where: { id: payload.sub },
relations: ['defaultWorkspace'],
@@ -32,10 +32,6 @@ export class OpenApiService {
try {
const workspace = await this.tokenService.validateToken(request);
if (!workspace) {
return schema;
}
objectMetadataItems =
await this.objectMetadataService.findManyWithinWorkspace(workspace.id);
} catch (err) {
@@ -2,7 +2,6 @@ import { InjectRepository } from '@nestjs/typeorm';
import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm';
import { Repository } from 'typeorm';
import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity';
import { assert } from 'src/utils/assert';
import { User } from 'src/core/user/user.entity';
@@ -88,10 +87,6 @@ export class UserService extends TypeOrmQueryService<User> {
);
}
async updateUser(userId: string, data: QueryDeepPartialEntity<User>) {
await this.userRepository.update(userId, data);
}
async getUser(userId: string): Promise<User> {
return await this.userRepository.findOneOrFail({
where: { id: userId },
@@ -66,7 +66,7 @@ export class User {
@Column({ nullable: true })
deletedAt: Date;
@Field(() => Workspace, { nullable: true })
@Field(() => Workspace, { nullable: false })
@ManyToOne(() => Workspace, (workspace) => workspace.users, {
onDelete: 'SET NULL',
})
@@ -1,10 +1,10 @@
import { InjectRepository } from '@nestjs/typeorm';
import { BadRequestException } from '@nestjs/common';
import assert from 'assert';
import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm';
import { Repository } from 'typeorm';
import { v4 } from 'uuid';
import { WorkspaceManagerService } from 'src/workspace/workspace-manager/workspace-manager.service';
import { Workspace } from 'src/core/workspace/workspace.entity';
@@ -23,25 +23,16 @@ export class WorkspaceService extends TypeOrmQueryService<Workspace> {
}
async createWorkspace(user: User, data: CreateWorkspaceInput) {
const workspaceToCreate = this.workspaceRepository.create({
displayName: '',
domainName: '',
inviteHash: v4(),
subscriptionStatus: 'active',
...data,
if (!data.displayName || !data.displayName.length) {
throw new BadRequestException("'displayName' not provided");
}
await this.workspaceRepository.update(user.defaultWorkspace.id, {
displayName: data.displayName,
});
const workspace = await this.workspaceRepository.save(workspaceToCreate);
await this.workspaceManagerService.init(user.defaultWorkspace.id);
await this.userService.createWorkspaceMember(user);
await this.userService.updateUser(user.id, {
defaultWorkspace: workspace,
});
await this.workspaceManagerService.init(workspace.id);
const updatedUser = await this.userService.getUser(user.id);
await this.userService.createWorkspaceMember(updatedUser);
return updatedUser;
return await this.userService.getUser(user.id);
}
async deleteWorkspace(id: string) {
@@ -62,10 +62,6 @@ export class GraphQLConfigService
const workspace = await this.tokenService.validateToken(context.req);
if (!workspace) {
return new GraphQLSchema({});
}
return await this.createSchema(context, workspace);
} catch (error) {
if (error instanceof UnauthorizedException) {