This commit is contained in:
count-null 2025-02-25 19:21:31 -05:00
parent 27df1a73b5
commit a0cb5fb6b0
36 changed files with 1886 additions and 187 deletions

View file

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

View file

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

505
composer.lock generated
View file

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

View file

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

View file

@ -1,9 +1,7 @@
<?php
namespace app;
// for email
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\SMTP;
use PHPMailer\PHPMailer\Exception;
class app
{
@ -12,38 +10,13 @@ class app
public static function init_db()
{
try {
self::$db = new \PDO('sqlite:../' . $_ENV['SQLITE_DB']);
self::$db = new \PDO('sqlite:' . $_ENV['SQLITE_DB']);
self::$db->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)
{
http_response_code($status);

58
src/colors.php Normal file
View file

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

View file

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

184
src/controllers/admin.php Normal file
View file

@ -0,0 +1,184 @@
<?php
namespace app\controllers;
use app\models\transactions;
use app\models\emails;
use app\models\users;
class admin
{
public static function index($defaults)
{
echo $GLOBALS['twig']->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;
}
}

View file

@ -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=<name>` 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

View file

@ -0,0 +1,172 @@
<?php
namespace app\controllers;
use app\models\transactions;
use app\models\users;
use app\controllers\lost;
class transaction
{
public static function view($defaults, $txid)
{
$tx = transactions::getById($txid);
if (!$tx) {
lost::index($defaults);
}
$user = users::getById($tx['user_id']);
echo $GLOBALS['twig']->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;
}
}

94
src/models/emails.php Normal file
View file

@ -0,0 +1,94 @@
<?php
namespace app\models;
use app\app;
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\SMTP;
use PHPMailer\PHPMailer\Exception;
class emails
{
public static function init()
{
app::$db->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();
}
}

96
src/models/invoices.php Normal file
View file

@ -0,0 +1,96 @@
<?php
namespace app\models;
use app\models\transactions;
use app\app;
class invoices
{
public static function init()
{
$query = "CREATE TABLE IF NOT EXISTS invoices (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
order_id INTEGER,
invoice TEXT NOT NULL,
verify TEXT NOT NULL,
amount_msats REAL NOT NULL,
expiry_date DATETIME NOT NULL,
settled BOOLEAN DEFAULT FALSE,
FOREIGN KEY(user_id) REFERENCES users(id),
FOREIGN KEY(order_id) REFERENCES orders(id)
)";
app::$db->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);
}
}
}
}

View file

@ -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: <a href='$link'>$link</a> or enter this code:<br/>$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;
}

View file

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

View file

@ -21,6 +21,15 @@ class transactions
)");
}
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)
{
if (!in_array($transaction_type, self::TYPES)) {
@ -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.");

View file

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

View file

@ -0,0 +1,25 @@
<?php
//
// Run this script to check if any proxied invocies from the LN_SERVICE are paid or expired
// It will apply store_credit if the invoice was paid and associated with a deposit
// It will make adjustments to quotes or orders if the invoice was paid and associated with a quote or order payment
// It will mark invoices as expired if they are expired
//
// Run this every 10 seconds with cron like this...
// * * * * * bash -c 'start=$(date +%s); for i in {1..6}; do php /path/to/scripts/check_all_invoices.php; sleep $((10 - ($(date +%s) - start) % 10)); done'
//
// Cron only lets you run every minute, using this we can run every 10 seconds.
// It accounts for the execution time of this script
// so there will be minimal 'drift' over time.
//
require_once __DIR__ . '/../../vendor/autoload.php';
// Load environment variables from the .env file at project root
Dotenv\Dotenv::createImmutable(__DIR__ . '/../../')->load();
use app\app;
use app\models\invoices;
app::init_db();
invoices::checkAll();

View file

@ -0,0 +1,21 @@
<?php
//
// Run this script to check the status of subscriptions
// It should run at least once a day with cron or some other task runner
// It will check for subscriptions that have expired
// If the expired subscription was CANCELED, do nothing
// If the expired subscription was not CANCELED, update it to COMPLETED
// If the newly COMPLETED subscription was TRIAL, then START the subscription based on the TRIAL - bill the user
// If the newly COMPLETED subscription was START, then RENEW the subscription - bill the user
// If the newly COMPLETED subscription was RENEW, then RENEW the subscription - bill the user
// It will check for subcriptions that are about to expire
// It sends an email notification to user based on their email preferences
// The content of the notification is based on:
// If user has enough credit to cover the upcomming bill
// If the user does not have enough credit to cover the upcomming bill
// php /path/to/scripts/check_lightning_invoices.php
//
require_once __DIR__ . '/../../vendor/autoload.php';
// Load environment variables from the .env file at project root
Dotenv\Dotenv::createImmutable(__DIR__ . '/../../')->load();

60
src/scripts/init_db.php Normal file
View file

@ -0,0 +1,60 @@
<?php
//
// Use this to initialize the database models
// Run it in command-line like
// php scripts/init_db.php
//
require_once __DIR__ . '/../../vendor/autoload.php';
// Load environment variables from the .env file at project root
Dotenv\Dotenv::createImmutable(__DIR__ . '/../../')->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();

View file

@ -0,0 +1,70 @@
<section class="flex flex-col gap-4">
<div class="flex flex-col gap-2">
<div class="flex flex-col gap-1 mb-4">
<h3 class="text-2xl font-semibold">Check Your Email</h3>
<p>We have sent a verification code to your email.</p>
{% include 'lib/rule.twig' %}
</div>
{% include 'lib/alert.twig' %}
<style>
.code-input {
font-size: 24px;
font-family: monospace;
text-align: left;
letter-spacing: 1.15em;
border: 2px solid #000;
padding: 10px;
width: 250px;
outline: none;
background: linear-gradient(
90deg,
white 0, white 36px, /* Box width */
black 36px, black 38px /* Border between boxes */
);
background-size: 42px 100%; /* Adjust size to fit each character in a cell */
background-clip: padding-box;
}
</style>
<form action="/account/verify" method="post" class="flex flex-col items-center gap-4">
<input type="tel" name="code" placeholder="******" class="code-input" maxlength="6" inputmode="numeric" pattern="[0-9]*">
<script>
document.addEventListener("DOMContentLoaded", function () {
const input = document.querySelector(".code-input");
input.addEventListener("input", function (e) {
// Remove any non-digit characters immediately
this.value = this.value.replace(/\D/g, "");
// Move the cursor back one space then blur when the 6th digit is entered
if (this.value.length === 6) {
this.setSelectionRange(this.value.length - 1, this.value.length - 1);
this.blur();
}
});
input.addEventListener("paste", function (e) {
e.preventDefault();
let pastedData = e.clipboardData.getData("text").replace(/\D/g, ""); // Allow only digits
this.value = pastedData.substring(0, this.maxLength);
// Clear focus if pasted data fills the input
if (this.value.length === 6) {
this.blur();
}
});
input.addEventListener("keypress", function (e) {
if (!/[0-9]/.test(e.key)) {
e.preventDefault(); // Block non-numeric input
}
});
});
</script>
{% include 'lib/button.twig' with {
label: 'Verify Code',
onclick: 'this.parentNode.submit()',
captcha: true
} %}
</form>
</div>
</section>

View file

@ -0,0 +1,27 @@
<section class="flex flex-col gap-4">
<h3 class="text-2xl font-semibold">Recently Sent Emails</h3>
<table class="min-w-full bg-white">
<thead>
<tr>
<th class="py-2">To</th>
<th class="py-2">From</th>
<th class="py-2">Subject</th>
<th class="py-2">Created At</th>
</tr>
</thead>
<tbody>
{% for email in recent_emails %}
<tr>
<td class="border px-4 py-2">{{ email.to_email }}</td>
<td class="border px-4 py-2">{{ email.from_email }}</td>
<td class="border px-4 py-2">{{ email.subject }}</td>
<td class="border px-4 py-2">{{ email.created_at }}</td>
</tr>
{% else %}
<tr>
<td class="border px-4 py-2" colspan="4">No recent emails found.</td>
</tr>
{% endfor %}
</tbody>
</table>
</section>

View file

@ -0,0 +1,10 @@
<section class="flex flex-col gap-4">
<a href="/admin">Dashboard</a>
<a href="/admin/users">Users</a>
<a href="/admin/orders">Orders</a>
<a href="/admin/returns">Returns</a>
<a href="/admin/emails">Emails</a>
<a href="/admin/transactions">Transactions</a>
INDEX
</section>

View file

@ -0,0 +1,3 @@
<section class="flex flex-col gap-4">
ORDERS
</section>

View file

@ -0,0 +1,3 @@
<section class="flex flex-col gap-4">
RETURNS
</section>

View file

@ -0,0 +1,61 @@
<section class="flex flex-col gap-4">
{% include 'lib/alert.twig' %}
<form action="/admin/transactions/add" method="post" class="flex flex-col gap-4">
{% if session.last_post.confirm %}
Confirm
<input type="hidden" name="confirm" id="confirm" value="{{ session.last_post.confirm }}">
{% endif %}
{% if session.last_post.amount is defined %}
{{ session.last_post.amount }} {{ session.last_post.currency }}
<input type="hidden" name="amount" id="amount" value="{{ session.last_post.amount }}">
{% 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 %}
<input type="hidden" name="currency" id="currency" value="{{ session.last_post.currency }}">
{% 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 %}
<input type="hidden" name="user_identifier" id="user_identifier" value="{{ session.last_post.user_identifier }}">
{% 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()'
} %}
</form>
{% if session.last_post %}
{% include 'lib/button.twig' with {
label: 'Cancel',
href: '/admin/transactions/reset'
} %}
{% endif %}
</section>

View file

@ -0,0 +1,144 @@
<section class="flex flex-col gap-4">
{% include 'lib/alert.twig' %}
<form action="/admin/transactions/add" method="post" class="flex flex-col gap-4">
{% 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()'
} %}
</form>
<h3 class="text-2xl font-semibold">Liabilities</h3>
<table class="min-w-full bg-white">
<thead>
<tr>
<th class="py-2">Currency</th>
<th class="py-2">Total Liability</th>
</tr>
</thead>
<tbody>
<tr>
<td class="border px-4 py-2">Sats</td>
<td class="border px-4 py-2">{{ sats_liability }}</td>
</tr>
<tr>
<td class="border px-4 py-2">Cents</td>
<td class="border px-4 py-2">{{ cents_liability }}</td>
</tr>
</tbody>
</table>
<h3 class="text-2xl font-semibold">Sats Transactions</h3>
<table class="min-w-full bg-white">
<thead>
<tr>
<th class="py-2">Type</th>
<th class="py-2">Amount (Sats)</th>
<th class="py-2">Date</th>
</tr>
</thead>
<tbody>
{% if recent_sats is not empty %}
{% for transaction in recent_sats %}
<tr>
<td class="border px-4 py-2">{{ transaction.type }}</td>
<td class="border px-4 py-2">{{ transaction.sats }}</td>
<td class="border px-4 py-2">{{ transaction.date }}</td>
</tr>
{% endfor %}
{% else %}
<tr>
<td class="border px-4 py-2" colspan="3">No Sats transactions available.</td>
</tr>
{% endif %}
</tbody>
</table>
<h3 class="text-2xl font-semibold">Cents Transactions</h3>
<table class="min-w-full bg-white">
<thead>
<tr>
<th class="py-2">Type</th>
<th class="py-2">Amount (Cents)</th>
<th class="py-2">Date</th>
</tr>
</thead>
<tbody>
{% if recent_cents is not empty %}
{% for transaction in recent_cents %}
<tr>
<td class="border px-4 py-2">{{ transaction.type }}</td>
<td class="border px-4 py-2">{{ transaction.cents }}</td>
<td class="border px-4 py-2">{{ transaction.date }}</td>
</tr>
{% endfor %}
{% else %}
<tr>
<td class="border px-4 py-2" colspan="3">No Cents transactions available.</td>
</tr>
{% endif %}
</tbody>
</table>
<h3 class="text-2xl font-semibold">Whales Sats</h3>
<table class="min-w-full bg-white">
<thead>
<tr>
<th class="py-2">User ID</th>
<th class="py-2">Total Sats</th>
</tr>
</thead>
<tbody>
{% if whales_sats is not empty %}
{% for whale in whales_sats %}
<tr>
<td class="border px-4 py-2">{{ whale.user_id }}</td>
<td class="border px-4 py-2">{{ whale.total }}</td>
</tr>
{% endfor %}
{% else %}
<tr>
<td class="border px-4 py-2" colspan="2">No Sats whales available.</td>
</tr>
{% endif %}
</tbody>
</table>
<h3 class="text-2xl font-semibold">Whales Cents</h3>
<table class="min-w-full bg-white">
<thead>
<tr>
<th class="py-2">User ID</th>
<th class="py-2">Total Cents</th>
</tr>
</thead>
<tbody>
{% if whales_cents is not empty %}
{% for whale in whales_cents %}
<tr>
<td class="border px-4 py-2">{{ whale.user_id }}</td>
<td class="border px-4 py-2">{{ whale.total }}</td>
</tr>
{% endfor %}
{% else %}
<tr>
<td class="border px-4 py-2" colspan="2">No Cents whales available.</td>
</tr>
{% endif %}
</tbody>
</table>
</section>

View file

@ -0,0 +1,3 @@
<section class="flex flex-col gap-4">
USERS
</section>

View file

@ -1,6 +1,4 @@
<style>
/* #Mega Menu Styles
*/
.mega-menu {
opacity: 0;
visibility: hidden;
@ -12,7 +10,6 @@
width: 100%;
transition: all 0.15s linear 0s;
}
/* #hoverable Class Styles */
.hoverable {
position: static;
}
@ -106,6 +103,12 @@
{% endif %}
</ul>
{% if is_admin %}
{% include 'lib/rule.twig' %}
<ul class="py-2">
<li><a href="/admin" class="block px-4 py-2 m-1 rounded-lg {{ colors.dropdown.item }}">Admin</a></li>
</ul>
{% endif %}
{% if session.user_id is defined %}
{% include 'lib/rule.twig' %}
<ul class="py-2">

View file

@ -1,5 +1,10 @@
<div onclick="{{ onclick }}"
class="cursor-pointer {{ submit is defined ? 'px-4 rounded-l-none' : 'w-full' }} {{ colors.button.primary }} rounded-lg h-[42px] flex items-center justify-center">
{% if href is defined %}
<a href="{{ href }}"
class="cursor-pointer {{ submit is defined ? 'px-4 rounded-l-none' : 'w-full' }} {{ colors.button.primary }} rounded-lg h-[42px] flex items-center justify-center">
{% else %}
<div onclick="{{ onclick }}"
class="cursor-pointer {{ submit is defined ? 'px-4 rounded-l-none' : 'w-full' }} {{ colors.button.primary }} rounded-lg h-[42px] flex items-center justify-center">
{% endif %}
{% if label is defined %}
<span>{{ label }}</span>
{% endif %}
@ -10,23 +15,27 @@
<circle cx="11" cy="11" r="8"></circle>
<path d="m21 21-4.3-4.3"></path>
</svg>
{% elseif icon == 'add' %}
{% elseif icon == 'add' %}
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="lucide lucide-plus">
<path d="M5 12h14" />
<path d="M12 5v14" />
</svg>
{% elseif icon == 'enter' %}
{% elseif icon == 'enter' %}
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="lucide lucide-arrow-right">
<path d="M5 12h14" />
<path d="m12 5 7 7-7 7" />
</svg>
{% endif %}
{% endif %}
{% endif %}
</div>
{% if href is defined %}
</a>
{% else %}
</div>
{% endif %}
{% if captcha is defined %}
<div class="flex justify-center {{ colors.text.muted }}">
<p class="w-[250px] text-[10px] text-center">This form is protected by reCAPTCHA and the Google

View file

@ -0,0 +1,89 @@
<table align="center" class="x_225906249wrapper x_225906249wrapper-callout x_225906249wrapper-without-padding-top" border="0" cellpadding="0" cellspacing="0" style="background: rgb(251, 251, 251); background-color: rgb(251, 251, 251); width: 100%">
<tbody>
<tr>
<td>
<div style="margin: 0px auto; max-width: 648px">
<table align="center" border="0" cellpadding="0" cellspacing="0" style="width: 100%">
<tbody>
<tr>
<td style="direction: ltr; font-size: 0px; padding: 60px 0px; text-align: center">
<div style="margin: 0px auto; max-width: 648px">
<table align="center" border="0" cellpadding="0" cellspacing="0" style="width: 100%">
<tbody>
<tr>
<td style="direction: ltr; font-size: 0px; padding: 0; text-align: center">
<div class="x_225906249mj-column-per-100 x_225906249mj-outlook-group-fix" style="font-size: 0px; text-align: left; direction: ltr; display: inline-block; vertical-align: top; width: 100%">
<table border="0" cellpadding="0" cellspacing="0" style="vertical-align: top" width="100%">
<tbody>
<tr>
<td align="left" class="x_225906249text x_225906249text-surtitle" style="font-size: 0px; padding: 0 24px; padding-bottom: 12px">
<div style="font-family: Greycliff, system-ui, -apple-system, &quot;Segoe UI&quot;, Roboto, Ubuntu, Cantarell, &quot;Noto Sans&quot;, sans-serif, &quot;Apple Color Emoji&quot;, &quot;Segoe UI Emoji&quot;, &quot;Segoe UI Symbol&quot;, &quot;Noto Color Emoji&quot;; font-size: 16px; letter-spacing: 0.12em; line-height: 24px; text-align: left; text-transform: uppercase; color: rgb(49, 89, 128)">Your Account</div>
</td>
</tr>
<tr>
<td align="left" class="x_225906249text x_225906249text-header-title" style="font-size: 0px; padding: 0 24px; padding-bottom: 24px">
<div style="font-family: &quot;Source Serif Pro&quot;, Georgia, Cambria, &quot;Times New Roman&quot;, Times, serif, &quot;Apple Color Emoji&quot;, &quot;Segoe UI Emoji&quot;, &quot;Segoe UI Symbol&quot;, &quot;Noto Color Emoji&quot;; font-size: 41px; font-weight: 600; line-height: 48px; text-align: left; color: rgb(0, 9, 19)">One-Time Passcode</div>
</td>
</tr>
<tr>
<td align="left" class="x_225906249text" style="font-size: 0px; padding: 0 24px">
<div style="font-family: Greycliff, system-ui, -apple-system, &quot;Segoe UI&quot;, Roboto, Ubuntu, Cantarell, &quot;Noto Sans&quot;, sans-serif, &quot;Apple Color Emoji&quot;, &quot;Segoe UI Emoji&quot;, &quot;Segoe UI Symbol&quot;, &quot;Noto Color Emoji&quot;; font-size: 16px; line-height: 24px; text-align: left; color: rgb(0, 9, 19)">Click the button below to access your secure login form, then enter your one-time passcode.</div>
</td>
</tr>
<tr>
<td align="left" class="x_225906249text x_225906249text-code" style="font-size: 0px; padding: 0 24px; padding-top: 24px">
<div style="font-family: &quot;Source Code Pro&quot;, &quot;ui-monospace&quot;, Menlo, Consolas, &quot;Roboto Mono&quot;, &quot;Ubuntu Monospace&quot;, &quot;Noto Mono&quot;, &quot;Oxygen Mono&quot;, &quot;Liberation Mono&quot;, monospace, &quot;Apple Color Emoji&quot;, &quot;Segoe UI Emoji&quot;, &quot;Segoe UI Symbol&quot;, &quot;Noto Color Emoji&quot; !important; font-size: 41px; font-weight: bold; letter-spacing: 0.12em; line-height: 48px; text-align: left; color: rgb(0, 48, 94)">{{ code }}</div>
</td>
</tr>
<tr>
<td align="left" class="x_225906249button" style="font-size: 0px; padding: 24px 24px">
<table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; line-height: 100%">
<tbody>
<tr>
<td align="center" bgcolor="#FCC800" style="border: none; border-radius: 6px; cursor: auto; background: rgb(252, 200, 0)" valign="middle">
<a href="{{ link }}" style="display: inline-block; background: rgb(252, 200, 0); color: rgb(0, 48, 94); font-family: Greycliff, system-ui, -apple-system, &quot;Segoe UI&quot;, Roboto, Ubuntu, Cantarell, &quot;Noto Sans&quot;, sans-serif, &quot;Apple Color Emoji&quot;, &quot;Segoe UI Emoji&quot;, &quot;Segoe UI Symbol&quot;, &quot;Noto Color Emoji&quot;; font-size: 16px; font-weight: bold; line-height: 24px; margin: 0; text-decoration: none; text-transform: none; padding: 18px 24px; border-radius: 6px" target="_blank"> Log in&nbsp;→ </a>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
<tr>
<td align="left" class="x_225906249text" style="font-size: 0px; padding: 0 24px">
<div style="font-family: Greycliff, system-ui, -apple-system, &quot;Segoe UI&quot;, Roboto, Ubuntu, Cantarell, &quot;Noto Sans&quot;, sans-serif, &quot;Apple Color Emoji&quot;, &quot;Segoe UI Emoji&quot;, &quot;Segoe UI Symbol&quot;, &quot;Noto Color Emoji&quot;; font-size: 16px; font-weight: bold; line-height: 24px; text-align: left">BuysForLife agents will never ask you for this code. Do not share this passcode with anyone for any reason.</div>
</td>
</tr>
<tr>
<td align="left" style="font-size: 0px; padding: 0 24px">
<div style="font-family: Greycliff, system-ui, -apple-system, &quot;Segoe UI&quot;, Roboto, Ubuntu, Cantarell, &quot;Noto Sans&quot;, sans-serif, &quot;Apple Color Emoji&quot;, &quot;Segoe UI Emoji&quot;, &quot;Segoe UI Symbol&quot;, &quot;Noto Color Emoji&quot;; font-size: 16px; line-height: 24px; text-align: left; color: rgb(0, 9, 19)"></div>
</td>
</tr>
</tbody>
</table>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</td>
</tr>
</tbody>
</table>

View file

@ -28,6 +28,14 @@
{% if readonly is not null %}
readonly
{% endif %}
{% if type == 'number' %}
{% if min is defined %}
min="{{ min }}"
{% endif %}
{% if max is defined %}
max="{{ max }}"
{% endif %}
{% endif %}
class="{{ colors.input }} {{ submit is defined ? 'rounded-l-lg border-r-0' : 'rounded-lg' }} w-full p-3 h-[42px] border focus:ring-1 focus:outline-none">
{% if submit is defined %}
{% include 'lib/button.twig' with {

View file

@ -0,0 +1,17 @@
<label for="{{ id }}" class="block text-sm font-medium text-gray-700 mt-2">
{{ label }}
</label>
<input type="number"
id="{{ id }}"
name="{{ name }}"
class="border rounded-lg p-2 w-full"
{% if required %} required {% endif %}
{% if min is defined %} min="{{ min }}" {% endif %}
{% if max is defined %} max="{{ max }}" {% endif %}
{% if step is defined %} step="{{ step }}" {% endif %}
{% if value is defined %} value="{{ value }}" {% endif %}
placeholder="{{ placeholder | default('Enter a number') }}">
{% if subtext is defined %}
<p class="text-xs text-gray-500">{{ subtext }}</p>
{% endif %}

View file

@ -2,7 +2,7 @@
<div class="text-lg font-semibold mb-4">Why do I have sats?</div>
<ul class="list-disc pl-6 mb-4">
<li>You may have received sats from a promotional event</li>
<li>You may have recieved sats sent to your default generated Lightning Address (LNURL)</li>
<li>You may have recieved sats sent to your default generated Lightning Address (LNURL): {{ user.npub }}@{{ http_host }}</li>
</ul>
<div class="text-lg font-semibold mb-4">What can I do with sats?</div>
<ul class="list-disc pl-6">

View file

@ -0,0 +1,8 @@
<label for="{{ id }}" class="block text-sm font-medium text-gray-700 mt-2">
{{ label }}
</label>
<select id="{{ id }}" name="{{ name }}" class="border rounded-lg p-2 w-full" {% if required %} required {% endif %}>
{% for option in options %}
<option value="{{ option.value }}" {% if option.value == value %} selected {% endif %}>{{ option.text }}</option>
{% endfor %}
</select>

View file

@ -0,0 +1,8 @@
<section class="flex flex-col gap-4">
Transaction Id: {{ tx.id }}
Date: {{ tx.date }}
User: {{ user.email }}
Transaction Type: {{ tx.type }}
Sats: {{ tx.sats }}
Cents: {{ tx.cents }}
</section>

View file

@ -1,3 +1,3 @@
module.exports = {
content: ["./src/**/*.twig", "./public/index.php"],
content: ["./src/**/*.twig", "./src/colors.php"],
};