Skip to content

Commit b238bc6

Browse files
committed
feat: implement workspace management and update user registration flow
- Added workspace model and relationships in Prisma schema. - Updated user and team member models to include workspace references. - Enhanced registration process to include workspace creation. - Modified authentication responses to include workspace ID. - Implemented workspace selection in the frontend with corresponding API updates. - Refactored project management to support workspace-specific operations. - Added workspace management features in the sidebar and project management components. - Updated various components to handle workspace context and interactions.
1 parent e64a4e7 commit b238bc6

34 files changed

+2874
-820
lines changed

backend/package-lock.json

Lines changed: 328 additions & 34 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

backend/prisma/schema.prisma

Lines changed: 62 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -13,37 +13,6 @@ datasource db {
1313
provider = "postgresql"
1414
url = env("DATABASE_URL")
1515
}
16-
model User {
17-
id String @id @default(cuid())
18-
email String? @unique
19-
phone String?
20-
name String?
21-
password String? // kalau pake password
22-
session_token String? @unique
23-
createdAt DateTime @default(now())
24-
updatedAt DateTime @updatedAt
25-
26-
// FK ke TeamMember (one-to-one)
27-
teamMemberId String? @unique
28-
teamMember TeamMember? @relation(fields: [teamMemberId], references: [id])
29-
}
30-
31-
model TeamMember {
32-
id String @id @default(uuid()) // pilih uuid supaya beda style dari user.id
33-
clientId String? @unique
34-
name String
35-
role String?
36-
email String? @unique
37-
photo String?
38-
phone String?
39-
isTrash Boolean @default(false)
40-
createdAt DateTime @default(now())
41-
updatedAt DateTime?
42-
Task Task[]
43-
44-
// back relation ke User
45-
user User? @relation
46-
}
4716

