Browse Source

#54: Added auth, manual propagation shield, key gen. command

master
Diane 2 years ago
parent
commit
301e0f041f
No known key found for this signature in database GPG Key ID: B13153D92467FCC1
  1. 3
      artemis.php
  2. 3
      composer.json
  3. 1
      config.php
  4. 2
      public/assets/admin.css
  5. 2
      public/assets/admin.css.sha
  6. 2
      public/assets/style.css
  7. 2
      public/assets/style.css.sha
  8. 5
      resources/assets/admin.css
  9. 10
      resources/assets/style.css
  10. 27
      resources/templates/admin/login.html.twig
  11. 2
      resources/templates/article-list.html.twig
  12. 18
      routes.php
  13. 5
      src/Action.php
  14. 3
      src/Actions/API/ListCategories.php
  15. 5
      src/Actions/API/Preview.php
  16. 3
      src/Actions/API/Search.php
  17. 3
      src/Actions/API/Slugify.php
  18. 3
      src/Actions/About.php
  19. 3
      src/Actions/Admin/Article/Create.php
  20. 3
      src/Actions/Admin/Article/Delete.php
  21. 5
      src/Actions/Admin/Article/Edit.php
  22. 3
      src/Actions/Admin/Article/Import.php
  23. 29
      src/Actions/Admin/CheckAuthentication.php
  24. 3
      src/Actions/Admin/Dashboard.php
  25. 52
      src/Actions/Admin/Login.php
  26. 39
      src/Actions/Admin/Logout.php
  27. 5
      src/Actions/Article/Index.php
  28. 5
      src/Actions/Article/RSS.php
  29. 7
      src/Actions/Article/Show.php
  30. 3
      src/Actions/Home.php
  31. 3
      src/Actions/NotFound.php
  32. 3
      src/Actions/Portfolio.php
  33. 3
      src/Actions/Rewrite/Image.php
  34. 3
      src/Actions/Search.php
  35. 86
      src/Commands/Create/Key.php
  36. 5
      src/Helpers/RequestHelper.php
  37. 5
      src/Helpers/StringHelper.php
  38. 12
      src/Runner.php
  39. 46
      src/Services/Auth.php

3
artemis.php

@ -10,7 +10,8 @@ $config = require_once(__DIR__ . '/config.php');
$app = new Application();
$commands = [
'Minify'
'Minify',
'Create\\Key'
];
(new Runner($config))->cli($app, $commands);

3
composer.json

