Auth works; empty journal works

This commit is contained in:
Daniel J. Summers 2023-08-27 22:35:12 -04:00
parent 7421f9c788
commit 907d759a23
16 changed files with 2472 additions and 35 deletions

1
.gitignore vendored
View File

@ -257,3 +257,4 @@ paket-files/
# in-progress: PHP version # in-progress: PHP version
src/app/vendor src/app/vendor
**/.env

98
src/app/AppUser.php Normal file
View File

@ -0,0 +1,98 @@
<?php
declare(strict_types=1);
namespace MyPrayerJournal;
use Auth0\SDK\Auth0;
class AppUser
{
/** The Auth0 client instance to use for authentication */
private static ?Auth0 $auth0 = null;
/**
* Get the Auth0 instance
*
* @return Auth0 The Auth0 instance, lazily initialized
*/
private static function auth0Instance(): Auth0
{
if (is_null(self::$auth0)) {
self::$auth0 = new \Auth0\SDK\Auth0([
'domain' => $_ENV['AUTH0_DOMAIN'],
'clientId' => $_ENV['AUTH0_CLIENT_ID'],
'clientSecret' => $_ENV['AUTH0_CLIENT_SECRET'],
'cookieSecret' => $_ENV['AUTH0_COOKIE_SECRET']
]);
}
return self::$auth0;
}
/**
* Determine the host to use for return URLs
*
* @return string The host for return URLs
*/
private static function host()
{
return 'http' . ($_SERVER['SERVER_PORT'] == 443 ? 's' : '' ) . "://{$_SERVER['HTTP_HOST']}";
}
/**
* Generate the log on callback URL
*
* @return string The log on callback URL
*/
private static function logOnCallback()
{
return self::host() . '/user/log-on/success';
}
/**
* Initiate a redirect to the Auth0 log on page
*
* @param string $nextUrl The URL (other than /journal) to which the user should be redirected
* @return never This function exits the currently running script
*/
public static function logOn(?string $nextUrl = null): never
{
// TODO: pass the next URL in the Auth0 callback
self::auth0Instance()->clear();
header('Location: ' . self::auth0Instance()->login(self::logOnCallback()));
exit;
}
/**
* Process the log on response from Auth0
*
* @return never This function exits the currently running script
*/
public static function processLogOn(): never
{
self::auth0Instance()->exchange(self::logOnCallback());
// TODO: check for next URL and redirect if present
header('Location: /journal');
exit;
}
/**
* Log off the current user
*
* @return never This function exits the currently running script
*/
public static function logOff(): never
{
header('Location: ' . self::auth0Instance()->logout(self::host() . '/'));
exit;
}
/**
* Get the current user
*
* @return ?object The current user, or null if one is not signed in
*/
public static function current(): ?object
{
return self::auth0Instance()->getCredentials();
}
}

View File

@ -16,7 +16,6 @@ class Data
*/ */
public static function configure() public static function configure()
{ {
Configuration::$connectionString = 'pgsql:host=localhost;port=5432;dbname=leafjson;user=leaf;password=leaf';
Configuration::$startUp = '\MyPrayerJournal\Data::startUp'; Configuration::$startUp = '\MyPrayerJournal\Data::startUp';
} }

63
src/app/Handlers.php Normal file
View File

@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
namespace MyPrayerJournal;
class Handlers
{
/**
* Render a BareUI template
*
* @param string $template The template name to render
* @param string $pageTitle The title for the page
* @param ?array $params Parameters to use to render the page (optional)
*/
public static function render(string $template, string $pageTitle, ?array $params = null)
{
$params = array_merge($params ?? [], [
'pageTitle' => $pageTitle,
'isHtmx' =>
array_key_exists('HTTP_HX_REQUEST', $_SERVER)
&& (!array_key_exists('HTTP_HX_HISTORY_RESTORE_REQUEST', $_SERVER)),
'user' => AppUser::current(),
'hasSnoozed' => false,
]);
$params['pageContent'] = app()->template->render($template, $params);
// TODO: make the htmx distinction here
response()->markup(app()->template->render('layout/full', $params));
}
/**
* Render a BareUI component template
*
* @param string $template The template name to render
* @param ?array $params Parameter to use to render the component (optional)
*/
private static function renderComponent(string $template, ?array $params = null)
{
$params = $params ?? [];
$params['pageContent'] = app()->template->render($template, $params);
header('Cache-Control: no-cache, max-age=-1');
response()->markup(app()->template->render('layout/component', $params));
}
/** GET: /journal */
public static function journal()
{
if (!AppUser::current()) AppUser::logOn();
$user = AppUser::current()->user;
$firstName = (array_key_exists('given_name', $user) ? $user['given_name'] : null) ?? 'Your';
self::render('journal', $firstName . ($firstName == 'Your' ? '' : '&rsquo;s') . ' Prayer Journal');
}
/** GET: /components/journal-items */
public static function journalItems()
{
if (!AppUser::current()) AppUser::logOn();
self::renderComponent('components/journal_items', [
'requests' => Data::getJournal(AppUser::current()->user['sub'])
]);
}
}

View File

@ -4,7 +4,12 @@
"leafs/bareui": "^1.1", "leafs/bareui": "^1.1",
"leafs/db": "^2.1", "leafs/db": "^2.1",
"netresearch/jsonmapper": "^4.2", "netresearch/jsonmapper": "^4.2",
"visus/cuid2": "^3.0" "visus/cuid2": "^3.0",
"guzzlehttp/guzzle": "^7.8",
"guzzlehttp/psr7": "^2.6",
"http-interop/http-factory-guzzle": "^1.2",
"auth0/auth0-php": "^8.7",
"vlucas/phpdotenv": "^5.5"
}, },
"autoload": { "autoload": {
"psr-4": { "psr-4": {
@ -12,5 +17,10 @@
"MyPrayerJournal\\": [ "." ], "MyPrayerJournal\\": [ "." ],
"MyPrayerJournal\\Domain\\": [ "./domain" ] "MyPrayerJournal\\Domain\\": [ "./domain" ]
} }
},
"config": {
"allow-plugins": {
"php-http/discovery": true
}
} }
} }

2186
src/app/composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -28,7 +28,7 @@ class Configuration
$db = $_ENV['PGDOC_DB'] ?? 'postgres'; $db = $_ENV['PGDOC_DB'] ?? 'postgres';
$user = $_ENV['PGDOC_USER'] ?? 'postgres'; $user = $_ENV['PGDOC_USER'] ?? 'postgres';
$pass = $_ENV['PGDOC_PASS'] ?? 'postgres'; $pass = $_ENV['PGDOC_PASS'] ?? 'postgres';
self::$connectionString = "pgsql:host=$host;port=$port;dbname=$db;user=$user;pass=$pass"; self::$connectionString = "pgsql:host=$host;port=$port;dbname=$db;user=$user;password=$pass";
} }
} }

View File

@ -405,7 +405,7 @@ class Document
*/ */
private static function createCustomQuery(string $sql, array $params): PDOStatement private static function createCustomQuery(string $sql, array $params): PDOStatement
{ {
$query = pdo()->prepare($sql, self::NO_PREPARE); $query = pdo()->prepare($sql, [ \PDO::ATTR_EMULATE_PREPARES => false ]);
array_walk($params, fn ($value, $name) => $query->bindParam($name, $value)); array_walk($params, fn ($value, $name) => $query->bindParam($name, $value));
$query->execute(); $query->execute();
return $query; return $query;

View File

@ -1,8 +1,9 @@
<?php <?php
require __DIR__ . '/vendor/autoload.php'; require __DIR__ . '/vendor/autoload.php';
(Dotenv\Dotenv::createImmutable(__DIR__))->load();
use MyPrayerJournal\Data; use MyPrayerJournal\{ AppUser, Data, Handlers };
Data::configure(); Data::configure();
@ -18,31 +19,26 @@ app()->template->config('params', [
'version' => 'v4', 'version' => 'v4',
]); ]);
function renderPage(string $template, array $params, string $pageTitle)
app()->get('/', fn () => Handlers::render('home', 'Welcome'));
app()->get('/components/journal-items', Handlers::journalItems(...));
app()->get('/journal', Handlers::journal(...));
app()->get('/legal/privacy-policy', fn () => Handlers::render('legal/privacy-policy', 'Privacy Policy'));
app()->get('/legal/terms-of-service', fn () => Handlers::render('legal/terms-of-service', 'Terms of Service'));
app()->get('/user/log-on', AppUser::logOn(...));
app()->get('/user/log-on/success', AppUser::processLogOn(...));
app()->get('/user/log-off', AppUser::logOff(...));
// TODO: remove before go-live
$stdOut = fopen('php://stdout', 'w');
function stdout(string $msg)
{ {
if (is_null($params)) { global $stdOut;
$params = []; fwrite($stdOut, $msg . "\n");
}
$params['pageTitle'] = $pageTitle;
$params['isHtmx'] =
array_key_exists('HTTP_HX_REQUEST', $_SERVER)
&& (!array_key_exists('HTTP_HX_HISTORY_RESTORE_REQUEST', $_SERVER));
$params['userId'] = false;
$params['pageContent'] = app()->template->render($template, $params);
// TODO: make the htmx distinction here
response()->markup(app()->template->render('layout/full', $params));
} }
app()->get('/', function () {
renderPage('home', [], 'Welcome');
});
app()->get('/legal/privacy-policy', function () {
renderPage('legal/privacy-policy', [], 'Privacy Policy');
});
app()->get('/legal/terms-of-service', function () {
renderPage('legal/terms-of-service', [], 'Terms of Service');
});
app()->run(); app()->run();

View File

@ -0,0 +1,16 @@
<?php
if (count($requests) == 0) {
echo app()->template->render('components/no_results', [
'heading' => 'No Active Requests',
'link' => '/request/new/edit',
'buttonText' => 'Add a Request',
'text' => 'You have no requests to be shown; see the &ldquo;Active&rdquo; link above for snoozed or '
. 'deferred requests, and the &ldquo;Answered&rdquo; link for answered requests'
]);
} else { ?>
<section id="journalItems" class="row row-cols-1 row-cols-md-2 row-cols-lg-3 row-cols-xl-4 g-3" hx-target="this"
hx-swap="outerHTML" aria-label="Prayer Requests">
items
|> List.map (journalCard now tz)
</section><?php
}

View File

@ -0,0 +1,7 @@
<div class="card">
<h5 class="card-header"><?php echo $heading; ?></h5>
<div class="card-body text-center">
<p class="card-text"><?php echo $text; ?></p>
<a <?php $page_link($link); ?> class="btn btn-primary"><?php echo $buttonText; ?></a>
</div>
</div>

View File

