diff --git a/.github/workflows/unittests.yml b/.github/workflows/unittests.yml index 8146ca6..24ef6e8 100644 --- a/.github/workflows/unittests.yml +++ b/.github/workflows/unittests.yml @@ -17,7 +17,7 @@ jobs: - uses: actions/checkout@v3 - uses: php-actions/composer@v6 - run: | - cp ./tests/Includes/config.yaml ./src/Config/config.yaml + cp ./tests/Includes/Config.yaml ./src/Config/Config.yaml - uses: php-actions/phpunit@v3 with: bootstrap: src/Vendor/autoload.php diff --git a/.gitignore b/.gitignore index cd51bc1..2c57bfe 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ /src/Vendor/ node_modules src/Cache/* -src/Config/config.yaml -src/Config/pages.yaml \ No newline at end of file +src/Config/Config.yaml +src/Config/Pages.yaml \ No newline at end of file diff --git a/readme.md b/readme.md index 56fe40e..65a22a6 100644 --- a/readme.md +++ b/readme.md @@ -28,35 +28,32 @@ Here is an example of the default theme folder structure: - `/Themes` - `/Default` - `/Templates` - - `default_layout.html.twig` - - `nav_layout.html.twig` + - `default.html.twig` + - `nav.html.twig` - `/Assets` - `tailwind.css` -To change the active theme, simply edit `config.yaml` and set the `activeTheme` option to match the folder name of your custom theme. +To change the active theme, simply edit `Config.yaml` and set the `activeTheme` option to match the folder name of your custom theme. ### Configuring AntCMS -AntCMS stores its configuration in the human-readable "yaml" file format. The main configuration files are `config.yaml` and `pages.yaml`. These files will be automatically generated by AntCMS if they do not exist. +AntCMS stores its configuration in the human-readable "yaml" file format. The main configuration files are `Config.yaml`, `Pages.yaml`, and `Users.yaml`. These files will be automatically generated by AntCMS if they do not exist. -#### Options in `Config/config.yaml` +#### Options in `Config/Config.yaml` - `siteInfo:` - `siteTitle: AntCMS` - This configuration sets the title of your AntCMS website. - `forceHTTPS: true` - Set to 'true' by default, enables HTTPs redirection. - `activeTheme: Default` - Sets what theme AntCMS should use. should match the folder name of the theme you want to use. - `enableCache: true` - Enables or disables file caching in AntCMS. -- `admin:` - - `username: 'Admin'` - The username used to access any parts of AntCMS that may require authentication. - - `password: 'dontmakeitpassword123'` - The password associated with the admin account. Can be entered as plain text and then will automatically be hashed with the first login. This does need to be manually entered initially. - `debug: true`- Enabled or disables debug mode. - `baseURL: antcms.example.com/` - Used to set the baseURL for your AntCMS instance, without the protocol. This will be automatically generated for you, but can be changed if needed. -#### Options in `Config/pages.yaml` +#### Options in `Config/Pages.yaml` -The `pages.yaml` file holds a list of your pages. This file is automatically generated if it doesn't exist. At the moment, AntCMS doesn't automatically regenerate this for you, so for new content to appear you will need to delete the `pages.yaml` file. -The order of which files are stored inside of the `pages.yaml` file dictates what order they will be displayed in the browser window. -Here's what the `pages.yaml` file looks like: +The `Pages.yaml` file holds a list of your pages. This file is automatically generated if it doesn't exist. At the moment, AntCMS doesn't automatically regenerate this for you, so for new content to appear you will need to delete the `Pages.yaml` file. +The order of which files are stored inside of the `Pages.yaml` file dictates what order they will be displayed in the browser window. +Here's what the `Pages.yaml` file looks like: - `pageTitle: 'Hello World'` - This defines what the title of the page is in the navbar. - `fullPagePath: /antcms.example.com/public_html/Content/index.md` - This defines the full path to your page, as PHP would use to access it. @@ -65,7 +62,7 @@ Here's what the `pages.yaml` file looks like: #### The Admin Plugin -AntCMS has a very simple admin plugin. Once you set your password in your `Config/config.yaml`, you can access it by visiting `antcms.example.com/plugin/admin`. +AntCMS has a very simple admin plugin that you can access it by visiting `antcms.example.com/admin`. It will then require you to authenticate using your AntCMS credentials and from there will give you a few simple actions such as editing your config, a page, or regenerating the page list. The admin plugin also features a live preview of the content you are creating, but it's important to note that the preview doesn't support all of the markdown syntax that AntCMS does, such as emojis. diff --git a/src/AntCMS/AntAuth.php b/src/AntCMS/AntAuth.php index 43060c0..4ee5b2c 100644 --- a/src/AntCMS/AntAuth.php +++ b/src/AntCMS/AntAuth.php @@ -3,10 +3,34 @@ namespace AntCMS; use AntCMS\AntConfig; -use AntCMS\AntCMS; class AntAuth { + protected $role; + protected $username; + protected $authenticated; + + public function getRole() + { + return $this->role; + } + + public function getUsername() + { + return $this->username; + } + + public function getName() + { + $currentUser = AntUsers::getUser($this->username); + return $currentUser['name']; + } + + public function isAuthenticated() + { + return $this->authenticated ?? false; + } + /** * Check if the user is authenticated using the credentials in the config file. * If the plain text password in the config file is still present, it will be hashed and the config file will be updated. @@ -14,35 +38,38 @@ class AntAuth * * @return void */ - public static function checkAuth() + public function checkAuth() { - $currentConfig = AntConfig::currentConfig(); $username = $_SERVER['PHP_AUTH_USER'] ?? null; $password = $_SERVER['PHP_AUTH_PW'] ?? null; - if (empty($currentConfig['admin']['password'])) { - AntCMS::renderException('401', 401, 'You must set a password in your config.yaml file before you can authenticate within AntCMS.'); + $currentUser = AntUsers::getUser($username); + + if (is_null($currentUser) || empty($currentUser['password'])) { + $this->requireAuth(); } // If the stored password is not hashed in the config, hash it - if ($password == $currentConfig['admin']['password']) { - $currentConfig['admin']['password'] = password_hash($currentConfig['admin']['password'], PASSWORD_DEFAULT); - AntConfig::saveConfig($currentConfig); + if ($password == $currentUser['password']) { + AntUsers::updateUser($username, ['password' => $password]); - // Reload the config so the next step can pass - $currentConfig = AntConfig::currentConfig(); + // Reload the user info so the next step can pass + $currentUser = AntUsers::getUser($username); } // If the credentials are still set valid, but the auth cookie has expired, re-require authentication. - if (!isset($_COOKIE['auth'])) { - AntAuth::requireAuth(); + if (!isset($_COOKIE['auth']) && $_COOKIE['auth'] == 'valid') { + $this->requireAuth(); } - if ($currentConfig['admin']['username'] == $username && password_verify($password, $currentConfig['admin']['password'])) { + if (password_verify($password, $currentUser['password'])) { + $this->username = $username; + $this->role = $currentUser['role'] ?? ''; + return; } - AntAuth::requireAuth(); + $this->requireAuth(); } /** @@ -50,9 +77,9 @@ class AntAuth * * @return void */ - private static function requireAuth() + private function requireAuth() { - setcookie("auth", "true"); + setcookie("auth", "valid"); $title = AntConfig::currentConfig('siteInfo.siteTitle'); header('WWW-Authenticate: Basic realm="' . $title . '"'); @@ -60,4 +87,10 @@ class AntAuth echo 'You must enter a valid username and password to access this page'; exit; } + + public function invalidateSession() + { + $this->authenticated = false; + $this->requireAuth(); + } } diff --git a/src/AntCMS/AntCMS.php b/src/AntCMS/AntCMS.php index b5f24c7..e757a6b 100644 --- a/src/AntCMS/AntCMS.php +++ b/src/AntCMS/AntCMS.php @@ -8,6 +8,13 @@ use AntCMS\AntConfig; class AntCMS { + protected $antTwig; + + public function __construct() + { + $this->antTwig = new AntTwig(); + } + /** * Renders a page based on the provided page name. * @@ -31,8 +38,9 @@ class AntCMS 'AntCMSAuthor' => $content['author'], 'AntCMSKeywords' => $content['keywords'], 'AntCMSBody' => AntMarkdown::renderMarkdown($content['content']), + 'DisplayAuthor' => true, ]; - $pageTemplate = AntTwig::renderWithTiwg($pageTemplate, $params); + $pageTemplate = $this->antTwig->renderWithTiwg($pageTemplate, $params); $end_time = microtime(true); $elapsed_time = round($end_time - $start_time, 4); @@ -55,8 +63,8 @@ class AntCMS { $siteInfo = AntCMS::getSiteInfo(); - $pageTemplate = self::getThemeTemplate('default_layout', $theme); - $pageTemplate = str_replace('', AntPages::generateNavigation(self::getThemeTemplate('nav_layout', $theme), $currentPage), $pageTemplate); + $pageTemplate = self::getThemeTemplate('default', $theme); + $pageTemplate = str_replace('', AntPages::generateNavigation(self::getThemeTemplate('nav', $theme), $currentPage), $pageTemplate); return $pageTemplate = str_replace('', $siteInfo['siteTitle'], $pageTemplate); } @@ -69,7 +77,7 @@ class AntCMS * @param string $exceptionString An optional parameter to define a custom string to be displayed along side the exception. * @return never */ - public static function renderException(string $exceptionCode, int $httpCode = 404, string $exceptionString = 'That request caused an exception to be thrown.') + public function renderException(string $exceptionCode, int $httpCode = 404, string $exceptionString = 'That request caused an exception to be thrown.') { $exceptionString .= " (Code {$exceptionCode})"; $pageTemplate = self::getPageLayout(); @@ -79,7 +87,7 @@ class AntCMS 'AntCMSBody' => '

An error ocurred

' . $exceptionString . '

', ]; try { - $pageTemplate = AntTwig::renderWithTiwg($pageTemplate, $params); + $pageTemplate = $this->antTwig->renderWithTiwg($pageTemplate, $params); } catch (\Exception) { $pageTemplate = str_replace('{{ AntCMSTitle }}', $params['AntCMSTitle'], $pageTemplate); $pageTemplate = str_replace('{{ AntCMSBody | raw }} ', $params['AntCMSBody'], $pageTemplate); @@ -117,7 +125,7 @@ class AntCMS * @param string|null $theme * @return string */ - public static function getThemeTemplate(string $layout = 'default_layout', string $theme = null) + public static function getThemeTemplate(string $layout = 'default', string $theme = null) { $theme = $theme ?? AntConfig::currentConfig('activeTheme'); @@ -125,22 +133,25 @@ class AntCMS $theme = 'Default'; } - $templatePath = AntTools::repairFilePath(antThemePath . '/' . $theme . '/' . 'Templates'); - $defaultTemplates = AntTools::repairFilePath(antThemePath . '/Default/Templates'); - - $templates = AntTools::getFileList($templatePath, 'twig'); + if (strpos($layout, '_') !== false) { + $layoutPrefix = explode('_', $layout)[0]; + $templatePath = AntTools::repairFilePath(antThemePath . '/' . $theme . '/' . 'Templates' . '/' . $layoutPrefix); + $defaultTemplates = AntTools::repairFilePath(antThemePath . '/Default/Templates' . '/' . $layoutPrefix); + } else { + $templatePath = AntTools::repairFilePath(antThemePath . '/' . $theme . '/' . 'Templates'); + $defaultTemplates = AntTools::repairFilePath(antThemePath . '/Default/Templates'); + } try { - if (in_array($layout . '.html.twig', $templates)) { - $template = file_get_contents(AntTools::repairFilePath($templatePath . '/' . $layout . '.html.twig')); - } else { + $template = @file_get_contents(AntTools::repairFilePath($templatePath . '/' . $layout . '.html.twig')); + if (empty($template)) { $template = file_get_contents(AntTools::repairFilePath($defaultTemplates . '/' . $layout . '.html.twig')); } } catch (\Exception) { } if (empty($template)) { - if ($layout == 'default_layout') { + if ($layout == 'default') { $template = ' @@ -222,4 +233,11 @@ class AntCMS readfile($path); } } + + public static function redirect(string $url) + { + $url = '//' . AntTools::repairURL(AntConfig::currentConfig('baseURL') . $url); + header("Location: $url"); + exit; + } } diff --git a/src/AntCMS/AntConfig.php b/src/AntCMS/AntConfig.php index ed25d38..134e3ae 100644 --- a/src/AntCMS/AntConfig.php +++ b/src/AntCMS/AntConfig.php @@ -12,7 +12,6 @@ class AntConfig 'forceHTTPS', 'activeTheme', 'enableCache', - 'admin', 'debug', 'baseURL', ]; @@ -30,10 +29,6 @@ class AntConfig 'forceHTTPS' => true, 'activeTheme' => 'Default', 'enableCache' => true, - 'admin' => array( - 'username' => 'Admin', - 'password' => '', - ), 'debug' => true, 'baseURL' => $_SERVER['HTTP_HOST'] . dirname($_SERVER['PHP_SELF']), ]; diff --git a/src/AntCMS/AntPages.php b/src/AntCMS/AntPages.php index 291a128..50ca583 100644 --- a/src/AntCMS/AntPages.php +++ b/src/AntCMS/AntPages.php @@ -66,6 +66,7 @@ class AntPages { $pages = AntPages::getPages(); $antCache = new AntCache; + $antTwig = new AntTwig(); $theme = AntConfig::currentConfig('activeTheme'); $cacheKey = $antCache->createCacheKey(json_encode($pages), $theme . $currentPage); @@ -95,7 +96,7 @@ class AntPages } } - $navHTML = AntTwig::renderWithTiwg($navTemplate, array('pages' => $pages)); + $navHTML = $antTwig->renderWithTiwg($navTemplate, array('pages' => $pages)); $antCache->setCache($cacheKey, $navHTML); return $navHTML; diff --git a/src/AntCMS/AntTools.php b/src/AntCMS/AntTools.php index f0dd60a..8f6c23f 100644 --- a/src/AntCMS/AntTools.php +++ b/src/AntCMS/AntTools.php @@ -65,4 +65,15 @@ class AntTools return AntTools::repairFilePath($pagePath); } + + public static function valuesNotNull(array $required, array $actual) + { + foreach ($required as $key) { + if (!key_exists($key, $actual) or is_null($actual[$key])) { + return false; + } + } + + return true; + } } diff --git a/src/AntCMS/AntTwig.php b/src/AntCMS/AntTwig.php index e5ba7c4..44e1353 100644 --- a/src/AntCMS/AntTwig.php +++ b/src/AntCMS/AntTwig.php @@ -3,36 +3,40 @@ namespace AntCMS; use AntCMS\AntConfig; -use AntCMS\AntTwigFilters; class AntTwig { - /** - * @param array $params - * @param string|null $theme - * @return string - */ - public static function renderWithTiwg(string $content = '', array $params = array(), string $theme = null) + protected $twigEnvironment; + protected $theme; + + public function __construct(string $theme = null) { $twigCache = AntConfig::currentConfig('enableCache') ? AntCachePath : false; - $theme = $theme ?? AntConfig::currentConfig('activeTheme'); + $this->theme = $theme ?? AntConfig::currentConfig('activeTheme'); - if (!is_dir(antThemePath . '/' . $theme)) { - $theme = 'Default'; + if (!is_dir(antThemePath . '/' . $this->theme)) { + $this->theme = 'Default'; } - $templatePath = AntTools::repairFilePath(antThemePath . '/' . $theme . '/' . 'Templates'); - - $filesystemLoader = new \Twig\Loader\FilesystemLoader($templatePath); - $stringLoader = new \Shapecode\Twig\Loader\StringLoader(); - $chainLoader = new \Twig\Loader\ChainLoader([$stringLoader, $filesystemLoader]); - $twigEnvironment = new \Twig\Environment($chainLoader, [ + $this->twigEnvironment = new \Twig\Environment(new \Shapecode\Twig\Loader\StringLoader(), [ 'cache' => $twigCache, 'debug' => AntConfig::currentConfig('debug'), ]); - $twigEnvironment->addExtension(new \AntCMS\AntTwigFilters); + $this->twigEnvironment->addExtension(new \AntCMS\AntTwigFilters); + } - return $twigEnvironment->render($content, $params); + public function renderWithSubLayout(string $layout, array $params = array()) + { + $subLayout = AntCMS::getThemeTemplate($layout, $this->theme); + $mainLayout = AntCMS::getPageLayout($this->theme); + $params['AntCMSBody'] = $this->twigEnvironment->render($subLayout, $params); + + return $this->twigEnvironment->render($mainLayout, $params); + } + + public function renderWithTiwg(string $content = '', array $params = array()) + { + return $this->twigEnvironment->render($content, $params); } } diff --git a/src/AntCMS/AntUsers.php b/src/AntCMS/AntUsers.php new file mode 100644 index 0000000..00e7d8b --- /dev/null +++ b/src/AntCMS/AntUsers.php @@ -0,0 +1,130 @@ + password_hash($data['password'], PASSWORD_DEFAULT), + 'role' => $data['role'], + 'name' => $data['display-name'], + ]; + return AntYaml::saveFile(antUsersList, $users); + } + + public static function updateUser($username, $newData) + { + foreach ($newData as $key => $value) { + if (empty($value)) { + throw new \Exception("Key $key cannot be empty."); + } + } + + $users = self::getUsers(); + if (!key_exists($username, $users)) { + throw new \Exception("There was an error when updating the selected user."); + } + + if (isset($newData['password'])) { + $users[$username]['password'] = password_hash($newData['password'], PASSWORD_DEFAULT); + } + + if (isset($newData['role'])) { + $users[$username]['role'] = $newData['role']; + } + + if (isset($newData['name'])) { + $newData['name'] = trim($newData['name']); + $users[$username]['name'] = $newData['name']; + } + + if (isset($newData['username'])) { + $newData['username'] = trim($newData['username']); + Self::validateUsername($newData['username']); + if (key_exists($newData['username'], $users) && $newData['username'] !== $username) { + throw new \Exception("Username is already taken."); + } + + $user = $users[$username]; + unset($users[$username]); + $users[$newData['username']] = $user; + } + + return AntYaml::saveFile(antUsersList, $users); + } + + public static function setupFirstUser($data) + { + if (file_exists(antUsersList)) { + AntCMS::redirect('/'); + } + + $data['username'] = trim($data['username']); + $data['name'] = trim($data['name']); + Self::validateUsername($data['username']); + + $users = [ + $data['username'] => [ + 'password' => password_hash($data['password'], PASSWORD_DEFAULT), + 'role' => 'admin', + 'name' => $data['name'], + ], + ]; + + return AntYaml::saveFile(antUsersList, $users); + } + + private static function validateUsername($username) + { + $pattern = '/^[\p{L}\p{M}*0-9]+$/u'; + if (!preg_match($pattern, $username)) { + throw new \Exception("Invalid username: \"$username\". Usernames can only contain letters, numbers, and combining marks."); + } + return true; + } +} diff --git a/src/Constants.php b/src/Constants.php index c989bc8..d59359b 100644 --- a/src/Constants.php +++ b/src/Constants.php @@ -1,8 +1,9 @@ getPageLayout(); + + $this->auth = new AntAuth; + $this->auth->checkAuth(); + + $this->antCMS = new AntCMS; + $this->AntTwig = new AntTwig(); + array_shift($route); switch ($currentStep) { @@ -36,20 +44,19 @@ class AdminPlugin extends AntPlugin case 'pages': $this->managePages($route); - default: - $HTMLTemplate = "

AntCMS Admin Plugin

\n"; - $HTMLTemplate .= "AntCMS Configuration
\n"; - $HTMLTemplate .= "Page management
\n"; + case 'users': + $this->userManagement($route); - $params = array( + default: + $params = [ 'AntCMSTitle' => 'AntCMS Admin Dashboard', 'AntCMSDescription' => 'The AntCMS admin dashboard', 'AntCMSAuthor' => 'AntCMS', 'AntCMSKeywords' => '', - 'AntCMSBody' => $HTMLTemplate, - ); + 'user' => AntUsers::getUserPublicalKeys($this->auth->getUsername()), - echo AntTwig::renderWithTiwg($pageTemplate, $params); + ]; + echo $this->AntTwig->renderWithSubLayout('admin_landing', $params); break; } } @@ -60,27 +67,30 @@ class AdminPlugin extends AntPlugin */ private function configureAntCMS(array $route) { - $antCMS = new AntCMS; - $pageTemplate = $antCMS->getPageLayout(); - $HTMLTemplate = $antCMS->getThemeTemplate('textarea_edit_layout'); + if ($this->auth->getRole() != 'admin') { + $this->antCMS->renderException("You are not permitted to visit this page."); + } + $currentConfig = AntConfig::currentConfig(); $currentConfigFile = file_get_contents(antConfigFile); $params = array( 'AntCMSTitle' => 'AntCMS Configuration', 'AntCMSDescription' => 'The AntCMS configuration screen', 'AntCMSAuthor' => 'AntCMS', - 'AntCMSKeywords' => 'N/A', + 'AntCMSKeywords' => '', ); switch ($route[0] ?? 'none') { case 'edit': - $HTMLTemplate = str_replace('', '//' . $currentConfig['baseURL'] . 'plugin/admin/config/save', $HTMLTemplate); - $HTMLTemplate = str_replace('', htmlspecialchars($currentConfigFile), $HTMLTemplate); + $params['AntCMSActionURL'] = '//' . $currentConfig['baseURL'] . 'admin/config/save'; + $params['AntCMSTextAreaContent'] = htmlspecialchars($currentConfigFile); + + echo $this->AntTwig->renderWithSubLayout('textareaEdit', $params); break; case 'save': if (!$_POST['textarea']) { - header('Location: //' . $currentConfig['baseURL'] . "plugin/admin/config/"); + AntCMS::redirect('/admin/config'); } $yaml = AntYaml::parseYaml($_POST['textarea']); @@ -88,34 +98,26 @@ class AdminPlugin extends AntPlugin AntYaml::saveFile(antConfigFile, $yaml); } - header('Location: //' . $currentConfig['baseURL'] . "plugin/admin/config/"); - exit; + AntCMS::redirect('/admin/config'); + break; default: - $HTMLTemplate = "

AntCMS Configuration

\n"; - $HTMLTemplate .= "Click here to edit the config file
\n"; - $HTMLTemplate .= "\n"; + $params['currentConfig'] = $currentConfig; + echo $this->AntTwig->renderWithSubLayout('admin_config', $params); + break; } - - $params['AntCMSBody'] = $HTMLTemplate; - echo AntTwig::renderWithTiwg($pageTemplate, $params); exit; } @@ -125,21 +127,18 @@ class AdminPlugin extends AntPlugin */ private function managePages(array $route) { - $antCMS = new AntCMS; - $pageTemplate = $antCMS->getPageLayout(); - $HTMLTemplate = $antCMS->getThemeTemplate('markdown_edit_layout'); $pages = AntPages::getPages(); $params = array( 'AntCMSTitle' => 'AntCMS Page Management', 'AntCMSDescription' => 'The AntCMS page management screen', 'AntCMSAuthor' => 'AntCMS', - 'AntCMSKeywords' => 'N/A', + 'AntCMSKeywords' => '', ); switch ($route[0] ?? 'none') { case 'regenerate': AntPages::generatePages(); - header('Location: //' . AntConfig::currentConfig('baseURL') . "plugin/admin/pages/"); + AntCMS::redirect('/admin/pages'); exit; case 'edit': @@ -159,11 +158,14 @@ class AdminPlugin extends AntPlugin } $pagePath = AntTools::repairFilePath($pagePath); - $page = "--AntCMS--\nTitle: New Page Title\nAuthor: Author\nDescription: Description of this page.\nKeywords: Keywords\n--AntCMS--\n"; + $name = $this->auth->getName(); + $page = "--AntCMS--\nTitle: New Page Title\nAuthor: $name\nDescription: Description of this page.\nKeywords: Keywords\n--AntCMS--\n"; } - $HTMLTemplate = str_replace('', '//' . AntConfig::currentConfig('baseURL') . "plugin/admin/pages/save/{$pagePath}", $HTMLTemplate); - $HTMLTemplate = str_replace('', htmlspecialchars($page), $HTMLTemplate); + $params['AntCMSActionURL'] = '//' . AntConfig::currentConfig('baseURL') . "admin/pages/save/{$pagePath}"; + $params['AntCMSTextAreaContent'] = $page; + + echo $this->AntTwig->renderWithSubLayout('markdownEdit', $params); break; case 'save': @@ -171,22 +173,16 @@ class AdminPlugin extends AntPlugin $pagePath = AntTools::repairFilePath(antContentPath . '/' . implode('/', $route)); if (!isset($_POST['textarea'])) { - header('Location: //' . AntConfig::currentConfig('baseURL') . "plugin/admin/pages/"); + AntCMS::redirect('/admin/pages'); } file_put_contents($pagePath, $_POST['textarea']); - header('Location: //' . AntConfig::currentConfig('baseURL') . "plugin/admin/pages/"); + AntCMS::redirect('/admin/pages'); exit; case 'create': - $HTMLTemplate = "

Page Management

\n"; - $HTMLTemplate .= "

Create new page

\n"; - $HTMLTemplate .= '
'; - $HTMLTemplate .= - '
- - -
'; + $params['BaseURL'] = AntConfig::currentConfig('baseURL'); + echo $this->AntTwig->renderWithSubLayout('admin_newPage', $params); break; case 'delete': @@ -205,7 +201,7 @@ class AdminPlugin extends AntPlugin AntYaml::saveFile(antPagesList, $pages); } - header('Location: //' . AntConfig::currentConfig('baseURL') . "plugin/admin/pages/"); + AntCMS::redirect('/admin/pages'); break; case 'togglevisibility': @@ -219,23 +215,103 @@ class AdminPlugin extends AntPlugin } AntYaml::saveFile(antPagesList, $pages); - header('Location: //' . AntConfig::currentConfig('baseURL') . "plugin/admin/pages/"); + AntCMS::redirect('/admin/pages'); break; default: - $HTMLTemplate = $antCMS->getThemeTemplate('admin_manage_pages_layout'); foreach ($pages as $key => $page) { - $pages[$key]['editurl'] = '//' . AntTools::repairURL(AntConfig::currentConfig('baseURL') . "/plugin/admin/pages/edit/" . $page['functionalPagePath']); - $pages[$key]['deleteurl'] = '//' . AntTools::repairURL(AntConfig::currentConfig('baseURL') . "/plugin/admin/pages/delete/" . $page['functionalPagePath']); - $pages[$key]['togglevisibility'] = '//' . AntTools::repairURL(AntConfig::currentConfig('baseURL') . "/plugin/admin/pages/togglevisibility/" . $page['functionalPagePath']); + $pages[$key]['editurl'] = '//' . AntTools::repairURL(AntConfig::currentConfig('baseURL') . "/admin/pages/edit/" . $page['functionalPagePath']); + $pages[$key]['deleteurl'] = '//' . AntTools::repairURL(AntConfig::currentConfig('baseURL') . "/admin/pages/delete/" . $page['functionalPagePath']); + $pages[$key]['togglevisibility'] = '//' . AntTools::repairURL(AntConfig::currentConfig('baseURL') . "/admin/pages/togglevisibility/" . $page['functionalPagePath']); $pages[$key]['isvisable'] = $this->boolToWord($page['showInNav']); } - $params['pages'] = $pages; - $HTMLTemplate = AntTwig::renderWithTiwg($HTMLTemplate, $params); + $params = [ + 'AntCMSTitle' => 'AntCMS Admin Dashboard', + 'AntCMSDescription' => 'The AntCMS admin dashboard', + 'AntCMSAuthor' => 'AntCMS', + 'AntCMSKeywords' => '', + 'pages' => $pages, + ]; + echo $this->AntTwig->renderWithSubLayout('admin_managePages', $params); + break; + } + exit; + } + + private function userManagement(array $route) + { + if ($this->auth->getRole() != 'admin') { + $this->antCMS->renderException("You are not permitted to visit this page."); } - $params['AntCMSBody'] = $HTMLTemplate; - echo AntTwig::renderWithTiwg($pageTemplate, $params); + $params = array( + 'AntCMSTitle' => 'AntCMS User Management', + 'AntCMSDescription' => 'The AntCMS user management screen', + 'AntCMSAuthor' => 'AntCMS', + 'AntCMSKeywords' => '', + ); + + switch ($route[0] ?? 'none') { + case 'add': + echo $this->AntTwig->renderWithSubLayout('admin_userAdd', $params); + break; + + case 'edit': + $user = AntUsers::getUserPublicalKeys($route[1]); + + if (!$user) { + AntCMS::redirect('/admin/users'); + } + + $user['username'] = $route[1]; + $params['user'] = $user; + + echo $this->AntTwig->renderWithSubLayout('admin_userEdit', $params); + break; + + case 'resetpassword': + $user = AntUsers::getUserPublicalKeys($route[1]); + + if (!$user) { + AntCMS::redirect('/admin/users'); + } + + $user['username'] = $route[1]; + $params['user'] = $user; + + echo $this->AntTwig->renderWithSubLayout('admin_userResetPassword', $params); + break; + + case 'save': + $data['username'] = $_POST['username'] ?? null; + $data['name'] = $_POST['display-name'] ?? null; + $data['role'] = $_POST['role'] ?? null; + $data['password'] = $_POST['password'] ?? null; + + foreach ($data as $key => $value) { + if (is_null($value)) { + unset($data[$key]); + } + } + + AntUsers::updateUser($_POST['originalusername'], $data); + AntCMS::redirect('/admin/users'); + break; + case 'savenew': + AntUsers::addUser($_POST); + AntCMS::redirect('/admin/users'); + break; + + default: + $users = AntUsers::getUsers(); + foreach ($users as $key => $user) { + unset($users[$key]['password']); + $users[$key]['username'] = $key; + } + $params['users'] = $users; + echo $this->AntTwig->renderWithSubLayout('admin_users', $params); + break; + } exit; } diff --git a/src/Plugins/Profile/ProfilePlugin.php b/src/Plugins/Profile/ProfilePlugin.php new file mode 100644 index 0000000..792df49 --- /dev/null +++ b/src/Plugins/Profile/ProfilePlugin.php @@ -0,0 +1,119 @@ +antAuth = new AntAuth; + $this->antTwig = new AntTwig; + $currentStep = $route[0] ?? 'none'; + + $params = [ + 'AntCMSTitle' => 'AntCMS Profile Management', + 'AntCMSDescription' => 'AntCMS Profile Management', + 'AntCMSAuthor' => 'AntCMS', + 'AntCMSKeywords' => '', + ]; + + switch ($currentStep) { + case 'firsttime': + if (file_exists(antUsersList)) { + AntCMS::redirect('/admin'); + } + echo $this->antTwig->renderWithSubLayout('profile_firstTime', $params); + break; + + case 'submitfirst': + if (file_exists(antUsersList)) { + AntCMS::redirect('/admin'); + } + + if (isset($_POST['username']) && isset($_POST['password']) && isset($_POST['display-name'])) { + $data = [ + 'username' => $_POST['username'], + 'password' => $_POST['password'], + 'name' => $_POST['display-name'], + ]; + AntUsers::setupFirstUser($data); + AntCMS::redirect('/admin'); + } else { + AntCMS::redirect('/profile/firsttime'); + } + break; + + case 'edit': + $this->antAuth->checkAuth(); + $user = AntUsers::getUserPublicalKeys($this->antAuth->getUsername()); + + if (!$user) { + AntCMS::redirect('/profile'); + } + + $user['username'] = $this->antAuth->getUsername(); + $params['user'] = $user; + + echo $this->antTwig->renderWithSubLayout('profile_edit', $params); + break; + + case 'resetpassword': + $this->antAuth->checkAuth(); + $user = AntUsers::getUserPublicalKeys($this->antAuth->getUsername()); + + if (!$user) { + AntCMS::redirect('/profile'); + } + + $user['username'] = $this->antAuth->getUsername(); + $params['user'] = $user; + + echo $this->antTwig->renderWithSubLayout('profile_resetPassword', $params); + break; + + case 'save': + $this->antAuth->checkAuth(); + $data['username'] = $_POST['username'] ?? null; + $data['name'] = $_POST['display-name'] ?? null; + $data['password'] = $_POST['password'] ?? null; + + foreach ($data as $key => $value) { + if (is_null($value)) { + unset($data[$key]); + } + } + + AntUsers::updateUser($this->antAuth->getUsername(), $data); + AntCMS::redirect('/profile'); + break; + + case 'logout': + $this->antAuth->invalidateSession(); + if (!$this->antAuth->isAuthenticated()) { + echo "You have been logged out."; + } else { + echo "There was an error logging you out."; + } + exit; + + default: + $this->antAuth->checkAuth(); + $params['user'] = AntUsers::getUserPublicalKeys($this->antAuth->getUsername()); + echo $this->antTwig->renderWithSubLayout('profile_landing', $params); + } + exit; + } + + public function getName(): string + { + return 'Profile'; + } +} diff --git a/src/Themes/Bootstrap/Templates/admin/admin_newPage.html.twig b/src/Themes/Bootstrap/Templates/admin/admin_newPage.html.twig new file mode 100644 index 0000000..e3cdba3 --- /dev/null +++ b/src/Themes/Bootstrap/Templates/admin/admin_newPage.html.twig @@ -0,0 +1,9 @@ +

Page Management

+

Create new page

+
+
+ + + +
+
diff --git a/src/Themes/Bootstrap/Templates/admin/admin_userAdd.html.twig b/src/Themes/Bootstrap/Templates/admin/admin_userAdd.html.twig new file mode 100644 index 0000000..4da6e04 --- /dev/null +++ b/src/Themes/Bootstrap/Templates/admin/admin_userAdd.html.twig @@ -0,0 +1,28 @@ +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+
diff --git a/src/Themes/Bootstrap/Templates/admin/admin_userEdit.html.twig b/src/Themes/Bootstrap/Templates/admin/admin_userEdit.html.twig new file mode 100644 index 0000000..ff07b93 --- /dev/null +++ b/src/Themes/Bootstrap/Templates/admin/admin_userEdit.html.twig @@ -0,0 +1,25 @@ +
+ + +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+
diff --git a/src/Themes/Bootstrap/Templates/admin/admin_userResetPassword.html.twig b/src/Themes/Bootstrap/Templates/admin/admin_userResetPassword.html.twig new file mode 100644 index 0000000..3e3d5c0 --- /dev/null +++ b/src/Themes/Bootstrap/Templates/admin/admin_userResetPassword.html.twig @@ -0,0 +1,12 @@ +
+ + +
+ + +
+ +
+ +
+
diff --git a/src/Themes/Bootstrap/Templates/default_layout.html.twig b/src/Themes/Bootstrap/Templates/default.html.twig similarity index 90% rename from src/Themes/Bootstrap/Templates/default_layout.html.twig rename to src/Themes/Bootstrap/Templates/default.html.twig index 9a3a2c9..3130499 100644 --- a/src/Themes/Bootstrap/Templates/default_layout.html.twig +++ b/src/Themes/Bootstrap/Templates/default.html.twig @@ -38,7 +38,12 @@
-
{{ AntCMSBody | raw }}
+
+ {% if DisplayAuthor and false %} +

This content was written by {{ AntCMSAuthor }}

+ {% endif %} + {{ AntCMSBody | raw }} +
diff --git a/src/Themes/Bootstrap/Templates/markdown_edit_layout.html.twig b/src/Themes/Bootstrap/Templates/markdownEdit.html.twig similarity index 78% rename from src/Themes/Bootstrap/Templates/markdown_edit_layout.html.twig rename to src/Themes/Bootstrap/Templates/markdownEdit.html.twig index a73f479..aa792c2 100644 --- a/src/Themes/Bootstrap/Templates/markdown_edit_layout.html.twig +++ b/src/Themes/Bootstrap/Templates/markdownEdit.html.twig @@ -2,9 +2,9 @@

Page Content

-
+ + class="form-control">{{ AntCMSTextAreaContent }}
@@ -18,7 +18,7 @@ const output = document.getElementById("markdown-output"); function parseMarkdown() { - const inputValue = input.value.replace(/\A--AntCMS--.*?--AntCMS--/sm, ""); + const inputValue = input.value.replace(/^--AntCMS--[\s\S]*?--AntCMS--/gm, ""); output.innerHTML = marked.parse(inputValue); } diff --git a/src/Themes/Bootstrap/Templates/nav_layout.html.twig b/src/Themes/Bootstrap/Templates/nav.html.twig similarity index 100% rename from src/Themes/Bootstrap/Templates/nav_layout.html.twig rename to src/Themes/Bootstrap/Templates/nav.html.twig diff --git a/src/Themes/Bootstrap/Templates/profile/profile_edit.html.twig b/src/Themes/Bootstrap/Templates/profile/profile_edit.html.twig new file mode 100644 index 0000000..56d6485 --- /dev/null +++ b/src/Themes/Bootstrap/Templates/profile/profile_edit.html.twig @@ -0,0 +1,15 @@ +
+
+ + +
+ +
+ + +
+ +
+ +
+
diff --git a/src/Themes/Bootstrap/Templates/profile/profile_resetPassword.html.twig b/src/Themes/Bootstrap/Templates/profile/profile_resetPassword.html.twig new file mode 100644 index 0000000..df6d290 --- /dev/null +++ b/src/Themes/Bootstrap/Templates/profile/profile_resetPassword.html.twig @@ -0,0 +1,10 @@ +
+
+ + +
+ +
+ +
+
diff --git a/src/Themes/Bootstrap/Templates/textareaEdit.html.twig b/src/Themes/Bootstrap/Templates/textareaEdit.html.twig new file mode 100644 index 0000000..2a5c678 --- /dev/null +++ b/src/Themes/Bootstrap/Templates/textareaEdit.html.twig @@ -0,0 +1,5 @@ +
+ + +
diff --git a/src/Themes/Bootstrap/Templates/textarea_edit_layout.html.twig b/src/Themes/Bootstrap/Templates/textarea_edit_layout.html.twig deleted file mode 100644 index f4e8b21..0000000 --- a/src/Themes/Bootstrap/Templates/textarea_edit_layout.html.twig +++ /dev/null @@ -1,5 +0,0 @@ -
- - -
\ No newline at end of file diff --git a/src/Themes/Default/Assets/Dist/tailwind.css b/src/Themes/Default/Assets/Dist/tailwind.css index 7d84a2c..319dc7a 100644 --- a/src/Themes/Default/Assets/Dist/tailwind.css +++ b/src/Themes/Default/Assets/Dist/tailwind.css @@ -1 +1 @@ -/*! tailwindcss v3.2.4 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:initial}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;font-weight:inherit;line-height:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button;background-color:initial;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:initial}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]{display:none}[multiple],[type=date],[type=datetime-local],[type=email],[type=month],[type=number],[type=password],[type=search],[type=tel],[type=text],[type=time],[type=url],[type=week],select,textarea{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:#fff;border-color:#6b7280;border-width:1px;border-radius:0;padding:.5rem .75rem;font-size:1rem;line-height:1.5rem;--tw-shadow:0 0 #0000}[multiple]:focus,[type=date]:focus,[type=datetime-local]:focus,[type=email]:focus,[type=month]:focus,[type=number]:focus,[type=password]:focus,[type=search]:focus,[type=tel]:focus,[type=text]:focus,[type=time]:focus,[type=url]:focus,[type=week]:focus,select:focus,textarea:focus{outline:2px solid #0000;outline-offset:2px;--tw-ring-inset:var(--tw-empty,/*!*/ /*!*/);--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#2563eb;--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);border-color:#2563eb}input::-moz-placeholder,textarea::-moz-placeholder{color:#6b7280;opacity:1}input::placeholder,textarea::placeholder{color:#6b7280;opacity:1}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-date-and-time-value{min-height:1.5em}::-webkit-datetime-edit,::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-meridiem-field,::-webkit-datetime-edit-millisecond-field,::-webkit-datetime-edit-minute-field,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-second-field,::-webkit-datetime-edit-year-field{padding-top:0;padding-bottom:0}select{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3E%3Cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='m6 8 4 4 4-4'/%3E%3C/svg%3E");background-position:right .5rem center;background-repeat:no-repeat;background-size:1.5em 1.5em;padding-right:2.5rem;-webkit-print-color-adjust:exact;print-color-adjust:exact}[multiple]{background-image:none;background-position:0 0;background-repeat:unset;background-size:initial;padding-right:.75rem;-webkit-print-color-adjust:unset;print-color-adjust:unset}[type=checkbox],[type=radio]{-webkit-appearance:none;-moz-appearance:none;appearance:none;padding:0;-webkit-print-color-adjust:exact;print-color-adjust:exact;display:inline-block;vertical-align:middle;background-origin:border-box;-webkit-user-select:none;-moz-user-select:none;user-select:none;flex-shrink:0;height:1rem;width:1rem;color:#2563eb;background-color:#fff;border-color:#6b7280;border-width:1px;--tw-shadow:0 0 #0000}[type=checkbox]{border-radius:0}[type=radio]{border-radius:100%}[type=checkbox]:focus,[type=radio]:focus{outline:2px solid #0000;outline-offset:2px;--tw-ring-inset:var(--tw-empty,/*!*/ /*!*/);--tw-ring-offset-width:2px;--tw-ring-offset-color:#fff;--tw-ring-color:#2563eb;--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}[type=checkbox]:checked,[type=radio]:checked{border-color:#0000;background-color:currentColor;background-size:100% 100%;background-position:50%;background-repeat:no-repeat}[type=checkbox]:checked{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 16 16' fill='%23fff' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M12.207 4.793a1 1 0 0 1 0 1.414l-5 5a1 1 0 0 1-1.414 0l-2-2a1 1 0 0 1 1.414-1.414L6.5 9.086l4.293-4.293a1 1 0 0 1 1.414 0z'/%3E%3C/svg%3E")}[type=radio]:checked{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 16 16' fill='%23fff' xmlns='http://www.w3.org/2000/svg'%3E%3Ccircle cx='8' cy='8' r='3'/%3E%3C/svg%3E")}[type=checkbox]:checked:focus,[type=checkbox]:checked:hover,[type=checkbox]:indeterminate,[type=radio]:checked:focus,[type=radio]:checked:hover{border-color:#0000;background-color:currentColor}[type=checkbox]:indeterminate{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 16 16'%3E%3Cpath stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M4 8h8'/%3E%3C/svg%3E");background-size:100% 100%;background-position:50%;background-repeat:no-repeat}[type=checkbox]:indeterminate:focus,[type=checkbox]:indeterminate:hover{border-color:#0000;background-color:currentColor}[type=file]{background:unset;border-color:inherit;border-width:0;border-radius:0;padding:0;font-size:unset;line-height:inherit}[type=file]:focus{outline:1px solid ButtonText;outline:1px auto -webkit-focus-ring-color}*,::backdrop,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#3b82f680;--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.container{width:100%}@media (min-width:640px){.container{max-width:640px}}@media (min-width:768px){.container{max-width:768px}}@media (min-width:1024px){.container{max-width:1024px}}@media (min-width:1280px){.container{max-width:1280px}}@media (min-width:1536px){.container{max-width:1536px}}.prose{color:var(--tw-prose-body);max-width:65ch}.prose :where([class~=lead]):not(:where([class~=not-prose] *)){color:var(--tw-prose-lead);font-size:1.25em;line-height:1.6;margin-top:1.2em;margin-bottom:1.2em}.prose :where(a):not(:where([class~=not-prose] *)){color:var(--tw-prose-links);text-decoration:underline;font-weight:500}.prose :where(strong):not(:where([class~=not-prose] *)){color:var(--tw-prose-bold);font-weight:600}.prose :where(a strong):not(:where([class~=not-prose] *)){color:inherit}.prose :where(blockquote strong):not(:where([class~=not-prose] *)){color:inherit}.prose :where(thead th strong):not(:where([class~=not-prose] *)){color:inherit}.prose :where(ol):not(:where([class~=not-prose] *)){list-style-type:decimal;margin-top:1.25em;margin-bottom:1.25em;padding-left:1.625em}.prose :where(ol[type=A]):not(:where([class~=not-prose] *)){list-style-type:upper-alpha}.prose :where(ol[type=a]):not(:where([class~=not-prose] *)){list-style-type:lower-alpha}.prose :where(ol[type=A s]):not(:where([class~=not-prose] *)){list-style-type:upper-alpha}.prose :where(ol[type=a s]):not(:where([class~=not-prose] *)){list-style-type:lower-alpha}.prose :where(ol[type=I]):not(:where([class~=not-prose] *)){list-style-type:upper-roman}.prose :where(ol[type=i]):not(:where([class~=not-prose] *)){list-style-type:lower-roman}.prose :where(ol[type=I s]):not(:where([class~=not-prose] *)){list-style-type:upper-roman}.prose :where(ol[type=i s]):not(:where([class~=not-prose] *)){list-style-type:lower-roman}.prose :where(ol[type="1"]):not(:where([class~=not-prose] *)){list-style-type:decimal}.prose :where(ul):not(:where([class~=not-prose] *)){list-style-type:disc;margin-top:1.25em;margin-bottom:1.25em;padding-left:1.625em}.prose :where(ol>li):not(:where([class~=not-prose] *))::marker{font-weight:400;color:var(--tw-prose-counters)}.prose :where(ul>li):not(:where([class~=not-prose] *))::marker{color:var(--tw-prose-bullets)}.prose :where(hr):not(:where([class~=not-prose] *)){border-color:var(--tw-prose-hr);border-top-width:1px;margin-top:3em;margin-bottom:3em}.prose :where(blockquote):not(:where([class~=not-prose] *)){font-weight:500;font-style:italic;color:var(--tw-prose-quotes);border-left-width:.25rem;border-left-color:var(--tw-prose-quote-borders);quotes:"\201C""\201D""\2018""\2019";margin-top:1.6em;margin-bottom:1.6em;padding-left:1em}.prose :where(blockquote p:first-of-type):not(:where([class~=not-prose] *)):before{content:open-quote}.prose :where(blockquote p:last-of-type):not(:where([class~=not-prose] *)):after{content:close-quote}.prose :where(h1):not(:where([class~=not-prose] *)){color:var(--tw-prose-headings);font-weight:800;font-size:2.25em;margin-top:0;margin-bottom:.8888889em;line-height:1.1111111}.prose :where(h1 strong):not(:where([class~=not-prose] *)){font-weight:900;color:inherit}.prose :where(h2):not(:where([class~=not-prose] *)){color:var(--tw-prose-headings);font-weight:700;font-size:1.5em;margin-top:2em;margin-bottom:1em;line-height:1.3333333}.prose :where(h2 strong):not(:where([class~=not-prose] *)){font-weight:800;color:inherit}.prose :where(h3):not(:where([class~=not-prose] *)){color:var(--tw-prose-headings);font-weight:600;font-size:1.25em;margin-top:1.6em;margin-bottom:.6em;line-height:1.6}.prose :where(h3 strong):not(:where([class~=not-prose] *)){font-weight:700;color:inherit}.prose :where(h4):not(:where([class~=not-prose] *)){color:var(--tw-prose-headings);font-weight:600;margin-top:1.5em;margin-bottom:.5em;line-height:1.5}.prose :where(h4 strong):not(:where([class~=not-prose] *)){font-weight:700;color:inherit}.prose :where(img):not(:where([class~=not-prose] *)){margin-top:2em;margin-bottom:2em}.prose :where(figure>*):not(:where([class~=not-prose] *)){margin-top:0;margin-bottom:0}.prose :where(figcaption):not(:where([class~=not-prose] *)){color:var(--tw-prose-captions);font-size:.875em;line-height:1.4285714;margin-top:.8571429em}.prose :where(code):not(:where([class~=not-prose] *)){color:#f4f4f5;font-weight:600;font-size:.875em;background-color:#3f3f46;padding:.25rem .375rem;border-radius:.25rem}.prose :where(code):not(:where([class~=not-prose] *)):before{content:none}.prose :where(code):not(:where([class~=not-prose] *)):after{content:none}.prose :where(a code):not(:where([class~=not-prose] *)){color:inherit}.prose :where(h1 code):not(:where([class~=not-prose] *)){color:inherit}.prose :where(h2 code):not(:where([class~=not-prose] *)){color:inherit;font-size:.875em}.prose :where(h3 code):not(:where([class~=not-prose] *)){color:inherit;font-size:.9em}.prose :where(h4 code):not(:where([class~=not-prose] *)){color:inherit}.prose :where(blockquote code):not(:where([class~=not-prose] *)){color:inherit}.prose :where(thead th code):not(:where([class~=not-prose] *)){color:inherit}.prose :where(pre):not(:where([class~=not-prose] *)){color:var(--tw-prose-pre-code);background-color:var(--tw-prose-pre-bg);overflow-x:auto;font-weight:400;font-size:.875em;line-height:1.7142857;margin-top:1.7142857em;margin-bottom:1.7142857em;border-radius:.375rem;padding:.8571429em 1.1428571em}.prose :where(pre code):not(:where([class~=not-prose] *)){background-color:initial;border-width:0;border-radius:0;padding:0;font-weight:inherit;color:inherit;font-size:inherit;font-family:inherit;line-height:inherit}.prose :where(pre code):not(:where([class~=not-prose] *)):before{content:none}.prose :where(pre code):not(:where([class~=not-prose] *)):after{content:none}.prose :where(table):not(:where([class~=not-prose] *)){width:100%;table-layout:auto;text-align:left;margin-top:2em;margin-bottom:2em;font-size:.875em;line-height:1.7142857}.prose :where(thead):not(:where([class~=not-prose] *)){border-bottom-width:1px;border-bottom-color:var(--tw-prose-th-borders)}.prose :where(thead th):not(:where([class~=not-prose] *)){color:var(--tw-prose-headings);font-weight:600;vertical-align:bottom;padding-right:.5714286em;padding-bottom:.5714286em;padding-left:.5714286em}.prose :where(tbody tr):not(:where([class~=not-prose] *)){border-bottom-width:1px;border-bottom-color:var(--tw-prose-td-borders)}.prose :where(tbody tr:last-child):not(:where([class~=not-prose] *)){border-bottom-width:0}.prose :where(tbody td):not(:where([class~=not-prose] *)){vertical-align:initial}.prose :where(tfoot):not(:where([class~=not-prose] *)){border-top-width:1px;border-top-color:var(--tw-prose-th-borders)}.prose :where(tfoot td):not(:where([class~=not-prose] *)){vertical-align:top}.prose{--tw-prose-body:#374151;--tw-prose-headings:#111827;--tw-prose-lead:#4b5563;--tw-prose-links:#111827;--tw-prose-bold:#111827;--tw-prose-counters:#6b7280;--tw-prose-bullets:#d1d5db;--tw-prose-hr:#e5e7eb;--tw-prose-quotes:#111827;--tw-prose-quote-borders:#e5e7eb;--tw-prose-captions:#6b7280;--tw-prose-code:#111827;--tw-prose-pre-code:#e5e7eb;--tw-prose-pre-bg:#1f2937;--tw-prose-th-borders:#d1d5db;--tw-prose-td-borders:#e5e7eb;--tw-prose-invert-body:#d1d5db;--tw-prose-invert-headings:#fff;--tw-prose-invert-lead:#9ca3af;--tw-prose-invert-links:#fff;--tw-prose-invert-bold:#fff;--tw-prose-invert-counters:#9ca3af;--tw-prose-invert-bullets:#4b5563;--tw-prose-invert-hr:#374151;--tw-prose-invert-quotes:#f3f4f6;--tw-prose-invert-quote-borders:#374151;--tw-prose-invert-captions:#9ca3af;--tw-prose-invert-code:#fff;--tw-prose-invert-pre-code:#d1d5db;--tw-prose-invert-pre-bg:#00000080;--tw-prose-invert-th-borders:#4b5563;--tw-prose-invert-td-borders:#374151;font-size:1rem;line-height:1.75}.prose :where(p):not(:where([class~=not-prose] *)){margin-top:1.25em;margin-bottom:1.25em}.prose :where(video):not(:where([class~=not-prose] *)){margin-top:2em;margin-bottom:2em}.prose :where(figure):not(:where([class~=not-prose] *)){margin-top:2em;margin-bottom:2em}.prose :where(li):not(:where([class~=not-prose] *)){margin-top:.5em;margin-bottom:.5em}.prose :where(ol>li):not(:where([class~=not-prose] *)){padding-left:.375em}.prose :where(ul>li):not(:where([class~=not-prose] *)){padding-left:.375em}.prose :where(.prose>ul>li p):not(:where([class~=not-prose] *)){margin-top:.75em;margin-bottom:.75em}.prose :where(.prose>ul>li>:first-child):not(:where([class~=not-prose] *)){margin-top:1.25em}.prose :where(.prose>ul>li>:last-child):not(:where([class~=not-prose] *)){margin-bottom:1.25em}.prose :where(.prose>ol>li>:first-child):not(:where([class~=not-prose] *)){margin-top:1.25em}.prose :where(.prose>ol>li>:last-child):not(:where([class~=not-prose] *)){margin-bottom:1.25em}.prose :where(ul ul,ul ol,ol ul,ol ol):not(:where([class~=not-prose] *)){margin-top:.75em;margin-bottom:.75em}.prose :where(hr+*):not(:where([class~=not-prose] *)){margin-top:0}.prose :where(h2+*):not(:where([class~=not-prose] *)){margin-top:0}.prose :where(h3+*):not(:where([class~=not-prose] *)){margin-top:0}.prose :where(h4+*):not(:where([class~=not-prose] *)){margin-top:0}.prose :where(thead th:first-child):not(:where([class~=not-prose] *)){padding-left:0}.prose :where(thead th:last-child):not(:where([class~=not-prose] *)){padding-right:0}.prose :where(tbody td,tfoot td):not(:where([class~=not-prose] *)){padding:.5714286em}.prose :where(tbody td:first-child,tfoot td:first-child):not(:where([class~=not-prose] *)){padding-left:0}.prose :where(tbody td:last-child,tfoot td:last-child):not(:where([class~=not-prose] *)){padding-right:0}.prose :where(.prose>:first-child):not(:where([class~=not-prose] *)){margin-top:0}.prose :where(.prose>:last-child):not(:where([class~=not-prose] *)){margin-bottom:0}.form-input,.form-multiselect,.form-select,.form-textarea{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:#fff;border-color:#6b7280;border-width:1px;border-radius:0;padding:.5rem .75rem;font-size:1rem;line-height:1.5rem;--tw-shadow:0 0 #0000}.form-input:focus,.form-multiselect:focus,.form-select:focus,.form-textarea:focus{outline:2px solid #0000;outline-offset:2px;--tw-ring-inset:var(--tw-empty,/*!*/ /*!*/);--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#2563eb;--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);border-color:#2563eb}.form-input::-moz-placeholder,.form-textarea::-moz-placeholder{color:#6b7280;opacity:1}.form-input::placeholder,.form-textarea::placeholder{color:#6b7280;opacity:1}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}.mx-auto{margin-left:auto;margin-right:auto}.my-4{margin-top:1rem;margin-bottom:1rem}.-mx-4{margin-left:-1rem;margin-right:-1rem}.ml-3{margin-left:.75rem}.mt-4{margin-top:1rem}.block{display:block}.flex{display:flex}.inline-flex{display:inline-flex}.hidden{display:none}.h-6{height:1.5rem}.w-6{width:1.5rem}.w-full{width:100%}.w-1\/12{width:8.333333%}.w-10\/12{width:83.333333%}.w-1\/2{width:50%}.max-w-none{max-width:none}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.justify-between{justify-content:space-between}.self-center{align-self:center}.whitespace-nowrap{white-space:nowrap}.rounded-lg{border-radius:.5rem}.rounded{border-radius:.25rem}.border-gray-200{--tw-border-opacity:1;border-color:rgb(229 231 235/var(--tw-border-opacity))}.bg-gray-50{--tw-bg-opacity:1;background-color:rgb(249 250 251/var(--tw-bg-opacity))}.bg-gray-100{--tw-bg-opacity:1;background-color:rgb(243 244 246/var(--tw-bg-opacity))}.p-3{padding:.75rem}.p-2{padding:.5rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.pl-3{padding-left:.75rem}.pr-4{padding-right:1rem}.text-center{text-align:center}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-2xl{font-size:1.5rem;line-height:2rem}.font-semibold{font-weight:600}.font-bold{font-weight:700}.text-gray-500{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity))}.text-blue-500{--tw-text-opacity:1;color:rgb(59 130 246/var(--tw-text-opacity))}.text-gray-900{--tw-text-opacity:1;color:rgb(17 24 39/var(--tw-text-opacity))}.text-gray-700{--tw-text-opacity:1;color:rgb(55 65 81/var(--tw-text-opacity))}.hover\:bg-gray-100:hover{--tw-bg-opacity:1;background-color:rgb(243 244 246/var(--tw-bg-opacity))}.hover\:text-blue-400:hover{--tw-text-opacity:1;color:rgb(96 165 250/var(--tw-text-opacity))}.focus\:outline-none:focus{outline:2px solid #0000;outline-offset:2px}.focus\:ring-2:focus{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.focus\:ring-gray-200:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(229 231 235/var(--tw-ring-opacity))}@media (prefers-color-scheme:dark){.dark\:border-gray-700{--tw-border-opacity:1;border-color:rgb(55 65 81/var(--tw-border-opacity))}.dark\:bg-zinc-800{--tw-bg-opacity:1;background-color:rgb(39 39 42/var(--tw-bg-opacity))}.dark\:bg-zinc-900{--tw-bg-opacity:1;background-color:rgb(24 24 27/var(--tw-bg-opacity))}.dark\:prose-invert{--tw-prose-body:var(--tw-prose-invert-body);--tw-prose-headings:var(--tw-prose-invert-headings);--tw-prose-lead:var(--tw-prose-invert-lead);--tw-prose-links:var(--tw-prose-invert-links);--tw-prose-bold:var(--tw-prose-invert-bold);--tw-prose-counters:var(--tw-prose-invert-counters);--tw-prose-bullets:var(--tw-prose-invert-bullets);--tw-prose-hr:var(--tw-prose-invert-hr);--tw-prose-quotes:var(--tw-prose-invert-quotes);--tw-prose-quote-borders:var(--tw-prose-invert-quote-borders);--tw-prose-captions:var(--tw-prose-invert-captions);--tw-prose-code:var(--tw-prose-invert-code);--tw-prose-pre-code:var(--tw-prose-invert-pre-code);--tw-prose-pre-bg:var(--tw-prose-invert-pre-bg);--tw-prose-th-borders:var(--tw-prose-invert-th-borders);--tw-prose-td-borders:var(--tw-prose-invert-td-borders)}.dark\:text-gray-400{--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity))}.dark\:text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.dark\:text-blue-400{--tw-text-opacity:1;color:rgb(96 165 250/var(--tw-text-opacity))}.dark\:text-gray-200{--tw-text-opacity:1;color:rgb(229 231 235/var(--tw-text-opacity))}.dark\:hover\:bg-gray-700:hover{--tw-bg-opacity:1;background-color:rgb(55 65 81/var(--tw-bg-opacity))}.dark\:hover\:text-blue-500:hover{--tw-text-opacity:1;color:rgb(59 130 246/var(--tw-text-opacity))}.dark\:hover\:text-white:hover{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.dark\:focus\:ring-gray-600:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(75 85 99/var(--tw-ring-opacity))}}@media (min-width:768px){.md\:mt-0{margin-top:0}.md\:block{display:block}.md\:hidden{display:none}.md\:w-auto{width:auto}.md\:flex-row{flex-direction:row}.md\:space-x-8>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(2rem*var(--tw-space-x-reverse));margin-left:calc(2rem*(1 - var(--tw-space-x-reverse)))}.md\:border-0{border-width:0}.md\:bg-transparent{background-color:initial}.md\:p-0{padding:0}.md\:text-sm{font-size:.875rem;line-height:1.25rem}.md\:font-medium{font-weight:500}.md\:hover\:bg-transparent:hover{background-color:initial}.md\:hover\:text-blue-700:hover{--tw-text-opacity:1;color:rgb(29 78 216/var(--tw-text-opacity))}@media (prefers-color-scheme:dark){.md\:dark\:bg-transparent,.md\:dark\:hover\:bg-transparent:hover{background-color:initial}.md\:dark\:hover\:text-white:hover{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}}} \ No newline at end of file +/*! tailwindcss v3.2.6 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:initial}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;font-weight:inherit;line-height:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button;background-color:initial;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:initial}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]{display:none}[multiple],[type=date],[type=datetime-local],[type=email],[type=month],[type=number],[type=password],[type=search],[type=tel],[type=text],[type=time],[type=url],[type=week],select,textarea{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:#fff;border-color:#6b7280;border-width:1px;border-radius:0;padding:.5rem .75rem;font-size:1rem;line-height:1.5rem;--tw-shadow:0 0 #0000}[multiple]:focus,[type=date]:focus,[type=datetime-local]:focus,[type=email]:focus,[type=month]:focus,[type=number]:focus,[type=password]:focus,[type=search]:focus,[type=tel]:focus,[type=text]:focus,[type=time]:focus,[type=url]:focus,[type=week]:focus,select:focus,textarea:focus{outline:2px solid #0000;outline-offset:2px;--tw-ring-inset:var(--tw-empty,/*!*/ /*!*/);--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#2563eb;--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);border-color:#2563eb}input::-moz-placeholder,textarea::-moz-placeholder{color:#6b7280;opacity:1}input::placeholder,textarea::placeholder{color:#6b7280;opacity:1}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-date-and-time-value{min-height:1.5em}::-webkit-datetime-edit,::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-meridiem-field,::-webkit-datetime-edit-millisecond-field,::-webkit-datetime-edit-minute-field,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-second-field,::-webkit-datetime-edit-year-field{padding-top:0;padding-bottom:0}select{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3E%3Cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='m6 8 4 4 4-4'/%3E%3C/svg%3E");background-position:right .5rem center;background-repeat:no-repeat;background-size:1.5em 1.5em;padding-right:2.5rem;-webkit-print-color-adjust:exact;print-color-adjust:exact}[multiple]{background-image:none;background-position:0 0;background-repeat:unset;background-size:initial;padding-right:.75rem;-webkit-print-color-adjust:unset;print-color-adjust:unset}[type=checkbox],[type=radio]{-webkit-appearance:none;-moz-appearance:none;appearance:none;padding:0;-webkit-print-color-adjust:exact;print-color-adjust:exact;display:inline-block;vertical-align:middle;background-origin:border-box;-webkit-user-select:none;-moz-user-select:none;user-select:none;flex-shrink:0;height:1rem;width:1rem;color:#2563eb;background-color:#fff;border-color:#6b7280;border-width:1px;--tw-shadow:0 0 #0000}[type=checkbox]{border-radius:0}[type=radio]{border-radius:100%}[type=checkbox]:focus,[type=radio]:focus{outline:2px solid #0000;outline-offset:2px;--tw-ring-inset:var(--tw-empty,/*!*/ /*!*/);--tw-ring-offset-width:2px;--tw-ring-offset-color:#fff;--tw-ring-color:#2563eb;--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}[type=checkbox]:checked,[type=radio]:checked{border-color:#0000;background-color:currentColor;background-size:100% 100%;background-position:50%;background-repeat:no-repeat}[type=checkbox]:checked{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 16 16' fill='%23fff' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M12.207 4.793a1 1 0 0 1 0 1.414l-5 5a1 1 0 0 1-1.414 0l-2-2a1 1 0 0 1 1.414-1.414L6.5 9.086l4.293-4.293a1 1 0 0 1 1.414 0z'/%3E%3C/svg%3E")}[type=radio]:checked{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 16 16' fill='%23fff' xmlns='http://www.w3.org/2000/svg'%3E%3Ccircle cx='8' cy='8' r='3'/%3E%3C/svg%3E")}[type=checkbox]:checked:focus,[type=checkbox]:checked:hover,[type=checkbox]:indeterminate,[type=radio]:checked:focus,[type=radio]:checked:hover{border-color:#0000;background-color:currentColor}[type=checkbox]:indeterminate{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 16 16'%3E%3Cpath stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M4 8h8'/%3E%3C/svg%3E");background-size:100% 100%;background-position:50%;background-repeat:no-repeat}[type=checkbox]:indeterminate:focus,[type=checkbox]:indeterminate:hover{border-color:#0000;background-color:currentColor}[type=file]{background:unset;border-color:inherit;border-width:0;border-radius:0;padding:0;font-size:unset;line-height:inherit}[type=file]:focus{outline:1px solid ButtonText;outline:1px auto -webkit-focus-ring-color}*,::backdrop,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#3b82f680;--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.container{width:100%}@media (min-width:640px){.container{max-width:640px}}@media (min-width:768px){.container{max-width:768px}}@media (min-width:1024px){.container{max-width:1024px}}@media (min-width:1280px){.container{max-width:1280px}}@media (min-width:1536px){.container{max-width:1536px}}.prose{color:var(--tw-prose-body);max-width:65ch}.prose :where(p):not(:where([class~=not-prose] *)){margin-top:1.25em;margin-bottom:1.25em}.prose :where([class~=lead]):not(:where([class~=not-prose] *)){color:var(--tw-prose-lead);font-size:1.25em;line-height:1.6;margin-top:1.2em;margin-bottom:1.2em}.prose :where(a):not(:where([class~=not-prose] *)){color:var(--tw-prose-links);text-decoration:underline;font-weight:500}.prose :where(strong):not(:where([class~=not-prose] *)){color:var(--tw-prose-bold);font-weight:600}.prose :where(a strong):not(:where([class~=not-prose] *)){color:inherit}.prose :where(blockquote strong):not(:where([class~=not-prose] *)){color:inherit}.prose :where(thead th strong):not(:where([class~=not-prose] *)){color:inherit}.prose :where(ol):not(:where([class~=not-prose] *)){list-style-type:decimal;margin-top:1.25em;margin-bottom:1.25em;padding-left:1.625em}.prose :where(ol[type=A]):not(:where([class~=not-prose] *)){list-style-type:upper-alpha}.prose :where(ol[type=a]):not(:where([class~=not-prose] *)){list-style-type:lower-alpha}.prose :where(ol[type=A s]):not(:where([class~=not-prose] *)){list-style-type:upper-alpha}.prose :where(ol[type=a s]):not(:where([class~=not-prose] *)){list-style-type:lower-alpha}.prose :where(ol[type=I]):not(:where([class~=not-prose] *)){list-style-type:upper-roman}.prose :where(ol[type=i]):not(:where([class~=not-prose] *)){list-style-type:lower-roman}.prose :where(ol[type=I s]):not(:where([class~=not-prose] *)){list-style-type:upper-roman}.prose :where(ol[type=i s]):not(:where([class~=not-prose] *)){list-style-type:lower-roman}.prose :where(ol[type="1"]):not(:where([class~=not-prose] *)){list-style-type:decimal}.prose :where(ul):not(:where([class~=not-prose] *)){list-style-type:disc;margin-top:1.25em;margin-bottom:1.25em;padding-left:1.625em}.prose :where(ol>li):not(:where([class~=not-prose] *))::marker{font-weight:400;color:var(--tw-prose-counters)}.prose :where(ul>li):not(:where([class~=not-prose] *))::marker{color:var(--tw-prose-bullets)}.prose :where(hr):not(:where([class~=not-prose] *)){border-color:var(--tw-prose-hr);border-top-width:1px;margin-top:3em;margin-bottom:3em}.prose :where(blockquote):not(:where([class~=not-prose] *)){font-weight:500;font-style:italic;color:var(--tw-prose-quotes);border-left-width:.25rem;border-left-color:var(--tw-prose-quote-borders);quotes:"\201C""\201D""\2018""\2019";margin-top:1.6em;margin-bottom:1.6em;padding-left:1em}.prose :where(blockquote p:first-of-type):not(:where([class~=not-prose] *)):before{content:open-quote}.prose :where(blockquote p:last-of-type):not(:where([class~=not-prose] *)):after{content:close-quote}.prose :where(h1):not(:where([class~=not-prose] *)){color:var(--tw-prose-headings);font-weight:800;font-size:2.25em;margin-top:0;margin-bottom:.8888889em;line-height:1.1111111}.prose :where(h1 strong):not(:where([class~=not-prose] *)){font-weight:900;color:inherit}.prose :where(h2):not(:where([class~=not-prose] *)){color:var(--tw-prose-headings);font-weight:700;font-size:1.5em;margin-top:2em;margin-bottom:1em;line-height:1.3333333}.prose :where(h2 strong):not(:where([class~=not-prose] *)){font-weight:800;color:inherit}.prose :where(h3):not(:where([class~=not-prose] *)){color:var(--tw-prose-headings);font-weight:600;font-size:1.25em;margin-top:1.6em;margin-bottom:.6em;line-height:1.6}.prose :where(h3 strong):not(:where([class~=not-prose] *)){font-weight:700;color:inherit}.prose :where(h4):not(:where([class~=not-prose] *)){color:var(--tw-prose-headings);font-weight:600;margin-top:1.5em;margin-bottom:.5em;line-height:1.5}.prose :where(h4 strong):not(:where([class~=not-prose] *)){font-weight:700;color:inherit}.prose :where(img):not(:where([class~=not-prose] *)){margin-top:2em;margin-bottom:2em}.prose :where(figure>*):not(:where([class~=not-prose] *)){margin-top:0;margin-bottom:0}.prose :where(figcaption):not(:where([class~=not-prose] *)){color:var(--tw-prose-captions);font-size:.875em;line-height:1.4285714;margin-top:.8571429em}.prose :where(code):not(:where([class~=not-prose] *)){color:#f4f4f5;font-weight:600;font-size:.875em;background-color:#3f3f46;padding:.25rem .375rem;border-radius:.25rem}.prose :where(code):not(:where([class~=not-prose] *)):before{content:none}.prose :where(code):not(:where([class~=not-prose] *)):after{content:none}.prose :where(a code):not(:where([class~=not-prose] *)){color:inherit}.prose :where(h1 code):not(:where([class~=not-prose] *)){color:inherit}.prose :where(h2 code):not(:where([class~=not-prose] *)){color:inherit;font-size:.875em}.prose :where(h3 code):not(:where([class~=not-prose] *)){color:inherit;font-size:.9em}.prose :where(h4 code):not(:where([class~=not-prose] *)){color:inherit}.prose :where(blockquote code):not(:where([class~=not-prose] *)){color:inherit}.prose :where(thead th code):not(:where([class~=not-prose] *)){color:inherit}.prose :where(pre):not(:where([class~=not-prose] *)){color:var(--tw-prose-pre-code);background-color:var(--tw-prose-pre-bg);overflow-x:auto;font-weight:400;font-size:.875em;line-height:1.7142857;margin-top:1.7142857em;margin-bottom:1.7142857em;border-radius:.375rem;padding:.8571429em 1.1428571em}.prose :where(pre code):not(:where([class~=not-prose] *)){background-color:initial;border-width:0;border-radius:0;padding:0;font-weight:inherit;color:inherit;font-size:inherit;font-family:inherit;line-height:inherit}.prose :where(pre code):not(:where([class~=not-prose] *)):before{content:none}.prose :where(pre code):not(:where([class~=not-prose] *)):after{content:none}.prose :where(table):not(:where([class~=not-prose] *)){width:100%;table-layout:auto;text-align:left;margin-top:2em;margin-bottom:2em;font-size:.875em;line-height:1.7142857}.prose :where(thead):not(:where([class~=not-prose] *)){border-bottom-width:1px;border-bottom-color:var(--tw-prose-th-borders)}.prose :where(thead th):not(:where([class~=not-prose] *)){color:var(--tw-prose-headings);font-weight:600;vertical-align:bottom;padding-right:.5714286em;padding-bottom:.5714286em;padding-left:.5714286em}.prose :where(tbody tr):not(:where([class~=not-prose] *)){border-bottom-width:1px;border-bottom-color:var(--tw-prose-td-borders)}.prose :where(tbody tr:last-child):not(:where([class~=not-prose] *)){border-bottom-width:0}.prose :where(tbody td):not(:where([class~=not-prose] *)){vertical-align:initial}.prose :where(tfoot):not(:where([class~=not-prose] *)){border-top-width:1px;border-top-color:var(--tw-prose-th-borders)}.prose :where(tfoot td):not(:where([class~=not-prose] *)){vertical-align:top}.prose{--tw-prose-body:#374151;--tw-prose-headings:#111827;--tw-prose-lead:#4b5563;--tw-prose-links:#111827;--tw-prose-bold:#111827;--tw-prose-counters:#6b7280;--tw-prose-bullets:#d1d5db;--tw-prose-hr:#e5e7eb;--tw-prose-quotes:#111827;--tw-prose-quote-borders:#e5e7eb;--tw-prose-captions:#6b7280;--tw-prose-code:#111827;--tw-prose-pre-code:#e5e7eb;--tw-prose-pre-bg:#1f2937;--tw-prose-th-borders:#d1d5db;--tw-prose-td-borders:#e5e7eb;--tw-prose-invert-body:#d1d5db;--tw-prose-invert-headings:#fff;--tw-prose-invert-lead:#9ca3af;--tw-prose-invert-links:#fff;--tw-prose-invert-bold:#fff;--tw-prose-invert-counters:#9ca3af;--tw-prose-invert-bullets:#4b5563;--tw-prose-invert-hr:#374151;--tw-prose-invert-quotes:#f3f4f6;--tw-prose-invert-quote-borders:#374151;--tw-prose-invert-captions:#9ca3af;--tw-prose-invert-code:#fff;--tw-prose-invert-pre-code:#d1d5db;--tw-prose-invert-pre-bg:#00000080;--tw-prose-invert-th-borders:#4b5563;--tw-prose-invert-td-borders:#374151;font-size:1rem;line-height:1.75}.prose :where(video):not(:where([class~=not-prose] *)){margin-top:2em;margin-bottom:2em}.prose :where(figure):not(:where([class~=not-prose] *)){margin-top:2em;margin-bottom:2em}.prose :where(li):not(:where([class~=not-prose] *)){margin-top:.5em;margin-bottom:.5em}.prose :where(ol>li):not(:where([class~=not-prose] *)){padding-left:.375em}.prose :where(ul>li):not(:where([class~=not-prose] *)){padding-left:.375em}.prose :where(.prose>ul>li p):not(:where([class~=not-prose] *)){margin-top:.75em;margin-bottom:.75em}.prose :where(.prose>ul>li>:first-child):not(:where([class~=not-prose] *)){margin-top:1.25em}.prose :where(.prose>ul>li>:last-child):not(:where([class~=not-prose] *)){margin-bottom:1.25em}.prose :where(.prose>ol>li>:first-child):not(:where([class~=not-prose] *)){margin-top:1.25em}.prose :where(.prose>ol>li>:last-child):not(:where([class~=not-prose] *)){margin-bottom:1.25em}.prose :where(ul ul,ul ol,ol ul,ol ol):not(:where([class~=not-prose] *)){margin-top:.75em;margin-bottom:.75em}.prose :where(hr+*):not(:where([class~=not-prose] *)){margin-top:0}.prose :where(h2+*):not(:where([class~=not-prose] *)){margin-top:0}.prose :where(h3+*):not(:where([class~=not-prose] *)){margin-top:0}.prose :where(h4+*):not(:where([class~=not-prose] *)){margin-top:0}.prose :where(thead th:first-child):not(:where([class~=not-prose] *)){padding-left:0}.prose :where(thead th:last-child):not(:where([class~=not-prose] *)){padding-right:0}.prose :where(tbody td,tfoot td):not(:where([class~=not-prose] *)){padding:.5714286em}.prose :where(tbody td:first-child,tfoot td:first-child):not(:where([class~=not-prose] *)){padding-left:0}.prose :where(tbody td:last-child,tfoot td:last-child):not(:where([class~=not-prose] *)){padding-right:0}.prose :where(.prose>:first-child):not(:where([class~=not-prose] *)){margin-top:0}.prose :where(.prose>:last-child):not(:where([class~=not-prose] *)){margin-bottom:0}.form-input,.form-multiselect,.form-select,.form-textarea{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:#fff;border-color:#6b7280;border-width:1px;border-radius:0;padding:.5rem .75rem;font-size:1rem;line-height:1.5rem;--tw-shadow:0 0 #0000}.form-input:focus,.form-multiselect:focus,.form-select:focus,.form-textarea:focus{outline:2px solid #0000;outline-offset:2px;--tw-ring-inset:var(--tw-empty,/*!*/ /*!*/);--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#2563eb;--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);border-color:#2563eb}.form-input::-moz-placeholder,.form-textarea::-moz-placeholder{color:#6b7280;opacity:1}.form-input::placeholder,.form-textarea::placeholder{color:#6b7280;opacity:1}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}.-mx-4{margin-left:-1rem;margin-right:-1rem}.mx-auto{margin-left:auto;margin-right:auto}.my-4{margin-top:1rem;margin-bottom:1rem}.mb-2{margin-bottom:.5rem}.mb-4{margin-bottom:1rem}.ml-3{margin-left:.75rem}.mt-4{margin-top:1rem}.block{display:block}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.hidden{display:none}.h-6{height:1.5rem}.w-1\/12{width:8.333333%}.w-1\/2{width:50%}.w-10\/12{width:83.333333%}.w-6{width:1.5rem}.w-full{width:100%}.max-w-none{max-width:none}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.justify-between{justify-content:space-between}.self-center{align-self:center}.whitespace-nowrap{white-space:nowrap}.rounded{border-radius:.25rem}.rounded-lg{border-radius:.5rem}.border-2{border-width:2px}.border-gray-200{--tw-border-opacity:1;border-color:rgb(229 231 235/var(--tw-border-opacity))}.border-gray-400{--tw-border-opacity:1;border-color:rgb(156 163 175/var(--tw-border-opacity))}.bg-blue-500{--tw-bg-opacity:1;background-color:rgb(59 130 246/var(--tw-bg-opacity))}.bg-gray-100{--tw-bg-opacity:1;background-color:rgb(243 244 246/var(--tw-bg-opacity))}.bg-gray-50{--tw-bg-opacity:1;background-color:rgb(249 250 251/var(--tw-bg-opacity))}.p-2{padding:.5rem}.p-3{padding:.75rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.pl-3{padding-left:.75rem}.pr-4{padding-right:1rem}.text-center{text-align:center}.text-2xl{font-size:1.5rem;line-height:2rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-semibold{font-weight:600}.text-blue-500{--tw-text-opacity:1;color:rgb(59 130 246/var(--tw-text-opacity))}.text-gray-500{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity))}.text-gray-700{--tw-text-opacity:1;color:rgb(55 65 81/var(--tw-text-opacity))}.text-gray-900{--tw-text-opacity:1;color:rgb(17 24 39/var(--tw-text-opacity))}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}@media (prefers-color-scheme:dark){.dark\:prose-invert{--tw-prose-body:var(--tw-prose-invert-body);--tw-prose-headings:var(--tw-prose-invert-headings);--tw-prose-lead:var(--tw-prose-invert-lead);--tw-prose-links:var(--tw-prose-invert-links);--tw-prose-bold:var(--tw-prose-invert-bold);--tw-prose-counters:var(--tw-prose-invert-counters);--tw-prose-bullets:var(--tw-prose-invert-bullets);--tw-prose-hr:var(--tw-prose-invert-hr);--tw-prose-quotes:var(--tw-prose-invert-quotes);--tw-prose-quote-borders:var(--tw-prose-invert-quote-borders);--tw-prose-captions:var(--tw-prose-invert-captions);--tw-prose-code:var(--tw-prose-invert-code);--tw-prose-pre-code:var(--tw-prose-invert-pre-code);--tw-prose-pre-bg:var(--tw-prose-invert-pre-bg);--tw-prose-th-borders:var(--tw-prose-invert-th-borders);--tw-prose-td-borders:var(--tw-prose-invert-td-borders)}}.hover\:bg-blue-400:hover{--tw-bg-opacity:1;background-color:rgb(96 165 250/var(--tw-bg-opacity))}.hover\:bg-gray-100:hover{--tw-bg-opacity:1;background-color:rgb(243 244 246/var(--tw-bg-opacity))}.hover\:text-blue-400:hover{--tw-text-opacity:1;color:rgb(96 165 250/var(--tw-text-opacity))}.focus\:outline-none:focus{outline:2px solid #0000;outline-offset:2px}.focus\:ring-2:focus{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.focus\:ring-gray-200:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(229 231 235/var(--tw-ring-opacity))}@media (prefers-color-scheme:dark){.dark\:border-gray-700{--tw-border-opacity:1;border-color:rgb(55 65 81/var(--tw-border-opacity))}.dark\:bg-zinc-800{--tw-bg-opacity:1;background-color:rgb(39 39 42/var(--tw-bg-opacity))}.dark\:bg-zinc-900{--tw-bg-opacity:1;background-color:rgb(24 24 27/var(--tw-bg-opacity))}.dark\:text-blue-400{--tw-text-opacity:1;color:rgb(96 165 250/var(--tw-text-opacity))}.dark\:text-gray-200{--tw-text-opacity:1;color:rgb(229 231 235/var(--tw-text-opacity))}.dark\:text-gray-400{--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity))}.dark\:text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.dark\:hover\:bg-gray-700:hover{--tw-bg-opacity:1;background-color:rgb(55 65 81/var(--tw-bg-opacity))}.dark\:hover\:text-blue-500:hover{--tw-text-opacity:1;color:rgb(59 130 246/var(--tw-text-opacity))}.dark\:hover\:text-white:hover{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.dark\:focus\:ring-gray-600:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(75 85 99/var(--tw-ring-opacity))}}@media (min-width:768px){.md\:mt-0{margin-top:0}.md\:block{display:block}.md\:hidden{display:none}.md\:w-auto{width:auto}.md\:flex-row{flex-direction:row}.md\:space-x-8>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(2rem*var(--tw-space-x-reverse));margin-left:calc(2rem*(1 - var(--tw-space-x-reverse)))}.md\:border-0{border-width:0}.md\:bg-transparent{background-color:initial}.md\:p-0{padding:0}.md\:text-sm{font-size:.875rem;line-height:1.25rem}.md\:font-medium{font-weight:500}.md\:hover\:bg-transparent:hover{background-color:initial}.md\:hover\:text-blue-700:hover{--tw-text-opacity:1;color:rgb(29 78 216/var(--tw-text-opacity))}@media (prefers-color-scheme:dark){.md\:dark\:bg-transparent,.md\:dark\:hover\:bg-transparent:hover{background-color:initial}.md\:dark\:hover\:text-white:hover{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}}} \ No newline at end of file diff --git a/src/Themes/Default/Templates/admin/admin_config.html.twig b/src/Themes/Default/Templates/admin/admin_config.html.twig new file mode 100644 index 0000000..d7eaddf --- /dev/null +++ b/src/Themes/Default/Templates/admin/admin_config.html.twig @@ -0,0 +1,17 @@ +

