save
This commit is contained in:
parent
27df1a73b5
commit
a0cb5fb6b0
|
@ -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"
|
||||
|
|
|
@ -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
505
composer.lock
generated
|
@ -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",
|
||||
|
|
167
public/index.php
167
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
|
||||
|
|
31
src/app.php
31
src/app.php
|
@ -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
58
src/colors.php
Normal 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"
|
||||
],
|
||||
];
|
|
@ -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
184
src/controllers/admin.php
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
172
src/controllers/transaction.php
Normal file
172
src/controllers/transaction.php
Normal 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
94
src/models/emails.php
Normal 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
96
src/models/invoices.php
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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.");
|
||||
|
|
|
@ -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";
|
||||
|
|
25
src/scripts/check_all_invoices.php
Normal file
25
src/scripts/check_all_invoices.php
Normal 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();
|
||||
|
21
src/scripts/check_subscriptions.php
Normal file
21
src/scripts/check_subscriptions.php
Normal 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
60
src/scripts/init_db.php
Normal 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();
|
70
src/views/account/verify.twig
Normal file
70
src/views/account/verify.twig
Normal 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>
|
27
src/views/admin/emails.twig
Normal file
27
src/views/admin/emails.twig
Normal 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>
|
10
src/views/admin/index.twig
Normal file
10
src/views/admin/index.twig
Normal 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>
|
3
src/views/admin/orders.twig
Normal file
3
src/views/admin/orders.twig
Normal file
|
@ -0,0 +1,3 @@
|
|||
<section class="flex flex-col gap-4">
|
||||
ORDERS
|
||||
</section>
|
3
src/views/admin/returns.twig
Normal file
3
src/views/admin/returns.twig
Normal file
|
@ -0,0 +1,3 @@
|
|||
<section class="flex flex-col gap-4">
|
||||
RETURNS
|
||||
</section>
|
61
src/views/admin/transactions/add.twig
Normal file
61
src/views/admin/transactions/add.twig
Normal 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>
|
144
src/views/admin/transactions/index.twig
Normal file
144
src/views/admin/transactions/index.twig
Normal 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>
|
3
src/views/admin/users.twig
Normal file
3
src/views/admin/users.twig
Normal file
|
@ -0,0 +1,3 @@
|
|||
<section class="flex flex-col gap-4">
|
||||
USERS
|
||||
</section>
|
|
@ -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">
|
||||
|
|
|
@ -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
|
||||
|
|
89
src/views/lib/emails/verify.twig
Normal file
89
src/views/lib/emails/verify.twig
Normal 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, "Segoe UI", Roboto, Ubuntu, Cantarell, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; 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: "Source Serif Pro", Georgia, Cambria, "Times New Roman", Times, serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; 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, "Segoe UI", Roboto, Ubuntu, Cantarell, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; 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: "Source Code Pro", "ui-monospace", Menlo, Consolas, "Roboto Mono", "Ubuntu Monospace", "Noto Mono", "Oxygen Mono", "Liberation Mono", monospace, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji" !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, "Segoe UI", Roboto, Ubuntu, Cantarell, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; 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 → </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, "Segoe UI", Roboto, Ubuntu, Cantarell, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; 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, "Segoe UI", Roboto, Ubuntu, Cantarell, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; 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>
|
|
@ -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 {
|
||||
|
|
17
src/views/lib/number_input.twig
Normal file
17
src/views/lib/number_input.twig
Normal 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 %}
|
|
@ -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">
|
||||
|
|
8
src/views/lib/select.twig
Normal file
8
src/views/lib/select.twig
Normal 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>
|
8
src/views/transaction.twig
Normal file
8
src/views/transaction.twig
Normal 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>
|
|
@ -1,3 +1,3 @@
|
|||
module.exports = {
|
||||
content: ["./src/**/*.twig", "./public/index.php"],
|
||||
content: ["./src/**/*.twig", "./src/colors.php"],
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue