@devstroupe/devkit-cli 1.0.1 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/boilerplates/angular-template/src/app/services/user.service.ts +12 -0
- package/dist/boilerplates/nest-template/src/app.module.ts +23 -0
- package/dist/boilerplates/nest-template/src/database/migrations/1600000000000-create-roles.ts +20 -0
- package/dist/boilerplates/nest-template/src/database/migrations/1700000000000-create-users.ts +13 -2
- package/dist/boilerplates/nest-template/src/modules/auth/auth.controller.ts +16 -0
- package/dist/boilerplates/nest-template/src/modules/auth/auth.service.ts +44 -5
- package/dist/boilerplates/nest-template/src/modules/storage/infra/http/storage-download.controller.ts +38 -0
- package/dist/boilerplates/nest-template/src/modules/storage/storage.module.ts +7 -0
- package/dist/boilerplates/nest-template/src/modules/user/database-seed.service.ts +31 -18
- package/dist/boilerplates/nest-template/src/modules/user/domain/user.repository.ts +1 -0
- package/dist/boilerplates/nest-template/src/modules/user/domain/user.ts +1 -0
- package/dist/boilerplates/nest-template/src/modules/user/infra/database/user.orm-entity.ts +3 -0
- package/dist/boilerplates/nest-template/src/modules/user/infra/database/user.repository.adapter.ts +6 -0
- package/dist/boilerplates/nest-template/src/modules/user/infra/http/user.controller.ts +56 -0
- package/dist/boilerplates/nest-template/src/modules/user/service/user.service.ts +4 -0
- package/dist/boilerplates/nest-template/src/modules/user/user.module.ts +2 -0
- package/dist/index.js +31 -4
- package/dist/templates/angular/form.template.js +85 -9
- package/dist/templates/angular/list.template.js +27 -5
- package/dist/templates/nest/crud.templates.d.ts +16 -2
- package/dist/templates/nest/crud.templates.js +175 -13
- package/dist/templates/nest/migration.template.js +82 -6
- package/dist/templates/shared/relationships.js +7 -4
- package/dist/templates/ui/components/index.d.ts +1 -0
- package/dist/templates/ui/components/index.js +1 -0
- package/dist/templates/ui/components/input.template.js +17 -9
- package/dist/templates/ui/components/textarea.template.d.ts +4 -0
- package/dist/templates/ui/components/textarea.template.js +76 -0
- package/package.json +3 -3
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Injectable } from '@angular/core';
|
|
2
|
+
import { HttpClient } from '@angular/common/http';
|
|
3
|
+
import { BaseService } from '@devstroupe/devkit-angular';
|
|
4
|
+
|
|
5
|
+
@Injectable({
|
|
6
|
+
providedIn: 'root',
|
|
7
|
+
})
|
|
8
|
+
export class UserService extends BaseService<any> {
|
|
9
|
+
constructor(http: HttpClient) {
|
|
10
|
+
super(http as any, '/api/user');
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { Module } from '@nestjs/common';
|
|
2
2
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
|
3
|
+
import { StorageModule } from '@devstroupe/devkit-nest';
|
|
3
4
|
import { UserModule } from './modules/user/user.module';
|
|
4
5
|
import { AuthModule } from './modules/auth/auth.module';
|
|
5
6
|
import { RoleModule } from './modules/role/role.module';
|
|
7
|
+
import { StorageDownloadModule } from './modules/storage/storage.module';
|
|
6
8
|
|
|
7
9
|
@Module({
|
|
8
10
|
imports: [
|
|
@@ -16,9 +18,30 @@ import { RoleModule } from './modules/role/role.module';
|
|
|
16
18
|
autoLoadEntities: true,
|
|
17
19
|
synchronize: false,
|
|
18
20
|
}),
|
|
21
|
+
(StorageModule as any).registerAsync({
|
|
22
|
+
useFactory: () => ({
|
|
23
|
+
driver: (process.env.STORAGE_DRIVER || 'local') as 'local' | 's3' | 'r2',
|
|
24
|
+
local: {
|
|
25
|
+
storagePath: process.env.STORAGE_LOCAL_PATH || './storage',
|
|
26
|
+
},
|
|
27
|
+
s3: {
|
|
28
|
+
bucket: process.env.S3_BUCKET || 'my-bucket',
|
|
29
|
+
region: process.env.SRegion || 'us-east-1',
|
|
30
|
+
accessKeyId: process.env.S3_ACCESS_KEY || '',
|
|
31
|
+
secretAccessKey: process.env.S3_SECRET_KEY || '',
|
|
32
|
+
},
|
|
33
|
+
r2: {
|
|
34
|
+
bucket: process.env.R2_BUCKET || 'my-bucket',
|
|
35
|
+
accountId: process.env.R2_ACCOUNT_ID || '',
|
|
36
|
+
accessKeyId: process.env.R2_ACCESS_KEY || '',
|
|
37
|
+
secretAccessKey: process.env.R2_SECRET_KEY || '',
|
|
38
|
+
}
|
|
39
|
+
})
|
|
40
|
+
}),
|
|
19
41
|
UserModule,
|
|
20
42
|
AuthModule,
|
|
21
43
|
RoleModule,
|
|
44
|
+
StorageDownloadModule,
|
|
22
45
|
],
|
|
23
46
|
controllers: [],
|
|
24
47
|
providers: [],
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { MigrationInterface, QueryRunner, Table } from 'typeorm';
|
|
2
|
+
|
|
3
|
+
export class CreateRoles1600000000000 implements MigrationInterface {
|
|
4
|
+
name = 'CreateRoles1600000000000';
|
|
5
|
+
|
|
6
|
+
async up(queryRunner: QueryRunner): Promise<void> {
|
|
7
|
+
await queryRunner.createTable(new Table({
|
|
8
|
+
name: 'roles',
|
|
9
|
+
columns: [
|
|
10
|
+
{ name: 'id', type: 'int', isPrimary: true, isGenerated: true, generationStrategy: 'increment' },
|
|
11
|
+
{ name: 'name', type: 'varchar', length: '255', isUnique: true },
|
|
12
|
+
{ name: 'permissions', type: 'text', isNullable: true }
|
|
13
|
+
]
|
|
14
|
+
}), true);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async down(queryRunner: QueryRunner): Promise<void> {
|
|
18
|
+
await queryRunner.dropTable('roles', true);
|
|
19
|
+
}
|
|
20
|
+
}
|
package/dist/boilerplates/nest-template/src/database/migrations/1700000000000-create-users.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { MigrationInterface, QueryRunner, Table } from 'typeorm';
|
|
1
|
+
import { MigrationInterface, QueryRunner, Table, TableForeignKey } from 'typeorm';
|
|
2
2
|
|
|
3
3
|
export class CreateUsers1700000000000 implements MigrationInterface {
|
|
4
4
|
name = 'CreateUsers1700000000000';
|
|
@@ -11,16 +11,27 @@ export class CreateUsers1700000000000 implements MigrationInterface {
|
|
|
11
11
|
{ name: 'name', type: 'varchar', length: '255' },
|
|
12
12
|
{ name: 'email', type: 'varchar', length: '255', isUnique: true },
|
|
13
13
|
{ name: 'password', type: 'varchar', length: '255' },
|
|
14
|
-
{ name: '
|
|
14
|
+
{ name: 'role_id', type: 'int' },
|
|
15
|
+
{ name: 'current_refresh_token', type: 'varchar', length: '500', isNullable: true },
|
|
15
16
|
{ name: 'tenant_id', type: 'varchar', length: '255', isNullable: true },
|
|
16
17
|
{ name: 'is_active', type: 'boolean', default: "'1'" },
|
|
17
18
|
{ name: 'created_at', type: 'datetime', default: 'CURRENT_TIMESTAMP' },
|
|
18
19
|
{ name: 'updated_at', type: 'datetime', default: 'CURRENT_TIMESTAMP', onUpdate: 'CURRENT_TIMESTAMP' }
|
|
19
20
|
]
|
|
20
21
|
}), true);
|
|
22
|
+
|
|
23
|
+
await queryRunner.createForeignKey('users', new TableForeignKey({
|
|
24
|
+
name: 'FK_users_role_id',
|
|
25
|
+
columnNames: ['role_id'],
|
|
26
|
+
referencedColumnNames: ['id'],
|
|
27
|
+
referencedTableName: 'roles',
|
|
28
|
+
onDelete: 'RESTRICT'
|
|
29
|
+
}));
|
|
21
30
|
}
|
|
22
31
|
|
|
23
32
|
async down(queryRunner: QueryRunner): Promise<void> {
|
|
33
|
+
await queryRunner.dropForeignKey('users', 'FK_users_role_id');
|
|
24
34
|
await queryRunner.dropTable('users', true);
|
|
25
35
|
}
|
|
26
36
|
}
|
|
37
|
+
|
|
@@ -16,6 +16,22 @@ export class AuthController {
|
|
|
16
16
|
return this.authService.login(body.email, body.password);
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
@Post('refresh')
|
|
20
|
+
@ApiOperation({ summary: 'Atualiza o access token a partir do refresh token' })
|
|
21
|
+
@ApiResponse({ status: 200, description: 'Tokens atualizados com sucesso.' })
|
|
22
|
+
async refresh(@Body() body: any) {
|
|
23
|
+
return this.authService.refresh(body.email, body.refreshToken);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
@Post('logout')
|
|
27
|
+
@UseGuards(JwtAuthGuard)
|
|
28
|
+
@ApiBearerAuth()
|
|
29
|
+
@ApiOperation({ summary: 'Efetua logout e revoga a sessão do usuário' })
|
|
30
|
+
@ApiResponse({ status: 200, description: 'Logout efetuado com sucesso.' })
|
|
31
|
+
async logout(@CurrentUser() user: any) {
|
|
32
|
+
return this.authService.logout(user.sub);
|
|
33
|
+
}
|
|
34
|
+
|
|
19
35
|
@Post('register')
|
|
20
36
|
@ApiOperation({ summary: 'Auto-cadastro de novos usuários' })
|
|
21
37
|
@ApiResponse({ status: 201, description: 'Usuário cadastrado com sucesso.' })
|
|
@@ -25,7 +25,7 @@ export class AuthService {
|
|
|
25
25
|
});
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
async login(email: string, pass: string): Promise<{ accessToken: string; user: Omit<User, 'password'> }> {
|
|
28
|
+
async login(email: string, pass: string): Promise<{ accessToken: string; refreshToken: string; user: Omit<User, 'password' | 'currentRefreshToken'> }> {
|
|
29
29
|
const user = await this.userService.findByEmail(email);
|
|
30
30
|
if (!user) {
|
|
31
31
|
throw new UnauthorizedException('Invalid credentials');
|
|
@@ -40,6 +40,37 @@ export class AuthService {
|
|
|
40
40
|
throw new UnauthorizedException('User is inactive');
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
return this.generateTokensForUser(user);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async refresh(email: string, refreshToken: string): Promise<{ accessToken: string; refreshToken: string; user: Omit<User, 'password' | 'currentRefreshToken'> }> {
|
|
47
|
+
try {
|
|
48
|
+
const payload = await this.jwtService.verifyAsync(refreshToken);
|
|
49
|
+
if (payload.email !== email) {
|
|
50
|
+
throw new UnauthorizedException('Invalid refresh token');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const user = await this.userService.findByEmail(email);
|
|
54
|
+
if (!user || !user.currentRefreshToken || !user.isActive) {
|
|
55
|
+
throw new UnauthorizedException('Session expired or user inactive');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const isMatch = await bcrypt.compare(refreshToken, user.currentRefreshToken);
|
|
59
|
+
if (!isMatch) {
|
|
60
|
+
throw new UnauthorizedException('Invalid session');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return this.generateTokensForUser(user);
|
|
64
|
+
} catch (e) {
|
|
65
|
+
throw new UnauthorizedException('Invalid or expired refresh token');
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async logout(userId: number): Promise<void> {
|
|
70
|
+
await this.userService.update(userId, { currentRefreshToken: undefined } as any);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
private async generateTokensForUser(user: User) {
|
|
43
74
|
const payload = {
|
|
44
75
|
sub: user.id,
|
|
45
76
|
email: user.email,
|
|
@@ -48,17 +79,25 @@ export class AuthService {
|
|
|
48
79
|
permissions: user.role?.permissions || []
|
|
49
80
|
};
|
|
50
81
|
|
|
51
|
-
const
|
|
82
|
+
const accessToken = await this.jwtService.signAsync(payload, { expiresIn: '15m' });
|
|
83
|
+
const refreshToken = await this.jwtService.signAsync({ email: user.email }, { expiresIn: '7d' });
|
|
84
|
+
|
|
85
|
+
const salt = await bcrypt.genSalt(10);
|
|
86
|
+
const hashedRefreshToken = await bcrypt.hash(refreshToken, salt);
|
|
87
|
+
await this.userService.update(user.id, { currentRefreshToken: hashedRefreshToken } as any);
|
|
88
|
+
|
|
89
|
+
const { password, currentRefreshToken, ...result } = user;
|
|
52
90
|
return {
|
|
53
|
-
accessToken
|
|
91
|
+
accessToken,
|
|
92
|
+
refreshToken,
|
|
54
93
|
user: result,
|
|
55
94
|
};
|
|
56
95
|
}
|
|
57
96
|
|
|
58
|
-
async validateUserById(id: number): Promise<Omit<User, 'password'> | null> {
|
|
97
|
+
async validateUserById(id: number): Promise<Omit<User, 'password' | 'currentRefreshToken'> | null> {
|
|
59
98
|
const user = await this.userService.findById(id);
|
|
60
99
|
if (!user) return null;
|
|
61
|
-
const { password, ...result } = user;
|
|
100
|
+
const { password, currentRefreshToken, ...result } = user;
|
|
62
101
|
return result;
|
|
63
102
|
}
|
|
64
103
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Controller, Get, Param, Res, NotFoundException, Inject } from '@nestjs/common';
|
|
2
|
+
import { Response } from 'express';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import * as fs from 'fs';
|
|
5
|
+
import { STORAGE_OPTIONS_TOKEN, StorageService } from '@devstroupe/devkit-nest';
|
|
6
|
+
|
|
7
|
+
@Controller('storage')
|
|
8
|
+
export class StorageDownloadController {
|
|
9
|
+
constructor(
|
|
10
|
+
@Inject(STORAGE_OPTIONS_TOKEN)
|
|
11
|
+
private readonly storageOptions: any,
|
|
12
|
+
private readonly storageService: StorageService,
|
|
13
|
+
) {}
|
|
14
|
+
|
|
15
|
+
@Get('download/:key(*)')
|
|
16
|
+
async download(@Param('key') key: string, @Res() res: Response) {
|
|
17
|
+
if (this.storageOptions.driver === 'local') {
|
|
18
|
+
const storagePath = path.resolve(this.storageOptions.local?.storagePath || './storage');
|
|
19
|
+
const targetFile = path.resolve(storagePath, key);
|
|
20
|
+
const rootPrefix = storagePath.endsWith(path.sep) ? storagePath : storagePath + path.sep;
|
|
21
|
+
|
|
22
|
+
// Mitigação Directory Traversal
|
|
23
|
+
if (!targetFile.startsWith(rootPrefix)) {
|
|
24
|
+
throw new NotFoundException('Arquivo não encontrado ou acesso inválido.');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (!fs.existsSync(targetFile)) {
|
|
28
|
+
throw new NotFoundException('Arquivo não encontrado.');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return res.sendFile(targetFile);
|
|
32
|
+
} else {
|
|
33
|
+
// S3 / R2
|
|
34
|
+
const url = await this.storageService.getSignedUrl(key, 300);
|
|
35
|
+
return res.redirect(url);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -17,6 +17,8 @@ export class DatabaseSeedService implements OnApplicationBootstrap {
|
|
|
17
17
|
|
|
18
18
|
private async seedUsers() {
|
|
19
19
|
try {
|
|
20
|
+
const isProduction = process.env.NODE_ENV === 'production';
|
|
21
|
+
|
|
20
22
|
// 1. Criar Perfil Administrador se não existir
|
|
21
23
|
let adminRole = await this.roleService.findByName('Administrador');
|
|
22
24
|
if (!adminRole) {
|
|
@@ -31,9 +33,9 @@ export class DatabaseSeedService implements OnApplicationBootstrap {
|
|
|
31
33
|
});
|
|
32
34
|
}
|
|
33
35
|
|
|
34
|
-
// 2. Criar Perfil Usuário Comum se não existir
|
|
36
|
+
// 2. Criar Perfil Usuário Comum se não existir (apenas se não for produção)
|
|
35
37
|
let userRole = await this.roleService.findByName('Usuário Comum');
|
|
36
|
-
if (!userRole) {
|
|
38
|
+
if (!userRole && !isProduction) {
|
|
37
39
|
this.logger.log('Semente: Criando Perfil Usuário Comum...');
|
|
38
40
|
userRole = await this.roleService.create({
|
|
39
41
|
name: 'Usuário Comum',
|
|
@@ -41,32 +43,43 @@ export class DatabaseSeedService implements OnApplicationBootstrap {
|
|
|
41
43
|
});
|
|
42
44
|
}
|
|
43
45
|
|
|
44
|
-
|
|
46
|
+
// 3. Admin Inicial das Envs
|
|
47
|
+
const adminEmail = process.env.ADMIN_EMAIL || 'admin@devstroupe.com';
|
|
48
|
+
const adminPassword = process.env.ADMIN_PASSWORD;
|
|
49
|
+
|
|
50
|
+
if (isProduction && !process.env.ADMIN_PASSWORD) {
|
|
51
|
+
this.logger.error('Semente: ERRO! A variável de ambiente ADMIN_PASSWORD precisa ser definida em produção.');
|
|
52
|
+
throw new Error('ADMIN_PASSWORD não configurada em produção.');
|
|
53
|
+
}
|
|
54
|
+
|
|
45
55
|
const existingAdmin = await this.userService.findByEmail(adminEmail);
|
|
46
56
|
if (!existingAdmin) {
|
|
47
|
-
this.logger.log('Semente: Criando usuário administrador
|
|
57
|
+
this.logger.log('Semente: Criando usuário administrador...');
|
|
48
58
|
await this.userService.create({
|
|
49
|
-
name: 'Administrador
|
|
59
|
+
name: 'Administrador',
|
|
50
60
|
email: adminEmail,
|
|
51
|
-
password: 'adminpassword123',
|
|
61
|
+
password: adminPassword || 'adminpassword123',
|
|
52
62
|
role: adminRole,
|
|
53
63
|
isActive: true,
|
|
54
64
|
});
|
|
55
65
|
this.logger.log('Semente: Usuário administrador cadastrado com sucesso!');
|
|
56
66
|
}
|
|
57
67
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
this.
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
68
|
+
// 4. Criar Usuário Comum apenas se não for ambiente de produção
|
|
69
|
+
if (!isProduction) {
|
|
70
|
+
const userEmail = 'user@devstroupe.com';
|
|
71
|
+
const existingUser = await this.userService.findByEmail(userEmail);
|
|
72
|
+
if (!existingUser && userRole) {
|
|
73
|
+
this.logger.log('Semente: Criando usuário comum padrão...');
|
|
74
|
+
await this.userService.create({
|
|
75
|
+
name: 'Usuário Comum',
|
|
76
|
+
email: userEmail,
|
|
77
|
+
password: 'userpassword123',
|
|
78
|
+
role: userRole,
|
|
79
|
+
isActive: true,
|
|
80
|
+
});
|
|
81
|
+
this.logger.log('Semente: Usuário comum cadastrado com sucesso!');
|
|
82
|
+
}
|
|
70
83
|
}
|
|
71
84
|
} catch (error) {
|
|
72
85
|
this.logger.error('Erro durante a execução do seeding de usuários:', error);
|
|
@@ -22,6 +22,9 @@ export class UserOrmEntity {
|
|
|
22
22
|
@Column({ type: 'varchar', length: 255, nullable: true, name: 'tenant_id' })
|
|
23
23
|
tenantId?: string;
|
|
24
24
|
|
|
25
|
+
@Column({ type: 'varchar', length: 500, nullable: true, name: 'current_refresh_token' })
|
|
26
|
+
currentRefreshToken?: string;
|
|
27
|
+
|
|
25
28
|
@Column({ type: 'boolean', default: true, name: 'is_active' })
|
|
26
29
|
isActive!: boolean;
|
|
27
30
|
|
package/dist/boilerplates/nest-template/src/modules/user/infra/database/user.repository.adapter.ts
CHANGED
|
@@ -20,6 +20,7 @@ export class UserRepositoryAdapter implements IUserRepository {
|
|
|
20
20
|
password: orm.password,
|
|
21
21
|
role: orm.role,
|
|
22
22
|
tenantId: orm.tenantId,
|
|
23
|
+
currentRefreshToken: orm.currentRefreshToken,
|
|
23
24
|
isActive: orm.isActive,
|
|
24
25
|
};
|
|
25
26
|
}
|
|
@@ -47,6 +48,11 @@ export class UserRepositoryAdapter implements IUserRepository {
|
|
|
47
48
|
return found ? this.toDomain(found) : null;
|
|
48
49
|
}
|
|
49
50
|
|
|
51
|
+
async findAll(): Promise<User[]> {
|
|
52
|
+
const found = await this.repo.find({ relations: ['role'] });
|
|
53
|
+
return found.map((u) => this.toDomain(u));
|
|
54
|
+
}
|
|
55
|
+
|
|
50
56
|
async delete(id: number): Promise<void> {
|
|
51
57
|
await this.repo.delete(id);
|
|
52
58
|
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { Controller, Get, Post, Put, Delete, Body, Param, UseGuards, ParseIntPipe } from '@nestjs/common';
|
|
2
|
+
import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth } from '@nestjs/swagger';
|
|
3
|
+
import { UserService } from '../../service/user.service';
|
|
4
|
+
import { JwtAuthGuard } from '../../../auth/jwt-auth.guard';
|
|
5
|
+
import { Permissions, PermissionsGuard } from '@devstroupe/devkit-nest';
|
|
6
|
+
import { User } from '../../domain/user';
|
|
7
|
+
|
|
8
|
+
@ApiTags('users')
|
|
9
|
+
@Controller('user')
|
|
10
|
+
@UseGuards(JwtAuthGuard, PermissionsGuard)
|
|
11
|
+
@ApiBearerAuth()
|
|
12
|
+
export class UserController {
|
|
13
|
+
constructor(private readonly userService: UserService) {}
|
|
14
|
+
|
|
15
|
+
@Get()
|
|
16
|
+
@Permissions('read:user')
|
|
17
|
+
@ApiOperation({ summary: 'Listar todos os usuários (Admin)' })
|
|
18
|
+
@ApiResponse({ status: 200, description: 'Usuários listados com sucesso.' })
|
|
19
|
+
async findAll() {
|
|
20
|
+
// Retorna todos os usuários (como é para administração)
|
|
21
|
+
// Nota: O ideal em produção seria paginar, mas para o CRUD administrativo básico serve
|
|
22
|
+
// Vamos chamar o repositório ou expor um método de listagem.
|
|
23
|
+
// Como o UserService atual não possui findAll, vamos buscar direto
|
|
24
|
+
// do repositório ou implementar no UserService.
|
|
25
|
+
return this.userService.findAll();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
@Get(':id')
|
|
29
|
+
@Permissions('read:user')
|
|
30
|
+
@ApiOperation({ summary: 'Obter um usuário pelo ID' })
|
|
31
|
+
async findOne(@Param('id', ParseIntPipe) id: number) {
|
|
32
|
+
return this.userService.findById(id);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
@Post()
|
|
36
|
+
@Permissions('create:user')
|
|
37
|
+
@ApiOperation({ summary: 'Criar um novo usuário' })
|
|
38
|
+
async create(@Body() body: Omit<User, 'id'>) {
|
|
39
|
+
return this.userService.create(body);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
@Put(':id')
|
|
43
|
+
@Permissions('update:user')
|
|
44
|
+
@ApiOperation({ summary: 'Atualizar dados de um usuário' })
|
|
45
|
+
async update(@Param('id', ParseIntPipe) id: number, @Body() body: Partial<User>) {
|
|
46
|
+
return this.userService.update(id, body);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
@Delete(':id')
|
|
50
|
+
@Permissions('delete:user')
|
|
51
|
+
@ApiOperation({ summary: 'Excluir um usuário' })
|
|
52
|
+
async delete(@Param('id', ParseIntPipe) id: number) {
|
|
53
|
+
await this.userService.delete(id);
|
|
54
|
+
return { success: true };
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -5,12 +5,14 @@ import { UserRepositoryAdapter } from './infra/database/user.repository.adapter'
|
|
|
5
5
|
import { UserService } from './service/user.service';
|
|
6
6
|
import { DatabaseSeedService } from './database-seed.service';
|
|
7
7
|
import { RoleModule } from '../role/role.module';
|
|
8
|
+
import { UserController } from './infra/http/user.controller';
|
|
8
9
|
|
|
9
10
|
@Module({
|
|
10
11
|
imports: [
|
|
11
12
|
TypeOrmModule.forFeature([UserOrmEntity]),
|
|
12
13
|
RoleModule,
|
|
13
14
|
],
|
|
15
|
+
controllers: [UserController],
|
|
14
16
|
providers: [
|
|
15
17
|
UserService,
|
|
16
18
|
DatabaseSeedService,
|
package/dist/index.js
CHANGED
|
@@ -450,6 +450,7 @@ function initUi(frontendRoot, config) {
|
|
|
450
450
|
fs.ensureDirSync(path.join(targetDir, 'card-list'));
|
|
451
451
|
fs.ensureDirSync(path.join(targetDir, 'simple-list'));
|
|
452
452
|
fs.ensureDirSync(path.join(targetDir, 'dialog'));
|
|
453
|
+
fs.ensureDirSync(path.join(targetDir, 'textarea'));
|
|
453
454
|
fs.ensureDirSync(path.join(targetDir, 'shell'));
|
|
454
455
|
fs.ensureDirSync(path.join(targetDir, 'sidebar'));
|
|
455
456
|
fs.ensureDirSync(path.join(targetDir, 'header'));
|
|
@@ -468,6 +469,10 @@ function initUi(frontendRoot, config) {
|
|
|
468
469
|
const inp = (0, ui_templates_1.devkitInputTemplate)();
|
|
469
470
|
fs.writeFileSync(path.join(targetDir, 'input', 'input.component.ts'), inp.ts, 'utf-8');
|
|
470
471
|
fs.writeFileSync(path.join(targetDir, 'input', 'input.component.html'), inp.html, 'utf-8');
|
|
472
|
+
// 3.5. Escrever Textarea Component
|
|
473
|
+
const txt = (0, ui_templates_1.devkitTextareaTemplate)();
|
|
474
|
+
fs.writeFileSync(path.join(targetDir, 'textarea', 'textarea.component.ts'), txt.ts, 'utf-8');
|
|
475
|
+
fs.writeFileSync(path.join(targetDir, 'textarea', 'textarea.component.html'), txt.html, 'utf-8');
|
|
471
476
|
// 4. Escrever Select Component
|
|
472
477
|
const sel = (0, ui_templates_1.devkitSelectTemplate)();
|
|
473
478
|
fs.writeFileSync(path.join(targetDir, 'select', 'select.component.ts'), sel.ts, 'utf-8');
|
|
@@ -714,10 +719,16 @@ function ensureNavigationIcons(frontendRoot, icons) {
|
|
|
714
719
|
content = content.replace(/(provideIcons\(\{[\s\S]*?)(\n\s*}\))/, (match, providers, suffix) => `${providers.replace(/\s*$/, '')},\n ${missingIcons.join(',\n ')}${suffix}`);
|
|
715
720
|
fs.writeFileSync(configPath, content, 'utf-8');
|
|
716
721
|
}
|
|
722
|
+
let cliVersion = '1.1.0';
|
|
723
|
+
try {
|
|
724
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8'));
|
|
725
|
+
cliVersion = pkg.version;
|
|
726
|
+
}
|
|
727
|
+
catch { }
|
|
717
728
|
program
|
|
718
729
|
.name('devstroupe')
|
|
719
730
|
.description('DevsTroupe Development Kit (DT-DevKit) CLI')
|
|
720
|
-
.version(
|
|
731
|
+
.version(cliVersion);
|
|
721
732
|
// Comando init
|
|
722
733
|
program
|
|
723
734
|
.command('init')
|
|
@@ -1039,7 +1050,7 @@ SERVICE_PORT=${port}
|
|
|
1039
1050
|
// Controller (TCP)
|
|
1040
1051
|
fs.writeFileSync(path.join(modulePath, 'infra', 'http', `${entity.name}.ms.controller.ts`), (0, cli_templates_1.nestMsControllerTemplate)(entity.name), 'utf-8');
|
|
1041
1052
|
// ORM Entity
|
|
1042
|
-
fs.writeFileSync(path.join(modulePath, 'infra', 'database', `${entity.name}.orm-entity.ts`), (0, cli_templates_1.nestOrmEntityTemplate)(entity.name, backendProperties, entity.tableName || `${entity.name}s`, isTenantScoped), 'utf-8');
|
|
1053
|
+
fs.writeFileSync(path.join(modulePath, 'infra', 'database', `${entity.name}.orm-entity.ts`), (0, cli_templates_1.nestOrmEntityTemplate)(entity.name, backendProperties, entity.tableName || `${entity.name}s`, isTenantScoped, entity.indexes, entity.persistence, entity.automaticFields), 'utf-8');
|
|
1043
1054
|
// Repository Adapter
|
|
1044
1055
|
fs.writeFileSync(path.join(modulePath, 'infra', 'database', `${entity.name}.repository.ts`), (0, cli_templates_1.nestRepositoryAdapterTemplate)(entity.name, backendProperties, isTenantScoped), 'utf-8');
|
|
1045
1056
|
// Service
|
|
@@ -1079,9 +1090,9 @@ SERVICE_PORT=${port}
|
|
|
1079
1090
|
// DTO
|
|
1080
1091
|
fs.writeFileSync(path.join(modulePath, 'infra', 'http', `${entity.name}.dto.ts`), (0, cli_templates_1.nestDtoTemplate)(entity.name, entity.properties), 'utf-8');
|
|
1081
1092
|
// Controller
|
|
1082
|
-
fs.writeFileSync(path.join(modulePath, 'infra', 'http', `${entity.name}.controller.ts`), (0, cli_templates_1.nestControllerTemplate)(entity.name), 'utf-8');
|
|
1093
|
+
fs.writeFileSync(path.join(modulePath, 'infra', 'http', `${entity.name}.controller.ts`), (0, cli_templates_1.nestControllerTemplate)(entity.name, entity.persistence), 'utf-8');
|
|
1083
1094
|
// ORM Entity
|
|
1084
|
-
fs.writeFileSync(path.join(modulePath, 'infra', 'database', `${entity.name}.orm-entity.ts`), (0, cli_templates_1.nestOrmEntityTemplate)(entity.name, entity.properties, entity.tableName || `${entity.name}s`, isTenantScoped), 'utf-8');
|
|
1095
|
+
fs.writeFileSync(path.join(modulePath, 'infra', 'database', `${entity.name}.orm-entity.ts`), (0, cli_templates_1.nestOrmEntityTemplate)(entity.name, entity.properties, entity.tableName || `${entity.name}s`, isTenantScoped, entity.indexes, entity.persistence, entity.automaticFields), 'utf-8');
|
|
1085
1096
|
// Repository Adapter
|
|
1086
1097
|
fs.writeFileSync(path.join(modulePath, 'infra', 'database', `${entity.name}.repository.ts`), (0, cli_templates_1.nestRepositoryAdapterTemplate)(entity.name, entity.properties, isTenantScoped), 'utf-8');
|
|
1087
1098
|
// Service
|
|
@@ -1681,6 +1692,22 @@ DB_PORT=3306
|
|
|
1681
1692
|
DB_USER=root
|
|
1682
1693
|
DB_PASSWORD=root
|
|
1683
1694
|
DB_NAME=devstroupe
|
|
1695
|
+
|
|
1696
|
+
# Configuração de Storage Nátivo
|
|
1697
|
+
STORAGE_DRIVER=local
|
|
1698
|
+
STORAGE_LOCAL_PATH=./storage
|
|
1699
|
+
|
|
1700
|
+
# Configuração AWS S3 (Opcional se STORAGE_DRIVER=s3)
|
|
1701
|
+
S3_BUCKET=my-bucket
|
|
1702
|
+
SRegion=us-east-1
|
|
1703
|
+
S3_ACCESS_KEY=
|
|
1704
|
+
S3_SECRET_KEY=
|
|
1705
|
+
|
|
1706
|
+
# Configuração Cloudflare R2 (Opcional se STORAGE_DRIVER=r2)
|
|
1707
|
+
R2_BUCKET=my-bucket
|
|
1708
|
+
R2_ACCOUNT_ID=
|
|
1709
|
+
R2_ACCESS_KEY=
|
|
1710
|
+
R2_SECRET_KEY=
|
|
1684
1711
|
`;
|
|
1685
1712
|
fs.writeFileSync(path.join(backendDest, '.env'), envContent, 'utf-8');
|
|
1686
1713
|
fs.writeFileSync(path.join(backendDest, '.env.example'), envContent, 'utf-8');
|