Initial commit
4
.dockerignore
Normal file
|
@ -0,0 +1,4 @@
|
|||
/.git
|
||||
/target
|
||||
/.editorconfig
|
||||
/.gitignore
|
11
.editorconfig
Normal file
|
@ -0,0 +1,11 @@
|
|||
root = true
|
||||
|
||||
[*]
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[*.rs]
|
||||
indent_size = 4
|
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
/target
|
1608
Cargo.lock
generated
Normal file
12
Cargo.toml
Normal file
|
@ -0,0 +1,12 @@
|
|||
[package]
|
||||
name = "comfycamp"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
tokio = { version = "1.41.0", features = ["full"] }
|
||||
axum = "0.7.7"
|
||||
tera = "1.20.0"
|
||||
lazy_static = "1.5.0"
|
||||
tower-http = { version = "0.6.1", features = ["fs"] }
|
||||
notify = "7.0.0"
|
15
Dockerfile
Normal file
|
@ -0,0 +1,15 @@
|
|||
FROM rust:1.82-slim-bookworm AS builder
|
||||
|
||||
WORKDIR /usr/src/comfycamp
|
||||
COPY . .
|
||||
RUN cargo install --path .
|
||||
|
||||
FROM debian:bookworm-slim
|
||||
|
||||
WORKDIR /usr/src/comfycamp
|
||||
RUN mkdir -p src/web
|
||||
COPY src/web/assets src/web/assets
|
||||
COPY src/web/templates src/web/templates
|
||||
|
||||
COPY --from=builder /usr/local/cargo/bin/comfycamp /usr/local/bin/comfycamp
|
||||
CMD ["comfycamp"]
|
0
src/internal/mod.rs
Normal file
41
src/main.rs
Normal file
|
@ -0,0 +1,41 @@
|
|||
use axum::{routing::get, Router};
|
||||
use tower_http::services::ServeDir;
|
||||
use tokio::signal;
|
||||
|
||||
use crate::web::controllers;
|
||||
|
||||
mod internal;
|
||||
mod web;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let app = Router::new()
|
||||
.route("/", get(controllers::home))
|
||||
.nest_service("/assets", ServeDir::new("src/web/assets"));
|
||||
let listener = tokio::net::TcpListener::bind("0.0.0.0:4000").await.unwrap();
|
||||
|
||||
axum::serve(listener, app)
|
||||
.with_graceful_shutdown(shutdown_signal())
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
async fn shutdown_signal() {
|
||||
let ctrl_c = async {
|
||||
signal::ctrl_c()
|
||||
.await
|
||||
.expect("failed to start CTRL+C handler");
|
||||
};
|
||||
|
||||
let terminate = async {
|
||||
signal::unix::signal(signal::unix::SignalKind::terminate())
|
||||
.expect("failed to install signal handler")
|
||||
.recv()
|
||||
.await;
|
||||
};
|
||||
|
||||
tokio::select!(
|
||||
_ = ctrl_c => {},
|
||||
_ = terminate => {},
|
||||
);
|
||||
}
|
36
src/web/assets/css/home.css
Normal file
|
@ -0,0 +1,36 @@
|
|||
.service {
|
||||
margin-top: 28px;
|
||||
}
|
||||
|
||||
.service .title {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.service .title h3 {
|
||||
margin-right: 8px;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.service .tag {
|
||||
font-size: 12px;
|
||||
background-color: #333;
|
||||
padding: 4px 6px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.service .link {
|
||||
color: var(--accent);
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.service svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
76
src/web/assets/css/layout.css
Normal file
|
@ -0,0 +1,76 @@
|
|||
:root {
|
||||
--bg: #13151a;
|
||||
--accent: #b283e5;
|
||||
--input-bg: #28253c;
|
||||
--input-border: #4c4c6d;
|
||||
}
|
||||
|
||||
html {
|
||||
font-family: Georgia, serif;
|
||||
background-color: var(--bg);
|
||||
color: white;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
*::selection {
|
||||
background-color: var(--accent);
|
||||
color: var(--bg);
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--accent);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin-top: 36px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin-top: 32px;
|
||||
}
|
||||
|
||||
nav,
|
||||
main,
|
||||
footer {
|
||||
max-width: 800px;
|
||||
width: 100%;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
nav {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
padding-top: 16px;
|
||||
}
|
||||
|
||||
nav .space {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
footer {
|
||||
margin-top: 32px;
|
||||
}
|
||||
|
||||
footer .link-row {
|
||||
margin-top: 12px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
footer .link-row a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
BIN
src/web/assets/favicons/16.png
Normal file
After Width: | Height: | Size: 650 B |
BIN
src/web/assets/favicons/180.png
Normal file
After Width: | Height: | Size: 2.2 KiB |
BIN
src/web/assets/favicons/192.png
Normal file
After Width: | Height: | Size: 2.2 KiB |
BIN
src/web/assets/favicons/32.png
Normal file
After Width: | Height: | Size: 858 B |
BIN
src/web/assets/favicons/shortcut.ico
Normal file
After Width: | Height: | Size: 7.2 KiB |
1
src/web/assets/favicons/vector.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg width="512" height="512" viewBox="0 0 135.467 135.467" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path style="fill:#2b0759;fill-opacity:1;stroke:none;stroke-width:1.8432;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1" d="M0 0h135.467v135.467H0z"/><path d="m34.467 56.644 33.266-25.873L101 56.644v40.66a7.393 7.393 0 0 1-7.393 7.392H41.86a7.393 7.393 0 0 1-7.393-7.393z" style="fill:none;stroke:#fff;stroke-width:12.793;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"/></svg>
|
After Width: | Height: | Size: 541 B |
1
src/web/assets/icons/code.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#FFFFFF"><path d="M320-240 80-480l240-240 57 57-184 184 183 183-56 56Zm320 0-57-57 184-184-183-183 56-56 240 240-240 240Z"/></svg>
|
After Width: | Height: | Size: 229 B |
1
src/web/assets/icons/external.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#FFFFFF"><path d="M200-120q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h280v80H200v560h560v-280h80v280q0 33-23.5 56.5T760-120H200Zm188-212-56-56 372-372H560v-80h280v280h-80v-144L388-332Z"/></svg>
|
After Width: | Height: | Size: 301 B |
1
src/web/assets/icons/mail.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#FFFFFF"><path d="M160-160q-33 0-56.5-23.5T80-240v-480q0-33 23.5-56.5T160-800h640q33 0 56.5 23.5T880-720v480q0 33-23.5 56.5T800-160H160Zm320-280L160-640v400h640v-400L480-440Zm0-80 320-200H160l320 200ZM160-640v-80 480-400Z"/></svg>
|
After Width: | Height: | Size: 328 B |
1
src/web/assets/icons/mastodon.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" fill="#FFFFFF"><path d="M23.268 5.313c-.35-2.578-2.617-4.61-5.304-5.004C17.51.242 15.792 0 11.813 0h-.03c-3.98 0-4.835.242-5.288.309C3.882.692 1.496 2.518.917 5.127.64 6.412.61 7.837.661 9.143c.074 1.874.088 3.745.26 5.611.118 1.24.325 2.47.62 3.68.55 2.237 2.777 4.098 4.96 4.857 2.336.792 4.849.923 7.256.38.265-.061.527-.132.786-.213.585-.184 1.27-.39 1.774-.753a.057.057 0 0 0 .023-.043v-1.809a.052.052 0 0 0-.02-.041.053.053 0 0 0-.046-.01 20.282 20.282 0 0 1-4.709.545c-2.73 0-3.463-1.284-3.674-1.818a5.593 5.593 0 0 1-.319-1.433.053.053 0 0 1 .066-.054c1.517.363 3.072.546 4.632.546.376 0 .75 0 1.125-.01 1.57-.044 3.224-.124 4.768-.422.038-.008.077-.015.11-.024 2.435-.464 4.753-1.92 4.989-5.604.008-.145.03-1.52.03-1.67.002-.512.167-3.63-.024-5.545zm-3.748 9.195h-2.561V8.29c0-1.309-.55-1.976-1.67-1.976-1.23 0-1.846.79-1.846 2.35v3.403h-2.546V8.663c0-1.56-.617-2.35-1.848-2.35-1.112 0-1.668.668-1.67 1.977v6.218H4.822V8.102c0-1.31.337-2.35 1.011-3.12.696-.77 1.608-1.164 2.74-1.164 1.311 0 2.302.5 2.962 1.498l.638 1.06.638-1.06c.66-.999 1.65-1.498 2.96-1.498 1.13 0 2.043.395 2.74 1.164.675.77 1.012 1.81 1.012 3.12z"/></svg>
|
After Width: | Height: | Size: 1.2 KiB |
1
src/web/assets/icons/matrix.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" fill="#FFFFFF"><path d="M.632.55v22.9H2.28V24H0V0h2.28v.55zm7.043 7.26v1.157h.033c.309-.443.683-.784 1.117-1.024.433-.245.936-.365 1.5-.365.54 0 1.033.107 1.481.314.448.208.785.582 1.02 1.108.254-.374.6-.706 1.034-.992.434-.287.95-.43 1.546-.43.453 0 .872.056 1.26.167.388.11.716.286.993.53.276.245.489.559.646.951.152.392.23.863.23 1.417v5.728h-2.349V11.52c0-.286-.01-.559-.032-.812a1.755 1.755 0 0 0-.18-.66 1.106 1.106 0 0 0-.438-.448c-.194-.11-.457-.166-.785-.166-.332 0-.6.064-.803.189a1.38 1.38 0 0 0-.48.499 1.946 1.946 0 0 0-.231.696 5.56 5.56 0 0 0-.06.785v4.768h-2.35v-4.8c0-.254-.004-.503-.018-.752a2.074 2.074 0 0 0-.143-.688 1.052 1.052 0 0 0-.415-.503c-.194-.125-.476-.19-.854-.19-.111 0-.259.024-.439.074-.18.051-.36.143-.53.282-.171.138-.319.337-.439.595-.12.259-.18.6-.18 1.02v4.966H5.46V7.81zm15.693 15.64V.55H21.72V0H24v24h-2.28v-.55z"/></svg>
|
After Width: | Height: | Size: 923 B |
1
src/web/assets/icons/xmpp.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" fill="#FFFFFF"><path d="m3.401 4.594 1.025.366 3.08.912c-.01.18-.016.361-.016.543 0 3.353 1.693 7.444 4.51 10.387 2.817-2.943 4.51-7.034 4.51-10.387 0-.182-.006-.363-.016-.543l3.08-.912 1.025-.366L24 3.276C23.854 8.978 19.146 14.9 13.502 18.17c1.302 1.028 2.778 1.81 4.388 2.215v.114l.004.001v.224a14.55 14.55 0 0 1-4.829-1.281A20.909 20.909 0 0 1 12 18.966c-.353.17-.708.329-1.065.477a14.55 14.55 0 0 1-4.829 1.281V20.5l.004-.001v-.113c1.61-.406 3.086-1.188 4.389-2.216C4.854 14.9.146 8.978 0 3.276l3.401 1.318Z"/></svg>
|
After Width: | Height: | Size: 582 B |
6
src/web/controllers/home.rs
Normal file
|
@ -0,0 +1,6 @@
|
|||
use axum::response::Html;
|
||||
use crate::web::templates;
|
||||
|
||||
pub async fn home() -> Html<String> {
|
||||
templates::render("home.html", &tera::Context::new())
|
||||
}
|
3
src/web/controllers/mod.rs
Normal file
|
@ -0,0 +1,3 @@
|
|||
pub mod home;
|
||||
|
||||
pub use home::home;
|
2
src/web/mod.rs
Normal file
|
@ -0,0 +1,2 @@
|
|||
pub mod controllers;
|
||||
pub mod templates;
|
29
src/web/templates.rs
Normal file
|
@ -0,0 +1,29 @@
|
|||
use lazy_static::lazy_static;
|
||||
use axum::response::Html;
|
||||
use tera::Tera;
|
||||
|
||||
lazy_static! {
|
||||
pub static ref TEMPLATES: Tera = {
|
||||
let mut tera = match Tera::new("src/web/templates/**/*") {
|
||||
Ok(t) => t,
|
||||
Err(e) => {
|
||||
println!("Templates parsing error(s): {}", e);
|
||||
::std::process::exit(1);
|
||||
}
|
||||
};
|
||||
tera.autoescape_on(vec![".html"]);
|
||||
tera
|
||||
};
|
||||
}
|
||||
|
||||
/// Render HTML template.
|
||||
///
|
||||
/// Parameters:
|
||||
/// - `name`: html file path relative to `templates` dir.
|
||||
/// - `ctx`: context with parameters for rendering.
|
||||
pub fn render(name: &str, ctx: &tera::Context) -> Html<String> {
|
||||
match TEMPLATES.render(name, ctx) {
|
||||
Ok(res) => Html(res),
|
||||
Err(err) => Html(err.to_string()),
|
||||
}
|
||||
}
|
85
src/web/templates/home.html
Normal file
|
@ -0,0 +1,85 @@
|
|||
{% extends "layout.html" %}
|
||||
|
||||
{% block head %}
|
||||
<link rel="stylesheet" href="/assets/css/home.css" />
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Уютный домик</h1>
|
||||
|
||||
<p>
|
||||
У меня есть несколько проектов, запущенных на домашнем сервере.
|
||||
Я буду рад, если они будут полезны другим людям.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Создайте один аккаунт, чтобы получить доступ к большинству сервисов.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Не стоит ожидать большой надёжности, однако я прикладываю все усилия,
|
||||
чтобы сервисы были доступны 24/7.
|
||||
</p>
|
||||
|
||||
<p><i>- Иван, администратор comfycamp.space</i></p>
|
||||
|
||||
<div class="service">
|
||||
<div class="title">
|
||||
<h3>Mastodon</h3>
|
||||
</div>
|
||||
<a class="link" href="https://m.comfycamp.space" target="_blank">
|
||||
m.comfycamp.space <img src="/assets/icons/external.svg" width="20" height="20" alt="" />
|
||||
</a>
|
||||
<p>
|
||||
Микроблоги с поддержкой fediverse.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="service">
|
||||
<div class="title">
|
||||
<h3>Peertube</h3>
|
||||
</div>
|
||||
<a class="link" href="https://v.comfycamp.space" target="_blank">
|
||||
v.comfycamp.space <img src="/assets/icons/external.svg" width="20" height="20" alt="" />
|
||||
</a>
|
||||
<p>
|
||||
Видеохостинг, альтернатива YouTube.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="service">
|
||||
<div class="title">
|
||||
<h3>Matrix</h3>
|
||||
</div>
|
||||
<a class="link">
|
||||
matrix.comfycamp.space
|
||||
</a>
|
||||
<p>
|
||||
Современный протокол для общения.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="service">
|
||||
<div class="title">
|
||||
<h3>Forgejo</h3>
|
||||
</div>
|
||||
<a class="link" href="https://git.comfycamp.space" target="_blank">
|
||||
git.comfycamp.space <img src="/assets/icons/external.svg" width="20" height="20" alt="" />
|
||||
</a>
|
||||
<p>
|
||||
Хостинг для git-проектов.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="service">
|
||||
<div class="title">
|
||||
<h3>Nextcloud</h3>
|
||||
</div>
|
||||
<a class="link" href="https://nc.comfycamp.space" target="_blank">
|
||||
nc.comfycamp.space <img src="/assets/icons/external.svg" width="20" height="20" alt="" />
|
||||
</a>
|
||||
<p>
|
||||
Облако, календарь, задачи.
|
||||
</p>
|
||||
</div>
|
||||
{% endblock %}
|
60
src/web/templates/layout.html
Normal file
|
@ -0,0 +1,60 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Comfy Camp</title>
|
||||
<link rel="stylesheet" href="/assets/css/layout.css" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="icon" type="image/svg+xml" href="/assets/favicons/vector.svg" />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/assets/favicons/16.png" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/assets/favicons/32.png" />
|
||||
<link rel="apple-touch-icon" type="image/png" href="/assets/favicons/180.png" />
|
||||
<link rel="icon" type="image/png" sizes="192x192" href="/assets/favicons/192.png" />
|
||||
<link rel="shortcut icon" type="image/x-icon" href="/assets/favicons/shortcut.ico" />
|
||||
{% block head %}{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
<nav>
|
||||
<a href="/">Главная</a>
|
||||
<div class="space"></div>
|
||||
<a href="https://auth.comfycamp.space/if/flow/enrollment/">Зарегистрироваться</a>
|
||||
</nav>
|
||||
|
||||
<main>
|
||||
<div>
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<div class="link-row">
|
||||
<a href="https://git.comfycamp.space/lumin/comfycamp">
|
||||
<img src="/assets/icons/code.svg" height="20" width="20" alt="" />
|
||||
Исходный код
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="link-row">
|
||||
<a href="https://m.comfycamp.space/@lumin" rel="me">
|
||||
<img src="/assets/icons/mastodon.svg" height="20" width="20" alt="" />
|
||||
Mastodon
|
||||
</a>
|
||||
<a href="mailto:admin@comfycamp.space">
|
||||
<img src="/assets/icons/mail.svg" height="20" width="20" alt="" />
|
||||
admin@comfycamp.space
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="link-row">
|
||||
<a href="https://matrix.to/#/@lumin:matrix.comfycamp.space">
|
||||
<img src="/assets/icons/matrix.svg" height="20" width="20" alt="" />
|
||||
@lumin:matrix.comfycamp.space
|
||||
</a>
|
||||
<a href="xmpp://lumin@xmpp.comfycamp.space">
|
||||
<img src="/assets/icons/xmpp.svg" height="20" width="20" alt="" />
|
||||
lumin@xmpp.comfycamp.space
|
||||
</a>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|