29
src/server/main.rs
Normal file
29
src/server/main.rs
Normal 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
6
src/server/mod.rs
Normal file
@ -0,0 +1,6 @@
|
||||
mod main;
|
||||
mod routes;
|
||||
mod schema;
|
||||
mod templates;
|
||||
|
||||
pub use main::start;
|
49
src/server/routes.rs
Normal file
49
src/server/routes.rs
Normal 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
7
src/server/schema.rs
Normal file
@ -0,0 +1,7 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct LoginRequestDTO {
|
||||
pub code: String,
|
||||
pub password: String,
|
||||
}
|
181
src/server/templates/index.html
Normal file
181
src/server/templates/index.html
Normal 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>
|
9
src/server/templates/mod.rs
Normal file
9
src/server/templates/mod.rs
Normal 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,
|
||||
}
|
Reference in New Issue
Block a user