почти рабочая авторизация. оказывается сейчас нет payload у запроса, поэтому невозможно распарсить из него json.
This commit is contained in:
parent
0b794fac40
commit
b561dedb2b
@ -40,6 +40,8 @@ add_executable(terminal-web-server
|
||||
src/auth/resources.h
|
||||
src/auth/jwt.cpp
|
||||
src/auth/jwt.h
|
||||
src/auth/utils.cpp
|
||||
src/auth/utils.h
|
||||
)
|
||||
|
||||
find_package(Boost 1.53.0 COMPONENTS system thread filesystem log log_setup REQUIRED)
|
||||
|
@ -1,5 +1,80 @@
|
||||
//
|
||||
// Created by vlad on 04.11.2024.
|
||||
//
|
||||
|
||||
#include "jwt.h"
|
||||
#include <random>
|
||||
|
||||
#include "utils.h"
|
||||
|
||||
std::string http::auth::jwt::secretKey;
|
||||
|
||||
void http::auth::jwt::generateSecretKey() {
|
||||
secretKey.clear();
|
||||
|
||||
std::random_device rd;
|
||||
std::mt19937 generator(rd());
|
||||
std::uniform_int_distribution<> distribution('!', '~');
|
||||
for (size_t i = 0; i < 32; ++i) {
|
||||
secretKey += static_cast<char>(distribution(generator));
|
||||
}
|
||||
}
|
||||
|
||||
// payload, signature
|
||||
static std::pair<std::string, std::string> parseJwtFromCookie(const std::string& input) {
|
||||
std::string val1, val2;
|
||||
size_t dotPos = input.find('.');
|
||||
|
||||
// Если точка найдена
|
||||
if (dotPos != std::string::npos) {
|
||||
val1 = input.substr(0, dotPos);
|
||||
|
||||
// Если в val1 есть еще точки, нужно найти последнюю точку
|
||||
size_t lastDotPos = val1.find_last_of('.');
|
||||
if (lastDotPos != std::string::npos) {
|
||||
val1 = val1.substr(0, lastDotPos + 1);
|
||||
}
|
||||
|
||||
val2 = input.substr(dotPos + 1);
|
||||
} else {
|
||||
// Точка не найдена, val1 - вся строка
|
||||
val1 = input;
|
||||
}
|
||||
|
||||
return std::make_pair(http::utils::b64Decode(val1), val2);
|
||||
}
|
||||
|
||||
http::auth::jwt::Jwt http::auth::jwt::Jwt::fromCookies(const std::string &cookie) {
|
||||
auto pc = utils::parseCookies(cookie);
|
||||
Jwt t;
|
||||
if (pc.find("auth") != pc.end()) {
|
||||
auto tmp = parseJwtFromCookie(pc.at("auth"));
|
||||
t.payload = tmp.first;
|
||||
t.signature = tmp.second;
|
||||
}
|
||||
|
||||
return t;
|
||||
}
|
||||
|
||||
http::auth::jwt::Jwt http::auth::jwt::Jwt::fromUser(const std::string &user) {
|
||||
Jwt t;
|
||||
t.payload = user;
|
||||
return t;
|
||||
}
|
||||
|
||||
bool http::auth::jwt::Jwt::isValid() {
|
||||
if (payload.empty() || signature.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto realSignature = utils::sha256(this->payload + secretKey);
|
||||
return signature == realSignature;
|
||||
}
|
||||
|
||||
std::string http::auth::jwt::Jwt::getUsername() {
|
||||
return payload;
|
||||
}
|
||||
|
||||
std::string http::auth::jwt::Jwt::asCookie() {
|
||||
signature = utils::sha256(this->payload + secretKey);
|
||||
auto val = utils::b64Encode(payload) + "." + signature;
|
||||
return val + ";Path=/; Max-Age=86400; HttpOnly; SameSite=Lax";
|
||||
}
|
||||
|
||||
http::auth::jwt::Jwt::~Jwt() = default;
|
||||
|
@ -4,10 +4,20 @@
|
||||
#include "resources.h"
|
||||
|
||||
namespace http::auth::jwt {
|
||||
extern std::string secretKey;
|
||||
|
||||
void generateSecretKey();
|
||||
|
||||
/**
|
||||
* Упрощенная реализация JWT (Json Web Token). Токен имеет вид: `{ username | base64 }.{ signature | base64 }`.
|
||||
* Сигнатура вычисляется следующим образом: `SHA256(username + secret)`.
|
||||
* Имя cookie: `auth`.
|
||||
*/
|
||||
class Jwt {
|
||||
std::string payload;
|
||||
std::string signature;
|
||||
public:
|
||||
static Jwt fromCookies(const std::string& cookie);
|
||||
static Jwt fromString(const std::string& cookie);
|
||||
static Jwt fromUser(const std::string& User);
|
||||
|
||||
bool isValid();
|
||||
|
@ -1,15 +1,79 @@
|
||||
//
|
||||
// Created by vlad on 31.10.2024.
|
||||
//
|
||||
|
||||
#include "resources.h"
|
||||
#include <boost/log/trivial.hpp>
|
||||
#include <boost/algorithm/string.hpp>
|
||||
#include "jwt.h"
|
||||
#include "utils.h"
|
||||
|
||||
#include <utility>
|
||||
|
||||
// http::auth::AuentificationRequiredResource::AuentificationRequiredResource(const std::string &path, AuthProvider& provider, resource::respGenerator generator): BasicResource(path), generator_(std::move(generator)) {
|
||||
// }
|
||||
//
|
||||
// void http::auth::AuentificationRequiredResource::handle(const server::Request &req, server::Reply &rep) {
|
||||
// }
|
||||
//
|
||||
// http::auth::AuentificationRequiredResource::~AuentificationRequiredResource() = default;
|
||||
http::auth::User::User(const std::string &username, const std::string &passwordHash): username(username), passwordHash(passwordHash) {}
|
||||
|
||||
bool http::auth::User::checkPassword(const std::string &pass) const {
|
||||
return utils::sha256(pass) == passwordHash;
|
||||
}
|
||||
|
||||
void http::auth::User::setPassword(const std::string &pass) {
|
||||
this->passwordHash = utils::sha256(pass);
|
||||
}
|
||||
|
||||
bool http::auth::User::checkPremisions(uint32_t p) const {
|
||||
if (this->perms & SUPERUSER) {
|
||||
return true;
|
||||
} else {
|
||||
return (this->perms & p) == p;
|
||||
}
|
||||
}
|
||||
|
||||
void http::auth::User::setPremisions(uint32_t p) {
|
||||
if (p & SUPERUSER) {
|
||||
this->perms = SUPERUSER;
|
||||
} else {
|
||||
this->perms |= p;
|
||||
}
|
||||
}
|
||||
|
||||
void http::auth::User::resetPremisions(uint32_t p) {
|
||||
this->perms &= p;
|
||||
}
|
||||
|
||||
http::auth::User::~User() = default;
|
||||
|
||||
|
||||
http::auth::AuthProvider::AuthProvider() = default;
|
||||
|
||||
std::shared_ptr<http::auth::User> http::auth::AuthProvider::doAuth(const std::string &username, const std::string &password, server::Reply &rep) {
|
||||
for (const auto& u: users) {
|
||||
if (u->username == username) {
|
||||
if (u->checkPassword(password)) {
|
||||
auto t = jwt::Jwt::fromUser(u->username);
|
||||
rep.headers.push_back({.name = "Set-Cookie", .value = t.asCookie()});
|
||||
return u;
|
||||
}
|
||||
BOOST_LOG_TRIVIAL(warning) << "http::auth::AuthProvider::doAuth(): Failed to login " << username << ", password: " << password << " (incorrect password)";
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
BOOST_LOG_TRIVIAL(warning) << "http::auth::AuthProvider::doAuth(): Failed to login " << username << ", password: " << password << " (user not found)";
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::shared_ptr<http::auth::User> http::auth::AuthProvider::getSession(const server::Request &req) {
|
||||
for (const auto& header: req.headers) {
|
||||
if (boost::iequals(header.name, "cookie")) {
|
||||
auto t = jwt::Jwt::fromCookies(header.value);
|
||||
if (t.isValid()) {
|
||||
const auto name = t.getUsername();
|
||||
// токен валидный, ищем юзера
|
||||
for (auto& u: users) {
|
||||
if (u->username == name) {
|
||||
return u;
|
||||
}
|
||||
}
|
||||
|
||||
BOOST_LOG_TRIVIAL(warning) << "http::auth::AuthProvider::getSession(): Found valid session for a non-existent user " << name;
|
||||
}
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
http::auth::AuthProvider::~AuthProvider() = default;
|
||||
|
@ -9,20 +9,25 @@ namespace http::auth {
|
||||
*/
|
||||
class User {
|
||||
private:
|
||||
uint32_t perms;
|
||||
uint32_t perms{};
|
||||
|
||||
public:
|
||||
const std::string username;
|
||||
std::string passwordHash;
|
||||
|
||||
User(const std::string& username, const std::string& passwordHash);
|
||||
/**
|
||||
* Конструктор пользователя.
|
||||
* @param username Имя пользователя, он же - логин.
|
||||
* @param passwordHash Хеш sha256 пароля пользователя. Если передать пустой, то пароль будет сгенерирован такой же, как и имя пользователя.
|
||||
*/
|
||||
explicit User(const std::string& username, const std::string& passwordHash = "");
|
||||
|
||||
/**
|
||||
* Проверить пароль на соответствие хешу
|
||||
* @param pass
|
||||
* @return
|
||||
*/
|
||||
bool checkPassword(const std::string& pass);
|
||||
bool checkPassword(const std::string& pass) const;
|
||||
|
||||
/**
|
||||
* Установка пароля
|
||||
@ -43,7 +48,7 @@ namespace http::auth {
|
||||
* @param p набор прав, из констант данного класса.
|
||||
* @return
|
||||
*/
|
||||
bool checkPremisions(uint32_t p);
|
||||
bool checkPremisions(uint32_t p) const;
|
||||
|
||||
void setPremisions(uint32_t p);
|
||||
void resetPremisions(uint32_t p);
|
||||
@ -65,9 +70,11 @@ namespace http::auth {
|
||||
* @param rep
|
||||
* @return true, в случае успешной авторизации
|
||||
*/
|
||||
bool doAuth(const std::string& username, const std::string& password, server::Reply& rep);
|
||||
std::shared_ptr<http::auth::User> doAuth(const std::string &username, const std::string &password, server::Reply &rep);
|
||||
|
||||
std::shared_ptr<User> getSession(server::Request& req);
|
||||
std::vector<std::shared_ptr<User>> users;
|
||||
|
||||
std::shared_ptr<User> getSession(const server::Request &req);
|
||||
|
||||
~AuthProvider();
|
||||
};
|
||||
|
78
src/auth/utils.cpp
Normal file
78
src/auth/utils.cpp
Normal file
@ -0,0 +1,78 @@
|
||||
#include "utils.h"
|
||||
#include <openssl/sha.h>
|
||||
#include <openssl/evp.h>
|
||||
#include <openssl/bio.h>
|
||||
#include <openssl/buffer.h>
|
||||
#include <string>
|
||||
#include <iomanip>
|
||||
|
||||
std::string http::utils::sha256(const std::string &payload) {
|
||||
// Вычисляем SHA256 хеш
|
||||
unsigned char hash[SHA256_DIGEST_LENGTH];
|
||||
SHA256(reinterpret_cast<const unsigned char *>(payload.c_str()), payload.length(), hash);
|
||||
|
||||
// Преобразуем хеш в шестнадцатеричную строку
|
||||
std::stringstream ss;
|
||||
for (unsigned char i : hash) {
|
||||
ss << std::hex << std::setw(2) << std::setfill('0') << static_cast<int>(i);
|
||||
}
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
std::string http::utils::b64Encode(const std::string &payload) {
|
||||
BIO *bio_mem = BIO_new(BIO_s_mem());
|
||||
BIO *bio_b64 = BIO_new(BIO_f_base64());
|
||||
bio_b64 = BIO_push(bio_b64, bio_mem);
|
||||
|
||||
BIO_write(bio_b64, payload.c_str(), payload.length());
|
||||
BIO_flush(bio_b64);
|
||||
|
||||
BUF_MEM *bptr = nullptr;
|
||||
BIO_get_mem_data(bio_mem, &bptr);
|
||||
|
||||
std::string result(bptr->data, bptr->length);
|
||||
BIO_free_all(bio_b64);
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string http::utils::b64Decode(const std::string &payload) {
|
||||
BIO *bio_mem = BIO_new(BIO_s_mem());
|
||||
BIO *bio_b64 = BIO_new(BIO_f_base64());
|
||||
bio_b64 = BIO_push(bio_b64, bio_mem);
|
||||
|
||||
BIO_write(bio_b64, payload.c_str(), static_cast<int>(payload.size()));
|
||||
BIO_flush(bio_b64);
|
||||
|
||||
const int len = BIO_get_mem_data(bio_mem, NULL);
|
||||
char buffer[static_cast<size_t>(len)];
|
||||
BIO_read(bio_mem, buffer, len);
|
||||
|
||||
std::string result(buffer, static_cast<unsigned int>(len));
|
||||
BIO_free_all(bio_b64);
|
||||
return result;
|
||||
}
|
||||
|
||||
std::map<std::string, std::string> http::utils::parseCookies(const std::string& cookieString) {
|
||||
std::map<std::string, std::string> cookies;
|
||||
std::istringstream cookieStream(cookieString);
|
||||
|
||||
std::string cookie;
|
||||
while (std::getline(cookieStream, cookie, ';')) {
|
||||
// Разделяем имя и значение Cookie
|
||||
size_t equalPos = cookie.find('=');
|
||||
if (equalPos == std::string::npos) {
|
||||
continue; // Неверный формат Cookie
|
||||
}
|
||||
std::string name = cookie.substr(0, equalPos);
|
||||
std::string value = cookie.substr(equalPos + 1);
|
||||
|
||||
// Удаляем пробелы с начала и конца значения Cookie
|
||||
value.erase(0, value.find_first_not_of(' '));
|
||||
value.erase(value.find_last_not_of(' ') + 1);
|
||||
|
||||
// Добавляем Cookie в map
|
||||
cookies[name] = value;
|
||||
}
|
||||
|
||||
return cookies;
|
||||
}
|
17
src/auth/utils.h
Normal file
17
src/auth/utils.h
Normal file
@ -0,0 +1,17 @@
|
||||
#ifndef UTILS_H
|
||||
#define UTILS_H
|
||||
|
||||
#include <map>
|
||||
#include <string>
|
||||
|
||||
namespace http::utils {
|
||||
std::string sha256(const std::string& payload);
|
||||
std::string sha256AsB64(const std::string& payload);
|
||||
|
||||
std::string b64Encode(const std::string& payload);
|
||||
std::string b64Decode(const std::string& payload);
|
||||
|
||||
std::map<std::string, std::string> parseCookies(const std::string& cookieStrin);
|
||||
}
|
||||
|
||||
#endif //UTILS_H
|
38
src/main.cpp
38
src/main.cpp
@ -11,12 +11,15 @@
|
||||
#include <boost/log/utility/setup/formatter_parser.hpp>
|
||||
#include <boost/asio/buffer.hpp>
|
||||
#include <boost/asio/ssl/context.hpp>
|
||||
#include <boost/property_tree/ptree.hpp>
|
||||
#include <boost/property_tree/json_parser.hpp>
|
||||
#include <cstddef>
|
||||
#include <memory>
|
||||
#include <fstream>
|
||||
|
||||
#include "terminal_api_driver.h"
|
||||
#include "auth/resources.h"
|
||||
#include "auth/jwt.h"
|
||||
|
||||
|
||||
namespace ssl = boost::asio::ssl; // from <boost/asio/ssl.hpp>
|
||||
@ -76,11 +79,11 @@ void init_logging() {
|
||||
class ServerResources {
|
||||
std::unique_ptr<http::resource::StaticFileFactory> sf;
|
||||
std::unique_ptr<api_driver::ApiDriver> api;
|
||||
http::auth::AuthProvider auth{};
|
||||
|
||||
public:
|
||||
static constexpr const char* INDEX_HTML = "static/main.html";
|
||||
static constexpr const char* LOGIN_HTML = "static/login.html";
|
||||
static constexpr const char* LOGIN_FAILED_HTML = "static/login-failed.html";
|
||||
|
||||
// картинки, их даже можно кешировать
|
||||
static constexpr const char* FAVICON_ICO = "static/favicon.png";
|
||||
@ -94,9 +97,10 @@ public:
|
||||
ServerResources(const ServerResources&) = delete;
|
||||
|
||||
ServerResources(): sf(std::make_unique<http::resource::StaticFileFactory>()), api(std::make_unique<api_driver::ApiDriver>()) {
|
||||
auth.users.emplace_back(std::make_shared<http::auth::User>("admin"));
|
||||
|
||||
sf->registerFile(INDEX_HTML, mime_types::text_html, false);
|
||||
sf->registerFile(LOGIN_HTML, mime_types::text_html, false);
|
||||
sf->registerFile(LOGIN_FAILED_HTML, mime_types::text_html, false);
|
||||
|
||||
sf->registerFile(FAVICON_ICO, mime_types::image_png, true);
|
||||
sf->registerFile(KROKODIL_GIF, mime_types::image_gif, true);
|
||||
@ -114,15 +118,36 @@ public:
|
||||
|
||||
void registerResources(http::server::Server& s) {
|
||||
s.resources.emplace_back(std::make_unique<http::resource::GenericResource>("/", [this](const auto& req, auto& rep) {
|
||||
boost::ignore_unused(req);
|
||||
sf->serve(INDEX_HTML, rep);
|
||||
auto user = auth.getSession(req);
|
||||
if (user == nullptr) {
|
||||
http::server::httpRedirect(rep, "/login");
|
||||
} else {
|
||||
sf->serve(INDEX_HTML, rep);
|
||||
}
|
||||
}));
|
||||
|
||||
s.resources.emplace_back(std::make_unique<http::resource::GenericResource>("/login", [this](const auto& req, auto& rep) {
|
||||
if (req.method == "GET") {
|
||||
sf->serve(LOGIN_HTML, rep);
|
||||
} else if (req.method == "POST") {
|
||||
sf->serve(LOGIN_FAILED_HTML, rep);
|
||||
rep.status = http::server::ok;
|
||||
rep.headers.clear();
|
||||
rep.headers.push_back({.name = "Content-Type", .value = toString(mime_types::json)});
|
||||
try {
|
||||
std::istringstream is(req.body);
|
||||
boost::property_tree::ptree pt;
|
||||
boost::property_tree::read_json(is, pt);
|
||||
|
||||
auto u = auth.doAuth(req);
|
||||
if (u == nullptr) {
|
||||
throw std::runtime_error("invalid session");
|
||||
}
|
||||
std::string result = R"({"redirect":"/"})";
|
||||
rep.content.insert(rep.content.end(), result.c_str(), result.c_str() + result.size());
|
||||
} catch (std::exception &e) {
|
||||
std::string result = R"({"error":"Неверный логин или пароль"})";
|
||||
rep.content.insert(rep.content.end(), result.c_str(), result.c_str() + result.size());
|
||||
}
|
||||
} else {
|
||||
http::server::stockReply(http::server::bad_request, rep);
|
||||
}
|
||||
@ -214,6 +239,9 @@ int main(int argc, char *argv[]) {
|
||||
BOOST_LOG_TRIVIAL(info) << "Starting RELEASE build" << argv[0];
|
||||
#endif
|
||||
|
||||
http::auth::jwt::generateSecretKey();
|
||||
BOOST_LOG_TRIVIAL(info) << "Generated new secret key " << http::auth::jwt::secretKey;
|
||||
|
||||
ServerResources resources;
|
||||
|
||||
// Initialise the server.
|
||||
|
@ -126,9 +126,7 @@ namespace http::server {
|
||||
|
||||
void SslConnection::doWrite() {
|
||||
reply_.headers.push_back({.name = "Server", .value = SERVER_HEADER_VALUE});
|
||||
if (!reply_.content.empty()) {
|
||||
reply_.headers.push_back({.name = "Content-Length", .value = std::to_string(reply_.content.size())});
|
||||
}
|
||||
reply_.headers.push_back({.name = "Content-Length", .value = std::to_string(reply_.content.size())});
|
||||
if (request_.http_version_major == 1) {
|
||||
reply_.headers.push_back({.name = "Connection", .value = "keep-alive"});
|
||||
}
|
||||
|
@ -13,6 +13,7 @@ namespace http::server {
|
||||
const std::string multiple_choices = "HTTP/1.1 300 Multiple Choices\r\n";
|
||||
const std::string moved_permanently = "HTTP/1.1 301 Moved Permanently\r\n";
|
||||
const std::string moved_temporarily = "HTTP/1.1 302 Moved Temporarily\r\n";
|
||||
const std::string see_other_redirect = "HTTP/1.1 303 See Other\r\n";
|
||||
const std::string not_modified = "HTTP/1.1 304 Not Modified\r\n";
|
||||
const std::string bad_request = "HTTP/1.1 400 Bad Request\r\n";
|
||||
const std::string unauthorized = "HTTP/1.1 401 Unauthorized\r\n";
|
||||
@ -39,6 +40,8 @@ namespace http::server {
|
||||
return boost::asio::buffer(moved_permanently);
|
||||
case status_type::moved_temporarily:
|
||||
return boost::asio::buffer(moved_temporarily);
|
||||
case status_type::see_other_redirect:
|
||||
return boost::asio::buffer(see_other_redirect);
|
||||
case status_type::not_modified:
|
||||
return boost::asio::buffer(not_modified);
|
||||
case status_type::bad_request:
|
||||
@ -78,7 +81,9 @@ namespace http::server {
|
||||
buffers.push_back(boost::asio::buffer(misc_strings::crlf));
|
||||
}
|
||||
buffers.emplace_back(boost::asio::buffer(misc_strings::crlf));
|
||||
buffers.emplace_back(boost::asio::buffer(content));
|
||||
if (!content.empty()) {
|
||||
buffers.emplace_back(boost::asio::buffer(content));
|
||||
}
|
||||
return buffers;
|
||||
}
|
||||
|
||||
@ -212,4 +217,11 @@ namespace http::server {
|
||||
rep.headers.push_back({.name = "Content-Type", .value = toString(mime_types::text_html)});
|
||||
stock_replies::as_content(status, rep.content);
|
||||
}
|
||||
|
||||
void httpRedirect(Reply &rep, const std::string &location) {
|
||||
rep.status = see_other_redirect;
|
||||
rep.content.clear();
|
||||
rep.headers.push_back({.name = "Content-Type", .value = toString(mime_types::text_html)});
|
||||
rep.headers.push_back({.name = "Location", .value = location});
|
||||
}
|
||||
} // namespace http::server
|
||||
|
@ -16,6 +16,7 @@ namespace http::server {
|
||||
multiple_choices = 300,
|
||||
moved_permanently = 301,
|
||||
moved_temporarily = 302,
|
||||
see_other_redirect = 303,
|
||||
not_modified = 304,
|
||||
bad_request = 400,
|
||||
unauthorized = 401,
|
||||
@ -46,6 +47,7 @@ namespace http::server {
|
||||
/// Get a stock reply.
|
||||
void stockReply(status_type status, Reply& rep);
|
||||
|
||||
void httpRedirect(Reply& rep, const std::string& location);
|
||||
} // namespace http::Server
|
||||
|
||||
|
||||
|
@ -1,99 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>RSCM-101 | Вход</title>
|
||||
<link rel="stylesheet" type="text/css" href="/style.css">
|
||||
<style>
|
||||
#form-wrapper {
|
||||
overflow: hidden;
|
||||
max-width: 27em;
|
||||
margin: 5em auto;
|
||||
height: auto;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
padding: 4px 0;
|
||||
margin: 1.5em;
|
||||
}
|
||||
|
||||
.form-row * {
|
||||
font-size: 1em;
|
||||
text-align: left;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.form-row label {
|
||||
line-height: 2em;
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
.form-row input {
|
||||
padding: 8px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
border: none;
|
||||
border-bottom: var(--brand-bg) 2px solid;
|
||||
background-color: var(--bg-color);
|
||||
text-overflow: ellipsis;
|
||||
min-height: 2em;
|
||||
}
|
||||
|
||||
.form-row input:focus {
|
||||
outline: none;
|
||||
border: none;
|
||||
border-bottom: var(--brand-text) 2px solid;
|
||||
background-color: var(--bg-selected);
|
||||
}
|
||||
|
||||
#submit {
|
||||
border: none;
|
||||
font-weight: bolder;
|
||||
background: var(--bg-action);
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id="form-wrapper">
|
||||
<h1> Вход </h1>
|
||||
<form method="POST" id="login-form">
|
||||
{% csrf_token %}
|
||||
<div class="form-row value-bad">
|
||||
Неверный логин или пароль
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="username">Имя пользователя</label>
|
||||
<input type="text" name="username" id="username" required/>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="password">Пароль</label>
|
||||
<input type="password" name="password" id="password" required/>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<input id="submit" type="submit" value="Войти">
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.getElementById("username").onkeydown = (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
document.getElementById("password").focus()
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById("password").onkeydown = (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
document.getElementById("login-form").submit()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
@ -61,13 +61,8 @@
|
||||
|
||||
<div id="form-wrapper">
|
||||
<h1> Вход </h1>
|
||||
<form method="POST" id="login-form">
|
||||
<!-- {% csrf_token %}-->
|
||||
<!-- {% if message %}-->
|
||||
<!-- <div class="form-row value-bad">-->
|
||||
<!-- {{ message }}-->
|
||||
<!-- </div>-->
|
||||
<!-- {% endif %}-->
|
||||
<form id="login-form" onsubmit="submitLoginForm(); return false">
|
||||
<div class="form-row value-bad" hidden id="form-error-message"></div>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="username">Имя пользователя</label>
|
||||
@ -97,6 +92,41 @@
|
||||
document.getElementById("login-form").submit()
|
||||
}
|
||||
}
|
||||
|
||||
const loginForm = document.getElementById('login-form');
|
||||
const formErrorMessage = document.getElementById('form-error-message')
|
||||
|
||||
function submitLoginForm() {
|
||||
const username = document.getElementById('username').value
|
||||
const password = document.getElementById('password').value
|
||||
|
||||
const requestData = {
|
||||
"username": username,
|
||||
"password": password
|
||||
};
|
||||
|
||||
fetch('/login', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(requestData)
|
||||
}).then(response => {
|
||||
// Обработка ответа сервера
|
||||
response.json().then((value) => {
|
||||
if (value["error"]) {
|
||||
formErrorMessage.innerText = value["error"]
|
||||
formErrorMessage.removeAttribute("hidden")
|
||||
} else {
|
||||
window.location = "/"
|
||||
}
|
||||
})
|
||||
}).catch(error => {
|
||||
formErrorMessage.innerText = error
|
||||
formErrorMessage.removeAttribute("hidden")
|
||||
console.error('Ошибка отправки запроса:', error) // Обработка ошибки отправки запроса
|
||||
})
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
Loading…
x
Reference in New Issue
Block a user