This commit is contained in:
2021-06-28 23:15:13 -04:00
parent 0061a3d920
commit f4a8db56ff
93 changed files with 2422 additions and 0 deletions

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
# Env files
**/*.env
# Logs
**/logs/

2
backend/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
# Certs
**/certs/

7
backend/PHP.Dockerfile Normal file
View File

@ -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

37
backend/Py.Dockerfile Normal file
View File

@ -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" ]

29
backend/api/Readme.md Normal file
View File

@ -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

View File

@ -0,0 +1,46 @@
<?php
namespace ProVM\Crypto\Common\Controller;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Message\ResponseInterface as Response;
use ProVM\Common\Define\Controller\JSON;
class API {
use JSON;
public function __invoke(Request $request, Response $response): Response {
$output = [
'version' => '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);
}
}

View File

@ -0,0 +1,66 @@
<?php
namespace ProVM\Crypto\Common\Controller;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Message\ResponseInterface as Response;
use ProVM\Common\Define\Controller\JSON;
use ProVM\Common\Factory\Model as ModelFactory;
use ProVM\Crypto\Coin;
class Coins {
use JSON;
public function __invoke(Request $request, Response $response, ModelFactory $factory): Response {
$coins = $factory->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);
}
}

View File

@ -0,0 +1,70 @@
<?php
namespace ProVM\Crypto\Common\Controller;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Message\ResponseInterface as Response;
use ProVM\Common\Define\Controller\JSON;
use ProVM\Common\Factory\Model as ModelFactory;
use ProVM\Crypto\Location;
class Locations {
use JSON;
public function __invoke(Request $request, Response $response, ModelFactory $factory): Response {
$locations = $factory->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);
}
}

View File

@ -0,0 +1,27 @@
<?php
namespace ProVM\Crypto\Common\Controller;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Message\ResponseInterface as Response;
use ProVM\Common\Define\Controller\JSON;
use ProVM\Crypto\Common\Service\Update as Updater;
class Update {
use JSON;
public function __invoke(Request $request, Response $response, Updater $updater) {
$result = $updater->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);
}
}

View File

@ -0,0 +1,71 @@
<?php
namespace ProVM\Crypto\Common\Controller;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Message\ResponseInterface as Response;
use ProVM\Common\Define\Controller\JSON;
use ProVM\Common\Factory\Model as ModelFactory;
use ProVM\Crypto\Wallet;
class Wallets {
use JSON;
public function __invoke(Request $request, Response $response, ModelFactory $factory): Response {
$wallets = $factory->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);
}
}

View File

@ -0,0 +1,7 @@
<?php
namespace ProVM\Crypto\Common\Factory;
use ProVM\Common\Factory\Model as BaseFactory;
class Model extends BaseFactory {
}

View File

@ -0,0 +1,38 @@
<?php
namespace ProVM\Crypto\Common\Middleware;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface as Handler;
use Slim\Routing\RouteContext;
final class CORS implements MiddlewareInterface
{
/**
* Invoke middleware.
*
* @param ServerRequestInterface $request The request
* @param RequestHandlerInterface $handler The handler
*
* @return ResponseInterface The response
*/
public function process(Request $request, Handler $handler): Response {
$routeContext = RouteContext::fromRequest($request);
$routingResults = $routeContext->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;
}
}

View File

@ -0,0 +1,49 @@
<?php
namespace ProVM\Crypto\Common\Service;
use Carbon\Carbon;
use ProVM\Common\Factory\Model as ModelFactory;
use ProVM\Crypt\User;
use ProVM\Crypt\Login;
class Auth {
protected $login_time;
public function __construct(int $login_time) {
$this->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();
}
}

View File

@ -0,0 +1,60 @@
<?php
namespace ProVM\Crypto\Common\Service;
use Carbon\Carbon;
use ProVM\Common\Factory\Model as Factory;
use ProVM\Crypto\Coin;
class Update {
protected $factory;
protected $execs;
public function __construct(Factory $factory, array $executables) {
$this->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() {
}
}

44
backend/api/composer.json Normal file
View File

@ -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
}
}

View File