4817
model aiToolCache {
4918
id String @id @default(cuid())
@@ -78,15 +47,63 @@ model chatMessage {
7847
@@map("chat_message")
7948
}
8049

81-
model Project {
82-
id String @id @default(uuid())
83-
clientId String? @unique
50+
model User {
51+
id String @id @default(cuid())
52+
email String? @unique
53+
phone String?
54+
name String?
55+
role Role? @default(USER)
56+
password String? // kalau pake password
57+
session_token String? @unique
58+
createdAt DateTime @default(now())
59+
updatedAt DateTime @updatedAt
60+
members TeamMember[] @relation("UserMembers")
61+
62+
}
63+
64+
model TeamMember {
65+
id String @id @default(uuid()) // pilih uuid supaya beda style dari user.id
66+
clientId String? @unique
67+
userId String
68+
workspaceId String
69+
name String
70+
role String?
71+
email String?
72+
photo String?
73+
phone String?
74+
isTrash Boolean @default(false)
75+
createdAt DateTime @default(now())
76+
updatedAt DateTime?
77+
Task Task[]
78+
79+
// FK ke Workspace
80+
workspace Workspace? @relation("WorkspaceMembers", fields: [workspaceId], references: [id])
81+
user User? @relation("UserMembers", fields: [userId], references: [id])
82+
}
83+
84+
model Workspace {
85+
id String @id @default(uuid())
86+
clientId String? @unique
8487
name String
8588
description String?
86-
isTrash Boolean @default(false)
87-
createdAt DateTime @default(now())
89+
isTrash Boolean @default(false)
90+
createdAt DateTime @default(now())
8891
updatedAt DateTime?
89-
tasks Task[] @relation("ProjectTasks")
92+
projects Project[] @relation("WorkspaceProjects")
93+
members TeamMember[] @relation("WorkspaceMembers")
94+
}
95+
96+
model Project {
97+
id String @id @default(uuid())
98+
workspaceId String?
99+
clientId String? @unique
100+
name String
101+
description String?
102+
isTrash Boolean @default(false)
103+
createdAt DateTime @default(now())
104+
updatedAt DateTime?
105+
workspace Workspace? @relation("WorkspaceProjects", fields: [workspaceId], references: [id])
106+
tasks Task[] @relation("ProjectTasks")
90107
}
91108

92109
model Task {
@@ -109,13 +126,18 @@ model Task {
109126
}
110127

111128
model Comment {
112-
id String @id @default(uuid())
129+
id String @id @default(uuid())
113130
taskId String
114-
task Task @relation("TaskComments", fields: [taskId], references: [id])
131+
task Task @relation("TaskComments", fields: [taskId], references: [id])
115132
author String
116133
body String
117134
attachments Json? // optional JSON array of attachments metadata
118135
isTrash Boolean @default(false)
119-
createdAt DateTime @default(now())
136+
createdAt DateTime @default(now())
120137
updatedAt DateTime?
138+
}
139+
140+
enum Role {
141+
USER
142+
ADMIN
121143
}

backend/src/auth/auth.controller.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export class AuthController {
2323
return {
2424
token: result.token,
2525
userId: result.user.id,
26+
workspaceId: result.workspace.id,
2627
teamMemberId: result.teamMember.id,
2728
clientTempId: result.clientTempId ?? null,
2829
};
@@ -36,7 +37,6 @@ export class AuthController {
3637
return {
3738
token: result.token,
3839
userId: result?.user?.id ?? "",
39-
teamMemberId: result.teamMember?.id ?? null,
4040
};
4141
}
4242
}

backend/src/auth/auth.service.ts

Lines changed: 26 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -48,40 +48,50 @@ export class AuthService {
4848
}
4949

5050
async register(dto: RegisterDto) {
51-
const { clientTempId, email, name, role, photo, password } = dto;
51+
const { clientTempId, workspace, email, name, role, photo, password } = dto;
5252

5353
// Basic uniqueness check
5454
const existing = await this.prisma.user.findUnique({ where: { email } });
5555
if (existing) throw new ConflictException("Email already registered");
5656

5757
// Use transaction to create TeamMember then User referencing member.id
5858
try {
59+
console.log(workspace);
5960
const result = await this.prisma.$transaction(async (tx) => {
60-
// 1) create TeamMember first
61-
const teamMember = await tx.teamMember.create({
61+
// 1) create Workspace first
62+
const createWorkspace = await tx.workspace.create({
6263
data: {
63-
name,
64-
email,
65-
role: role ?? null,
66-
photo: photo ?? null,
67-
// optionally store clientTempId if you want mapping
68-
// clientTempId: clientTempId ?? null // uncomment if you added field
64+
name: workspace,
6965
createdAt: new Date(),
7066
},
7167
});
7268

73-
// 2) create User referencing teamMember.id
69+
// 2) create User referencing
7470
const hashed = password ? hashPassword(password) : undefined;
7571
const user = await tx.user.create({
7672
data: {
7773
email,
7874
name,
7975
password: hashed,
80-
teamMemberId: teamMember.id, // <-- link via member id
8176
},
8277
});
8378

84-
return { user, teamMember };
79+
// 3) create TeamMember
80+
const teamMember = await tx.teamMember.create({
81+
data: {
82+
userId: user.id,
83+
workspaceId: createWorkspace.id,
84+
name,
85+
email,
86+
role: role ?? null,
87+
photo: photo ?? null,
88+
// optionally store clientTempId if you want mapping
89+
// clientTempId: clientTempId ?? null // uncomment if you added field
90+
createdAt: new Date(),
91+
},
92+
});
93+
94+
return { user, teamMember, workspace: createWorkspace };
8595
});
8696

8797
// Create JWT (you can include teamMemberId in payload too)
@@ -100,6 +110,7 @@ export class AuthService {
100110
token: jwt,
101111
user: result.user,
102112
teamMember: result.teamMember,
113+
workspace: result.workspace,
103114
// optionally map clientTempId => memberId so FE can replace optimistic id
104115
clientTempId,
105116
};
@@ -116,9 +127,9 @@ export class AuthService {
116127
if (!email) throw new UnauthorizedException("Invalid credentials");
117128

118129
// find user and include relation if exists
119-
let user = await this.prisma.user.findUnique({
130+
const user = await this.prisma.user.findUnique({
120131
where: { email },
121-
include: { teamMember: true },
132+
include: { members: true },
122133
});
123134

124135
if (!user) throw new UnauthorizedException("Invalid credentials");
@@ -127,42 +138,16 @@ export class AuthService {
127138
const ok = comparePassword(password || "", user.password);
128139
if (!ok) throw new UnauthorizedException("Invalid credentials");
129140

130-
// If relation missing, try to find TeamMember by email and attach it (best-effort)
131-
let teamMember = (user as any).teamMember ?? null;
132-
if (!teamMember) {
133-
if (user.email) {
134-
const found = await this.prisma.teamMember.findUnique({
135-
where: { email: user.email },
136-
});
137-
if (found) {
138-
// attach FK so next time it's present
139-
try {
140-
await this.prisma.user.update({
141-
where: { id: user.id },
142-
data: { teamMemberId: found.id },
143-
});
144-
teamMember = found;
145-
// also reflect in local user object
146-
user = { ...user, teamMemberId: found.id } as any;
147-
} catch (err) {
148-
// ignore update failure (concurrency/unique) — still continue
149-
console.warn("Failed to attach teamMemberId:", err);
150-
}
151-
}
152-
}
153-
}
154-
155141
const jwt = this.jwtService.sign({
156142
userId: user?.id,
157-
teamMemberId: teamMember?.id ?? null,
158143
});
159144

160145
await this.prisma.user.update({
161146
where: { id: user?.id },
162147
data: { session_token: jwt },
163148
});
164149

165-
return { token: jwt, user, teamMember };
150+
return { token: jwt, user };
166151
}
167152

168153
async validateUser(token: string) {

backend/src/auth/dto/register.dto.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ export class RegisterDto {
66
@IsString()
77
clientTempId?: string; // optional, FE optimistic id
88

9+
@IsString()
10+
workspace: string;
11+
912
@IsEmail()
1013
email: string;
1114

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,22 @@
1-
import { IsOptional, IsString } from 'class-validator';
1+
import { IsOptional, IsString } from "class-validator";
22

33
export class CreateProjectDto {
4-
@IsString()
5-
name: string;
4+
@IsString()
5+
name: string;
6+
@IsString()
7+
workspaceId: string;
68

7-
@IsOptional()
8-
@IsString()
9-
description?: string;
9+
@IsOptional()
10+
@IsString()
11+
description?: string;
1012
}
1113

1214
export class UpdateProjectDto {
13-
@IsOptional()
14-
@IsString()
15-
name?: string;
15+
@IsOptional()
16+
@IsString()
17+
name?: string;
1618

17-
@IsOptional()
18-
@IsString()
19-
description?: string;
19+
@IsOptional()
20+
@IsString()
21+
description?: string;
2022
}

backend/src/project-management/dto/team.dto.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ export class CreateTeamMemberDto {
2323
@IsOptional()
2424
@IsString()
2525
photo?: string;
26+
27+
@IsString()
28+
workspaceId?: string;
2629
}
2730

2831
export class UpdateTeamMemberDto {
@@ -34,10 +37,6 @@ export class UpdateTeamMemberDto {
3437
@IsString()
3538
role?: string;
3639

37-
@IsOptional()
38-
@IsString()
39-
email?: string;
40-
4140
@IsOptional()
4241
@IsString()
4342
password?: string;
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { IsOptional, IsString } from "class-validator";
2+
3+
export class CreateWorkspaceDto {
4+
@IsString()
5+
name: string;
6+
7+
@IsOptional()
8+
@IsString()
9+
description?: string;
10+
}
11+
12+
export class UpdateWorkspaceDto {
13+
@IsOptional()
14+
@IsString()
15+
name?: string;
16+
17+
@IsOptional()
18+
@IsString()
19+
description?: string;
20+
}
Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,20 @@
1-
import { Test, TestingModule } from '@nestjs/testing';
2-
import { ProjectManagementController } from './project-management.controller';
1+
import { Test, TestingModule } from "@nestjs/testing";
2+
import { ProjectManagementController } from "./project-management.controller";
33

4-
describe('ProjectManagementController', () => {
4+
describe("ProjectManagementController", () => {
55
let controller: ProjectManagementController;
66

77
beforeEach(async () => {
88
const module: TestingModule = await Test.createTestingModule({
99
controllers: [ProjectManagementController],
1010
}).compile();
1111

12-
controller = module.get<ProjectManagementController>(ProjectManagementController);
12+
controller = module.get<ProjectManagementController>(
13+
ProjectManagementController
14+
);
1315
});
1416

15-
it('should be defined', () => {
17+
it("should be defined", () => {
1618
expect(controller).toBeDefined();
1719
});
1820
});

0 commit comments

Comments
 (0)