From a0cb5fb6b0f9534f8d69f94baf1339dd54eeadec Mon Sep 17 00:00:00 2001 From: count-null <70529195+count-null@users.noreply.github.com> Date: Tue, 25 Feb 2025 19:21:31 -0500 Subject: [PATCH] save --- .env.example | 2 +- composer.json | 3 +- composer.lock | 505 +++++++++++++++++++++++- public/index.php | 167 +++----- src/app.php | 31 +- src/colors.php | 58 +++ src/controllers/account.php | 47 ++- src/controllers/admin.php | 184 +++++++++ src/controllers/lnurlp.php | 60 ++- src/controllers/transaction.php | 172 ++++++++ src/models/emails.php | 94 +++++ src/models/invoices.php | 96 +++++ src/models/magic_links.php | 27 +- src/models/subscriptions.php | 4 +- src/models/transactions.php | 19 +- src/models/users.php | 13 + src/scripts/check_all_invoices.php | 25 ++ src/scripts/check_subscriptions.php | 21 + src/scripts/init_db.php | 60 +++ src/views/account/verify.twig | 70 ++++ src/views/admin/emails.twig | 27 ++ src/views/admin/index.twig | 10 + src/views/admin/orders.twig | 3 + src/views/admin/returns.twig | 3 + src/views/admin/transactions/add.twig | 61 +++ src/views/admin/transactions/index.twig | 144 +++++++ src/views/admin/users.twig | 3 + src/views/header.twig | 9 +- src/views/lib/button.twig | 21 +- src/views/lib/emails/verify.twig | 89 +++++ src/views/lib/input.twig | 8 + src/views/lib/number_input.twig | 17 + src/views/lib/policy/sats.twig | 2 +- src/views/lib/select.twig | 8 + src/views/transaction.twig | 8 + tailwind.config.js | 2 +- 36 files changed, 1886 insertions(+), 187 deletions(-) create mode 100644 src/colors.php create mode 100644 src/controllers/admin.php create mode 100644 src/controllers/transaction.php create mode 100644 src/models/emails.php create mode 100644 src/models/invoices.php create mode 100644 src/scripts/check_all_invoices.php create mode 100644 src/scripts/check_subscriptions.php create mode 100644 src/scripts/init_db.php create mode 100644 src/views/account/verify.twig create mode 100644 src/views/admin/emails.twig create mode 100644 src/views/admin/index.twig create mode 100644 src/views/admin/orders.twig create mode 100644 src/views/admin/returns.twig create mode 100644 src/views/admin/transactions/add.twig create mode 100644 src/views/admin/transactions/index.twig create mode 100644 src/views/admin/users.twig create mode 100644 src/views/lib/emails/verify.twig create mode 100644 src/views/lib/number_input.twig create mode 100644 src/views/lib/select.twig create mode 100644 src/views/transaction.twig diff --git a/.env.example b/.env.example index 16cc6ca..99b8d90 100644 --- a/.env.example +++ b/.env.example @@ -1,6 +1,6 @@ APP_HOST="localhost:8080" APP_NAME="BuysForLife" -SQLITE_DB="db-dev.sqlite" +SQLITE_DB="/Full/Path/to/your/db-dev.sqlite" # SMTP for login, order, and admin notifications SMTP_HOST="smtp.example.com" # SMTP uses TLS SMTP_USER="user@example.com" diff --git a/composer.json b/composer.json index 654ed34..cb34158 100644 --- a/composer.json +++ b/composer.json @@ -11,6 +11,7 @@ "vlucas/phpdotenv": "^5.6", "web-auth/webauthn-lib": "^5.0", "twig/twig": "^3.0", - "swentel/nostr-php": "^1.5" + "swentel/nostr-php": "^1.5", + "jorijn/bitcoin-bolt11": "^1.0" } } diff --git a/composer.lock b/composer.lock index bc0d4d2..1baf487 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "408b4a1daa73232eabf14c566a3e5d8d", + "content-hash": "3f3a1571b095d2550e901def5ae562ca", "packages": [ { "name": "bitwasp/bech32", @@ -112,6 +112,87 @@ ], "time": "2023-11-29T23:19:16+00:00" }, + { + "name": "composer/semver", + "version": "3.4.3", + "source": { + "type": "git", + "url": "https://github.com/composer/semver.git", + "reference": "4313d26ada5e0c4edfbd1dc481a92ff7bff91f12" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/semver/zipball/4313d26ada5e0c4edfbd1dc481a92ff7bff91f12", + "reference": "4313d26ada5e0c4edfbd1dc481a92ff7bff91f12", + "shasum": "" + }, + "require": { + "php": "^5.3.2 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.11", + "symfony/phpunit-bridge": "^3 || ^7" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Semver\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nils Adermann", + "email": "naderman@naderman.de", + "homepage": "http://www.naderman.de" + }, + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + }, + { + "name": "Rob Bast", + "email": "rob.bast@gmail.com", + "homepage": "http://robbast.nl" + } + ], + "description": "Semver library that offers utilities, version constraint parsing and validation.", + "keywords": [ + "semantic", + "semver", + "validation", + "versioning" + ], + "support": { + "irc": "ircs://irc.libera.chat:6697/composer", + "issues": "https://github.com/composer/semver/issues", + "source": "https://github.com/composer/semver/tree/3.4.3" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2024-09-19T14:15:21+00:00" + }, { "name": "doctrine/deprecations", "version": "1.1.4", @@ -157,6 +238,82 @@ }, "time": "2024-12-07T21:18:45+00:00" }, + { + "name": "fgrosse/phpasn1", + "version": "v2.5.0", + "source": { + "type": "git", + "url": "https://github.com/fgrosse/PHPASN1.git", + "reference": "42060ed45344789fb9f21f9f1864fc47b9e3507b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/fgrosse/PHPASN1/zipball/42060ed45344789fb9f21f9f1864fc47b9e3507b", + "reference": "42060ed45344789fb9f21f9f1864fc47b9e3507b", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "php-coveralls/php-coveralls": "~2.0", + "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0" + }, + "suggest": { + "ext-bcmath": "BCmath is the fallback extension for big integer calculations", + "ext-curl": "For loading OID information from the web if they have not bee defined statically", + "ext-gmp": "GMP is the preferred extension for big integer calculations", + "phpseclib/bcmath_compat": "BCmath polyfill for servers where neither GMP nor BCmath is available" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "FG\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Friedrich Große", + "email": "friedrich.grosse@gmail.com", + "homepage": "https://github.com/FGrosse", + "role": "Author" + }, + { + "name": "All contributors", + "homepage": "https://github.com/FGrosse/PHPASN1/contributors" + } + ], + "description": "A PHP Framework that allows you to encode and decode arbitrary ASN.1 structures using the ITU-T X.690 Encoding Rules.", + "homepage": "https://github.com/FGrosse/PHPASN1", + "keywords": [ + "DER", + "asn.1", + "asn1", + "ber", + "binary", + "decoding", + "encoding", + "x.509", + "x.690", + "x509", + "x690" + ], + "support": { + "issues": "https://github.com/fgrosse/PHPASN1/issues", + "source": "https://github.com/fgrosse/PHPASN1/tree/v2.5.0" + }, + "abandoned": true, + "time": "2022-12-19T11:08:26+00:00" + }, { "name": "graham-campbell/result-type", "version": "v1.1.3", @@ -219,6 +376,112 @@ ], "time": "2024-07-20T21:45:45+00:00" }, + { + "name": "jorijn/bitcoin-bolt11", + "version": "v1.0.1", + "source": { + "type": "git", + "url": "https://github.com/Jorijn/bitcoin-bolt11.git", + "reference": "ecfe3ddf42559297c8baedfca4b1edbedf319ebd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Jorijn/bitcoin-bolt11/zipball/ecfe3ddf42559297c8baedfca4b1edbedf319ebd", + "reference": "ecfe3ddf42559297c8baedfca4b1edbedf319ebd", + "shasum": "" + }, + "require": { + "bitwasp/bech32": "^0.0.1", + "ext-bcmath": "*", + "ext-gmp": "*", + "php": "^7.4|^8.0", + "protonlabs/bitcoin": "^1.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.10", + "phpunit/phpunit": "^9" + }, + "type": "library", + "autoload": { + "psr-4": { + "Jorijn\\Bitcoin\\Bolt11\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jorijn Schrijvershof", + "email": "jorijn@jorijn.com", + "homepage": "https://jorijn.com" + } + ], + "description": "A library for decoding lightning network payment requests as defined in BOLT #11", + "homepage": "https://github.com/Jorijn/bitcoin-bolt11/", + "keywords": [ + "bitcoin", + "bolt11", + "lightning-network" + ], + "support": { + "issues": "https://github.com/Jorijn/bitcoin-bolt11/issues", + "source": "https://github.com/Jorijn/bitcoin-bolt11/tree/v1.0.1" + }, + "time": "2024-07-16T05:23:44+00:00" + }, + { + "name": "lastguest/murmurhash", + "version": "2.1.1", + "source": { + "type": "git", + "url": "https://github.com/lastguest/murmurhash-php.git", + "reference": "0150ba26fb7025d1f936983a167cdc74149f87c8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/lastguest/murmurhash-php/zipball/0150ba26fb7025d1f936983a167cdc74149f87c8", + "reference": "0150ba26fb7025d1f936983a167cdc74149f87c8", + "shasum": "" + }, + "require": { + "php": "^7||^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^0.12", + "phpunit/phpunit": "^7||^9" + }, + "type": "library", + "autoload": { + "psr-4": { + "lastguest\\": "src/lastguest/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Stefano Azzolini", + "email": "lastguest@gmail.com", + "homepage": "https://github.com/lastguest/murmurhash-php" + } + ], + "description": "MurmurHash3 Hash", + "homepage": "https://github.com/lastguest/murmurhash-php", + "keywords": [ + "hash", + "hashing", + "murmur" + ], + "support": { + "issues": "https://github.com/lastguest/murmurhash-php/issues", + "source": "https://github.com/lastguest/murmurhash-php/tree/2.1.1" + }, + "time": "2021-04-13T16:23:45+00:00" + }, { "name": "paragonie/constant_time_encoding", "version": "v3.0.0", @@ -897,6 +1160,166 @@ }, "time": "2025-01-10T09:41:26+00:00" }, + { + "name": "pleonasm/merkle-tree", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/pleonasm/merkle-tree.git", + "reference": "6abdf5aacd79b6d502f944c96edd1a896ef6039b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pleonasm/merkle-tree/zipball/6abdf5aacd79b6d502f944c96edd1a896ef6039b", + "reference": "6abdf5aacd79b6d502f944c96edd1a896ef6039b", + "shasum": "" + }, + "require": { + "php": ">=5.6.0" + }, + "require-dev": { + "phpunit/php-invoker": "*", + "phpunit/phpunit": "^5.7", + "satooshi/php-coveralls": "*@dev", + "squizlabs/php_codesniffer": "*" + }, + "type": "library", + "autoload": { + "psr-0": { + "Pleo": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-2-Clause" + ], + "authors": [ + { + "name": "Matt Nagi", + "email": "matthew.nagi@base-2.net" + } + ], + "description": "An implementation of a Merkle Tree in PHP", + "support": { + "issues": "https://github.com/pleonasm/merkle-tree/issues", + "source": "https://github.com/pleonasm/merkle-tree/tree/master" + }, + "time": "2017-02-10T15:26:01+00:00" + }, + { + "name": "protonlabs/bitcoin", + "version": "1.0.10", + "source": { + "type": "git", + "url": "https://github.com/ProtonMail/bitcoin-php.git", + "reference": "475361ce56f1601164cc447cbb78f859799d9eaf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ProtonMail/bitcoin-php/zipball/475361ce56f1601164cc447cbb78f859799d9eaf", + "reference": "475361ce56f1601164cc447cbb78f859799d9eaf", + "shasum": "" + }, + "require": { + "bitwasp/bech32": "^0.0.1", + "composer/semver": "^1.4.0|^3.2.0", + "lastguest/murmurhash": "^v2.0.0", + "php-64bit": ">=7.0", + "pleonasm/merkle-tree": "~1.0.0", + "protonlabs/buffertools": "^0.5.0", + "shanecurran/phpecc": "^0.0.1" + }, + "require-dev": { + "bitwasp/bitcoinconsensus": "v3.0.0", + "bitwasp/secp256k1-php": "^v0.2.0", + "ext-json": "*", + "nbobtc/bitcoind-php": "v2.0.2", + "phpunit/phpunit": "^8.0.0", + "squizlabs/php_codesniffer": "^3.0.0" + }, + "suggest": { + "ext-bitcoinconsensus": "The bitcoinconsensus library for safest possible script verification", + "ext-secp256k1": "The secp256k1 library for fast and safe elliptic curve operations" + }, + "type": "library", + "autoload": { + "files": [ + "src/Script/functions.php" + ], + "psr-4": { + "BitWasp\\Bitcoin\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Unlicense" + ], + "authors": [ + { + "name": "Thomas Kerin", + "homepage": "https://thomaskerin.io", + "role": "Author" + } + ], + "description": "PHP Bitcoin library with functions for transactions, signatures, serialization, Random/Deterministic ECDSA keys, blocks, RPC bindings", + "homepage": "https://github.com/bit-wasp/bitcoin-php", + "support": { + "issues": "https://github.com/ProtonMail/bitcoin-php/issues", + "source": "https://github.com/ProtonMail/bitcoin-php/tree/1.0.10" + }, + "time": "2024-04-17T17:01:22+00:00" + }, + { + "name": "protonlabs/buffertools", + "version": "v0.5.8", + "source": { + "type": "git", + "url": "https://github.com/ProtonMail/buffertools-php.git", + "reference": "9bb64c124f93f3e373e61806d1e10d8feb0d5751" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ProtonMail/buffertools-php/zipball/9bb64c124f93f3e373e61806d1e10d8feb0d5751", + "reference": "9bb64c124f93f3e373e61806d1e10d8feb0d5751", + "shasum": "" + }, + "require": { + "php-64bit": ">=7.0.0" + }, + "replace": { + "bitwasp/buffertools": "^0.5.0" + }, + "require-dev": { + "phpstan/phpstan": "v0.9.x", + "phpunit/phpunit": "^6.0", + "squizlabs/php_codesniffer": "~2.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "BitWasp\\Buffertools\\": "src/Buffertools/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Thomas Kerin", + "homepage": "https://thomaskerin.io" + }, + { + "name": "Ruben de Vries", + "email": "ruben@rubensayshi.com" + } + ], + "description": "Toolbox for working with binary and hex data. Similar to NodeJS Buffer.", + "support": { + "source": "https://github.com/ProtonMail/buffertools-php/tree/v0.5.8" + }, + "time": "2023-07-17T08:19:22+00:00" + }, { "name": "psr/clock", "version": "1.0.0", @@ -1206,6 +1629,86 @@ }, "time": "2024-09-11T13:17:53+00:00" }, + { + "name": "shanecurran/phpecc", + "version": "v0.0.1", + "source": { + "type": "git", + "url": "https://github.com/shanecurran/phpecc.git", + "reference": "2f99b2c785e7bac48485b0da7cc1db7d9c0e5d87" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/shanecurran/phpecc/zipball/2f99b2c785e7bac48485b0da7cc1db7d9c0e5d87", + "reference": "2f99b2c785e7bac48485b0da7cc1db7d9c0e5d87", + "shasum": "" + }, + "require": { + "ext-gmp": "*", + "fgrosse/phpasn1": "^2.0", + "php": "^7.0||^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.0||^8.0||^9.0", + "squizlabs/php_codesniffer": "^2.0", + "symfony/yaml": "^2.6|^3.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Mdanter\\Ecc\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Matyas Danter", + "homepage": "http://matejdanter.com/", + "role": "Author" + }, + { + "name": "Thibaud Fabre", + "email": "thibaud@aztech.io", + "homepage": "http://aztech.io", + "role": "Maintainer" + }, + { + "name": "Thomas Kerin", + "email": "afk11@users.noreply.github.com", + "role": "Maintainer" + }, + { + "name": "Shane Curran", + "email": "shanecurran@users.noreply.github.com", + "role": "Maintainer" + } + ], + "description": "PHP Elliptic Curve Cryptography library", + "homepage": "https://github.com/shanecurran/phpecc", + "keywords": [ + "Diffie", + "ECDSA", + "Hellman", + "curve", + "ecdh", + "elliptic", + "nistp192", + "nistp224", + "nistp256", + "nistp384", + "nistp521", + "phpecc", + "secp256k1", + "secp256r1" + ], + "support": { + "source": "https://github.com/shanecurran/phpecc/tree/v0.0.1" + }, + "time": "2023-03-08T19:51:13+00:00" + }, { "name": "simplito/bigint-wrapper-php", "version": "1.0.0", diff --git a/public/index.php b/public/index.php index 4e4c1bf..cf73f45 100644 --- a/public/index.php +++ b/public/index.php @@ -4,6 +4,7 @@ // use app\app; use app\controllers\account; +use app\controllers\admin; use app\controllers\category; use app\controllers\cart; use app\controllers\checkout; @@ -12,6 +13,7 @@ use app\controllers\lnurlp; use app\controllers\lost; use app\controllers\magic_link; use app\controllers\support; +use app\controllers\transaction; require_once __DIR__ . '/../vendor/autoload.php'; @@ -20,36 +22,6 @@ Dotenv\Dotenv::createImmutable(__DIR__ . '/../')->load(); // Start the session app::init_db(); -use app\models\addresses; -use app\models\cart_items; -use app\models\carts; -use app\models\magic_links; -use app\models\order_items; -use app\models\orders; -use app\models\products; -use app\models\quote_items; -use app\models\quotes; -use app\models\subscriptions; -use app\models\transactions; -use app\models\user_addresses; -use app\models\users; - -if (!app::$db->query("SELECT name FROM sqlite_master WHERE type='table' AND name='users'")->fetch()) { - addresses::init(); - cart_items::init(); - carts::init(); - magic_links::init(); - order_items::init(); - orders::init(); - products::init(); - quote_items::init(); - quotes::init(); - subscriptions::init(); - transactions::init(); - user_addresses::init(); - users::init(); -} - session_start(); session_regenerate_id(true); // prevent session fixation attacks @@ -63,76 +35,20 @@ if (!isset($_SESSION['fingerprint'])) { } } - // these will be available to use in all twig templates $defaults = [ 'copyright_year' => date('Y'), 'session' => $_SESSION, + 'http_host' => $_SERVER['HTTP_HOST'], 'env' => $_ENV, + 'is_admin' => isset($_SESSION['user_id']) && $_SESSION['user_id'] == 1, // uses cookie-js to get the client's preferred theme // used to conditionally deliver image assets // or styles based on theme 'theme' => isset($_COOKIE["theme"]) ? $_COOKIE["theme"] : 'light', // set your tailwind colors here for app themeing // the idea is to avoid using colors in your templates - 'colors' => [ - 'header' => [ - 'banner' => 'bg-gray-100 dark:bg-gray-600 text-gray-200 dark:text-gray-200', - - ], - 'anchor' => [ - 'primary' => 'text-blue-400 dark:text-blue-200' - ], - 'body' => 'bg-white dark:bg-gray-800 text-gray-600 dark:text-gray-300', - 'button' => [ - 'primary' => 'border-blue-400 dark:border-blue-600 dark:hover:border-blue-800 bg-blue-400 dark:bg-blue-600 hover:bg-blue-600 hover:dark:bg-blue-800 text-white dark:text-white', - 'default' => 'hover:bg-gray-50 dark:hover:bg-gray-900' - ], - 'breadcrumb' => [ - 'parent' => 'text-gray-300 dark:text-gray-400 hover:text-gray-400 dark:hover:text-gray-500', - 'seperator' => 'text-gray-200 dark:text-gray-200', - 'child' => 'text-gray-200 dark:text-gray-300' - ], - 'dropdown' => [ - 'list' => 'bg-white dark:bg-blue-900 border-gray-600 dark:border-gray-300', - 'item' => 'hover:bg-gray-200 dark:hover:bg-gray-900' - ], - 'input' => 'text-gray-800 dark:text-gray-300 bg-white dark:bg-gray-800 border-gray-300 dark:border-gray-500 focus:ring-blue-500', - 'error' => [ - 'text' => 'text-red-600', - 'alert' => 'bg-red-100 text-gray-800 border-red-600' - ], - 'warning' => [ - 'text' => 'text-yellow-400', - 'alert' => 'bg-yellow-100 text-gray-800 border-yellow-400' - ], - 'success' => [ - 'text' => 'text-green-600', - 'alert' => 'bg-green-100 text-gray-800 border-green-600' - ], - 'info' => [ - 'text' => 'text-blue-400', - 'alert' => 'bg-blue-200 text-gray-800 border-blue-400' - ], - 'modal' => [ - 'content' => 'bg-white dark:bg-blue-900 border-gray-600 dark:border-gray-300', - 'shadow' => 'bg-black/70' - ], - 'nav' => [ - 'bar' => 'bg-blue-400 dark:bg-blue-600 text-gray-200 dark:text-gray-200', - 'item' => 'hover:bg-blue-600 dark:hover:bg-blue-800 hover:text-gray-200 dark:hover:text-gray-300 text-white border-blue-400 dark:border-blue-600', - 'hovercontent' => 'bg-white dark:bg-slate-700 text-gray-800 dark:text-gray-300' - ], - 'rule' => 'border-gray-400 dark:border-gray-400', - 'text' => [ - 'muted' => 'text-gray-400 dark:text-gray-300' - ], - 'toggle' => "bg-gray-300 peer-checked:bg-green-400 after:bg-white", - 'footer' => [ - "primary" => "bg-gray-200 dark:bg-slate-600 text-gray-500 dark:text-gray-300", - "policy" => "bg-slate-400 dark:bg-slate-800 text-gray-200 dark:text-gray-400" - ], - ] + 'colors' => require dirname(__DIR__) . '/src/colors.php', ]; // Setup a twig @@ -147,29 +63,56 @@ if (str_starts_with(haystack: $route, needle: '/.well-known/lnurlp/')) { $route = '/lnurlp'; } -$controller = match ($route) { - '/' => home::index($defaults), - '/account' => account::index($defaults), - '/account/profile' => account::profile(), - '/account/login' => account::login($defaults), - '/account/email' => account::email(), - '/account/logout' => account::logout(), - '/magic-link' => magic_link::index(), - '/account/returns' => account::returns($defaults), - '/account/signup' => account::signup($defaults), - '/account/billing' => account::billing($defaults), - '/account/orders' => account::orders($defaults), - '/account/shipping' => account::shipping($defaults), - '/checkout/confirmed' => checkout::confirmed($defaults), - '/checkout/review-pay' => checkout::review_pay($defaults), - '/checkout/shipping-billing' => checkout::shipping_billing($defaults), - '/support/ask' => support::index($defaults), - '/support/bitcoin' => support::bitcoin($defaults), - '/cart' => cart::index($defaults), - '/lnurlp' => lnurlp::index(), - // product categories - '/power-meters' => category::power_meters($defaults), - default => lost::index($defaults) +// Combined regex to match multiple dynamic routes in one go +if (preg_match('/^\/(transaction|user|order|product)\/([\w-]+)$/', $route, $matches)) { + [$full, $type, $id] = $matches; + $controllers = [ + 'transaction' => fn($id) => transaction::view($defaults, $id), + 'user' => fn($id) => users::view($id), + 'order' => fn($id) => orders::view($id), + 'quote' => fn($id) => quotes::view($id), + 'product' => fn($id) => products::view($id), + 'subscription' => fn($id) => subscriptions::view($id), + 'cart' => fn($id) => cart::index($id), + ]; + + if (isset($controllers[$type])) { + $controller = $controllers[$type]($id); + } +} else { + $controller = match ($route) { + '/' => home::index($defaults), + '/account' => account::index($defaults), + '/account/profile' => account::profile(), + '/account/login' => account::login($defaults), + '/account/email' => account::email(), + '/account/logout' => account::logout(), + '/account/returns' => account::returns($defaults), + '/account/signup' => account::signup($defaults), + '/account/billing' => account::billing($defaults), + '/account/orders' => account::orders($defaults), + '/account/shipping' => account::shipping($defaults), + '/account/verify' => account::verify($defaults), + '/admin' => $defaults['is_admin'] ? admin::index($defaults) : lost::index($defaults), + '/admin/users' => $defaults['is_admin'] ? admin::users($defaults) : lost::index($defaults), + '/admin/orders' => $defaults['is_admin'] ? admin::orders($defaults) : lost::index($defaults), + '/admin/emails' => $defaults['is_admin'] ? admin::emails($defaults) : lost::index($defaults), + '/admin/transactions' => $defaults['is_admin'] ? admin::transactions($defaults) : lost::index($defaults), + '/admin/transactions/add' => $defaults['is_admin'] ? admin::transactions_add($defaults) : lost::index($defaults), + '/admin/transactions/reset' => $defaults['is_admin'] ? admin::transactions_reset($defaults) : lost::index($defaults), + '/admin/returns' => $defaults['is_admin'] ? admin::returns($defaults) : lost::index($defaults), + '/magic-link' => magic_link::index(), + '/checkout/confirmed' => checkout::confirmed($defaults), + '/checkout/review-pay' => checkout::review_pay($defaults), + '/checkout/shipping-billing' => checkout::shipping_billing($defaults), + '/support/ask' => support::index($defaults), + '/support/bitcoin' => support::bitcoin($defaults), + '/cart' => cart::index($defaults), + '/lnurlp' => lnurlp::index(), + // product categories + '/power-meters' => category::power_meters($defaults), + default => lost::index($defaults) + }; }; // Clear alerts after rendering diff --git a/src/app.php b/src/app.php index 834a1c8..899ff8a 100644 --- a/src/app.php +++ b/src/app.php @@ -1,9 +1,7 @@ setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION); } catch (\PDOException $e) { die("Database error: " . $e->getMessage()); } } - - public static function send_mail($to, $from, $from_name, $subject, $message, $HTML_message) - { - $mail = new PHPMailer(exceptions: true); - //Server settings - $mail->SMTPDebug = SMTP::DEBUG_SERVER; - $mail->isSMTP(); - $mail->Host = $_ENV['SMTP_HOST']; - $mail->SMTPAuth = true; - $mail->Username = $_ENV['SMTP_USER']; - $mail->Password = $_ENV['SMTP_PASS']; - $mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS; - $mail->Port = 587; - $mail->isHTML(true); - $mail->setFrom($from, $from_name); - $mail->addAddress(address: $to); - $mail->Subject = $subject; - $mail->Body = $HTML_message; - $mail->AltBody = $message; - - // Buffer the output - ob_start(); - $mail->send(); - ob_end_clean(); - } public static function sendJson($data, $status = 200) { diff --git a/src/colors.php b/src/colors.php new file mode 100644 index 0000000..e51a152 --- /dev/null +++ b/src/colors.php @@ -0,0 +1,58 @@ + [ + 'banner' => 'bg-gray-100 dark:bg-gray-600 text-gray-200 dark:text-gray-200', + ], + 'anchor' => [ + 'primary' => 'text-blue-400 dark:text-blue-200' + ], + 'body' => 'bg-white dark:bg-gray-800 text-gray-600 dark:text-gray-300', + 'button' => [ + 'primary' => 'border-blue-400 dark:border-blue-600 dark:hover:border-blue-800 bg-blue-400 dark:bg-blue-600 hover:bg-blue-600 hover:dark:bg-blue-800 text-white dark:text-white', + 'default' => 'hover:bg-gray-50 dark:hover:bg-gray-900' + ], + 'breadcrumb' => [ + 'parent' => 'text-gray-300 dark:text-gray-400 hover:text-gray-400 dark:hover:text-gray-500', + 'seperator' => 'text-gray-200 dark:text-gray-200', + 'child' => 'text-gray-200 dark:text-gray-300' + ], + 'dropdown' => [ + 'list' => 'bg-white dark:bg-blue-900 border-gray-600 dark:border-gray-300', + 'item' => 'hover:bg-gray-200 dark:hover:bg-gray-900' + ], + 'input' => 'text-gray-800 dark:text-gray-300 bg-white dark:bg-gray-800 border-gray-300 dark:border-gray-500 focus:ring-blue-500', + 'error' => [ + 'text' => 'text-red-600', + 'alert' => 'bg-red-100 text-gray-800 border-red-600' + ], + 'warning' => [ + 'text' => 'text-yellow-400', + 'alert' => 'bg-yellow-100 text-gray-800 border-yellow-400' + ], + 'success' => [ + 'text' => 'text-green-600', + 'alert' => 'bg-green-100 text-gray-800 border-green-600' + ], + 'info' => [ + 'text' => 'text-blue-400', + 'alert' => 'bg-blue-200 text-gray-800 border-blue-400' + ], + 'modal' => [ + 'content' => 'bg-white dark:bg-blue-900 border-gray-600 dark:border-gray-300', + 'shadow' => 'bg-black/70' + ], + 'nav' => [ + 'bar' => 'bg-blue-400 dark:bg-blue-600 text-gray-200 dark:text-gray-200', + 'item' => 'hover:bg-blue-600 dark:hover:bg-blue-800 hover:text-gray-200 dark:hover:text-gray-300 text-white border-blue-400 dark:border-blue-600', + 'hovercontent' => 'bg-white dark:bg-slate-700 text-gray-800 dark:text-gray-300' + ], + 'rule' => 'border-gray-400 dark:border-gray-400', + 'text' => [ + 'muted' => 'text-gray-400 dark:text-gray-300' + ], + 'toggle' => "bg-gray-300 peer-checked:bg-green-400 after:bg-white", + 'footer' => [ + "primary" => "bg-gray-200 dark:bg-slate-600 text-gray-500 dark:text-gray-300", + "policy" => "bg-slate-400 dark:bg-slate-800 text-gray-200 dark:text-gray-400" + ], + ]; \ No newline at end of file diff --git a/src/controllers/account.php b/src/controllers/account.php index 26ec594..443229b 100644 --- a/src/controllers/account.php +++ b/src/controllers/account.php @@ -3,6 +3,7 @@ namespace app\controllers; use app\models\addresses; use app\models\users; +use app\models\emails; use app\models\user_addresses; use app\models\magic_links; @@ -138,6 +139,49 @@ class account } } + public static function verify($defaults) + { + if ($_SERVER['REQUEST_METHOD'] == 'POST') { + $code = $_POST['code']; + $link = magic_links::validateCode($code); + if ($link) { + $user = $link['user_id'] ? users::getById($link['user_id']) : users::getByEmail($link['email']); + if ($user) { + $_SESSION['user_email'] = $link['email']; + $_SESSION['user_id'] = $user['id']; + if (!$user['verified']) { + users::verify($link['email']); + } + header('Location: /account'); + exit; + } else { + $_SESSION['user_email'] = $link['email']; + header('Location: /account/signup'); + exit; + } + } else { + $_SESSION['error'] = "Invalid or expired verification code."; + header('Location: /account/verify'); + exit; + } + } + + echo $GLOBALS['twig']->render('lib/page/index.twig', array_merge($defaults, [ + 'child_template' => 'account/verify.twig', + 'page_title' => $_ENV['APP_NAME'], + 'breadcrumbs' => [ + [ + 'url' => '/account', + 'title' => 'My Account' + ], + [ + 'url' => null, + 'title' => 'Verify' + ] + ] + ])); + } + public static function login($defaults) { if ($_SERVER['REQUEST_METHOD'] == 'POST') { @@ -148,7 +192,7 @@ class account exit; } else { $token = magic_links::add($email, null); - header('Location: /account/login'); + header('Location: /account/verify'); exit; } } @@ -339,6 +383,7 @@ class account $verified, $dark_theme ); + emails::updateUserIdByEmail($email, $user_id); user_addresses::add( user_id: $user_id, address_id: $ship_id diff --git a/src/controllers/admin.php b/src/controllers/admin.php new file mode 100644 index 0000000..290fbd6 --- /dev/null +++ b/src/controllers/admin.php @@ -0,0 +1,184 @@ +render('lib/page/index.twig', array_merge($defaults, [ + 'child_template' => 'admin/index.twig', + 'page_title' => 'Dashboard', + 'breadcrumbs' => [ + [ + 'url' => '/admin', + 'title' => 'Admin' + ] + ], + ])); + } + + public static function users($defaults) + { + echo $GLOBALS['twig']->render('lib/page/index.twig', array_merge($defaults, [ + 'child_template' => 'admin/users.twig', + 'page_title' => 'Users', + 'breadcrumbs' => [ + [ + 'url' => '/admin', + 'title' => 'Admin' + ], + [ + 'url' => '/admin/users', + 'title' => 'Users' + ] + ], + ])); + } + + public static function orders($defaults) + { + echo $GLOBALS['twig']->render('lib/page/index.twig', array_merge($defaults, [ + 'child_template' => 'admin/orders.twig', + 'page_title' => 'Orders', + 'breadcrumbs' => [ + [ + 'url' => '/admin', + 'title' => 'Admin' + ], + [ + 'url' => '/admin/orders', + 'title' => 'Orders' + ] + ], + ])); + } + + public static function returns($defaults) + { + echo $GLOBALS['twig']->render('lib/page/index.twig', array_merge($defaults, [ + 'child_template' => 'admin/returns.twig', + 'page_title' => 'Returns', + 'breadcrumbs' => [ + [ + 'url' => '/admin', + 'title' => 'Admin' + ], + [ + 'url' => '/admin/returns', + 'title' => 'Returns' + ] + ], + ])); + } + + public static function transactions($defaults) + { + echo $GLOBALS['twig']->render('lib/page/index.twig', array_merge($defaults, [ + 'child_template' => 'admin/transactions/index.twig', + 'page_title' => 'Transactions', + 'recent_sats' => transactions::getRecent(10, 'sats'), + 'recent_cents' => transactions::getRecent(10, 'cents'), + 'whales_sats' => transactions::getWhales(10, 'sats'), + 'whales_cents' => transactions::getWhales(10, 'cents'), + 'sats_liability' => transactions::getLiabilities('sats'), + 'cents_liability' => transactions::getLiabilities('cents'), + 'breadcrumbs' => [ + [ + 'url' => '/admin', + 'title' => 'Admin' + ], + [ + 'url' => '/admin/transactions', + 'title' => 'Transactions' + ] + ], + ])); + } + + public static function transactions_add($defaults) + { + if ($_SERVER['REQUEST_METHOD'] == 'POST') { + $amount = $_POST['amount'] ?? null; + $currency = $_POST['currency']; + $user_identifier = $_POST['user_identifier'] ?? null; + if (!$amount || !$user_identifier) { + $_SESSION['error'] = !$amount ? "Please enter an amount for the transaction." : "Please enter a user email or id for the transaction."; + $_SESSION['last_post'] = $_POST; + } else { + if (strpos($user_identifier, '@') !== false && strpos($user_identifier, '.') !== false) { + $user = users::getByEmail($user_identifier); + } elseif (is_numeric($user_identifier)) { + $user = users::getById((int)$user_identifier); + } else { + $_SESSION['error'] = "Invalid user identifier. Please enter a valid email or user ID."; + $_SESSION['last_post'] = $_POST; + } + if (!$user) { + $_SESSION['error'] = "User not found. Please enter a valid email or user ID."; + $_SESSION['last_post'] = $_POST; + } else { + if($_POST['confirm']){ + // create the transaction + $txid = transactions::add($user['id'], $amount > 0 ? 'CREDIT' : 'REVOKE', $currency == 'cents' ? $amount : 0, $currency == 'sats' ? $amount : 0); + header('Location: /transaction/' . $txid); + exit; + } else { + $_SESSION['last_post'] = $_POST; + $_SESSION['last_post']['confirm'] = true; + $_SESSION['last_post']['email'] = $user['email']; + $_SESSION['last_post']['id'] = $user['id']; + } + } + } + } + echo $GLOBALS['twig']->render('lib/page/index.twig', array_merge($defaults, [ + 'child_template' => 'admin/transactions/add.twig', + 'page_title' => 'Add a Transaction', + 'breadcrumbs' => [ + [ + 'url' => '/admin', + 'title' => 'Admin' + ], + [ + 'url' => '/admin/transactions', + 'title' => 'Transactions' + ], + [ + 'url' => '/admin/transactions/add', + 'title' => 'Add' + ] + ], + ])); + } + + public static function emails($defaults) + { + $recent_emails = emails::getRecent(20); + echo $GLOBALS['twig']->render('lib/page/index.twig', array_merge($defaults, [ + 'child_template' => 'admin/emails.twig', + 'page_title' => 'Emails', + 'recent_emails' => $recent_emails, + 'breadcrumbs' => [ + [ + 'url' => '/admin', + 'title' => 'Admin' + ], + [ + 'url' => '/admin/emails', + 'title' => 'Emails' + ] + ], + ])); + } + + public static function transactions_reset($defaults) + { + $_SESSION['last_post'] = null; + header('Location: /admin/transactions/add'); + exit; + } +} \ No newline at end of file diff --git a/src/controllers/lnurlp.php b/src/controllers/lnurlp.php index 2ffcf20..d3e0b65 100644 --- a/src/controllers/lnurlp.php +++ b/src/controllers/lnurlp.php @@ -2,13 +2,18 @@ namespace app\controllers; use app\app; +use app\models\invoices; +$invoice = 'lnbc234340n1pnm5uh2pp5pwekmjzj3hgadahfeprtc6f2ppmhnqjzh556q4fvve8z953v3t0sdqqcqzysxqrrsssp5uq25yjnmvdpnglmv42nf64wk0pugrynq549f3wgghgtkfapwdfhq9qxpqysgqyjq2ewqkm6s2dlhuruuc4k9md22wraz829tlhfeuxrsnwmephfkjz8e9g7j6373989mfccajy3cxexac8xu6yen4qfs4947fkrg9ynsq7x72ze'; +use Jorijn\Bitcoin\Bolt11\Encoder\PaymentRequestDecoder; +use Jorijn\Bitcoin\Bolt11\Model\Tag; +use Jorijn\Bitcoin\Bolt11\Normalizer\PaymentRequestDenormalizer; + class lnurlp { - public static function index() + public static function index($defaults) { header(header: 'Content-Type: application/json'); - $host = $_SERVER['HTTP_HOST']; - $user = $_GET["username"] ?? false; + $username = $_GET["username"] ?? false; $paymentRequest = $_GET["pay"] ?? false; $verify = $_GET["verify"] ?? false; @@ -25,25 +30,29 @@ class lnurlp 'reason' => 'invalid value for `pay` param (set `pay=1` or exclude `pay` from the url)', ]); } - // for when the user is missing - if ($user == false && $verify == false) { + // for when the user is not specified + if ($username == false && $verify == false) { returnJson([ 'status' => 'ERROR', 'reason' => 'no user specified (set `username=` in the url)', ]); } - - - list($proxy_user, $proxy_host) = explode("@", $_ENV['LN_ADDRESS']); - - + // for when the user is not registered + $user = users::getByNpub($username); + if (!$user){ + returnJson([ + 'status' => 'ERROR', + 'reason' => "@$username is not registered" + ]); + } // for when the client makes it's first call (querying the lightning address) - $metadata = "[[\"text/plain\",\"Funding @$user on $host\"],[\"text/identifier\",\"$user@$host\"]]"; + list($proxy_user, $proxy_host) = explode("@", $_ENV['LN_ADDRESS']); + $metadata = "[[\"text/plain\",\"Funding @$username on $host\"],[\"text/identifier\",\"$username@$host\"]]"; if ($paymentRequest == false && $verify == false) { $res = json_decode(file_get_contents("https://$proxy_host/.well-known/lnurlp/$proxy_user"), true); returnJson( [ - 'callback' => "https://$host/lnurlp?pay=1&username=$user", + 'callback' => "https://$host/lnurlp?pay=1&username=$username", 'maxSendable' => $res['maxSendable'], 'minSendable' => $res['minSendable'], 'metadata' => $metadata, @@ -54,19 +63,34 @@ class lnurlp ); } - // for when the client makes it's second call (callback) + // for when the client makes it's second call (callback) to get an invoice if ($paymentRequest == "1") { - $proxy_url = "https://$proxy_host/lnurlp?pay=1&username=$proxy_user"; + $res = json_decode(file_get_contents("https://$proxy_host/.well-known/lnurlp/$proxy_user"), true); + $proxy_url = $res['callback']; if (isset($_GET["amount"])) { $proxy_url .= "&amount=" . urlencode($_GET["amount"]); } $res = json_decode(file_get_contents($proxy_url), true); + if ($res['status'] === 'OK'){ + // subscribe to this invoice by adding to our db + $invoice = $res['pr']; + $decoder = new PaymentRequestDecoder(); + $denormalizer = new PaymentRequestDenormalizer(); + $paymentRequest = $denormalizer->denormalize($decoder->decode($invoice)); + invoices::add( + $user['id'], + null, + $invoice, + $res['verify'], + $paymentRequest->getMilliSatoshis(), + $paymentRequest->getExpiryDateTime()['date'] + ); $boom = explode("=", $res['verify']); $proxy_verify = end($boom); returnJson([ 'status' => 'OK', - 'pr' => $res['pr'], + 'pr' => $invoice, 'routes' => $res['routes'], 'verify' => "https://$host/lnurlp?verify=$proxy_verify" ]); @@ -77,8 +101,10 @@ class lnurlp // for when they want to verify the payment succeeded if ($verify) { - $res = json_decode(file_get_contents("https://$proxy_host/lnurlp?verify=$verify"), true); - returnJson($res); + $res = json_decode(file_get_contents("https://$proxy_host/.well-known/lnurlp/$proxy_user"), true); + $proxy_url = $res['verify']; + $prox_res = json_decode(file_get_contents($proxy_url), true); + returnJson($prox_res); } // for when none of the above conditions are met diff --git a/src/controllers/transaction.php b/src/controllers/transaction.php new file mode 100644 index 0000000..213e5ca --- /dev/null +++ b/src/controllers/transaction.php @@ -0,0 +1,172 @@ +render('lib/page/index.twig', array_merge($defaults, [ + 'child_template' => 'transaction.twig', + 'page_title' => 'Transaction Reciept #' . $txid, + 'tx' => $tx, + 'user' => $user, + 'breadcrumbs' => [ + [ + 'url' => "/transaction/" . $txid, + 'title' => 'Transaction Reciept' + ] + ], + ])); + } + + public static function users($defaults) + { + echo $GLOBALS['twig']->render('lib/page/index.twig', array_merge($defaults, [ + 'child_template' => 'admin/users.twig', + 'page_title' => $_ENV['APP_NAME'] . ' Users', + 'breadcrumbs' => [ + [ + 'url' => '/admin', + 'title' => 'Admin' + ], + [ + 'url' => '/admin/users', + 'title' => 'Users' + ] + ], + ])); + } + + public static function orders($defaults) + { + echo $GLOBALS['twig']->render('lib/page/index.twig', array_merge($defaults, [ + 'child_template' => 'admin/orders.twig', + 'page_title' => $_ENV['APP_NAME'] . ' Orders', + 'breadcrumbs' => [ + [ + 'url' => '/admin', + 'title' => 'Admin' + ], + [ + 'url' => '/admin/orders', + 'title' => 'Orders' + ] + ], + ])); + } + + public static function returns($defaults) + { + echo $GLOBALS['twig']->render('lib/page/index.twig', array_merge($defaults, [ + 'child_template' => 'admin/returns.twig', + 'page_title' => $_ENV['APP_NAME'] . ' Returns', + 'breadcrumbs' => [ + [ + 'url' => '/admin', + 'title' => 'Admin' + ], + [ + 'url' => '/admin/returns', + 'title' => 'Returns' + ] + ], + ])); + } + + public static function transactions($defaults) + { + echo $GLOBALS['twig']->render('lib/page/index.twig', array_merge($defaults, [ + 'child_template' => 'admin/transactions/index.twig', + 'page_title' => $_ENV['APP_NAME'] . ' Transactions', + 'recent_sats' => transactions::getRecent(10, 'sats'), + 'recent_cents' => transactions::getRecent(10, 'cents'), + 'whales_sats' => transactions::getWhales(10, 'sats'), + 'whales_cents' => transactions::getWhales(10, 'cents'), + 'sats_liability' => transactions::getLiabilities('sats'), + 'cents_liability' => transactions::getLiabilities('cents'), + 'breadcrumbs' => [ + [ + 'url' => '/admin', + 'title' => 'Admin' + ], + [ + 'url' => '/admin/transactions', + 'title' => 'Transactions' + ] + ], + ])); + } + + public static function transactions_add($defaults) + { + if ($_SERVER['REQUEST_METHOD'] == 'POST') { + $amount = $_POST['amount'] ?? null; + $currency = $_POST['currency']; + $user_identifier = $_POST['user_identifier'] ?? null; + if (!$amount || !$user_identifier) { + $_SESSION['error'] = !$amount ? "Please enter an amount for the transaction." : "Please enter a user email or id for the transaction."; + $_SESSION['last_post'] = $_POST; + } else { + if (strpos($user_identifier, '@') !== false && strpos($user_identifier, '.') !== false) { + $user = users::getByEmail($user_identifier); + } elseif (is_numeric($user_identifier)) { + $user = users::getById((int)$user_identifier); + } else { + $_SESSION['error'] = "Invalid user identifier. Please enter a valid email or user ID."; + $_SESSION['last_post'] = $_POST; + } + if (!$user) { + $_SESSION['error'] = "User not found. Please enter a valid email or user ID."; + $_SESSION['last_post'] = $_POST; + } else { + if($_POST['confirm']){ + // create the transaction + $txid = transactions::add($user['id'], 'CREDIT', $currency == 'cents' ? $amount : 0, $currency == 'sats' ? $amount : 0); + header('Location: /transaction/' . $txid); + exit; + } else { + $_SESSION['last_post'] = $_POST; + $_SESSION['last_post']['confirm'] = true; + $_SESSION['last_post']['email'] = $user['email']; + $_SESSION['last_post']['id'] = $user['id']; + } + } + } + } + echo $GLOBALS['twig']->render('lib/page/index.twig', array_merge($defaults, [ + 'child_template' => 'admin/transactions/add.twig', + 'page_title' => 'Add a Transaction', + 'breadcrumbs' => [ + [ + 'url' => '/admin', + 'title' => 'Admin' + ], + [ + 'url' => '/admin/transactions', + 'title' => 'Transactions' + ], + [ + 'url' => '/admin/transactions/add', + 'title' => 'Add' + ] + ], + ])); + } + + public static function transactions_reset($defaults) + { + $_SESSION['last_post'] = null; + header('Location: /admin/transactions/add'); + exit; + } +} \ No newline at end of file diff --git a/src/models/emails.php b/src/models/emails.php new file mode 100644 index 0000000..b1afc7e --- /dev/null +++ b/src/models/emails.php @@ -0,0 +1,94 @@ +exec("CREATE TABLE IF NOT EXISTS emails ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER, + from_email TEXT NOT NULL, + from_name TEXT NOT NULL, + to_email TEXT NOT NULL, + subject TEXT NOT NULL, + message TEXT NOT NULL, + html_message TEXT NOT NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users(id) + );"); + } + + public static function getRecentByUserId($user_id, $n) + { + $query = "SELECT * FROM emails WHERE user_id = :user_id ORDER BY created_at DESC LIMIT :n"; + $stmt = app::$db->prepare($query); + $stmt->bindParam(':user_id', $user_id, \PDO::PARAM_INT); + $stmt->bindParam(':n', $n, \PDO::PARAM_INT); + $stmt->execute(); + return $stmt->fetchAll(\PDO::FETCH_ASSOC); + } + + public static function getRecent($n) + { + $query = "SELECT * FROM emails ORDER BY created_at DESC LIMIT :n"; + $stmt = app::$db->prepare($query); + $stmt->bindParam(':n', $n, \PDO::PARAM_INT); + $stmt->execute(); + return $stmt->fetchAll(\PDO::FETCH_ASSOC); + } + + public static function updateUserIdByEmail($email, $user_id) + { + $query = "UPDATE emails SET user_id = :user_id WHERE to_email = :email AND user_id IS NULL"; + $stmt = app::$db->prepare($query); + $stmt->bindParam(':user_id', $user_id, \PDO::PARAM_INT); + $stmt->bindParam(':email', $email); + $stmt->execute(); + } + + public static function send($subject, $from_email, $from_name, $to_email, $message, $template, $template_vars) + { + $user = users::getByEmail($to_email); + $user_id = $user ? $user['id'] : null; + + $HTML_message = $GLOBALS['twig']->render("lib/emails/$template", $template_vars); + $query = "INSERT INTO emails (user_id, from_email, from_name, to_email, subject, message, html_message) + VALUES (:user_id, :from_email, :from_name, :to_email, :subject, :message, :html_message)"; + $stmt = app::$db->prepare($query); + $stmt->bindParam(':user_id', $user_id); + $stmt->bindParam(':from_email', $from_email); + $stmt->bindParam(':from_name', $from_name); + $stmt->bindParam(':to_email', $to_email); + $stmt->bindParam(':subject', $subject); + $stmt->bindParam(':message', $message); + $stmt->bindParam(':html_message', $HTML_message); + $stmt->execute(); + $mail = new PHPMailer(exceptions: true); + // Mail Server settings + $mail->SMTPDebug = SMTP::DEBUG_SERVER; + $mail->isSMTP(); + $mail->Host = $_ENV['SMTP_HOST']; + $mail->SMTPAuth = true; + $mail->Username = $_ENV['SMTP_USER']; + $mail->Password = $_ENV['SMTP_PASS']; + $mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS; + $mail->Port = 587; + $mail->isHTML(true); + $mail->setFrom($from_email, $from_name); + $mail->addAddress($to_email); + $mail->Subject = $subject; + $mail->Body = $HTML_message; + $mail->AltBody = $message; + // Buffer the output + ob_start(); + $mail->send(); + ob_end_clean(); + } +} diff --git a/src/models/invoices.php b/src/models/invoices.php new file mode 100644 index 0000000..4aafbd3 --- /dev/null +++ b/src/models/invoices.php @@ -0,0 +1,96 @@ +exec($query); + } + + public static function add($user_id, $order_id, $invoice, $verify, $amount_msats, $expiry_date) + { + $query = "INSERT INTO invoices (user_id, order_id, invoice, verify, amount_msats, expiry_date) VALUES (:user_id, :order_id, :invoice, :verify, :amount_msats, :expiry_date)"; + $stmt = app::$db->prepare($query); + $stmt->bindParam(':user_id', $user_id, \PDO::PARAM_INT); + $stmt->bindParam(':order_id', $order_id, \PDO::PARAM_INT); + $stmt->bindParam(':invoice', $invoice); + $stmt->bindParam(':verify', $verify); + $stmt->bindParam(':amount_msats', $amount_milisats); + $stmt->bindParam(':expiry_date', $expiry_date); + $stmt->execute(); + + return app::$db->lastInsertId(); + } + + public static function getById($invoice_id) + { + $stmt = app::$db->prepare("SELECT * FROM invoices WHERE id = :invoice_id"); + $stmt->bindParam(':invoice_id', $invoice_id, \PDO::PARAM_INT); + $stmt->execute(); + return $stmt->fetch(\PDO::FETCH_ASSOC); + } + + public static function delete($invoice_id) + { + $deleteStmt = app::$db->prepare("DELETE FROM invoices WHERE id = :invoice_id"); + $deleteStmt->bindParam(':invoice_id', $invoice_id, \PDO::PARAM_INT); + $deleteStmt->execute(); + } + + public static function markSettled($invoice_id) + { + $stmt = app::$db->prepare("UPDATE invoices SET settled = 1 WHERE id = :invoice_id"); + $stmt->bindParam(':invoice_id', $invoice_id, \PDO::PARAM_INT); + $stmt->execute(); + } + + public static function checkAll() + { + $stmt = app::$db->prepare("SELECT id FROM invoices WHERE settled = 0"); + $stmt->execute(); + $invoiceIds = $stmt->fetchAll(\PDO::FETCH_COLUMN); + + foreach ($invoiceIds as $invoiceId) { + self::check($invoiceId); + } + } + + public static function check($invoice_id) + { + $invoice = self::get_byId($invoice_id); + // Use LUD-21 Payment Verify + $responseData = json_decode(file_get_contents($invoice['verify']), true); + if ($responseData['settled']) { + if ($invoice['order_id']) { + orders::updateStatus($invoice['order_id'], "PROCESSING"); + self::markSettled($invoice_id); + } else { + transactions::add($invoice['user_id'], 'DEPOSIT', 0, $invoice['amount_msats'] / 1000); + } + } else { + // Check if more than 24 hours past expiry + $expiryDate = new \DateTime($invoice['expiry_date']); + $currentDate = new \DateTime(); + $interval = $expiryDate->diff($currentDate); + if ($interval->days >= 1) { + self::delete($invoice_id); + } + } + } +} + diff --git a/src/models/magic_links.php b/src/models/magic_links.php index 83c89ac..66b4a27 100644 --- a/src/models/magic_links.php +++ b/src/models/magic_links.php @@ -2,6 +2,8 @@ namespace app\models; use app\app; +use app\models\emails; + class magic_links { public static function init() @@ -12,6 +14,7 @@ class magic_links email TEXT NOT NULL, code TEXT NOT NULL, token TEXT NOT NULL, + ipv4 TEXT NOT NULL, expires_at DATETIME NOT NULL, used BOOLEAN DEFAULT FALSE )"); @@ -19,21 +22,26 @@ class magic_links public static function add($email, $user_id) { - $code = str_pad(strval(random_int(0, 999999)), 6, "0", STR_PAD_LEFT); $token = bin2hex(random_bytes(32)); + $seed = hexdec(substr($token, 0, 8)); // Use the first 8 characters of the token as a seed + mt_srand($seed); + $code = str_pad(strval(mt_rand(0, 999999)), 6, "0", STR_PAD_LEFT); $expires_at = date('Y-m-d H:i:s', time() + 60 * 15); + $ipv4 = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0'; // Get client's IPv4 address $query = "INSERT INTO magic_links ( email, user_id, token, code, - expires_at + expires_at, + ipv4 ) VALUES ( :email, :user_id, :token, :code, - :expires_at + :expires_at, + :ipv4 )"; $stmt = app::$db->prepare($query); $stmt->bindParam(':email', $email); @@ -41,12 +49,21 @@ class magic_links $stmt->bindParam(':token', $token); $stmt->bindParam(':code', $code); $stmt->bindParam(':expires_at', $expires_at); + $stmt->bindParam(':ipv4', $ipv4); $stmt->execute(); $link = $_ENV['APP_HOST'] . "/magic-link?token=" . urlencode($token); $subject = "Your Magic Sign-In Link"; $message = "Enter this code into the sign-in form\n$code\n or copy-paste this link into your browser to sign in:\n$link"; - $HTML_message = "Click the link to sign in: $link or enter this code:
$code"; - app::send_mail(to: $email, from: $_ENV['SMTP_FROM'], from_name: $_ENV['APP_NAME'], subject: $subject, message: $message, HTML_message: $HTML_message); + $template_vars = ['code' => $code, 'link' => $link]; + emails::send( + $subject, + $_ENV['SMTP_FROM'], + $_ENV['APP_NAME'], + $email, + $message, + 'verify.twig', + $template_vars + ); $_SESSION['success'] = 'Link sent to your email!'; return $token; } diff --git a/src/models/subscriptions.php b/src/models/subscriptions.php index 04eeb66..8189de7 100644 --- a/src/models/subscriptions.php +++ b/src/models/subscriptions.php @@ -3,7 +3,7 @@ namespace app\models; use app\app; -class Subscriptions +class subscriptions { const STATES = [ 'TRIAL', 'START', 'RENEWAL' @@ -29,7 +29,7 @@ class Subscriptions );"); } - public static function createSubscription(int $userId, int $productId, string $state = 'TRIAL', string $status = 'COMPLETED', string $startDate, string $renewAt, string $invoiceDate): int + public static function createSubscription( $userId, $productId, $state, $startDate, $renewAt, $invoiceDate) { self::validateState($state); self::validateStatus($status); diff --git a/src/models/transactions.php b/src/models/transactions.php index adc3d3d..6e54e53 100644 --- a/src/models/transactions.php +++ b/src/models/transactions.php @@ -20,6 +20,15 @@ class transactions FOREIGN KEY (user_id) REFERENCES users(id) )"); } + + public static function getById($id) + { + $query = "SELECT * FROM transactions WHERE id = :id"; + $stmt = app::$db->prepare($query); + $stmt->bindParam(':id', $id, \PDO::PARAM_INT); + $stmt->execute(); + return $stmt->fetch(); + } public static function add($user_id, $transaction_type, $cents, $sats) { @@ -29,13 +38,10 @@ class transactions if ($cents < 0 || $sats < 0) { throw new \Exception("Amounts must be non-negative integers."); } - $currentBalance = self::getUserBalance($user_id); - if (in_array($transaction_type, ['REDEEM', 'REVOKE']) && ($currentBalance['total_cents'] < $cents || $currentBalance['total_sats'] < $sats)) { throw new \Exception("Insufficient funds."); } - $query = "INSERT INTO transactions (user_id, type, cents, sats) VALUES (:user_id, :transaction_type, :cents, :sats)"; $stmt = app::$db->prepare($query); $stmt->bindParam(':user_id', $user_id); @@ -43,7 +49,6 @@ class transactions $stmt->bindParam(':cents', $cents); $stmt->bindParam(':sats', $sats); $stmt->execute(); - return app::$db->lastInsertId(); } @@ -56,9 +61,9 @@ class transactions return $stmt->fetch(); } - public static function getRecent($n) + public static function getRecent($n, $currency) { - $query = "SELECT * FROM transactions ORDER BY date DESC LIMIT :n"; + $query = "SELECT * FROM transactions WHERE $currency > 0 ORDER BY date DESC LIMIT :n"; $stmt = app::$db->prepare($query); $stmt->bindParam(':n', $n, \PDO::PARAM_INT); $stmt->execute(); @@ -77,7 +82,7 @@ class transactions return $stmt->fetchAll(); } - public static function liabilities($currency) + public static function getLiabilities($currency) { if (!in_array($currency, ['cents', 'sats'])) { throw new \Exception("Invalid currency type."); diff --git a/src/models/users.php b/src/models/users.php index c4c0fe4..cafe0af 100644 --- a/src/models/users.php +++ b/src/models/users.php @@ -14,6 +14,10 @@ class users shipping_address_id INTEGER, billing_address_id INTEGER, opt_in_promotional BOOLEAN NOT NULL, + opt_in_subscription BOOLEAN DEFAULT TRUE, + opt_in_orders BOOLEAN DEFAULT TRUE, + lifetime_spend INTEGER DEFAULT 0, + lifetime_orders INTEGER DEFAULT 0, verified BOOLEAN NOT NULL, dark_theme BOOLEAN NOT NULL, nsec TEXT, @@ -129,6 +133,15 @@ class users return $stmt->fetch(\PDO::FETCH_ASSOC); } + public static function getByNpub($npub) + { + $query = "SELECT * FROM users WHERE npub = :npub"; + $stmt = app::$db->prepare($query); + $stmt->bindParam(':npub', $npub); + $stmt->execute(); + return $stmt->fetch(\PDO::FETCH_ASSOC); + } + public static function getByEmail($email) { $query = "SELECT * FROM users WHERE email = :email"; diff --git a/src/scripts/check_all_invoices.php b/src/scripts/check_all_invoices.php new file mode 100644 index 0000000..433a8dd --- /dev/null +++ b/src/scripts/check_all_invoices.php @@ -0,0 +1,25 @@ +load(); + +use app\app; +use app\models\invoices; + +app::init_db(); +invoices::checkAll(); + diff --git a/src/scripts/check_subscriptions.php b/src/scripts/check_subscriptions.php new file mode 100644 index 0000000..d3259ae --- /dev/null +++ b/src/scripts/check_subscriptions.php @@ -0,0 +1,21 @@ +load(); \ No newline at end of file diff --git a/src/scripts/init_db.php b/src/scripts/init_db.php new file mode 100644 index 0000000..4031b97 --- /dev/null +++ b/src/scripts/init_db.php @@ -0,0 +1,60 @@ +load(); + +// establish the db connection +use app\app; +app::init_db(); + +// db models go brrr... +use app\models\addresses; +addresses::init(); + +use app\models\cart_items; +cart_items::init(); + +use app\models\carts; +carts::init(); + +use app\models\emails; +emails::init(); + +use app\models\invoices; +invoices::init(); + +use app\models\magic_links; +magic_links::init(); + +use app\models\order_items; +order_items::init(); + +use app\models\orders; +orders::init(); + +use app\models\products; +products::init(); + +use app\models\quote_items; +quote_items::init(); + +use app\models\quotes; +quotes::init(); + +use app\models\subscriptions; +subscriptions::init(); + +use app\models\transactions; +transactions::init(); + +use app\models\user_addresses; +user_addresses::init(); + +use app\models\users; +users::init(); diff --git a/src/views/account/verify.twig b/src/views/account/verify.twig new file mode 100644 index 0000000..c692deb --- /dev/null +++ b/src/views/account/verify.twig @@ -0,0 +1,70 @@ +
+
+
+

