diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..661b217 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +# Env files +**/*.env + +# Logs +**/logs/ diff --git a/backend/.gitignore b/backend/.gitignore new file mode 100644 index 0000000..c051b8c --- /dev/null +++ b/backend/.gitignore @@ -0,0 +1,2 @@ +# Certs +**/certs/ diff --git a/backend/PHP.Dockerfile b/backend/PHP.Dockerfile new file mode 100644 index 0000000..de722b3 --- /dev/null +++ b/backend/PHP.Dockerfile @@ -0,0 +1,7 @@ +FROM php:7.4-fpm + +RUN docker-php-ext-install pdo pdo_mysql + +RUN pecl install xdebug && docker-php-ext-enable xdebug + +WORKDIR /app/backend/api diff --git a/backend/Py.Dockerfile b/backend/Py.Dockerfile new file mode 100644 index 0000000..90dff4c --- /dev/null +++ b/backend/Py.Dockerfile @@ -0,0 +1,37 @@ +FROM continuumio/miniconda3 as build + +WORKDIR /app + +COPY ./python /app/src + +RUN conda env create -f src/environment.yml + +#RUN echo "conda activate cryptos" >> ~/.bashrc +#SHELL ["/bin/bash", "--login", "-c"] + +RUN conda install -c conda-forge conda-pack + +RUN conda pack -n cryptos -o /tmp/env.tar && \ + mkdir /venv && cd /venv && tar xf /tmp/env.tar && \ + rm /tmp/env.tar + +RUN /venv/bin/conda-unpack + + +FROM python:buster as runtime + +WORKDIR /app + +COPY ./python /app/src +COPY ./api/bin /app/bin + +COPY --from=build /venv /venv + +SHELL ["/bin/bash", "-c"] + +RUN pip install pyinstaller + +RUN pyinstaller -F -n coingecko --clean --log-level DEBUG --distpath /app/bin /app/src/coingecko.py && \ + pyinstaller -F -n mindicador --clean --log-level DEBUG --distpath /app/bin /app/src/miindicador.py + +ENTRYPOINT [ "/bin/bash" ] diff --git a/backend/api/Readme.md b/backend/api/Readme.md new file mode 100644 index 0000000..6f2bd78 --- /dev/null +++ b/backend/api/Readme.md @@ -0,0 +1,29 @@ +# API + +## Concepts ++ [x] Coins ++ [x] Wallets + + Name + + Public Address + + [x] Location + + Home PC + + Notebook + + Exchange ++ [x] Transactions - + Read from blockchain ++ [x] Values + + [ ] In USD + + [ ] In CLF + + [ ] In BTC + +## Actions + ++ List ++ Show/Read ++ Add ++ Edit ++ Delete + ++ [x] Coins ++ [x] Wallets ++ [x] Locations diff --git a/backend/api/common/Controller/API.php b/backend/api/common/Controller/API.php new file mode 100644 index 0000000..15b610e --- /dev/null +++ b/backend/api/common/Controller/API.php @@ -0,0 +1,46 @@ + '1.0.0', + 'routes' => [ + '/coins' => [ + '/' => 'List all coins', + '/add' => 'Add coin' + ], + '/coin/{coin_id}' => [ + '/' => 'Show coin information', + '/edit' => 'Edit coin', + '/delete' => 'Delete coin' + ], + '/locations' => [ + '/' => 'List all locations', + '/add' => 'Add location' + ], + '/location/{location_id}' => [ + '/' => 'Show location information', + '/edit' => 'Edit location', + '/delete' => 'Delete location' + ], + '/wallets' => [ + '/' => 'List all wallets', + '/add' => 'Add wallet' + ], + '/wallet/{wallet_id}' => [ + '/' => 'Show wallet information', + '/edit' => 'Edit wallet', + '/delete' => 'Delete wallet' + ] + ] + ]; + return $this->withJson($response, $output); + } +} diff --git a/backend/api/common/Controller/Coins.php b/backend/api/common/Controller/Coins.php new file mode 100644 index 0000000..66a8ffc --- /dev/null +++ b/backend/api/common/Controller/Coins.php @@ -0,0 +1,66 @@ +find(Coin::class)->array(); + usort($coins, function($a, $b) { + return strcmp($a['code'], $b['code']); + }); + $output = compact('coins'); + return $this->withJson($response, $output); + } + public function show(Request $request, Response $response, ModelFactory $factory, $coin_id): Response { + $coin = $factory->find(Coin::class)->one($coin_id); + if (!$coin) { + return $this->withJson($response, ['coin' => null]); + } + $output = ['coin' => $coin->toArray()]; + return $this->withJson($response, $output); + } + public function add(Request $request, Response $response, ModelFactory $factory): Response { + $post = $request->getBody()->getContents(); + $post = json_decode($post); + $coin = Coin::add($factory, $post); + $status = false; + if ($coin->isNew()) { + $status = $coin->save(); + } + $output = [ + 'input' => $post, + 'coin' => $coin->toArray(), + 'created' => $status + ]; + return $this->withJson($response, $output); + } + public function edit(Request $request, Response $response, ModelFactory $factory, $coin_id): Response { + $coin = $factory->find(Coin::class)->one($coin_id); + if (!$coin) { + return $this->withJson($response, ['coin' => null]); + } + $post = json_decode($request->getBody()->getContents()); + $edited = $coin->edit($post); + $output = ['input' => $post, 'coin' => $coin->toArray(), 'edited' => $edited]; + return $this->withJson($response, $output); + } + public function delete(Request $request, Response $response, ModelFactory $factory, $coin_id): Response { + $coin = $factory->find(Coin::class)->one($coin_id); + if (!$coin) { + return $this->withJson($response, ['coin' => null, 'deleted' => false]); + } + $output = [ + 'coin' => $coin->toArray() + ]; + $status = $coin->delete(); + $output['deleted'] = $status; + return $this->withJson($response, $output); + } +} diff --git a/backend/api/common/Controller/Locations.php b/backend/api/common/Controller/Locations.php new file mode 100644 index 0000000..d811a69 --- /dev/null +++ b/backend/api/common/Controller/Locations.php @@ -0,0 +1,70 @@ +find(Location::class)->array(); + $output = compact('locations'); + return $this->withJson($response, $output); + } + public function show(Request $request, Response $response, ModelFactory $factory, $location_id): Response { + $location = $factory->find(Location::class)->one($location_id); + if (!$location) { + return $this->withJson($response, ['location' => null]); + } + $output = ['location' => $location->asArray()]; + return $this->withJson($response, $output); + } + public function add(Request $request, Response $response, ModelFactory $factory): Response { + $post = json_decode($request->getBody()->getContents()); + $fields = [ + 'name' => 'name', + 'description' => 'description' + ]; + $data = array_combine($fields, array_merge(array_intersect_key((array) $post, $fields), array_fill_keys(array_keys(array_diff_key($fields, (array) $post)), null))); + $location = $factory->find(Location::class)->where([ + ['name', $data['name']] + ])->one(); + $status = true; + if (!$location) { + $location = $factory->create(Location::class, $data); + $status = $location->save(); + } + $output = [ + 'information_provided' => $post, + 'used_data' => $data, + 'location' => $location->asArray(), + 'saved' => $status + ]; + return $this->withJson($response, $output); + } + public function edit(Request $request, Response $response, ModelFactory $factory, $location_id): Response { + $location = $factory->find(Location::class)->one($location_id); + if (!$location) { + return $this->withJson($response, ['location' => null]); + } + $post = json_decode($request->getBody()->getContents()); + $output = compact('location'); + return $this->withJson($response, $output); + } + public function delete(Request $request, Response $response, ModelFactory $factory, $location_id): Response { + $location = $factory->find(Location::class)->one($location_id); + if (!$location) { + return $this->withJson($response, ['location' => null, 'deleted' => false]); + } + $output = [ + 'location' => $location->asArray() + ]; + $status = $location->delete(); + $output['deleted'] = $status; + return $this->withJson($response, $output); + } +} diff --git a/backend/api/common/Controller/Update.php b/backend/api/common/Controller/Update.php new file mode 100644 index 0000000..c12ef28 --- /dev/null +++ b/backend/api/common/Controller/Update.php @@ -0,0 +1,27 @@ +run(); + $output = [ + 'result' => $result + ]; + return $this->withJson($response, $output); + } + public function register(Request $request, Response $response, Updater $updater, $coin_id, $type) { + $result = $updater->register($coin_id, $type); + $output = [ + 'input' => ['coin_id' => $coin_id, 'type' => $type], + 'result' => $result + ]; + return $this->withJson($response, $output); + } +} diff --git a/backend/api/common/Controller/Wallets.php b/backend/api/common/Controller/Wallets.php new file mode 100644 index 0000000..781bff3 --- /dev/null +++ b/backend/api/common/Controller/Wallets.php @@ -0,0 +1,71 @@ +find(Wallet::class)->array(); + $output = compact('wallets'); + return $this->withJson($response, $output); + } + public function show(Request $request, Response $response, ModelFactory $factory, $wallet_id): Response { + $wallet = $factory->find(Wallet::class)->one($wallet_id); + if (!$wallet) { + return $this->withJson($response, ['wallet' => null]); + } + $output = ['wallet' => $wallet->asArray()]; + return $this->withJson($response, $output); + } + public function add(Request $request, Response $response, ModelFactory $factory): Response { + $post = json_decode($request->getBody()->getContents()); + $fields = [ + 'name' => 'name', + 'location' => 'location_id', + 'address' => 'public_address' + ]; + $data = array_combine($fields, array_merge(array_intersect_key((array) $post, $fields), array_fill_keys(array_keys(array_diff_key($fields, (array) $post)), null))); + $wallet = $factory->find(Wallet::class)->where([ + ['name', $data['name']] + ])->one(); + $status = true; + if (!$wallet) { + $wallet = $factory->create(Wallet::class, $data); + $status = $wallet->save(); + } + $output = [ + 'information_provided' => $post, + 'used_data' => $data, + 'wallet' => $wallet->asArray(), + 'saved' => $status + ]; + return $this->withJson($response, $output); + } + public function edit(Request $request, Response $response, ModelFactory $factory, $wallet_id): Response { + $wallet = $factory->find(Wallet::class)->one($wallet_id); + if (!$wallet) { + return $this->withJson($response, ['wallet' => null]); + } + $post = json_decode($request->getBody()->getContents()); + $output = compact('wallet'); + return $this->withJson($response, $output); + } + public function delete(Request $request, Response $response, ModelFactory $factory, $wallet_id): Response { + $wallet = $factory->find(Wallet::class)->one($wallet_id); + if (!$wallet) { + return $this->withJson($response, ['wallet' => null, 'deleted' => false]); + } + $output = [ + 'wallet' => $wallet->asArray() + ]; + $status = $wallet->delete(); + $output['deleted'] = $status; + return $this->withJson($response, $output); + } +} diff --git a/backend/api/common/Factory/Model.php b/backend/api/common/Factory/Model.php new file mode 100644 index 0000000..36a2327 --- /dev/null +++ b/backend/api/common/Factory/Model.php @@ -0,0 +1,7 @@ +getRoutingResults(); + $methods = $routingResults->getAllowedMethods(); + $requestHeaders = $request->getHeaderLine('Access-Control-Request-Headers'); + + $response = $handler->handle($request); + + /*$response = $response + ->withHeader('Access-Control-Allow-Origin', '*') + ->withHeader('Access-Control-Allow-Methods', implode(', ', $methods)) + ->withHeader('Access-Control-Allow-Headers', $requestHeaders ?: '*');*/ + + // Optional: Allow Ajax CORS requests with Authorization header + $response = $response->withHeader('Access-Control-Allow-Credentials', 'true'); + + return $response; + } +} diff --git a/backend/api/common/Service/Auth.php b/backend/api/common/Service/Auth.php new file mode 100644 index 0000000..62ede76 --- /dev/null +++ b/backend/api/common/Service/Auth.php @@ -0,0 +1,49 @@ +login_time = $login_time; + } + protected $factory; + public function setFactory(ModelFactory $factory) { + $this->factory = $factory; + } + protected function createToken() { + return password_hash(random_bytes(100), \PASSWORD_BCRYPT); + } + public function login(string $username, string $password) { + $user = $this->factory->find(User::class)->where([['name', $username]])->one(); + if (!$user) { + return false; + } + if (!password_verify($password, $user->password)) { + return false; + } + $now = Carbon::now(); + $login = $this->factory->find(Login::class)->where([['user_id', $user->id], ['date_time', $now->copy()->subSeconds($this->login_time), '>=']])->one(); + if (!$login) { + $token = $this->createToken(); + $data = [ + 'user_id' => $user->id, + 'token' => $token, + 'date_time' => $now->format('Y-m-d H:i:s') + ]; + $login = $this->factory->create(Login::class, $data); + } else { + $login->date($now); + } + $login->save(); + return $token; + } + public function isLoggedIn($token) { + $login = $this->factory->find(Login::class)->where([['token', $token]])->one(); + return $login->user(); + } +} diff --git a/backend/api/common/Service/Update.php b/backend/api/common/Service/Update.php new file mode 100644 index 0000000..3add238 --- /dev/null +++ b/backend/api/common/Service/Update.php @@ -0,0 +1,60 @@ +factory = $factory; + $this->execs = $executables; + $this->load(); + } + public function load() { + $this->coins = [[], []]; + $results = \ORM::for_table('coin_registers')->find_many(); + foreach ($results as $result) { + $this->coins[$result->type] []= $result->coin_id; + } + } + protected $coins; + public function register(int $coin_id, int $type = 0) { + /*if (array_search($coin_id, $this->coins[$type]) !== false) { + return; + }*/ + $this->coins[$type] []= $coin_id; + $this->getHistorical($coin_id, $type); + $check = \ORM::for_table('coin_registers')->where('coin_id', $coin_id)->find_one(); + if (!$check) { + \ORM::raw_execute("INSERT INTO coin_registers (coin_id, type) VALUES (?, ?)", [$coin_id, $type]); + } + return true; + } + protected function getHistorical(int $coin_id, int $type) { + $coin = $this->factory->find(Coin::class)->one($coin_id); + $f = Carbon::now(); + $exe = [$this->execs[$type]]; + switch ($type) { + case 0: + $exe []= '-i ' . $coin->identifier; + $exe []= '-c usd,clp'; + $exe []= 'hist -hi'; + $exe []= '-f ' . $f->copy()->subYears(10)->timestamp; + $exe []= '-t ' . $f->timestamp(); + break; + case 1: + $exe []= '-i ' . $coin->identifier; + $exe []= 'hist -hi'; + $exe []= '-s ' . $f->copy()->subYears(10)->year; + break; + } + !d(implode(' ', $exe)); + $output = shell_exec(implode(' ', $exe)); + !d($output); + } + public function run() { + } +} diff --git a/backend/api/composer.json b/backend/api/composer.json new file mode 100644 index 0000000..c1d8a3d --- /dev/null +++ b/backend/api/composer.json @@ -0,0 +1,44 @@ +{ + "name": "provm/crypto", + "description": "Crypto currency API", + "type": "project", + "require": { + "slim/slim": "^4.7", + "php-di/slim-bridge": "^3.1", + "nyholm/psr7": "^1.4", + "nyholm/psr7-server": "^1.0", + "zeuxisoo/slim-whoops": "^0.7.3", + "provm/models": "^1.0-rc", + "spatie/crypto": "^2.0", + "robmorgan/phinx": "^0.12.5", + "nesbot/carbon": "^2.49" + }, + "require-dev": { + "phpunit/phpunit": "^9.5", + "kint-php/kint": "^3.3" + }, + "license": "MIT", + "authors": [ + { + "name": "Aldarien", + "email": "aldarien85@gmail.com" + } + ], + "autoload": { + "psr-4": { + "ProVM\\Crypto\\Common\\": "common", + "ProVM\\Crypto\\": "src", + "ProVM\\Common\\": "../../provm/common", + "ProVM\\": "../../provm/src" + } + }, + "repositories": [ + { + "type": "git", + "url": "http://git.provm.cl/ProVM/models.git" + } + ], + "config": { + "secure-http": false + } +} diff --git a/backend/api/db/migrations/20210621171837_create_tables.php b/backend/api/db/migrations/20210621171837_create_tables.php new file mode 100644 index 0000000..0fc9f29 --- /dev/null +++ b/backend/api/db/migrations/20210621171837_create_tables.php @@ -0,0 +1,73 @@ +table('coins') + ->addColumn('identifier', 'string') + ->addColumn('code', 'string', ['limit' => 5]) + ->addColumn('name', 'string') + ->addColumn('prefix', 'string', ['default' => '']) + ->addColumn('suffix', 'string', ['default' => '']) + ->addColumn('decimals', 'integer', ['default' => 0]) + ->addColumn('ref_url', 'text', ['default' => '']) + ->create(); + + $this->table('locations') + ->addColumn('name', 'string') + ->addColumn('description', 'string') + ->create(); + + $cascade = ['update' => 'CASCADE', 'delete' => 'CASCADE']; + + $this->table('wallets') + ->addColumn('name', 'string') + ->addColumn('location_id', 'integer') + ->addForeignKey('location_id', 'locations', 'id', $cascade) + ->addColumn('public_address', 'string') + ->create(); + + $this->table('values') + ->addColumn('date_time', 'datetime') + ->addColumn('coin_id', 'integer') + ->addForeignKey('coin_id', 'coins', 'id', $cascade) + ->addColumn('value', 'double') + ->addColumn('unit_id', 'integer') + ->addForeignKey('unit_id', 'coins', 'id', $cascade) + ->create(); + + $this->table('transactions') + ->addColumn('date_time', 'datetime') + ->addColumn('coin_id', 'integer') + ->addForeignKey('coin_id', 'coins', 'id', $cascade) + ->addColumn('wallet_id', 'integer') + ->addForeignKey('wallet_id', 'wallets', 'id', $cascade) + ->addColumn('amount', 'double') + ->addColumn('value', 'double') + ->addColumn('unit_id', 'integer') + ->addForeignKey('unit_id', 'coins', 'id', $cascade) + ->create(); + + $this->table('coin_registers') + ->addColumn('type', 'integer') + ->addColumn('coin_id', 'integer') + ->addForeignKey('coin_id', 'coins', 'id') + ->addColumn('date_time', 'datetime') + ->create(); + } +} diff --git a/backend/api/db/migrations/schema.yml b/backend/api/db/migrations/schema.yml new file mode 100644 index 0000000..7ccd19d --- /dev/null +++ b/backend/api/db/migrations/schema.yml @@ -0,0 +1,89 @@ +name: crypto +tables: + - name: coins + columns: + - name: id + type: int + unsigned: true + auto_increment: true + - name: code + type: varchar(5) + - name: name + type: varchar(200) + - name: ref_url + type: text + primary_key: id + - name: locations + columns: + - name: id + type: int + unsigned: true + auto_increment: true + - name: name + type: varchar(100) + - name: description + type: varchar(200) + primary_key: id + - name: transactions + columns: + - name: id + type: int + unsigned: true + auto_increment: true + - name: date_time + type: datetime + - name: coin_id + type: int + unsigned: true + - name: wallet_id + type: int + unsigned: true + - name: amount + type: double + - name: value + type: double + - name: unit_id + type: int + unsigned: true + primary_key: id + foreign_keys: + - table: coins + local_column: coin_id + foreign_column: id + - table: wallets + local_column: wallet_id + foreign_column: id + - table: coins + local_column: unit_id + foreign_column: id + - name: values + columns: + - name: id + type: int + unsigned: true + auto_increment: true + - name: date_time + type: datetime + - name: coin_id + type: int + unsigned: true + - name: value + type: double + - name: unit_id + type: int + unsigned: true + primary_key: id + foreign_keys: + - table: coins + local_column: coin_id + foreign_column: id + - table: coins + local_column: unit_id + foreign_column: id + - name: wallets + columns: + - name: id + - name: name + - name: location_id + - name: public_address + primary_key: id diff --git a/backend/api/phinx.php b/backend/api/phinx.php new file mode 100644 index 0000000..b3d2ebe --- /dev/null +++ b/backend/api/phinx.php @@ -0,0 +1,41 @@ + [ + 'migrations' => '%%PHINX_CONFIG_DIR%%/db/migrations', + 'seeds' => '%%PHINX_CONFIG_DIR%%/db/seeds' + ], + 'environments' => [ + 'default_migration_table' => 'phinxlog', + 'default_environment' => 'development', + 'production' => [ + 'adapter' => 'mysql', + 'host' => $_ENV['DB_HOST'], + 'name' => $_ENV['MYSQL_DATABASE'], + 'user' => $_ENV['MYSQL_USER'], + 'pass' => $_ENV['MYSQL_PASSWORD'], + //'port' => '3306', + 'charset' => 'utf8', + ], + 'development' => [ + 'adapter' => 'mysql', + 'host' => $_ENV['DB_HOST'], + 'name' => $_ENV['MYSQL_DATABASE'] . '_dev', + 'user' => $_ENV['MYSQL_USER'], + 'pass' => $_ENV['MYSQL_PASSWORD'], + //'port' => '3306', + 'charset' => 'utf8', + ], + 'testing' => [ + 'adapter' => 'mysql', + 'host' => $_ENV['DB_HOST'], + 'name' => $_ENV['MYSQL_DATABASE'] . '_test', + 'user' => $_ENV['MYSQL_USER'], + 'pass' => $_ENV['MYSQL_PASSWORD'], + //'port' => '3306', + 'charset' => 'utf8', + ] + ], + 'version_order' => 'creation' +]; diff --git a/backend/api/public/.htaccess b/backend/api/public/.htaccess new file mode 100644 index 0000000..e69de29 diff --git a/backend/api/public/index.php b/backend/api/public/index.php new file mode 100644 index 0000000..52d1e18 --- /dev/null +++ b/backend/api/public/index.php @@ -0,0 +1,9 @@ +run(); diff --git a/backend/api/resources/data/api.json b/backend/api/resources/data/api.json new file mode 100644 index 0000000..d269d34 --- /dev/null +++ b/backend/api/resources/data/api.json @@ -0,0 +1,12 @@ +{ + "version": "1.0.0", + "routes": [ + { + "alias": [ + "/", + "/help" + ], + "description": "API help" + } + ] +} diff --git a/backend/api/resources/routes/api.php b/backend/api/resources/routes/api.php new file mode 100644 index 0000000..96efeba --- /dev/null +++ b/backend/api/resources/routes/api.php @@ -0,0 +1,15 @@ +getExtension() != 'php') { + continue; + } + include_once $file->getRealPath(); + } +} + +$app->get('[/]', API::class); diff --git a/backend/api/resources/routes/api/coins.php b/backend/api/resources/routes/api/coins.php new file mode 100644 index 0000000..8a0e4b4 --- /dev/null +++ b/backend/api/resources/routes/api/coins.php @@ -0,0 +1,13 @@ +group('/coins', function($app) { + $app->post('/add', [Coins::class, 'add']); + $app->get('[/]', Coins::class); +}); + +$app->group('/coin/{coin_id}', function($app) { + $app->put('/edit', [Coins::class, 'edit']); + $app->delete('/delete', [Coins::class, 'delete']); + $app->get('[/]', [Coins::class, 'show']); +}); diff --git a/backend/api/resources/routes/api/locations.php b/backend/api/resources/routes/api/locations.php new file mode 100644 index 0000000..36ee342 --- /dev/null +++ b/backend/api/resources/routes/api/locations.php @@ -0,0 +1,13 @@ +group('/locations', function($app) { + $app->post('/add', [Locations::class, 'add']); + $app->get('[/]', Locations::class); +}); + +$app->group('/location/{location_id}', function($app) { + $app->put('/edit', [Locations::class, 'edit']); + $app->delete('/delete', [Locations::class, 'delete']); + $app->get('[/]', [Locations::class, 'show']); +}); diff --git a/backend/api/resources/routes/api/update.php b/backend/api/resources/routes/api/update.php new file mode 100644 index 0000000..3313aa1 --- /dev/null +++ b/backend/api/resources/routes/api/update.php @@ -0,0 +1,7 @@ +group('/update', function($app) { + $app->get('/register/{type:[0,1]}/{coin_id:[\d+]}', [Update::class, 'register']); + $app->get('[/]', Update::class); +}); diff --git a/backend/api/resources/routes/api/wallets.php b/backend/api/resources/routes/api/wallets.php new file mode 100644 index 0000000..f0b0eec --- /dev/null +++ b/backend/api/resources/routes/api/wallets.php @@ -0,0 +1,13 @@ +group('/wallets', function($app) { + $app->post('/add', [Wallets::class, 'add']); + $app->get('[/]', Wallets::class); +}); + +$app->group('/wallet/{wallet_id}', function($app) { + $app->put('/edit', [Wallets::class, 'edit']); + $app->delete('/delete', [Wallets::class, 'delete']); + $app->get('[/]', [Wallets::class, 'show']); +}); diff --git a/backend/api/setup/api/middleware.php b/backend/api/setup/api/middleware.php new file mode 100644 index 0000000..b3d9bbc --- /dev/null +++ b/backend/api/setup/api/middleware.php @@ -0,0 +1 @@ + function() { + $arr = ['base' => dirname(__DIR__, 2)]; + $arr['resources'] = implode(DIRECTORY_SEPARATOR, [ + $arr['base'], + 'resources' + ]); + $arr['data'] = implode(DIRECTORY_SEPARATOR, [ + $arr['resources'], + 'data' + ]); + $arr['routes'] = implode(DIRECTORY_SEPARATOR, [ + $arr['resources'], + 'routes' + ]); + $arr['bin'] = implode(DIRECTORY_SEPARATOR, [ + dirname($arr['base']), + 'automation', + 'bin' + ]); + return (object) $arr; + }, + 'coingecko' => function(Container $c) { + return implode(DIRECTORY_SEPARATOR, [ + $c->get('locations')->bin, + 'coingecko' + ]); + }, + 'mindicador' => function(Container $c) { + return implode(DIRECTORY_SEPARATOR, [ + $c->get('locations')->bin, + 'mindicador' + ]); + } +]; diff --git a/backend/api/setup/api/setups.php b/backend/api/setup/api/setups.php new file mode 100644 index 0000000..98501da --- /dev/null +++ b/backend/api/setup/api/setups.php @@ -0,0 +1,24 @@ + function(Container $container) { + $filename = implode(DIRECTORY_SEPARATOR, [ + $container->get('locations')->data, + 'api.json' + ]); + return new ProVM\Crypto\Common\Service\API($filename); + }, + ProVM\Common\Factory\Model::class => function(Container $container) { + return new ProVM\Crypto\Common\Factory\Model(); + }, + ProVM\Crypto\Common\Service\Update::class => function(Container $container) { + return new ProVM\Crypto\Common\Service\Update( + $container->get(ProVM\Crypto\Common\Factory\Model::class), + [ + $container->get('coingecko'), + $container->get('mindicador') + ] + ); + } +]; diff --git a/backend/api/setup/app.php b/backend/api/setup/app.php new file mode 100644 index 0000000..b7a1770 --- /dev/null +++ b/backend/api/setup/app.php @@ -0,0 +1,55 @@ +addDefinitions($filename); + } +} + +$container = $builder->build(); +$app = Bridge::create($container); +//$app->setBasePath($container->get('base_url')); +$app->add(new ProVM\Crypto\Common\Middleware\CORS()); +$app->addRoutingMiddleware(); + +include_once 'databases.php'; + +foreach ($folders as $folder) { + $filename = implode(DIRECTORY_SEPARATOR, [ + __DIR__, + $folder, + 'middlewares.php' + ]); + if (!file_exists($filename)) { + continue; + } + include_once $filename; +} + +include_once 'router.php'; + +$app->add(new Zeuxisoo\Whoops\Slim\WhoopsMiddleware(['enable' => $container->get('debug') ?: true])); diff --git a/backend/api/setup/composer.php b/backend/api/setup/composer.php new file mode 100644 index 0000000..2d27ee1 --- /dev/null +++ b/backend/api/setup/composer.php @@ -0,0 +1,6 @@ +getContainer()->get(ProVM\Common\Service\Database::class); +$service->load(); diff --git a/backend/api/setup/env/settings.php b/backend/api/setup/env/settings.php new file mode 100644 index 0000000..7c8bbc5 --- /dev/null +++ b/backend/api/setup/env/settings.php @@ -0,0 +1,25 @@ + '', + 'debug' => $_ENV['DEBUG'], + 'databases' => function() { + $settings = [ + 'short_names' => true, + 'dbs' => [] + ]; + $default = [ + 'engine' => 'mysql', + 'host' => (object) [ + 'name' => $_ENV['DB_HOST'], + 'port' => $_ENV['DB_PORT'] ?? null + ], + 'user' => (object) [ + 'name' => $_ENV['MYSQL_USER'], + 'password' => $_ENV['MYSQL_PASSWORD'] + ], + 'name' => $_ENV['MYSQL_DATABASE'] . ($_ENV['ENV'] ? '_' . $_ENV['ENV'] : '') + ]; + $settings['dbs']['default'] = (object) $default; + return (object) $settings; + } +]; diff --git a/backend/api/setup/env/setups.php b/backend/api/setup/env/setups.php new file mode 100644 index 0000000..dcf8821 --- /dev/null +++ b/backend/api/setup/env/setups.php @@ -0,0 +1,8 @@ + function(Container $container) { + return new ProVM\Common\Service\Database($container->get('databases')); + } +]; diff --git a/backend/api/setup/router.php b/backend/api/setup/router.php new file mode 100644 index 0000000..d1d0372 --- /dev/null +++ b/backend/api/setup/router.php @@ -0,0 +1,5 @@ +getContainer()->get('locations')->routes, + 'api.php' +]); diff --git a/backend/api/src/Coin.php b/backend/api/src/Coin.php new file mode 100644 index 0000000..1819aa9 --- /dev/null +++ b/backend/api/src/Coin.php @@ -0,0 +1,36 @@ +prefix == '') { + $output []= $this->prefix; + } + $output []= number_format($value, $this->decimals ?? 0, ',', '.'); + if ($this->suffix == '') { + $output []= $this->suffix; + } + return implode(' ', $output); + } + + public static function find(Factory $factory, $input) { + return $factory->find(Coin::class)->where([['code', $input->code]])->one(); + } +} diff --git a/backend/api/src/Location.php b/backend/api/src/Location.php new file mode 100644 index 0000000..f13ecf7 --- /dev/null +++ b/backend/api/src/Location.php @@ -0,0 +1,14 @@ +coin === null) { + $this->coin = $this->childOf(Coin::class, [Model::SELF_KEY => 'coin_id']); + } + return $this->coin; + } + protected $wallet; + public function wallet() { + if ($this->wallet === null) { + $this->wallet = $this->childOf(Wallet::class, [Model::SELF_KEY => 'wallet_id']); + } + return $this->wallet; + } + protected $unit; + public function unit() { + if ($this->unit === null) { + $this->unit = $this->childOf(Coin::class, [Model::SELF_KEY => 'unit_id']); + } + return $this->unit; + } +} diff --git a/backend/api/src/Value.php b/backend/api/src/Value.php new file mode 100644 index 0000000..3c01f52 --- /dev/null +++ b/backend/api/src/Value.php @@ -0,0 +1,34 @@ +coin === null) { + $this->coin = $this->childOf(Coin::class, [Model::SELF_KEY => 'coin_id']); + } + return $this->coin; + } + protected $unit; + public function unit() { + if ($this->unit === null) { + $this->unit = $this->childOf(Coin::class, [Model::SELF_KEY => 'unit_id']); + } + return $this->unit; + } +} diff --git a/backend/api/src/Wallet.php b/backend/api/src/Wallet.php new file mode 100644 index 0000000..1ef43b4 --- /dev/null +++ b/backend/api/src/Wallet.php @@ -0,0 +1,29 @@ +location === null) { + $this->location = $this->childOf(Location::class, [Model::SELF_KEY => 'location_id']); + } + return $this->location; + } + public function setLocation(Location $location) { + if ($location->name == $this->location->name) { + return; + } + $this->location = $location; + } +} diff --git a/backend/automation/bin/coingecko b/backend/automation/bin/coingecko new file mode 100644 index 0000000..2513233 Binary files /dev/null and b/backend/automation/bin/coingecko differ diff --git a/backend/automation/bin/mindicador b/backend/automation/bin/mindicador new file mode 100644 index 0000000..3c002b8 Binary files /dev/null and b/backend/automation/bin/mindicador differ diff --git a/backend/automation/crontab b/backend/automation/crontab new file mode 100644 index 0000000..59446c8 --- /dev/null +++ b/backend/automation/crontab @@ -0,0 +1 @@ +0 2 * * * curl backend/update diff --git a/backend/docker-compose.yml b/backend/docker-compose.yml new file mode 100644 index 0000000..477a8e5 --- /dev/null +++ b/backend/docker-compose.yml @@ -0,0 +1,31 @@ +version: '3' +services: + web: + image: jwilder/nginx-proxy + ports: + - "8086:80" + - "4430:443" + volumes: + - //var/run/docker.sock:/tmp/docker.sock:ro + - ./nginx.conf:/etc/nginx/conf.d/nginx.conf + - ./proxy.conf:/etc/nginx/proxy.conf + - ./api:/app + - ./certs:/etc/nginx/certs + php: + build: + context: . + dockerfile: PHP.Dockerfile + volumes: + - ./api:/app + db: + image: mariadb:latest + env_file: .db.env + volumes: + - mysqldata:/var/lib/mysql + adminer: + image: adminer:alpine + env_file: .db.env + ports: + - 8087:8080 +volumes: + mysqldata: {} diff --git a/backend/nginx.conf b/backend/nginx.conf new file mode 100644 index 0000000..6cb397a --- /dev/null +++ b/backend/nginx.conf @@ -0,0 +1,29 @@ +server { + listen ${BACKEND_PORT} default_server; + root /app/backend/api/public; + + access_log /var/log/nginx/backend.access.log; + error_log /var/log/nginx/backend.error.log; + + index index.php index.html index.htm; + + add_header 'Access-Control-Allow-Origin' '*'; + add_header 'Access-Control-Allow-Headers' 'Authorization,Accept,Origin,DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Range,Range'; + add_header 'Access-Control-Allow-Methods' 'GET,POST,OPTIONS,PUT,DELETE,PATCH'; + + location / { + try_files $uri /index.php$is_args$args; + } + + location ~ \.php$ { + fastcgi_pass backend:9000; + fastcgi_split_path_info ^(.+\.php)(/.+)$; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + fastcgi_param SCRIPT_NAME $fastcgi_script_name; + fastcgi_param PATH_INFO $fastcgi_path_info; + fastcgi_index index.php; + include fastcgi_params; + + include /app/backend/proxy.conf; + } +} diff --git a/backend/proxy.conf b/backend/proxy.conf new file mode 100644 index 0000000..d4dfd11 --- /dev/null +++ b/backend/proxy.conf @@ -0,0 +1,19 @@ +# HTTP 1.1 support +proxy_http_version 1.1; +proxy_buffering off; +proxy_set_header Host $http_host; +proxy_set_header Upgrade $http_upgrade; +#proxy_set_header Connection $proxy_connection; +proxy_set_header X-Real-IP $remote_addr; +proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; +#proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto; +#proxy_set_header X-Forwarded-Ssl $proxy_x_forwarded_ssl; +#proxy_set_header X-Forwarded-Port $proxy_x_forwarded_port; + +# Mitigate httpoxy attack +proxy_set_header Proxy ""; + +# Custom Values Higher Buffer Size +proxy_buffer_size 128k; +proxy_buffers 4 256k; +proxy_busy_buffers_size 256k; diff --git a/backend/python/__pycache__/coingecko.cpython-39.pyc b/backend/python/__pycache__/coingecko.cpython-39.pyc new file mode 100644 index 0000000..0e76043 Binary files /dev/null and b/backend/python/__pycache__/coingecko.cpython-39.pyc differ diff --git a/backend/python/__pycache__/miindicador.cpython-39.pyc b/backend/python/__pycache__/miindicador.cpython-39.pyc new file mode 100644 index 0000000..3a00642 Binary files /dev/null and b/backend/python/__pycache__/miindicador.cpython-39.pyc differ diff --git a/backend/python/coingecko.py b/backend/python/coingecko.py new file mode 100644 index 0000000..53c9ba3 --- /dev/null +++ b/backend/python/coingecko.py @@ -0,0 +1,90 @@ +import argparse +import httpx +import json +import datetime + + +class CoinGecko: + def __init__(self, base_url: str = None): + if base_url is None: + base_url = 'https://api.coingecko.com/api/v3' + self.base_url = base_url + + def __build_url(self, sub_url: str, query: str = ''): + sub = sub_url + if query != '': + sub = '?'.join([ + sub, + query + ]) + url = '/'.join([ + self.base_url, + sub + ]) + return url + + def __get(self, url: str): + resp = httpx.get(url) + if resp.status_code != httpx.codes.OK: + raise Exception(resp.reason_phrase) + return json.loads(resp.text) + + def list(self): + url = self.__build_url('coins/list') + return self.__get(url) + + def get(self, ids: tuple, currencies: tuple, last_updated: bool = True): + sub = 'simple/price' + query = '&'.join([ + '='.join(['ids', ','.join(ids)]), + '='.join(['vs_currencies', ','.join(currencies)]), + '='.join(['include_last_updated_at', 'true' if last_updated else 'false']) + ]) + url = self.__build_url(sub, query) + res = self.__get(url) + for k, d in res.items(): + res[k]['last_updated_at'] = datetime.datetime.fromtimestamp(d['last_updated_at'])\ + .strftime('%Y-%m-%d %H:%M:%S.%f%z') + return res + + def historical(self, id_: str, currency: str, from_: str, to: str): + sub = '/'.join([ + 'coins', + id_, + 'market_chart', + 'range' + ]) + query = '&'.join([ + '='.join(['vs_currency', currency]), + '='.join(['from', from_]), + '='.join(['to', to]) + ]) + url = self.__build_url(sub, query) + res = self.__get(url) + return res + + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument('-u', '--url') + parser.add_argument('-i', '--ids', type=str) + parser.add_argument('-c', '--currencies', type=str) + hist = parser.add_subparsers() + hparser = hist.add_parser('hist') + hparser.add_argument('-hi', '--historical', action='store_true') + hparser.add_argument('-f', '--from_') + hparser.add_argument('-t', '--to') + args = parser.parse_args() + cg = CoinGecko(args.url) + _ids = tuple(args.ids.split(',')) + _currencies = tuple(args.currencies.split(',')) + if 'historical' in args and args.historical: + from_ = args.from_ + if '-' in from_: + from_ = str(datetime.datetime.fromisoformat(from_).timestamp()) + to = args.to + if '-' in to: + to = str(datetime.datetime.fromisoformat(to).timestamp()) + print(cg.historical(id_=_ids[0], currency=_currencies[0], from_=from_, to=to)) + exit() + print(cg.get(ids=_ids, currencies=_currencies)) diff --git a/backend/python/environment.yml b/backend/python/environment.yml new file mode 100644 index 0000000..544e187 --- /dev/null +++ b/backend/python/environment.yml @@ -0,0 +1,10 @@ +name: cryptos +channels: + - defaults +dependencies: + - httpx=0.17.1 + - pip=21.1.2 + - python=3.9.5 + - setuptools=52.0.0 +# - pip: +# - pyinstaller==4.3 diff --git a/backend/python/miindicador.py b/backend/python/miindicador.py new file mode 100644 index 0000000..e997d5e --- /dev/null +++ b/backend/python/miindicador.py @@ -0,0 +1,72 @@ +import argparse +import httpx +import json +import datetime + + +class MiIndicador: + def __init__(self, base_url: str = None): + if base_url is None: + base_url = 'https://mindicador.cl/api' + self.base_url = base_url + + def __build_url(self, sub_url: str, query: str = ''): + sub = sub_url + if query != '': + sub = '?'.join([ + sub, + query + ]) + url = '/'.join([ + self.base_url, + sub + ]) + return url + + def __get(self, url: str): + resp = httpx.get(url) + if resp.status_code != httpx.codes.OK: + raise Exception(resp.reason_phrase) + return json.loads(resp.text) + + def list(self): + url = self.__build_url('') + return self.__get(url) + + def get(self, indicador: str, fecha: str = None): + url = indicador + if fecha is not None: + url = '/'.join([url, fecha]) + url = self.__build_url(url) + res = self.__get(url) + for i, item in enumerate(res['serie']): + res['serie'][i]['fecha'] = datetime.datetime.fromisoformat(item['fecha'].replace('T', ' ').replace('Z', ''))\ + .strftime('%Y-%m-%d') + return res + + def historical(self, indicador: str, since: str = None): + sub = indicador + if since is not None: + sub = '/'.join([sub, since]) + url = self.__build_url(sub) + res = self.__get(url) + for i, item in enumerate(res['serie']): + res['serie'][i]['fecha'] = datetime.datetime.fromisoformat(item['fecha'].replace('T', ' ').replace('Z', ''))\ + .strftime('%Y-%m-%d') + return res + + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument('-u', '--url') + parser.add_argument('-i', '--indicador') + hist = parser.add_subparsers() + hparser = hist.add_parser('hist') + hparser.add_argument('-hi', '--historical', action='store_true') + hparser.add_argument('-s', '--since') + args = parser.parse_args() + mi = MiIndicador(args.url) + if 'historical' in args and args.historical: + print(mi.historical(args.indicador, args.since)) + exit() + print(mi.get(args.indicador)) diff --git a/backend/python/requirements.txt b/backend/python/requirements.txt new file mode 100644 index 0000000..5f4c202 --- /dev/null +++ b/backend/python/requirements.txt @@ -0,0 +1,30 @@ +# This file may be used to create an environment using: +# $ conda create --name --file +# platform: win-64 +altgraph=0.17=pypi_0 +ca-certificates=2021.5.25=haa95532_1 +certifi=2021.5.30=py39haa95532_0 +future=0.18.2=pypi_0 +h11=0.12.0=pyhd3eb1b0_0 +h2=4.0.0=py39haa95532_3 +hpack=4.0.0=py_0 +httpcore=0.12.3=pyhd3eb1b0_0 +httpx=0.17.1=pyhd3eb1b0_0 +hyperframe=6.0.1=pyhd3eb1b0_0 +idna=2.10=pyhd3eb1b0_0 +openssl=1.1.1k=h2bbff1b_0 +pefile=2021.5.24=pypi_0 +pip=21.1.2=py39haa95532_0 +pyinstaller=4.3=pypi_0 +pyinstaller-hooks-contrib=2021.1=pypi_0 +python=3.9.5=h6244533_3 +pywin32-ctypes=0.2.0=pypi_0 +rfc3986=1.4.0=py_0 +setuptools=52.0.0=py39haa95532_0 +sniffio=1.2.0=py39haa95532_1 +sqlite=3.35.4=h2bbff1b_0 +tzdata=2020f=h52ac0ba_0 +vc=14.2=h21ff451_1 +vs2015_runtime=14.27.29016=h5e58377_2 +wheel=0.36.2=pyhd3eb1b0_0 +wincertstore=0.2=py39h2bbff1b_0 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..228e6b5 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,77 @@ +version: '3' + +services: + proxy: + container_name: crypto-proxy + restart: unless-stopped + image: nginx:latest + ports: + - 8080:${FRONTEND_PORT} + - 8081:${BACKEND_PORT} + volumes: + - .:/app + - ./frontend/nginx.conf:/etc/nginx/templates/default.conf.template + - ./backend/nginx.conf:/etc/nginx/templates/backend.conf.template + - ./logs:/var/log/nginx + env_file: .env + depends_on: + - frontend + - backend + + frontend: + container_name: crypto-frontend + restart: unless-stopped + build: + context: ./frontend + dockerfile: PHP.Dockerfile + env_file: common.env + volumes: + - .:/app + depends_on: + - db + + backend: + container_name: crypto-backend + build: + context: ./backend + dockerfile: PHP.Dockerfile + env_file: + - common.env + - .db.env + volumes: + - .:/app + depends_on: + - db + + # python: + # container_name: python + # build: + # context: ./backend + # dockerfile: Py.Dockerfile + # volumes: + # - ./backend/python:/app/src + # - ./backend/api/bin:/app/bin + # tty: true + # stdin_open: true + + db: + container_name: crypto-db + restart: unless-stopped + image: mariadb:latest + env_file: .db.env + volumes: + - mysqldata:/var/lib/mysql + + adminer: + container_name: crypto-adminer + restart: unless-stopped + image: adminer + depends_on: + - db + ports: + - 8082:8080 + environment: + ADMINER_DESIGN: "dracula" + +volumes: + mysqldata: diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..64fb036 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,6 @@ +# Composer +/vendor/ +composer.lock + +# Cache +**/cache/ diff --git a/frontend/PHP.Dockerfile b/frontend/PHP.Dockerfile new file mode 100644 index 0000000..052d09b --- /dev/null +++ b/frontend/PHP.Dockerfile @@ -0,0 +1,7 @@ +FROM php:7.4-fpm + +RUN docker-php-ext-install pdo pdo_mysql + +RUN pecl install xdebug && docker-php-ext-enable xdebug + +WORKDIR /app/frontend diff --git a/frontend/common/Controller/Coins.php b/frontend/common/Controller/Coins.php new file mode 100644 index 0000000..9c8e47b --- /dev/null +++ b/frontend/common/Controller/Coins.php @@ -0,0 +1,15 @@ +render($response, 'coins.list'); + } + public function get(Request $request, Response $response, View $view, $coin_id): Response { + return $view->render($response, 'coins.show', compact('coin_id')); + } +} diff --git a/frontend/common/Controller/Home.php b/frontend/common/Controller/Home.php new file mode 100644 index 0000000..55767f7 --- /dev/null +++ b/frontend/common/Controller/Home.php @@ -0,0 +1,12 @@ +render($response, 'home'); + } +} diff --git a/frontend/composer.json b/frontend/composer.json new file mode 100644 index 0000000..288caf1 --- /dev/null +++ b/frontend/composer.json @@ -0,0 +1,44 @@ +{ + "name": "provm/crypto-ui", + "description": "Crypto currency UI", + "type": "project", + "require": { + "slim/slim": "^4.7", + "rubellum/slim-blade-view": "^0.1.1", + "nyholm/psr7": "^1.4", + "nyholm/psr7-server": "^1.0", + "php-di/slim-bridge": "^3.1", + "zeuxisoo/slim-whoops": "^0.7.3", + "provm/models": "dev-master", + "vlucas/phpdotenv": "^5.3", + "spatie/crypto": "^2.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.5", + "kint-php/kint": "^3.3" + }, + "license": "MIT", + "authors": [ + { + "name": "Aldarien", + "email": "aldarien85@gmail.com" + } + ], + "autoload": { + "psr-4": { + "ProVM\\Crypto\\Common\\": "common", + "ProVM\\Crypto\\": "../src", + "ProVM\\Common\\": "../provm/common", + "ProVM\\": "../provm/src" + } + }, + "repositories": [ + { + "type": "git", + "url": "http://git.provm.cl/ProVM/models.git" + } + ], + "config": { + "secure-http": false + } +} diff --git a/frontend/nginx.conf b/frontend/nginx.conf new file mode 100644 index 0000000..871cfe6 --- /dev/null +++ b/frontend/nginx.conf @@ -0,0 +1,22 @@ +server { + listen ${FRONTEND_PORT} default_server; + root /app/frontend/public; + + access_log /var/log/nginx/frontend.access.log; + error_log /var/log/nginx/frontend.error.log; + + index index.php index.html index.htm; + + location / { + try_files $uri /index.php$is_args$args; + } + + location ~ \.php$ { + fastcgi_pass frontend:9000; + fastcgi_split_path_info ^(.+\.php)(/.+)$; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + fastcgi_param SCRIPT_NAME $fastcgi_script_name; + fastcgi_index index.php; + include fastcgi_params; + } +} diff --git a/frontend/public/.htaccess b/frontend/public/.htaccess new file mode 100644 index 0000000..e69de29 diff --git a/frontend/public/index.php b/frontend/public/index.php new file mode 100644 index 0000000..52d1e18 --- /dev/null +++ b/frontend/public/index.php @@ -0,0 +1,9 @@ +run(); diff --git a/frontend/resources/routes/web.php b/frontend/resources/routes/web.php new file mode 100644 index 0000000..d7361c0 --- /dev/null +++ b/frontend/resources/routes/web.php @@ -0,0 +1,12 @@ +isDir()) { + continue; + } + include $file->getRealPath(); +} + +$app->get('/', Home::class); diff --git a/frontend/resources/routes/web/coins.php b/frontend/resources/routes/web/coins.php new file mode 100644 index 0000000..45cd662 --- /dev/null +++ b/frontend/resources/routes/web/coins.php @@ -0,0 +1,11 @@ +group('/coins', function($app) { + $app->get('/add', [Coins::class, 'add']); + $app->get('[/]', Coins::class); +}); + +$app->group('/coin/{coin_id}', function($app) { + $app->get('[/]', [Coins::class, 'get']); +}); diff --git a/frontend/resources/views/coins/base.blade.php b/frontend/resources/views/coins/base.blade.php new file mode 100644 index 0000000..8eea865 --- /dev/null +++ b/frontend/resources/views/coins/base.blade.php @@ -0,0 +1,5 @@ +@extends('layout.base') + +@section('page_title') + Coins +@endsection diff --git a/frontend/resources/views/coins/list.blade.php b/frontend/resources/views/coins/list.blade.php new file mode 100644 index 0000000..3dec2af --- /dev/null +++ b/frontend/resources/views/coins/list.blade.php @@ -0,0 +1,264 @@ +@extends('coins.base') + +@section('page_content') +