AntCMS Configuration

+Back | +Edit Config + diff --git a/src/Themes/Default/Templates/admin/admin_landing.html.twig b/src/Themes/Default/Templates/admin/admin_landing.html.twig new file mode 100644 index 0000000..7df1d8e --- /dev/null +++ b/src/Themes/Default/Templates/admin/admin_landing.html.twig @@ -0,0 +1,9 @@ +

AntCMS Admin Plugin

+

Welcome, {{ user.name }}. Below are all of the available options for you.

+{% if user.role == 'admin' %} +AntCMS Configuration | +AntCMS User Management | +{% endif %} +Page Management | +Profile | +Logout diff --git a/src/Themes/Default/Templates/admin_manage_pages_layout.html.twig b/src/Themes/Default/Templates/admin/admin_managePages.html.twig similarity index 75% rename from src/Themes/Default/Templates/admin_manage_pages_layout.html.twig rename to src/Themes/Default/Templates/admin/admin_managePages.html.twig index 9955989..0011e1f 100644 --- a/src/Themes/Default/Templates/admin_manage_pages_layout.html.twig +++ b/src/Themes/Default/Templates/admin/admin_managePages.html.twig @@ -1,7 +1,7 @@

Page Management

-Back | -Regenerate Page List | -Create New Page +Back | +Regenerate Page List | +Create New Page diff --git a/src/Themes/Default/Templates/admin/admin_newPage.html.twig b/src/Themes/Default/Templates/admin/admin_newPage.html.twig new file mode 100644 index 0000000..a6c7097 --- /dev/null +++ b/src/Themes/Default/Templates/admin/admin_newPage.html.twig @@ -0,0 +1,9 @@ +

