chore: align dev branch with main isolation preserving new files

This commit is contained in:
Tiago Ribeiro 2026-03-09 16:01:45 -03:00
parent 16c1eb0efc
commit c901580a49
18 changed files with 0 additions and 4654 deletions

View file

@ -1,12 +0,0 @@
# Identity Gateway
VITE_IDENTITY_GATEWAY_URL=https://ig-dev.rede5.com.br
VITE_TENANT_ID=your-tenant-id-here
# Appwrite (legacy - can be removed)
VITE_APPWRITE_ENDPOINT=
VITE_APPWRITE_PROJECT_ID=
VITE_APPWRITE_DATABASE_ID=
VITE_APPWRITE_COLLECTION_SERVERS_ID=
VITE_APPWRITE_COLLECTION_GITHUB_REPOS_ID=
VITE_APPWRITE_COLLECTION_AUDIT_LOGS_ID=
VITE_APPWRITE_COLLECTION_CLOUDFLARE_ACCOUNTS_ID=

View file

@ -1,142 +0,0 @@
const API_URL = import.meta.env.VITE_IDENTITY_GATEWAY_URL || "https://ig-dev.rede5.com.br";
// Fallback to Rede5 dev tenant ID if env var is missing
const TENANT_ID = import.meta.env.VITE_TENANT_ID || "aff0064a-6797-421f-af0f-832438608a95";
export interface User {
id: string;
identifier: string;
status: string;
roles?: string[];
permissions?: string[];
tenantId?: string;
}
export interface LoginResponse {
accessToken: string;
refreshToken: string;
}
let accessToken: string | null = null;
export const getAccessToken = () => accessToken;
export const setAccessToken = (token: string | null) => {
accessToken = token;
};
export const loginUser = async (email: string, password: string): Promise<LoginResponse> => {
const response = await fetch(`${API_URL}/auth/login`, {
method: "POST",
credentials: "include",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
identifier: email,
secret: password,
tenantId: TENANT_ID,
}),
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.message || "Login failed");
}
const data = await response.json();
accessToken = data.accessToken;
return data;
};
export const logoutUser = async (): Promise<void> => {
await fetch(`${API_URL}/auth/logout`, {
method: "POST",
credentials: "include",
headers: {
"Content-Type": "application/json",
...(accessToken && { Authorization: `Bearer ${accessToken}` }),
},
});
accessToken = null;
};
export const refreshToken = async (): Promise<string | null> => {
const response = await fetch(`${API_URL}/auth/refresh`, {
method: "POST",
credentials: "include",
headers: {
"Content-Type": "application/json",
},
});
if (!response.ok) {
accessToken = null;
return null;
}
const data = await response.json();
accessToken = data.accessToken;
return data.accessToken;
};
export const getCurrentUser = async (): Promise<User | null> => {
if (!accessToken) {
// Try to refresh token first
const refreshed = await refreshToken();
if (!refreshed) {
return null;
}
}
const response = await fetch(`${API_URL}/users/me`, {
method: "GET",
credentials: "include",
headers: {
Authorization: `Bearer ${accessToken}`,
},
});
if (!response.ok) {
if (response.status === 401) {
// Try refresh and retry
const refreshed = await refreshToken();
if (!refreshed) {
return null;
}
const retryResponse = await fetch(`${API_URL}/users/me`, {
method: "GET",
credentials: "include",
headers: {
Authorization: `Bearer ${accessToken}`,
},
});
if (!retryResponse.ok) {
return null;
}
return retryResponse.json();
}
return null;
}
return response.json();
};
// Helper for authenticated API calls
export const authFetch = async (url: string, options: RequestInit = {}): Promise<Response> => {
const headers = {
...options.headers,
...(accessToken && { Authorization: `Bearer ${accessToken}` }),
};
let response = await fetch(url, { ...options, credentials: "include", headers });
if (response.status === 401) {
const refreshed = await refreshToken();
if (refreshed) {
headers.Authorization = `Bearer ${accessToken}`;
response = await fetch(url, { ...options, credentials: "include", headers });
}
}
return response;
};