@ -17,7 +17,8 @@
"erusev/parsedown-extra": "^0.8.1",
"symfony/console": "^5.0",
"matthiasmullie/minify": "^1.3",
"scrivo/highlight.php": "v9.18.1.1"
"scrivo/highlight.php": "v9.18.1.1",
"ext-readline": "*"
},
"license": "Apache-2.0",
"authors": [

1
config.php

@ -13,6 +13,7 @@ function try_read_hash(string $hash_file): ?string
}
return [
'auth_token' => getenv('AUTH_TOKEN'),
'db' => getenv('DB_DSN'),
'resources' => [
'css' => [

2
public/assets/admin.css

@ -1 +1 @@
table{width:100%;box-sizing:border-box;border:2px solid darkgray}main{width:unset}section.element{padding-top:10px;padding-bottom:10px}section.element>input,section.element>select,section.element>textarea{width:100%}fieldset{margin:0}input,textarea{box-sizing:border-box}fieldset,textarea{border:2px solid darkgray}.error{text-transform:initial;color:darkred}
table{width:100%;box-sizing:border-box;border:2px solid darkgray}main{width:unset}section.element{padding-top:10px;padding-bottom:10px}section.element>input,section.element>select,section.element>textarea{width:100%}fieldset{margin:0}input,textarea{box-sizing:border-box}fieldset,textarea{border:2px solid darkgray}

2
public/assets/admin.css.sha

@ -1 +1 @@
mgu6G5T1qhmea6/7x3cu/wz4bWnsYWWHrT1tTcQ/x0zWH8sEnhxOrUsOKAMK8Oj8
VDnK0Eof3RP/mLCHkL+EgNEO1h4W+wDztjqNkjegWquW3hMkSOpSdWRVW0L90SRQ

2
public/assets/style.css

@ -1,2 +1,2 @@
/*! normalize.assets v8.0.1 | MIT License | github.com/necolas/normalize.assets */
html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}main{display:block}h1{font-size:2em;margin:.67em 0}hr{box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}button,[type="button"],[type="reset"],[type="submit"]{-webkit-appearance:button}button::-moz-focus-inner,[type="button"]::-moz-focus-inner,[type="reset"]::-moz-focus-inner,[type="submit"]::-moz-focus-inner{border-style:none;padding:0}button:-moz-focusring,[type="button"]:-moz-focusring,[type="reset"]:-moz-focusring,[type="submit"]:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{vertical-align:baseline}textarea{overflow:auto}[type="checkbox"],[type="radio"]{box-sizing:border-box;padding:0}[type="number"]::-webkit-inner-spin-button,[type="number"]::-webkit-outer-spin-button{height:auto}[type="search"]{-webkit-appearance:textfield;outline-offset:-2px}[type="search"]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details{display:block}summary{display:list-item}template{display:none}[hidden]{display:none}body{border-top:6px solid #F7A8B8;margin:0;padding:1em;text-rendering:optimizeLegibility;font-size:1.2rem}h1,h2,h3,h4,h5,h6,a,b,.mono{font-family:monospace,monospace;font-weight:700}h1>a,h2>a,h3>a,h4>a,h5>a,h6>a{color:#55CDFC}.up{text-transform:uppercase}.date{font-style:italic}.unstyled{list-style:none;padding-left:0}p,li{line-height:1.5}main,article,aside{font-family:sans-serif,sans-serif}pre{padding:0;overflow-x:auto}blockquote,pre,code{background-color:#fafafa}a{text-decoration:none;color:#1e88e5}a:hover,.current{color:#333;background-color:rgba(0,0,0,.2)}a:visited{color:#802e89}blockquote{border-left:2px solid #55CDFC;margin:0}pre>code,blockquote{padding:.1em .8em}code{padding:.1em}pre>code{display:block}code{word-break:break-word}article{padding-bottom:2em}hr{border:1px solid rgba(0,0,0,.1)}footer{padding:2em 0;border-top:2px solid rgba(0,0,0,.1)}img,video{max-width:100%}header>aside>form{display:flex}header>aside>form>input#searchbar{flex:1}.centered{text-align:center}.fullWidth{width:100%}.active-page{text-decoration:underline #55CDFC}#search{display:flex;flex-direction:column}#search>#searchbar{flex:1}ul.shortened{padding:0}ul.shortened>li{list-style-type:none}@media screen and (min-width:800px){main{width:calc(800px - 2em);margin:0 auto}#search{flex-direction:row}}.hljs-comment,.hljs-quote{color:#696969}.hljs-variable,.hljs-template-variable,.hljs-tag,.hljs-name,.hljs-selector-id,.hljs-selector-class,.hljs-regexp,.hljs-deletion{color:#d91e18}.hljs-number,.hljs-built_in,.hljs-builtin-name,.hljs-literal,.hljs-type,.hljs-params,.hljs-meta,.hljs-link{color:#aa5d00}.hljs-attribute{color:#aa5d00}.hljs-string,.hljs-symbol,.hljs-bullet,.hljs-addition{color:green}.hljs-title,.hljs-section{color:#007faa}.hljs-keyword,.hljs-selector-tag{color:#7928a1}.hljs{display:block;overflow-x:auto;background:#fefefe;color:#545454;padding:.5em}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700}@media screen and (-ms-high-contrast:active){.hljs-addition,.hljs-attribute,.hljs-built_in,.hljs-builtin-name,.hljs-bullet,.hljs-comment,.hljs-link,.hljs-literal,.hljs-meta,.hljs-number,.hljs-params,.hljs-string,.hljs-symbol,.hljs-type,.hljs-quote{color:highlight}.hljs-keyword,.hljs-selector-tag{font-weight:700}}
html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}main{display:block}h1{font-size:2em;margin:.67em 0}hr{box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}button,[type="button"],[type="reset"],[type="submit"]{-webkit-appearance:button}button::-moz-focus-inner,[type="button"]::-moz-focus-inner,[type="reset"]::-moz-focus-inner,[type="submit"]::-moz-focus-inner{border-style:none;padding:0}button:-moz-focusring,[type="button"]:-moz-focusring,[type="reset"]:-moz-focusring,[type="submit"]:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{vertical-align:baseline}textarea{overflow:auto}[type="checkbox"],[type="radio"]{box-sizing:border-box;padding:0}[type="number"]::-webkit-inner-spin-button,[type="number"]::-webkit-outer-spin-button{height:auto}[type="search"]{-webkit-appearance:textfield;outline-offset:-2px}[type="search"]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details{display:block}summary{display:list-item}template{display:none}[hidden]{display:none}body{border-top:6px solid #F7A8B8;margin:0;padding:1em;text-rendering:optimizeLegibility;font-size:1.2rem}h1,h2,h3,h4,h5,h6,a,b,.mono{font-family:monospace,monospace;font-weight:700}h1>a,h2>a,h3>a,h4>a,h5>a,h6>a{color:#55CDFC}.up{text-transform:uppercase}.date{font-style:italic}.unstyled{list-style:none;padding-left:0}p,li{line-height:1.5}main,article,aside{font-family:sans-serif,sans-serif}pre{padding:0;overflow-x:auto}blockquote,pre,code{background-color:#fafafa}a{text-decoration:none;color:#1e88e5}a:hover,.current{color:#333;background-color:rgba(0,0,0,.2)}a:visited{color:#802e89}blockquote{border-left:2px solid #55CDFC;margin:0}pre>code,blockquote{padding:.1em .8em}code{padding:.1em}pre>code{display:block}code{word-break:break-word}article{padding-bottom:2em}hr{border:1px solid rgba(0,0,0,.1)}footer{padding:2em 0;border-top:2px solid rgba(0,0,0,.1)}img,video{max-width:100%}header>aside>form{display:flex}header>aside>form>input#searchbar{flex:1}.centered{text-align:center}.fullWidth{width:100%}.active-page{text-decoration:underline #55CDFC}#search{display:flex;flex-direction:column}#search>#searchbar{flex:1}ul.shortened{padding:0}ul.shortened>li{list-style-type:none}@media screen and (min-width:800px){main{width:calc(800px - 2em);margin:0 auto}#search{flex-direction:row}}.spaced{margin-top:1em;margin-bottom:1em}.error{text-transform:initial;color:darkred}.hljs-comment,.hljs-quote{color:#696969}.hljs-variable,.hljs-template-variable,.hljs-tag,.hljs-name,.hljs-selector-id,.hljs-selector-class,.hljs-regexp,.hljs-deletion{color:#d91e18}.hljs-number,.hljs-built_in,.hljs-builtin-name,.hljs-literal,.hljs-type,.hljs-params,.hljs-meta,.hljs-link{color:#aa5d00}.hljs-attribute{color:#aa5d00}.hljs-string,.hljs-symbol,.hljs-bullet,.hljs-addition{color:green}.hljs-title,.hljs-section{color:#007faa}.hljs-keyword,.hljs-selector-tag{color:#7928a1}.hljs{display:block;overflow-x:auto;background:#fefefe;color:#545454;padding:.5em}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700}@media screen and (-ms-high-contrast:active){.hljs-addition,.hljs-attribute,.hljs-built_in,.hljs-builtin-name,.hljs-bullet,.hljs-comment,.hljs-link,.hljs-literal,.hljs-meta,.hljs-number,.hljs-params,.hljs-string,.hljs-symbol,.hljs-type,.hljs-quote{color:highlight}.hljs-keyword,.hljs-selector-tag{font-weight:700}}

2
public/assets/style.css.sha

@ -1 +1 @@
4VPCOGIjWcS+G0mRoor5UZH2M8Y9d67StlJbuZw/5lIaciftT2lvpJaqqZIi20rR
3kpD8pQEI9JB/jey5cSNFWTuxPg8GeqezLDbgAh45qX0CW/yJdZy2l4IN4qROXsQ

5
resources/assets/admin.css

@ -28,8 +28,3 @@ input, textarea {
fieldset, textarea {
border: 2px solid darkgray;
}
.error {
text-transform: initial;
color: darkred;
}

10
resources/assets/style.css

@ -146,3 +146,13 @@ ul.shortened > li {
flex-direction: row;
}
}
.spaced {
margin-top: 1em;
margin-bottom: 1em;
}
.error {
text-transform: initial;
color: darkred;
}

27
resources/templates/admin/login.html.twig

@ -0,0 +1,27 @@
{% set has_search_suggestions = false %}
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
{% include 'fragments/head.html.twig' %}
<title>Log in - {{ metadata['author'] }}</title>
</head>
<body id="top">
<main>
<section class="mono">
<form method="post">
<fieldset>
<legend>Log in</legend>
<label for="key">Your authentication key</label>
<input class="spaced fullWidth" type="password" id="key" name="key" placeholder="*****" autofocus/>
{% if error is defined %}
<p><strong class="error">{{ error }}</strong></p>
{% endif %}
<input type="submit" value="Log in"/>
</fieldset>
</form>
</section>
</main>
</body>
</html>

2
resources/templates/article-list.html.twig

@ -10,7 +10,7 @@
<h2>
{{ category['title'] }}
(<a href="/blog/feed/{{ category['slug'] }}.xml">
(<a href="/blog/feeds/{{ category['slug'] }}.xml">
RSS
</a>)
</h2>

18
routes.php

@ -48,6 +48,22 @@ return [
'path' => '/search(.html)?',
'action' => 'Search'
],
// Authentication
[
'methods' => ['GET', 'POST'],
'path' => '/login',
'action' => 'Admin\\Login'
],
[
'methods' => ['GET'],
'path' => '/admin/logout',
'action' => 'Admin\\Logout'
],
[
'methods' => ['GET', 'POST'],
'path' => '/admin(.*)',
'action' => 'Admin\\CheckAuthentication'
],
// API
[
'methods' => ['GET'],
@ -72,7 +88,7 @@ return [
// admin panel
[
'methods' => ['GET'],
'path' => '/admin/?',
'path' => '/admin(/?)',
'action' => 'Admin\\Dashboard'
],
[

5
src/Action.php

@ -2,6 +2,7 @@
namespace App;
interface Action {
public function __invoke(?array $routeParams): void;
interface Action
{
public function __invoke(?array $routeParams): bool;
}

3
src/Actions/API/ListCategories.php

@ -18,10 +18,11 @@ class ListCategories implements Action
$this->categoryRepository = $categoryRepository;
}
public function __invoke(?array $routeParams): void
public function __invoke(?array $routeParams): bool
{
json([
'categories' => $this->categoryRepository->listCategories(),
]);
return true;
}
}

5
src/Actions/API/Preview.php

@ -12,17 +12,18 @@ use function Siler\Http\Response\json;
class Preview implements Action
{
public function __invoke(?array $routeParams): void
public function __invoke(?array $routeParams): bool
{
$body = post('article');
if (empty($body)) {
json([], 400);
return;
return false;
}
json([
'rendered' => ArticleHelper::render($body),
]);
return true;
}
}

3
src/Actions/API/Search.php

@ -19,7 +19,7 @@ class Search implements Action
$this->articleRepository = $articleRepository;
}
public function __invoke(?array $routeParams): void
public function __invoke(?array $routeParams): bool
{
$query = get('q', null);
@ -32,5 +32,6 @@ class Search implements Action
'count' => count($results),
'had_search_terms' => !is_null($query)
]);
return false;
}
}

3
src/Actions/API/Slugify.php

@ -11,12 +11,13 @@ use function Siler\Http\Response\json;
class Slugify implements Action
{
public function __invoke(?array $routeParams): void
public function __invoke(?array $routeParams): bool
{
$raw = post('raw', '');
json([
'slugified' => StringHelper::slugify($raw)
]);
return false;
}
}

3
src/Actions/About.php

@ -19,8 +19,9 @@ class About implements Action
$this->html = $html;
}
public function __invoke(?array $routeParams): void
public function __invoke(?array $routeParams): bool
{
html($this->html->forPage(Page::ABOUT)->render('about.html.twig'));
return false;
}
}