@ -0,0 +1,73 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class CreateTables extends AbstractMigration
{
/**
* Change Method.
*
* Write your reversible migrations using this method.
*
* More information on writing migrations is available here:
* https://book.cakephp.org/phinx/0/en/migrations.html#the-change-method
*
* Remember to call "create()" or "update()" and NOT "save()" when working
* with the Table class.
*/
public function change(): void
{
$this->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();
}
}

View File

@ -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

41
backend/api/phinx.php Normal file
View File

@ -0,0 +1,41 @@
<?php
return
[
'paths' => [
'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'
];

View File

View File

@ -0,0 +1,9 @@
<?php
session_start();
include_once implode(DIRECTORY_SEPARATOR, [
dirname(__DIR__),
'setup',
'app.php'
]);
$app->run();

View File

@ -0,0 +1,12 @@
{
"version": "1.0.0",
"routes": [
{
"alias": [
"/",
"/help"
],
"description": "API help"
}
]
}

View File

@ -0,0 +1,15 @@
<?php
use ProVM\Crypto\Common\Controller\API;
$folder = __DIR__ . DIRECTORY_SEPARATOR . 'api';
if (file_exists($folder)) {
$files = new DirectoryIterator($folder);
foreach ($files as $file) {
if ($file->getExtension() != 'php') {
continue;
}
include_once $file->getRealPath();
}
}
$app->get('[/]', API::class);

View File

@ -0,0 +1,13 @@
<?php
use ProVM\Crypto\Common\Controller\Coins;
$app->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']);
});

View File

@ -0,0 +1,13 @@
<?php
use ProVM\Crypto\Common\Controller\Locations;
$app->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']);
});

View File

@ -0,0 +1,7 @@
<?php
use ProVM\Crypto\Common\Controller\Update;
$app->group('/update', function($app) {
$app->get('/register/{type:[0,1]}/{coin_id:[\d+]}', [Update::class, 'register']);
$app->get('[/]', Update::class);
});

View File

@ -0,0 +1,13 @@
<?php
use ProVM\Crypto\Common\Controller\Wallets;
$app->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']);
});

View File

@ -0,0 +1 @@
<?php

View File

@ -0,0 +1,38 @@
<?php
use Psr\Container\ContainerInterface as Container;
return [
'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['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'
]);
}
];

View File

@ -0,0 +1,24 @@
<?php
use Psr\Container\ContainerInterface as Container;
return [
ProVM\Crypto\Common\Service\API::class => 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')
]
);
}
];

55
backend/api/setup/app.php Normal file
View File

@ -0,0 +1,55 @@
<?php
use DI\ContainerBuilder as Builder;
use DI\Bridge\Slim\Bridge;
include_once 'composer.php';
$builder = new Builder();
$folders = [
'env',
'common',
'api'
];
$files = [
'settings',
'setups'
];
foreach ($files as $file) {
foreach ($folders as $folder) {
$filename = implode(DIRECTORY_SEPARATOR, [
__DIR__,
$folder,
$file . '.php'
]);
if (!file_exists($filename)) {
continue;
}
$builder->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]));

View File

@ -0,0 +1,6 @@
<?php
include_once implode(DIRECTORY_SEPARATOR, [
dirname(__DIR__),
'vendor',
'autoload.php'
]);

View File

@ -0,0 +1,3 @@
<?php
$service = $app->getContainer()->get(ProVM\Common\Service\Database::class);
$service->load();

25
backend/api/setup/env/settings.php vendored Normal file
View File

@ -0,0 +1,25 @@
<?php
return [
'base_url' => '',
'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;
}
];

8
backend/api/setup/env/setups.php vendored Normal file
View File

@ -0,0 +1,8 @@
<?php
use Psr\Container\ContainerInterface as Container;
return [
ProVM\Common\Service\Database::class => function(Container $container) {
return new ProVM\Common\Service\Database($container->get('databases'));
}
];

View File

@ -0,0 +1,5 @@
<?php
include_once implode(DIRECTORY_SEPARATOR, [
$app->getContainer()->get('locations')->routes,
'api.php'
]);

36
backend/api/src/Coin.php Normal file
View File

