workspace - wip

This commit is contained in:
Philipinho 2023-08-07 18:16:51 +01:00
parent 021a99e716
commit fe7c3ede01
25 changed files with 370 additions and 9464 deletions

1
server/.gitignore vendored
View File

@ -1,4 +1,5 @@
.env
package-lock.json
# compiled output
/dist
/node_modules

9436
server/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -35,6 +35,7 @@
"@nestjs/platform-express": "^10.0.0",
"@nestjs/platform-fastify": "^10.1.3",
"@nestjs/typeorm": "^10.0.0",
"@types/uuid": "^9.0.2",
"bcrypt": "^5.1.0",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.0",
@ -42,7 +43,8 @@
"pg": "^8.11.2",
"reflect-metadata": "^0.1.13",
"rxjs": "^7.8.1",
"typeorm": "^0.3.17"
"typeorm": "^0.3.17",
"uuid": "^9.0.0"
},
"devDependencies": {
"@nestjs/cli": "^10.0.0",

View File

@ -19,6 +19,7 @@ export class AuthController {
return await this.authService.login(loginInput);
}
@HttpCode(HttpStatus.OK)
@Post('register')
async register(@Body() createUserDto: CreateUserDto) {
return await this.authService.register(createUserDto);

View File

@ -1,11 +1,10 @@
import { Module } from '@nestjs/common';
import { forwardRef, Module } from '@nestjs/common';
import { AuthController } from './auth.controller';
import { AuthService } from './services/auth.service';
import { JwtModule } from '@nestjs/jwt';
import { EnvironmentService } from '../../environment/environment.service';
import { TokenService } from './services/token.service';
import { UserService } from '../user/user.service';
import { UserRepository } from '../user/repositories/user.repository';
import { UserModule } from '../user/user.module';
@Module({
imports: [
@ -21,9 +20,10 @@ import { UserRepository } from '../user/repositories/user.repository';
},
inject: [EnvironmentService],
}),
forwardRef(() => UserModule),
],
exports: [TokenService],
controllers: [AuthController],
providers: [AuthService, TokenService, UserService, UserRepository],
providers: [AuthService, TokenService],
exports: [TokenService],
})
export class AuthModule {}

View File

@ -1,8 +1,9 @@
import { Module } from '@nestjs/common';
import { UserModule } from './user/user.module';
import { AuthModule } from './auth/auth.module';
import { WorkspaceModule } from './workspace/workspace.module';
@Module({
imports: [UserModule, AuthModule],
imports: [UserModule, AuthModule, WorkspaceModule],
})
export class CoreModule {}

View File

