login now works and files are protected
This commit is contained in:
parent
79b5737bcb
commit
fbb54b461e
@ -2,59 +2,57 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { API_BASE } from '$lib/config';
|
import { API_BASE } from '$lib/config';
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
|
||||||
let user : any = '';
|
let error = "";
|
||||||
let password : any = '';
|
|
||||||
let loading : any = false;
|
|
||||||
let error : any = '';
|
|
||||||
|
|
||||||
async function handleLogin() {
|
let Credentials = {
|
||||||
let error : any = '';
|
username: "",
|
||||||
let loading : Boolean = true;
|
password: "",
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await fetch(`${API_BASE}/api/shadowrun/characters`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({
|
|
||||||
username: user,
|
|
||||||
password: password,
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
if (res.ok) {
|
|
||||||
characters = await res.json();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
alert('Logged in!');
|
async function handleLogin() {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${API_BASE}/login`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
credentials: 'include',
|
||||||
|
body: JSON.stringify(
|
||||||
|
Credentials )
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
error = await res.text();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
goto("/shadowrun")
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error = e.message;
|
error = e.message;
|
||||||
} finally {
|
|
||||||
loading = false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<form class="login" on:submit|preventDefault={handleLogin}>
|
<form class="login">
|
||||||
<h2>Login</h2>
|
<h2>Login</h2>
|
||||||
|
|
||||||
<label>
|
<label>
|
||||||
User
|
User
|
||||||
<input type="text" bind:value={user} required />
|
<input type="text" bind:value={Credentials.username} required />
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label>
|
<label>
|
||||||
Password
|
Password
|
||||||
<input type="password" bind:value={password} required />
|
<input type="password" bind:value={Credentials.password} required />
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
<button on:click={handleLogin}>
|
||||||
|
Login
|
||||||
|
</button>
|
||||||
|
|
||||||
{#if error}
|
{#if error}
|
||||||
<p class="error">{error}</p>
|
<p class="error">{error}</p>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<button disabled={loading}>
|
|
||||||
{loading ? 'Logging in…' : 'Login'}
|
|
||||||
</button>
|
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -29,20 +29,21 @@ bool isLoggedIn(const crow::request& req) {
|
|||||||
std::optional<std::string> loginUser(const std::string& username, const std::string& password)
|
std::optional<std::string> loginUser(const std::string& username, const std::string& password)
|
||||||
{
|
{
|
||||||
auto user = getVerifiedUser(username, password);
|
auto user = getVerifiedUser(username, password);
|
||||||
if (user.has_value()) {
|
if (user) {
|
||||||
return sessionHandler.createSession(user.value().id);
|
return sessionHandler.createSession(user->id);
|
||||||
}
|
}
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
void initLogin(crow::SimpleApp& app)
|
void initLogin(crow::SimpleApp& app)
|
||||||
{
|
{
|
||||||
// createUser("lukas", "Trollar4928");
|
|
||||||
|
//createUser("lukas", "Trollar4928");
|
||||||
|
|
||||||
CROW_ROUTE(app, "/login").methods("POST"_method)
|
CROW_ROUTE(app, "/login").methods("POST"_method)
|
||||||
([](const crow::request& req) {
|
([](const crow::request& req) {
|
||||||
nlohmann::json body = nlohmann::json::parse(req.body); // parse JSON from HTTP body
|
nlohmann::json body = nlohmann::json::parse(req.body); // parse JSON from HTTP body
|
||||||
if (!body.empty())
|
if (body.empty())
|
||||||
return crow::response(400, "Invalid JSON");
|
return crow::response(400, "Invalid JSON");
|
||||||
|
|
||||||
auto usenameIt = body.find("username");
|
auto usenameIt = body.find("username");
|
||||||
|
|||||||
@ -11,15 +11,27 @@ void initLogin(crow::SimpleApp& app);
|
|||||||
|
|
||||||
bool isLoggedIn(const crow::request& req);
|
bool isLoggedIn(const crow::request& req);
|
||||||
|
|
||||||
// lambda to be used by endpoint that requiere login
|
// login_required lambda that works for any handler with arbitrary args
|
||||||
inline auto login_required = [](auto handler){
|
inline auto login_required = [](auto handler){
|
||||||
return [handler](const crow::request& req){
|
return [handler](auto&&... args) -> crow::response {
|
||||||
|
// the first argument is always crow::request
|
||||||
|
const crow::request& req = std::get<0>(std::forward_as_tuple(args...));
|
||||||
if (!isLoggedIn(req)) {
|
if (!isLoggedIn(req)) {
|
||||||
return crow::response(401, "Login required");
|
return crow::response(401, "Login required");
|
||||||
}
|
}
|
||||||
return handler(req);
|
|
||||||
|
// call original handler with all arguments
|
||||||
|
auto result = handler(std::forward<decltype(args)>(args)...);
|
||||||
|
|
||||||
|
// ensure crow::response return type
|
||||||
|
if constexpr (std::is_same_v<decltype(result), crow::response>) {
|
||||||
|
return result;
|
||||||
|
} else {
|
||||||
|
return crow::response(result);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
#endif // __LOGIN_H__
|
#endif // __LOGIN_H__
|
||||||
@ -1,8 +1,11 @@
|
|||||||
#include "loginDb.hpp"
|
#include "loginDb.hpp"
|
||||||
#include "databasepool.h"
|
#include "databasepool.h"
|
||||||
|
#include <memory>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
extern "C" {
|
extern "C" {
|
||||||
#include "monocypher.h"
|
#include "monocypher.h"
|
||||||
}
|
}
|
||||||
@ -59,8 +62,14 @@ int createUser(const std::string& username, const std::string& password){
|
|||||||
int64_t id;
|
int64_t id;
|
||||||
auto db = dbpool.acquire();
|
auto db = dbpool.acquire();
|
||||||
|
|
||||||
auto user = db->get_optional<User>(
|
for (auto &u : db->get_all<login::User>()) {
|
||||||
where(c(&User::username) == username)
|
if (u.username == username){
|
||||||
|
std::cout << "WTF" << std::endl;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
auto user = db->get_optional<login::User>(
|
||||||
|
where(c(&login::User::username) == username)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (user.has_value()) {
|
if (user.has_value()) {
|
||||||
@ -76,11 +85,16 @@ int createUser(const std::string& username, const std::string& password){
|
|||||||
|
|
||||||
std::optional<User> getUser(const std::string& username){
|
std::optional<User> getUser(const std::string& username){
|
||||||
auto db = dbpool.acquire();
|
auto db = dbpool.acquire();
|
||||||
auto user = db->get_optional<User>(
|
auto user = db->get_all<login::User>(
|
||||||
where(c(&User::username) == username)
|
where(c(&login::User::username) == username)
|
||||||
);
|
);
|
||||||
dbpool.release(db);
|
dbpool.release(db);
|
||||||
return user;
|
|
||||||
|
if(user.size() > 0){
|
||||||
|
return user[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
std::optional<User> getVerifiedUser(const std::string& username, const std::string& password){
|
std::optional<User> getVerifiedUser(const std::string& username, const std::string& password){
|
||||||
@ -88,7 +102,7 @@ std::optional<User> getVerifiedUser(const std::string& username, const std::stri
|
|||||||
if (!user.has_value())
|
if (!user.has_value())
|
||||||
return {};
|
return {};
|
||||||
|
|
||||||
if (verifyUser(user.value(), password)){
|
if (verifyUser(*user, password)){
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
return {};
|
return {};
|
||||||
|
|||||||
@ -3,7 +3,6 @@
|
|||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <optional>
|
|
||||||
#include "json.hpp"
|
#include "json.hpp"
|
||||||
#include "utils.hpp"
|
#include "utils.hpp"
|
||||||
namespace login
|
namespace login
|
||||||
|
|||||||
55
src/main.cpp
55
src/main.cpp
@ -7,38 +7,12 @@
|
|||||||
|
|
||||||
using namespace std;
|
using namespace std;
|
||||||
|
|
||||||
std::string read_file(const std::string& path) {
|
|
||||||
std::ifstream file(path, std::ios::binary);
|
|
||||||
if (!file) {
|
|
||||||
throw std::runtime_error("Cannot open file: " + path);
|
|
||||||
}
|
|
||||||
std::ostringstream ss;
|
|
||||||
ss << file.rdbuf();
|
|
||||||
return ss.str();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Simple MIME type detection
|
|
||||||
std::string get_mime_type(const std::string& path) {
|
|
||||||
if (path.ends_with(".html")) return "text/html";
|
|
||||||
if (path.ends_with(".js")) return "text/javascript";
|
|
||||||
if (path.ends_with(".css")) return "text/css";
|
|
||||||
if (path.ends_with(".json")) return "application/json";
|
|
||||||
if (path.ends_with(".svg")) return "image/svg+xml";
|
|
||||||
if (path.ends_with(".png")) return "image/png";
|
|
||||||
if (path.ends_with(".jpg") || path.ends_with(".jpeg")) return "image/jpeg";
|
|
||||||
if (path.ends_with(".woff")) return "font/woff";
|
|
||||||
if (path.ends_with(".woff2")) return "font/woff2";
|
|
||||||
if (path.ends_with(".pdf")) return "application/pdf";
|
|
||||||
return "application/octet-stream";
|
|
||||||
}
|
|
||||||
|
|
||||||
int main() {
|
int main() {
|
||||||
crow::SimpleApp app;
|
crow::SimpleApp app;
|
||||||
const filesystem::path build_dir = "frontend/build/"; // <-- set your build folder
|
|
||||||
|
|
||||||
// Root route
|
// Root route
|
||||||
CROW_ROUTE(app, "/")([&]() {
|
CROW_ROUTE(app, "/")([&]() {
|
||||||
auto data = read_file(build_dir / "index.html");
|
auto data = utils::read_file(utils::build_dir / "index.html");
|
||||||
return crow::response(200, "text/html", data);
|
return crow::response(200, "text/html", data);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -63,40 +37,19 @@ int main() {
|
|||||||
([&](const std::string& p) {
|
([&](const std::string& p) {
|
||||||
const filesystem::path assets_dir = "assets/"; // <-- set your build folder
|
const filesystem::path assets_dir = "assets/"; // <-- set your build folder
|
||||||
filesystem::path file_path = assets_dir / p;
|
filesystem::path file_path = assets_dir / p;
|
||||||
|
return utils::getFile(file_path);
|
||||||
// If file exists, serve it
|
|
||||||
if (filesystem::exists(file_path)) {
|
|
||||||
auto data = read_file(file_path);
|
|
||||||
auto mime = get_mime_type(file_path.string());
|
|
||||||
return crow::response(200, mime.c_str(), data);
|
|
||||||
}
|
|
||||||
|
|
||||||
// SPA fallback: serve root index.html for unknown routes
|
|
||||||
filesystem::path fallback = build_dir / "index.html";
|
|
||||||
auto data = read_file(fallback);
|
|
||||||
return crow::response(404, "text/html", data);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Catch-all route for static files and SPA fallback
|
// Catch-all route for static files and SPA fallback
|
||||||
CROW_ROUTE(app, "/<path>")
|
CROW_ROUTE(app, "/<path>")
|
||||||
([&](const std::string& p) {
|
([&](const std::string& p) {
|
||||||
filesystem::path file_path = build_dir / p;
|
filesystem::path file_path = utils::build_dir / p;
|
||||||
|
|
||||||
// If path is a directory, serve index.html inside it
|
// If path is a directory, serve index.html inside it
|
||||||
if (filesystem::is_directory(file_path))
|
if (filesystem::is_directory(file_path))
|
||||||
file_path /= "index.html";
|
file_path /= "index.html";
|
||||||
|
|
||||||
// If file exists, serve it
|
return utils::getFile(file_path);
|
||||||
if (filesystem::exists(file_path)) {
|
|
||||||
auto data = read_file(file_path);
|
|
||||||
auto mime = get_mime_type(file_path.string());
|
|
||||||
return crow::response(200, mime.c_str(), data);
|
|
||||||
}
|
|
||||||
|
|
||||||
// SPA fallback: serve root index.html for unknown routes
|
|
||||||
filesystem::path fallback = build_dir / "index.html";
|
|
||||||
auto data = read_file(fallback);
|
|
||||||
return crow::response(404, "text/html", data);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
app.loglevel(crow::LogLevel::INFO);
|
app.loglevel(crow::LogLevel::INFO);
|
||||||
|
|||||||
@ -26,16 +26,26 @@ static std::unordered_map<std::string, std::string> parse_query_string(const std
|
|||||||
return params;
|
return params;
|
||||||
}
|
}
|
||||||
|
|
||||||
static crow::response rsp(const std::string& msg){
|
void initApi(crow::SimpleApp& app){
|
||||||
auto str = format("<div class='alert alert-success'>"
|
|
||||||
"{} </div>", msg);
|
CROW_ROUTE(app, "/assets/shadowrun/<path>")
|
||||||
return crow::response{str};
|
([&](const crow::request& req, const std::string& p) {
|
||||||
}
|
if (!login::isLoggedIn(req)) {
|
||||||
|
return crow::response(401, "Login required");
|
||||||
|
}
|
||||||
|
|
||||||
|
const filesystem::path assets_dir = "assets/shadowrun/";
|
||||||
|
filesystem::path file_path = assets_dir / p;
|
||||||
|
return utils::getFile(file_path);
|
||||||
|
});
|
||||||
|
|
||||||
void initApi(crow::SimpleApp& app)
|
|
||||||
{
|
|
||||||
CROW_ROUTE(app, "/api/shadowrun/characters")
|
CROW_ROUTE(app, "/api/shadowrun/characters")
|
||||||
([&]() {
|
([&](const crow::request& req) {
|
||||||
|
|
||||||
|
if (!login::isLoggedIn(req)) {
|
||||||
|
return crow::response(401, "Login required");
|
||||||
|
}
|
||||||
|
|
||||||
auto characters = getCharacters();
|
auto characters = getCharacters();
|
||||||
auto res =
|
auto res =
|
||||||
crow::response(200, utils::toJsonArray(characters));
|
crow::response(200, utils::toJsonArray(characters));
|
||||||
@ -45,6 +55,10 @@ void initApi(crow::SimpleApp& app)
|
|||||||
|
|
||||||
CROW_ROUTE(app, "/api/shadowrun/characters").methods("POST"_method)
|
CROW_ROUTE(app, "/api/shadowrun/characters").methods("POST"_method)
|
||||||
([](const crow::request& req) {
|
([](const crow::request& req) {
|
||||||
|
if (!login::isLoggedIn(req)) {
|
||||||
|
return crow::response(401, "Login required");
|
||||||
|
}
|
||||||
|
|
||||||
nlohmann::json data = nlohmann::json::parse(req.body); // parse JSON from HTTP body
|
nlohmann::json data = nlohmann::json::parse(req.body); // parse JSON from HTTP body
|
||||||
auto name = data["name"];
|
auto name = data["name"];
|
||||||
int id = createCharacter(name);
|
int id = createCharacter(name);
|
||||||
@ -60,7 +74,10 @@ void initApi(crow::SimpleApp& app)
|
|||||||
});
|
});
|
||||||
|
|
||||||
CROW_ROUTE(app, "/api/shadowrun/characters/<int>")
|
CROW_ROUTE(app, "/api/shadowrun/characters/<int>")
|
||||||
([&](int id) {
|
([&](const crow::request& req, int id) {
|
||||||
|
if (!login::isLoggedIn(req)) {
|
||||||
|
return crow::response(401, "Login required");
|
||||||
|
}
|
||||||
auto optCharacter = getChracter(id);
|
auto optCharacter = getChracter(id);
|
||||||
if (!optCharacter.has_value())
|
if (!optCharacter.has_value())
|
||||||
return crow::response(404, "Character not found");
|
return crow::response(404, "Character not found");
|
||||||
@ -71,7 +88,10 @@ void initApi(crow::SimpleApp& app)
|
|||||||
});
|
});
|
||||||
|
|
||||||
CROW_ROUTE(app, "/api/shadowrun/characters_data/<int>")
|
CROW_ROUTE(app, "/api/shadowrun/characters_data/<int>")
|
||||||
([&](int id) {
|
([&](const crow::request& req, int id) {
|
||||||
|
if (!login::isLoggedIn(req)) {
|
||||||
|
return crow::response(401, "Login required");
|
||||||
|
}
|
||||||
nlohmann::json j;
|
nlohmann::json j;
|
||||||
const auto characterData = getChracterData(id);
|
const auto characterData = getChracterData(id);
|
||||||
|
|
||||||
@ -99,6 +119,9 @@ void initApi(crow::SimpleApp& app)
|
|||||||
|
|
||||||
CROW_ROUTE(app, "/api/shadowrun/characters_data/<int>").methods("POST"_method)
|
CROW_ROUTE(app, "/api/shadowrun/characters_data/<int>").methods("POST"_method)
|
||||||
([&](const crow::request& req, int id) {
|
([&](const crow::request& req, int id) {
|
||||||
|
if (!login::isLoggedIn(req)) {
|
||||||
|
return crow::response(401, "Login required");
|
||||||
|
}
|
||||||
nlohmann::json j = nlohmann::json::parse(req.body);
|
nlohmann::json j = nlohmann::json::parse(req.body);
|
||||||
|
|
||||||
for (auto type : magic_enum::enum_values<Type>()) {
|
for (auto type : magic_enum::enum_values<Type>()) {
|
||||||
@ -110,7 +133,6 @@ void initApi(crow::SimpleApp& app)
|
|||||||
|
|
||||||
auto res = crow::response(200, "Saved Character data");
|
auto res = crow::response(200, "Saved Character data");
|
||||||
return res;
|
return res;
|
||||||
//return crow::response(405, ret.error());
|
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,6 +6,7 @@
|
|||||||
#include <netinet/in.h>
|
#include <netinet/in.h>
|
||||||
#include <netdb.h>
|
#include <netdb.h>
|
||||||
#include "utils.hpp"
|
#include "utils.hpp"
|
||||||
|
#include "crow/http_response.h"
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
@ -141,6 +142,45 @@ string currentTime(){
|
|||||||
return ss.str();
|
return ss.str();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::string read_file(const std::string& path) {
|
||||||
|
std::ifstream file(path, std::ios::binary);
|
||||||
|
if (!file) {
|
||||||
|
throw std::runtime_error("Cannot open file: " + path);
|
||||||
|
}
|
||||||
|
std::ostringstream ss;
|
||||||
|
ss << file.rdbuf();
|
||||||
|
return ss.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simple MIME type detection
|
||||||
|
std::string get_mime_type(const std::string& path) {
|
||||||
|
if (path.ends_with(".html")) return "text/html";
|
||||||
|
if (path.ends_with(".js")) return "text/javascript";
|
||||||
|
if (path.ends_with(".css")) return "text/css";
|
||||||
|
if (path.ends_with(".json")) return "application/json";
|
||||||
|
if (path.ends_with(".svg")) return "image/svg+xml";
|
||||||
|
if (path.ends_with(".png")) return "image/png";
|
||||||
|
if (path.ends_with(".jpg") || path.ends_with(".jpeg")) return "image/jpeg";
|
||||||
|
if (path.ends_with(".woff")) return "font/woff";
|
||||||
|
if (path.ends_with(".woff2")) return "font/woff2";
|
||||||
|
if (path.ends_with(".pdf")) return "application/pdf";
|
||||||
|
return "application/octet-stream";
|
||||||
|
}
|
||||||
|
|
||||||
|
crow::response getFile(const filesystem::path& file_path){
|
||||||
|
// If file exists, serve it
|
||||||
|
if (filesystem::exists(file_path)) {
|
||||||
|
auto data = read_file(file_path);
|
||||||
|
auto mime = get_mime_type(file_path.string());
|
||||||
|
return crow::response(200, mime.c_str(), data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// SPA fallback: serve root index.html for unknown routes
|
||||||
|
filesystem::path fallback = build_dir / "index.html";
|
||||||
|
auto data = read_file(fallback);
|
||||||
|
return crow::response(404, "text/html", data);
|
||||||
|
}
|
||||||
|
|
||||||
std::expected<nlohmann::json, std::string> parseJson(const std::string& input) {
|
std::expected<nlohmann::json, std::string> parseJson(const std::string& input) {
|
||||||
try {
|
try {
|
||||||
return nlohmann::json::parse(input);
|
return nlohmann::json::parse(input);
|
||||||
|
|||||||
@ -5,10 +5,13 @@
|
|||||||
#include <string>
|
#include <string>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <map>
|
|
||||||
#include <expected>
|
#include <expected>
|
||||||
#include "json.hpp"
|
#include "json.hpp"
|
||||||
|
#include "crow.h"
|
||||||
|
|
||||||
namespace utils {
|
namespace utils {
|
||||||
|
const std::filesystem::path build_dir = "frontend/build/"; // <-- set your build folder
|
||||||
|
|
||||||
std::map<std::string, std::string> parseBody(const std::string& body);
|
std::map<std::string, std::string> parseBody(const std::string& body);
|
||||||
|
|
||||||
std::optional<std::string> getBodyName(const std::string& body);
|
std::optional<std::string> getBodyName(const std::string& body);
|
||||||
@ -45,8 +48,10 @@ namespace utils {
|
|||||||
catch (const nlohmann::json::exception& e) {
|
catch (const nlohmann::json::exception& e) {
|
||||||
return std::unexpected(e.what());
|
return std::unexpected(e.what());
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::string read_file(const std::string& path);
|
||||||
|
crow::response getFile(const std::filesystem::path& file_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user