@ -0,0 +1,36 @@
<?php
namespace ProVM\Crypto;
use ProVM\Common\Alias\Model;
use ProVM\Common\Factory\Model as Factory;
/**
* @property int $id
* @property string $identifier
* @property string $code
* @property string $name
* @property string $prefix
* @property string $suffix
* @property int $decimals
* @property string $ref_url
*/
class Coin extends Model {
protected static $_table = 'coins';
protected static $fields = ['code', 'name', 'prefix', 'suffix', 'decimals', 'ref_url'];
public function format(float $value): string {
$output = [];
if ($this->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();
}
}

View File

@ -0,0 +1,14 @@
<?php
namespace ProVM\Crypto;
use ProVM\Common\Alias\Model;
/**
* @property int $id
* @property string $name
* @property string $description
*/
class Location extends Model {
protected static $_table = 'locations';
protected static $fields = ['name', 'description'];
}

View File

@ -0,0 +1,43 @@
<?php
namespace ProVM\Crypto;
use ProVM\Common\Alias\Model;
use ProVM\Common\Define\Model\DateTime as DT;
/**
* @property int $id
* @property \DateTime $date_time
* @property Coin $coin_id
* @property Wallet $wallet_id
* @property float $amount
* @property float $value
* @property Coin $unit_id
*/
class Transaction extends Model {
use DT;
protected static $_table = 'transactions';
protected static $fields = ['date_time', 'coin_id', 'wallet_id', 'amount', 'value', 'unit_id'];
protected $coin;
public function coin() {
if ($this->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;
}
}

34
backend/api/src/Value.php Normal file
View File

@ -0,0 +1,34 @@
<?php
namespace ProVM\Crypto;
use ProVM\Common\Alias\Model;
use ProVM\Common\Define\Model\DateTime as DT;
/**
* @property int $id
* @property \DateTime $date_time
* @property Coin $coin_id
* @property float $value
* @property Coin $unit_id
*/
class Value extends Model {
use DT;
public static $_table = 'values';
protected static $fields = ['date_time', 'coin_id', 'value', 'unit_id'];
protected $coin;
public function coin() {
if ($this->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;
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace ProVM\Crypto;
use ProVM\Common\Alias\Model;
/**
* @property int $id
* @property string $name
* @property Location $location_id
* @property string $public_address
*/
class Wallet extends Model {
protected static $_table = 'wallets';
protected static $fields = ['name', 'location_id', 'public_address'];
protected $location;
public function location() {
if ($this->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;
}
}

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1 @@
0 2 * * * curl backend/update

View File

@ -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: {}

29
backend/nginx.conf Normal file
View File

@ -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;
}
}

19
backend/proxy.conf Normal file
View File

@ -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;

Binary file not shown.

Binary file not shown.

View File

@ -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))

View File

@ -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

View File

@ -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))

View File

@ -0,0 +1,30 @@
# This file may be used to create an environment using:
# $ conda create --name <env> --file <this 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

77
docker-compose.yml Normal file
View File

@ -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:

6
frontend/.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
# Composer
/vendor/
composer.lock
# Cache
**/cache/

7
frontend/PHP.Dockerfile Normal file
View File

@ -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

View File

@ -0,0 +1,15 @@
<?php
namespace ProVM\Crypto\Common\Controller;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Message\ResponseInterface as Response;
use Slim\Views\Blade as View;
class Coins {
public function __invoke(Request $request, Response $response, View $view): Response {
return $view->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'));
}
}

View File

@ -0,0 +1,12 @@
<?php
namespace ProVM\Crypto\Common\Controller;
use Psr\Http\Message\ServerRequestInterface as Request;
use PSr\Http\Message\ResponseInterface as Response;
use Slim\Views\Blade as View;
class Home {
public function __invoke(Request $request, Response $response, View $view) {
return $view->render($response, 'home');
}
}

44
frontend/composer.json Normal file
View File

@ -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
}
}

22
frontend/nginx.conf Normal file
View File

@ -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;
}
}

View File

View File

@ -0,0 +1,9 @@
<?php
session_start();
include_once implode(DIRECTORY_SEPARATOR, [
dirname(__DIR__),
'setup',
'app.php'
]);
$app->run();

View File

@ -0,0 +1,12 @@
<?php
use ProVM\Crypto\Common\Controller\Home;
$files = new DirectoryIterator(__DIR__ . DIRECTORY_SEPARATOR . 'web');
foreach ($files as $file) {
if ($file->isDir()) {
continue;
}
include $file->getRealPath();
}
$app->get('/', Home::class);

View File