File diff suppressed because it is too large Load diff

View file

@ -1,85 +0,0 @@
import { db } from "./db";
import { hashSecret } from "./crypto";
const ADMIN_EMAIL = process.env.ADMIN_EMAIL || "admin@rede5.com.br";
const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || "admin123";
const DEFAULT_TENANT_NAME = "Rede5";
export const seed = async () => {
console.log("[Seed] Checking if seed is needed...");
// Check if default tenant exists
const tenantResult = await db.query<{ id: string }>(
"SELECT id FROM tenants WHERE name = $1",
[DEFAULT_TENANT_NAME]
);
let tenantId: string;
if (tenantResult.rowCount === 0) {
console.log("[Seed] Creating default tenant...");
const newTenant = await db.query<{ id: string }>(
"INSERT INTO tenants (name) VALUES ($1) RETURNING id",
[DEFAULT_TENANT_NAME]
);
tenantId = newTenant.rows[0].id;
} else {
tenantId = tenantResult.rows[0].id;
console.log("[Seed] Default tenant already exists.");
}
// Check if admin user exists
const userResult = await db.query<{ id: string }>(
"SELECT id FROM users WHERE identifier = $1",
[ADMIN_EMAIL]
);
let userId: string;
if (userResult.rowCount === 0) {
console.log("[Seed] Creating admin user...");
const passwordHash = await hashSecret(ADMIN_PASSWORD);
const newUser = await db.query<{ id: string }>(
"INSERT INTO users (identifier, password_hash, status) VALUES ($1, $2, 'active') RETURNING id",
[ADMIN_EMAIL, passwordHash]
);
userId = newUser.rows[0].id;
} else {
userId = userResult.rows[0].id;
console.log("[Seed] Admin user already exists.");
}
// Check if admin role exists
const roleResult = await db.query<{ id: string }>(
"SELECT id FROM roles WHERE name = $1",
["admin"]
);
let roleId: string;
if (roleResult.rowCount === 0) {
console.log("[Seed] Creating admin role...");
const newRole = await db.query<{ id: string }>(
"INSERT INTO roles (name, description) VALUES ('admin', 'Administrator with full access') RETURNING id",
[]
);
roleId = newRole.rows[0].id;
} else {
roleId = roleResult.rows[0].id;
}
// Link user to tenant
await db.query(
"INSERT INTO user_tenants (user_id, tenant_id) VALUES ($1, $2) ON CONFLICT DO NOTHING",
[userId, tenantId]
);
// Assign admin role to user in tenant
await db.query(
"INSERT INTO user_roles (user_id, tenant_id, role_id) VALUES ($1, $2, $3) ON CONFLICT DO NOTHING",
[userId, tenantId, roleId]
);
console.log("[Seed] Seed completed successfully!");
console.log(`[Seed] Admin: ${ADMIN_EMAIL} | Tenant: ${DEFAULT_TENANT_NAME}`);
};

View file