Check Your Email

+

We have sent a verification code to your email.

+ {% include 'lib/rule.twig' %} +
+ {% include 'lib/alert.twig' %} + + +
+ + + + {% include 'lib/button.twig' with { + label: 'Verify Code', + onclick: 'this.parentNode.submit()', + captcha: true + } %} +
+
+
\ No newline at end of file diff --git a/src/views/admin/emails.twig b/src/views/admin/emails.twig new file mode 100644 index 0000000..f2a5e20 --- /dev/null +++ b/src/views/admin/emails.twig @@ -0,0 +1,27 @@ +
+

Recently Sent Emails

+ + + + + + + + + + + {% for email in recent_emails %} + + + + + + + {% else %} + + + + {% endfor %} + +
ToFromSubjectCreated At
{{ email.to_email }}{{ email.from_email }}{{ email.subject }}{{ email.created_at }}
No recent emails found.
+
\ No newline at end of file diff --git a/src/views/admin/index.twig b/src/views/admin/index.twig new file mode 100644 index 0000000..b17ceae --- /dev/null +++ b/src/views/admin/index.twig @@ -0,0 +1,10 @@ +
+ Dashboard + Users + Orders + Returns + Emails + Transactions + + INDEX +
\ No newline at end of file diff --git a/src/views/admin/orders.twig b/src/views/admin/orders.twig new file mode 100644 index 0000000..c4c3e92 --- /dev/null +++ b/src/views/admin/orders.twig @@ -0,0 +1,3 @@ +
+ ORDERS +
\ No newline at end of file diff --git a/src/views/admin/returns.twig b/src/views/admin/returns.twig new file mode 100644 index 0000000..a165393 --- /dev/null +++ b/src/views/admin/returns.twig @@ -0,0 +1,3 @@ +
+ RETURNS +
\ No newline at end of file diff --git a/src/views/admin/transactions/add.twig b/src/views/admin/transactions/add.twig new file mode 100644 index 0000000..e9fde96 --- /dev/null +++ b/src/views/admin/transactions/add.twig @@ -0,0 +1,61 @@ +
+ {% include 'lib/alert.twig' %} +
+ {% if session.last_post.confirm %} + Confirm + + {% endif %} + {% if session.last_post.amount is defined %} + {{ session.last_post.amount }} {{ session.last_post.currency }} + + {% else %} + {% include 'lib/number_input.twig' with { + id: 'amount', + name: 'amount', + label: 'Amount', + placeholder: 'Enter the amount', + value: session.last_post.amount, + required: true + } %} + {% endif %} + {% if session.last_post.currency is defined %} + + {% else %} + {% include 'lib/select.twig' with { + id: 'currency', + name: 'currency', + label: 'Currency', + value: session.last_post.currency, + options: [ + { 'value': 'sats', 'text': 'Sats' }, + { 'value': 'cents', 'text': 'Cents' } + ], + required: true + } %} + {% endif %} + {% if session.last_post.user_identifier is defined %} + {% if session.last_post.email is defined %} + {{ session.last_post.id }} {{ session.last_post.email }} + {% endif %} + + {% else %} + {% include 'lib/input.twig' with { + type: 'text', + name: 'user_identifier', + label: 'User Identifier', + placeholder: 'Enter email or user index', + value: session.last_post.user_identifier + } %} + {% endif %} + {% include 'lib/button.twig' with { + label: 'Submit', + onclick: 'this.parentNode.submit()' + } %} +
+ {% if session.last_post %} + {% include 'lib/button.twig' with { + label: 'Cancel', + href: '/admin/transactions/reset' + } %} + {% endif %} +
\ No newline at end of file diff --git a/src/views/admin/transactions/index.twig b/src/views/admin/transactions/index.twig new file mode 100644 index 0000000..d20c344 --- /dev/null +++ b/src/views/admin/transactions/index.twig @@ -0,0 +1,144 @@ +
+ {% include 'lib/alert.twig' %} +
+ {% include 'lib/number_input.twig' with { + id: 'amount', + name: 'amount', + label: 'Amount', + required: true + } %} + {% include 'lib/select.twig' with { + id: 'currency', + name: 'currency', + label: 'Currency', + options: [ + { value: 'cents', text: 'Cents' }, + { value: 'sats', text: 'Sats' } + ], + required: true + } %} + {% include 'lib/button.twig' with { + label: 'Submit', + onclick: 'this.parentNode.submit()' + } %} +
+ +