@ -0,0 +1,11 @@
<?php
use ProVM\Crypto\Common\Controller\Coins;
$app->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']);
});

View File

@ -0,0 +1,5 @@
@extends('layout.base')
@section('page_title')
Coins
@endsection

View File

@ -0,0 +1,264 @@
@extends('coins.base')
@section('page_content')
<h3 class="ui basic segment header">Monedas</h3>
<table class="ui striped table" id="coins">
<thead>
<tr>
<th>
Moneda
</th>
<th>
Código
</th>
<th class="right aligned">
<i class="plus icon"></i>
</th>
</tr>
</thead>
<tbody></tbody>
</table>
<div class="ui modal" id="coin_modal">
<div class="header"></div>
<div class="content"></div>
<div class="actions">
<div class="ui approve button"></div>
</div>
</div>
@endsection
@push('page_scripts')
<script type="text/javascript">
function reload() {
window.location.reload()
}
const coins = {
urls: {
api: '{{$urls->api}}',
base: '{{$urls->base}}'
},
modal: null,
setup: function(table, modal) {
this.modal = modal
table.find('plus icon').css('cursor', 'pointer').click(() => {
addCoin()
})
table.find('tbody').append($('<div></div>').attr('class', 'ui active dimmer').append(
$('<div></div>').attr('class', 'ui loader')
))
table.find('.plus.icon').css('cursor', 'pointer').click((e) => {
this.add()
})
this.load(table)
},
load: function(table) {
const url = this.urls.api + '/coins'
const body = table.find('tbody')
$.getJSON(url, (data) => {
body.html('')
data.coins.forEach((v) => {
const u = this.urls.base + '/coin/' + v.id
const tr = $('<tr></tr>').append(
$('<td></td>').append($('<a></a>').attr('href', u).html(v.name))
).append(
$('<td></td>').append($('<a></a>').attr('href', u).html(v.code))
).append(
$('<td></td>').attr('class', 'right aligned').append(
$('<i></i>').attr('class', 'edit icon edit_coin').attr('data-id', v.id)
).append(
$('<i></i>').attr('class', 'minus icon minus_coin').attr('data-id', v.id)
)
)
body.append(tr)
})
$('.edit_coin').css('cursor', 'pointer').click((e) => {
const elem = $(e.target)
const id = elem.attr('data-id')
this.edit(id)
})
$('.minus_coin').css('cursor', 'pointer').click((e) => {
const elem = $(e.target)
const id = elem.attr('data-id')
this.delete(id)
})
}).catch(() => {
body.html('')
})
},
add: function() {
this.modal.find('.header').html('Agregar')
this.modal.find('.actions .approve.button').html('Agregar')
this.modal.find('.content').html('')
const form = $('<form></form>').attr('class', 'ui form').append(
$('<div></div>').attr('class', 'field').append(
$('<label></label>').html('Nombre')
).append(
$('<input />').attr('type', 'text').attr('name', 'name')
)
).append(
$('<div></div>').attr('class', 'field').append(
$('<label></label>').html('Código')
).append(
$('<input />').attr('type', 'text').attr('name', 'code').attr('size', '5').attr('maxlength', '5')
)
).append(
$('<div></div>').attr('class', 'field').append(
$('<label></label>').html('Prefijo')
).append(
$('<input />').attr('type', 'text').attr('name', 'prefix')
)
).append(
$('<div></div>').attr('class', 'field').append(
$('<label></label>').html('Sufijo')
).append(
$('<input />').attr('type', 'text').attr('name', 'suffix')
)
).append(
$('<div></div>').attr('class', 'field').append(
$('<label></label>').html('Decimales')
).append(
$('<input />').attr('type', 'text').attr('name', 'decimals')
)
).append(
$('<div></div>').attr('class', 'field').append(
$('<label></label>').html('Url')
).append(
$('<input />').attr('type', 'text').attr('name', 'ref_url')
)
)
this.modal.find('.content').append(form)
this.modal.modal('show')
this.modal.modal({
onApprove: () => {
const url = this.urls.api + '/coins/add'
const fields = [
'name',
'code',
'prefix',
'suffix',
'decimals',
'ref_url'
]
let data = {}
fields.forEach((k) => {
const val = form.find("[name='" + k + "']").val()
if (val != '') {
data[k] = val
}
})
data = JSON.stringify(data)
$.post(url, data, (response) => {
if (response.created === true) {
return reload()
}
}, 'json')
}
})
},
edit: function(id) {
const url = this.urls.api + '/coin/' + id
$.getJSON(url, (data) => {
const elem = data.coin
this.modal.find('.header').html('Editar')
this.modal.find('.actions .approve.button').html('Editar')
this.modal.find('.content').html('')
const form = $('<form></form>').attr('class', 'ui form').append(
$('<div></div>').attr('class', 'field').append(
$('<label></label>').html('Nombre')
).append(
$('<input />').attr('type', 'text').attr('name', 'name').attr('value', elem.name)
)
).append(
$('<div></div>').attr('class', 'field').append(
$('<label></label>').html('Código')
).append(
$('<input />').attr('type', 'text').attr('name', 'code').attr('size', '5').attr('maxlength', '5').attr('value', elem.code)
)
).append(
$('<div></div>').attr('class', 'field').append(
$('<label></label>').html('Prefijo')
).append(
$('<input />').attr('type', 'text').attr('name', 'prefix').attr('value', elem.prefix)
)
).append(
$('<div></div>').attr('class', 'field').append(
$('<label></label>').html('Sufijo')
).append(
$('<input />').attr('type', 'text').attr('name', 'suffix').attr('value', elem.suffix)
)
).append(
$('<div></div>').attr('class', 'field').append(
$('<label></label>').html('Decimales')
).append(
$('<input />').attr('type', 'text').attr('name', 'decimals').attr('value', elem.decimals)
)
).append(
$('<div></div>').attr('class', 'field').append(
$('<label></label>').html('Url')
).append(
$('<input />').attr('type', 'text').attr('name', 'ref_url').attr('value', elem.ref_url)
)
)
this.modal.find('.content').append(form)
this.modal.modal('show')
this.modal.find('.actions .approve.button').click(() => {
const url = this.urls.api + '/coin/' + id + '/edit'
const fields = [
'name',
'code',
'prefix',
'suffix',
'decimals',
'ref_url'
]
let data = {}
fields.forEach((k) => {
const val = form.find("[name='" + k + "']").val()
if (val != '' && val != elem[k]) {
data[k] = val
}
})
data = JSON.stringify(data)
$.ajax(
{
url: url,
method: 'PUT',
data: data,
success: (response) => {
if (response.edited === true) {
return reload()
}
},
dataType: 'json'
}
)
})
})
},
delete: function(id) {
const url = this.urls.api + '/coin/' + id + '/delete'
$.ajax(
{
url: url,
method: 'DELETE',
success: (response) => {
console.debug(response)
if (response.deleted === true) {
return reload()
}
},
dataType: 'json'
}
)
}
}
$(document).ready(() => {
const modal = $('#coin_modal')
modal.modal()
modal.modal('setting', 'closable', true)
const table = $('#coins')
coins.setup(table, modal)
})
</script>
@endpush