3
src/Actions/Admin/Article/Create.php

@ -82,7 +82,7 @@ class Create implements Action
redirect('/admin/');
}
public function __invoke(?array $routeParams): void
public function __invoke(?array $routeParams): bool
{
switch (method()) {
case 'GET':
@ -92,5 +92,6 @@ class Create implements Action
$this->handleNewArticle();
break;
}
return true;
}
}

3
src/Actions/Admin/Article/Delete.php

@ -19,10 +19,11 @@ class Delete implements Action
$this->articleRepository = $articleRepository;
}
public function __invoke(?array $routeParams): void
public function __invoke(?array $routeParams): bool
{
$slug = $routeParams['slug'];
$this->articleRepository->deleteIfExists($slug);
redirect(url('/admin/'));
return true;
}
}

5
src/Actions/Admin/Article/Edit.php

@ -88,12 +88,12 @@ class Edit implements Action
redirect(path());
}
public function __invoke(?array $routeParams): void
public function __invoke(?array $routeParams): bool
{
$articleId = (int) $routeParams[1];
if (!$this->articleRepository->exists($articleId)) {
redirect(url('/admin/new-article'));
return;
return false;
}
switch (method()) {
@ -108,5 +108,6 @@ class Edit implements Action
$this->handleUpdateArticle($articleId);
break;
}
return true;
}
}