Liabilities

+ + + + + + + + + + + + + + + + + +
CurrencyTotal Liability
Sats{{ sats_liability }}
Cents{{ cents_liability }}
+

Sats Transactions

+ + + + + + + + + + {% if recent_sats is not empty %} + {% for transaction in recent_sats %} + + + + + + {% endfor %} + {% else %} + + + + {% endif %} + +
TypeAmount (Sats)Date
{{ transaction.type }}{{ transaction.sats }}{{ transaction.date }}
No Sats transactions available.
+ +

Cents Transactions

+ + + + + + + + + + {% if recent_cents is not empty %} + {% for transaction in recent_cents %} + + + + + + {% endfor %} + {% else %} + + + + {% endif %} + +
TypeAmount (Cents)Date
{{ transaction.type }}{{ transaction.cents }}{{ transaction.date }}
No Cents transactions available.
+ +

Whales Sats

+ + + + + + + + + {% if whales_sats is not empty %} + {% for whale in whales_sats %} + + + + + {% endfor %} + {% else %} + + + + {% endif %} + +
User IDTotal Sats
{{ whale.user_id }}{{ whale.total }}
No Sats whales available.
+ +

Whales Cents

+ + + + + + + + + {% if whales_cents is not empty %} + {% for whale in whales_cents %} + + + + + {% endfor %} + {% else %} + + + + {% endif %} + +
User IDTotal Cents
{{ whale.user_id }}{{ whale.total }}
No Cents whales available.
+
\ No newline at end of file diff --git a/src/views/admin/users.twig b/src/views/admin/users.twig new file mode 100644 index 0000000..b6c41cb --- /dev/null +++ b/src/views/admin/users.twig @@ -0,0 +1,3 @@ +
+ USERS +
\ No newline at end of file diff --git a/src/views/header.twig b/src/views/header.twig index dadf1e0..d9f5c9a 100644 --- a/src/views/header.twig +++ b/src/views/header.twig @@ -1,6 +1,4 @@