View File

@ -0,0 +1,31 @@
@extends('coins.base')
@section('page_content')
<h3 class="ui header">Moneda - <span class="coin_name"></span></h3>
<div class="ui list" id="coin_data">
</div>
@endsection
@push('page_scripts')
<script type="text/javascript">
$(document).ready(() => {
const id = '{{$coin_id}}'
const api_url = '{{$urls->api}}'
const url = api_url + '/coin/' + id
$.getJSON(url, (data) => {
$('.coin_name').html(data.coin.name)
$('#coin_data').append(
$('<div></div>').attr('class', 'item').html('Código: ' + data.coin.code)
).append(
$('<div></div>').attr('class', 'item').html('Prefijo: ' + data.coin.prefix)
).append(
$('<div></div>').attr('class', 'item').html('Sufijo: ' + data.coin.suffix)
).append(
$('<div></div>').attr('class', 'item').html('Decimales: ' + data.coin.decimals)
).append(
$('<div></div>').attr('class', 'item').html('Url: ' + data.coin.ref_url)
)
})
})
</script>
@endpush

View File

@ -0,0 +1,5 @@
@extends('layout.base')
@section('page_content')
Home
@endsection

View File

@ -0,0 +1,5 @@
<!DOCTYPE html>
<html lang="es">
@include('layout.head')
@include('layout.body')
</html>

View File

@ -0,0 +1,16 @@
<body>
@include('layout.body.header')
<div class="ui attached segment" id="page_content">
@hasSection('page_sidebar')
<div class="ui visible sidebar inverted vertical labeled icon menu" id="page_sidebar">
@yield('page_sidebar')
</div>
<div class="pusher">
@endif
@yield('page_content')
@hasSection('page_sidebar')
</div>
@endif
</div>
@include('layout.body.footer')
</body>

View File

@ -0,0 +1,4 @@
<footer>
</footer>
@include('layout.body.scripts')

View File

@ -0,0 +1,6 @@
<header class="ui top attached menu">
<h1 class="brand item">
<a href="{{$urls->base}}">Cryptos</a>
</h1>
<a class="item" href="{{$urls->base}}/coins">Monedas</a>
</header>

View File

@ -0,0 +1,15 @@
@if (isset($page_scripts))
@foreach ($page_scripts as $script)
@if (isset($script->url))
<script src="{{$script->url}}"></script>
@elseif (isset($script->full))
{!!$script->full!!}
@endif
@endforeach
@endif
@stack('page_scripts')
<script type="text/javascript">
@stack('global_script')
</script>

View File

@ -0,0 +1,11 @@
<head>
<meta charset="utf8" />
<title>
Crypto
@hasSection('page_title')
-
@yield('page_title')
@endif
</title>
@include('layout.head.styles')
</head>

View File

@ -0,0 +1,11 @@
@if (isset($page_styles))
@foreach ($page_styles as $style)
@if (isset($style->url))
<link href="{{$style->url}}" />
@elseif (isset($style->link))
{!!$style->link!!}
@endif
@endforeach
@endif
@stack('page_styles')

52
frontend/setup/app.php Normal file
View File

@ -0,0 +1,52 @@
<?php
use DI\ContainerBuilder as Builder;
use DI\Bridge\Slim\Bridge;
include_once 'composer.php';
$builder = new Builder();
$folders = [
'env',
'common',
'web'
];
$files = [
'settings',
'setups'
];
foreach ($files as $file) {
foreach ($folders as $folder) {
$filename = implode(DIRECTORY_SEPARATOR, [
__DIR__,
$folder,
$file . '.php'
]);
if (!file_exists($filename)) {
continue;
}
$builder->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]));

View File

@ -0,0 +1,9 @@
<?php
return [
'base_url' => function() {
if (isset($_ENV['BASE_URL'])) {
return $_ENV['BASE_URL'];
}
return $_SERVER['REQUEST_SCHEME'] . '://' . $_SERVER['HTTP_HOST'];
}
];

View File

@ -0,0 +1,6 @@
<?php
include_once implode(DIRECTORY_SEPARATOR, [
dirname(__DIR__),
'vendor',
'autoload.php'
]);

4
frontend/setup/env/settings.php vendored Normal file
View File

@ -0,0 +1,4 @@
<?php
return [
'debug' => $_ENV['DEBUG'] ?? false
];

View File

@ -0,0 +1,5 @@
<?php
include_once implode(DIRECTORY_SEPARATOR, [
$app->getContainer()->get('locations')->routes,
'web.php'
]);

View File

@ -0,0 +1,68 @@
<?php
use Psr\Container\ContainerInterface as Container;
return [
'login_time' => 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' => '<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js" integrity="sha512-894YE6QWD5I59HgZOGReFYm4dnWc1Qt5NtvYSaNcOP+u1T9qYdvdihz0PPSiiqn/+/3e7Jo4EaG7TubfWGUrMQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>'
],
[
'full' => '<script src="https://cdnjs.cloudflare.com/ajax/libs/fomantic-ui/2.8.7/semantic.min.js" integrity="sha512-1Nyd5H4Aad+OyvVfUOkO/jWPCrEvYIsQENdnVXt1+Jjc4NoJw28nyRdrpOCyFH4uvR3JmH/5WmfX1MJk2ZlhgQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>'
]
];
foreach ($arr as $i => $a) {
$arr[$i] = (object) $a;
}
return $arr;
},
'styles' => function() {
$arr = [
[
'link' => '<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/fomantic-ui/2.8.7/semantic.min.css" integrity="sha512-g/MzOGVPy3OQ4ej1U+qe4D/xhLwUn5l5xL0Fa7gdC258ZWVJQGwsbIR47SWMpRxSPjD0tfu/xkilTy+Lhrl3xg==" crossorigin="anonymous" referrerpolicy="no-referrer" />'
],
[
'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;
}
];

View File

@ -0,0 +1,17 @@
<?php
use Psr\Container\ContainerInterface as Container;
return [
Slim\Views\Blade::class => 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')
]
);
}
];

