chore: align dev branch with main isolation preserving new files
This commit is contained in:
parent
16c1eb0efc
commit
c901580a49
18 changed files with 0 additions and 4654 deletions
|
|
@ -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=
|
||||
|
|
@ -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;
|
||||
};
|
||||
3710
identity-gateway/package-lock.json
generated
3710
identity-gateway/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -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}`);
|
||||
};
|
||||
|
|
@ -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();
|
||||
}
|
||||
);
|
||||
};
|
||||
|
|
@ -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();
|
||||
}
|
||||
);
|
||||
};
|
||||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
DROP TABLE IF EXISTS project_tasks;
|
||||
|
|
@ -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);
|
||||
|
|
@ -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;
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
Loading…
Reference in a new issue