EduLascala 4 недель назад
Родитель
Сommit
61fba5e31a

+ 6 - 1
.env.example

@@ -1,7 +1,12 @@
 APP_PORT=8080
-DB_FILE=test.db
 JWT_SECRET=Aer8woa9zeec2gai4ahQuah3Ahbee5eiSefae8pheepahnootuShoo0oKahf
 
+#================= DATABASE (PostgreSQL) =================
+DB_HOST=
+DB_PORT=
+DB_NAME=
+DB_USER=
+DB_PASSWORD=
 
 #================= BUILD =================
 BUILD_APP="php-api"

+ 10 - 1
README.md

@@ -1,3 +1,12 @@
+1. run ```./bin/setup``` 
+
+(cria banco de dados e aplica migrations; 
+NOTA: company não está sendo seedada no setup, deve ser criado um usuario junto com a company com a rota /companyWithUser/create)
+
+2. run ```composer install (instala dependencias)```
+
+3. run ```X_LISTEN=127.0.0.1:8000 php public/index.php (inicia servidor)```
+
 ```
 fpm -s dir -t deb \
   -n php-api -v 1.0.0 \
@@ -13,4 +22,4 @@ fpm -s dir -t deb \
   --depends "php8.2-cli | php-cli (>= 8.2)" \
   opt/php-api etc/php-api var/log/php-api
 Created package {:path=>"php-api_1.0.0_all.deb"}
-```
+```

+ 46 - 0
TODO.md

@@ -0,0 +1,46 @@
+# TODO LIST
+
+### - Criar uma instancia global do banco de dados na index.php ```Feito```
+---
+### - UserGetController ```Feito```
+---
+### - UserDeleteController ```Feito```
+---
+### - RegisterController ```Feito```
+---
+### - LoginController ```Feito```
+---
+### - UserModel ```Feito```
+---
+### - JwtAuthMiddleware ```Feito```
+---
+### - Migration and setup ```Feito```
+---
+### - CompanyWithUserController (Já está criando a company, user e wallet da company juntos e salvando address, privatekey, publickey direto na tabela da wallet) ```Feito```
+---
+### - CompanyModel ```Feito```
+---
+### - Add Respect Validation to all controllers
+---
+### - POST CHANGE EMAIL ```Feito```
+### - POST CHANGE PASSWORD ```Feito```
+---
+### - POST CPR CREATE
+### - POST CPR DELETE
+### - POST CPR UPDATE
+### - GET CPR
+---
+### - POST COMMODITY CREATE ```Feito```
+### - POST COMMODITY DELETE ```Feito```
+### - POST COMMODITY UPDATE ```Feito```
+### - GET COMMODITYS ```Feito```
+---
+### - POST NEW ORDER BUY
+### - POST NEW ORDER SELL
+### - POST DELETE ORDER BUY
+### - POST DELETE ORDER SELL
+### - GET ORDER BUY
+### - GET ORDER SELL
+### - GET with grafico
+---
+### - GET WALLET


+ 80 - 42
bin/setup

@@ -1,44 +1,82 @@
 #!/bin/bash
 
-# Nome do arquivo do banco de dados
-DB_FILE="test.db"
-
-# Executa comandos SQL no SQLite
-sqlite3 "$DB_FILE" <<EOF
--- Cria tabela 'user' se não existir, com coluna 'user_password'
-CREATE TABLE IF NOT EXISTS user (
-    user_id INTEGER PRIMARY KEY AUTOINCREMENT,
-    user_name TEXT NOT NULL,
-    user_flag TEXT NOT NULL,
-    user_password TEXT NOT NULL  -- Nova coluna para senha hasheada
-);
-
--- Cria tabela 'api_key' se não existir
-CREATE TABLE IF NOT EXISTS api_key (
-    api_key_id INTEGER PRIMARY KEY AUTOINCREMENT,
-    user_id INTEGER NOT NULL,
-    api_key_user TEXT NOT NULL,
-    api_key_secret TEXT NOT NULL,
-    FOREIGN KEY (user_id) REFERENCES user(user_id)
-);
-
--- Insere usuário de exemplo ('admin') com senha hasheada se não existir
--- Hash de 'pass' (gere com user_password_hash em PHP e substitua)
-INSERT OR IGNORE INTO user (user_name, user_flag, user_password) VALUES ('admin', 'a', '\$2y\$10\$K.0XhB3kXjZfZfZfZfZfZfZfZfZfZfZfZfZfZfZfZfZfZfZfZfZ');
-
--- Insere chave API para o usuário 'admin' se não existir
-INSERT OR IGNORE INTO api_key (user_id, api_key_user, api_key_secret)
-SELECT user_id, 'myapikey', 'myapisecret' FROM user WHERE user_name = 'admin';
-
--- Opcional: Insere mais um usuário de teste com senha hasheada
--- Hash de 'testpass' (substitua pelo real)
-INSERT OR IGNORE INTO user (user_name, user_flag, user_password) VALUES ('testuser', 'a', '\$2y\$10\$AnotherHashHereForTestPass');
-INSERT OR IGNORE INTO api_key (user_id, api_key_user, api_key_secret)
-SELECT user_id, 'testapikey', 'testapisecret' FROM user WHERE user_name = 'testuser';
-
--- Exibe os dados inseridos para verificação (sem mostrar hash real por segurança)
-SELECT user_id, user_name, user_flag FROM user;
-SELECT * FROM api_key;
-EOF
-
-echo "Banco de dados '$DB_FILE' criado e populado com sucesso! Senhas estão hasheadas."
+set -euo pipefail
+
+# Caminhos
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
+MIGRATIONS_FILE="${ROOT_DIR}/migrations/migrations_v1.sql"
+
+# Carrega variáveis do .env se existir
+if [[ -f "${ROOT_DIR}/.env" ]]; then
+  set -a
+  # shellcheck disable=SC1090
+  source "${ROOT_DIR}/.env"
+  set +a
+fi
+
+# Configurações de conexão (PostgreSQL)
+DB_HOST=${DB_HOST:-localhost}
+DB_PORT=${DB_PORT:-5432}
+DB_NAME=${DB_NAME:-tooeasy}
+DB_USER=${DB_USER:-postgres}
+DB_PASSWORD=${DB_PASSWORD:-}
+
+echo "[setup] Host=${DB_HOST} Port=${DB_PORT} DB=${DB_NAME} User=${DB_USER}"
+
+if [[ ! -f "${MIGRATIONS_FILE}" ]]; then
+  echo "[setup] ERRO: Arquivo de migração não encontrado: ${MIGRATIONS_FILE}" >&2
+  exit 1
+fi
+
+# 1) Cria o banco de dados se não existir
+echo "[setup] Verificando se o banco '${DB_NAME}' existe..."
+if ! PGPASSWORD="${DB_PASSWORD}" psql -h "${DB_HOST}" -p "${DB_PORT}" -U "${DB_USER}" -d postgres -tAc "SELECT 1 FROM pg_database WHERE datname='${DB_NAME}'" | grep -q 1; then
+  echo "[setup] Criando banco de dados '${DB_NAME}'..."
+  PGPASSWORD="${DB_PASSWORD}" psql -h "${DB_HOST}" -p "${DB_PORT}" -U "${DB_USER}" -d postgres -c "CREATE DATABASE \"${DB_NAME}\";"
+else
+  echo "[setup] Banco '${DB_NAME}' já existe."
+fi
+
+# 2) Executa migração (schema) somente se ainda não existir
+echo "[setup] Verificando schema..."
+if [[ -z "$(PGPASSWORD="${DB_PASSWORD}" psql -h "${DB_HOST}" -p "${DB_PORT}" -U "${DB_USER}" -d "${DB_NAME}" -tAc "SELECT to_regclass('public.company')")" ]]; then
+  echo "[setup] Aplicando migrações do arquivo: ${MIGRATIONS_FILE}"
+  PGPASSWORD="${DB_PASSWORD}" psql -h "${DB_HOST}" -p "${DB_PORT}" -U "${DB_USER}" -d "${DB_NAME}" -v ON_ERROR_STOP=1 -f "${MIGRATIONS_FILE}"
+else
+  echo "[setup] Schema já existente. Pulando migrações."
+fi
+
+# 3) Seed inicial (idempotente)
+echo "[setup] Inserindo dados iniciais (seed)..."
+PGPASSWORD="${DB_PASSWORD}" psql -h "${DB_HOST}" -p "${DB_PORT}" -U "${DB_USER}" -d "${DB_NAME}" -v ON_ERROR_STOP=1 -c "
+INSERT INTO \"role\" (company_id, role_name, role_permission, role_flag)
+SELECT c.company_id, 'Admin', '{}'::jsonb, 'a'
+FROM \"company\" c
+WHERE c.company_name = 'LumyonTech'
+  AND NOT EXISTS (
+    SELECT 1 FROM \"role\" r WHERE r.role_name = 'Admin' AND r.company_id = c.company_id
+  );
+"
+
+PGPASSWORD="${DB_PASSWORD}" psql -h "${DB_HOST}" -p "${DB_PORT}" -U "${DB_USER}" -d "${DB_NAME}" -v ON_ERROR_STOP=1 -c "
+INSERT INTO \"status\" (status_status)
+SELECT 'OPEN'
+WHERE NOT EXISTS (SELECT 1 FROM \"status\" WHERE status_status = 'OPEN');
+"
+
+PGPASSWORD="${DB_PASSWORD}" psql -h "${DB_HOST}" -p "${DB_PORT}" -U "${DB_USER}" -d "${DB_NAME}" -v ON_ERROR_STOP=1 -c "
+INSERT INTO \"chain\" (chain_name)
+SELECT 'polygon'
+WHERE NOT EXISTS (SELECT 1 FROM \"chain\" WHERE chain_name = 'polygon');
+"
+
+# 4) Resumo
+echo "[setup] Finalizado com sucesso!"
+echo "[setup] Tabelas seed (amostra):"
+PGPASSWORD="${DB_PASSWORD}" psql -h "${DB_HOST}" -p "${DB_PORT}" -U "${DB_USER}" -d "${DB_NAME}" -c "
+TABLE \"company\";
+TABLE \"role\";
+TABLE \"status\";
+TABLE \"chain\";
+" | sed 's/\x1b\[[0-9;]*m//g'

+ 31 - 0
controllers/CommoditiesGetController.php

@@ -0,0 +1,31 @@
+<?php
+
+namespace Controllers;
+
+use Libs\ResponseLib;
+use Models\CommodityModel;
+use Psr\Http\Message\ServerRequestInterface;
+
+class CommoditiesGetController
+{
+    public function __invoke(ServerRequestInterface $request)
+    {
+        $query = $request->getQueryParams();
+        $flag = 'a';
+        if (array_key_exists('flag', $query)) {
+            $v = (string)$query['flag'];
+            if ($v === '' || strtolower($v) === 'all') {
+                $flag = null; // no filter
+            } else {
+                $flag = $v;
+            }
+        }
+
+        $model = new CommodityModel();
+        $rows = $model->getAll($flag);
+        if (!$rows) {
+            return ResponseLib::sendFail('Commodities Not Found', [], 'E_DATABASE')->withStatus(204);
+        }
+        return ResponseLib::sendOk($rows);
+    }
+}

+ 26 - 0
controllers/CommodityCreateController.php

@@ -0,0 +1,26 @@
+<?php
+
+namespace Controllers;
+
+use Libs\ResponseLib;
+use Models\CommodityModel;
+use Psr\Http\Message\ServerRequestInterface;
+
+class CommodityCreateController
+{
+    public function __invoke(ServerRequestInterface $request)
+    {
+        $body = json_decode((string)$request->getBody(), true) ?? [];
+        $name = trim((string)($body['name'] ?? ''));
+        $flag = (string)($body['flag'] ?? 'a');
+
+        if ($name === '') {
+            return ResponseLib::sendFail('Validation failed: name is required', [], 'E_VALIDATE')->withStatus(400);
+        }
+
+        $model = new CommodityModel();
+        $created = $model->create($name, $flag);
+
+        return ResponseLib::sendOk($created, 'S_CREATED');
+    }
+}

+ 24 - 0
controllers/CommodityDeleteController.php

@@ -0,0 +1,24 @@
+<?php
+
+namespace Controllers;
+
+use Libs\ResponseLib;
+use Models\CommodityModel;
+use Psr\Http\Message\ServerRequestInterface;
+
+class CommodityDeleteController
+{
+    public function __invoke(ServerRequestInterface $request)
+    {
+        $body = json_decode((string)$request->getBody(), true) ?? [];
+        $id = isset($body['commodities_id']) ? (int)$body['commodities_id'] : 0;
+        if ($id <= 0) {
+            return ResponseLib::sendFail('Validation failed: invalid commodities_id', [], 'E_VALIDATE')->withStatus(400);
+        }
+        $model = new CommodityModel();
+        $deleted = $model->delete($id);
+        return $deleted
+            ? ResponseLib::sendOk(['deleted' => true], 'S_DELETED')
+            : ResponseLib::sendFail('Commodity Not Found', [], 'E_DATABASE')->withStatus(204);
+    }
+}

+ 35 - 0
controllers/CommodityUpdateController.php

@@ -0,0 +1,35 @@
+<?php
+
+namespace Controllers;
+
+use Libs\ResponseLib;
+use Models\CommodityModel;
+use Psr\Http\Message\ServerRequestInterface;
+
+class CommodityUpdateController
+{
+    public function __invoke(ServerRequestInterface $request)
+    {
+        $body = json_decode((string)$request->getBody(), true) ?? [];
+        $id = isset($body['commodities_id']) ? (int)$body['commodities_id'] : 0;
+        $name = array_key_exists('name', $body) ? trim((string)$body['name']) : null;
+        $flag = array_key_exists('flag', $body) ? (string)$body['flag'] : null;
+
+        if ($id <= 0) {
+            return ResponseLib::sendFail('Validation failed: invalid commodities_id', [], 'E_VALIDATE')->withStatus(400);
+        }
+        if ($name === null && $flag === null) {
+            return ResponseLib::sendFail('Validation failed: nothing to update', [], 'E_VALIDATE')->withStatus(400);
+        }
+        if ($name !== null && $name === '') {
+            return ResponseLib::sendFail('Validation failed: name cannot be empty', [], 'E_VALIDATE')->withStatus(400);
+        }
+
+        $model = new CommodityModel();
+        $updated = $model->update($id, $name, $flag);
+        if (!$updated) {
+            return ResponseLib::sendFail('Commodity Not Found or Not Updated', [], 'E_DATABASE')->withStatus(204);
+        }
+        return ResponseLib::sendOk($updated, 'S_UPDATED');
+    }
+}

+ 125 - 0
controllers/CompanyWithUserController.php

@@ -0,0 +1,125 @@
+<?php
+
+namespace Controllers;
+
+use Libs\BashExecutor;
+use Libs\ResponseLib;
+use Models\CompanyModel;
+use Models\UserModel;
+use Psr\Http\Message\ServerRequestInterface;
+
+class CompanyWithUserController
+{
+    public function __invoke(ServerRequestInterface $request)
+    {
+        $body = json_decode((string)$request->getBody(), true) ?? [];
+
+        $required = [
+            'company_name',
+            'username','email','password','phone','address','city','state','zip','country',
+            'kyc','birthdate','cpf'
+        ];
+        foreach ($required as $field) {
+            if (!isset($body[$field]) || $body[$field] === '') {
+                return ResponseLib::sendFail("Missing field: $field", [], "E_VALIDATE")->withStatus(400);
+            }
+        }
+        if (!filter_var($body['email'], FILTER_VALIDATE_EMAIL)) {
+            return ResponseLib::sendFail("Invalid email format", [], "E_VALIDATE")->withStatus(400);
+        }
+        if (strlen($body['password']) < 8) {
+            return ResponseLib::sendFail("Password must be at least 8 characters", [], "E_VALIDATE")->withStatus(400);
+        }
+
+        try {
+            $pdo = $GLOBALS['pdo'];
+            $pdo->beginTransaction();
+
+            $companyModel = new CompanyModel();
+            $companyId = $companyModel->createCompany($body['company_name'], 'a');
+            $roleId = 1;
+            $chk = $pdo->prepare('SELECT 1 FROM "role" WHERE role_id = :rid');
+            $chk->execute(['rid' => $roleId]);
+            if (!$chk->fetchColumn()) {
+                $pdo->rollBack();
+                return ResponseLib::sendFail('Default role_id 1 not found', [], 'E_DATABASE')->withStatus(500);
+            }
+
+            $userModel = new UserModel();
+            $userPayload = [
+                'username' => $body['username'],
+                'email' => $body['email'],
+                'password' => $body['password'],
+                'phone' => $body['phone'],
+                'address' => $body['address'],
+                'city' => $body['city'],
+                'state' => $body['state'],
+                'zip' => $body['zip'],
+                'country' => $body['country'],
+                'kyc' => (int)$body['kyc'],
+                'birthdate' => (int)$body['birthdate'],
+                'cpf' => $body['cpf'],
+                'company_id' => $companyId,
+                'role_id' => $roleId
+            ];
+            $userData = $userModel->createUser($userPayload);
+            if (!$userData) {
+                $pdo->rollBack();
+                return ResponseLib::sendFail("Email already exists or creation failed", [], "E_VALIDATE")->withStatus(400);
+            }
+
+            $bin = dirname(__DIR__) . '/bin/easycli';
+            $result = BashExecutor::run($bin . ' polygon create-new-address');
+            if (($result['exitCode'] ?? 1) !== 0) {
+                $pdo->rollBack();
+                return ResponseLib::sendFail("Wallet generation failed", ['error' => $result['error'] ?? ''], "E_INTERNAL")->withStatus(500);
+            }
+            $output = trim((string)($result['output'] ?? ''));
+            $parsed = [];
+            foreach (preg_split('/\r?\n/', $output) as $line) {
+                $line = trim($line);
+                if ($line === '' || strpos($line, '=') === false) { continue; }
+                [$k, $v] = explode('=', $line, 2);
+                $parsed[trim($k)] = trim($v);
+            }
+            if (!isset($parsed['privateKey'], $parsed['publicKey'], $parsed['address'])) {
+                $pdo->rollBack();
+                return ResponseLib::sendFail("Wallet parsing failed", ['raw' => $output], "E_INTERNAL")->withStatus(500);
+            }
+
+            $stmt = $pdo->prepare('SELECT chain_id FROM "chain" WHERE chain_name = :name');
+            $stmt->execute(['name' => 'primalchain']);
+            $chainId = $stmt->fetchColumn();
+            if (!$chainId) {
+                $pdo->rollBack();
+                return ResponseLib::sendFail("Chain not found", [], "E_DATABASE")->withStatus(500);
+            }
+
+            $stmt = $pdo->prepare('INSERT INTO "wallet" (company_id, wallet_public_key, wallet_address, wallet_private_key, wallet_flag, chain_id) VALUES (:company_id, :public_key, :address, :private_key, :flag, :chain_id) RETURNING wallet_id');
+            $stmt->execute([
+                'company_id' => $companyId,
+                'public_key' => $parsed['publicKey'],
+                'address' => $parsed['address'],
+                'private_key' => $parsed['privateKey'],
+                'flag' => 'a',
+                'chain_id' => (int)$chainId
+            ]);
+            $walletId = (int)$stmt->fetchColumn();
+
+            $pdo->commit();
+
+            return ResponseLib::sendOk([
+                'company_id' => $companyId,
+                'role_id' => $roleId,
+                'user' => $userData,
+                'wallet_id' => $walletId,
+                'wallet_address' => $parsed['address']
+            ], 'S_CREATED');
+
+        } catch (\Throwable $e) {
+            if (isset($pdo) && $pdo->inTransaction()) { $pdo->rollBack(); }
+            return ResponseLib::sendFail($e->getMessage(), [], 'E_DATABASE')->withStatus(500);
+        }
+    }
+}
+

+ 6 - 6
controllers/LoginController.php

@@ -12,15 +12,15 @@ class LoginController
     public function __invoke(ServerRequestInterface $request)
     {
         $body = json_decode((string) $request->getBody(), true);
-        $username = $body['username'] ?? '';
+        $email = $body['email'] ?? '';
         $password = $body['password'] ?? '';
 
-        if (empty($username) || empty($password)) {
-            return ResponseLib::sendFail("Missing username or password", [], "E_VALIDATE")->withStatus(401);
+        if (empty($email) || empty($password)) {
+            return ResponseLib::sendFail("Missing email or password", [], "E_VALIDATE")->withStatus(401);
         }
 
         $userModel = new UserModel();
-        $user = $userModel->validateLogin($username, $password);
+        $user = $userModel->validateLogin($email, $password);
 
         if (!$user) {
             return ResponseLib::sendFail("Invalid credentials", [], "E_VALIDATE")->withStatus(401);
@@ -29,12 +29,12 @@ class LoginController
         // Gera JWT
         $payload = [
             'sub' => $user['user_id'],
-            'username' => $user['user_name'],
+            'email' => $user['user_email'],
             'iat' => time(),
             'exp' => time() + 3600  // 1 hora
         ];
         $jwt = JWT::encode($payload, $_ENV['JWT_SECRET'], 'HS256');
 
-        return ResponseLib::sendOk(['token' => $jwt, 'user_id' => $user['user_id']]);
+        return ResponseLib::sendOk(['token' => $jwt, 'user_id' => $user['user_id'], 'company_id' => $user['company_id']]);
     }
 }   

+ 15 - 8
controllers/RegisterController.php

@@ -11,23 +11,30 @@ class RegisterController
     public function __invoke(ServerRequestInterface $request)
     {
         $body = json_decode((string) $request->getBody(), true);
-        $username = $body['username'] ?? '';
-        $password = $body['password'] ?? '';
+        
+        $required = [
+            'username','email','password','phone','address','city','state','zip','country',
+            'kyc','birthdate','cpf','company_id','role_id'
+        ];
+        foreach ($required as $field) {
+            if (!isset($body[$field]) || $body[$field] === '') {
+                return ResponseLib::sendFail("Missing field: $field", [], "E_VALIDATE")->withStatus(400);
+            }
+        }
 
-        if (empty($username) || empty($password)) {
-            return ResponseLib::sendFail("Missing username or password", [], "E_VALIDATE")->withStatus(400);
+        if (!filter_var($body['email'], FILTER_VALIDATE_EMAIL)) {
+            return ResponseLib::sendFail("Invalid email format", [], "E_VALIDATE")->withStatus(400);
         }
 
-        // Validação básica (ex: comprimento mínimo)
-        if (strlen($password) < 8) {
+        if (strlen($body['password']) < 8) {
             return ResponseLib::sendFail("Password must be at least 8 characters", [], "E_VALIDATE")->withStatus(400);
         }
 
         $userModel = new UserModel();
-        $userData = $userModel->createUser($username, $password);
+        $userData = $userModel->createUser($body);
 
         if (!$userData) {
-            return ResponseLib::sendFail("Username already exists or creation failed", [], "E_VALIDATE")->withStatus(400);
+            return ResponseLib::sendFail("Email already exists or creation failed", [], "E_VALIDATE")->withStatus(400);
         }
 
         return ResponseLib::sendOk($userData, "S_CREATED");

+ 33 - 0
controllers/UserChangeEmailController.php

@@ -0,0 +1,33 @@
+<?php
+
+namespace Controllers;
+
+use Libs\ResponseLib;
+use Models\UserModel;
+use Psr\Http\Message\ServerRequestInterface;
+
+class UserChangeEmailController
+{
+    public function __invoke(ServerRequestInterface $request)
+    {
+        $userId = (int)($request->getAttribute('api_user_id') ?? 0);
+        if ($userId <= 0) {
+            return ResponseLib::sendFail('Unauthorized', [], 'E_VALIDATE')->withStatus(401);
+        }
+
+        $body = json_decode((string)$request->getBody(), true) ?? [];
+        $email = $body['email'] ?? '';
+
+        if (empty($email) || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
+            return ResponseLib::sendFail('Validation failed: invalid email', [], 'E_VALIDATE')->withStatus(400);
+        }
+
+        $model = new UserModel();
+        $ok = $model->updateEmail($userId, $email);
+        if (!$ok) {
+            return ResponseLib::sendFail('Email already in use or update failed', [], 'E_VALIDATE')->withStatus(400);
+        }
+
+        return ResponseLib::sendOk(['user_id' => $userId, 'user_email' => $email], 'S_UPDATED');
+    }
+}

+ 37 - 0
controllers/UserChangePasswordController.php

@@ -0,0 +1,37 @@
+<?php
+
+namespace Controllers;
+
+use Libs\ResponseLib;
+use Models\UserModel;
+use Psr\Http\Message\ServerRequestInterface;
+
+class UserChangePasswordController
+{
+    public function __invoke(ServerRequestInterface $request)
+    {
+        $userId = (int)($request->getAttribute('api_user_id') ?? 0);
+        if ($userId <= 0) {
+            return ResponseLib::sendFail('Unauthorized', [], 'E_VALIDATE')->withStatus(401);
+        }
+
+        $body = json_decode((string)$request->getBody(), true) ?? [];
+        $current = $body['current_password'] ?? '';
+        $new = $body['new_password'] ?? '';
+
+        if ($current === '' || $new === '' || strlen($new) < 8) {
+            return ResponseLib::sendFail('Validation failed: invalid passwords', [], 'E_VALIDATE')->withStatus(400);
+        }
+        if ($current === $new) {
+            return ResponseLib::sendFail('New password must be different from current password', [], 'E_VALIDATE')->withStatus(400);
+        }
+
+        $model = new UserModel();
+        $ok = $model->changePassword($userId, $current, $new);
+        if (!$ok) {
+            return ResponseLib::sendFail('Invalid current password or update failed', [], 'E_VALIDATE')->withStatus(400);
+        }
+
+        return ResponseLib::sendOk(['user_id' => $userId], 'S_UPDATED');
+    }
+}

+ 38 - 0
controllers/UserDeleteController.php

@@ -0,0 +1,38 @@
+<?php
+
+namespace Controllers;
+
+use Libs\ResponseLib;
+use Models\UserModel;
+use Psr\Http\Message\ServerRequestInterface;
+
+class UserDeleteController
+{
+    private UserModel $model;
+
+    public function __construct()
+    {
+        $this->model = new UserModel();
+    }
+
+    public function __invoke(ServerRequestInterface $request)
+    {
+        $body = json_decode((string)$request->getBody(), true) ?? [];
+
+        if (!isset($body['company_id']) || !is_numeric($body['company_id']) || (int)$body['company_id'] <= 0) {
+            return ResponseLib::sendFail("Validation failed: invalid company_id", [], "E_VALIDATE")->withStatus(400);
+        }
+        if (!isset($body['user_id']) || !is_numeric($body['user_id']) || (int)$body['user_id'] <= 0) {
+            return ResponseLib::sendFail("Validation failed: invalid user_id", [], "E_VALIDATE")->withStatus(400);
+        }
+
+        $companyId = (int) $body['company_id'];
+        $userId = (int) $body['user_id'];
+
+        $deleted = $this->model->deleteUserById($userId, $companyId);
+
+        return $deleted
+            ? ResponseLib::sendOk(['deleted' => true])
+            : ResponseLib::sendFail("Failed to Delete User or User Not Found", [], "E_DATABASE")->withStatus(204);
+    }
+}

+ 33 - 0
controllers/UserGetController.php

@@ -0,0 +1,33 @@
+<?php
+
+namespace Controllers;
+
+use Libs\ResponseLib;
+use Models\UserModel;
+use Psr\Http\Message\ServerRequestInterface;
+
+class UserGetController
+{
+    private UserModel $model;
+
+    public function __construct()
+    {
+        $this->model = new UserModel();
+    }
+
+    public function __invoke(ServerRequestInterface $request)
+    {
+        $body = json_decode((string)$request->getBody(), true) ?? [];
+
+        if (!isset($body['company_id']) || !is_numeric($body['company_id']) || (int)$body['company_id'] <= 0) {
+            return ResponseLib::sendFail("Validation failed: invalid company_id", [], "E_VALIDATE")->withStatus(400);
+        }
+
+        $companyId = (int) $body['company_id'];
+        $users = $this->model->getUsersByCompany($companyId);
+
+        return $users
+            ? ResponseLib::sendOk($users)
+            : ResponseLib::sendFail("User Not Found", [], "E_DATABASE")->withStatus(204);
+    }
+}

+ 61 - 0
libs/BashExecutor.php

@@ -0,0 +1,61 @@
+<?php
+
+namespace Libs;
+
+class BashExecutor
+{
+    public static function run(string $command, int $timeoutSeconds = 30): array
+    {
+        $descriptorSpec = [
+            0 => ["pipe", "r"],   // STDIN
+            1 => ["pipe", "w"],   // STDOUT
+            2 => ["pipe", "w"]    // STDERR
+        ];
+
+        $process = proc_open($command, $descriptorSpec, $pipes);
+
+        if (!is_resource($process)) {
+            return [
+                "exitCode" => -1,
+                "output" => "",
+                "error" => "Failed to start process"
+            ];
+        }
+
+        // Timeout handling
+        $start = time();
+        $stdout = "";
+        $stderr = "";
+
+        do {
+            $status = proc_get_status($process);
+
+            $stdout .= stream_get_contents($pipes[1]);
+            $stderr .= stream_get_contents($pipes[2]);
+
+            usleep(100000); // 0.1 segundo
+
+            if ((time() - $start) > $timeoutSeconds) {
+                proc_terminate($process);
+                return [
+                    "exitCode" => -1,
+                    "output" => $stdout,
+                    "error" => "Timeout of {$timeoutSeconds}s reached"
+                ];
+            }
+
+        } while ($status['running']);
+
+        foreach ($pipes as $pipe) {
+            fclose($pipe);
+        }
+
+        $exitCode = proc_close($process);
+
+        return [
+            "exitCode" => $exitCode,
+            "output" => trim($stdout),
+            "error" => trim($stderr)
+        ];
+    }
+}

+ 9 - 11
middlewares/JwtAuthMiddleware.php

@@ -6,7 +6,6 @@ use Firebase\JWT\JWT;
 use Firebase\JWT\Key;
 use Libs\ResponseLib;
 use Psr\Http\Message\ServerRequestInterface;
-use React\Http\Message\Response;
 
 class JwtAuthMiddleware
 {
@@ -30,19 +29,18 @@ class JwtAuthMiddleware
         try {
             $decoded = JWT::decode($token, new Key($this->jwtSecret, 'HS256'));
             $userId = $decoded->sub ?? null;
-            $apiUser = $decoded->username ?? null;
+            $email = $decoded->email ?? null;
 
-            if (empty($userId) || empty($apiUser)) {
+            if (empty($userId) || empty($email)) {
                 return ResponseLib::sendFail("Unauthorized: Invalid JWT claims", [], "E_VALIDATE")->withStatus(401);
             }
 
-            $dbFile = $_ENV['DB_FILE'] ?? 'bridge.db';
-            $dbPath = __DIR__ . '/../' . $dbFile;
-            $pdo = new \PDO("sqlite:" . $dbPath);
-            $pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
-
-            $stmt = $pdo->prepare("SELECT user_id FROM user WHERE user_id = :user_id AND user_name = :user_name AND user_flag = 'a'");
-            $stmt->execute(['user_id' => $userId, 'user_name' => $apiUser]);
+            if (isset($GLOBALS['pdo']) && $GLOBALS['pdo'] instanceof \PDO) {
+                $pdo = $GLOBALS['pdo'];
+            }
+            
+            $stmt = $pdo->prepare('SELECT user_id FROM "user" WHERE user_id = :user_id AND user_email = :email AND user_flag = \'a\'');
+            $stmt->execute(['user_id' => $userId, 'email' => $email]);
             $user = $stmt->fetch(\PDO::FETCH_ASSOC);
 
             if (!$user) {
@@ -50,7 +48,7 @@ class JwtAuthMiddleware
             }
 
             $request = $request
-                ->withAttribute('api_user', $apiUser)
+                ->withAttribute('api_user', $email)
                 ->withAttribute('api_user_id', $userId);
 
             return $next($request);

+ 243 - 16
migrations/migrations_v1.sql

@@ -1,16 +1,243 @@
-PRAGMA foreign_keys=ON;
-
-CREATE TABLE user (
-    user_id INTEGER PRIMARY KEY AUTOINCREMENT,
-    user_name TEXT NOT NULL,
-    user_flag TEXT NOT NULL,
-    user_password TEXT NOT NULL  -- Nova coluna para senha hasheada
-);
-
-CREATE TABLE api_key (
-    api_key_id INTEGER PRIMARY KEY AUTOINCREMENT,
-    user_id INTEGER NOT NULL,
-    api_key_user TEXT NOT NULL,
-    api_key_secret TEXT NOT NULL,
-    FOREIGN KEY (user_id) REFERENCES user(user_id)
-);
+
+CREATE TABLE "chain" (
+    "chain_id"   SERIAL PRIMARY KEY,
+    "chain_name" TEXT NOT NULL
+);
+
+CREATE TABLE "company" (
+    "company_id" SERIAL PRIMARY KEY,
+    "company_name" TEXT NOT NULL,
+    "company_flag" TEXT NOT NULL
+);
+
+CREATE TABLE "role" (
+    "role_id" SERIAL PRIMARY KEY,
+    "company_id" INTEGER NOT NULL,
+    "role_name" TEXT NOT NULL,
+    "role_permission" JSONB NOT NULL,
+    "role_flag" TEXT NOT NULL,
+    CHECK (jsonb_typeof("role_permission") = 'object'),
+    FOREIGN KEY ("company_id") REFERENCES "company" ("company_id")
+);
+
+CREATE TABLE "status" (
+    "status_id" SERIAL PRIMARY KEY,
+    "status_status" TEXT NOT NULL
+);
+
+CREATE TABLE "cpr" (
+    "cpr_id" SERIAL PRIMARY KEY,
+    "cpr_additive" TEXT NOT NULL,
+    "cpr_agents_sender_phone" TEXT NOT NULL,
+    "cpr_agents_sender_cep" TEXT NOT NULL,
+    "cpr_agents_creditor_document_number" TEXT NOT NULL,
+    "cpr_agents_creditor_status" BOOLEAN NOT NULL,
+    "cpr_agents_creditor_person_type" INTEGER NOT NULL,
+    "cpr_agents_endorsement_date" TEXT NOT NULL,
+    "cpr_agents_endorser" BOOLEAN NOT NULL,
+    "cpr_agents_creditor_id" INTEGER NOT NULL,
+    "cpr_agents_wallet_holder" TEXT NOT NULL,
+    "cpr_agents_cnpj_holder" TEXT NOT NULL,
+    "cpr_agents_debtor_cep" TEXT NOT NULL,
+    "cpr_agents_debtor_email" TEXT NOT NULL,
+    "cpr_agents_debtor_name_corporate_name" TEXT NOT NULL,
+    "cpr_agents_debtor_phone_number" TEXT NOT NULL,
+    "cpr_agents_debtor_document_number" TEXT NOT NULL,
+    "cpr_agents_debtor_status" BOOLEAN NOT NULL,
+    "cpr_agents_debtor_person_type" INTEGER NOT NULL,
+    "cpr_agents_debtor_id" INTEGER NOT NULL,
+    "cpr_agents_sender_email" TEXT NOT NULL,
+    "cpr_agents_sender_credit_agency" TEXT NOT NULL,
+    "cpr_agents_sender_credit_bank" TEXT NOT NULL,
+    "cpr_agents_sender_credit_checking_account" TEXT NOT NULL,
+    "cpr_agents_guarantor_phone" TEXT NOT NULL,
+    "cpr_agents_guarantor_email" TEXT NOT NULL,
+    "cpr_agents_guarantor_cep" TEXT NOT NULL,
+    "cpr_agents_guarantor_document_number" TEXT NOT NULL,
+    "cpr_agents_guarantor_status" BOOLEAN NOT NULL,
+    "cpr_agents_guarantor_person_type" INTEGER NOT NULL,
+    "cpr_agents_guarantor_id" INTEGER NOT NULL,
+    "cpr_agents_guarantor_type" INTEGER NOT NULL,
+    "cpr_agents_sender_document_number" TEXT NOT NULL,
+    "cpr_agents_sender_person_type" INTEGER NOT NULL,
+    "cpr_area_total" NUMERIC NOT NULL,
+    "cpr_area_registry" TEXT NOT NULL,
+    "cpr_area_cep" TEXT NOT NULL,
+    "cpr_area_address" TEXT NOT NULL,
+    "cpr_area_property_unitary_fraction" TEXT NOT NULL,
+    "cpr_area_id" INTEGER NOT NULL,
+    "cpr_area_latitude" NUMERIC NOT NULL,
+    "cpr_area_logitude" NUMERIC NOT NULL,
+    "cpr_area_registration" TEXT NOT NULL,
+    "cpr_area_farm_name" TEXT NOT NULL,
+    "cpr_area_product_cpr_necessery_area" NUMERIC NOT NULL,
+    "cpr_area_product_total_productive_area" NUMERIC NOT NULL,
+    "cpr_area_product_class_type_ph" TEXT NOT NULL,
+    "cpr_area_product_culture" TEXT NOT NULL,
+    "cpr_area_product_culture_specificity" TEXT NOT NULL,
+    "cpr_area_product_packaging_method" TEXT NOT NULL,
+    "cpr_area_product_id" INTEGER NOT NULL,
+    "cpr_area_product_assessment_index" TEXT NOT NULL,
+    "cpr_area_product_institution_responsible_index" TEXT NOT NULL,
+    "cpr_area_product_delivery_location_city" TEXT NOT NULL,
+    "cpr_area_product_delivery_location_regional" TEXT NOT NULL,
+    "cpr_area_product_delivery_location_fu" TEXT NOT NULL,
+    "cpr_area_product_volume_price" NUMERIC NOT NULL,
+    "cpr_area_product_production" TEXT NOT NULL,
+    "cpr_area_product_productivity" TEXT NOT NULL,
+    "cpr_area_product_harvest" TEXT NOT NULL,
+    "cpr_area_product_situation" TEXT NOT NULL,
+    "cpr_area_product_status" BOOLEAN NOT NULL,
+    "cpr_area_product_volume_mesuring_unit" TEXT NOT NULL,
+    "cpr_area_product_register_value" TEXT NOT NULL,
+    "cpr_area_product_volume_quantity" TEXT NOT NULL,
+    "cpr_area_owner" TEXT NOT NULL,
+    "cpr_area_legal_reserve" NUMERIC NOT NULL,
+    "cpr_area_status" BOOLEAN NOT NULL,
+    "cpr_contracts_registration_authorized_and_clear_fi_record" BOOLEAN NOT NULL, --autorizo_registro_e_declaro_que_foi_dado_baixa_no_registro_em_quaisquer_outras_if
+    "cpr_contracts_credit_operation_contract_code" TEXT NOT NULL,
+    "cpr_contracts_contract_scr" TEXT NOT NULL,
+    "cpr_contracts_coin_code" TEXT NOT NULL,
+    "cpr_contracts_other_fi_discharge_date" TIMESTAMPTZ NOT NULL,
+    "cpr_contracts_fi_hiring_date" TIMESTAMPTZ NOT NULL,
+    "cpr_contracts_original_register_date" TIMESTAMPTZ NOT NULL,
+    "cpr_contracts_transaction_date" DATE NOT NULL,
+    "cpr_contracts_scr_standardized_identification" TEXT NOT NULL,
+    "cpr_contracts_indexation" TEXT NOT NULL,
+    "cpr_contracts_operation_nature" TEXT NOT NULL,
+    "cpr_contracts_indexing_percentage" NUMERIC NOT NULL,
+    "cpr_contracts_installments_quantity_contracted" INTEGER NOT NULL,
+    "cpr_contracts_extern_reference" TEXT NOT NULL,
+    "cpr_contracts_operation_interest_rate" NUMERIC NOT NULL,
+    "cpr_contracts_asset_type" TEXT NOT NULL,
+    "cpr_contracts_contract_financed_value" NUMERIC NOT NULL,
+    "cpr_contracts_credit_liquid_value" NUMERIC NOT NULL,
+    "cpr_contracts_credit_total_value" NUMERIC NOT NULL,
+    "cpr_emission_delivery_date" DATE NOT NULL,
+    "cpr_emission_id" INTEGER NOT NULL,
+    "cpr_emission_location" TEXT NOT NULL,
+    "cpr_emission_issuer_corporate_name" TEXT NOT NULL,
+    "cpr_emission_cpr_type" TEXT NOT NULL, --fisico, financeiro
+    "cpr_guarantee_status" BOOLEAN NOT NULL,
+    "cpr_guarantee_id" INTEGER NOT NULL,
+    "cpr_guarantee_type" TEXT NOT NULL,
+    "cpr_installment_control_code" TEXT NOT NULL,
+    "cpr_installment_due_date" TEXT NOT NULL,
+    "cpr_installment_register_id" INTEGER NOT NULL,
+    "cpr_installment_number" INTEGER NOT NULL,
+    "cpr_installment_assignment_price" INTEGER NOT NULL,
+    "cpr_installment_status" INTEGER NOT NULL,
+    "cpr_installment_value" NUMERIC NOT NULL,
+    "cpr_installment_main_value" NUMERIC NOT NULL,
+    "cpr_status" TEXT NOT NULL --1 - ACEITO, 2 - LIQUIDADO, 3 - RECUSADO, 4 - CANCELADO
+);
+
+CREATE TABLE "commodities" (
+    "commodities_id" SERIAL PRIMARY KEY,
+    "commodities_name" TEXT NOT NULL,
+    "commodities_flag" TEXT NOT NULL
+);
+
+CREATE TABLE "wallet" (
+    "wallet_id" SERIAL PRIMARY KEY,
+    "company_id" INTEGER NOT NULL,
+    "wallet_public_key" TEXT NOT NULL,
+    "wallet_address" TEXT NOT NULL,
+    "wallet_private_key" TEXT NOT NULL,
+    "wallet_flag" TEXT NOT NULL,
+    "chain_id" INTEGER NOT NULL,
+    FOREIGN KEY ("company_id") REFERENCES "company" ("company_id"),
+    FOREIGN KEY ("chain_id") REFERENCES "chain" ("chain_id")
+);
+
+CREATE TABLE "currency" (
+    "currency_id"   SERIAL PRIMARY KEY,
+    "currency_external_id" TEXT NOT NULL,
+    "currency_name" TEXT NOT NULL,
+    "currency_digits" INTEGER NOT NULL,
+    "chain_id" INTEGER NOT NULL,
+    "currency_flag" TEXT NOT NULL,
+    FOREIGN KEY ("chain_id") REFERENCES "chain" ("chain_id")
+);
+
+CREATE TABLE "token" (
+    "token_id" SERIAL PRIMARY KEY,
+    "token_external_id" TEXT NOT NULL,
+    "token_commodities_amount" INTEGER NOT NULL,
+    "token_flag" TEXT NOT NULL,
+    "token_commodities_value" INTEGER NOT NULL,
+    "wallet_id" INTEGER NOT NULL,
+    "chain_id" INTEGER NOT NULL,
+    "commodities_id" INTEGER NOT NULL,
+    "cpr_id" INTEGER NOT NULL,
+    FOREIGN KEY ("wallet_id") REFERENCES "wallet" ("wallet_id"),
+    FOREIGN KEY ("chain_id") REFERENCES "chain" ("chain_id"),
+    FOREIGN KEY ("commodities_id") REFERENCES "commodities" ("commodities_id"),
+    FOREIGN KEY ("cpr_id") REFERENCES "cpr" ("cpr_id")
+);
+
+CREATE TABLE "user" (
+    "user_id" SERIAL PRIMARY KEY,
+    "user_name" TEXT NOT NULL,
+    "user_email" TEXT NOT NULL UNIQUE,
+    "user_password" TEXT NOT NULL,
+    "user_phone" TEXT NOT NULL,
+    "user_address" TEXT NOT NULL,
+    "user_city" TEXT NOT NULL,
+    "user_state" TEXT NOT NULL,
+    "user_zip" TEXT NOT NULL,
+    "user_country" TEXT NOT NULL,
+    "user_kyc" INTEGER NOT NULL,
+    "user_birthdate" INTEGER NOT NULL,
+    "user_cpf" TEXT NOT NULL,
+    "company_id" INTEGER NOT NULL,
+    "role_id" INTEGER NOT NULL,
+    "user_flag" TEXT NOT NULL,
+    FOREIGN KEY ("company_id") REFERENCES "company" ("company_id"),
+    FOREIGN KEY ("role_id") REFERENCES "role" ("role_id")
+);
+
+CREATE TABLE "tx_coin" (
+    "tx_coin_id" TEXT PRIMARY KEY,
+    "tx_coin_value" TEXT NOT NULL,
+    "tx_coin_flag" TEXT NOT NULL,
+    "tx_coin_ts" INTEGER NOT NULL,
+    "tx_coin_from_address" TEXT NOT NULL,
+    "tx_coin_to_address" TEXT NOT NULL,
+    "currency_id" INTEGER NOT NULL,
+    "chain_id" INTEGER NOT NULL,
+    FOREIGN KEY ("currency_id") REFERENCES "currency" ("currency_id"),
+    FOREIGN KEY ("chain_id") REFERENCES "chain" ("chain_id")
+);
+
+CREATE TABLE "tx_token" (
+    "tx_token_id" TEXT PRIMARY KEY,
+    "tx_token_flag" TEXT NOT NULL,
+    "tx_token_ts" INTEGER NOT NULL,
+    "tx_token_from_address" TEXT NOT NULL,
+    "tx_token_to_address" TEXT NOT NULL,
+    "token_id" INTEGER NOT NULL,
+    "chain_id" INTEGER NOT NULL,
+    FOREIGN KEY ("token_id") REFERENCES "token" ("token_id"),
+    FOREIGN KEY ("chain_id") REFERENCES "chain" ("chain_id")
+);
+
+CREATE TABLE "orderbook" (
+    "orderbook_id" SERIAL PRIMARY KEY,
+    "orderbook_flag" TEXT NOT NULL,
+    "orderbook_ts" INTEGER NOT NULL,
+    "orderbook_is_token" BOOLEAN NOT NULL, -- true = venda, false = compra
+    "orderbook_amount" TEXT NOT NULL,
+    "status_id" INTEGER NOT NULL,
+    "user_id" INTEGER NOT NULL,
+    "wallet_id" INTEGER NOT NULL,
+    "token_id" INTEGER,
+    "currency_id" INTEGER,
+    "chain_id" INTEGER NOT NULL,
+    FOREIGN KEY ("status_id") REFERENCES "status" ("status_id"),
+    FOREIGN KEY ("user_id") REFERENCES "user" ("user_id"),
+    FOREIGN KEY ("wallet_id") REFERENCES "wallet" ("wallet_id"),
+    FOREIGN KEY ("token_id") REFERENCES "token" ("token_id"),
+    FOREIGN KEY ("currency_id") REFERENCES "currency" ("currency_id"),
+    FOREIGN KEY ("chain_id") REFERENCES "chain" ("chain_id")
+);

+ 65 - 0
models/CommodityModel.php

@@ -0,0 +1,65 @@
+<?php
+
+namespace Models;
+
+class CommodityModel
+{
+    private \PDO $pdo;
+
+    public function __construct()
+    {
+        if (isset($GLOBALS['pdo']) && $GLOBALS['pdo'] instanceof \PDO) {
+            $this->pdo = $GLOBALS['pdo'];
+            return;
+        }
+        throw new \RuntimeException('Global PDO connection not initialized');
+    }
+
+    public function create(string $name, string $flag = 'a'): array
+    {
+        $stmt = $this->pdo->prepare('INSERT INTO "commodities" (commodities_name, commodities_flag) VALUES (:name, :flag) RETURNING commodities_id');
+        $stmt->execute(['name' => $name, 'flag' => $flag]);
+        $id = (int)$stmt->fetchColumn();
+        return [
+            'commodities_id' => $id,
+            'commodities_name' => $name,
+            'commodities_flag' => $flag,
+        ];
+    }
+
+    public function update(int $id, ?string $name = null, ?string $flag = null): ?array
+    {
+        $fields = [];
+        $params = ['id' => $id];
+        if ($name !== null) { $fields[] = 'commodities_name = :name'; $params['name'] = $name; }
+        if ($flag !== null) { $fields[] = 'commodities_flag = :flag'; $params['flag'] = $flag; }
+        if (!$fields) { return null; }
+
+        $sql = 'UPDATE "commodities" SET ' . implode(', ', $fields) . ' WHERE commodities_id = :id';
+        $stmt = $this->pdo->prepare($sql);
+        $ok = $stmt->execute($params);
+        if (!$ok) { return null; }
+
+        $get = $this->pdo->prepare('SELECT commodities_id, commodities_name, commodities_flag FROM "commodities" WHERE commodities_id = :id');
+        $get->execute(['id' => $id]);
+        return $get->fetch(\PDO::FETCH_ASSOC) ?: null;
+    }
+
+    public function delete(int $id): bool
+    {
+        $stmt = $this->pdo->prepare('DELETE FROM "commodities" WHERE commodities_id = :id');
+        $stmt->execute(['id' => $id]);
+        return $stmt->rowCount() > 0;
+    }
+
+    public function getAll(?string $flag = 'a'): array
+    {
+        if ($flag === null) {
+            $stmt = $this->pdo->query('SELECT commodities_id, commodities_name, commodities_flag FROM "commodities" ORDER BY commodities_id');
+            return $stmt->fetchAll(\PDO::FETCH_ASSOC);
+        }
+        $stmt = $this->pdo->prepare('SELECT commodities_id, commodities_name, commodities_flag FROM "commodities" WHERE commodities_flag = :flag ORDER BY commodities_id');
+        $stmt->execute(['flag' => $flag]);
+        return $stmt->fetchAll(\PDO::FETCH_ASSOC);
+    }
+}

+ 36 - 0
models/CompanyModel.php

@@ -0,0 +1,36 @@
+<?php
+
+namespace Models;
+
+class CompanyModel
+{
+    private \PDO $pdo;
+
+    public function __construct()
+    {
+        if (isset($GLOBALS['pdo']) && $GLOBALS['pdo'] instanceof \PDO) {
+            $this->pdo = $GLOBALS['pdo'];
+        } else {
+            throw new \RuntimeException('Global PDO connection not initialized');
+        }
+    }
+
+    public function createCompany(string $name, string $flag = 'a'): int
+    {
+        $stmt = $this->pdo->prepare('INSERT INTO "company" (company_name, company_flag) VALUES (:name, :flag) RETURNING company_id');
+        $stmt->execute(['name' => $name, 'flag' => $flag]);
+        return (int)$stmt->fetchColumn();
+    }
+
+    public function createRole(int $companyId, string $roleName, array $permission = [], string $flag = 'a'): int
+    {
+        $stmt = $this->pdo->prepare('INSERT INTO "role" (company_id, role_name, role_permission, role_flag) VALUES (:company_id, :role_name, :permission::jsonb, :flag) RETURNING role_id');
+        $stmt->execute([
+            'company_id' => $companyId,
+            'role_name' => $roleName,
+            'permission' => json_encode($permission, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE),
+            'flag' => $flag,
+        ]);
+        return (int)$stmt->fetchColumn();
+    }
+}

+ 86 - 45
models/UserModel.php

@@ -8,74 +8,115 @@ class UserModel
 
     public function __construct()
     {
-        // Conecta ao DB usando variável do .env
-        $dbFile = $_ENV['DB_FILE'];
-        $dbPath = __DIR__ . '/../' . $dbFile;
-        $this->pdo = new \PDO("sqlite:" . $dbPath);
-        $this->pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
+        if (isset($GLOBALS['pdo']) && $GLOBALS['pdo'] instanceof \PDO) {
+            $this->pdo = $GLOBALS['pdo'];
+            return;
+        }
     }
 
-    /**
-     * Valida credenciais de login e retorna dados do usuário se válido.
-     *
-     * @param string $username
-     * @param string $password Plain-text password para verificar
-     * @return array|null Dados do usuário (user_id, user_name, etc.) ou null se inválido
-     */
-    public function validateLogin(string $username, string $password): ?array
+    public function validateLogin(string $email, string $password): ?array
     {
-        $stmt = $this->pdo->prepare("SELECT user_id, user_name, user_password FROM user WHERE user_name = :username AND user_flag = 'a'");
-        $stmt->execute(['username' => $username]);
+        $stmt = $this->pdo->prepare('SELECT user_id, user_email, user_password, company_id FROM "user" WHERE user_email = :email AND user_flag = \'a\'');
+        $stmt->execute(['email' => $email]);
         $user = $stmt->fetch(\PDO::FETCH_ASSOC);
 
         if ($user && password_verify($password, $user['user_password'])) {
-            unset($user['password']);  // Remove hash por segurança
+            unset($user['user_password']);
             return $user;
         }
 
         return null;
     }
 
-    /**
-     * Cria um novo usuário com senha hasheada e gera chaves API.
-     *
-     * @param string $username
-     * @param string $password Plain-text password
-     * @param string $flag Default 'a' para ativo
-     * @return array|bool Dados do usuário criado (incluindo api_key) ou false em erro
-     */
-    public function createUser(string $username, string $password, string $flag = 'a')
+    public function createUser(array $data, string $flag = 'a')
     {
-        // Verifica se username já existe
-        $stmt = $this->pdo->prepare("SELECT user_id FROM user WHERE user_name = :username");
-        $stmt->execute(['username' => $username]);
+        // Verifica se email já existe
+        $stmt = $this->pdo->prepare('SELECT user_id FROM "user" WHERE user_email = :email');
+        $stmt->execute(['email' => $data['email']]);
         if ($stmt->fetch()) {
-            return false;  // Já existe
+            return false;
         }
 
-        $hash = password_hash($password, PASSWORD_DEFAULT);
+        $hash = password_hash($data['password'], PASSWORD_DEFAULT);
 
-        // Insere usuário
-        $stmt = $this->pdo->prepare("INSERT INTO user (user_name, user_flag, user_password) VALUES (:username, :flag, :hash)");
-        if (!$stmt->execute(['username' => $username, 'flag' => $flag, 'hash' => $hash])) {
-            return false;
-        }
+        $stmt = $this->pdo->prepare(
+            'INSERT INTO "user" (
+                user_name, user_email, user_password, user_phone, user_address, user_city, user_state, user_zip, user_country,
+                user_kyc, user_birthdate, user_cpf, company_id, role_id, user_flag
+            ) VALUES (
+                :user_name, :user_email, :hash, :user_phone, :user_address, :user_city, :user_state, :user_zip, :user_country,
+                :user_kyc, :user_birthdate, :user_cpf, :company_id, :role_id, :flag
+            ) RETURNING user_id'
+        );
 
-        $userId = $this->pdo->lastInsertId();
+        $ok = $stmt->execute([
+            'user_name' => $data['username'],
+            'user_email' => $data['email'],
+            'hash' => $hash,
+            'user_phone' => $data['phone'],
+            'user_address' => $data['address'],
+            'user_city' => $data['city'],
+            'user_state' => $data['state'],
+            'user_zip' => $data['zip'],
+            'user_country' => $data['country'],
+            'user_kyc' => (int)$data['kyc'],
+            'user_birthdate' => (int)$data['birthdate'],
+            'user_cpf' => $data['cpf'],
+            'company_id' => (int)$data['company_id'],
+            'role_id' => (int)$data['role_id'],
+            'flag' => $flag
+        ]);
 
-        // Gera e insere chaves API (random para HMAC)
-        $apiKey = bin2hex(random_bytes(16));  // Ex: 32 chars hex
-        $apiSecret = bin2hex(random_bytes(32));  // Mais longo para secret
-        $stmt = $this->pdo->prepare("INSERT INTO api_key (user_id, api_key_user, api_key_secret) VALUES (:user_id, :api_key, :api_secret)");
-        if (!$stmt->execute(['user_id' => $userId, 'api_key' => $apiKey, 'api_secret' => $apiSecret])) {
+        if (!$ok) {
             return false;
         }
 
+        $userId = $stmt->fetchColumn();
+
         return [
-            'user_id' => $userId,
-            'user_name' => $username,
-            'api_key_user' => $apiKey,
-            'api_key_secret' => $apiSecret  // Retorne para o usuário (apenas uma vez!)
+            'user_id' => (int)$userId,
+            'user_name' => $data['username'],
+            'user_email' => $data['email'],
+            'company_id' => (int)$data['company_id'],
+            'role_id' => (int)$data['role_id']
         ];
     }
+
+    public function getUsersByCompany(int $companyId): array
+    {
+        $stmt = $this->pdo->prepare("SELECT user_id, user_name, user_email, role_id FROM \"user\" WHERE company_id = :company_id AND user_flag = 'a'");
+        $stmt->execute(['company_id' => $companyId]);
+        return $stmt->fetchAll(\PDO::FETCH_ASSOC);
+    }
+
+    public function deleteUserById(int $userId, int $companyId): bool
+    {
+        $stmt = $this->pdo->prepare("DELETE FROM \"user\" WHERE user_id = :user_id AND company_id = :company_id");
+        return $stmt->execute(['user_id' => $userId, 'company_id' => $companyId]);
+    }
+
+    public function updateEmail(int $userId, string $newEmail): bool
+    {
+        // check duplicate
+        $chk = $this->pdo->prepare('SELECT 1 FROM "user" WHERE user_email = :email AND user_id <> :uid');
+        $chk->execute(['email' => $newEmail, 'uid' => $userId]);
+        if ($chk->fetchColumn()) {
+            return false;
+        }
+        $stmt = $this->pdo->prepare('UPDATE "user" SET user_email = :email WHERE user_id = :uid AND user_flag = \'a\'');
+        return $stmt->execute(['email' => $newEmail, 'uid' => $userId]);
+    }
+
+    public function changePassword(int $userId, string $currentPassword, string $newPassword): bool
+    {
+        $stmt = $this->pdo->prepare('SELECT user_password FROM "user" WHERE user_id = :uid AND user_flag = \'a\'');
+        $stmt->execute(['uid' => $userId]);
+        $hash = $stmt->fetchColumn();
+        if (!$hash || !password_verify($currentPassword, $hash)) {
+            return false;
+        }
+        $newHash = password_hash($newPassword, PASSWORD_DEFAULT);
+        $up = $this->pdo->prepare('UPDATE "user" SET user_password = :hash WHERE user_id = :uid');
+        return $up->execute(['hash' => $newHash, 'uid' => $userId]);
+    }
 }

+ 29 - 6
public/index.php

@@ -2,6 +2,9 @@
 
 require __DIR__ . '/../vendor/autoload.php';
 
+use FrameworkX\App;
+use Middlewares\JwtAuthMiddleware;
+
 $path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
 $file = __DIR__ . $path;
 if (php_sapi_name() === 'cli-server' && is_file($file)) {
@@ -18,18 +21,38 @@ if (class_exists(Dotenv\Dotenv::class) && file_exists(__DIR__ . '/../.env')) {
 
 error_reporting(E_ALL);
 
-use FrameworkX\App;
-use Middlewares\HmacAuthMiddleware;
-use Middlewares\JWTAuthMiddleware;
+$dsn = $_ENV['DB_DSN'] ?? (function () {
+    $host = $_ENV['DB_HOST'] ?? 'localhost';
+    $port = $_ENV['DB_PORT'] ?? '5432';
+    $name = $_ENV['DB_NAME'] ?? 'postgres';
+    return "pgsql:host={$host};port={$port};dbname={$name}";
+})();
+$dbUser = $_ENV['DB_USER'] ?? 'postgres';
+$dbPass = $_ENV['DB_PASSWORD'] ?? '';
+$GLOBALS['pdo'] = new \PDO($dsn, $dbUser, $dbPass);
+$GLOBALS['pdo']->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
 
 $app = new App();
-$authHmac = new HmacAuthMiddleware();
-$authJwt = new JWTAuthMiddleware();
+$authJwt = new JwtAuthMiddleware();
 
-$app->get('/hmachelloworld', $authHmac,\Controllers\HelloController::class);
 $app->get('/jwthelloworld', $authJwt,\Controllers\HelloController::class);
 
 $app->post('/login', \Controllers\LoginController::class);
 $app->post('/register', \Controllers\RegisterController::class);
+$app->post('/user/get', $authJwt, \Controllers\UserGetController::class);
+$app->post('/user/delete', $authJwt, \Controllers\UserDeleteController::class);
+
+// Public endpoint to create company, user, and wallet in a single transaction
+$app->post('/companyWithUser/create', \Controllers\CompanyWithUserController::class);
+
+// Authenticated user profile updates
+$app->post('/user/change-email', $authJwt, \Controllers\UserChangeEmailController::class);
+$app->post('/user/change-password', $authJwt, \Controllers\UserChangePasswordController::class);
+
+// Commodities (JWT-protected)
+$app->post('/commodity/create', $authJwt, \Controllers\CommodityCreateController::class);
+$app->post('/commodity/update', $authJwt, \Controllers\CommodityUpdateController::class);
+$app->post('/commodity/delete', $authJwt, \Controllers\CommodityDeleteController::class);
+$app->get('/commodities', $authJwt, \Controllers\CommoditiesGetController::class);
 
 $app->run();

+ 659 - 0
routes.md

@@ -0,0 +1,659 @@
+# API Routes Documentation
+
+This document outlines the available endpoints based on the provided controllers (**LoginController** and **RegisterController**).
+
+**Base URL Placeholder:** `http://localhost:8000`
+
+---
+
+## 1. User Login
+
+Authenticates a user and returns a JWT token.
+
+### **Endpoint**
+
+`POST /login`
+
+### **Headers**
+
+`Content-Type: application/json`
+
+### **Request Body (JSON)**
+
+```json
+{
+  "email": "john.doe@example.com",
+  "password": "securepassword123"
+}
+```
+
+### **cURL Example**
+
+```bash
+curl --location 'http://localhost:8000/login' \
+--header 'Content-Type: application/json' \
+--data '{
+    "email": "john.doe@example.com",
+    "password": "securepassword123"
+}'
+```
+
+### **Responses**
+
+#### **200 OK (Success)**
+
+```json
+{
+  "status": "ok",
+  "msg": "[100] Request ok.",
+  "code": "S_OK",
+  "data": {
+    "token": "<jwt>",
+    "user_id": 1,
+    "company_id": 1
+  }
+}
+```
+
+#### **401 Unauthorized (Missing fields or Invalid credentials)**
+
+```json
+{
+  "status": "fail",
+  "message": "Invalid credentials",
+  "code": "E_VALIDATE"
+}
+```
+
+---
+
+## 2. User Register
+
+Creates a new user account in the database.
+
+### **Endpoint**
+
+`POST /register`
+
+### **Headers**
+
+`Content-Type: application/json`
+
+### **Request Body (JSON)**
+
+```json
+{
+  "username": "John Doe",
+  "email": "john.doe@example.com",
+  "password": "securepassword123",
+  "phone": "+15550199",
+  "address": "123 Tech Street",
+  "city": "Silicon Valley",
+  "state": "CA",
+  "zip": "94000",
+  "country": "USA",
+  "kyc": 1,
+  "birthdate": 631152000,
+  "cpf": "123.456.789-00",
+  "company_id": 1,
+  "role_id": 2
+}
+```
+
+### **cURL Example**
+
+```bash
+curl --location 'http://localhost:8000/register' \
+--header 'Content-Type: application/json' \
+--data '{
+  "username": "John Doe",
+  "email": "john.doe@example.com",
+  "password": "securepassword123",
+  "phone": "+15550199",
+  "address": "123 Tech Street",
+  "city": "Silicon Valley",
+  "state": "CA",
+  "zip": "94000",
+  "country": "USA",
+  "kyc": 1,
+  "birthdate": 631152000,
+  "cpf": "123.456.789-00",
+  "company_id": 1,
+  "role_id": 1
+}'
+```
+
+### **Responses**
+
+#### **200 OK (Success)**
+
+```json
+{
+  "status": "success",
+  "code": "S_CREATED",
+  "data": {
+    "user_id": 45,
+    "user_name": "John Doe",
+    "user_email": "john.doe@example.com",
+    "company_id": 1,
+    "role_id": 2
+  }
+}
+```
+
+#### **400 Bad Request (Validation Errors)**
+
+* Missing required fields
+* Invalid email format
+* Password too short (< 8 characters)
+* Email already exists
+
+```json
+{
+  "status": "fail",
+  "message": "Missing field: phone",
+  "code": "E_VALIDATE"
+}
+```
+
+
+---
+
+## 3. Create Company with User and Wallet
+
+Creates a company, a user linked to it, and a wallet (address/publicKey/privateKey) in a single transaction. Public endpoint (no auth).
+
+### **Endpoint**
+
+`POST /companyWithUser/create`
+
+### **Headers**
+
+`Content-Type: application/json`
+
+### **Request Body (JSON)**
+
+```json
+{
+  "company_name": "Acme Corp",
+  "username": "John Doe",
+  "email": "john.doe@example.com",
+  "password": "secret123",
+  "phone": "+55 11 99999-9999",
+  "address": "Rua A, 123",
+  "city": "Sao Paulo",
+  "state": "SP",
+  "zip": "01234-567",
+  "country": "BR",
+  "kyc": 1,
+  "birthdate": 631152000,
+  "cpf": "123.456.789-00"
+}
+```
+
+### **cURL Example**
+
+```bash
+curl --location 'http://localhost:8000/companyWithUser/create' \
+  -H 'Content-Type: application/json' \
+  --data '{
+    "company_name": "Acme Corp",
+    "username": "John Doe",
+    "email": "john.doe@example.com",
+    "password": "secret123",
+    "phone": "+55 11 99999-9999",
+    "address": "Rua A, 123",
+    "city": "Sao Paulo",
+    "state": "SP",
+    "zip": "01234-567",
+    "country": "BR",
+    "kyc": 1,
+    "birthdate": 631152000,
+    "cpf": "123.456.789-00"
+  }'
+```
+
+### **Responses**
+
+#### **200 OK (Success)**
+
+```json
+{
+  "status": "ok",
+  "msg": "[100] Request ok.",
+  "code": "S_CREATED",
+  "data": {
+    "company_id": 1,
+    "role_id": 1,
+    "user": {
+      "user_id": 1,
+      "user_name": "John Doe",
+      "user_email": "john.doe@example.com",
+      "company_id": 1,
+      "role_id": 1
+    },
+    "wallet_id": 1,
+    "wallet_address": "0x8AC9615b1a555BeA2938778aAef1ead35205c167"
+  }
+}
+```
+
+#### **400 Bad Request (Validation Errors)**
+
+```json
+{
+  "status": "failed",
+  "msg": "Password must be at least 8 characters",
+  "code": "E_VALIDATE",
+  "data": []
+}
+```
+
+#### **500 Internal Server Error (Wallet/DB issues)**
+
+```json
+{
+  "status": "failed",
+  "msg": "Wallet generation failed",
+  "code": "E_INTERNAL",
+  "data": []
+}
+```
+
+
+---
+
+## 4. Change User Email
+
+Updates the authenticated user's email. Requires JWT.
+
+### **Endpoint**
+
+`POST /user/change-email`
+
+### **Headers**
+
+`Content-Type: application/json`
+
+`Authorization: Bearer <JWT>`
+
+### **Request Body (JSON)**
+
+```json
+{
+  "email": "new.email@example.com"
+}
+```
+
+### **cURL Example**
+
+```bash
+curl --location 'http://localhost:8000/user/change-email' \
+  -H 'Content-Type: application/json' \
+  -H 'Authorization: Bearer <JWT>' \
+  --data '{"email":"new.email@example.com"}'
+```
+
+### **Responses**
+
+#### **200 OK (Updated)**
+
+```json
+{
+  "status": "ok",
+  "msg": "[100] Request ok.",
+  "code": "S_UPDATED",
+  "data": {
+    "user_id": 1,
+    "user_email": "new.email@example.com"
+  }
+}
+```
+
+#### **400 Bad Request (Validation Errors)**
+
+```json
+{
+  "status": "failed",
+  "msg": "Email already in use or update failed",
+  "code": "E_VALIDATE",
+  "data": []
+}
+```
+
+#### **401 Unauthorized**
+
+```json
+{
+  "status": "failed",
+  "msg": "Unauthorized",
+  "code": "E_VALIDATE",
+  "data": []
+}
+```
+
+
+---
+
+## 5. Change User Password
+
+Changes the authenticated user's password. Requires JWT.
+
+### **Endpoint**
+
+`POST /user/change-password`
+
+### **Headers**
+
+`Content-Type: application/json`
+
+`Authorization: Bearer <JWT>`
+
+### **Request Body (JSON)**
+
+```json
+{
+  "current_password": "minhaSenhaAtual",
+  "new_password": "novaSenhaForte123"
+}
+```
+
+### **cURL Example**
+
+```bash
+curl --location 'http://localhost:8000/user/change-password' \
+  -H 'Content-Type: application/json' \
+  -H 'Authorization: Bearer <JWT>' \
+  --data '{"current_password":"minhaSenhaAtual","new_password":"novaSenhaForte123"}'
+```
+
+### **Responses**
+
+#### **200 OK (Updated)**
+
+```json
+{
+  "status": "ok",
+  "msg": "[100] Request ok.",
+  "code": "S_UPDATED",
+  "data": {
+    "user_id": 1
+  }
+}
+```
+
+#### **400 Bad Request (Validation Errors)**
+
+```json
+{
+  "status": "failed",
+  "msg": "Invalid current password or update failed",
+  "code": "E_VALIDATE",
+  "data": []
+}
+```
+
+#### **401 Unauthorized**
+
+```json
+{
+  "status": "failed",
+  "msg": "Unauthorized",
+  "code": "E_VALIDATE",
+  "data": []
+}
+```
+
+## 6. Commodities — List
+
+Returns a list of commodities filtered by `flag` or all if `flag=all`.
+
+### **Endpoint**
+
+`GET /commodities`
+
+### **cURL Example**
+
+```bash
+curl --location 'http://localhost:8000/commodities?flag=all'
+```
+
+### **Responses**
+
+#### **200 OK**
+
+```json
+{
+  "status": "ok",
+  "msg": "[100] Request ok.",
+  "code": "S_OK",
+  "data": [
+    {
+      "commodities_id": 1,
+      "name": "Gold",
+      "flag": "a"
+    }
+  ]
+}
+```
+
+#### **204 No Content**
+
+```json
+{
+  "status": "fail",
+  "msg": "Commodities Not Found",
+  "code": "E_DATABASE",
+  "data": []
+}
+```
+
+---
+
+## 7. Commodity Create
+
+Creates a new commodity.
+
+### **Endpoint**
+
+`POST /commodity/create`
+
+### **Headers**
+
+`Content-Type: application/json`
+
+### **Request Body (JSON)**
+
+```json
+{
+  "name": "Gold",
+  "flag": "a"
+}
+```
+
+### **cURL Example**
+
+```bash
+curl --location 'http://localhost:8000/commodity/create' \
+  -H 'Content-Type: application/json' \
+  --data '{
+    "name": "Gold",
+    "flag": "a"
+  }'
+```
+
+### **Responses**
+
+#### **200 OK (Created)**
+
+```json
+{
+  "status": "ok",
+  "msg": "[100] Request ok.",
+  "code": "S_CREATED",
+  "data": {
+    "commodities_id": 10,
+    "name": "Gold",
+    "flag": "a"
+  }
+}
+```
+
+#### **400 Bad Request (Missing name)**
+
+```json
+{
+  "status": "fail",
+  "msg": "Validation failed: name is required",
+  "code": "E_VALIDATE",
+  "data": []
+}
+```
+
+---
+
+## 8. Commodity Update
+
+Updates an existing commodity.
+
+### **Endpoint**
+
+`POST /commodity/update`
+
+### **Headers**
+
+`Content-Type: application/json`
+
+### **Request Body (JSON)**
+
+```json
+{
+  "commodities_id": 10,
+  "name": "Silver",
+  "flag": "b"
+}
+```
+
+You may send **only name**, **only flag**, or both.
+
+### **cURL Example**
+
+```bash
+curl --location 'http://localhost:8000/commodity/update' \
+  -H 'Content-Type: application/json' \
+  --data '{
+    "commodities_id": 10,
+    "name": "Silver",
+    "flag": "b"
+  }'
+```
+
+### **Responses**
+
+#### **200 OK**
+
+```json
+{
+  "status": "ok",
+  "msg": "[100] Request ok.",
+  "code": "S_UPDATED",
+  "data": {
+    "commodities_id": 10,
+    "name": "Silver",
+    "flag": "b"
+  }
+}
+```
+
+#### **400 Bad Request**
+
+* Missing commodities_id
+* Empty name
+* No fields to update
+
+```json
+{
+  "status": "fail",
+  "msg": "Validation failed: nothing to update",
+  "code": "E_VALIDATE",
+  "data": []
+}
+```
+
+#### **204 No Content**
+
+```json
+{
+  "status": "fail",
+  "msg": "Commodity Not Found or Not Updated",
+  "code": "E_DATABASE",
+  "data": []
+}
+```
+
+---
+
+## 9. Commodity Delete
+
+Deletes a commodity by ID.
+
+### **Endpoint**
+
+`POST /commodity/delete`
+
+### **Headers**
+
+`Content-Type: application/json`
+
+### **Request Body (JSON)**
+
+```json
+{
+  "commodities_id": 10
+}
+```
+
+### **cURL Example**
+
+```bash
+curl --location 'http://localhost:8000/commodity/delete' \
+  -H 'Content-Type: application/json' \
+  --data '{"commodities_id":10}'
+```
+
+### **Responses**
+
+#### **200 OK (Deleted)**
+
+```json
+{
+  "status": "ok",
+  "msg": "[100] Request ok.",
+  "code": "S_DELETED",
+  "data": {
+    "deleted": true
+  }
+}
+```
+
+#### **400 Bad Request**
+
+```json
+{
+  "status": "fail",
+  "msg": "Validation failed: invalid commodities_id",
+  "code": "E_VALIDATE",
+  "data": []
+}
+```
+
+#### **204 No Content**
+
+```json
+{
+  "status": "fail",
+  "msg": "Commodity Not Found",
+  "code": "E_DATABASE",
+  "data": []
+}
+```

+ 22 - 0
test_bash_executor.php

@@ -0,0 +1,22 @@
+<?php
+
+$bin = __DIR__ . "/bin/easycli";
+require __DIR__ . '/vendor/autoload.php';
+
+use Libs\BashExecutor;
+
+echo "=== TESTE 1: Comando simples ===\n";
+$result = BashExecutor::run("{$bin} polygon create-new-address");
+print_r($result);
+
+echo "\n=== TESTE 2: Listar arquivos ===\n";
+$result = BashExecutor::run("ls -la");
+print_r($result);
+
+echo "\n=== TESTE 3: Comando inválido ===\n";
+$result = BashExecutor::run("comando_inexistente");
+print_r($result);
+
+echo "\n=== TESTE 4: Teste de timeout ===\n";
+$result = BashExecutor::run("sleep 5", 2);
+print_r($result);