From 27df1a73b52debe5d8712356baada7e8c7afb875 Mon Sep 17 00:00:00 2001
From: count-null <70529195+count-null@users.noreply.github.com>
Date: Sat, 15 Feb 2025 00:21:37 -0500
Subject: [PATCH] save
---
.env.example | 6 -
composer.json | 3 +-
composer.lock | 825 ++++++++++++++++++++++++++++++--
public/index.php | 13 +
src/app.php | 4 +
src/controllers/account.php | 92 +++-
src/controllers/magic_link.php | 65 +--
src/models/addresses.php | 58 ++-
src/models/cart_items.php | 53 ++
src/models/carts.php | 42 +-
src/models/magic_links.php | 80 +++-
src/models/order_items.php | 61 +++
src/models/orders.php | 67 ++-
src/models/products.php | 63 ++-
src/models/quote_items.php | 70 +++
src/models/quotes.php | 69 +++
src/models/subscriptions.php | 101 ++++
src/models/transactions.php | 82 +++-
src/models/user_addresses.php | 16 +-
src/models/users.php | 71 ++-
src/style.css | 29 --
src/views/account/billing.twig | 4 +-
src/views/account/index.twig | 8 +-
src/views/account/login.twig | 2 +-
src/views/account/shipping.twig | 4 +-
src/views/account/signup.twig | 11 +-
src/views/header.twig | 31 ++
src/views/lib/form/address.twig | 12 +-
28 files changed, 1695 insertions(+), 247 deletions(-)
create mode 100644 src/models/cart_items.php
create mode 100644 src/models/order_items.php
create mode 100644 src/models/quote_items.php
create mode 100644 src/models/quotes.php
create mode 100644 src/models/subscriptions.php
diff --git a/.env.example b/.env.example
index 9191d51..16cc6ca 100644
--- a/.env.example
+++ b/.env.example
@@ -11,9 +11,3 @@ SMTP_FROM="noreply@example.com"
# !! Choose your LN_SERVICE carefully!!
# NOTE: The LN_SERVICE must support LUD-21 Payment Verification
LN_ADDRESS="your@node.win"
-# maps.co for GeoCoder (postal address verification)
-# Get your free API key: https://geocode.maps.co
-GEOCODE_MAPS_CO_API_KEY="your-api-key-from-geocode.maps.co"
-# Plausible for privacy-respecting page analytics
-# https://github.com/plausible/analytics
-PLAUSIBLE_HOST="https://plausible.io/"
\ No newline at end of file
diff --git a/composer.json b/composer.json
index 96eee48..654ed34 100644
--- a/composer.json
+++ b/composer.json
@@ -10,6 +10,7 @@
"phpmailer/phpmailer": "^6.9.2",
"vlucas/phpdotenv": "^5.6",
"web-auth/webauthn-lib": "^5.0",
- "twig/twig": "^3.0"
+ "twig/twig": "^3.0",
+ "swentel/nostr-php": "^1.5"
}
}
diff --git a/composer.lock b/composer.lock
index f70a913..bc0d4d2 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,8 +4,54 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "ea128e458544f87060ad53880489fcc2",
+ "content-hash": "408b4a1daa73232eabf14c566a3e5d8d",
"packages": [
+ {
+ "name": "bitwasp/bech32",
+ "version": "v0.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/Bit-Wasp/bech32.git",
+ "reference": "e1ea58c848a4ec59d81b697b3dfe9cc99968d0e7"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/Bit-Wasp/bech32/zipball/e1ea58c848a4ec59d81b697b3dfe9cc99968d0e7",
+ "reference": "e1ea58c848a4ec59d81b697b3dfe9cc99968d0e7",
+ "shasum": ""
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^5.4.0",
+ "squizlabs/php_codesniffer": "^2.0.0"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "src/bech32.php"
+ ],
+ "psr-4": {
+ "BitWasp\\Bech32\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "Unlicense"
+ ],
+ "authors": [
+ {
+ "name": "Thomas Kerin",
+ "homepage": "https://thomaskerin.io",
+ "role": "Author"
+ }
+ ],
+ "description": "Pure (no dependencies) implementation of bech32",
+ "homepage": "https://github.com/bit-wasp/bech32",
+ "support": {
+ "issues": "https://github.com/Bit-Wasp/bech32/issues",
+ "source": "https://github.com/Bit-Wasp/bech32/tree/more-tests"
+ },
+ "time": "2018-02-05T22:23:47+00:00"
+ },
{
"name": "brick/math",
"version": "0.12.1",
@@ -618,6 +664,239 @@
},
"time": "2024-10-13T11:29:49+00:00"
},
+ {
+ "name": "phrity/net-stream",
+ "version": "2.1.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sirn-se/phrity-net-stream.git",
+ "reference": "e6ace997168bebcce814c95cd5c78c78663ae49a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sirn-se/phrity-net-stream/zipball/e6ace997168bebcce814c95cd5c78c78663ae49a",
+ "reference": "e6ace997168bebcce814c95cd5c78c78663ae49a",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^8.0",
+ "phrity/util-errorhandler": "^1.1",
+ "psr/http-factory": "^1.0",
+ "psr/http-message": "^1.1 | ^2.0"
+ },
+ "require-dev": {
+ "php-coveralls/php-coveralls": "^2.0",
+ "phpunit/phpunit": "^9.0 | ^10.0 | ^11.0",
+ "phrity/net-uri": "^2.0",
+ "squizlabs/php_codesniffer": "^3.5"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Phrity\\Net\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Sören Jensen",
+ "email": "sirn@sirn.se",
+ "homepage": "https://phrity.sirn.se"
+ }
+ ],
+ "description": "Socket stream classes implementing PSR-7 Stream and PSR-17 StreamFactory",
+ "homepage": "https://phrity.sirn.se/net-stream",
+ "keywords": [
+ "Socket",
+ "client",
+ "psr-17",
+ "psr-7",
+ "server",
+ "stream",
+ "stream factory"
+ ],
+ "support": {
+ "issues": "https://github.com/sirn-se/phrity-net-stream/issues",
+ "source": "https://github.com/sirn-se/phrity-net-stream/tree/2.1.2"
+ },
+ "time": "2025-01-09T08:07:34+00:00"
+ },
+ {
+ "name": "phrity/net-uri",
+ "version": "2.1.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sirn-se/phrity-net-uri.git",
+ "reference": "841190135af4fab18135226aaaf99ec5791377ac"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sirn-se/phrity-net-uri/zipball/841190135af4fab18135226aaaf99ec5791377ac",
+ "reference": "841190135af4fab18135226aaaf99ec5791377ac",
+ "shasum": ""
+ },
+ "require": {
+ "ext-mbstring": "*",
+ "php": "^8.0",
+ "psr/http-factory": "^1.0",
+ "psr/http-message": "^1.1 | ^2.0"
+ },
+ "require-dev": {
+ "php-coveralls/php-coveralls": "^2.0",
+ "phpunit/phpunit": "^9.0 | ^10.0 | ^11.0",
+ "phrity/util-errorhandler": "^1.1",
+ "squizlabs/php_codesniffer": "^3.5"
+ },
+ "suggest": {
+ "ext-intl": "Enables IDN conversion for non-ASCII domains"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Phrity\\Net\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Sören Jensen",
+ "email": "sirn@sirn.se",
+ "homepage": "https://phrity.sirn.se"
+ }
+ ],
+ "description": "PSR-7 Uri and PSR-17 UriFactory implementation",
+ "homepage": "https://phrity.sirn.se/net-uri",
+ "keywords": [
+ "psr-17",
+ "psr-7",
+ "uri",
+ "uri factory"
+ ],
+ "support": {
+ "issues": "https://github.com/sirn-se/phrity-net-uri/issues",
+ "source": "https://github.com/sirn-se/phrity-net-uri/tree/2.1.0"
+ },
+ "time": "2024-07-08T06:14:09+00:00"
+ },
+ {
+ "name": "phrity/util-errorhandler",
+ "version": "1.1.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sirn-se/phrity-util-errorhandler.git",
+ "reference": "483228156e06673963902b1cc1e6bd9541ab4d5e"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sirn-se/phrity-util-errorhandler/zipball/483228156e06673963902b1cc1e6bd9541ab4d5e",
+ "reference": "483228156e06673963902b1cc1e6bd9541ab4d5e",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.4 | ^8.0"
+ },
+ "require-dev": {
+ "php-coveralls/php-coveralls": "^2.0",
+ "phpunit/phpunit": "^9.0 | ^10.0 | ^11.0",
+ "squizlabs/php_codesniffer": "^3.5"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Phrity\\Util\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Sören Jensen",
+ "email": "sirn@sirn.se",
+ "homepage": "https://phrity.sirn.se"
+ }
+ ],
+ "description": "Inline error handler; catch and resolve errors for code block.",
+ "homepage": "https://phrity.sirn.se/util-errorhandler",
+ "keywords": [
+ "error",
+ "warning"
+ ],
+ "support": {
+ "issues": "https://github.com/sirn-se/phrity-util-errorhandler/issues",
+ "source": "https://github.com/sirn-se/phrity-util-errorhandler/tree/1.1.1"
+ },
+ "time": "2024-09-12T06:49:16+00:00"
+ },
+ {
+ "name": "phrity/websocket",
+ "version": "3.2.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sirn-se/websocket-php.git",
+ "reference": "6dceb42be8bfd500806a5508721ae14cf0d4d737"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sirn-se/websocket-php/zipball/6dceb42be8bfd500806a5508721ae14cf0d4d737",
+ "reference": "6dceb42be8bfd500806a5508721ae14cf0d4d737",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^8.1",
+ "phrity/net-stream": "^2.1",
+ "phrity/net-uri": "^2.1",
+ "psr/http-message": "^1.1 | ^2.0",
+ "psr/log": "^1.0 | ^2.0 | ^3.0"
+ },
+ "require-dev": {
+ "php-coveralls/php-coveralls": "^2.0",
+ "phpstan/phpstan": "^2.0",
+ "phpunit/phpunit": "^10.0 | ^11.0",
+ "phrity/net-mock": "^2.1",
+ "phrity/util-errorhandler": "^1.1",
+ "squizlabs/php_codesniffer": "^3.5"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "WebSocket\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "ISC"
+ ],
+ "authors": [
+ {
+ "name": "Fredrik Liljegren"
+ },
+ {
+ "name": "Sören Jensen",
+ "email": "sirn@sirn.se",
+ "homepage": "https://phrity.sirn.se"
+ }
+ ],
+ "description": "WebSocket client and server",
+ "homepage": "https://phrity.sirn.se/websocket",
+ "keywords": [
+ "client",
+ "server",
+ "websocket"
+ ],
+ "support": {
+ "issues": "https://github.com/sirn-se/websocket-php/issues",
+ "source": "https://github.com/sirn-se/websocket-php/tree/3.2.2"
+ },
+ "time": "2025-01-10T09:41:26+00:00"
+ },
{
"name": "psr/clock",
"version": "1.0.0",
@@ -769,6 +1048,114 @@
},
"time": "2019-01-08T18:20:26+00:00"
},
+ {
+ "name": "psr/http-factory",
+ "version": "1.1.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/http-factory.git",
+ "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a",
+ "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.1",
+ "psr/http-message": "^1.0 || ^2.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Http\\Message\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "https://www.php-fig.org/"
+ }
+ ],
+ "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories",
+ "keywords": [
+ "factory",
+ "http",
+ "message",
+ "psr",
+ "psr-17",
+ "psr-7",
+ "request",
+ "response"
+ ],
+ "support": {
+ "source": "https://github.com/php-fig/http-factory"
+ },
+ "time": "2024-04-15T12:06:14+00:00"
+ },
+ {
+ "name": "psr/http-message",
+ "version": "2.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/http-message.git",
+ "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71",
+ "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.2 || ^8.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Http\\Message\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "https://www.php-fig.org/"
+ }
+ ],
+ "description": "Common interface for HTTP messages",
+ "homepage": "https://github.com/php-fig/http-message",
+ "keywords": [
+ "http",
+ "http-message",
+ "psr",
+ "psr-7",
+ "request",
+ "response"
+ ],
+ "support": {
+ "source": "https://github.com/php-fig/http-message/tree/2.0"
+ },
+ "time": "2023-04-04T09:54:51+00:00"
+ },
{
"name": "psr/log",
"version": "3.0.2",
@@ -819,6 +1206,153 @@
},
"time": "2024-09-11T13:17:53+00:00"
},
+ {
+ "name": "simplito/bigint-wrapper-php",
+ "version": "1.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/simplito/bigint-wrapper-php.git",
+ "reference": "cf21ec76d33f103add487b3eadbd9f5033a25930"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/simplito/bigint-wrapper-php/zipball/cf21ec76d33f103add487b3eadbd9f5033a25930",
+ "reference": "cf21ec76d33f103add487b3eadbd9f5033a25930",
+ "shasum": ""
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "BI\\": "lib/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Simplito Team",
+ "email": "s.smyczynski@simplito.com",
+ "homepage": "https://simplito.com"
+ }
+ ],
+ "description": "Common interface for php_gmp and php_bcmath modules",
+ "support": {
+ "issues": "https://github.com/simplito/bigint-wrapper-php/issues",
+ "source": "https://github.com/simplito/bigint-wrapper-php/tree/1.0.0"
+ },
+ "time": "2018-02-27T12:38:08+00:00"
+ },
+ {
+ "name": "simplito/bn-php",
+ "version": "1.1.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/simplito/bn-php.git",
+ "reference": "83446756a81720eacc2ffb87ff97958431451fd6"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/simplito/bn-php/zipball/83446756a81720eacc2ffb87ff97958431451fd6",
+ "reference": "83446756a81720eacc2ffb87ff97958431451fd6",
+ "shasum": ""
+ },
+ "require": {
+ "simplito/bigint-wrapper-php": "~1.0.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "*"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "BN\\": "lib/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Simplito Team",
+ "email": "s.smyczynski@simplito.com",
+ "homepage": "https://simplito.com"
+ }
+ ],
+ "description": "Big number implementation compatible with bn.js",
+ "support": {
+ "issues": "https://github.com/simplito/bn-php/issues",
+ "source": "https://github.com/simplito/bn-php/tree/1.1.4"
+ },
+ "time": "2024-01-10T16:16:59+00:00"
+ },
+ {
+ "name": "simplito/elliptic-php",
+ "version": "1.0.12",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/simplito/elliptic-php.git",
+ "reference": "be321666781be2be2c89c79c43ffcac834bc8868"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/simplito/elliptic-php/zipball/be321666781be2be2c89c79c43ffcac834bc8868",
+ "reference": "be321666781be2be2c89c79c43ffcac834bc8868",
+ "shasum": ""
+ },
+ "require": {
+ "ext-gmp": "*",
+ "simplito/bn-php": "~1.1.0"
+ },
+ "require-dev": {
+ "phpbench/phpbench": "@dev",
+ "phpunit/phpunit": "*"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Elliptic\\": "lib/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Simplito Team",
+ "email": "s.smyczynski@simplito.com",
+ "homepage": "https://simplito.com"
+ }
+ ],
+ "description": "Fast elliptic curve cryptography",
+ "homepage": "https://github.com/simplito/elliptic-php",
+ "keywords": [
+ "Curve25519",
+ "ECDSA",
+ "Ed25519",
+ "EdDSA",
+ "cryptography",
+ "curve",
+ "curve25519-weier",
+ "ecc",
+ "ecdh",
+ "elliptic",
+ "nistp192",
+ "nistp224",
+ "nistp256",
+ "nistp384",
+ "nistp521",
+ "secp256k1"
+ ],
+ "support": {
+ "issues": "https://github.com/simplito/elliptic-php/issues",
+ "source": "https://github.com/simplito/elliptic-php/tree/1.0.12"
+ },
+ "time": "2024-01-09T14:57:04+00:00"
+ },
{
"name": "spomky-labs/cbor-php",
"version": "3.1.0",
@@ -1011,6 +1545,73 @@
],
"time": "2025-01-03T09:35:48+00:00"
},
+ {
+ "name": "swentel/nostr-php",
+ "version": "1.5.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/nostrver-se/nostr-php.git",
+ "reference": "6ef1e05da3845e352593c7d119fc00e716b3321f"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/nostrver-se/nostr-php/zipball/6ef1e05da3845e352593c7d119fc00e716b3321f",
+ "reference": "6ef1e05da3845e352593c7d119fc00e716b3321f",
+ "shasum": ""
+ },
+ "require": {
+ "bitwasp/bech32": "^0.0.1",
+ "ext-gmp": "*",
+ "ext-xml": "*",
+ "php": ">=8.1 <8.5",
+ "phrity/websocket": "^3.0",
+ "simplito/elliptic-php": "^1.0",
+ "uma/phpecc": "^0.2.0"
+ },
+ "require-dev": {
+ "friendsofphp/php-cs-fixer": "^3.51",
+ "phpunit/phpunit": "^10.5"
+ },
+ "bin": [
+ "bin/nostr-php"
+ ],
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "swentel\\nostr\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Hagens",
+ "email": "info@sebastix.nl",
+ "homepage": "https://sebastix.nl",
+ "role": "Developer & maintainer"
+ },
+ {
+ "name": "Kristof De Jaeger",
+ "homepage": "https://realize.be",
+ "role": "Original author"
+ }
+ ],
+ "description": "Nostr helper library for PHP",
+ "homepage": "https://nostr-php.dev",
+ "keywords": [
+ "library",
+ "nostr"
+ ],
+ "support": {
+ "chat": "https://t.me/nostr_php",
+ "issue": "https://github.com/swentel/nostr-php/issues",
+ "issues": "https://github.com/nostrver-se/nostr-php/issues",
+ "source": "https://github.com/nostrver-se/nostr-php/tree/push"
+ },
+ "time": "2025-01-04T21:46:58+00:00"
+ },
{
"name": "symfony/clock",
"version": "v7.2.0",
@@ -1783,16 +2384,16 @@
},
{
"name": "symfony/property-access",
- "version": "v7.2.0",
+ "version": "v7.2.3",
"source": {
"type": "git",
"url": "https://github.com/symfony/property-access.git",
- "reference": "3ae42efba01e45aaedecf5c93c8d6a3ab3a82276"
+ "reference": "b28732e315d81fbec787f838034de7d6c9b2b902"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/property-access/zipball/3ae42efba01e45aaedecf5c93c8d6a3ab3a82276",
- "reference": "3ae42efba01e45aaedecf5c93c8d6a3ab3a82276",
+ "url": "https://api.github.com/repos/symfony/property-access/zipball/b28732e315d81fbec787f838034de7d6c9b2b902",
+ "reference": "b28732e315d81fbec787f838034de7d6c9b2b902",
"shasum": ""
},
"require": {
@@ -1839,7 +2440,7 @@
"reflection"
],
"support": {
- "source": "https://github.com/symfony/property-access/tree/v7.2.0"
+ "source": "https://github.com/symfony/property-access/tree/v7.2.3"
},
"funding": [
{
@@ -1855,20 +2456,20 @@
"type": "tidelift"
}
],
- "time": "2024-09-26T12:28:35+00:00"
+ "time": "2025-01-17T10:56:55+00:00"
},
{
"name": "symfony/property-info",
- "version": "v7.2.2",
+ "version": "v7.2.3",
"source": {
"type": "git",
"url": "https://github.com/symfony/property-info.git",
- "reference": "1dfeb0dac7a99f7b3be42db9ccc299c5a6483fcf"
+ "reference": "dedb118fd588a92f226b390250b384d25f4192fe"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/property-info/zipball/1dfeb0dac7a99f7b3be42db9ccc299c5a6483fcf",
- "reference": "1dfeb0dac7a99f7b3be42db9ccc299c5a6483fcf",
+ "url": "https://api.github.com/repos/symfony/property-info/zipball/dedb118fd588a92f226b390250b384d25f4192fe",
+ "reference": "dedb118fd588a92f226b390250b384d25f4192fe",
"shasum": ""
},
"require": {
@@ -1879,7 +2480,9 @@
"conflict": {
"phpdocumentor/reflection-docblock": "<5.2",
"phpdocumentor/type-resolver": "<1.5.1",
- "symfony/dependency-injection": "<6.4"
+ "symfony/cache": "<6.4",
+ "symfony/dependency-injection": "<6.4",
+ "symfony/serializer": "<6.4"
},
"require-dev": {
"phpdocumentor/reflection-docblock": "^5.2",
@@ -1922,7 +2525,7 @@
"validator"
],
"support": {
- "source": "https://github.com/symfony/property-info/tree/v7.2.2"
+ "source": "https://github.com/symfony/property-info/tree/v7.2.3"
},
"funding": [
{
@@ -1938,20 +2541,20 @@
"type": "tidelift"
}
],
- "time": "2024-12-31T11:04:50+00:00"
+ "time": "2025-01-27T11:08:17+00:00"
},
{
"name": "symfony/serializer",
- "version": "v7.2.0",
+ "version": "v7.2.3",
"source": {
"type": "git",
"url": "https://github.com/symfony/serializer.git",
- "reference": "3f5ed9f5e6c02e3853109190ba38408f5e1d2dd0"
+ "reference": "320f30beb419ce4f96363ada5e225c41f1ef08ab"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/serializer/zipball/3f5ed9f5e6c02e3853109190ba38408f5e1d2dd0",
- "reference": "3f5ed9f5e6c02e3853109190ba38408f5e1d2dd0",
+ "url": "https://api.github.com/repos/symfony/serializer/zipball/320f30beb419ce4f96363ada5e225c41f1ef08ab",
+ "reference": "320f30beb419ce4f96363ada5e225c41f1ef08ab",
"shasum": ""
},
"require": {
@@ -2020,7 +2623,7 @@
"description": "Handles serializing and deserializing data structures, including object graphs, into array structures or other formats like XML and JSON.",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/serializer/tree/v7.2.0"
+ "source": "https://github.com/symfony/serializer/tree/v7.2.3"
},
"funding": [
{
@@ -2036,7 +2639,7 @@
"type": "tidelift"
}
],
- "time": "2024-11-25T15:21:05+00:00"
+ "time": "2025-01-29T07:13:55+00:00"
},
{
"name": "symfony/string",
@@ -2276,16 +2879,16 @@
},
{
"name": "twig/twig",
- "version": "v3.18.0",
+ "version": "v3.19.0",
"source": {
"type": "git",
"url": "https://github.com/twigphp/Twig.git",
- "reference": "acffa88cc2b40dbe42eaf3a5025d6c0d4600cc50"
+ "reference": "d4f8c2b86374f08efc859323dbcd95c590f7124e"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/twigphp/Twig/zipball/acffa88cc2b40dbe42eaf3a5025d6c0d4600cc50",
- "reference": "acffa88cc2b40dbe42eaf3a5025d6c0d4600cc50",
+ "url": "https://api.github.com/repos/twigphp/Twig/zipball/d4f8c2b86374f08efc859323dbcd95c590f7124e",
+ "reference": "d4f8c2b86374f08efc859323dbcd95c590f7124e",
"shasum": ""
},
"require": {
@@ -2340,7 +2943,7 @@
],
"support": {
"issues": "https://github.com/twigphp/Twig/issues",
- "source": "https://github.com/twigphp/Twig/tree/v3.18.0"
+ "source": "https://github.com/twigphp/Twig/tree/v3.19.0"
},
"funding": [
{
@@ -2352,7 +2955,165 @@
"type": "tidelift"
}
],
- "time": "2024-12-29T10:51:50+00:00"
+ "time": "2025-01-29T07:06:14+00:00"
+ },
+ {
+ "name": "uma/phpasn1",
+ "version": "v2.5.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/1ma/PHPASN1.git",
+ "reference": "dd805d3157ddc90515ee13562a9bb241c68f4f88"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/1ma/PHPASN1/zipball/dd805d3157ddc90515ee13562a9bb241c68f4f88",
+ "reference": "dd805d3157ddc90515ee13562a9bb241c68f4f88",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^8.1"
+ },
+ "require-dev": {
+ "php-coveralls/php-coveralls": "^2.0",
+ "phpunit/phpunit": "^9.6"
+ },
+ "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": {
+ "source": "https://github.com/1ma/PHPASN1/tree/v2.5.1"
+ },
+ "abandoned": "genkgo/php-asn1",
+ "time": "2024-11-29T17:34:36+00:00"
+ },
+ {
+ "name": "uma/phpecc",
+ "version": "v0.2.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/1ma/phpecc.git",
+ "reference": "e269be5eaef099fb46831037d097c647a6da2f69"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/1ma/phpecc/zipball/e269be5eaef099fb46831037d097c647a6da2f69",
+ "reference": "e269be5eaef099fb46831037d097c647a6da2f69",
+ "shasum": ""
+ },
+ "require": {
+ "ext-gmp": "*",
+ "php": "^8.0",
+ "uma/phpasn1": "^2.5.1"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.0",
+ "squizlabs/php_codesniffer": "^2.0",
+ "symfony/yaml": "^6.4 || ^7.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": "Ology Newswire, Inc",
+ "email": "protocol@vpsqr.com",
+ "homepage": "https://vpsqr.com/",
+ "role": "Maintainer"
+ }
+ ],
+ "description": "Temporary fork of public-square/phpecc",
+ "homepage": "https://github.com/1ma/phpecc",
+ "keywords": [
+ "Diffie",
+ "ECDSA",
+ "Hellman",
+ "curve",
+ "ecdh",
+ "elliptic",
+ "nistp192",
+ "nistp224",
+ "nistp256",
+ "nistp384",
+ "nistp521",
+ "phpecc",
+ "schnorr",
+ "secp256k1",
+ "secp256r1"
+ ],
+ "support": {
+ "source": "https://github.com/1ma/phpecc/tree/v0.2.1"
+ },
+ "abandoned": "paragonie/ecc",
+ "time": "2025-01-21T00:38:50+00:00"
},
{
"name": "vlucas/phpdotenv",
@@ -2522,16 +3283,16 @@
},
{
"name": "web-auth/webauthn-lib",
- "version": "5.0.1",
+ "version": "5.1.1",
"source": {
"type": "git",
"url": "https://github.com/web-auth/webauthn-lib.git",
- "reference": "2cc8262b885cf01eee3c4c10ca3985bdd2614c97"
+ "reference": "6b95b2b3902d943796c3c2bac2dd14af9d031fc2"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/web-auth/webauthn-lib/zipball/2cc8262b885cf01eee3c4c10ca3985bdd2614c97",
- "reference": "2cc8262b885cf01eee3c4c10ca3985bdd2614c97",
+ "url": "https://api.github.com/repos/web-auth/webauthn-lib/zipball/6b95b2b3902d943796c3c2bac2dd14af9d031fc2",
+ "reference": "6b95b2b3902d943796c3c2bac2dd14af9d031fc2",
"shasum": ""
},
"require": {
@@ -2592,7 +3353,7 @@
"webauthn"
],
"support": {
- "source": "https://github.com/web-auth/webauthn-lib/tree/5.0.1"
+ "source": "https://github.com/web-auth/webauthn-lib/tree/5.1.1"
},
"funding": [
{
@@ -2604,7 +3365,7 @@
"type": "patreon"
}
],
- "time": "2024-07-20T05:24:59+00:00"
+ "time": "2025-01-03T23:01:20+00:00"
},
{
"name": "webmozart/assert",
diff --git a/public/index.php b/public/index.php
index 406eaac..4e4c1bf 100644
--- a/public/index.php
+++ b/public/index.php
@@ -21,19 +21,31 @@ 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();
}
@@ -140,6 +152,7 @@ $controller = match ($route) {
'/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),
diff --git a/src/app.php b/src/app.php
index 3eef2ac..834a1c8 100644
--- a/src/app.php
+++ b/src/app.php
@@ -1,5 +1,6 @@
getMessage());
}
}
+
public static function send_mail($to, $from, $from_name, $subject, $message, $HTML_message)
{
$mail = new PHPMailer(exceptions: true);
@@ -40,6 +43,7 @@ class app
$mail->send();
ob_end_clean();
}
+
public static function sendJson($data, $status = 200)
{
http_response_code($status);
diff --git a/src/controllers/account.php b/src/controllers/account.php
index cb13e1e..26ec594 100644
--- a/src/controllers/account.php
+++ b/src/controllers/account.php
@@ -4,6 +4,7 @@ namespace app\controllers;
use app\models\addresses;
use app\models\users;
use app\models\user_addresses;
+use app\models\magic_links;
class account
{
@@ -12,8 +13,8 @@ class account
if (!isset($_SESSION['user_id'])) {
header('Location: /account/login');
}
- $email = $_SESSION['user_email'];
- $user = users::getByEmail($email);
+ $user_id = $_SESSION['user_id'];
+ $user = users::getById($user_id);
$default_shipping = null;
$default_billing = null;
$ship_addrs = [];
@@ -60,8 +61,8 @@ class account
$bill_id = addresses::add(
$bill['name'],
$bill['company'],
- $bill['street'],
- $bill['boxapt'],
+ $bill['addressLine1'],
+ $bill['addressLine2'],
$bill['city'],
$bill['state'],
$bill['zip'],
@@ -76,8 +77,8 @@ class account
$_SESSION['success'] = "Billing address saved!";
header('Location: /account/billing');
}
- $email = $_SESSION['user_email'];
- $user = users::getByEmail($email);
+ $user_id = $_SESSION['user_id'];
+ $user = users::getById($user_id);
$default_billing = null;
$bill_addrs = [];
$bill_addresses = user_addresses::getBillingByUserId($_SESSION['user_id']);
@@ -115,8 +116,42 @@ class account
header('Location: /account');
}
}
+
+ public static function email()
+ {
+ $user_id = $_SESSION['user_id'] ?? null;
+ if (empty($user_id)){
+ header('Location: /account/login');
+ }
+ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
+ $email = $_POST['email'] ?? null;
+ if (empty($email)) {
+ $_SESSION['error'] = "Enter your email to get a login link";
+ header('Location: /account');
+ exit;
+ } else {
+ $token = magic_links::add($email, $user_id);
+ users::updateReplaceEmailTokenById($user_id, $token);
+ header('Location: /account');
+ exit;
+ }
+ }
+ }
+
public static function login($defaults)
{
+ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
+ $email = $_POST['email'] ?? false;
+ if (empty($email)) {
+ $_SESSION['error'] = "Enter your email to get a login link";
+ header('Location: /account/login');
+ exit;
+ } else {
+ $token = magic_links::add($email, null);
+ header('Location: /account/login');
+ exit;
+ }
+ }
if (isset($_SESSION['user_id'])) {
header('Location: /account');
}
@@ -131,12 +166,14 @@ class account
]
]));
}
+
public static function logout()
{
session_unset();
session_destroy();
header('Location: /');
}
+
public static function orders($defaults)
{
if (!isset($_SESSION['user_id'])) {
@@ -185,8 +222,8 @@ class account
$ship_id = addresses::add(
$ship['name'],
$ship['company'],
- $ship['street'],
- $ship['boxapt'],
+ $ship['addressLine1'],
+ $ship['addressLine2'],
$ship['city'],
$ship['state'],
$ship['zip'],
@@ -201,8 +238,8 @@ class account
$_SESSION['success'] = "Shipping address saved!";
header('Location: /account/shipping');
}
- $email = $_SESSION['user_email'];
- $user = users::getByEmail($email);
+ $user_id = $_SESSION['user_id'];
+ $user = users::getById($user_id);
$addresses = user_addresses::getShippingByUserId($user['id']);
$default_shipping = null;
$ship_addrs = [];
@@ -235,27 +272,30 @@ class account
{
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
$email = $_POST['email'];
+ if (empty($email)) {
+ $_SESSION['error'] = 'Email is required.';
+ }
$existingUser = users::getByEmail($email);
if ($existingUser) {
$_SESSION['error'] = 'Email already exists. Please choose a different email or log in.';
+ $_SESSION['last_post'] = $_POST;
header('Location: /account/signup');
exit;
}
- if (empty($email)) {
- $_SESSION['error'] = 'Email is required.';
- }
- if (isset($_SESSION['error'])) {
+ $useShipping = $_POST['use_shipping'] ?? false;
+ $ship = addresses::validatePost("shipping");
+ if (!isset($ship['name'])){
+ $_SESSION['error'] = "Shipping address verification failed. Check your entry for errors.";
+ $_SESSION['last_post'] = $_POST;
header('Location: /account/signup');
}
- $useShipping = $_POST['use_shipping'] ?? false;
- if ($useShipping) {
- $ship = addresses::validatePost("shipping");
- } else {
- $ship = addresses::validatePost("shipping");
+ if (!$useShipping) {
$bill = addresses::validatePost("billing");
- }
- if (empty($email)) {
- $_SESSION['error'] = 'Email is required.';
+ if (!isset($bill['name'])){
+ $_SESSION['error'] = "Billing address verification failed. Check your entry for errors.";
+ $_SESSION['last_post'] = $_POST;
+ header('Location: /account/signup');
+ }
}
if (isset($_SESSION['error'])) {
$_SESSION['last_post'] = $_POST;
@@ -264,8 +304,8 @@ class account
$ship_id = addresses::add(
$ship['name'],
$ship['company'],
- $ship['street'],
- $ship['boxapt'],
+ $ship['addressLine1'],
+ $ship['addressLine2'],
$ship['city'],
$ship['state'],
$ship['zip'],
@@ -278,8 +318,8 @@ class account
$bill_id = addresses::add(
$bill['name'],
$bill['company'],
- $bill['street'],
- $bill['boxapt'],
+ $bill['addressLine1'],
+ $bill['addressLine2'],
$bill['city'],
$bill['state'],
$bill['zip'],
diff --git a/src/controllers/magic_link.php b/src/controllers/magic_link.php
index 11af968..8b38d84 100644
--- a/src/controllers/magic_link.php
+++ b/src/controllers/magic_link.php
@@ -8,60 +8,45 @@ class magic_link
{
public static function index()
{
- $email = $_GET['email'] ?? null;
$token = $_GET['token'] ?? null;
- $signup = $_GET['signup'] ?? null;
-
- if (empty($email) && empty($token)) {
- $_SESSION['error'] = "Enter your email to get a login link";
+ if (!$token) {
+ $_SESSION['error'] = "Invalid or expired link.";
header('Location: /account/login');
exit;
- }
-
- if ($email && empty($token) && empty($signup)) {
- $link = magic_links::add(email: $email);
- $subject = "Your Magic Sign-In Link";
- $message = "Copy and paste the link into your browser: $link";
- $HTML_message = "Click the link to sign in: $link";
- app::send_mail(to: $email, from: $_ENV['SMTP_FROM'], from_name: $_ENV['APP_NAME'], subject: $subject, message: $message, HTML_message: $HTML_message);
- $_SESSION['success'] = 'Link sent to your email!';
- header('Location: /account/login');
- exit;
- }
-
- if ($email && empty($token) && $signup == "1") {
- $link = magic_links::add(email: $email);
- $subject = "Your Magic Sign-In Link";
- $message = "Copy and paste the link into your browser: $link";
- $HTML_message = "Click the link to sign in: $link";
- app::send_mail(to: $email, from: $_ENV['SMTP_FROM'], from_name: $_ENV['APP_NAME'], subject: $subject, message: $message, HTML_message: $HTML_message);
- $_SESSION['success'] = 'Account created! Please check your email inbox for the verification link.';
- header('Location: /account/login');
- exit;
- }
-
- if ($token && empty($email)) {
- $link = magic_links::validate(token: $token);
-
+ } else {
+ $link = magic_links::validateToken(token: $token);
if (!$link) {
$_SESSION['error'] = "Invalid or expired link.";
header('Location: /account/login');
+ exit;
}
- // handle signup vs. login
- $user = users::getByEmail($link['email']);
- if ($user) {
+ $user = $link['user_id'] ? users::getById($link['user_id']) : users::getByEmail($link['email']);
+ if ($user) { // user with this email exists, log them in
$_SESSION['user_email'] = $link['email'];
$_SESSION['user_id'] = $user['id'];
if (!$user['verified']) {
users::verify($link['email']);
}
header('Location: /account');
- } else {
- // used to pre-fill email signup field
- $_SESSION['user_email'] = $link['email'];
- header('Location: /account/signup');
+ exit;
+ } else { // no users with this email
+ $user_replacing_email = users::getByReplaceEmailToken($token);
+ if ($user_replacing_email) { // user is replacing their email
+ $user_id = $user_replacing_email['id'];
+ users::updateEmailById($user_id, $link['email']);
+ $_SESSION['user_email'] = $link['email'];
+ $_SESSION['user_id'] = $user_id;
+ if (!$user['verified']) {
+ users::verify($link['email']);
+ }
+ header('Location: /account');
+ exit;
+ } else { // new user signup
+ $_SESSION['user_email'] = $link['email'];
+ header('Location: /account/signup');
+ exit;
+ }
}
- exit();
}
}
}
diff --git a/src/models/addresses.php b/src/models/addresses.php
index e29adaa..ecbfb81 100644
--- a/src/models/addresses.php
+++ b/src/models/addresses.php
@@ -10,8 +10,8 @@ class addresses
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
company TEXT,
- street TEXT NOT NULL,
- boxapt TEXT NOT NULL,
+ addressLine1 TEXT NOT NULL,
+ addressLine2 TEXT NOT NULL,
city TEXT NOT NULL,
state TEXT NOT NULL,
zip TEXT NOT NULL,
@@ -25,36 +25,52 @@ class addresses
{
$name = $_POST["{$type}_name"];
$company = $_POST["{$type}_company"] ?? null;
- $boxapt = $_POST["{$type}_boxapt"] ?? null;
- $street = $_POST["{$type}_street"];
+ $addressLine2 = $_POST["{$type}_addressLine2"] ?? null;
+ $addressLine1 = $_POST["{$type}_addressLine1"];
$city = $_POST["{$type}_city"];
$state = $_POST["{$type}_state"];
$zip = $_POST["{$type}_zip"];
$phone = $_POST["{$type}_phone"];
// check all required fields are set
- if (empty($name) || empty($street) || empty($city) || empty($state) || empty($zip) || empty($phone)) {
+ if (empty($name) || empty($addressLine1) || empty($city) || empty($state) || empty($zip) || empty($phone)) {
$_SESSION['error'] = "Missing required {$type} information.";
}
- // TODO: find a match using postal database and return that
+ $url = "https://nominatim.openstreetmap.org/search?" . http_build_query([
+ "q" => implode(" ", array_filter([$addressLine1, $addressLine2, $city, $state, $zip])),
+ "format" => "json",
+ "addressdetails" => 1,
+ "limit" => 1
+ ]);
+ $ch = curl_init();
+ curl_setopt($ch, CURLOPT_URL, $url);
+ curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
+ curl_setopt($ch, CURLOPT_USERAGENT, "AddressValidator/1.0");
+ $response = curl_exec($ch);
+ curl_close($ch);
+ $data = json_decode($response, true);
+ if (empty($data)) {
+ return ["error" => "Address not found"];
+ }
+ $addressDetails = $data[0]['address'];
return [
- 'name' => $name,
- 'company' => $company,
- 'street' => $street,
- 'boxapt' => $boxapt,
- 'city' => $city,
- 'state' => $state,
- 'zip' => $zip,
- 'phone' => $phone
+ 'name' => $name,
+ 'company' => $company,
+ "addressLine1" => ($addressDetails['house_number'] ?? '') . ' ' . ($addressDetails['building'] ?? '') . ' ' . ($addressDetails['road'] ?? ''),
+ "addressLine2" => $addressLine2,
+ "city" => $addressDetails['city'] ?? $addressDetails['town'] ?? $addressDetails['village'] ?? '',
+ "state" => $addressDetails['state_code'] ?? ($addressDetails['state'] ?? ''),
+ "zip" => $addressDetails['postcode'] ?? '',
+ 'phone' => $phone
];
}
- public static function add($name, $company, $street, $boxapt, $city, $state, $zip, $phone, $billing, $shipping)
+ public static function add($name, $company, $addressLine1, $addressLine2, $city, $state, $zip, $phone, $billing, $shipping)
{
$query = "INSERT INTO addresses (
name,
company,
- street,
- boxapt,
+ addressLine1,
+ addressLine2,
city,
state,
zip,
@@ -64,8 +80,8 @@ class addresses
) VALUES (
:name,
:company,
- :street,
- :boxapt,
+ :addressLine1,
+ :addressLine2,
:city,
:state,
:zip,
@@ -76,8 +92,8 @@ class addresses
$stmt = app::$db->prepare($query);
$stmt->bindParam(':name', $name);
$stmt->bindParam(':company', $company);
- $stmt->bindParam(':street', $street);
- $stmt->bindParam(':boxapt', $boxapt);
+ $stmt->bindParam(':addressLine1', $addressLine1);
+ $stmt->bindParam(':addressLine2', $addressLine2);
$stmt->bindParam(':city', $city);
$stmt->bindParam(':state', $state);
$stmt->bindParam(':zip', $zip);
diff --git a/src/models/cart_items.php b/src/models/cart_items.php
new file mode 100644
index 0000000..d12b485
--- /dev/null
+++ b/src/models/cart_items.php
@@ -0,0 +1,53 @@
+exec("CREATE TABLE IF NOT EXISTS cart_items (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ cart_id INTEGER NOT NULL,
+ product_id INTEGER NOT NULL,
+ quantity INTEGER NOT NULL CHECK(quantity > 0),
+ added_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ FOREIGN KEY (cart_id) REFERENCES carts(id) ON DELETE CASCADE,
+ FOREIGN KEY (product_id) REFERENCES products(id)
+ );");
+ }
+
+ public static function addItem(int $cartId, int $productId, int $quantity)
+ {
+ $stmt = app::$db->prepare("INSERT INTO cart_items (cart_id, product_id, quantity)
+ VALUES (:cart_id, :product_id, :quantity)");
+ $stmt->execute([
+ 'cart_id' => $cartId,
+ 'product_id' => $productId,
+ 'quantity' => $quantity
+ ]);
+ }
+
+ public static function updateItem(int $cartItemId, int $quantity)
+ {
+ $stmt = app::$db->prepare("UPDATE cart_items SET quantity = :quantity WHERE cart_item_id = :cart_item_id");
+ $stmt->execute([
+ 'cart_item_id' => $cartItemId,
+ 'quantity' => $quantity
+ ]);
+ }
+
+ public static function removeItem(int $cartItemId)
+ {
+ $stmt = app::$db->prepare("DELETE FROM cart_items WHERE cart_item_id = :cart_item_id");
+ $stmt->execute(['cart_item_id' => $cartItemId]);
+ }
+
+ public static function getCartItems(int $cartId): array
+ {
+ $stmt = app::$db->prepare("SELECT * FROM cart_items WHERE cart_id = :cart_id");
+ $stmt->execute(['cart_id' => $cartId]);
+ return $stmt->fetchAll();
+ }
+}
diff --git a/src/models/carts.php b/src/models/carts.php
index d9cdc70..73b4813 100644
--- a/src/models/carts.php
+++ b/src/models/carts.php
@@ -2,20 +2,42 @@
namespace app\models;
use app\app;
+
class carts
{
public static function init()
{
- app::$db->exec("CREATE TABLE IF NOT EXISTS order_items (
- order_item_id INTEGER PRIMARY KEY AUTOINCREMENT,
- order_id INTEGER NOT NULL,
- product_id INTEGER NOT NULL,
- quantity INTEGER NOT NULL CHECK(quantity > 0),
- price REAL NOT NULL CHECK(price >= 0),
- FOREIGN KEY (order_id) REFERENCES orders(order_id),
- FOREIGN KEY (product_id) REFERENCES products(product_id)
+ app::$db->exec("CREATE TABLE IF NOT EXISTS carts (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ user_id INTEGER NOT NULL,
+ short_id TEXT UNIQUE NOT NULL,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ FOREIGN KEY (user_id) REFERENCES users(id)
);");
}
+
+ public static function addCart(int $userId): string
+ {
+ return self::createCart($userId, 'user_cart_id');
+ }
+
+ public static function addSaved(int $userId): string
+ {
+ return self::createCart($userId, 'user_saved_for_later_id');
+ }
+
+ private static function createCart(int $userId, string $column): string
+ {
+ $characters = '123456789ABCDEFGHJKLMNPQRSTUVWXYZ';
+ $shortId = '';
+
+ for ($i = 0; $i < 6; $i++) {
+ $shortId .= $characters[random_int(0, strlen($characters) - 1)];
+ }
+
+ app::$db->prepare("INSERT INTO carts (user_id, short_id) VALUES (:user_id, :short_id)")
+ ->execute(['user_id' => $userId, 'short_id' => $shortId]);
+
+ return $shortId;
+ }
}
-
-
diff --git a/src/models/magic_links.php b/src/models/magic_links.php
index b70d4cc..83c89ac 100644
--- a/src/models/magic_links.php
+++ b/src/models/magic_links.php
@@ -8,38 +8,84 @@ class magic_links
{
app::$db->exec("CREATE TABLE IF NOT EXISTS magic_links (
id INTEGER PRIMARY KEY AUTOINCREMENT,
+ user_id INTEGER REFERENCES users(id),
email TEXT NOT NULL,
+ code TEXT NOT NULL,
token TEXT NOT NULL,
expires_at DATETIME NOT NULL,
used BOOLEAN DEFAULT FALSE
)");
}
- public static function add($email)
+ 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));
$expires_at = date('Y-m-d H:i:s', time() + 60 * 15);
- app::$db->query("INSERT INTO magic_links (
- email,
- token,
+ $query = "INSERT INTO magic_links (
+ email,
+ user_id,
+ token,
+ code,
expires_at
) VALUES (
- '$email',
- '$token',
- '$expires_at'
- )");
- return $_ENV['APP_HOST'] . "/magic-link?token=" . urlencode($token);
+ :email,
+ :user_id,
+ :token,
+ :code,
+ :expires_at
+ )";
+ $stmt = app::$db->prepare($query);
+ $stmt->bindParam(':email', $email);
+ $stmt->bindParam(':user_id', $user_id);
+ $stmt->bindParam(':token', $token);
+ $stmt->bindParam(':code', $code);
+ $stmt->bindParam(':expires_at', $expires_at);
+ $stmt->execute();
+ $link = $_ENV['APP_HOST'] . "/magic-link?token=" . urlencode($token);
+ $subject = "Your Magic Sign-In Link";
+ $message = "Enter this code into the sign-in form\n$code\n or copy-paste this link into your browser to sign in:\n$link";
+ $HTML_message = "Click the link to sign in: $link or enter this code:
$code";
+ app::send_mail(to: $email, from: $_ENV['SMTP_FROM'], from_name: $_ENV['APP_NAME'], subject: $subject, message: $message, HTML_message: $HTML_message);
+ $_SESSION['success'] = 'Link sent to your email!';
+ return $token;
}
- public static function validate($token)
+ public static function validateToken($token)
{
- $link = app::$db->query("SELECT * FROM magic_links
- WHERE token = '$token'
- AND used = FALSE
- AND expires_at > datetime('now')
- ")->fetch(\PDO::FETCH_ASSOC);
- // void the token once validated
- app::$db->query("UPDATE magic_links SET used = TRUE WHERE token = '$token'");
+ $query = "SELECT * FROM magic_links
+ WHERE token = :token
+ AND used = FALSE
+ AND expires_at > datetime('now')";
+ $stmt = app::$db->prepare($query);
+ $stmt->bindParam(':token', $token);
+ $stmt->execute();
+ $link = $stmt->fetch(\PDO::FETCH_ASSOC);
+
+ $updateQuery = "UPDATE magic_links SET used = TRUE WHERE token = :token";
+ $updateStmt = app::$db->prepare($updateQuery);
+ $updateStmt->bindParam(':token', $token);
+ $updateStmt->execute();
+
+ return $link;
+ }
+
+ public static function validateCode($code)
+ {
+ $query = "SELECT * FROM magic_links
+ WHERE code = :code
+ AND used = FALSE
+ AND expires_at > datetime('now')";
+ $stmt = app::$db->prepare($query);
+ $stmt->bindParam(':code', $code);
+ $stmt->execute();
+ $link = $stmt->fetch(\PDO::FETCH_ASSOC);
+
+ $updateQuery = "UPDATE magic_links SET used = TRUE WHERE code = :code";
+ $updateStmt = app::$db->prepare($updateQuery);
+ $updateStmt->bindParam(':code', $code);
+ $updateStmt->execute();
+
return $link;
}
}
diff --git a/src/models/order_items.php b/src/models/order_items.php
new file mode 100644
index 0000000..1d03b3e
--- /dev/null
+++ b/src/models/order_items.php
@@ -0,0 +1,61 @@
+exec("CREATE TABLE IF NOT EXISTS order_items (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ order_id INTEGER NOT NULL,
+ product_id INTEGER NOT NULL,
+ quantity INTEGER NOT NULL CHECK(quantity > 0),
+ added_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ FOREIGN KEY (order_id) REFERENCES orders(id) ON DELETE CASCADE,
+ FOREIGN KEY (product_id) REFERENCES products(id)
+ );");
+ }
+
+ public static function addItem(int $orderId, int $productId, int $quantity)
+ {
+ if ($quantity <= 0) {
+ throw new \InvalidArgumentException('Quantity must be greater than zero.');
+ }
+
+ $stmt = app::$db->prepare("INSERT INTO order_items (order_id, product_id, quantity)
+ VALUES (:order_id, :product_id, :quantity)");
+ $stmt->execute([
+ 'order_id' => $orderId,
+ 'product_id' => $productId,
+ 'quantity' => $quantity
+ ]);
+ }
+
+ public static function updateItem(int $orderItemId, int $quantity)
+ {
+ if ($quantity <= 0) {
+ throw new \InvalidArgumentException('Quantity must be greater than zero.');
+ }
+
+ $stmt = app::$db->prepare("UPDATE order_items SET quantity = :quantity WHERE order_item_id = :order_item_id");
+ $stmt->execute([
+ 'order_item_id' => $orderItemId,
+ 'quantity' => $quantity
+ ]);
+ }
+
+ public static function removeItem(int $orderItemId)
+ {
+ $stmt = app::$db->prepare("DELETE FROM order_items WHERE order_item_id = :order_item_id");
+ $stmt->execute(['order_item_id' => $orderItemId]);
+ }
+
+ public static function getOrderItems(int $orderId): array
+ {
+ $stmt = app::$db->prepare("SELECT * FROM order_items WHERE order_id = :order_id");
+ $stmt->execute(['order_id' => $orderId]);
+ return $stmt->fetchAll();
+ }
+}
diff --git a/src/models/orders.php b/src/models/orders.php
index 14b2296..0df971d 100644
--- a/src/models/orders.php
+++ b/src/models/orders.php
@@ -2,17 +2,72 @@
namespace app\models;
use app\app;
+
class orders
{
+ const STATUSES = [
+ 'SHIPPED', 'PENDING', 'HOLD', 'PARTIAL',
+ 'BACKORDER', 'FAILED', 'CANCELED', 'PROCESSING'
+ ];
+
public static function init()
{
app::$db->exec("CREATE TABLE IF NOT EXISTS orders (
- order_id INTEGER PRIMARY KEY AUTOINCREMENT,
- user_id INTEGER NOT NULL,
- total_amount REAL NOT NULL CHECK(total_amount >= 0),
- status TEXT NOT NULL CHECK(status IN ('pending', 'completed', 'cancelled')),
- created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
- FOREIGN KEY (user_id) REFERENCES users(user_id)
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ user_id INTEGER NOT NULL,
+ value_sats INTEGER NOT NULL CHECK(value_sats >= 0),
+ value_cents INTEGER NOT NULL CHECK(value_cents >= 0),
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ status TEXT CHECK(status IN ('" . implode("', '", self::STATUSES) . "')) NOT NULL DEFAULT 'PENDING',
+ FOREIGN KEY (user_id) REFERENCES users(id)
);");
}
+
+ public static function createOrder(int $userId, int $valueSats, int $valueCents, string $status = 'PENDING'): int
+ {
+ self::validateStatus($status);
+
+ $stmt = app::$db->prepare("INSERT INTO orders (user_id, value_sats, value_cents, status)
+ VALUES (:user_id, :value_sats, :value_cents, :status)");
+ $stmt->execute([
+ 'user_id' => $userId,
+ 'value_sats' => $valueSats,
+ 'value_cents' => $valueCents,
+ 'status' => $status
+ ]);
+
+ return app::$db->lastInsertId();
+ }
+
+ public static function updateStatus(int $orderId, string $status)
+ {
+ self::validateStatus($status);
+
+ $stmt = app::$db->prepare("UPDATE orders SET status = :status WHERE order_id = :order_id");
+ $stmt->execute([
+ 'order_id' => $orderId,
+ 'status' => $status
+ ]);
+ }
+
+ public static function getOrder(int $orderId): array
+ {
+ $stmt = app::$db->prepare("SELECT * FROM orders WHERE order_id = :order_id");
+ $stmt->execute(['order_id' => $orderId]);
+ return $stmt->fetch() ?: [];
+ }
+
+ public static function getUserOrders(int $userId): array
+ {
+ $stmt = app::$db->prepare("SELECT * FROM orders WHERE user_id = :user_id ORDER BY created_at DESC");
+ $stmt->execute(['user_id' => $userId]);
+ return $stmt->fetchAll();
+ }
+
+ private static function validateStatus(string $status)
+ {
+ if (!in_array($status, self::STATUSES, true)) {
+ throw new \InvalidArgumentException("Invalid order status: $status");
+ }
+ }
}
diff --git a/src/models/products.php b/src/models/products.php
index 45bf7ab..985c19b 100644
--- a/src/models/products.php
+++ b/src/models/products.php
@@ -2,16 +2,69 @@
namespace app\models;
use app\app;
+
class products
{
public static function init()
{
app::$db->exec("CREATE TABLE IF NOT EXISTS products (
- product_id INTEGER PRIMARY KEY AUTOINCREMENT,
- name TEXT NOT NULL,
- description TEXT,
- price REAL NOT NULL CHECK(price >= 0),
- qty INTEGER NOT NULL DEFAULT 0 CHECK(qty >= 0)
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ title TEXT NOT NULL,
+ desc TEXT,
+ stock_qty INTEGER NOT NULL DEFAULT 0 CHECK(stock_qty >= 0),
+ specs_json TEXT,
+ sats_price INTEGER NOT NULL DEFAULT 0 CHECK(sats_price >= 0),
+ cents_price INTEGER NOT NULL DEFAULT 0 CHECK(cents_price >= 0),
+ digital BOOLEAN NOT NULL DEFAULT 0,
+ subscription BOOLEAN NOT NULL DEFAULT 0,
+ image_url_0 TEXT,
+ image_url_1 TEXT,
+ image_url_2 TEXT,
+ image_url_3 TEXT,
+ image_url_4 TEXT,
+ image_url_5 TEXT,
+ image_url_6 TEXT,
+ image_url_7 TEXT,
+ image_url_8 TEXT,
+ image_url_9 TEXT,
+ image_url_10 TEXT,
+ image_url_11 TEXT
)");
}
+
+ public static function add($title, $desc, $stock_qty, $specs_json, $sats_price, $cents_price, $digital, $subscription, $images)
+ {
+ $stmt = app::$db->prepare("INSERT INTO products (
+ title, desc, stock_qty, specs_json, sats_price, cents_price, digital, subscription,
+ image_url_0, image_url_1, image_url_2, image_url_3, image_url_4, image_url_5,
+ image_url_6, image_url_7, image_url_8, image_url_9, image_url_10, image_url_11
+ ) VALUES (
+ :title, :desc, :stock_qty, :specs_json, :sats_price, :cents_price, :digital, :subscription,
+ :image_url_0, :image_url_1, :image_url_2, :image_url_3, :image_url_4, :image_url_5,
+ :image_url_6, :image_url_7, :image_url_8, :image_url_9, :image_url_10, :image_url_11
+ )");
+
+ $stmt->execute([
+ ':title' => $title,
+ ':desc' => $desc,
+ ':stock_qty' => $stock_qty,
+ ':specs_json' => $specs_json,
+ ':sats_price' => $sats_price,
+ ':cents_price' => $cents_price,
+ ':digital' => (int) $digital,
+ ':subscription' => (int) $subscription,
+ ':image_url_0' => $images[0] ?? null,
+ ':image_url_1' => $images[1] ?? null,
+ ':image_url_2' => $images[2] ?? null,
+ ':image_url_3' => $images[3] ?? null,
+ ':image_url_4' => $images[4] ?? null,
+ ':image_url_5' => $images[5] ?? null,
+ ':image_url_6' => $images[6] ?? null,
+ ':image_url_7' => $images[7] ?? null,
+ ':image_url_8' => $images[8] ?? null,
+ ':image_url_9' => $images[9] ?? null,
+ ':image_url_10' => $images[10] ?? null,
+ ':image_url_11' => $images[11] ?? null
+ ]);
+ }
}
diff --git a/src/models/quote_items.php b/src/models/quote_items.php
new file mode 100644
index 0000000..9df3cf3
--- /dev/null
+++ b/src/models/quote_items.php
@@ -0,0 +1,70 @@
+exec("CREATE TABLE IF NOT EXISTS quote_items (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ quote_id INTEGER NOT NULL,
+ product_id INTEGER NOT NULL,
+ quantity INTEGER NOT NULL CHECK(quantity > 0),
+ price REAL NOT NULL CHECK(price >= 0),
+ added_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ FOREIGN KEY (quote_id) REFERENCES quotes(id) ON DELETE CASCADE,
+ FOREIGN KEY (product_id) REFERENCES products(id)
+ );");
+ }
+
+ public static function addItem(int $quoteId, int $productId, int $quantity, float $price)
+ {
+ if ($quantity <= 0) {
+ throw new \InvalidArgumentException('Quantity must be greater than zero.');
+ }
+ if ($price < 0) {
+ throw new \InvalidArgumentException('Price must be non-negative.');
+ }
+
+ $stmt = app::$db->prepare("INSERT INTO quote_items (quote_id, product_id, quantity, price)
+ VALUES (:quote_id, :product_id, :quantity, :price)");
+ $stmt->execute([
+ 'quote_id' => $quoteId,
+ 'product_id' => $productId,
+ 'quantity' => $quantity,
+ 'price' => $price
+ ]);
+ }
+
+ public static function updateItem(int $quoteItemId, int $quantity, float $price)
+ {
+ if ($quantity <= 0) {
+ throw new \InvalidArgumentException('Quantity must be greater than zero.');
+ }
+ if ($price < 0) {
+ throw new \InvalidArgumentException('Price must be non-negative.');
+ }
+
+ $stmt = app::$db->prepare("UPDATE quote_items SET quantity = :quantity, price = :price WHERE quote_item_id = :quote_item_id");
+ $stmt->execute([
+ 'quote_item_id' => $quoteItemId,
+ 'quantity' => $quantity,
+ 'price' => $price
+ ]);
+ }
+
+ public static function removeItem(int $quoteItemId)
+ {
+ $stmt = app::$db->prepare("DELETE FROM quote_items WHERE quote_item_id = :quote_item_id");
+ $stmt->execute(['quote_item_id' => $quoteItemId]);
+ }
+
+ public static function getQuoteItems(int $quoteId): array
+ {
+ $stmt = app::$db->prepare("SELECT * FROM quote_items WHERE quote_id = :quote_id");
+ $stmt->execute(['quote_id' => $quoteId]);
+ return $stmt->fetchAll();
+ }
+}
diff --git a/src/models/quotes.php b/src/models/quotes.php
new file mode 100644
index 0000000..3d8e286
--- /dev/null
+++ b/src/models/quotes.php
@@ -0,0 +1,69 @@
+exec("CREATE TABLE IF NOT EXISTS quotes (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ user_id INTEGER NOT NULL,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ status TEXT CHECK(status IN ('" . implode("', '", self::STATUSES) . "')) NOT NULL DEFAULT 'DRAFT',
+ FOREIGN KEY (user_id) REFERENCES users(id)
+ );");
+ }
+
+ public static function createQuote(int $userId, string $status = 'DRAFT'): int
+ {
+ self::validateStatus($status);
+
+ $stmt = app::$db->prepare("INSERT INTO quotes (user_id, status)
+ VALUES (:user_id, :status)");
+ $stmt->execute([
+ 'user_id' => $userId,
+ 'status' => $status
+ ]);
+
+ return app::$db->lastInsertId();
+ }
+
+ public static function updateStatus(int $quoteId, string $status)
+ {
+ self::validateStatus($status);
+
+ $stmt = app::$db->prepare("UPDATE quotes SET status = :status WHERE quote_id = :quote_id");
+ $stmt->execute([
+ 'quote_id' => $quoteId,
+ 'status' => $status
+ ]);
+ }
+
+ public static function getQuote(int $quoteId): array
+ {
+ $stmt = app::$db->prepare("SELECT * FROM quotes WHERE quote_id = :quote_id");
+ $stmt->execute(['quote_id' => $quoteId]);
+ return $stmt->fetch() ?: [];
+ }
+
+ public static function getUserQuotes(int $userId): array
+ {
+ $stmt = app::$db->prepare("SELECT * FROM quotes WHERE user_id = :user_id ORDER BY created_at DESC");
+ $stmt->execute(['user_id' => $userId]);
+ return $stmt->fetchAll();
+ }
+
+ private static function validateStatus(string $status)
+ {
+ if (!in_array($status, self::STATUSES, true)) {
+ throw new \InvalidArgumentException("Invalid quote status: $status");
+ }
+ }
+}
diff --git a/src/models/subscriptions.php b/src/models/subscriptions.php
new file mode 100644
index 0000000..04eeb66
--- /dev/null
+++ b/src/models/subscriptions.php
@@ -0,0 +1,101 @@
+exec("CREATE TABLE IF NOT EXISTS subscriptions (
+ subscription_id INTEGER PRIMARY KEY AUTOINCREMENT,
+ user_id INTEGER NOT NULL,
+ product_id INTEGER NOT NULL,
+ start_date DATETIME NOT NULL,
+ renews_at DATETIME NOT NULL,
+ status TEXT CHECK(status IN ('" . implode("', '", self::STATUS) . "')) NOT NULL DEFAULT 'COMPLETED',
+ state TEXT CHECK(state IN ('" . implode("', '", self::STATES) . "')) NOT NULL DEFAULT 'TRIAL',
+ invoice_date DATETIME NOT NULL,
+ FOREIGN KEY (user_id) REFERENCES users(id),
+ FOREIGN KEY (product_id) REFERENCES products(id)
+ );");
+ }
+
+ public static function createSubscription(int $userId, int $productId, string $state = 'TRIAL', string $status = 'COMPLETED', string $startDate, string $renewAt, string $invoiceDate): int
+ {
+ self::validateState($state);
+ self::validateStatus($status);
+
+ $stmt = app::$db->prepare("INSERT INTO subscriptions (user_id, product_id, state, status, start_date, renews_at, invoice_date)
+ VALUES (:user_id, :product_id, :state, :status, :start_date, :renews_at, :invoice_date)");
+ $stmt->execute([
+ 'user_id' => $userId,
+ 'product_id' => $productId,
+ 'state' => $state,
+ 'status' => $status,
+ 'start_date' => $startDate,
+ 'renews_at' => $renewAt,
+ 'invoice_date' => $invoiceDate
+ ]);
+
+ return app::$db->lastInsertId();
+ }
+
+ public static function updateState(int $subscriptionId, string $state)
+ {
+ self::validateState($state);
+
+ $stmt = app::$db->prepare("UPDATE subscriptions SET state = :state WHERE subscription_id = :subscription_id");
+ $stmt->execute([
+ 'subscription_id' => $subscriptionId,
+ 'state' => $state
+ ]);
+ }
+
+ public static function updateStatus(int $subscriptionId, string $status)
+ {
+ self::validateStatus($status);
+
+ $stmt = app::$db->prepare("UPDATE subscriptions SET status = :status WHERE subscription_id = :subscription_id");
+ $stmt->execute([
+ 'subscription_id' => $subscriptionId,
+ 'status' => $status
+ ]);
+ }
+
+ public static function getSubscription(int $subscriptionId): array
+ {
+ $stmt = app::$db->prepare("SELECT * FROM subscriptions WHERE subscription_id = :subscription_id");
+ $stmt->execute(['subscription_id' => $subscriptionId]);
+ return $stmt->fetch() ?: [];
+ }
+
+ public static function getUserSubscriptions(int $userId): array
+ {
+ $stmt = app::$db->prepare("SELECT * FROM subscriptions WHERE user_id = :user_id ORDER BY start_date DESC");
+ $stmt->execute(['user_id' => $userId]);
+ return $stmt->fetchAll();
+ }
+
+ private static function validateState(string $state)
+ {
+ if (!in_array($state, self::STATES, true)) {
+ throw new \InvalidArgumentException("Invalid subscription state: $state");
+ }
+ }
+
+ private static function validateStatus(string $status)
+ {
+ if (!in_array($status, self::STATUS, true)) {
+ throw new \InvalidArgumentException("Invalid subscription status: $status");
+ }
+ }
+}
diff --git a/src/models/transactions.php b/src/models/transactions.php
index 4bc3450..adc3d3d 100644
--- a/src/models/transactions.php
+++ b/src/models/transactions.php
@@ -2,53 +2,89 @@
namespace app\models;
use app\app;
+
class transactions
{
+ const TYPES = ['CREDIT', 'REWARD', 'REDEEM', 'REVOKE', 'DEPOSIT'];
+
public static function init()
{
- app::$db->exec("CREATE TABLE transactions (
+ $typesList = "'" . implode("', '", self::TYPES) . "'";
+ app::$db->exec("CREATE TABLE IF NOT EXISTS transactions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
- type TEXT CHECK(transaction_type IN ('credit', 'spend', 'withdraw')) NOT NULL,
- cents REAL DEFAULT 0,
- sats REAL DEFAULT 0,
+ type TEXT CHECK(type IN ($typesList)) NOT NULL,
+ cents INTEGER DEFAULT 0,
+ sats INTEGER DEFAULT 0,
date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id)
)");
}
- public static function add($user_id, $transaction_type, $cents, $sats_amount)
+ public static function add($user_id, $transaction_type, $cents, $sats)
{
- $query = "INSERT INTO transactions (
- user_id,
- type,
- cents,
- sats
- ) VALUES (
- :user_id,
- :transaction_type,
- :cents,
- :sats
- )";
+ if (!in_array($transaction_type, self::TYPES)) {
+ throw new \Exception("Invalid transaction type.");
+ }
+ 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);
$stmt->bindParam(':transaction_type', $transaction_type);
$stmt->bindParam(':cents', $cents);
- $stmt->bindParam(':sats', $sats_amount);
+ $stmt->bindParam(':sats', $sats);
$stmt->execute();
+
return app::$db->lastInsertId();
}
public static function getUserBalance($user_id)
{
- $query = "SELECT SUM(cents) AS total_cents,
- SUM(sats) AS total_sats
- FROM transactions
- WHERE user_id = :user_id";
+ $query = "SELECT COALESCE(SUM(cents), 0) AS total_cents, COALESCE(SUM(sats), 0) AS total_sats FROM transactions WHERE user_id = :user_id";
$stmt = app::$db->prepare($query);
$stmt->bindParam(':user_id', $user_id);
$stmt->execute();
- $result = $stmt->fetch();
- return $result;
+ return $stmt->fetch();
+ }
+
+ public static function getRecent($n)
+ {
+ $query = "SELECT * FROM transactions ORDER BY date DESC LIMIT :n";
+ $stmt = app::$db->prepare($query);
+ $stmt->bindParam(':n', $n, \PDO::PARAM_INT);
+ $stmt->execute();
+ return $stmt->fetchAll();
+ }
+
+ public static function getWhales($n, $currency)
+ {
+ if (!in_array($currency, ['cents', 'sats'])) {
+ throw new \Exception("Invalid currency type.");
+ }
+ $query = "SELECT user_id, COALESCE(SUM($currency), 0) AS total FROM transactions GROUP BY user_id ORDER BY total DESC LIMIT :n";
+ $stmt = app::$db->prepare($query);
+ $stmt->bindParam(':n', $n, \PDO::PARAM_INT);
+ $stmt->execute();
+ return $stmt->fetchAll();
+ }
+
+ public static function liabilities($currency)
+ {
+ if (!in_array($currency, ['cents', 'sats'])) {
+ throw new \Exception("Invalid currency type.");
+ }
+ $query = "SELECT COALESCE(SUM($currency), 0) AS total FROM transactions";
+ $stmt = app::$db->prepare($query);
+ $stmt->execute();
+ return $stmt->fetchColumn();
}
}
diff --git a/src/models/user_addresses.php b/src/models/user_addresses.php
index 418f678..4de093d 100644
--- a/src/models/user_addresses.php
+++ b/src/models/user_addresses.php
@@ -18,19 +18,27 @@ class user_addresses
public static function getShippingByUserId($id)
{
- $addrs = app::$db->query("SELECT a.* FROM users u
+ $query = "SELECT a.* FROM users u
JOIN user_addresses ua ON u.id = ua.user_id
JOIN addresses a ON ua.address_id = a.id
- WHERE u.id = '$id' AND a.shipping = 1")->fetch(\PDO::FETCH_ASSOC);
+ WHERE u.id = :id AND a.shipping = 1";
+ $stmt = app::$db->prepare($query);
+ $stmt->bindParam(':id', $id);
+ $stmt->execute();
+ $addrs = $stmt->fetch(\PDO::FETCH_ASSOC);
return [$addrs];
}
public static function getBillingByUserId($id)
{
- $addrs = app::$db->query("SELECT a.* FROM users u
+ $query = "SELECT a.* FROM users u
JOIN user_addresses ua ON u.id = ua.user_id
JOIN addresses a ON ua.address_id = a.id
- WHERE u.id = '$id' AND a.billing = 1")->fetch(\PDO::FETCH_ASSOC);
+ WHERE u.id = :id AND a.billing = 1";
+ $stmt = app::$db->prepare($query);
+ $stmt->bindParam(':id', $id);
+ $stmt->execute();
+ $addrs = $stmt->fetch(\PDO::FETCH_ASSOC);
return [$addrs];
}
diff --git a/src/models/users.php b/src/models/users.php
index 4504003..c4c0fe4 100644
--- a/src/models/users.php
+++ b/src/models/users.php
@@ -2,6 +2,8 @@
namespace app\models;
use app\app;
+use swentel\nostr\Key\Key;
+
class users
{
public static function init()
@@ -14,15 +16,44 @@ class users
opt_in_promotional BOOLEAN NOT NULL,
verified BOOLEAN NOT NULL,
dark_theme BOOLEAN NOT NULL,
- generated_base58 TEXT UNIQUE,
+ nsec TEXT,
+ npub TEXT NOT NULL,
attached_lightning_address TEXT,
+ replace_email_token TEXT,
name TEXT,
company_name TEXT,
company_type TEXT,
company_size TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)");
- app::$db->exec('CREATE INDEX IF NOT EXISTS idx_user_email ON users (email)');
+ }
+
+ public static function updateReplaceEmailTokenById($user_id, $replace_token)
+ {
+ $query = "UPDATE users SET replace_email_token = :replace_token WHERE id = :user_id";
+ $stmt = app::$db->prepare($query);
+ $stmt->bindParam(':replace_token', $replace_token);
+ $stmt->bindParam(':user_id', $user_id);
+ $stmt->execute();
+ }
+
+ public static function updateEmailById($user_id, $email)
+ {
+ $query = "UPDATE users SET email = :email WHERE id = :user_id";
+ $stmt = app::$db->prepare($query);
+ $stmt->bindParam(':email', $email);
+ $stmt->bindParam(':user_id', $user_id);
+ $stmt->execute();
+ users::updateReplaceEmailTokenById($user_id, null);
+ }
+
+ public static function getByReplaceEmailToken($token)
+ {
+ $query = "SELECT * FROM users WHERE replace_email_token = :token";
+ $stmt = app::$db->prepare($query);
+ $stmt->bindParam(':token', $token);
+ $stmt->execute();
+ return $stmt->fetch(\PDO::FETCH_ASSOC);
}
public static function updateProfileById($user_id, $post)
@@ -44,20 +75,29 @@ class users
public static function add($email, $ship_id, $bill_id, $opt_in_promotional, $verified, $dark_theme)
{
+ $key = new Key();
+ $private_key = $key->generatePrivateKey();
+ $public_key = $key->getPublicKey($private_key);
+ $npub = $key->convertPublicKeyToBech32($public_key);
+ $nsec = $key->convertPrivateKeyToBech32($private_key);
$query = "INSERT INTO users (
email,
shipping_address_id,
billing_address_id,
opt_in_promotional,
verified,
- dark_theme
+ dark_theme,
+ nsec,
+ npub
) VALUES (
:email,
:shipping_address_id,
:billing_address_id,
:opt_in_promotional,
:verified,
- :dark_theme
+ :dark_theme,
+ :nsec,
+ :npub
)";
$stmt = app::$db->prepare($query);
$stmt->bindParam(':email', $email);
@@ -66,18 +106,35 @@ class users
$stmt->bindParam(':opt_in_promotional', $opt_in_promotional);
$stmt->bindParam(':verified', $verified);
$stmt->bindParam(':dark_theme', $dark_theme);
+ $stmt->bindParam(':nsec', $nsec);
+ $stmt->bindParam(':npub', $npub);
$stmt->execute();
return app::$db->lastInsertId();
-
}
public static function verify($email)
{
- app::$db->exec("UPDATE users SET verified = 1 WHERE email = '$email'");
+ $query = "UPDATE users SET verified = 1 WHERE email = :email";
+ $stmt = app::$db->prepare($query);
+ $stmt->bindParam(':email', $email);
+ $stmt->execute();
+ }
+
+ public static function getById($id)
+ {
+ $query = "SELECT * FROM users WHERE id = :id";
+ $stmt = app::$db->prepare($query);
+ $stmt->bindParam(':id', $id);
+ $stmt->execute();
+ return $stmt->fetch(\PDO::FETCH_ASSOC);
}
public static function getByEmail($email)
{
- return app::$db->query("SELECT * FROM users WHERE email = '$email'")->fetch(\PDO::FETCH_ASSOC);
+ $query = "SELECT * FROM users WHERE email = :email";
+ $stmt = app::$db->prepare($query);
+ $stmt->bindParam(':email', $email);
+ $stmt->execute();
+ return $stmt->fetch(\PDO::FETCH_ASSOC);
}
}
diff --git a/src/style.css b/src/style.css
index 7ceb693..b5c61c9 100644
--- a/src/style.css
+++ b/src/style.css
@@ -1,32 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
-/* #Mega Menu Styles
- –––––––––––––––––––––––––––––––––––––––––––––––––– */
-.mega-menu {
- opacity: 0;
- visibility: hidden;
- z-index: -900;
- left: 0;
- top: 38px;
- position: absolute;
- text-align: left;
- width: 100%;
- transition: all 0.15s linear 0s;
-}
-/* #hoverable Class Styles */
-.hoverable {
- position: static;
-}
-.hoverable > a:after {
- content: "\25BC";
- font-size: 10px;
- padding-left: 6px;
- position: relative;
- top: -1px;
-}
-.hoverable:hover .mega-menu {
- opacity: 1;
- visibility: visible;
- z-index: 900;
-}
diff --git a/src/views/account/billing.twig b/src/views/account/billing.twig
index a45938c..75c9c6b 100644
--- a/src/views/account/billing.twig
+++ b/src/views/account/billing.twig
@@ -17,8 +17,8 @@