3
src/Actions/Admin/Article/Import.php

@ -99,7 +99,7 @@ class Import implements Action
redirect('/admin/');
}
public function __invoke(?array $routeParams): void
public function __invoke(?array $routeParams): bool
{
switch (method()) {
case 'GET':
@ -109,5 +109,6 @@ class Import implements Action
$this->handleImportArticle();
break;
}
return true;
}
}

29
src/Actions/Admin/CheckAuthentication.php

@ -0,0 +1,29 @@
<?php
namespace App\Actions\Admin;
use App\Action;
use App\Services\Auth;
use function Siler\Http\Response\redirect;
class CheckAuthentication implements Action
{
/** @var Auth $auth */
private $auth;
public function __construct(Auth $auth)
{
$this->auth = $auth;
}
public function __invoke(?array $routeParams): bool
{
if (!$this->auth->isLoggedIn()) {
redirect('/login');
return false;
}
return true;
}
}

3
src/Actions/Admin/Dashboard.php

@ -24,7 +24,7 @@ class Dashboard implements Action
$this->articleRepository = $articleRepository;
}
public function __invoke(?array $routeParams): void
public function __invoke(?array $routeParams): bool
{
$articles = $this->articleRepository->getAllArticles();
@ -41,5 +41,6 @@ class Dashboard implements Action
html($this->html->render('admin/dashboard.html.twig', [
'articles' => $articles
]));
return false;
}
}

