Localization & stuff

This commit is contained in:
2022-11-30 16:42:24 +01:00
parent 25ef07b0b7
commit 1ba27e4f40
54 changed files with 856 additions and 494 deletions

View File

@@ -2,7 +2,6 @@
namespace Core\API {
use Core\Driver\SQL\Condition\Compare;
use Core\Objects\Context;
abstract class ApiKeyAPI extends Request {

View File

@@ -2,7 +2,6 @@
namespace Core\API {
use Core\Driver\SQL\Condition\Compare;
use Core\Objects\Context;
abstract class GroupsAPI extends Request {

View File

@@ -14,6 +14,7 @@ namespace Core\API {
namespace Core\API\Language {
use Core\API\LanguageAPI;
use Core\API\Parameter\ArrayType;
use Core\API\Parameter\Parameter;
use Core\API\Parameter\StringType;
use Core\Driver\SQL\Condition\Compare;
@@ -105,7 +106,58 @@ namespace Core\API\Language {
}
$this->context->setLanguage($this->language);
$this->result["language"] = $this->language->jsonSerialize();
return $this->success;
}
}
class GetEntries extends LanguageAPI {
public function __construct(Context $context, bool $externalCall = false) {
parent::__construct($context, $externalCall, [
"code" => new StringType("code", 5, true, NULL),
"modules" => new ArrayType("modules", Parameter::TYPE_STRING, true, false)
]);
$this->loginRequired = false;
$this->csrfTokenRequired = false;
}
protected function _execute(): bool {
$code = $this->getParam("code");
if ($code === null) {
$code = $this->context->getLanguage()->getCode();
}
if (!preg_match(Language::LANG_CODE_PATTERN, $code)) {
return $this->createError("Invalid lang code format: $code");
}
$entries = [];
$modulePaths = [];
$requestedModules = $this->getParam("modules");
foreach ($requestedModules as $module) {
if (!preg_match(Language::LANG_MODULE_PATTERN, $module)) {
return $this->createError("Invalid module name: $module");
}
$moduleFound = false;
foreach (["Site", "Core"] as $baseDir) {
$filePath = realpath(implode("/", [$baseDir, "Localization", $code, "$module.php"]));
if ($filePath && is_file($filePath)) {
$moduleFound = true;
$moduleEntries = @include_once $filePath;
$entries[$module] = $moduleEntries;
break;
}
}
if (!$moduleFound) {
return $this->createError("Module not found: $module");
}
}
$this->result["code"] = $code;
$this->result["entries"] = $entries;
return true;
}
}
}

View File

@@ -48,9 +48,7 @@ namespace Core\API\Mail {
use Core\API\Parameter\StringType;
use Core\Objects\DatabaseEntity\MailQueueItem;
use DateTimeInterface;
use Core\Driver\SQL\Column\Column;
use Core\Driver\SQL\Condition\Compare;
use Core\Driver\SQL\Condition\CondIn;
use Core\External\PHPMailer\Exception;
use Core\External\PHPMailer\PHPMailer;
use Core\Objects\Context;

View File

@@ -28,7 +28,6 @@ namespace Core\API\Permission {
use Core\API\Parameter\StringType;
use Core\API\PermissionAPI;
use Core\Driver\SQL\Column\Column;
use Core\Driver\SQL\Condition\Compare;
use Core\Driver\SQL\Condition\CondIn;
use Core\Driver\SQL\Condition\CondLike;
use Core\Driver\SQL\Condition\CondNot;

View File

@@ -6,7 +6,6 @@ use DateTime;
use Core\Driver\SQL\Condition\Compare;
use Core\Driver\SQL\Condition\CondBool;
use Core\Objects\Context;
use Core\Objects\DatabaseEntity\User;
class Stats extends Request {

View File

@@ -297,8 +297,8 @@ namespace Core\API\User {
$this->result["users"][$userId] = $serialized;
}
$this->result["pageCount"] = intval(ceil($this->userCount / $count));
$this->result["totalCount"] = $this->userCount;
$this->result["pageCount"] = intval(ceil($userCount / $count));
$this->result["totalCount"] = $userCount;
} else {
return $this->createError("Error fetching users: " . $sql->getLastError());
}
@@ -1580,4 +1580,33 @@ namespace Core\API\User {
return $this->success;
}
}
class CheckToken extends UserAPI {
private ?UserToken $userToken;
public function __construct($user, $externalCall = false) {
parent::__construct($user, $externalCall, array(
'token' => new StringType('token', 36),
));
$this->userToken = null;
}
public function getToken(): ?UserToken {
return $this->userToken;
}
public function _execute(): bool {
$token = $this->getParam('token');
$userToken = $this->checkToken($token);
if ($userToken === false) {
return false;
}
$this->userToken = $userToken;
$this->result["token"] = $userToken->jsonSerialize();
return $this->success;
}
}
}

View File

@@ -9,7 +9,6 @@ use Core\Objects\DatabaseEntity\Language;
use Core\Objects\DatabaseEntity\Route;
use Core\Objects\Router\DocumentRoute;
use Core\Objects\Router\StaticFileRoute;
use Core\Objects\Router\StaticRoute;
use PHPUnit\Util\Exception;
class CreateDatabase extends DatabaseScript {
@@ -21,7 +20,7 @@ class CreateDatabase extends DatabaseScript {
$queries[] = Language::getHandler($sql)->getInsertQuery([
new Language(Language::AMERICAN_ENGLISH, "en_US", 'American English'),
new Language(Language::AMERICAN_ENGLISH, "de_DE", 'Deutsch Standard'),
new Language(Language::GERMAN_STANDARD, "de_DE", 'Deutsch Standard'),
]);
$queries[] = Group::getHandler($sql)->getInsertQuery([

View File

@@ -4,12 +4,14 @@
namespace Core\Documents;
use Core\Elements\TemplateDocument;
use Core\Objects\DatabaseEntity\UserToken;
use Core\Objects\Router\Router;
class Account extends TemplateDocument {
public function __construct(Router $router, string $templateName) {
parent::__construct($router, $templateName);
$this->languageModules = ["general", "account"];
$this->title = "Account";
$this->searchable = false;
$this->enableCSP();
@@ -21,47 +23,67 @@ class Account extends TemplateDocument {
}
protected function loadParameters() {
$settings = $this->getSettings();
$templateName = $this->getTemplateName();
$language = $this->getContext()->getLanguage();
$this->parameters["view"] = ["success" => true];
if ($this->getTemplateName() === "account/reset_password.twig") {
if (isset($_GET["token"]) && is_string($_GET["token"]) && !empty($_GET["token"])) {
$this->parameters["view"]["token"] = $_GET["token"];
$req = new \Core\API\User\CheckToken($this->getContext());
$this->parameters["view"]["success"] = $req->execute(array("token" => $_GET["token"]));
if ($this->parameters["view"]["success"]) {
if (strcmp($req->getResult()["token"]["type"], "password_reset") !== 0) {
$this->createError("The given token has a wrong type.");
}
} else {
$this->createError("Error requesting password reset: " . $req->getLastError());
}
}
} else if ($this->getTemplateName() === "account/register.twig") {
$settings = $this->getSettings();
if ($this->getUser()) {
$this->createError("You are already logged in.");
} else if (!$settings->isRegistrationAllowed()) {
$this->createError("Registration is not enabled on this website.");
}
} else if ($this->getTemplateName() === "account/login.twig" && $this->getUser()) {
header("Location: /admin");
exit();
} else if ($this->getTemplateName() === "account/accept_invite.twig") {
if (isset($_GET["token"]) && is_string($_GET["token"]) && !empty($_GET["token"])) {
$this->parameters["view"]["token"] = $_GET["token"];
$req = new \Core\API\User\CheckToken($this->getContext());
$this->parameters["view"]["success"] = $req->execute(array("token" => $_GET["token"]));
if ($this->parameters["view"]["success"]) {
if (strcmp($req->getResult()["token"]["type"], "invite") !== 0) {
$this->createError("The given token has a wrong type.");
switch ($templateName) {
case "account/reset_password.twig": {
if (isset($_GET["token"]) && is_string($_GET["token"]) && !empty($_GET["token"])) {
$this->parameters["view"]["token"] = $_GET["token"];
$req = new \Core\API\User\CheckToken($this->getContext());
$this->parameters["view"]["success"] = $req->execute(array("token" => $_GET["token"]));
if ($this->parameters["view"]["success"]) {
if (strcmp($req->getToken()->getType(), UserToken::TYPE_PASSWORD_RESET) !== 0) {
$this->createError("The given token has a wrong type.");
}
} else {
$this->parameters["view"]["invited_user"] = $req->getResult()["user"];
$this->createError("Error requesting password reset: " . $req->getLastError());
}
}
break;
}
case "account/register.twig": {
if ($this->getUser()) {
$this->createError("You are already logged in.");
} else if (!$settings->isRegistrationAllowed()) {
$this->createError("Registration is not enabled on this website.");
}
break;
}
case "account/login.twig": {
if ($this->getUser()) {
header("Location: /admin");
exit();
}
break;
}
case "account/accept_invite.twig": {
if (isset($_GET["token"]) && is_string($_GET["token"]) && !empty($_GET["token"])) {
$this->parameters["view"]["token"] = $_GET["token"];
$req = new \Core\API\User\CheckToken($this->getContext());
$this->parameters["view"]["success"] = $req->execute(array("token" => $_GET["token"]));
if ($this->parameters["view"]["success"]) {
if (strcmp($req->getToken()->getType(), UserToken::TYPE_INVITE) !== 0) {
$this->createError("The given token has a wrong type.");
} else {
$this->parameters["view"]["invited_user"] = $req->getToken()->getUser()->jsonSerialize();
}
} else {
$this->createError("Error confirming e-mail address: " . $req->getLastError());
}
} else {
$this->createError("Error confirming e-mail address: " . $req->getLastError());
$this->createError("The link you visited is no longer valid");
}
} else {
$this->createError("The link you visited is no longer valid");
break;
}
default:
break;
}
}
}

View File

@@ -5,6 +5,7 @@ namespace Core\Documents;
use Core\Elements\EmptyHead;
use Core\Elements\HtmlDocument;
use Core\Elements\SimpleBody;
use Core\Objects\DatabaseEntity\Group;
use Core\Objects\Router\Router;
class Info extends HtmlDocument {
@@ -16,7 +17,7 @@ class Info extends HtmlDocument {
class InfoBody extends SimpleBody {
protected function getContent(): string {
$user = $this->getDocument()->getUser();
$user = $this->getContext()->getUser();
if ($user && $user->hasGroup(Group::ADMIN)) {
phpinfo();
return "";

View File

@@ -2,7 +2,6 @@
namespace Core\Driver\SQL\Column;
use Core\Driver\SQL\Column\Column;
class NumericColumn extends Column {

View File

@@ -2,8 +2,6 @@
namespace Core\Driver\SQL\Expression;
use Core\Driver\SQL\SQL;
class DateAdd extends Expression {
private Expression $lhs;

View File

@@ -2,8 +2,6 @@
namespace Core\Driver\SQL\Expression;
use Core\Driver\SQL\SQL;
class DateSub extends Expression {
private Expression $lhs;

View File

@@ -2,8 +2,6 @@
namespace Core\Driver\SQL\Expression;
use Core\Driver\SQL\SQL;
abstract class Expression {
}

View File

@@ -5,7 +5,6 @@ namespace Core\Driver\SQL\Query;
use Core\Driver\SQL\Column\Column;
use Core\Driver\SQL\Column\EnumColumn;
use Core\Driver\SQL\Constraint\Constraint;
use Core\Driver\SQL\Constraint\ForeignKey;
use Core\Driver\SQL\Constraint\PrimaryKey;
use Core\Driver\SQL\PostgreSQL;
use Core\Driver\SQL\SQL;

View File

@@ -2,7 +2,6 @@
namespace Core\Driver\SQL\Query;
use Core\API\User\Create;
use Core\Driver\SQL\SQL;
class CreateTrigger extends Query {

View File

@@ -2,7 +2,6 @@
namespace Core\Driver\SQL\Query;
use Core\Driver\SQL\Condition\CondOr;
use Core\Driver\SQL\SQL;
class Delete extends ConditionalQuery {

View File

@@ -6,7 +6,6 @@ use Core\Configuration\Settings;
use Core\Driver\Logger\Logger;
use Core\Driver\SQL\SQL;
use Core\Objects\Context;
use Core\Objects\lang\LanguageModule;
use Core\Objects\Router\DocumentRoute;
use Core\Objects\Router\Router;
use Core\Objects\DatabaseEntity\User;
@@ -24,6 +23,7 @@ abstract class Document {
private array $cspWhitelist;
private string $domain;
protected bool $searchable;
protected array $languageModules;
public function __construct(Router $router) {
$this->router = $router;
@@ -34,6 +34,7 @@ abstract class Document {
$this->domain = $this->getSettings()->getBaseUrl();
$this->logger = new Logger("Document", $this->getSQL());
$this->searchable = false;
$this->languageModules = [];
}
public abstract function getTitle(): string;
@@ -88,11 +89,6 @@ abstract class Document {
}
}
public function loadLanguageModule(LanguageModule|string $module) {
$language = $this->getContext()->getLanguage();
$language->loadModule($module);
}
public function sendHeaders() {
if ($this->cspEnabled) {
$cspWhiteList = implode(" ", $this->cspWhitelist);
@@ -126,8 +122,12 @@ abstract class Document {
}
}
$language = $this->getContext()->getLanguage();
foreach ($this->languageModules as $module) {
$language->loadModule($module);
}
$code = $this->getCode($params);
$code = $this->getCode($params);
$this->sendHeaders();
return $code;
}

View File

@@ -94,6 +94,12 @@ class TemplateDocument extends Document {
"csp" => [
"nonce" => $this->getCSPNonce(),
"enabled" => $this->isCSPEnabled()
],
"language" => [
"code" => $language->getCode(),
"shortCode" => $language->getShortCode(),
"name" => $language->getName(),
"entries" => $language->getEntries()
]
]
], $params);

View File

@@ -2,6 +2,8 @@
namespace Core\Elements;
use Core\Objects\Context;
abstract class View extends StaticView {
private Document $document;
@@ -18,9 +20,10 @@ abstract class View extends StaticView {
public function getTitle(): string { return $this->title; }
public function getDocument(): Document { return $this->document; }
public function getContext(): Context { return $this->document->getContext(); }
public function getSiteName(): string {
return $this->getDocument()->getSettings()->getSiteName();
return $this->getContext()->getSettings()->getSiteName();
}
protected function load(string $viewClass) : string {
@@ -38,23 +41,13 @@ abstract class View extends StaticView {
return "";
}
private function loadLanguageModules() {
$lang = $this->document->getContext()->getLanguage();
foreach ($this->langModules as $langModule) {
$lang->loadModule($langModule);
}
}
// Virtual Methods
public function loadView() { }
public function getCode(): string {
// Load translations
$this->loadLanguageModules();
// Load metadata + head (title, scripts, includes, ...)
if($this->loadView) {
if ($this->loadView) {
$this->loadView();
}

View File

@@ -0,0 +1,19 @@
<?php
return [
"title" => "Einloggen",
"description" => "Loggen Sie sich in Ihren Account ein",
"form_title" => "Bitte geben Sie ihre Daten ein",
"username" => "Benutzername",
"username_or_email" => "Benutzername oder E-Mail",
"password" => "Passwort",
"remember_me" => "Eingeloggt bleiben",
"signing_in" => "Einloggen",
"sign_in" => "Einloggen",
"forgot_password" => "Passwort vergessen?",
"passwords_do_not_match" => "Die Passwörter stimmen nicht überein",
"register_text" => "Noch keinen Account? Jetzt registrieren",
"6_digit_code" => "6-stelliger Code",
"2fa_title" => "Es werden weitere Informationen zum Einloggen benötigt",
"2fa_text" => "Stecke dein 2FA-Gerät ein. Möglicherweise wird noch eine Interaktion benötigt, z.B. durch Eingabe einer PIN oder durch Berühren des Geräts",
];

View File

@@ -0,0 +1,11 @@
<?php
return [
"something_went_wrong" => "Etwas ist schief gelaufen",
"retry" => "Erneut versuchen",
"Go back" => "Zurück",
"submitting" => "Übermittle",
"submit" => "Absenden",
"language" => "Sprache",
"loading" => "Laden",
];

View File

@@ -0,0 +1,19 @@
<?php
return [
"title" => "Sign In",
"description" => "Sign In into your account",
"form_title" => "Please fill with your details",
"username" => "Username",
"username_or_email" => "Username or E-Mail",
"password" => "Password",
"remember_me" => "Remember Me",
"signing_in" => "Signing in",
"sign_in" => "Sign In",
"forgot_password" => "Forgot password?",
"register_text" => "Don't have an account? Sign Up",
"passwords_do_not_match" => "Your passwords did not match",
"6_digit_code" => "6-Digit Code",
"2fa_title" => "Additional information is required for logging in",
"2fa_text" => "Plugin your 2FA-Device. Interaction might be required, e.g. typing in a PIN or touching it."
];

View File

@@ -0,0 +1,11 @@
<?php
return [
"something_went_wrong" => "Something went wrong",
"retry" => "Retry",
"go_back" => "Go Back",
"submitting" => "Submitting",
"submit" => "Submit",
"language" => "Language",
"loading" => "Loading",
];

View File

@@ -2,6 +2,7 @@
namespace Core\Objects;
use Core\Objects\DatabaseEntity\Language;
use Twig\Extension\AbstractExtension;
use Twig\TwigFunction;
@@ -9,10 +10,16 @@ class CustomTwigFunctions extends AbstractExtension {
public function getFunctions(): array {
return [
new TwigFunction('L', array($this, 'translate')),
new TwigFunction('LoadLanguageModule', array($this, 'loadLanguageModule')),
];
}
public function translate(string $key): string {
return L($key);
}
public function loadLanguageModule(string $module): void {
$language = Language::getInstance();
$language->loadModule($module);
}
}

View File

@@ -2,7 +2,6 @@
namespace Core\Objects\DatabaseEntity\Controller;
use Core\Driver\SQL\Condition\Compare;
use Core\Driver\SQL\Condition\Condition;
use Core\Driver\SQL\SQL;

View File

@@ -3,8 +3,6 @@
namespace Core\Objects\DatabaseEntity\Controller;
use Core\Driver\Logger\Logger;
use Core\Driver\SQL\Condition\Condition;
use Core\Driver\SQL\Join;
use Core\Driver\SQL\Query\Select;
use Core\Driver\SQL\SQL;

View File

@@ -4,7 +4,6 @@ namespace Core\Objects\DatabaseEntity {
use Core\Objects\DatabaseEntity\Attribute\MaxLength;
use Core\Objects\DatabaseEntity\Attribute\Transient;
use Core\Objects\lang\LanguageModule;
use Core\Objects\DatabaseEntity\Controller\DatabaseEntity;
// TODO: language from cookie?
@@ -14,11 +13,11 @@ namespace Core\Objects\DatabaseEntity {
const GERMAN_STANDARD = 2;
const LANG_CODE_PATTERN = "/^[a-zA-Z]{2}_[a-zA-Z]{2}$/";
const LANG_MODULE_PATTERN = "/[a-zA-Z0-9_-]/";
#[MaxLength(5)] private string $code;
#[MaxLength(32)] private string $name;
#[Transient] private array $modules = [];
#[Transient] protected array $entries = [];
public function __construct(int $id, string $code, string $name) {
@@ -39,22 +38,6 @@ namespace Core\Objects\DatabaseEntity {
return $this->name;
}
public function loadModule(LanguageModule|string $module) {
if (!is_object($module)) {
$module = new $module();
}
if (!in_array($module, $this->modules)) {
$moduleEntries = $module->getEntries($this->code);
$this->entries = array_merge($this->entries, $moduleEntries);
$this->modules[] = $module;
}
}
public function translate(string $key): string {
return $this->entries[$key] ?? $key;
}
public function sendCookie(string $domain) {
setcookie('lang', $this->code, 0, "/", $domain, false, false);
}
@@ -73,6 +56,11 @@ namespace Core\Objects\DatabaseEntity {
$LANGUAGE = $this;
}
public static function getInstance(): Language {
global $LANGUAGE;
return $LANGUAGE;
}
public static function DEFAULT_LANGUAGE(bool $fromCookie = true): Language {
if ($fromCookie && isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
$acceptLanguage = $_SERVER['HTTP_ACCEPT_LANGUAGE'];
@@ -94,16 +82,70 @@ namespace Core\Objects\DatabaseEntity {
return new Language(1, "en_US", "American English");
}
public function getEntries(): array {
return $this->entries;
public function getEntries(?string $module = null): ?array {
if (!$module) {
return $this->entries;
} else {
return $this->entries[$module] ?? null;
}
}
public function translate(string $key): string {
if (preg_match("/(\w+)\.(\w+)/", $key, $matches)) {
$module = $matches[1];
$moduleKey = $matches[2];
if ($this->hasModule($module) && array_key_exists($moduleKey, $this->entries[$module])) {
return $this->entries[$module][$moduleKey];
}
}
return "[$key]";
}
public function addModule(string $module, array $entries) {
if ($this->hasModule($module)) {
$this->entries[$module] = array_merge($this->entries[$module], $entries);
} else {
$this->entries[$module] = $entries;
}
}
public function loadModule(string $module, bool $forceReload=false): bool {
if ($this->hasModule($module) && !$forceReload) {
return true;
}
if (!preg_match(self::LANG_MODULE_PATTERN, $module)) {
return false;
}
if (!preg_match(self::LANG_CODE_PATTERN, $this->code)) {
return false;
}
foreach (["Site", "Core"] as $baseDir) {
$filePath = realpath(implode("/", [$baseDir, "Localization", $this->code, "$module.php"]));
if ($filePath && is_file($filePath)) {
$moduleEntries = @include_once $filePath;
$this->addModule($module, $moduleEntries);
return true;
}
}
return false;
}
public function hasModule(string $module): bool {
return array_key_exists($module, $this->entries);
}
}
}
namespace {
function L($key) {
if (!array_key_exists('LANGUAGE', $GLOBALS))
return $key;
function L(string $key): string {
if (!array_key_exists('LANGUAGE', $GLOBALS)) {
return "[$key]";
}
global $LANGUAGE;
return $LANGUAGE->translate($key);

View File

@@ -1,8 +0,0 @@
<?php
namespace Core\Objects\lang;
abstract class LanguageModule {
public abstract function getEntries(string $langCode);
}

View File

@@ -8,10 +8,13 @@
<script src="/js/bootstrap.bundle.min.js" nonce="{{ site.csp.nonce }}"></script>
<link rel="stylesheet" href="/css/fontawesome.min.css" nonce="{{ site.csp.nonce }}">
<link rel="stylesheet" href="/css/account.css" nonce="{{ site.csp.nonce }}">
<title>Account - {{ view_title }}</title>
<title>Account - {{ L(view_title) }}</title>
{% if site.recaptcha.enabled %}
<script src="https://www.google.com/recaptcha/api.js?render={{ site.recaptcha.key }}" nonce="{{ site.csp.nonce }}"></script>
{% endif %}
<script nonce="{{ site.csp.nonce }}">
window.languageEntries = {{ site.language.entries|json_encode()|raw }};
</script>
{% endblock %}
{% block body %}
@@ -20,8 +23,8 @@
<div class="col-md-3 py-5 bg-primary text-white text-center" style='border-top-left-radius:.4em;border-bottom-left-radius:.4em;margin-left: auto'>
<div class="card-body">
<i class="fas fa-{{ view_icon }} fa-3x"></i>
<h2 class="py-3">{{ view_title }}</h2>
<p>{{ view_description }}</p>
<h2 class="py-3">{{ L(view_title) }}</h2>
<p>{{ L(view_description) }}</p>
</div>
</div>
<div class="col-md-5 pt-5 pb-2 border border-info" style='border-top-right-radius:.4em;border-bottom-right-radius:.4em;margin-right:auto'>

View File

@@ -1,32 +1,36 @@
{% extends "account/account_base.twig" %}
{% set view_title = 'Sign In' %}
{% set view_title = 'account.title' %}
{% set view_icon = 'user-lock' %}
{% set view_description = 'Sign In into your account' %}
{% set view_description = 'account.description' %}
{% block view_content %}
<h4 class="pb-4">Please fill with your details</h4>
<h4 class="pb-4">{{ L("account.form_title") }}</h4>
<form>
<div class="input-group">
<div class="input-group-append">
<span class="input-group-text"><i class="fas fa-hashtag"></i></span>
</div>
<input id="username" autocomplete='username' name="username" placeholder="Username or E-Mail" class="form-control" type="text" maxlength="32">
<input id="username" autocomplete='username' name="username"
placeholder="{{ L("account.username_or_email") }}" class="form-control" type="text" maxlength="32">
</div>
<div class="input-group mt-3">
<div class="input-group-append">
<span class="input-group-text"><i class="fas fa-key"></i></span>
</div>
<input type="password" autocomplete='password' name='password' id='password' class="form-control" placeholder="Password">
<input type="password" autocomplete='password' name='password' id='password' class="form-control"
placeholder={{ L("account.password") }}>
</div>
<div class="ml-2" style="line-height: 38px;">
<a href="/resetPassword">Forgot Password?</a>
<a href="/resetPassword">{{ L("account.forgot_password") }}</a>
</div>
<div class="input-group mt-3 mb-4">
<button type="button" class="btn btn-primary" id='btnLogin'>Sign In</button>
<button type="button" class="btn btn-primary" id='btnLogin'>{{ L("account.sign_in") }}</button>
{% if site.registrationEnabled %}
<div class="ml-2" style="line-height: 38px;">Don't have an account yet? <a href="/register">Click here</a> to register.</div>
<div class="ml-2" style="line-height: 38px;">
<a href="/register">{{ L("account.register_text") }}</a>
</div>
{% endif %}
</div>
</form>

View File

@@ -9,4 +9,5 @@
<noscript>You need Javascript enabled to run this app</noscript>
<div class="wrapper" type="module" id="admin-panel"></div>
<script src="/js/admin-panel/index.js" nonce="{{ site.csp.nonce }}"></script>
<link rel="stylesheet" href="/js/admin-panel/index.css" nonce="{{ site.csp.nonce }}"></link>
{% endblock %}