@ -1,94 +0,0 @@
import { FastifyInstance } from "fastify";
import { RoleService } from "./role.service";
import { authGuard } from "../../core/auth.guard";
import { TokenService } from "../../core/token.service";
export const registerRoleRoutes = (
app: FastifyInstance,
roleService: RoleService,
tokenService: TokenService
) => {
// List all roles
app.get(
"/roles",
{ preHandler: authGuard(tokenService) },
async () => {
return roleService.listRoles();
}
);
// Get role by ID
app.get(
"/roles/:id",
{ preHandler: authGuard(tokenService) },
async (request) => {
const { id } = request.params as { id: string };
return roleService.findById(id);
}
);
// Create role
app.post(
"/roles",
{ preHandler: authGuard(tokenService) },
async (request, reply) => {
const { name, description } = request.body as { name: string; description?: string };
if (!name) {
reply.code(400).send({ message: "Name is required" });
return;
}
const role = await roleService.createRole(name, description);
reply.code(201).send(role);
}
);
// Update role
app.put(
"/roles/:id",
{ preHandler: authGuard(tokenService) },
async (request) => {
const { id } = request.params as { id: string };
const { name, description } = request.body as { name: string; description?: string };
return roleService.updateRole(id, name, description);
}
);
// Delete role
app.delete(
"/roles/:id",
{ preHandler: authGuard(tokenService) },
async (request, reply) => {
const { id } = request.params as { id: string };
await roleService.deleteRole(id);
reply.code(204).send();
}
);
// Assign role to user
app.post(
"/roles/:roleId/users/:userId",
{ preHandler: authGuard(tokenService) },
async (request, reply) => {
const { roleId, userId } = request.params as { roleId: string; userId: string };
const { tenantId } = request.body as { tenantId: string };
if (!tenantId) {
reply.code(400).send({ message: "tenantId is required" });
return;
}
await roleService.assignRoleToUser(userId, tenantId, roleId);
reply.code(201).send({ success: true });
}
);
// Remove role from user
app.delete(
"/roles/:roleId/users/:userId",
{ preHandler: authGuard(tokenService) },
async (request, reply) => {
const { roleId, userId } = request.params as { roleId: string; userId: string };
const { tenantId } = request.body as { tenantId: string };
await roleService.removeRoleFromUser(userId, tenantId, roleId);
reply.code(204).send();
}
);
};

View file

@ -1,66 +0,0 @@
import { FastifyInstance } from "fastify";
import { TenantService } from "./tenant.service";
import { authGuard } from "../../core/auth.guard";
import { TokenService } from "../../core/token.service";
export const registerTenantRoutes = (
app: FastifyInstance,
tenantService: TenantService,
tokenService: TokenService
) => {
// List all tenants
app.get(
"/tenants",
{ preHandler: authGuard(tokenService) },
async () => {
return tenantService.listTenants();
}
);
// Get tenant by ID
app.get(
"/tenants/:id",
{ preHandler: authGuard(tokenService) },
async (request) => {
const { id } = request.params as { id: string };
return tenantService.findById(id);
}
);
// Create tenant
app.post(
"/tenants",
{ preHandler: authGuard(tokenService) },
async (request, reply) => {
const { name } = request.body as { name: string };
if (!name) {
reply.code(400).send({ message: "Name is required" });
return;
}
const tenant = await tenantService.createTenant(name);
reply.code(201).send(tenant);
}
);
// Update tenant
app.put(
"/tenants/:id",
{ preHandler: authGuard(tokenService) },
async (request) => {
const { id } = request.params as { id: string };
const { name } = request.body as { name: string };
return tenantService.updateTenant(id, name);
}
);
// Delete tenant
app.delete(
"/tenants/:id",
{ preHandler: authGuard(tokenService) },
async (request, reply) => {
const { id } = request.params as { id: string };
await tenantService.deleteTenant(id);
reply.code(204).send();
}
);
};

View file

@ -1,61 +0,0 @@
import { db } from "../../lib/db";
export interface TenantEntity {
id: string;
name: string;
createdAt: Date;
}
export class TenantService {
async createTenant(name: string): Promise<TenantEntity> {
const result = await db.query<TenantEntity>(
'INSERT INTO tenants (name) VALUES ($1) RETURNING id, name, created_at as "createdAt"',
[name]
);
return result.rows[0];
}
async findById(id: string): Promise<TenantEntity> {
const result = await db.query<TenantEntity>(
'SELECT id, name, created_at as "createdAt" FROM tenants WHERE id = $1',
[id]
);
if (result.rowCount === 0) {
throw new Error("Tenant not found");
}
return result.rows[0];
}
async findByName(name: string): Promise<TenantEntity | null> {
const result = await db.query<TenantEntity>(
'SELECT id, name, created_at as "createdAt" FROM tenants WHERE name = $1',
[name]
);
return result.rows[0] || null;
}
async listTenants(): Promise<TenantEntity[]> {
const result = await db.query<TenantEntity>(
'SELECT id, name, created_at as "createdAt" FROM tenants ORDER BY created_at DESC'
);
return result.rows;
}
async updateTenant(id: string, name: string): Promise<TenantEntity> {
const result = await db.query<TenantEntity>(
'UPDATE tenants SET name = $2 WHERE id = $1 RETURNING id, name, created_at as "createdAt"',
[id, name]
);
if (result.rowCount === 0) {
throw new Error("Tenant not found");
}
return result.rows[0];
}
async deleteTenant(id: string): Promise<void> {
const result = await db.query("DELETE FROM tenants WHERE id = $1", [id]);
if (result.rowCount === 0) {
throw new Error("Tenant not found");
}
}
}