52
src/Actions/Admin/Login.php

@ -0,0 +1,52 @@
<?php
namespace App\Actions\Admin;
use App\Action;
use App\Helpers\RequestHelper;
use App\Services\Auth;
use App\Services\HTML;
use function Siler\Http\Request\method;
use function Siler\Http\Request\post;
use function Siler\Http\Response\html;
use function Siler\Http\Response\redirect;
class Login implements Action
{
/** @var HTML $html */
private $html;
/** @var Auth $auth */
private $auth;
public function __construct(HTML $html, Auth $auth)
{
$this->html = $html;
$this->auth = $auth;
}
public function __invoke(?array $routeParams): bool
{
if ($this->auth->isLoggedIn()) {
redirect('/admin/');
return false;
}
switch (method()) {
case 'GET':
html($this->html->render('admin/login.html.twig'));
break;
case 'POST':
$success = $this->auth->tryLogIn(post('key', ''));
if ($success) {
redirect('/admin/');
return false;
}
html($this->html->render('admin/login.html.twig', ['error' => 'Invalid key']), 403);
}
return true;
}
}

39
src/Actions/Admin/Logout.php

@ -0,0 +1,39 @@
<?php
namespace App\Actions\Admin;
use App\Action;
use App\Helpers\RequestHelper;
use App\Services\Auth;
use App\Services\HTML;
use function Siler\Http\Request\method;
use function Siler\Http\Request\post;
use function Siler\Http\Response\html;
use function Siler\Http\Response\redirect;
class Logout implements Action
{
/** @var HTML $html */
private $html;
/** @var Auth $auth */
private $auth;
public function __construct(HTML $html, Auth $auth)
{
$this->html = $html;
$this->auth = $auth;
}
public function __invoke(?array $routeParams): bool
{
if ($this->auth->isLoggedIn()) {
$this->auth->logout();
redirect('/');
}
return false;
}
}

5
src/Actions/Article/Index.php

