Skip to content

Commit 119e356

Browse files
Merge pull request #284 from shamoo53/user-module
initial commit
2 parents dd444c6 + 130c23f commit 119e356

File tree

11 files changed

+3352
-18
lines changed

11 files changed

+3352
-18
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
node_modules
2+
node_modules

backend/src/app.module.ts

Lines changed: 6 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,44 +7,32 @@ import { AssetCategoriesModule } from './asset-categories/asset-categories.modul
77
import { AssetCategory } from './asset-categories/asset-category.entity';
88
import { DepartmentsModule } from './departments/departments.module';
99
import { Department } from './departments/department.entity';
10+
import { UsersModule } from './users/users.module';
11+
import { User } from './users/entities/user.entity';
1012

1113
@Module({
1214
imports: [
1315
ConfigModule.forRoot({
1416
isGlobal: true,
1517
}),
18+
1619
TypeOrmModule.forRootAsync({
17-
imports: [ConfigModule,
18-
// --- ADD THIS CONFIGURATION ---
19-
I18nModule.forRoot({
20-
fallbackLanguage: 'en',
21-
loaderOptions: {
22-
path: path.join(__dirname, '/i18n/'), // Directory for translation files
23-
watch: true, // Watch for changes in translation files
24-
},
25-
resolvers: [
26-
// Order matters: checks query param, then header, then browser settings
27-
new QueryResolver(['lang', 'l']),
28-
new HeaderResolver(['x-custom-lang-header']),
29-
AcceptLanguageResolver, // Standard 'Accept-Language' header
30-
],
31-
}),
32-
// --- END OF CONFIGURATION ---
33-
],],
20+
imports: [ConfigModule],
3421
useFactory: (configService: ConfigService) => ({
3522
type: 'postgres',
3623
host: configService.get('DB_HOST', 'localhost'),
3724
port: configService.get('DB_PORT', 5432),
3825
username: configService.get('DB_USERNAME', 'postgres'),
3926
password: configService.get('DB_PASSWORD', 'password'),
4027
database: configService.get('DB_DATABASE', 'manage_assets'),
41-
entities: [AssetCategory, Department],
28+
entities: [AssetCategory, Department, User],
4229
synchronize: configService.get('NODE_ENV') !== 'production', // Only for development
4330
}),
4431
inject: [ConfigService],
4532
}),
4633
AssetCategoriesModule,
4734
DepartmentsModule,
35+
UsersModule,
4836
],
4937
controllers: [AppController],
5038
providers: [AppService],
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
import { Test, TestingModule } from '@nestjs/testing';
2+
import { INestApplication } from '@nestjs/common';
3+
import * as request from 'supertest';
4+
import { TypeOrmModule } from '@nestjs/typeorm';
5+
import { UsersModule } from '../users.module';
6+
import { User } from '../entities/user.entity';
7+
8+
describe('UsersController (e2e)', () => {
9+
let app: INestApplication;
10+
11+
beforeAll(async () => {
12+
const moduleFixture: TestingModule = await Test.createTestingModule({
13+
imports: [
14+
TypeOrmModule.forRoot({
15+
type: 'sqlite',
16+
database: ':memory:',
17+
dropSchema: true,
18+
entities: [User],
19+
synchronize: true,
20+
}),
21+
UsersModule,
22+
],
23+
}).compile();
24+
25+
app = moduleFixture.createNestApplication();
26+
await app.init();
27+
});
28+
29+
afterAll(async () => {
30+
await app.close();
31+
});
32+
33+
it('should create a user', async () => {
34+
const res = await request(app.getHttpServer())
35+
.post('/users')
36+
.send({
37+
fullName: 'Test User',
38+
email: 'test@example.com',
39+
password: 'password123',
40+
role: 'admin',
41+
});
42+
expect(res.status).toBe(201);
43+
expect(res.body).toHaveProperty('id');
44+
expect(res.body.email).toBe('test@example.com');
45+
});
46+
47+
it('should get all users', async () => {
48+
const res = await request(app.getHttpServer()).get('/users');
49+
expect(res.status).toBe(200);
50+
expect(Array.isArray(res.body)).toBe(true);
51+
});
52+
53+
it('should get a user by id', async () => {
54+
const createRes = await request(app.getHttpServer())
55+
.post('/users')
56+
.send({
57+
fullName: 'Another User',
58+
email: 'another@example.com',
59+
password: 'password123',
60+
role: 'user',
61+
});
62+
const userId = createRes.body.id;
63+
const res = await request(app.getHttpServer()).get(`/users/${userId}`);
64+
expect(res.status).toBe(200);
65+
expect(res.body.id).toBe(userId);
66+
});
67+
68+
it('should update a user', async () => {
69+
const createRes = await request(app.getHttpServer())
70+
.post('/users')
71+
.send({
72+
fullName: 'Update User',
73+
email: 'update@example.com',
74+
password: 'password123',
75+
role: 'user',
76+
});
77+
const userId = createRes.body.id;
78+
const res = await request(app.getHttpServer())
79+
.patch(`/users/${userId}`)
80+
.send({ fullName: 'Updated Name' });
81+
expect(res.status).toBe(200);
82+
expect(res.body.fullName).toBe('Updated Name');
83+
});
84+
85+
it('should delete a user', async () => {
86+
const createRes = await request(app.getHttpServer())
87+
.post('/users')
88+
.send({
89+
fullName: 'Delete User',
90+
email: 'delete@example.com',
91+
password: 'password123',
92+
role: 'user',
93+
});
94+
const userId = createRes.body.id;
95+
const res = await request(app.getHttpServer()).delete(`/users/${userId}`);
96+
expect(res.status).toBe(200);
97+
});
98+
99+
it('should not create user with duplicate email', async () => {
100+
await request(app.getHttpServer())
101+
.post('/users')
102+
.send({
103+
fullName: 'Dup User',
104+
email: 'dup@example.com',
105+
password: 'password123',
106+
role: 'user',
107+
});
108+
const res = await request(app.getHttpServer())
109+
.post('/users')
110+
.send({
111+
fullName: 'Dup User2',
112+
email: 'dup@example.com',
113+
password: 'password123',
114+
role: 'user',
115+
});
116+
expect(res.status).toBeGreaterThanOrEqual(400);
117+
});
118+
119+
it('should filter users by role', async () => {
120+
await request(app.getHttpServer())
121+
.post('/users')
122+
.send({
123+
fullName: 'Admin User',
124+
email: 'admin@example.com',
125+
password: 'password123',
126+
role: 'admin',
127+
});
128+
const res = await request(app.getHttpServer())
129+
.get('/users?role=admin');
130+
expect(res.status).toBe(200);
131+
expect(res.body.some((u: any) => u.role === 'admin')).toBe(true);
132+
});
133+
134+
it('should paginate users', async () => {
135+
for (let i = 0; i < 15; i++) {
136+
await request(app.getHttpServer())
137+
.post('/users')
138+
.send({
139+
fullName: `User${i}`,
140+
email: `user${i}@example.com`,
141+
password: 'password123',
142+
role: 'user',
143+
});
144+
}
145+
const res = await request(app.getHttpServer())
146+
.get('/users?page=2&limit=10');
147+
expect(res.status).toBe(200);
148+
expect(res.body.length).toBeLessThanOrEqual(10);
149+
});
150+
151+
it('should not expose passwordHash in user response', async () => {
152+
const res = await request(app.getHttpServer())
153+
.post('/users')
154+
.send({
155+
fullName: 'Hidden Password',
156+
email: 'hidden@example.com',
157+
password: 'password123',
158+
role: 'user',
159+
});
160+
expect(res.body.passwordHash).toBeUndefined();
161+
});
162+
163+
it('should return 404 for non-existent user', async () => {
164+
const res = await request(app.getHttpServer()).get('/users/non-existent-id');
165+
expect(res.status).toBe(404);
166+
});
167+
168+
it('should validate required fields on create', async () => {
169+
const res = await request(app.getHttpServer())
170+
.post('/users')
171+
.send({ email: 'invalid@example.com' });
172+
expect(res.status).toBeGreaterThanOrEqual(400);
173+
});
174+
});
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { IsEmail, IsNotEmpty, IsString, MinLength, IsOptional } from 'class-validator';
2+
3+
export class CreateUserDto {
4+
@IsNotEmpty()
5+
@IsString()
6+
fullName: string;
7+
8+
@IsEmail()
9+
email: string;
10+
11+
@IsOptional()
12+
@IsString()
13+
phoneNumber?: string;
14+
15+
@IsNotEmpty()
16+
@MinLength(6)
17+
password: string;
18+
19+
@IsNotEmpty()
20+
@IsString()
21+
@IsOptional()
22+
role: 'admin' | 'user' | 'manager';
23+
24+
@IsOptional()
25+
companyId?: number;
26+
@IsOptional()
27+
departmentId?: number;
28+
@IsOptional()
29+
branchId?: number;
30+
31+
// companyId, departmentId, branchId can be added for mapping
32+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { IsEmail, IsOptional, IsString, MinLength } from 'class-validator';
2+
3+
export class UpdateUserDto {
4+
@IsOptional()
5+
@IsString()
6+
fullName?: string;
7+
8+
@IsOptional()
9+
@IsEmail()
10+
email?: string;
11+
12+
@IsOptional()
13+
@IsString()
14+
phoneNumber?: string;
15+
16+
@IsOptional()
17+
@MinLength(6)
18+
password?: string;
19+
20+
@IsOptional()
21+
@IsString()
22+
role?: 'admin' | 'user' | 'manager';
23+
24+
@IsOptional()
25+
companyId?: number;
26+
@IsOptional()
27+
departmentId?: number;
28+
@IsOptional()
29+
branchId?: number;
30+
31+
// companyId, departmentId, branchId can be added for mapping
32+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne } from 'typeorm';
2+
3+
@Entity('users')
4+
export class User {
5+
@PrimaryGeneratedColumn('uuid')
6+
id: string;
7+
8+
@Column()
9+
fullName: string;
10+
11+
@Column({ unique: true })
12+
email: string;
13+
14+
@Column({ nullable: true })
15+
phoneNumber: string;
16+
17+
@Column()
18+
passwordHash: string;
19+
20+
@Column({ type: 'enum', enum: ['admin', 'user', 'manager'], default: 'user' })
21+
role: 'admin' | 'user' | 'manager';
22+
// Department relation temporarily commented out due to import error
23+
// @ManyToOne(() => Department, { nullable: true })
24+
// department?: Department;
25+
@Column({ nullable: true })
26+
companyId?: number;
27+
@Column({ nullable: true })
28+
branchId?: number;
29+
30+
@CreateDateColumn()
31+
createdAt: Date;
32+
@UpdateDateColumn()
33+
updatedAt: Date;
34+
35+
// Relations to company, department, branch (to be defined)
36+
// @ManyToOne(() => Company, company => company.users)
37+
// company: Company;
38+
// @ManyToOne(() => Department, department => department.users)
39+
// department: Department;
40+
// @ManyToOne(() => Branch, branch => branch.users)
41+
// branch: Branch;
42+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { Controller, Get, Post, Body, Patch, Param, Delete, Query } from '@nestjs/common';
2+
import { UsersService } from './users.service';
3+
import { CreateUserDto } from './dto/create-user.dto';
4+
import { UpdateUserDto } from './dto/update-user.dto';
5+
6+
@Controller('users')
7+
export class UsersController {
8+
constructor(private readonly usersService: UsersService) {}
9+
10+
@Post()
11+
create(@Body() createUserDto: CreateUserDto) {
12+
return this.usersService.create(createUserDto);
13+
}
14+
15+
@Get()
16+
findAll(@Query('page') page = 1, @Query('limit') limit = 10, @Query('role') role?: string) {
17+
return this.usersService.findAll(Number(page), Number(limit), role);
18+
}
19+
@Patch(':id/department')
20+
mapDepartment(@Param('id') id: string, @Body('department') department: any) {
21+
return this.usersService.mapUserToDepartment(id, department);
22+
}
23+
24+
@Patch(':id/company')
25+
mapCompany(@Param('id') id: string, @Body('companyId') companyId: number) {
26+
return this.usersService.mapUserToCompany(id, companyId);
27+
}
28+
29+
@Patch(':id/branch')
30+
mapBranch(@Param('id') id: string, @Body('branchId') branchId: number) {
31+
return this.usersService.mapUserToBranch(id, branchId);
32+
}
33+
34+
@Get(':id')
35+
findOne(@Param('id') id: string) {
36+
return this.usersService.findOne(id);
37+
}
38+
39+
@Patch(':id')
40+
update(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) {
41+
return this.usersService.update(id, updateUserDto);
42+
}
43+
44+
@Delete(':id')
45+
remove(@Param('id') id: string) {
46+
return this.usersService.remove(id);
47+
}
48+
}

0 commit comments

Comments
 (0)