Page Management

+

Create new page

+ +
+ + + +
+ diff --git a/src/Themes/Default/Templates/admin/admin_userAdd.html.twig b/src/Themes/Default/Templates/admin/admin_userAdd.html.twig new file mode 100644 index 0000000..4da69a8 --- /dev/null +++ b/src/Themes/Default/Templates/admin/admin_userAdd.html.twig @@ -0,0 +1,28 @@ + +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+ diff --git a/src/Themes/Default/Templates/admin/admin_userEdit.html.twig b/src/Themes/Default/Templates/admin/admin_userEdit.html.twig new file mode 100644 index 0000000..3bc14d0 --- /dev/null +++ b/src/Themes/Default/Templates/admin/admin_userEdit.html.twig @@ -0,0 +1,25 @@ + + + +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+ diff --git a/src/Themes/Default/Templates/admin/admin_userResetPassword.html.twig b/src/Themes/Default/Templates/admin/admin_userResetPassword.html.twig new file mode 100644 index 0000000..6d11621 --- /dev/null +++ b/src/Themes/Default/Templates/admin/admin_userResetPassword.html.twig @@ -0,0 +1,12 @@ + + + +
+ + +
+ +
+ +
+ diff --git a/src/Themes/Default/Templates/admin/admin_users.html.twig b/src/Themes/Default/Templates/admin/admin_users.html.twig new file mode 100644 index 0000000..22eb8f5 --- /dev/null +++ b/src/Themes/Default/Templates/admin/admin_users.html.twig @@ -0,0 +1,19 @@ +