@ -35,7 +35,7 @@ class Index implements Action
$this->articleRepository = $articleRepository;
}
public function __invoke(?array $routeParams): void
public function __invoke(?array $routeParams): bool
{
[$category, $entity] = CategoryHelper::getEntityAndDescriptor(
$routeParams['category'] ?? null,
@ -43,7 +43,7 @@ class Index implements Action
);
if (is_null($entity) && $category['slug'] !== 'all') {
redirect('/blog/');
return;
return false;
}
$articles = $this->articleRepository->getPublishedArticlesForCategory($entity);
@ -83,5 +83,6 @@ class Index implements Action
'category' => $category,
'by_year' => $by_year
]));
return false;
}
}

5
src/Actions/Article/RSS.php

@ -40,7 +40,7 @@ class RSS implements Action
$this->config = $config->getConfig();
}
public function __invoke(?array $routeParams): void
public function __invoke(?array $routeParams): bool
{
$categorySlug = $routeParams['category'] ?? 'all';
[$category, $entity] = CategoryHelper::getEntityAndDescriptor(
@ -49,7 +49,7 @@ class RSS implements Action
);
if (is_null($entity) && 'all' !== $categorySlug) {
redirect('/blog/feeds/all.xml');
return;
return false;
}
$articles = $this->articleRepository->getFullPublishedArticlesForCategory($entity);
@ -90,5 +90,6 @@ class RSS implements Action
}
output($feed->render(), 200, 'application/xml');
return false;
}
}

7
src/Actions/Article/Show.php

@ -41,19 +41,19 @@ class Show implements Action
$this->error = $error;
}
public function __invoke(?array $routeParams): void
public function __invoke(?array $routeParams): bool
{
$mode = $routeParams['mode'] ?? 'html';
if (!in_array($mode, RenderMode::RENDER_MODES)) {
$this->error->not_found();
return;
return false;
}
$slug = $routeParams['slug'];
$article = $this->articleRepository->getFullArticle($slug);
if (!$article) {
$this->error->not_found();
return;
return false;
}
// ViewModel preparation
@ -62,5 +62,6 @@ class Show implements Action
$article['has_category'] = CategoryHelper::hasCategory($article);
$this->renderer->render($mode, $article);
return false;
}
}

3
src/Actions/Home.php

@ -25,7 +25,7 @@ class Home implements Action
$this->articleRepository = $articleRepository;
}
public function __invoke(?array $routeParams): void
public function __invoke(?array $routeParams): bool
{
$articles = $this->articleRepository->getHomepageArticles();
@ -37,5 +37,6 @@ class Home implements Action
});
html($this->html->forPage(Page::HOME)->render('home.html.twig', ['articles' => $articles]));
return true;
}
}

3
src/Actions/NotFound.php

@ -17,8 +17,9 @@ class NotFound implements Action
$this->renderer = $error;
}
public function __invoke(?array $routeParams = []): void
public function __invoke(?array $routeParams = []): bool
{
$this->renderer->not_found();
return false;
}
}

3
src/Actions/Portfolio.php

@ -19,8 +19,9 @@ class Portfolio implements Action
$this->html = $html;
}
public function __invoke(?array $routeParams): void
public function __invoke(?array $routeParams): bool
{
html($this->html->forPage(Page::PORTFOLIO)->render('portfolio.html.twig'));
return false;
}
}

3
src/Actions/Rewrite/Image.php

@ -20,8 +20,9 @@ class Image implements Action
$this->config = $config->getConfig();
}
public function __invoke(?array $routeParams): void
public function __invoke(?array $routeParams): bool
{
redirect(self::IMAGE_BASE_URL . '/' . $routeParams['id']);
return false;
}
}

3
src/Actions/Search.php

@ -25,7 +25,7 @@ class Search implements Action
$this->html = $html;
}
public function __invoke(?array $routeParams): void
public function __invoke(?array $routeParams): bool
{
$query = get('q', null);
@ -49,5 +49,6 @@ class Search implements Action
'result_count' => $resultCount,
'results' => $results,
]));
return false;
}
}

86
src/Commands/Create/Key.php