View file

@ -1,162 +0,0 @@
package handlers
import (
"encoding/json"
"log/slog"
"net/http"
"time"
"github.com/go-chi/chi/v5"
"github.com/jackc/pgx/v5/pgxpool"
"platform-projects-core/internal/api/transport"
"platform-projects-core/internal/application/tasks"
"platform-projects-core/internal/infrastructure/repositories"
)
type TasksHandler struct {
logger *slog.Logger
usecases struct {
create tasks.CreateTaskUseCase
update tasks.UpdateTaskUseCase
archive tasks.ArchiveTaskUseCase
list tasks.ListTasksUseCase
}
}
func NewTasksHandler(logger *slog.Logger, pool *pgxpool.Pool) http.Handler {
repo := repositories.NewTasksRepository(pool)
h := &TasksHandler{logger: logger}
h.usecases.create = tasks.CreateTaskUseCase{Repo: repo}
h.usecases.update = tasks.UpdateTaskUseCase{Repo: repo}
h.usecases.archive = tasks.ArchiveTaskUseCase{Repo: repo}
h.usecases.list = tasks.ListTasksUseCase{Repo: repo}
r := chi.NewRouter()
r.Post("/", h.create)
r.Get("/", h.list)
r.Put("/{taskId}", h.update)
r.Delete("/{taskId}", h.archive)
return r
}
type createTaskRequest struct {
ProjectID string `json:"project_id"`
Title string `json:"title"`
Description string `json:"description"`
DueDate *string `json:"due_date"`
}
func (h *TasksHandler) create(w http.ResponseWriter, r *http.Request) {
tenantID, ok := transport.TenantFromContext(r.Context())
if !ok {
transport.WriteError(w, http.StatusUnauthorized, "unauthorized", "tenant missing")
return
}
var req createTaskRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
transport.WriteError(w, http.StatusBadRequest, "invalid_payload", "invalid JSON payload")
return
}
dueDate, err := parseDueDate(req.DueDate)
if err != nil {
transport.WriteError(w, http.StatusBadRequest, "invalid_payload", "invalid due_date")
return
}
task, err := h.usecases.create.Execute(r.Context(), tasks.CreateTaskInput{
TenantID: tenantID,
ProjectID: req.ProjectID,
Title: req.Title,
Description: req.Description,
DueDate: dueDate,
})
if err != nil {
writeDomainError(w, err)
return
}
transport.WriteJSON(w, http.StatusCreated, transport.Envelope{"task": task})
}
type updateTaskRequest struct {
Title string `json:"title"`
Description string `json:"description"`
Status string `json:"status"`
DueDate *string `json:"due_date"`
}
func (h *TasksHandler) update(w http.ResponseWriter, r *http.Request) {
tenantID, ok := transport.TenantFromContext(r.Context())
if !ok {
transport.WriteError(w, http.StatusUnauthorized, "unauthorized", "tenant missing")
return
}
taskID := chi.URLParam(r, "taskId")
var req updateTaskRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
transport.WriteError(w, http.StatusBadRequest, "invalid_payload", "invalid JSON payload")
return
}
dueDate, err := parseDueDate(req.DueDate)
if err != nil {
transport.WriteError(w, http.StatusBadRequest, "invalid_payload", "invalid due_date")
return
}
task, err := h.usecases.update.Execute(r.Context(), tasks.UpdateTaskInput{
TenantID: tenantID,
TaskID: taskID,
Title: req.Title,
Description: req.Description,
Status: req.Status,
DueDate: dueDate,
})
if err != nil {
writeDomainError(w, err)
return
}
transport.WriteJSON(w, http.StatusOK, transport.Envelope{"task": task})
}
func (h *TasksHandler) archive(w http.ResponseWriter, r *http.Request) {
tenantID, ok := transport.TenantFromContext(r.Context())
if !ok {
transport.WriteError(w, http.StatusUnauthorized, "unauthorized", "tenant missing")
return
}
taskID := chi.URLParam(r, "taskId")
if err := h.usecases.archive.Execute(r.Context(), tasks.ArchiveTaskInput{
TenantID: tenantID,
TaskID: taskID,
}); err != nil {
writeDomainError(w, err)
return
}
transport.WriteJSON(w, http.StatusOK, transport.Envelope{"status": "archived"})
}
func (h *TasksHandler) list(w http.ResponseWriter, r *http.Request) {
tenantID, ok := transport.TenantFromContext(r.Context())
if !ok {
transport.WriteError(w, http.StatusUnauthorized, "unauthorized", "tenant missing")
return
}
projectID := r.URL.Query().Get("project_id")
tasksList, err := h.usecases.list.Execute(r.Context(), tasks.ListTasksInput{
TenantID: tenantID,
ProjectID: projectID,
})
if err != nil {
writeDomainError(w, err)
return
}
transport.WriteJSON(w, http.StatusOK, transport.Envelope{"tasks": tasksList})
}
func parseDueDate(value *string) (*time.Time, error) {
if value == nil {
return nil, nil
}
parsed, err := time.Parse(time.RFC3339, *value)
if err != nil {
return nil, err
}
return &parsed, nil
}

