save
This commit is contained in:
parent
27df1a73b5
commit
a0cb5fb6b0
36 changed files with 1886 additions and 187 deletions
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,37 +10,12 @@ 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)
|
||||
{
|
||||
|
|
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);
|
||||
|
|
|
@ -20,6 +20,15 @@ class transactions
|
|||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
)");
|
||||
}
|
||||
|
||||
public static function getById($id)
|
||||
{
|
||||
$query = "SELECT * FROM transactions WHERE id = :id";
|
||||
$stmt = app::$db->prepare($query);
|
||||
$stmt->bindParam(':id', $id, \PDO::PARAM_INT);
|
||||
$stmt->execute();
|
||||
return $stmt->fetch();
|
||||
}
|
||||
|
||||
public static function add($user_id, $transaction_type, $cents, $sats)
|
||||
{
|
||||
|
@ -29,13 +38,10 @@ class transactions
|
|||
if ($cents < 0 || $sats < 0) {
|
||||
throw new \Exception("Amounts must be non-negative integers.");
|
||||
}
|
||||
|
||||
$currentBalance = self::getUserBalance($user_id);
|
||||
|
||||
if (in_array($transaction_type, ['REDEEM', 'REVOKE']) && ($currentBalance['total_cents'] < $cents || $currentBalance['total_sats'] < $sats)) {
|
||||
throw new \Exception("Insufficient funds.");
|
||||
}
|
||||
|
||||
$query = "INSERT INTO transactions (user_id, type, cents, sats) VALUES (:user_id, :transaction_type, :cents, :sats)";
|
||||
$stmt = app::$db->prepare($query);
|
||||
$stmt->bindParam(':user_id', $user_id);
|
||||
|
@ -43,7 +49,6 @@ class transactions
|
|||
$stmt->bindParam(':cents', $cents);
|
||||
$stmt->bindParam(':sats', $sats);
|
||||
$stmt->execute();
|
||||
|
||||
return app::$db->lastInsertId();
|
||||
}
|
||||
|
||||
|
@ -56,9 +61,9 @@ class transactions
|
|||
return $stmt->fetch();
|
||||
}
|
||||
|
||||
public static function getRecent($n)
|
||||
public static function getRecent($n, $currency)
|
||||
{
|
||||
$query = "SELECT * FROM transactions ORDER BY date DESC LIMIT :n";
|
||||
$query = "SELECT * FROM transactions WHERE $currency > 0 ORDER BY date DESC LIMIT :n";
|
||||
$stmt = app::$db->prepare($query);
|
||||
$stmt->bindParam(':n', $n, \PDO::PARAM_INT);
|
||||
$stmt->execute();
|
||||
|
@ -77,7 +82,7 @@ class transactions
|
|||
return $stmt->fetchAll();
|
||||
}
|
||||
|
||||
public static function liabilities($currency)
|
||||
public static function getLiabilities($currency)
|
||||
{
|
||||
if (!in_array($currency, ['cents', 'sats'])) {
|
||||
throw new \Exception("Invalid currency type.");
|
||||
|
|
|
@ -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>
|
Loading…
Add table
Add a link
Reference in a new issue