ar-saas 0.4.2 → 0.5.0
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/package.json +2 -1
- package/templates/backend/.env.example +13 -3
- package/templates/backend/README.md +22 -3
- package/templates/backend/package-lock.json +165 -2
- package/templates/backend/package.json +2 -0
- package/templates/backend/src/app.module.ts +14 -0
- package/templates/backend/src/common/guards/github-auth.guard.ts +5 -0
- package/templates/backend/src/main.ts +2 -2
- package/templates/backend/src/modules/auth/auth.controller.ts +51 -3
- package/templates/backend/src/modules/auth/auth.module.ts +2 -1
- package/templates/backend/src/modules/auth/auth.service.ts +96 -11
- package/templates/backend/src/modules/auth/strategies/github.strategy.ts +46 -0
- package/templates/backend/src/modules/auth/strategies/jwt.strategy.ts +1 -1
- package/templates/backend/src/modules/clients/clients.controller.ts +91 -0
- package/templates/backend/src/modules/clients/clients.module.ts +16 -0
- package/templates/backend/src/modules/clients/clients.repository.ts +14 -0
- package/templates/backend/src/modules/clients/clients.service.ts +52 -0
- package/templates/backend/src/modules/clients/dto/create-client.dto.ts +40 -0
- package/templates/backend/src/modules/clients/dto/query-client.dto.ts +30 -0
- package/templates/backend/src/modules/clients/dto/update-client.dto.ts +4 -0
- package/templates/backend/src/modules/clients/schemas/client.schema.ts +32 -0
- package/templates/backend/src/modules/invoices/dto/create-invoice.dto.ts +79 -0
- package/templates/backend/src/modules/invoices/dto/invoice-item.dto.ts +23 -0
- package/templates/backend/src/modules/invoices/dto/query-invoice.dto.ts +40 -0
- package/templates/backend/src/modules/invoices/dto/update-invoice.dto.ts +4 -0
- package/templates/backend/src/modules/invoices/invoices.controller.ts +91 -0
- package/templates/backend/src/modules/invoices/invoices.module.ts +18 -0
- package/templates/backend/src/modules/invoices/invoices.repository.ts +14 -0
- package/templates/backend/src/modules/invoices/invoices.service.ts +104 -0
- package/templates/backend/src/modules/invoices/schemas/invoice.schema.ts +75 -0
- package/templates/backend/src/modules/notifications/dto/create-notification.dto.ts +45 -0
- package/templates/backend/src/modules/notifications/dto/query-notification.dto.ts +30 -0
- package/templates/backend/src/modules/notifications/dto/update-notification.dto.ts +4 -0
- package/templates/backend/src/modules/notifications/notifications.controller.ts +119 -0
- package/templates/backend/src/modules/notifications/notifications.module.ts +16 -0
- package/templates/backend/src/modules/notifications/notifications.repository.ts +31 -0
- package/templates/backend/src/modules/notifications/notifications.service.ts +64 -0
- package/templates/backend/src/modules/notifications/schemas/notification.schema.ts +38 -0
- package/templates/backend/src/modules/pipeline/dto/create-deal.dto.ts +40 -0
- package/templates/backend/src/modules/pipeline/dto/query-deal.dto.ts +35 -0
- package/templates/backend/src/modules/pipeline/dto/update-deal.dto.ts +4 -0
- package/templates/backend/src/modules/pipeline/pipeline.controller.ts +91 -0
- package/templates/backend/src/modules/pipeline/pipeline.module.ts +18 -0
- package/templates/backend/src/modules/pipeline/pipeline.repository.ts +14 -0
- package/templates/backend/src/modules/pipeline/pipeline.service.ts +64 -0
- package/templates/backend/src/modules/pipeline/schemas/deal.schema.ts +39 -0
- package/templates/backend/src/modules/planner/dto/create-planner-block.dto.ts +66 -0
- package/templates/backend/src/modules/planner/dto/query-planner-block.dto.ts +48 -0
- package/templates/backend/src/modules/planner/dto/update-block-status.dto.ts +10 -0
- package/templates/backend/src/modules/planner/dto/update-planner-block.dto.ts +4 -0
- package/templates/backend/src/modules/planner/planner.controller.ts +124 -0
- package/templates/backend/src/modules/planner/planner.module.ts +16 -0
- package/templates/backend/src/modules/planner/planner.repository.ts +45 -0
- package/templates/backend/src/modules/planner/planner.service.ts +104 -0
- package/templates/backend/src/modules/planner/schemas/planner-block.schema.ts +56 -0
- package/templates/backend/src/modules/task-columns/dto/create-task-column.dto.ts +20 -0
- package/templates/backend/src/modules/task-columns/dto/reorder-columns.dto.ts +9 -0
- package/templates/backend/src/modules/task-columns/dto/update-task-column.dto.ts +4 -0
- package/templates/backend/src/modules/task-columns/schemas/task-column.schema.ts +21 -0
- package/templates/backend/src/modules/task-columns/task-columns.controller.ts +86 -0
- package/templates/backend/src/modules/task-columns/task-columns.module.ts +16 -0
- package/templates/backend/src/modules/task-columns/task-columns.repository.ts +15 -0
- package/templates/backend/src/modules/task-columns/task-columns.service.ts +49 -0
- package/templates/backend/src/modules/tasks/dto/checklist-item.dto.ts +13 -0
- package/templates/backend/src/modules/tasks/dto/create-task.dto.ts +67 -0
- package/templates/backend/src/modules/tasks/dto/label.dto.ts +12 -0
- package/templates/backend/src/modules/tasks/dto/move-task.dto.ts +15 -0
- package/templates/backend/src/modules/tasks/dto/query-task.dto.ts +40 -0
- package/templates/backend/src/modules/tasks/dto/update-task.dto.ts +4 -0
- package/templates/backend/src/modules/tasks/schemas/task.schema.ts +66 -0
- package/templates/backend/src/modules/tasks/tasks.controller.ts +104 -0
- package/templates/backend/src/modules/tasks/tasks.module.ts +18 -0
- package/templates/backend/src/modules/tasks/tasks.repository.ts +14 -0
- package/templates/backend/src/modules/tasks/tasks.service.ts +76 -0
- package/templates/backend/src/modules/users/schemas/user.schema.ts +3 -0
- package/templates/backend/src/modules/users/users.repository.ts +8 -0
- package/templates/backend/src/modules/users/users.service.ts +34 -0
- package/templates/frontend/.env.local.example +1 -1
- package/templates/frontend/README.md +43 -1
- package/templates/frontend/package.json +48 -45
- package/templates/frontend/pnpm-lock.yaml +5096 -5012
- package/templates/frontend/src/app/(auth)/layout.tsx +7 -1
- package/templates/frontend/src/app/(auth)/login/page.tsx +13 -0
- package/templates/frontend/src/app/(auth)/register/page.tsx +13 -0
- package/templates/frontend/src/app/(dashboard)/clients/page.tsx +295 -0
- package/templates/frontend/src/app/(dashboard)/invoices/page.tsx +305 -0
- package/templates/frontend/src/app/(dashboard)/notifications/page.tsx +173 -0
- package/templates/frontend/src/app/(dashboard)/pipeline/page.tsx +244 -0
- package/templates/frontend/src/app/(dashboard)/planner/page.tsx +287 -0
- package/templates/frontend/src/app/(dashboard)/settings/page.tsx +165 -128
- package/templates/frontend/src/app/(dashboard)/tasks/page.tsx +366 -0
- package/templates/frontend/src/app/auth/github/callback/page.tsx +82 -0
- package/templates/frontend/src/app/landing/page.tsx +21 -0
- package/templates/frontend/src/app/page.tsx +5 -5
- package/templates/frontend/src/app/setup/page.tsx +15 -14
- package/templates/frontend/src/components/auth/github-button.tsx +25 -0
- package/templates/frontend/src/components/dashboard/sidebar.tsx +90 -71
- package/templates/frontend/src/components/ui/alert-dialog.tsx +141 -0
- package/templates/frontend/src/components/ui/button.tsx +56 -52
- package/templates/frontend/src/components/ui/popover.tsx +31 -0
- package/templates/frontend/src/components/ui/select.tsx +160 -0
- package/templates/frontend/src/components/ui/sheet.tsx +140 -0
- package/templates/frontend/src/lib/api/auth.ts +7 -0
- package/templates/frontend/src/lib/api/clients.ts +17 -0
- package/templates/frontend/src/lib/api/invoices.ts +18 -0
- package/templates/frontend/src/lib/api/notifications.ts +27 -0
- package/templates/frontend/src/lib/api/pipeline.ts +18 -0
- package/templates/frontend/src/lib/api/planner.ts +26 -0
- package/templates/frontend/src/lib/api/task-columns.ts +17 -0
- package/templates/frontend/src/lib/api/tasks.ts +21 -0
- package/templates/frontend/src/lib/hooks/use-unread-notifications.ts +23 -0
- package/templates/frontend/src/providers/auth-provider.tsx +7 -1
- package/templates/frontend/src/types/clients.ts +38 -0
- package/templates/frontend/src/types/invoices.ts +51 -0
- package/templates/frontend/src/types/notifications.ts +30 -0
- package/templates/frontend/src/types/pipeline.ts +35 -0
- package/templates/frontend/src/types/planner.ts +49 -0
- package/templates/frontend/src/types/tasks.ts +65 -0
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Body,
|
|
3
|
+
Controller,
|
|
4
|
+
Delete,
|
|
5
|
+
Get,
|
|
6
|
+
HttpCode,
|
|
7
|
+
HttpStatus,
|
|
8
|
+
Param,
|
|
9
|
+
Patch,
|
|
10
|
+
Post,
|
|
11
|
+
Query,
|
|
12
|
+
UseGuards,
|
|
13
|
+
} from '@nestjs/common';
|
|
14
|
+
import {
|
|
15
|
+
ApiBearerAuth,
|
|
16
|
+
ApiOperation,
|
|
17
|
+
ApiResponse,
|
|
18
|
+
ApiTags,
|
|
19
|
+
} from '@nestjs/swagger';
|
|
20
|
+
import { CurrentUser } from '../../common/decorators/current-user.decorator';
|
|
21
|
+
import type { TokenPayload } from '../../common/decorators/current-user.decorator';
|
|
22
|
+
import { WorkspaceId } from '../../common/decorators/workspace-id.decorator';
|
|
23
|
+
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
|
|
24
|
+
import { CreateTaskDto } from './dto/create-task.dto';
|
|
25
|
+
import { MoveTaskDto } from './dto/move-task.dto';
|
|
26
|
+
import { QueryTaskDto } from './dto/query-task.dto';
|
|
27
|
+
import { UpdateTaskDto } from './dto/update-task.dto';
|
|
28
|
+
import { TasksService } from './tasks.service';
|
|
29
|
+
|
|
30
|
+
@ApiTags('Tasks')
|
|
31
|
+
@ApiBearerAuth()
|
|
32
|
+
@UseGuards(JwtAuthGuard)
|
|
33
|
+
@Controller('tasks')
|
|
34
|
+
export class TasksController {
|
|
35
|
+
constructor(private readonly tasksService: TasksService) {}
|
|
36
|
+
|
|
37
|
+
@Get()
|
|
38
|
+
@ApiOperation({ summary: 'Listar tareas del workspace' })
|
|
39
|
+
@ApiResponse({ status: 200, description: 'Lista paginada de tareas' })
|
|
40
|
+
findAll(
|
|
41
|
+
@WorkspaceId() workspaceId: string,
|
|
42
|
+
@Query() query: QueryTaskDto,
|
|
43
|
+
) {
|
|
44
|
+
return this.tasksService.findAll(workspaceId, query);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
@Get(':id')
|
|
48
|
+
@ApiOperation({ summary: 'Obtener una tarea por ID' })
|
|
49
|
+
@ApiResponse({ status: 200, description: 'Tarea encontrada' })
|
|
50
|
+
@ApiResponse({ status: 404, description: 'Tarea no encontrada' })
|
|
51
|
+
findOne(
|
|
52
|
+
@WorkspaceId() workspaceId: string,
|
|
53
|
+
@Param('id') id: string,
|
|
54
|
+
) {
|
|
55
|
+
return this.tasksService.findOne(workspaceId, id);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
@Post()
|
|
59
|
+
@ApiOperation({ summary: 'Crear una nueva tarea' })
|
|
60
|
+
@ApiResponse({ status: 201, description: 'Tarea creada' })
|
|
61
|
+
create(
|
|
62
|
+
@WorkspaceId() workspaceId: string,
|
|
63
|
+
@CurrentUser() user: TokenPayload,
|
|
64
|
+
@Body() dto: CreateTaskDto,
|
|
65
|
+
) {
|
|
66
|
+
return this.tasksService.create(workspaceId, user.userId, dto);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
@Patch(':id')
|
|
70
|
+
@ApiOperation({ summary: 'Actualizar una tarea' })
|
|
71
|
+
@ApiResponse({ status: 200, description: 'Tarea actualizada' })
|
|
72
|
+
@ApiResponse({ status: 404, description: 'Tarea no encontrada' })
|
|
73
|
+
update(
|
|
74
|
+
@WorkspaceId() workspaceId: string,
|
|
75
|
+
@Param('id') id: string,
|
|
76
|
+
@Body() dto: UpdateTaskDto,
|
|
77
|
+
) {
|
|
78
|
+
return this.tasksService.update(workspaceId, id, dto);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
@Patch(':id/move')
|
|
82
|
+
@ApiOperation({ summary: 'Mover una tarea a otra columna' })
|
|
83
|
+
@ApiResponse({ status: 200, description: 'Tarea movida' })
|
|
84
|
+
@ApiResponse({ status: 404, description: 'Tarea no encontrada' })
|
|
85
|
+
move(
|
|
86
|
+
@WorkspaceId() workspaceId: string,
|
|
87
|
+
@Param('id') id: string,
|
|
88
|
+
@Body() dto: MoveTaskDto,
|
|
89
|
+
) {
|
|
90
|
+
return this.tasksService.move(workspaceId, id, dto);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
@Delete(':id')
|
|
94
|
+
@HttpCode(HttpStatus.NO_CONTENT)
|
|
95
|
+
@ApiOperation({ summary: 'Eliminar una tarea (soft delete)' })
|
|
96
|
+
@ApiResponse({ status: 204, description: 'Tarea eliminada' })
|
|
97
|
+
@ApiResponse({ status: 404, description: 'Tarea no encontrada' })
|
|
98
|
+
remove(
|
|
99
|
+
@WorkspaceId() workspaceId: string,
|
|
100
|
+
@Param('id') id: string,
|
|
101
|
+
) {
|
|
102
|
+
return this.tasksService.remove(workspaceId, id);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Module } from '@nestjs/common';
|
|
2
|
+
import { MongooseModule } from '@nestjs/mongoose';
|
|
3
|
+
import { TaskColumnsModule } from '../task-columns/task-columns.module';
|
|
4
|
+
import { Task, TaskSchema } from './schemas/task.schema';
|
|
5
|
+
import { TasksController } from './tasks.controller';
|
|
6
|
+
import { TasksRepository } from './tasks.repository';
|
|
7
|
+
import { TasksService } from './tasks.service';
|
|
8
|
+
|
|
9
|
+
@Module({
|
|
10
|
+
imports: [
|
|
11
|
+
TaskColumnsModule,
|
|
12
|
+
MongooseModule.forFeature([{ name: Task.name, schema: TaskSchema }]),
|
|
13
|
+
],
|
|
14
|
+
controllers: [TasksController],
|
|
15
|
+
providers: [TasksService, TasksRepository],
|
|
16
|
+
exports: [TasksService],
|
|
17
|
+
})
|
|
18
|
+
export class TasksModule {}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Injectable } from '@nestjs/common';
|
|
2
|
+
import { InjectModel } from '@nestjs/mongoose';
|
|
3
|
+
import { Model } from 'mongoose';
|
|
4
|
+
import { BaseRepository } from '../../common/base/base.repository';
|
|
5
|
+
import { Task, TaskDocument } from './schemas/task.schema';
|
|
6
|
+
|
|
7
|
+
@Injectable()
|
|
8
|
+
export class TasksRepository extends BaseRepository<TaskDocument> {
|
|
9
|
+
constructor(
|
|
10
|
+
@InjectModel(Task.name) private readonly taskModel: Model<TaskDocument>,
|
|
11
|
+
) {
|
|
12
|
+
super(taskModel);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { Injectable, NotFoundException } from '@nestjs/common';
|
|
2
|
+
import { TaskColumnsService } from '../task-columns/task-columns.service';
|
|
3
|
+
import { CreateTaskDto } from './dto/create-task.dto';
|
|
4
|
+
import { MoveTaskDto } from './dto/move-task.dto';
|
|
5
|
+
import { QueryTaskDto } from './dto/query-task.dto';
|
|
6
|
+
import { UpdateTaskDto } from './dto/update-task.dto';
|
|
7
|
+
import { TasksRepository } from './tasks.repository';
|
|
8
|
+
|
|
9
|
+
@Injectable()
|
|
10
|
+
export class TasksService {
|
|
11
|
+
constructor(
|
|
12
|
+
private readonly tasksRepository: TasksRepository,
|
|
13
|
+
private readonly taskColumnsService: TaskColumnsService,
|
|
14
|
+
) {}
|
|
15
|
+
|
|
16
|
+
findAll(workspaceId: string, query: QueryTaskDto) {
|
|
17
|
+
const { page, limit, search, status, priority, columnId } = query;
|
|
18
|
+
const filter: Record<string, unknown> = {};
|
|
19
|
+
|
|
20
|
+
if (status) filter.status = status;
|
|
21
|
+
if (priority) filter.priority = priority;
|
|
22
|
+
if (columnId !== undefined) filter.columnId = columnId;
|
|
23
|
+
if (search) filter.title = { $regex: search, $options: 'i' };
|
|
24
|
+
|
|
25
|
+
return this.tasksRepository.paginate(workspaceId, {
|
|
26
|
+
page,
|
|
27
|
+
limit,
|
|
28
|
+
filter,
|
|
29
|
+
sort: { columnId: 1, order: 1 },
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async findOne(workspaceId: string, id: string) {
|
|
34
|
+
const task = await this.tasksRepository.findById(workspaceId, id);
|
|
35
|
+
if (!task) throw new NotFoundException('Tarea no encontrada');
|
|
36
|
+
return task;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async create(workspaceId: string, userId: string, dto: CreateTaskDto) {
|
|
40
|
+
if (dto.columnId) {
|
|
41
|
+
await this.taskColumnsService.findOne(workspaceId, dto.columnId);
|
|
42
|
+
}
|
|
43
|
+
return this.tasksRepository.create({
|
|
44
|
+
...dto,
|
|
45
|
+
dueDate: dto.dueDate ? new Date(dto.dueDate) : undefined,
|
|
46
|
+
workspaceId: workspaceId as unknown as import('mongoose').Types.ObjectId,
|
|
47
|
+
createdBy: userId as unknown as import('mongoose').Types.ObjectId,
|
|
48
|
+
deletedAt: null,
|
|
49
|
+
} as never);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async update(workspaceId: string, id: string, dto: UpdateTaskDto) {
|
|
53
|
+
await this.findOne(workspaceId, id);
|
|
54
|
+
if (dto.columnId) {
|
|
55
|
+
await this.taskColumnsService.findOne(workspaceId, dto.columnId);
|
|
56
|
+
}
|
|
57
|
+
const task = await this.tasksRepository.update(workspaceId, id, dto as never);
|
|
58
|
+
if (!task) throw new NotFoundException('Tarea no encontrada');
|
|
59
|
+
return task;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async move(workspaceId: string, id: string, dto: MoveTaskDto) {
|
|
63
|
+
await this.findOne(workspaceId, id);
|
|
64
|
+
if (dto.columnId) {
|
|
65
|
+
await this.taskColumnsService.findOne(workspaceId, dto.columnId);
|
|
66
|
+
}
|
|
67
|
+
const task = await this.tasksRepository.update(workspaceId, id, dto as never);
|
|
68
|
+
if (!task) throw new NotFoundException('Tarea no encontrada');
|
|
69
|
+
return task;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async remove(workspaceId: string, id: string) {
|
|
73
|
+
await this.findOne(workspaceId, id);
|
|
74
|
+
await this.tasksRepository.softDelete(workspaceId, id);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -20,6 +20,9 @@ export class User {
|
|
|
20
20
|
@Prop({ type: String, default: null, select: false })
|
|
21
21
|
password!: string | null;
|
|
22
22
|
|
|
23
|
+
@Prop({ type: String, default: null, index: true, sparse: true })
|
|
24
|
+
githubId!: string | null;
|
|
25
|
+
|
|
23
26
|
@Prop({ required: true })
|
|
24
27
|
workspaceId!: string;
|
|
25
28
|
|
|
@@ -9,6 +9,10 @@ export class UsersRepository {
|
|
|
9
9
|
@InjectModel(User.name) private readonly userModel: Model<UserDocument>,
|
|
10
10
|
) {}
|
|
11
11
|
|
|
12
|
+
async findByGithubId(githubId: string): Promise<UserDocument | null> {
|
|
13
|
+
return this.userModel.findOne({ githubId }).exec();
|
|
14
|
+
}
|
|
15
|
+
|
|
12
16
|
async findByEmail(email: string): Promise<UserDocument | null> {
|
|
13
17
|
return this.userModel
|
|
14
18
|
.findOne({ email: email.toLowerCase().trim() })
|
|
@@ -54,4 +58,8 @@ export class UsersRepository {
|
|
|
54
58
|
.findByIdAndUpdate(id, { $set: data }, { returnDocument: 'after' })
|
|
55
59
|
.exec();
|
|
56
60
|
}
|
|
61
|
+
|
|
62
|
+
async delete(id: string): Promise<UserDocument | null> {
|
|
63
|
+
return this.userModel.findByIdAndDelete(id).exec();
|
|
64
|
+
}
|
|
57
65
|
}
|
|
@@ -27,6 +27,32 @@ export class UsersService {
|
|
|
27
27
|
return this.usersRepository.findByEmail(email);
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
async findByGithubId(githubId: string): Promise<UserDocument | null> {
|
|
31
|
+
return this.usersRepository.findByGithubId(githubId);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async createGithubUser(data: {
|
|
35
|
+
name: string;
|
|
36
|
+
email: string;
|
|
37
|
+
githubId: string;
|
|
38
|
+
workspaceId: string;
|
|
39
|
+
role?: UserRole;
|
|
40
|
+
}): Promise<UserDocument> {
|
|
41
|
+
const existing = await this.usersRepository.findByEmail(data.email);
|
|
42
|
+
if (existing) {
|
|
43
|
+
throw new ConflictException('Este email ya está registrado.');
|
|
44
|
+
}
|
|
45
|
+
return this.usersRepository.create({
|
|
46
|
+
...data,
|
|
47
|
+
password: null,
|
|
48
|
+
emailVerified: true,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async linkGithubId(userId: string, githubId: string): Promise<void> {
|
|
53
|
+
await this.usersRepository.update(userId, { githubId });
|
|
54
|
+
}
|
|
55
|
+
|
|
30
56
|
async findByEmailWithPassword(email: string): Promise<UserDocument | null> {
|
|
31
57
|
return this.usersRepository.findByEmailWithPassword(email);
|
|
32
58
|
}
|
|
@@ -111,6 +137,14 @@ export class UsersService {
|
|
|
111
137
|
await this.usersRepository.update(userId, { lastLoginAt: new Date() });
|
|
112
138
|
}
|
|
113
139
|
|
|
140
|
+
async updateWorkspaceId(userId: string, workspaceId: string): Promise<void> {
|
|
141
|
+
await this.usersRepository.update(userId, { workspaceId });
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async delete(userId: string): Promise<void> {
|
|
145
|
+
await this.usersRepository.delete(userId);
|
|
146
|
+
}
|
|
147
|
+
|
|
114
148
|
async updateMe(
|
|
115
149
|
userId: string,
|
|
116
150
|
data: { name?: string; email?: string },
|
|
@@ -1 +1 @@
|
|
|
1
|
-
NEXT_PUBLIC_API_URL=http://localhost:
|
|
1
|
+
NEXT_PUBLIC_API_URL=http://localhost:3001
|
|
@@ -96,10 +96,14 @@ src/
|
|
|
96
96
|
│ ├── (auth)/ # Login, registro, verificación de email, reset de contraseña
|
|
97
97
|
│ ├── (dashboard)/ # Rutas protegidas (dashboard, perfil, billing, equipo)
|
|
98
98
|
│ ├── (legal)/ # Términos y privacidad
|
|
99
|
+
│ ├── auth/
|
|
100
|
+
│ │ └── github/
|
|
101
|
+
│ │ └── callback/ # Página que completa el intercambio de código OAuth de GitHub
|
|
99
102
|
│ ├── setup/ # Pantalla de setup inicial (se muestra al arrancar por primera vez)
|
|
100
103
|
│ ├── layout.tsx # Layout raíz con AuthProvider
|
|
101
104
|
│ └── page.tsx # Redirige a /login
|
|
102
105
|
├── components/
|
|
106
|
+
│ ├── auth/ # Componentes de autenticación (GitHubButton)
|
|
103
107
|
│ ├── landing/ # Componentes de la landing page (navbar, hero, features, pricing)
|
|
104
108
|
│ ├── dashboard/ # Sidebar, header y cards del dashboard
|
|
105
109
|
│ └── ui/ # Componentes shadcn/ui (no modificar directamente)
|
|
@@ -148,5 +152,43 @@ Para agregar nuevas rutas protegidas, crear la carpeta dentro de `src/app/(dashb
|
|
|
148
152
|
## Autenticación
|
|
149
153
|
|
|
150
154
|
- Tokens JWT en cookies HttpOnly — el frontend nunca lee ni guarda tokens.
|
|
151
|
-
- `useAuth()` expone `user`, `isAuthenticated`, `isLoading`, `login()`, `logout()`, `register()`.
|
|
155
|
+
- `useAuth()` expone `user`, `isAuthenticated`, `isLoading`, `login()`, `logout()`, `register()`, `refreshUser()`.
|
|
152
156
|
- El cliente axios renueva el access token automáticamente cuando recibe un 401.
|
|
157
|
+
|
|
158
|
+
### GitHub OAuth
|
|
159
|
+
|
|
160
|
+
El flujo de autenticación con GitHub está integrado en las páginas de login y registro.
|
|
161
|
+
|
|
162
|
+
**Flujo completo:**
|
|
163
|
+
|
|
164
|
+
1. Usuario hace click en "Continuar con GitHub"
|
|
165
|
+
2. El browser redirige a `{NEXT_PUBLIC_API_URL}/api/auth/github`
|
|
166
|
+
3. El backend inicia el OAuth flow con GitHub
|
|
167
|
+
4. GitHub redirige al backend con un código de autorización
|
|
168
|
+
5. El backend valida el código, busca o crea el usuario, y genera un código de intercambio (válido 30 segundos)
|
|
169
|
+
6. El backend redirige al frontend a `/auth/github/callback?code=...`
|
|
170
|
+
7. La página callback llama a `POST /api/auth/github/exchange` con el código
|
|
171
|
+
8. El backend setea las cookies `access_token` y `refresh_token` y retorna `{ success, alreadyExisted }`
|
|
172
|
+
9. El frontend refresca el estado de auth y redirige a `/dashboard`
|
|
173
|
+
|
|
174
|
+
**Casos manejados:**
|
|
175
|
+
|
|
176
|
+
| Caso | Comportamiento |
|
|
177
|
+
|---|---|
|
|
178
|
+
| Usuario nuevo | Se crea workspace + usuario, email marcado como verificado |
|
|
179
|
+
| Usuario existente por githubId | Login directo |
|
|
180
|
+
| Usuario existente por email | Se linkea el githubId, email marcado como verificado si no lo estaba |
|
|
181
|
+
| GitHub sin email público | Error — el usuario debe hacer público su email en GitHub |
|
|
182
|
+
| Código expirado o inválido | Redirección a `/auth/github/callback?error=github_failed`, se muestra pantalla de error |
|
|
183
|
+
|
|
184
|
+
**Archivos relevantes:**
|
|
185
|
+
|
|
186
|
+
| Archivo | Descripción |
|
|
187
|
+
|---|---|
|
|
188
|
+
| [`src/components/auth/github-button.tsx`](src/components/auth/github-button.tsx) | Botón reutilizable "Continuar con GitHub" |
|
|
189
|
+
| [`src/app/auth/github/callback/page.tsx`](src/app/auth/github/callback/page.tsx) | Página que recibe el código y completa el intercambio |
|
|
190
|
+
| [`src/lib/api/auth.ts`](src/lib/api/auth.ts) | `authApi.exchangeGithubCode(code)` |
|
|
191
|
+
|
|
192
|
+
**Configuración necesaria en el backend** (ver `backend/README.md`):
|
|
193
|
+
- Crear una GitHub OAuth App en [github.com/settings/applications/new](https://github.com/settings/applications/new)
|
|
194
|
+
- Configurar `GITHUB_CLIENT_ID`, `GITHUB_CLIENT_SECRET`, `GITHUB_CALLBACK_URL` y `FRONTEND_URL` en el `.env` del backend
|
|
@@ -1,45 +1,48 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "ar-saas-frontend",
|
|
3
|
-
"version": "0.1.0",
|
|
4
|
-
"private": true,
|
|
5
|
-
"license": "UNLICENSED",
|
|
6
|
-
"scripts": {
|
|
7
|
-
"dev": "next dev --turbopack",
|
|
8
|
-
"build": "next build",
|
|
9
|
-
"start": "next start",
|
|
10
|
-
"lint": "next lint"
|
|
11
|
-
},
|
|
12
|
-
"dependencies": {
|
|
13
|
-
"@radix-ui/react-accordion": "^1.2.13",
|
|
14
|
-
"@radix-ui/react-
|
|
15
|
-
"@radix-ui/react-
|
|
16
|
-
"@radix-ui/react-
|
|
17
|
-
"@radix-ui/react-
|
|
18
|
-
"@radix-ui/react-
|
|
19
|
-
"@radix-ui/react-
|
|
20
|
-
"@radix-ui/react-
|
|
21
|
-
"@radix-ui/react-
|
|
22
|
-
"@radix-ui/react-
|
|
23
|
-
"@radix-ui/react-
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
"react
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
"
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
"@
|
|
39
|
-
"@
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
|
|
45
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "ar-saas-frontend",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"license": "UNLICENSED",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "next dev --turbopack",
|
|
8
|
+
"build": "next build",
|
|
9
|
+
"start": "next start",
|
|
10
|
+
"lint": "next lint"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@radix-ui/react-accordion": "^1.2.13",
|
|
14
|
+
"@radix-ui/react-alert-dialog": "^1.1.16",
|
|
15
|
+
"@radix-ui/react-avatar": "^1.1.12",
|
|
16
|
+
"@radix-ui/react-checkbox": "^1.3.4",
|
|
17
|
+
"@radix-ui/react-dialog": "^1.1.16",
|
|
18
|
+
"@radix-ui/react-dropdown-menu": "^2.1.17",
|
|
19
|
+
"@radix-ui/react-label": "^2.1.2",
|
|
20
|
+
"@radix-ui/react-popover": "^1.1.16",
|
|
21
|
+
"@radix-ui/react-select": "^2.3.0",
|
|
22
|
+
"@radix-ui/react-separator": "^1.1.9",
|
|
23
|
+
"@radix-ui/react-slot": "^1.2.5",
|
|
24
|
+
"@radix-ui/react-switch": "^1.3.0",
|
|
25
|
+
"@radix-ui/react-tabs": "^1.1.14",
|
|
26
|
+
"@radix-ui/react-toast": "^1.2.6",
|
|
27
|
+
"axios": "^1.7.9",
|
|
28
|
+
"class-variance-authority": "^0.7.1",
|
|
29
|
+
"clsx": "^2.1.1",
|
|
30
|
+
"lucide-react": "^0.477.0",
|
|
31
|
+
"next": "^15.3.3",
|
|
32
|
+
"react": "^19.1.0",
|
|
33
|
+
"react-dom": "^19.1.0",
|
|
34
|
+
"react-hook-form": "^7.54.2",
|
|
35
|
+
"tailwind-merge": "^2.6.0"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@eslint/eslintrc": "^3.2.0",
|
|
39
|
+
"@tailwindcss/postcss": "^4.1.0",
|
|
40
|
+
"@types/node": "^22.10.7",
|
|
41
|
+
"@types/react": "^19.1.0",
|
|
42
|
+
"@types/react-dom": "^19.1.0",
|
|
43
|
+
"eslint": "^9.18.0",
|
|
44
|
+
"eslint-config-next": "^15.3.3",
|
|
45
|
+
"tailwindcss": "^4.1.0",
|
|
46
|
+
"typescript": "^5.7.3"
|
|
47
|
+
}
|
|
48
|
+
}
|