AntCMS User Management

+Back | +Add User +
Page Title
+ + + + + + + {% for user in users %} + + + + + + + {% endfor %} +
Display NameUsernameRoleActions
{{ user.name }}{{ user.username }}{{ user.role }}Edit | Reset Password
diff --git a/src/Themes/Default/Templates/default_layout.html.twig b/src/Themes/Default/Templates/default.html.twig similarity index 100% rename from src/Themes/Default/Templates/default_layout.html.twig rename to src/Themes/Default/Templates/default.html.twig diff --git a/src/Themes/Default/Templates/markdown_edit_layout.html.twig b/src/Themes/Default/Templates/markdownEdit.html.twig similarity index 72% rename from src/Themes/Default/Templates/markdown_edit_layout.html.twig rename to src/Themes/Default/Templates/markdownEdit.html.twig index 0fe0c7e..4f4cd3b 100644 --- a/src/Themes/Default/Templates/markdown_edit_layout.html.twig +++ b/src/Themes/Default/Templates/markdownEdit.html.twig @@ -2,10 +2,10 @@

Page Content

-
+ - + class="form-textarea p-3 border-gray-200 bg-gray-100 dark:bg-zinc-900 dark:border-gray-700">{{ AntCMSTextAreaContent }} +
@@ -18,7 +18,7 @@ const output = document.getElementById("markdown-output"); function parseMarkdown() { - const inputValue = input.value.replace(/\A--AntCMS--.*?--AntCMS--/sm, ""); + const inputValue = input.value.replace(/^--AntCMS--[\s\S]*?--AntCMS--/gm, ""); output.innerHTML = marked.parse(inputValue); } diff --git a/src/Themes/Default/Templates/nav_layout.html.twig b/src/Themes/Default/Templates/nav.html.twig similarity index 100% rename from src/Themes/Default/Templates/nav_layout.html.twig rename to src/Themes/Default/Templates/nav.html.twig diff --git a/src/Themes/Default/Templates/profile/profile_edit.html.twig b/src/Themes/Default/Templates/profile/profile_edit.html.twig new file mode 100644 index 0000000..609011a --- /dev/null +++ b/src/Themes/Default/Templates/profile/profile_edit.html.twig @@ -0,0 +1,15 @@ +
+
+ + +
+ +
+ + +
+ +
+ +
+
diff --git a/src/Themes/Default/Templates/profile/profile_firsttime.html.twig b/src/Themes/Default/Templates/profile/profile_firsttime.html.twig new file mode 100644 index 0000000..c92185d --- /dev/null +++ b/src/Themes/Default/Templates/profile/profile_firsttime.html.twig @@ -0,0 +1,23 @@ +