Monedas

+ + + + + + + + + +
+ Moneda + + Código + + +
+ +@endsection + +@push('page_scripts') + +@endpush diff --git a/frontend/resources/views/coins/show.blade.php b/frontend/resources/views/coins/show.blade.php new file mode 100644 index 0000000..c49c15b --- /dev/null +++ b/frontend/resources/views/coins/show.blade.php @@ -0,0 +1,31 @@ +@extends('coins.base') + +@section('page_content') +

Moneda -

+
+
+@endsection + +@push('page_scripts') + +@endpush diff --git a/frontend/resources/views/home.blade.php b/frontend/resources/views/home.blade.php new file mode 100644 index 0000000..8c9b10e --- /dev/null +++ b/frontend/resources/views/home.blade.php @@ -0,0 +1,5 @@ +@extends('layout.base') + +@section('page_content') + Home +@endsection diff --git a/frontend/resources/views/layout/base.blade.php b/frontend/resources/views/layout/base.blade.php new file mode 100644 index 0000000..9deb8c1 --- /dev/null +++ b/frontend/resources/views/layout/base.blade.php @@ -0,0 +1,5 @@ + + +@include('layout.head') +@include('layout.body') + diff --git a/frontend/resources/views/layout/body.blade.php b/frontend/resources/views/layout/body.blade.php new file mode 100644 index 0000000..d4dd7b4 --- /dev/null +++ b/frontend/resources/views/layout/body.blade.php @@ -0,0 +1,16 @@ + + @include('layout.body.header') +
+ @hasSection('page_sidebar') + +
+ @endif + @yield('page_content') + @hasSection('page_sidebar') +
+ @endif +
+ @include('layout.body.footer') + diff --git a/frontend/resources/views/layout/body/footer.blade.php b/frontend/resources/views/layout/body/footer.blade.php new file mode 100644 index 0000000..c3b1007 --- /dev/null +++ b/frontend/resources/views/layout/body/footer.blade.php @@ -0,0 +1,4 @@ +
+ +
+@include('layout.body.scripts') diff --git a/frontend/resources/views/layout/body/header.blade.php b/frontend/resources/views/layout/body/header.blade.php new file mode 100644 index 0000000..9432a2f --- /dev/null +++ b/frontend/resources/views/layout/body/header.blade.php @@ -0,0 +1,6 @@ + diff --git a/frontend/resources/views/layout/body/scripts.blade.php b/frontend/resources/views/layout/body/scripts.blade.php new file mode 100644 index 0000000..d40ee96 --- /dev/null +++ b/frontend/resources/views/layout/body/scripts.blade.php @@ -0,0 +1,15 @@ +@if (isset($page_scripts)) + @foreach ($page_scripts as $script) + @if (isset($script->url)) + + @elseif (isset($script->full)) + {!!$script->full!!} + @endif + @endforeach +@endif + +@stack('page_scripts') + + diff --git a/frontend/resources/views/layout/head.blade.php b/frontend/resources/views/layout/head.blade.php new file mode 100644 index 0000000..844934f --- /dev/null +++ b/frontend/resources/views/layout/head.blade.php @@ -0,0 +1,11 @@ + + + + Crypto + @hasSection('page_title') + - + @yield('page_title') + @endif + + @include('layout.head.styles') + diff --git a/frontend/resources/views/layout/head/styles.blade.php b/frontend/resources/views/layout/head/styles.blade.php new file mode 100644 index 0000000..f712ee3 --- /dev/null +++ b/frontend/resources/views/layout/head/styles.blade.php @@ -0,0 +1,11 @@ +@if (isset($page_styles)) + @foreach ($page_styles as $style) + @if (isset($style->url)) + + @elseif (isset($style->link)) + {!!$style->link!!} + @endif + @endforeach +@endif + +@stack('page_styles') diff --git a/frontend/setup/app.php b/frontend/setup/app.php new file mode 100644 index 0000000..9572044 --- /dev/null +++ b/frontend/setup/app.php @@ -0,0 +1,52 @@ +addDefinitions($filename); + } +} + +$container = $builder->build(); +$app = Bridge::create($container); +//$app->setBasePath($container->get('base_url')); +$app->addRoutingMiddleware(); + +foreach ($folders as $folder) { + $filename = implode(DIRECTORY_SEPARATOR, [ + __DIR__, + $folder, + 'middlewares.php' + ]); + if (!file_exists($filename)) { + continue; + } + include_once $filename; +} + +include_once 'router.php'; + +$app->add(new Zeuxisoo\Whoops\Slim\WhoopsMiddleware(['enable' => $container->get('debug') ?: true])); diff --git a/frontend/setup/common/settings.php b/frontend/setup/common/settings.php new file mode 100644 index 0000000..d0d3242 --- /dev/null +++ b/frontend/setup/common/settings.php @@ -0,0 +1,9 @@ + function() { + if (isset($_ENV['BASE_URL'])) { + return $_ENV['BASE_URL']; + } + return $_SERVER['REQUEST_SCHEME'] . '://' . $_SERVER['HTTP_HOST']; + } +]; diff --git a/frontend/setup/composer.php b/frontend/setup/composer.php new file mode 100644 index 0000000..2d27ee1 --- /dev/null +++ b/frontend/setup/composer.php @@ -0,0 +1,6 @@ + $_ENV['DEBUG'] ?? false +]; diff --git a/frontend/setup/router.php b/frontend/setup/router.php new file mode 100644 index 0000000..2c5b8c5 --- /dev/null +++ b/frontend/setup/router.php @@ -0,0 +1,5 @@ +getContainer()->get('locations')->routes, + 'web.php' +]); diff --git a/frontend/setup/web/settings.php b/frontend/setup/web/settings.php new file mode 100644 index 0000000..a330cf5 --- /dev/null +++ b/frontend/setup/web/settings.php @@ -0,0 +1,68 @@ + 5*60*60, + 'locations' => function() { + $arr = ['base' => dirname(__DIR__, 2)]; + $arr['resources'] = implode(DIRECTORY_SEPARATOR, [ + $arr['base'], + 'resources' + ]); + $arr['data'] = implode(DIRECTORY_SEPARATOR, [ + $arr['resources'], + 'data' + ]); + $arr['routes'] = implode(DIRECTORY_SEPARATOR, [ + $arr['resources'], + 'routes' + ]); + $arr['templates'] = implode(DIRECTORY_SEPARATOR, [ + $arr['resources'], + 'views' + ]); + $arr['cache'] = implode(DIRECTORY_SEPARATOR, [ + $arr['base'], + 'cache' + ]); + return (object) $arr; + }, + 'urls' => function(Container $c) { + $arr = ['base' => $c->get('base_url'), 'api' => $_ENV['API_URL']]; + return (object) $arr; + }, + 'scripts' => function() { + $arr = [ + [ + 'full' => '' + ], + [ + 'full' => '' + ] + ]; + foreach ($arr as $i => $a) { + $arr[$i] = (object) $a; + } + return $arr; + }, + 'styles' => function() { + $arr = [ + [ + 'link' => '' + ], + [ + 'url' => 'https://cdnjs.cloudflare.com/ajax/libs/fomantic-ui/2.8.7/themes/default/assets/fonts/brand-icons.woff2' + ], + [ + 'url' => 'https://cdnjs.cloudflare.com/ajax/libs/fomantic-ui/2.8.7/themes/default/assets/fonts/icons.woff2' + ], + [ + 'url' => 'https://cdnjs.cloudflare.com/ajax/libs/fomantic-ui/2.8.7/themes/default/assets/fonts/outline-icons.woff2' + ] + ]; + foreach ($arr as $i => $a) { + $arr[$i] = (object) $a; + } + return $arr; + } +]; diff --git a/frontend/setup/web/setups.php b/frontend/setup/web/setups.php new file mode 100644 index 0000000..423d622 --- /dev/null +++ b/frontend/setup/web/setups.php @@ -0,0 +1,17 @@ + function(Container $container) { + return new Slim\Views\Blade( + $container->get('locations')->templates, + $container->get('locations')->cache, + null, + [ + 'urls' => $container->get('urls'), + 'page_styles' => $container->get('styles'), + 'page_scripts' => $container->get('scripts') + ] + ); + } +]; diff --git a/provm/Migrator.md b/provm/Migrator.md new file mode 100644 index 0000000..ff8f2b8 --- /dev/null +++ b/provm/Migrator.md @@ -0,0 +1,20 @@ +# Migrator + +## Needs + ++ To detect changes in migration schema and create migrations with Phinx ++ Keep track of incremental changes in schema ++ Create migration for Phinx + +## Usage + +Check for changes: +vendor/bin/migrator check + +Create migrations: +vendor/bin/migrator create + +Check the create migrations and then: +vendor/bin/migrator migrate +or +vendor/bin/phinx migrate diff --git a/provm/common/Define/Controller/JSON.php b/provm/common/Define/Controller/JSON.php new file mode 100644 index 0000000..fa382e8 --- /dev/null +++ b/provm/common/Define/Controller/JSON.php @@ -0,0 +1,13 @@ +getBody()->write(json_encode($data)); + return $response + ->withHeader('Content-Type', 'application/json') + ->withStatus($status_code); + } +} diff --git a/provm/common/Define/Model/Date.php b/provm/common/Define/Model/Date.php new file mode 100644 index 0000000..cf68bbb --- /dev/null +++ b/provm/common/Define/Model/Date.php @@ -0,0 +1,13 @@ +date); + } + $this->date = $date->format('Y-m-d'); + } +} diff --git a/provm/common/Define/Model/DateTime.php b/provm/common/Define/Model/DateTime.php new file mode 100644 index 0000000..2b84129 --- /dev/null +++ b/provm/common/Define/Model/DateTime.php @@ -0,0 +1,13 @@ +date_time); + } + $this->date_time = $date_time->format('Y-m-d H:i:s'); + } +} diff --git a/provm/common/Define/Model/Time.php b/provm/common/Define/Model/Time.php new file mode 100644 index 0000000..8856e32 --- /dev/null +++ b/provm/common/Define/Model/Time.php @@ -0,0 +1,13 @@ +time); + } + $this->time = $time->format('H:i:s'); + } +} diff --git a/provm/common/Middleware/Auth.php b/provm/common/Middleware/Auth.php new file mode 100644 index 0000000..eaf0f97 --- /dev/null +++ b/provm/common/Middleware/Auth.php @@ -0,0 +1,23 @@ +factory = $factory; + } + public function __invoke(Request $request, Handler $handler): Response { + $response = $handler->handle($request); + if ($this->service->isLoggedIn()) { + return $response; + } + $content = $response->getBody(); + $response = $factory->createResponse(200)->withBody($content); + return $response; + } +} diff --git a/provm/common/Service/Database.php b/provm/common/Service/Database.php new file mode 100644 index 0000000..f5e4bb6 --- /dev/null +++ b/provm/common/Service/Database.php @@ -0,0 +1,30 @@ +settings = $settings; + } + public function load() { + foreach ($this->settings->dbs as $name => $data) { + switch (strtolower($data->engine)) { + case 'mysql': + $obj = new MySQL($data); + break; + } + ORM::configure($obj->dsn(), null, $name); + if ($obj->hasUser()) { + ORM::configure('username', $data->user->name, $name); + ORM::configure('password', $data->user->password, $name); + } + } + if (isset($this->settings->short_names)) { + Model::$short_table_names = $this->settings->short_names; + } + } +} diff --git a/provm/common/Service/Database/DSN.php b/provm/common/Service/Database/DSN.php new file mode 100644 index 0000000..2a099f4 --- /dev/null +++ b/provm/common/Service/Database/DSN.php @@ -0,0 +1,25 @@ +engine = $engine; + } + public $pairs; + public function addPair($name, $value) { + if ($this->pairs === null) { + $this->pairs = []; + } + $this->pairs []= [$name, $value]; + return $this; + } + public function __toString() { + return implode(':', [ + $this->engine, + implode(';', array_map(function($item) { + return implode('=', $item); + }, $this->pairs)) + ]); + } +} diff --git a/provm/common/Service/Database/MySQL.php b/provm/common/Service/Database/MySQL.php new file mode 100644 index 0000000..2f142ce --- /dev/null +++ b/provm/common/Service/Database/MySQL.php @@ -0,0 +1,21 @@ +settings = $settings; + } + public function dsn(): string { + $dsn = (new DSN($this->settings->engine)) + ->addPair('host', $this->settings->host->name); + if (isset($this->settings->host->port)) { + $dsn->addPair('port', $this->settings->host->port); + } + $dsn->addPair('dbname', $this->settings->name); + return '' . $dsn; + } + public function hasUser(): bool { + return true; + } +} diff --git a/provm/common/Service/Migrator.php b/provm/common/Service/Migrator.php new file mode 100644 index 0000000..5e728cd --- /dev/null +++ b/provm/common/Service/Migrator.php @@ -0,0 +1,19 @@ +schema_filename = $schema_filename; + $this->migrations_folder = $migrations_folder; + $this->seeds_folder = $seeds_folder; + } + protected $schema; + public function schema() { + if ($this->schema === null) { + $file = new \File($this->schema_filename); + } + } +} diff --git a/provm/src/Login.php b/provm/src/Login.php new file mode 100644 index 0000000..34e66ae --- /dev/null +++ b/provm/src/Login.php @@ -0,0 +1,22 @@ +user === null) { + $this->user = $this->childOf(User::class, [Model::SELF_KEY => 'user_id']); + } + return $this->user; + } +} diff --git a/provm/src/User.php b/provm/src/User.php new file mode 100644 index 0000000..3b26052 --- /dev/null +++ b/provm/src/User.php @@ -0,0 +1,15 @@ +password, $password); + } +}