@ -3,10 +3,13 @@ import {
Column,
CreateDateColumn,
Entity,
OneToMany,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
import * as bcrypt from 'bcrypt';
import { Workspace } from '../../workspace/entities/workspace.entity';
import { WorkspaceUser } from '../../workspace/entities/workspace-user.entity';
@Entity('users')
export class User {
@ -49,6 +52,16 @@ export class User {
@UpdateDateColumn()
updatedAt: Date;
@OneToMany(() => Workspace, (workspace) => workspace.creator, {
createForeignKeyConstraints: false,
})
workspaces: Workspace[];
@OneToMany(() => WorkspaceUser, (workspaceUser) => workspaceUser.user, {
createForeignKeyConstraints: false,
})
workspaceUser: WorkspaceUser[];
toJSON() {
delete this.password;
return this;

View File

@ -1,14 +1,20 @@
import { Module } from '@nestjs/common';
import { forwardRef, Module } from '@nestjs/common';
import { UserService } from './user.service';
import { UserController } from './user.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './entities/user.entity';
import { UserRepository } from './repositories/user.repository';
import { AuthModule } from '../auth/auth.module';
import { WorkspaceModule } from '../workspace/workspace.module';
@Module({
imports: [TypeOrmModule.forFeature([User]), AuthModule],
imports: [
TypeOrmModule.forFeature([User]),
forwardRef(() => AuthModule),
WorkspaceModule,
],
controllers: [UserController],
providers: [UserService, UserRepository],
exports: [UserService, UserRepository],
})
export class UserModule {}

View File

@ -5,10 +5,15 @@ import { User } from './entities/user.entity';
import { UserRepository } from './repositories/user.repository';
import { plainToClass } from 'class-transformer';
import * as bcrypt from 'bcrypt';
import { WorkspaceService } from '../workspace/services/workspace.service';
import { CreateWorkspaceDto } from '../workspace/dto/create-workspace.dto';
@Injectable()
export class UserService {
constructor(private userRepository: UserRepository) {}
constructor(
private userRepository: UserRepository,
private workspaceService: WorkspaceService,
) {}
async create(createUserDto: CreateUserDto): Promise<User> {
const existingUser: User = await this.findByEmail(createUserDto.email);
@ -16,11 +21,20 @@ export class UserService {
throw new BadRequestException('A user with this email already exists');
}
const user: User = plainToClass(User, createUserDto);
let user: User = plainToClass(User, createUserDto);
user.locale = 'en';
user.lastLoginAt = new Date();
return this.userRepository.save(user);
user = await this.userRepository.save(user);
//TODO: create workspace if it is not a signup to an existing workspace
const workspaceDto: CreateWorkspaceDto = {
name: user.name, // will be better handled
};
await this.workspaceService.create(workspaceDto, user.id);
return user;
}
findById(userId: string) {

View File

@ -0,0 +1,20 @@
import { Test, TestingModule } from '@nestjs/testing';
import { WorkspaceController } from './workspace.controller';
import { WorkspaceService } from '../services/workspace.service';
describe('WorkspaceController', () => {
let controller: WorkspaceController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [WorkspaceController],
providers: [WorkspaceService],
}).compile();
controller = module.get<WorkspaceController>(WorkspaceController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

View File

@ -0,0 +1,7 @@
import { Controller } from '@nestjs/common';
import { WorkspaceService } from '../services/workspace.service';
@Controller('workspace')
export class WorkspaceController {
constructor(private readonly workspaceService: WorkspaceService) {}
}

View File

@ -0,0 +1,18 @@
import { IsOptional, IsString, MaxLength, MinLength } from 'class-validator';
export class CreateWorkspaceDto {
@MinLength(4)
@MaxLength(64)
@IsString()
name: string;
@IsOptional()
@MinLength(4)
@MaxLength(30)
@IsString()
hostname?: string;
@IsOptional()
@IsString()
description?: string;
}

View File

@ -0,0 +1,4 @@
import { PartialType } from '@nestjs/mapped-types';
import { CreateWorkspaceDto } from './create-workspace.dto';
export class UpdateWorkspaceDto extends PartialType(CreateWorkspaceDto) {}

View File

@ -0,0 +1,46 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
ManyToOne,
JoinColumn,
} from 'typeorm';
import { Workspace } from './workspace.entity';
import { User } from '../../user/entities/user.entity';
@Entity('workspace_invitations')
export class WorkspaceInvitation {
@PrimaryGeneratedColumn('uuid')
id: string;
@ManyToOne(() => Workspace, {
onDelete: 'CASCADE',
createForeignKeyConstraints: false,
})
@JoinColumn({ name: 'workspaceId' })
workspace: Workspace;
@ManyToOne(() => User, {
onDelete: 'SET NULL',
createForeignKeyConstraints: false,
})
@JoinColumn({ name: 'invitedById' })
invitedBy: User;
@Column({ type: 'varchar', length: 255 })
email: string;
@Column({ type: 'varchar', length: 100, nullable: true })
role?: string;
@Column({ type: 'varchar', length: 100, nullable: true })
status?: string;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}

View File

@ -0,0 +1,48 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
ManyToOne,
JoinColumn,
Unique,
} from 'typeorm';
import { Workspace } from './workspace.entity';
import { User } from '../../user/entities/user.entity';
@Entity('workspace_users')
@Unique(['workspaceId', 'userId'])
export class WorkspaceUser {
@PrimaryGeneratedColumn('uuid')
id: string;
@ManyToOne(() => User, (user) => user.workspaceUser, {
onDelete: 'CASCADE',
createForeignKeyConstraints: false,
})
@JoinColumn({ name: 'userId' })
user: User;
@Column()
userId: string;
@ManyToOne(() => Workspace, (workspace) => workspace.workspaceUser, {
onDelete: 'CASCADE',
createForeignKeyConstraints: false,
})
@JoinColumn({ name: 'workspaceId' })
workspace: Workspace;
@Column()
workspaceId: string;
@Column({ type: 'varchar', length: 100, nullable: true })
role?: string;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}

View File

@ -0,0 +1,62 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
ManyToOne,
OneToMany,
JoinColumn,
} from 'typeorm';
import { User } from '../../user/entities/user.entity';
import { WorkspaceUser } from './workspace-user.entity';
@Entity('workspaces')
export class Workspace {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column()
name: string;
@Column({ type: 'text', nullable: true })
description?: string;
@Column({ nullable: true })
logo?: string;
@Column({ unique: true })
hostname: string;
@Column({ nullable: true })
customDomain?: string;
@Column({ type: 'boolean', default: true })
enableInvite: boolean;
@Column({ type: 'text', unique: true, nullable: true })
inviteCode?: string;
@Column({ type: 'jsonb', nullable: true })
settings?: any;
@ManyToOne(() => User, (user) => user.workspaces, {
createForeignKeyConstraints: false,
})
@JoinColumn({ name: 'creatorId' })
creator: User;
@Column()
creatorId: string;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
@OneToMany(() => WorkspaceUser, (workspaceUser) => workspaceUser.workspace, {
createForeignKeyConstraints: false,
})
workspaceUser: WorkspaceUser[];
}

View File

@ -0,0 +1,10 @@
import { Injectable } from '@nestjs/common';
import { DataSource, Repository } from 'typeorm';
import { WorkspaceUser } from '../entities/workspace-user.entity';
@Injectable()
export class WorkspaceUserRepository extends Repository<WorkspaceUser> {
constructor(private dataSource: DataSource) {
super(WorkspaceUser, dataSource.createEntityManager());
}
}

View File

@ -0,0 +1,10 @@
import { Injectable } from '@nestjs/common';
import { DataSource, Repository } from 'typeorm';
import { Workspace } from '../entities/workspace.entity';
@Injectable()
export class WorkspaceRepository extends Repository<Workspace> {
constructor(private dataSource: DataSource) {
super(Workspace, dataSource.createEntityManager());
}
}

View File

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { WorkspaceService } from './workspace.service';
describe('WorkspaceService', () => {
let service: WorkspaceService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [WorkspaceService],
}).compile();
service = module.get<WorkspaceService>(WorkspaceService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@ -0,0 +1,49 @@
import { Injectable } from '@nestjs/common';
import { CreateWorkspaceDto } from '../dto/create-workspace.dto';
import { WorkspaceRepository } from '../repositories/workspace.repository';
import { WorkspaceUserRepository } from '../repositories/workspace-user.repository';
import { WorkspaceUser } from '../entities/workspace-user.entity';
import { Workspace } from '../entities/workspace.entity';
import { plainToClass } from 'class-transformer';
import { v4 as uuid } from 'uuid';
import { generateHostname } from '../workspace.util';
@Injectable()
export class WorkspaceService {
constructor(
private workspaceRepository: WorkspaceRepository,
private workspaceUserRepository: WorkspaceUserRepository,
) {}
async create(
createWorkspaceDto: CreateWorkspaceDto,
userId: string,
): Promise<Workspace> {
let workspace: Workspace = plainToClass(Workspace, createWorkspaceDto);
workspace.inviteCode = uuid();
workspace.creatorId = userId;
if (!workspace.hostname?.trim()) {
workspace.hostname = generateHostname(createWorkspaceDto.name);
}
workspace = await this.workspaceRepository.save(workspace);
await this.addUserToWorkspace(userId, workspace.id, 'owner');
return workspace;
}
async addUserToWorkspace(
userId: string,
workspaceId: string,
role: string,
): Promise<WorkspaceUser> {
const workspaceUser = new WorkspaceUser();
workspaceUser.userId = userId;
workspaceUser.workspaceId = workspaceId;
workspaceUser.role = role;
return this.workspaceUserRepository.save(workspaceUser);
}
}

View File

@ -0,0 +1,19 @@
import { Module } from '@nestjs/common';
import { WorkspaceService } from './services/workspace.service';
import { WorkspaceController } from './controllers/workspace.controller';
import { WorkspaceRepository } from './repositories/workspace.repository';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Workspace } from './entities/workspace.entity';
import { WorkspaceUser } from './entities/workspace-user.entity';
import { WorkspaceInvitation } from './entities/workspace-invitation.entity';
import { WorkspaceUserRepository } from './repositories/workspace-user.repository';
@Module({
imports: [
TypeOrmModule.forFeature([Workspace, WorkspaceUser, WorkspaceInvitation]),
],
controllers: [WorkspaceController],
providers: [WorkspaceService, WorkspaceRepository, WorkspaceUserRepository],
exports: [WorkspaceService, WorkspaceRepository, WorkspaceUserRepository],
})
export class WorkspaceModule {}

View File

@ -0,0 +1,5 @@
export function generateHostname(name: string): string {
let hostname = name.replace(/[^a-z0-9]/gi, '').toLowerCase();
hostname = hostname.substring(0, 30);
return hostname;
}

View File

@ -0,0 +1,3 @@
# don't include frequently changing migrations yet
*
!.gitignore

View File

@ -1,15 +0,0 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class CreateUserTable1691158956520 implements MigrationInterface {
name = 'CreateUserTable1691158956520';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE "users" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "name" character varying NOT NULL, "email" character varying NOT NULL, "emailVerifiedAt" TIMESTAMP, "password" character varying NOT NULL, "avatar_url" character varying, "locale" character varying, "timezone" character varying, "settings" jsonb, "lastLoginAt" TIMESTAMP, "lastLoginIp" character varying, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "UQ_97672ac88f789774dd47f7c8be3" UNIQUE ("email"), CONSTRAINT "PK_a3ffb1c0c8416b9fc6f907b7433" PRIMARY KEY ("id"))`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP TABLE "users"`);
}
}

View File

@ -9,6 +9,6 @@ export const AppDataSource: DataSource = new DataSource({
entities: ['src/**/*.entity.{ts,js}'],
migrations: ['src/**/migrations/*.{ts,js}'],
subscribers: [],
synchronize: process.env.NODE_ENV === 'development',
synchronize: false,
logging: process.env.NODE_ENV === 'development',
});