AntCMS First Time User Setup

+

Please fill out the following form to create your new administrator account. This will allow you to access portions of AntCMS that require authentication, such as the Admin Panel.

+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+
diff --git a/src/Themes/Default/Templates/profile/profile_landing.html.twig b/src/Themes/Default/Templates/profile/profile_landing.html.twig new file mode 100644 index 0000000..f9071eb --- /dev/null +++ b/src/Themes/Default/Templates/profile/profile_landing.html.twig @@ -0,0 +1,5 @@ +

AntCMS Profile Plugin

+

Welcome, {{ user.name }}. Below are all of the available options for you.

+Edit Profile | +Reset Password | +Logout diff --git a/src/Themes/Default/Templates/profile/profile_resetPassword.html.twig b/src/Themes/Default/Templates/profile/profile_resetPassword.html.twig new file mode 100644 index 0000000..41c5945 --- /dev/null +++ b/src/Themes/Default/Templates/profile/profile_resetPassword.html.twig @@ -0,0 +1,10 @@ +
+
+ + +
+ +
+ +
+
diff --git a/src/Themes/Default/Templates/textareaEdit.html.twig b/src/Themes/Default/Templates/textareaEdit.html.twig new file mode 100644 index 0000000..7f36e19 --- /dev/null +++ b/src/Themes/Default/Templates/textareaEdit.html.twig @@ -0,0 +1,7 @@ +
+ +
+ +
+
diff --git a/src/Themes/Default/Templates/textarea_edit_layout.html.twig b/src/Themes/Default/Templates/textarea_edit_layout.html.twig deleted file mode 100644 index d1b137f..0000000 --- a/src/Themes/Default/Templates/textarea_edit_layout.html.twig +++ /dev/null @@ -1,5 +0,0 @@ -
- - -
\ No newline at end of file diff --git a/src/index.php b/src/index.php index 5eee177..3a34d47 100644 --- a/src/index.php +++ b/src/index.php @@ -12,8 +12,6 @@ use AntCMS\AntConfig; use AntCMS\AntPages; use AntCMS\AntPluginLoader; -$antCms = new AntCMS(); - if (!file_exists(antConfigFile)) { AntConfig::generateConfig(); } @@ -22,17 +20,19 @@ if (!file_exists(antPagesList)) { AntPages::generatePages(); } +$antCms = new AntCMS(); + if (AntConfig::currentConfig('forceHTTPS') && 'cli' !== PHP_SAPI) { $isHTTPS = false; if (!empty($_SERVER['HTTPS']) && strtolower($_SERVER['HTTPS']) !== 'off') { $isHTTPS = true; } - + if (!empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && strtolower($_SERVER['HTTP_X_FORWARDED_PROTO']) == 'https') { $isHTTPS = true; } - + if (!empty($_SERVER['HTTP_X_FORWARDED_SSL']) && strtolower($_SERVER['HTTP_X_FORWARDED_SSL']) !== 'off') { $isHTTPS = true; } @@ -44,7 +44,7 @@ if (AntConfig::currentConfig('forceHTTPS') && 'cli' !== PHP_SAPI) { } } -$requestedPage = strtolower(parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH)); +$requestedPage = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH); $segments = explode('/', $requestedPage); if ($segments[0] === '') { @@ -66,6 +66,14 @@ if ($segments[0] == 'robots.txt') { $segments[1] = 'robotstxt'; } +if ($segments[0] == 'admin') { + array_unshift($segments, 'plugin'); +} + +if ($segments[0] == 'profile') { + array_unshift($segments, 'plugin'); +} + if ($segments[0] === 'plugin') { $pluginName = $segments[1]; $pluginLoader = new AntPluginLoader(); @@ -80,7 +88,7 @@ if ($segments[0] === 'plugin') { exit; } } - + // plugin not found header("HTTP/1.0 404 Not Found"); echo ("Error 404"); @@ -88,7 +96,13 @@ if ($segments[0] === 'plugin') { } $indexes = ['/', '/index.php', '/index.html']; -if (in_array($segments[0], $indexes)) { +if (in_array($segments[0], $indexes) or empty($segments[0])) { + + // If the users list hasn't been created, redirect to the first-time setup + if (!file_exists(antUsersList)) { + AntCMS::redirect('/profile/firsttime'); + } + echo $antCms->renderPage('/'); exit; } else { diff --git a/tests/CMSTest.php b/tests/CMSTest.php index 49c6d29..e01c8dd 100644 --- a/tests/CMSTest.php +++ b/tests/CMSTest.php @@ -31,7 +31,7 @@ class CMSTest extends TestCase public function testGetPageLayout() { - //We need to generate the pages.yaml file so that the nav list can be generated. + //We need to generate the Pages.yaml file so that the nav list can be generated. AntPages::generatePages(); $antCMS = new AntCMS; diff --git a/tests/ConfigTest.php b/tests/ConfigTest.php index a44fee2..57732c3 100644 --- a/tests/ConfigTest.php +++ b/tests/ConfigTest.php @@ -16,7 +16,6 @@ class ConfigTest extends TestCase 'forceHTTPS', 'activeTheme', 'enableCache', - 'admin', 'debug', 'baseURL' ); diff --git a/tests/Includes/config.yaml b/tests/Includes/Config.yaml similarity index 54% rename from tests/Includes/config.yaml rename to tests/Includes/Config.yaml index c8d3de2..957ce65 100644 --- a/tests/Includes/config.yaml +++ b/tests/Includes/Config.yaml @@ -3,8 +3,5 @@ siteInfo: forceHTTPS: true activeTheme: Default enableCache: true -admin: - username: Admin - password: $2y$10$omNAQ5luPsFYOny0HA3i.O7Ce3XPult8Qn6uamT0b1cFWz.5WkGYO debug: true baseURL: antcms.org/ diff --git a/tests/PagesTest.php b/tests/PagesTest.php index f16181d..7e5a133 100644 --- a/tests/PagesTest.php +++ b/tests/PagesTest.php @@ -20,7 +20,7 @@ class PagesTest extends TestCase public function testGetNavigation(){ $antCMS = new AntCMS; $pageTemplate = $antCMS->getThemeTemplate(); - $navLayout = $antCMS->getThemeTemplate('nav_layout'); + $navLayout = $antCMS->getThemeTemplate('nav'); $result = AntPages::generateNavigation($navLayout, $pageTemplate);