20
provm/Migrator.md Normal file
View File

@ -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:
<code>vendor/bin/migrator check</code>
Create migrations:
<code>vendor/bin/migrator create</code>
Check the create migrations and then:
<code>vendor/bin/migrator migrate</code>
or
<code>vendor/bin/phinx migrate</code>

View File

@ -0,0 +1,13 @@
<?php
namespace ProVM\Common\Define\Controller;
use Psr\Http\Message\ResponseInterface as Response;
trait JSON {
public function withJson(Response $response, $data, $status_code = 200) {
$response->getBody()->write(json_encode($data));
return $response
->withHeader('Content-Type', 'application/json')
->withStatus($status_code);
}
}

View File

@ -0,0 +1,13 @@
<?php
namespace ProVM\Common\Define\Model;
use Carbon\Carbon;
trait Date {
public function date(\DateTime $date = null) {
if ($date === null) {
return Carbon::parse($this->date);
}
$this->date = $date->format('Y-m-d');
}
}

View File

@ -0,0 +1,13 @@
<?php
namespace ProVM\Common\Define\Model;
use Carbon\Carbon;
trait DateTime {
public function dateTime(\DateTime $date_time = null) {
if ($date_time === null) {
return Carbon::parse($this->date_time);
}
$this->date_time = $date_time->format('Y-m-d H:i:s');
}
}

View File

@ -0,0 +1,13 @@
<?php
namespace ProVM\Common\Define\Model;
use Carbon\Carbon;
trait Time {
public function date(\DateTime $time = null) {
if ($time === null) {
return Carbon::parse($this->time);
}
$this->time = $time->format('H:i:s');
}
}

View File

@ -0,0 +1,23 @@
<?php
namespace ProVM\Common\Middleware;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\RequestHandlerInterface as Handler;
use Psr\Http\Message\ResponseInterface as Response;
use Nyholm\Psr7\Factory\Psr17Factory;
class Auth {
protected $factory;
public function __construct(Psr17Factory $factory) {
$this->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;
}
}

View File

@ -0,0 +1,30 @@
<?php
namespace ProVM\Common\Service;
use \ORM;
use \Model;
use ProVM\Common\Service\Database\MySQL;
class Database {
protected $settings;
public function __construct($settings) {
$this->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;
}
}
}

View File

@ -0,0 +1,25 @@
<?php
namespace ProVM\Common\Service\Database;
class DSN {
public $engine;
public function __construct(string $engine) {
$this->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))
]);
}
}

View File

@ -0,0 +1,21 @@
<?php
namespace ProVM\Common\Service\Database;
class MySQL {
protected $settings;
public function __construct($settings) {
$this->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;
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace ProVM\Common\Service;
class Migrator {
protected $schema_filename;
protected $migrations_folder;
protected $seeds_folder;
public function __construct(string $schema_filename, string $migrations_folder, string $seeds_folder) {
$this->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);
}
}
}

22
provm/src/Login.php Normal file
View File

@ -0,0 +1,22 @@
<?php
namespace ProVM;
use ProVM\Common\Alias\Model;
use ProVM\Common\Define\Model\DateTime as DT;
/**
* @property User $user_id
* @property \DateTime $date_time
* @property string $token
*/
class Login extends Model {
use DT;
protected $user;
public function user() {
if ($this->user === null) {
$this->user = $this->childOf(User::class, [Model::SELF_KEY => 'user_id']);
}
return $this->user;
}
}

15
provm/src/User.php Normal file
View File

@ -0,0 +1,15 @@
<?php
namespace ProVM;
use ProVM\Common\Alias\Model;
/**
* @property int $id
* @property string $name
* @property string $password
*/
class User extends Model {
public function checkPassword(string $password): bool {
return password_verify($this->password, $password);
}
}