@ -0,0 +1,86 @@
<?php
namespace App\Commands\Create;
use App\Command;
use App\Helpers\StringHelper;
use App\Services\Config;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
class Key extends Command
{
public static $defaultName = 'new:key';
/** @var array $config */
private $config;
public function __construct(Config $config)
{
parent::__construct();
$this->config = $config->getConfig();
}
protected function configure()
{
$this
->setDescription('Create a new login key hash based on user input for the key')
->setHelp("This prompts the user for a key, and generates a hash that can be used for AUTH_TOKEN.")
->addOption(
'algorithm',
'a',
InputOption::VALUE_REQUIRED,
'The hash algorithm constant to use'
);
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$userProvidedAlgorithm = $input->getOption('algorithm');
$wasInputProvided = !empty($userProvidedAlgorithm);
$isDefined = defined($userProvidedAlgorithm);
if ($wasInputProvided) {
if ($isDefined) {
$output->writeln([
"Using algorithm $userProvidedAlgorithm for password hash generation",
]);
} else {
$output->writeln([
"The algorithm you provided ($userProvidedAlgorithm) isn't defined."
]);
return 1;
}
}
$key = readline("Enter your new authentication key: ");
$algorithm = ($wasInputProvided && $isDefined) ? $userProvidedAlgorithm
: (defined('PASSWORD_ARGON2ID') ? 'PASSWORD_ARGON2ID'
: (defined('PASSWORD_ARGON2I') ? 'PASSWORD_ARGON2I'
: (defined('PASSWORD_BCRYPT') ? 'PASSWORD_BCRYPT' : null)));
if (!$wasInputProvided) {
if (is_null($algorithm)) {
$output->writeln([
"No algorithm available."
]);
return 1;
}
$output->writeln([
"Using best available algorithm ($algorithm) for password hash generation"
]);
}
$hash = StringHelper::password_hash($key, constant($algorithm));
$output->writeln([
"Your password hash is:",
$hash,
]);
return 0;
}
}

5
src/Helpers/RequestHelper.php

@ -4,6 +4,7 @@
namespace App\Helpers;
use function Siler\array_get;
use function Siler\Http\Request\post;
class RequestHelper
@ -17,4 +18,8 @@ class RequestHelper
return $res;
}
public static function isSecure(): bool {
return array_key_exists('HTTPS', $_SERVER) && !empty($_SERVER['HTTPS']);
}
}

5
src/Helpers/StringHelper.php

@ -18,4 +18,9 @@ class StringHelper
}
return self::$slugifyInstanceCache->slugify($input);
}
static function password_hash(string $input, string $algorithm): string
{
return password_hash($input, $algorithm);
}
}

12
src/Runner.php

@ -8,6 +8,7 @@ use DI\ContainerBuilder;
use Siler\Route;
use Symfony\Component\Console\Application;
use function DI\create;
use function Siler\Container\set;
class Runner
{
@ -37,16 +38,21 @@ class Runner
*/
public function http(array $routes): void
{
// Disable route propagation shield
set(Route\STOP_PROPAGATION, false);
$di = $this->di;
$matched = false;
$continue = false;
foreach ($routes as $route) {
assert(array_key_exists('action', $route), "Route ${route['path']} must have an action");
Route\route($route['methods'], $route['path'], function ($params) use ($route, $di, &$matched) {
if (!$matched) $matched = true;
return $di->get('\\App\\Actions\\' . $route['action'])($params);
Route\route($route['methods'], $route['path'], function ($params) use ($route, $di, &$matched, &$continue) {
if (!$matched) $matched = true;
$continue = $di->get('\\App\\Actions\\' . $route['action'])($params);
});
if ($matched && !$continue) break;
}
if (!$matched) {

46
src/Services/Auth.php

@ -0,0 +1,46 @@
<?php
namespace App\Services;
use App\Helpers\RequestHelper;
use function Siler\Http\session;
use function Siler\Http\setsession;
class Auth
{
/** @var array $config */
private $config;
public function __construct(Config $config)
{
$this->config = $config->getConfig();
session_start([
'cookie_secure' => RequestHelper::isSecure(),
'cookie_httponly' => true,
'cookie_samesite' => true,
]);
}
public function isLoggedIn(): bool
{
return session('logged_in', false);
}
public function tryLogIn(string $key): bool
{
if (!password_verify($key, $this->config['auth_token'])) {
return false;
}
setsession('logged_in', true);
return true;
}
public function logout()
{
setsession('logged_in', false);
session_destroy();
}
}
Loading…
Cancel
Save