@ -0,0 +1,58 @@
<article class="container-fluid mt-3">
<h2 class="pb-3"><?php echo $pageTitle; ?></h2>
<p class="pb-3 text-center">
<a <?php $page_link('/request/new/edit'); ?> class="btn btn-primary">
<span class="material-icons">add_box</span> Add a Prayer Request
</a>
</p>
<p hx-get="/components/journal-items" hx-swap="outerHTML" hx-trigger="load">
Loading your prayer journal&hellip;
</p>
<!--
div [ _id "notesModal"
_class "modal fade"
_tabindex "-1"
_ariaLabelledBy "nodesModalLabel"
_ariaHidden "true" ] [
div [ _class "modal-dialog modal-dialog-scrollable" ] [
div [ _class "modal-content" ] [
div [ _class "modal-header" ] [
h5 [ _class "modal-title"; _id "nodesModalLabel" ] [ str "Add Notes to Prayer Request" ]
button [ _type "button"; _class "btn-close"; _data "bs-dismiss" "modal"; _ariaLabel "Close" ] []
]
div [ _class "modal-body"; _id "notesBody" ] [ ]
div [ _class "modal-footer" ] [
button [ _type "button"
_id "notesDismiss"
_class "btn btn-secondary"
_data "bs-dismiss" "modal" ] [
str "Close"
]
]
]
]
]
div [ _id "snoozeModal"
_class "modal fade"
_tabindex "-1"
_ariaLabelledBy "snoozeModalLabel"
_ariaHidden "true" ] [
div [ _class "modal-dialog modal-sm" ] [
div [ _class "modal-content" ] [
div [ _class "modal-header" ] [
h5 [ _class "modal-title"; _id "snoozeModalLabel" ] [ str "Snooze Prayer Request" ]
button [ _type "button"; _class "btn-close"; _data "bs-dismiss" "modal"; _ariaLabel "Close" ] []
]
div [ _class "modal-body"; _id "snoozeBody" ] [ ]
div [ _class "modal-footer" ] [
button [ _type "button"
_id "snoozeDismiss"
_class "btn btn-secondary"
_data "bs-dismiss" "modal" ] [
str "Close"
]
]
]
]
] -->
</article>

View File

@ -1,5 +1,5 @@
<head> <head>
<title><?php echo htmlentities($pageTitle); ?> &#xab; myPrayerJournal</title><?php <title><?php echo $pageTitle; ?> &#xab; myPrayerJournal</title><?php
if (!$isHtmx) { ?> if (!$isHtmx) { ?>
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="Online prayer journal - free w/Google or Microsoft account"> <meta name="description" content="Online prayer journal - free w/Google or Microsoft account">

View File

@ -4,7 +4,7 @@
<span class="m">my</span><span class="p">Prayer</span><span class="j">Journal</span> <span class="m">my</span><span class="p">Prayer</span><span class="j">Journal</span>
</a> </a>
<ul class="navbar-nav me-auto d-flex flex-row"><?php <ul class="navbar-nav me-auto d-flex flex-row"><?php
if ($userId) { ?> if ($user) { ?>
<li class="nav-item"><a <?php $page_link('/journal', true); ?>>Journal</a></li> <li class="nav-item"><a <?php $page_link('/journal', true); ?>>Journal</a></li>
<li class="nav-item"><a <?php $page_link('/requests/active', true); ?>>Active</a></li><?php <li class="nav-item"><a <?php $page_link('/requests/active', true); ?>>Active</a></li><?php
if ($hasSnoozed) { ?> if ($hasSnoozed) { ?>

View File

@ -0,0 +1,5 @@
<!DOCTYPE html>
<html>
<head><title></title></head>
<body><?php echo $pageContent; ?></body>
</html>

View File

@ -3,7 +3,7 @@
<?php echo app()->template->render('layout/_head', [ 'pageTitle' => $pageTitle, 'isHtmx' => $isHtmx ]); ?> <?php echo app()->template->render('layout/_head', [ 'pageTitle' => $pageTitle, 'isHtmx' => $isHtmx ]); ?>
<body> <body>
<section id="top" aria-label="Top navigation"> <section id="top" aria-label="Top navigation">
<?php echo app()->template->render('layout/_nav', [ 'userId' => $userId ]); ?> <?php echo app()->template->render('layout/_nav', [ 'user' => $user, 'hasSnoozed' => $hasSnoozed ]); ?>
<?php echo $pageContent; ?> <?php echo $pageContent; ?>
</section> </section>
<?php echo app()->template->render('layout/_foot', [ 'isHtmx' => $isHtmx ]); ?> <?php echo app()->template->render('layout/_foot', [ 'isHtmx' => $isHtmx ]); ?>