View file

@ -1,16 +0,0 @@
package tasks
import "context"
type ArchiveTaskUseCase struct {
Repo TaskRepository
}
type ArchiveTaskInput struct {
TenantID string
TaskID string
}
func (uc ArchiveTaskUseCase) Execute(ctx context.Context, input ArchiveTaskInput) error {
return uc.Repo.Archive(ctx, input.TenantID, input.TaskID)
}

View file

@ -1,36 +0,0 @@
package tasks
import (
"context"
"time"
"platform-projects-core/internal/domain/projects"
)
type TaskRepository interface {
Create(ctx context.Context, task projects.Task) (projects.Task, error)
Update(ctx context.Context, task projects.Task) (projects.Task, error)
Archive(ctx context.Context, tenantID, taskID string) error
List(ctx context.Context, tenantID, projectID string) ([]projects.Task, error)
Find(ctx context.Context, tenantID, taskID string) (projects.Task, error)
}
type CreateTaskUseCase struct {
Repo TaskRepository
}
type CreateTaskInput struct {
TenantID string
ProjectID string
Title string
Description string
DueDate *time.Time
}
func (uc CreateTaskUseCase) Execute(ctx context.Context, input CreateTaskInput) (projects.Task, error) {
task, err := projects.NewTask(input.TenantID, input.ProjectID, input.Title, input.Description, input.DueDate)
if err != nil {
return projects.Task{}, err
}
return uc.Repo.Create(ctx, task)
}

View file

@ -1,20 +0,0 @@
package tasks
import (
"context"
"platform-projects-core/internal/domain/projects"
)
type ListTasksUseCase struct {
Repo TaskRepository
}
type ListTasksInput struct {
TenantID string
ProjectID string
}
func (uc ListTasksUseCase) Execute(ctx context.Context, input ListTasksInput) ([]projects.Task, error) {
return uc.Repo.List(ctx, input.TenantID, input.ProjectID)
}

View file

