add: littleweb base
This commit is contained in:
parent
07a23f505b
commit
c4de17058b
20 changed files with 457 additions and 8 deletions
|
@ -35,6 +35,8 @@ A `docs` directory will be generated in the same directory that you ran the `tet
|
|||
|
||||
You can configure your port through the `port` key of the configuration file. You can also run the server with the `PORT` environment variable, which will override whatever is set in the configuration file. You can also use the `CACHE_BREAKER` environment variable to specify a version number to be used in static asset links in order to break cache entries.
|
||||
|
||||
You can launch with the `LITTLEWEB=true` environment variable to start the littleweb viewer/fake DNS server. This should be used in combination with `PORT`, as well as set as the `lw_host` in your configuration file. This secondary server is required to allow users to view their littleweb projects.
|
||||
|
||||
## Usage (as a user)
|
||||
|
||||
Tetratto is very simple once you get the hang of it! At the top of the page (or bottom if you're on mobile), you'll see the navigation bar. Once logged in, you'll be able to access "Home", "Popular", and "Communities" from there! You can also press your profile picture (on the right) to view your own profile, settings, or log out!
|
||||
|
|
|
@ -113,9 +113,22 @@ async fn main() {
|
|||
tera.register_filter("emojis", render_emojis);
|
||||
|
||||
let client = Client::new();
|
||||
let mut app = Router::new();
|
||||
|
||||
let app = Router::new()
|
||||
.merge(routes::routes(&config))
|
||||
// add correct routes
|
||||
if var("LITTLEWEB").is_ok() {
|
||||
app = app.merge(routes::lw_routes());
|
||||
} else {
|
||||
app = app
|
||||
.merge(routes::routes(&config))
|
||||
.layer(SetResponseHeaderLayer::if_not_present(
|
||||
HeaderName::from_static("content-security-policy"),
|
||||
HeaderValue::from_static("default-src 'self' *.spotify.com musicbrainz.org; img-src * data:; media-src *; font-src *; style-src 'unsafe-inline' 'self' *; script-src 'self' 'unsafe-inline' *; object-src 'self' *; upgrade-insecure-requests; connect-src * localhost; frame-src 'self' *.cloudflare.com; frame-ancestors 'self'"),
|
||||
));
|
||||
}
|
||||
|
||||
// add junk
|
||||
app = app
|
||||
.layer(Extension(Arc::new(RwLock::new((database, tera, client)))))
|
||||
.layer(axum::extract::DefaultBodyLimit::max(
|
||||
var("BODY_LIMIT")
|
||||
|
@ -128,12 +141,9 @@ async fn main() {
|
|||
.make_span_with(trace::DefaultMakeSpan::new().level(Level::INFO))
|
||||
.on_response(trace::DefaultOnResponse::new().level(Level::INFO)),
|
||||
)
|
||||
.layer(SetResponseHeaderLayer::if_not_present(
|
||||
HeaderName::from_static("content-security-policy"),
|
||||
HeaderValue::from_static("default-src 'self' *.spotify.com musicbrainz.org; img-src * data:; media-src *; font-src *; style-src 'unsafe-inline' 'self' *; script-src 'self' 'unsafe-inline' *; object-src 'self' *; upgrade-insecure-requests; connect-src * localhost; frame-src 'self' *.cloudflare.com; frame-ancestors 'self'"),
|
||||
))
|
||||
.layer(CatchPanicLayer::new());
|
||||
|
||||
// ...
|
||||
let listener = tokio::net::TcpListener::bind(format!("0.0.0.0:{}", config.port))
|
||||
.await
|
||||
.unwrap();
|
||||
|
|
|
@ -1277,11 +1277,22 @@ ${option.input_element_type === "textarea" ? `${option.value}</textarea>` : ""}
|
|||
}
|
||||
|
||||
if (
|
||||
text.includes(`!<!-- observer_disconnect_${window.BUILD_CODE} -->`)
|
||||
text.includes(
|
||||
`!<!-- observer_disconnect_${window.BUILD_CODE} -->`,
|
||||
) ||
|
||||
document.documentElement.innerHTML.includes("observer_disconnect")
|
||||
) {
|
||||
console.log("io_data_end; disconnect");
|
||||
self.IO_DATA_OBSERVER.disconnect();
|
||||
self.IO_DATA_ELEMENT.innerHTML += text;
|
||||
|
||||
if (
|
||||
!document.documentElement.innerHTML.includes(
|
||||
"observer_disconnect",
|
||||
)
|
||||
) {
|
||||
self.IO_DATA_ELEMENT.innerHTML += text;
|
||||
}
|
||||
|
||||
self.IO_DATA_DISCONNECTED = true;
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -207,6 +207,58 @@ pub async fn stripe_webhook(
|
|||
return Json(e.into());
|
||||
}
|
||||
}
|
||||
EventType::InvoicePaymentFailed => {
|
||||
// payment failed
|
||||
let subscription = match req.data.object {
|
||||
EventObject::Subscription(c) => c,
|
||||
_ => unreachable!("cannot be this"),
|
||||
};
|
||||
|
||||
let customer_id = subscription.customer.id();
|
||||
|
||||
let user = match data.get_user_by_stripe_id(customer_id.as_str()).await {
|
||||
Ok(ua) => ua,
|
||||
Err(e) => return Json(e.into()),
|
||||
};
|
||||
|
||||
tracing::info!(
|
||||
"unsubscribe (pay fail) {} (stripe: {})",
|
||||
user.id,
|
||||
customer_id
|
||||
);
|
||||
let new_user_permissions = user.permissions - FinePermission::SUPPORTER;
|
||||
|
||||
if let Err(e) = data
|
||||
.update_user_role(user.id, new_user_permissions, user.clone(), true)
|
||||
.await
|
||||
{
|
||||
return Json(e.into());
|
||||
}
|
||||
|
||||
if data.0.0.security.enable_invite_codes && user.was_purchased && user.invite_code == 0
|
||||
{
|
||||
// user doesn't come from an invite code, and is a purchased account
|
||||
// this means their account must be locked if they stop paying
|
||||
if let Err(e) = data
|
||||
.update_user_awaiting_purchased_status(user.id, true, user.clone(), false)
|
||||
.await
|
||||
{
|
||||
return Json(e.into());
|
||||
}
|
||||
}
|
||||
|
||||
if let Err(e) = data
|
||||
.create_notification(Notification::new(
|
||||
"It seems your recent payment has failed :(".to_string(),
|
||||
"No worries! Your subscription is still active and will be retried. Your supporter status will resume when you have a successful payment."
|
||||
.to_string(),
|
||||
user.id,
|
||||
))
|
||||
.await
|
||||
{
|
||||
return Json(e.into());
|
||||
}
|
||||
}
|
||||
_ => return Json(Error::Unknown.into()),
|
||||
}
|
||||
|
||||
|
|
|
@ -635,6 +635,10 @@ pub fn routes() -> Router {
|
|||
.route("/layouts/{id}/pages", post(layouts::update_pages_request))
|
||||
}
|
||||
|
||||
pub fn lw_routes() -> Router {
|
||||
Router::new()
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct LoginProps {
|
||||
pub username: String,
|
||||
|
|
|
@ -46,3 +46,14 @@ pub fn routes(config: &Config) -> Router {
|
|||
// pages
|
||||
.merge(pages::routes())
|
||||
}
|
||||
|
||||
/// These routes are only used when you provide the `LITTLEWEB` environment variable.
|
||||
///
|
||||
/// These routes are NOT for editing. These routes are only for viewing littleweb sites.
|
||||
pub fn lw_routes() -> Router {
|
||||
Router::new()
|
||||
// api
|
||||
.nest("/api/v1", api::v1::lw_routes())
|
||||
// pages
|
||||
.merge(pages::lw_routes())
|
||||
}
|
||||
|
|
|
@ -141,6 +141,10 @@ pub fn routes() -> Router {
|
|||
.route("/x/{note}", get(journals::global_view_request))
|
||||
}
|
||||
|
||||
pub fn lw_routes() -> Router {
|
||||
Router::new()
|
||||
}
|
||||
|
||||
pub async fn render_error(
|
||||
e: Error,
|
||||
jar: &CookieJar,
|
||||
|
|
|
@ -252,6 +252,10 @@ pub struct Config {
|
|||
/// so this host should be included in there as well.
|
||||
#[serde(default = "default_host")]
|
||||
pub host: String,
|
||||
/// The main public host of the littleweb server. **Not** used to check against banned hosts,
|
||||
/// so this host should be included in there as well.
|
||||
#[serde(default = "default_lw_host")]
|
||||
pub lw_host: String,
|
||||
/// Database security.
|
||||
#[serde(default = "default_security")]
|
||||
pub security: SecurityConfig,
|
||||
|
@ -319,6 +323,10 @@ fn default_host() -> String {
|
|||
String::new()
|
||||
}
|
||||
|
||||
fn default_lw_host() -> String {
|
||||
String::new()
|
||||
}
|
||||
|
||||
fn default_security() -> SecurityConfig {
|
||||
SecurityConfig::default()
|
||||
}
|
||||
|
@ -385,6 +393,7 @@ impl Default for Config {
|
|||
port: default_port(),
|
||||
banned_hosts: default_banned_hosts(),
|
||||
host: default_host(),
|
||||
lw_host: default_lw_host(),
|
||||
database: default_database(),
|
||||
security: default_security(),
|
||||
dirs: default_dirs(),
|
||||
|
|
|
@ -40,6 +40,9 @@ impl DataManager {
|
|||
execute!(&conn, common::CREATE_TABLE_NOTES).unwrap();
|
||||
execute!(&conn, common::CREATE_TABLE_MESSAGE_REACTIONS).unwrap();
|
||||
execute!(&conn, common::CREATE_TABLE_INVITE_CODES).unwrap();
|
||||
execute!(&conn, common::CREATE_TABLE_LAYOUTS).unwrap();
|
||||
execute!(&conn, common::CREATE_TABLE_DOMAINS).unwrap();
|
||||
execute!(&conn, common::CREATE_TABLE_SERVICES).unwrap();
|
||||
|
||||
self.0
|
||||
.1
|
||||
|
|
153
crates/core/src/database/domains.rs
Normal file
153
crates/core/src/database/domains.rs
Normal file
|
@ -0,0 +1,153 @@
|
|||
use crate::model::{
|
||||
auth::User,
|
||||
littleweb::{Domain, DomainData, DomainTld},
|
||||
permissions::{FinePermission, SecondaryPermission},
|
||||
Error, Result,
|
||||
};
|
||||
use crate::{auto_method, DataManager};
|
||||
use oiseau::{cache::Cache, execute, get, params, query_row, query_rows, PostgresRow};
|
||||
|
||||
impl DataManager {
|
||||
/// Get a [`Domain`] from an SQL row.
|
||||
pub(crate) fn get_domain_from_row(x: &PostgresRow) -> Domain {
|
||||
Domain {
|
||||
id: get!(x->0(i64)) as usize,
|
||||
created: get!(x->1(i64)) as usize,
|
||||
owner: get!(x->2(i64)) as usize,
|
||||
name: get!(x->3(String)),
|
||||
tld: (get!(x->4(String)).as_str()).into(),
|
||||
data: serde_json::from_str(&get!(x->5(String))).unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
auto_method!(get_domain_by_id(usize as i64)@get_domain_from_row -> "SELECT * FROM domains WHERE id = $1" --name="domain" --returns=Domain --cache-key-tmpl="atto.domain:{}");
|
||||
|
||||
/// Get a domain given its name and TLD.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `name`
|
||||
/// * `tld`
|
||||
pub async fn get_domain_by_name_tld(&self, name: &str, tld: &DomainTld) -> Result<Domain> {
|
||||
let conn = match self.0.connect().await {
|
||||
Ok(c) => c,
|
||||
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
|
||||
};
|
||||
|
||||
let res = query_row!(
|
||||
&conn,
|
||||
"SELECT * FROM domains WHERE name = $1 AND tld = $2",
|
||||
&[&name, &tld.to_string()],
|
||||
|x| { Ok(Self::get_domain_from_row(x)) }
|
||||
);
|
||||
|
||||
if res.is_err() {
|
||||
return Err(Error::GeneralNotFound("domain".to_string()));
|
||||
}
|
||||
|
||||
Ok(res.unwrap())
|
||||
}
|
||||
|
||||
/// Get all domains by user.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `id` - the ID of the user to fetch domains for
|
||||
pub async fn get_domains_by_user(&self, id: usize) -> Result<Vec<Domain>> {
|
||||
let conn = match self.0.connect().await {
|
||||
Ok(c) => c,
|
||||
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
|
||||
};
|
||||
|
||||
let res = query_rows!(
|
||||
&conn,
|
||||
"SELECT * FROM domains WHERE owner = $1 ORDER BY created DESC",
|
||||
&[&(id as i64)],
|
||||
|x| { Self::get_domain_from_row(x) }
|
||||
);
|
||||
|
||||
if res.is_err() {
|
||||
return Err(Error::GeneralNotFound("domain".to_string()));
|
||||
}
|
||||
|
||||
Ok(res.unwrap())
|
||||
}
|
||||
|
||||
/// Create a new domain in the database.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `data` - a mock [`Domain`] object to insert
|
||||
pub async fn create_domain(&self, data: Domain) -> Result<Domain> {
|
||||
// check values
|
||||
if data.name.len() < 2 {
|
||||
return Err(Error::DataTooShort("name".to_string()));
|
||||
} else if data.name.len() > 128 {
|
||||
return Err(Error::DataTooLong("name".to_string()));
|
||||
}
|
||||
|
||||
// check for existing
|
||||
if self
|
||||
.get_domain_by_name_tld(&data.name, &data.tld)
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
return Err(Error::MiscError(
|
||||
"Domain + TLD already in use. Maybe try another TLD!".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
// ...
|
||||
let conn = match self.0.connect().await {
|
||||
Ok(c) => c,
|
||||
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
|
||||
};
|
||||
|
||||
let res = execute!(
|
||||
&conn,
|
||||
"INSERT INTO domains VALUES ($1, $2, $3, $4, $5, $6)",
|
||||
params![
|
||||
&(data.id as i64),
|
||||
&(data.created as i64),
|
||||
&(data.owner as i64),
|
||||
&data.name,
|
||||
&data.tld.to_string(),
|
||||
&serde_json::to_string(&data.data).unwrap(),
|
||||
]
|
||||
);
|
||||
|
||||
if let Err(e) = res {
|
||||
return Err(Error::DatabaseError(e.to_string()));
|
||||
}
|
||||
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
pub async fn delete_domain(&self, id: usize, user: &User) -> Result<()> {
|
||||
let domain = self.get_domain_by_id(id).await?;
|
||||
|
||||
// check user permission
|
||||
if user.id != domain.owner
|
||||
&& !user
|
||||
.secondary_permissions
|
||||
.check(SecondaryPermission::MANAGE_DOMAINS)
|
||||
{
|
||||
return Err(Error::NotAllowed);
|
||||
}
|
||||
|
||||
// ...
|
||||
let conn = match self.0.connect().await {
|
||||
Ok(c) => c,
|
||||
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
|
||||
};
|
||||
|
||||
let res = execute!(&conn, "DELETE FROM domains WHERE id = $1", &[&(id as i64)]);
|
||||
|
||||
if let Err(e) = res {
|
||||
return Err(Error::DatabaseError(e.to_string()));
|
||||
}
|
||||
|
||||
// ...
|
||||
self.0.1.remove(format!("atto.domain:{}", id)).await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
auto_method!(update_domain_data(Vec<(String, DomainData)>)@get_domain_by_id:FinePermission::MANAGE_USERS; -> "UPDATE domains SET data = $1 WHERE id = $2" --serde --cache-key-tmpl="atto.domain:{}");
|
||||
}
|
|
@ -27,3 +27,6 @@ pub const CREATE_TABLE_JOURNALS: &str = include_str!("./sql/create_journals.sql"
|
|||
pub const CREATE_TABLE_NOTES: &str = include_str!("./sql/create_notes.sql");
|
||||
pub const CREATE_TABLE_MESSAGE_REACTIONS: &str = include_str!("./sql/create_message_reactions.sql");
|
||||
pub const CREATE_TABLE_INVITE_CODES: &str = include_str!("./sql/create_invite_codes.sql");
|
||||
pub const CREATE_TABLE_LAYOUTS: &str = include_str!("./sql/create_layouts.sql");
|
||||
pub const CREATE_TABLE_DOMAINS: &str = include_str!("./sql/create_domains.sql");
|
||||
pub const CREATE_TABLE_SERVICES: &str = include_str!("./sql/create_services.sql");
|
||||
|
|
8
crates/core/src/database/drivers/sql/create_domains.sql
Normal file
8
crates/core/src/database/drivers/sql/create_domains.sql
Normal file
|
@ -0,0 +1,8 @@
|
|||
CREATE TABLE IF NOT EXISTS domains (
|
||||
id BIGINT NOT NULL PRIMARY KEY,
|
||||
created BIGINT NOT NULL,
|
||||
owner BIGINT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
tld TEXT NOT NULL,
|
||||
data TEXT NOT NULL
|
||||
)
|
9
crates/core/src/database/drivers/sql/create_layouts.sql
Normal file
9
crates/core/src/database/drivers/sql/create_layouts.sql
Normal file
|
@ -0,0 +1,9 @@
|
|||
CREATE TABLE IF NOT EXISTS layouts (
|
||||
id BIGINT NOT NULL PRIMARY KEY,
|
||||
created BIGINT NOT NULL,
|
||||
owner BIGINT NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
privacy TEXT NOT NULL,
|
||||
pages TEXT NOT NULL,
|
||||
replaces TEXT NOT NULL
|
||||
)
|
7
crates/core/src/database/drivers/sql/create_services.sql
Normal file
7
crates/core/src/database/drivers/sql/create_services.sql
Normal file
|
@ -0,0 +1,7 @@
|
|||
CREATE TABLE IF NOT EXISTS services (
|
||||
id BIGINT NOT NULL PRIMARY KEY,
|
||||
created BIGINT NOT NULL,
|
||||
owner BIGINT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
files TEXT NOT NULL
|
||||
)
|
|
@ -5,6 +5,7 @@ mod channels;
|
|||
mod common;
|
||||
mod communities;
|
||||
pub mod connections;
|
||||
mod domains;
|
||||
mod drafts;
|
||||
mod drivers;
|
||||
mod emojis;
|
||||
|
|
154
crates/core/src/model/littleweb.rs
Normal file
154
crates/core/src/model/littleweb.rs
Normal file
|
@ -0,0 +1,154 @@
|
|||
use std::fmt::Display;
|
||||
use serde::{Serialize, Deserialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Service {
|
||||
pub id: usize,
|
||||
pub created: usize,
|
||||
pub owner: usize,
|
||||
pub name: String,
|
||||
pub files: Vec<ServiceFsEntry>,
|
||||
}
|
||||
|
||||
/// A file type for [`ServiceFsEntry`] structs.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub enum ServiceFsMime {
|
||||
#[serde(alias = "text/html")]
|
||||
Html,
|
||||
#[serde(alias = "text/css")]
|
||||
Css,
|
||||
#[serde(alias = "text/javascript")]
|
||||
Js,
|
||||
#[serde(alias = "application/json")]
|
||||
Json,
|
||||
#[serde(alias = "text/plain")]
|
||||
Plain,
|
||||
}
|
||||
|
||||
impl Display for ServiceFsMime {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(match self {
|
||||
Self::Html => "text/html",
|
||||
Self::Css => "text/css",
|
||||
Self::Js => "text/javascript",
|
||||
Self::Json => "application/json",
|
||||
Self::Plain => "text/plain",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// A single entry in the file system of [`Service`].
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ServiceFsEntry {
|
||||
pub name: String,
|
||||
pub mime: ServiceFsMime,
|
||||
pub children: Vec<ServiceFsEntry>,
|
||||
pub content: String,
|
||||
/// SHA-256 checksum of the entry's content.
|
||||
pub checksum: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub enum DomainTld {
|
||||
Bunny,
|
||||
}
|
||||
|
||||
impl Display for DomainTld {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(match self {
|
||||
Self::Bunny => "bunny",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for DomainTld {
|
||||
fn from(value: &str) -> Self {
|
||||
match value {
|
||||
"bunny" => Self::Bunny,
|
||||
_ => Self::Bunny,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Domain {
|
||||
pub id: usize,
|
||||
pub created: usize,
|
||||
pub owner: usize,
|
||||
pub name: String,
|
||||
pub tld: DomainTld,
|
||||
/// Data about the domain. This can only be configured by the domain's owner.
|
||||
///
|
||||
/// Maximum of 4 entries. Stored in a structure of `(subdomain string, data)`.
|
||||
pub data: Vec<(String, DomainData)>,
|
||||
}
|
||||
|
||||
impl Domain {
|
||||
/// Get the domain's subdomain, name, TLD, and path segments from a string.
|
||||
///
|
||||
/// If no subdomain is provided, the subdomain will be "@". This means that
|
||||
/// domain data entries should use "@" as the root service.
|
||||
pub fn from_str(value: &str) -> (&str, &str, DomainTld, Vec<String>) {
|
||||
// we're reversing this so it's predictable, as there might not always be a subdomain
|
||||
// (we shouldn't have the variable entry be first, there is always going to be a tld)
|
||||
let mut s: Vec<&str> = value.split(".").collect();
|
||||
s.reverse();
|
||||
let mut s = s.into_iter();
|
||||
|
||||
let tld = DomainTld::from(s.next().unwrap());
|
||||
let domain = s.next().unwrap();
|
||||
let subdomain = s.next().unwrap_or("@");
|
||||
|
||||
// get path
|
||||
let no_protocol = value.replace("atto://", "");
|
||||
let mut chars = no_protocol.chars();
|
||||
let mut char = '.';
|
||||
|
||||
while char != '/' {
|
||||
// we need to keep eating characters until we reach the first /
|
||||
// (marking the start of the path)
|
||||
char = chars.next().unwrap();
|
||||
}
|
||||
|
||||
let path: String = chars.collect();
|
||||
|
||||
// return
|
||||
(
|
||||
subdomain,
|
||||
domain,
|
||||
tld,
|
||||
path.split("/").map(|x| x.to_owned()).collect(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Update an HTML/JS/CSS string with the correct URL for all "atto://" protocol requests.
|
||||
///
|
||||
/// This would not be needed if the JS custom protocol API wasn't awful.
|
||||
pub fn http_assets(input: String) -> String {
|
||||
// this is served over the littleweb api NOT the main api!
|
||||
//
|
||||
// littleweb requests MUST be on another subdomain so cookies are
|
||||
// not shared with custom user HTML (since users can embed JS which can make POST requests)
|
||||
//
|
||||
// the littleweb routes are used by providing the "LITTLEWEB" env var
|
||||
input.replace("\"atto://", "/api/v1/over_http?addr=atto://")
|
||||
}
|
||||
|
||||
/// Get the domain's service ID.
|
||||
pub fn service(&self, subdomain: &str) -> Option<usize> {
|
||||
let s = self.data.iter().find(|x| x.0 == subdomain)?;
|
||||
match s.1 {
|
||||
DomainData::Service(id) => Some(id),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub enum DomainData {
|
||||
/// The ID of the service this domain points to. The first service found will
|
||||
/// always be used. This means having multiple service entires will be useless.
|
||||
Service(usize),
|
||||
/// A text entry with a maximum of 512 characters.
|
||||
Text(String),
|
||||
}
|
|
@ -7,6 +7,7 @@ pub mod communities;
|
|||
pub mod communities_permissions;
|
||||
pub mod journals;
|
||||
pub mod layouts;
|
||||
pub mod littleweb;
|
||||
pub mod moderation;
|
||||
pub mod oauth;
|
||||
pub mod permissions;
|
||||
|
|
|
@ -174,6 +174,8 @@ bitflags! {
|
|||
pub struct SecondaryPermission: u32 {
|
||||
const DEFAULT = 1 << 0;
|
||||
const ADMINISTRATOR = 1 << 1;
|
||||
const MANAGE_DOMAINS = 1 << 2;
|
||||
const MANAGE_SERVICES = 1 << 3;
|
||||
|
||||
const _ = !0;
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ color = "#c9b1bc"
|
|||
port = 4118
|
||||
banned_hosts = []
|
||||
host = "http://localhost:4118"
|
||||
lw_host = "http://localhost:4119"
|
||||
no_track = []
|
||||
banned_usernames = [
|
||||
"admin",
|
||||
|
|
4
justfile
4
justfile
|
@ -8,3 +8,7 @@ fix:
|
|||
|
||||
doc:
|
||||
cargo doc --document-private-items --no-deps
|
||||
|
||||
test:
|
||||
cd example && LITTLEWEB=true PORT=4119 cargo run &
|
||||
cd example && cargo run
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue