Initial commit.

Signed-off-by: Pavel Kirilin <win10@list.ru>
This commit is contained in:
2023-02-20 01:20:41 +04:00
parent 9a04acd753
commit faae5e4898
39 changed files with 3420 additions and 2 deletions

29
src/server/main.rs Normal file
View File

@ -0,0 +1,29 @@
use std::sync::Arc;
use tokio::sync::RwLock;
use actix_web::{web::Data, App, HttpServer};
use crate::args::ServerConfig;
use super::routes::{healthcheck, index, login};
pub async fn start(args: ServerConfig, token: Arc<RwLock<Option<String>>>) -> anyhow::Result<()> {
let addr = (args.host.clone(), args.port);
HttpServer::new(move || {
App::new()
.wrap(actix_web::middleware::Logger::new(
"\"%r\" \"-\" \"%s\" \"%a\" \"%D\"",
))
.app_data(Data::new(token.clone()))
.app_data(Data::new(args.clone()))
.service(login)
.service(index)
.service(healthcheck)
.service(actix_files::Files::new("/static", args.static_dir.clone()))
})
.bind(addr)?
.workers(1)
.run()
.await?;
Ok(())
}

6
src/server/mod.rs Normal file
View File

@ -0,0 +1,6 @@
mod main;
mod routes;
mod schema;
mod templates;
pub use main::start;

49
src/server/routes.rs Normal file
View File

@ -0,0 +1,49 @@
use std::sync::Arc;
use actix_web::{
get, post,
web::{Data, Json},
HttpResponse, Responder,
};
use tokio::sync::RwLock;
use super::{schema::LoginRequestDTO, templates::Index};
use crate::args::ServerConfig;
#[allow(clippy::unused_async)]
#[get("/health")]
async fn healthcheck() -> impl Responder {
HttpResponse::Ok().finish()
}
#[post("/login")]
pub async fn login(
code_data: Json<LoginRequestDTO>,
token: Data<Arc<RwLock<Option<String>>>>,
config: Data<ServerConfig>,
) -> impl Responder {
if code_data.password != config.server_pass {
log::warn!("Passwords don't match");
return HttpResponse::BadRequest().body("Passwords don't match");
}
if token.read().await.is_some() {
log::warn!("Token is already set.");
return HttpResponse::Conflict().body("Token is already set");
}
let mut token_write = token.write().await;
*token_write = Some(code_data.code.clone());
log::info!("Token is updated!");
HttpResponse::Ok().finish()
}
#[get("/")]
pub async fn index(
token: Data<Arc<RwLock<Option<String>>>>,
config: Data<ServerConfig>,
) -> impl Responder {
Index {
activated: token.read().await.is_some(),
username: config.username.clone(),
image_num: rand::random::<u8>() % 3,
}
}

7
src/server/schema.rs Normal file
View File

@ -0,0 +1,7 @@
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct LoginRequestDTO {
pub code: String,
pub password: String,
}

View File

@ -0,0 +1,181 @@
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="/static/nes.min.css" rel="stylesheet" />
<style>
@font-face {
font-family: 'Press Start 2P';
font-style: normal;
font-weight: 400;
src: url(/static/fonts/ps2p_latin.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
html,
body,
pre,
code,
kbd,
samp {
font-family: "Press Start 2P", serif;
}
</style>
<style>
.center {
position: absolute;
top: 50%;
left: 50%;
margin: 0 -50% 0 0;
transform: translate(-50%, -50%);
z-index: 10000;
}
.large-center-text {
text-align: center;
font-size: large;
}
.at_bottom {
position: absolute;
left: 0;
top: 100%;
font-size: large;
transform: translate(-0%, -100%)
}
html {
height: 100%;
background: #A1FFCE;
/* fallback for old browsers */
background: -webkit-linear-gradient(to right, #FAFFD1, #A1FFCE);
/* Chrome 10-25, Safari 5.1-6 */
background: linear-gradient(to right, #FAFFD1, #A1FFCE);
/* W3C, IE 10+/ Edge, Firefox 16+, Chrome 26+, Opera 12+, Safari 7+ */
}
.mt-15 {
margin-top: 15px;
}
.nes-input[type='submit']:hover {
background-color: #76c442;
}
.nes-input {
cursor: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAABFklEQVRYR9WXURLDIAhE6/0PbSdOtUpcd1Gnpv1KGpTHBpCE1/cXq+vrMph7dGvXZTtpfW10DCA5jrH1H0Jhs5E0hnZdCR+vb5S8Nn8mQCeS9BdSalYJqMBjAGzq59xAESN7VFVUgV8AZB/dZBR7QTFDCqGquvUBVVoEtgIwpQRzmANSFHgWQKExHdIrPeuMvQNDarXe6nC/AutgV3JW+6bgqQLeV8FekRtgV+ToDKEKnACYKsfZjjkam7a0ZpYTytwmgainpC3HvwBocgKOxqRjehoR9DFKNFYtOwCGYCszobeCbl26N6yyQ6g8X/Wex/rBPsNEV6qAMaJPMynIHQCoSqS9JSMmwef51LflTgCRszU7DvAGiV6mHWfsaVUAAAAASUVORK5CYII=), auto;
}
.mtr-2 {
margin-top: 2rem;
}
</style>
<title>Bot activation</title>
</head>
<body>
{% if activated %}
<div class="center large-center-text">
<div>
Hi, there! The bot is running.
</div>
<div>
Just write a ".h" to <a href="tg://resolve?domain={{username}}">@{{username}}</a> in telegram.
</div>
<div class="mtr-2">
If you want to raise an issue or request feature,
</div>
<div>
you can ask <a href="tg://resolve?domain={{username}}">me</a> for that
</div>
</div>
{%else%}
<div class="center large-center-text">
<div>
Welcome back, {{ username }}!
</div>
<dialog class="nes-dialog is-error" id="dialog-error">
<form method="dialog">
<p class="title">Failed to authorize.</p>
<p id="error_description"></p>
<menu class="dialog-menu">
<button class="nes-btn is-primary">Confirm</button>
</menu>
</form>
</dialog>
<dialog class="nes-dialog" id="dialog-success">
<form method="dialog">
<p class="title">Successfully authorized</p>
<p>You were successfully authorized.</p>
<menu class="dialog-menu">
<button class="nes-btn is-primary" onclick="location.reload()">Confirm</button>
</menu>
</form>
</dialog>
<form id="code_form" action="/code" method="post" class="mtr-2" autocomplete="off">
<div class="nes-field is-inline">
<label for="code_field">Verification code</label>
<input type="text" id="code_field" class="nes-input is-success" name="code" placeholder="Your code">
</div>
<div class="nes-field is-inline mt-15">
<label for="pass_field">Password</label>
<input type="password" id="pass_field" class="nes-input is-success" name="password"
placeholder="Password">
</div>
<div class="nes-field mt-15">
<input type="submit" class="nes-input is-success" value="Submit code">
</div>
</form>
</div>
{% endif %}
<img src="/static/images/girl_{{image_num}}.png" alt="" class="at_bottom">
</body>
{% if !activated %}
<script>
function authorize(form) {
// Bind the FormData object and the form element
const form_data = new FormData(form);
// Construct request JSON.
let request_json = {};
form_data.forEach((value, key) => {
request_json[key] = value;
});
fetch("/login", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(request_json),
}).then(async response => {
if (response.status == 200) {
document.getElementById('dialog-success').showModal();
return;
}
let desc = document.getElementById('error_description');
desc.innerText = await response.text();
document.getElementById('dialog-error').showModal();
});
}
const form = document.getElementById("code_form");
// ...and take over its submit event.
form.addEventListener("submit", function (event) {
event.preventDefault();
authorize(form);
});
</script>
{% endif %}
</html>

View File

@ -0,0 +1,9 @@
use askama::Template;
#[derive(Template)]
#[template(path = "index.html")]
pub struct Index {
pub activated: bool,
pub username: String,
pub image_num: u8,
}