@ -1,37 +0,0 @@
package tasks
import (
"context"
"time"
"platform-projects-core/internal/domain/projects"
)
type UpdateTaskUseCase struct {
Repo TaskRepository
}
type UpdateTaskInput struct {
TenantID string
TaskID string
Title string
Description string
Status string
DueDate *time.Time
}
func (uc UpdateTaskUseCase) Execute(ctx context.Context, input UpdateTaskInput) (projects.Task, error) {
existing, err := uc.Repo.Find(ctx, input.TenantID, input.TaskID)
if err != nil {
return projects.Task{}, err
}
status, err := projects.ParseTaskStatus(input.Status)
if err != nil {
return projects.Task{}, err
}
updated, err := existing.WithUpdates(input.Title, input.Description, status, input.DueDate)
if err != nil {
return projects.Task{}, err
}
return uc.Repo.Update(ctx, updated)
}

View file

@ -1 +0,0 @@
DROP TABLE IF EXISTS project_tasks;

View file

@ -1,14 +0,0 @@
CREATE TABLE IF NOT EXISTS project_tasks (
id UUID PRIMARY KEY,
tenant_id UUID NOT NULL,
project_id UUID NOT NULL REFERENCES projects(id),
title TEXT NOT NULL,
description TEXT NOT NULL DEFAULT '',
status TEXT NOT NULL,
due_date TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL,
updated_at TIMESTAMPTZ NOT NULL
);
CREATE INDEX IF NOT EXISTS project_tasks_tenant_idx ON project_tasks (tenant_id);
CREATE INDEX IF NOT EXISTS project_tasks_project_idx ON project_tasks (project_id);

View file

@ -1,26 +0,0 @@
-- name: CreateTask :exec
INSERT INTO project_tasks (id, tenant_id, project_id, title, description, status, due_date, created_at, updated_at)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9);
-- name: UpdateTask :exec
UPDATE project_tasks
SET title = $1,
description = $2,
status = $3,
due_date = $4,
updated_at = $5
WHERE tenant_id = $6
AND id = $7;
-- name: ArchiveTask :exec
UPDATE project_tasks
SET status = 'archived',
updated_at = $1
WHERE tenant_id = $2
AND id = $3;
-- name: ListTasks :many
SELECT id, tenant_id, project_id, title, description, status, due_date, created_at, updated_at
FROM project_tasks
WHERE tenant_id = $1
ORDER BY created_at DESC;

View file

@ -1,51 +0,0 @@
package projects
import (
"strings"
"time"
"platform-projects-core/internal/domain/common"
)
type Task struct {
ID string
TenantID string
ProjectID string
Title string
Description string
Status TaskStatus
DueDate *time.Time
CreatedAt time.Time
UpdatedAt time.Time
}
func NewTask(tenantID, projectID, title, description string, dueDate *time.Time) (Task, error) {
title = strings.TrimSpace(title)
if tenantID == "" || projectID == "" || title == "" {
return Task{}, common.ErrInvalidInput
}
return Task{
ID: common.NewID(),
TenantID: tenantID,
ProjectID: projectID,
Title: title,
Description: strings.TrimSpace(description),
Status: TaskStatusOpen,
DueDate: dueDate,
CreatedAt: time.Now().UTC(),
UpdatedAt: time.Now().UTC(),
}, nil
}
func (t Task) WithUpdates(title, description string, status TaskStatus, dueDate *time.Time) (Task, error) {
title = strings.TrimSpace(title)
if title == "" {
return Task{}, common.ErrInvalidInput
}
t.Title = title
t.Description = strings.TrimSpace(description)
t.Status = status
t.DueDate = dueDate
t.UpdatedAt = time.Now().UTC()
return t, nil
}

View file

@ -1,31 +0,0 @@
package projects
import (
"strings"
"platform-projects-core/internal/domain/common"
)
type TaskStatus string
const (
TaskStatusOpen TaskStatus = "open"
TaskStatusInProgress TaskStatus = "in_progress"
TaskStatusDone TaskStatus = "done"
TaskStatusArchived TaskStatus = "archived"
)
func ParseTaskStatus(value string) (TaskStatus, error) {
switch strings.TrimSpace(value) {
case string(TaskStatusOpen):
return TaskStatusOpen, nil
case string(TaskStatusInProgress):
return TaskStatusInProgress, nil
case string(TaskStatusDone):
return TaskStatusDone, nil
case string(TaskStatusArchived):
return TaskStatusArchived, nil
default:
return "", common.ErrInvalidInput
}
}

View file

@ -1,90 +0,0 @@
package repositories
import (
"context"
"time"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
"platform-projects-core/internal/domain/common"
"platform-projects-core/internal/domain/projects"
)
type TasksRepository struct {
pool *pgxpool.Pool
}
func NewTasksRepository(pool *pgxpool.Pool) *TasksRepository {
return &TasksRepository{pool: pool}
}
func (r *TasksRepository) Create(ctx context.Context, task projects.Task) (projects.Task, error) {
query := `INSERT INTO project_tasks (id, tenant_id, project_id, title, description, status, due_date, created_at, updated_at)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9)`
_, err := r.pool.Exec(ctx, query, task.ID, task.TenantID, task.ProjectID, task.Title, task.Description, task.Status, task.DueDate, task.CreatedAt, task.UpdatedAt)
if err != nil {
return projects.Task{}, err
}
return task, nil
}
func (r *TasksRepository) Update(ctx context.Context, task projects.Task) (projects.Task, error) {
query := `UPDATE project_tasks SET title=$1, description=$2, status=$3, due_date=$4, updated_at=$5 WHERE tenant_id=$6 AND id=$7`
cmd, err := r.pool.Exec(ctx, query, task.Title, task.Description, task.Status, task.DueDate, time.Now().UTC(), task.TenantID, task.ID)
if err != nil {
return projects.Task{}, err
}
if cmd.RowsAffected() == 0 {
return projects.Task{}, common.ErrNotFound
}
return task, nil
}
func (r *TasksRepository) Archive(ctx context.Context, tenantID, taskID string) error {
query := `UPDATE project_tasks SET status='archived', updated_at=$1 WHERE tenant_id=$2 AND id=$3`
cmd, err := r.pool.Exec(ctx, query, time.Now().UTC(), tenantID, taskID)
if err != nil {
return err
}
if cmd.RowsAffected() == 0 {
return common.ErrNotFound
}
return nil
}
func (r *TasksRepository) List(ctx context.Context, tenantID, projectID string) ([]projects.Task, error) {
query := `SELECT id, tenant_id, project_id, title, description, status, due_date, created_at, updated_at FROM project_tasks WHERE tenant_id=$1`
args := []any{tenantID}
if projectID != "" {
query += " AND project_id=$2"
args = append(args, projectID)
}
query += " ORDER BY created_at DESC"
rows, err := r.pool.Query(ctx, query, args...)
if err != nil {
return nil, err
}
defer rows.Close()
var results []projects.Task
for rows.Next() {
var t projects.Task
if err := rows.Scan(&t.ID, &t.TenantID, &t.ProjectID, &t.Title, &t.Description, &t.Status, &t.DueDate, &t.CreatedAt, &t.UpdatedAt); err != nil {
return nil, err
}
results = append(results, t)
}
return results, rows.Err()
}
func (r *TasksRepository) Find(ctx context.Context, tenantID, taskID string) (projects.Task, error) {
query := `SELECT id, tenant_id, project_id, title, description, status, due_date, created_at, updated_at FROM project_tasks WHERE tenant_id=$1 AND id=$2`
var t projects.Task
if err := r.pool.QueryRow(ctx, query, tenantID, taskID).Scan(&t.ID, &t.TenantID, &t.ProjectID, &t.Title, &t.Description, &t.Status, &t.DueDate, &t.CreatedAt, &t.UpdatedAt); err != nil {
if err == pgx.ErrNoRows {
return projects.Task{}, common.ErrNotFound
}
return projects.Task{}, err
}
return t, nil
}