Fix and improve all login and password-related workflows #83
4
Cargo.lock
generated
4
Cargo.lock
generated
@ -1393,6 +1393,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "golgi"
|
||||
version = "0.1.1"
|
||||
source = "git+https://git.coopcloud.tech/golgi-ssb/golgi.git#77dd75bcd4649b7487069a61e2a8069b49f60a1d"
|
||||
dependencies = [
|
||||
"async-std",
|
||||
"async-stream 0.3.2",
|
||||
@ -2068,6 +2069,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "kuska-ssb"
|
||||
version = "0.4.0"
|
||||
source = "git+https://github.com/Kuska-ssb/ssb#fb7062de606e7c9cae8dd4df402a122db46c1b77"
|
||||
dependencies = [
|
||||
"async-std",
|
||||
"async-stream 0.2.1",
|
||||
@ -2817,9 +2819,11 @@ dependencies = [
|
||||
name = "peach-lib"
|
||||
version = "1.3.2"
|
||||
dependencies = [
|
||||
"async-std",
|
||||
"chrono",
|
||||
"dirs 4.0.0",
|
||||
"fslock",
|
||||
"golgi",
|
||||
"jsonrpc-client-core",
|
||||
"jsonrpc-client-http",
|
||||
"jsonrpc-core 8.0.1",
|
||||
|
||||
@ -5,9 +5,11 @@ authors = ["Andrew Reid <glyph@mycelial.technology>"]
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
async-std = "1.10.0"
|
||||
chrono = "0.4.19"
|
||||
dirs = "4.0"
|
||||
fslock="0.1.6"
|
||||
golgi = { git = "https://git.coopcloud.tech/golgi-ssb/golgi.git" }
|
||||
jsonrpc-client-core = "0.5"
|
||||
jsonrpc-client-http = "0.5"
|
||||
jsonrpc-core = "8.0.1"
|
||||
|
||||
@ -1,12 +1,14 @@
|
||||
//! Interfaces for writing and reading PeachCloud configurations, stored in yaml.
|
||||
//!
|
||||
//! Different PeachCloud microservices import peach-lib, so that they can share this interface.
|
||||
//! Different PeachCloud microservices import peach-lib, so that they can share
|
||||
//! this interface.
|
||||
//!
|
||||
//! The configuration file is located at: "/var/lib/peachcloud/config.yml"
|
||||
|
||||
use std::fs;
|
||||
|
||||
use fslock::LockFile;
|
||||
use log::debug;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::error::PeachError;
|
||||
@ -49,7 +51,7 @@ pub struct PeachConfig {
|
||||
}
|
||||
|
||||
// helper functions for serializing and deserializing PeachConfig from disc
|
||||
fn save_peach_config(peach_config: PeachConfig) -> Result<PeachConfig, PeachError> {
|
||||
pub fn save_peach_config(peach_config: PeachConfig) -> Result<PeachConfig, PeachError> {
|
||||
// use a file lock to avoid race conditions while saving config
|
||||
let mut lock = LockFile::open(LOCK_FILE_PATH)?;
|
||||
lock.lock()?;
|
||||
@ -72,6 +74,7 @@ pub fn load_peach_config() -> Result<PeachConfig, PeachError> {
|
||||
let peach_config_exists = std::path::Path::new(YAML_PATH).exists();
|
||||
|
||||
let peach_config: PeachConfig = if !peach_config_exists {
|
||||
debug!("Loading peach config: {} does not exist", YAML_PATH);
|
||||
PeachConfig {
|
||||
external_domain: "".to_string(),
|
||||
dyn_domain: "".to_string(),
|
||||
@ -81,12 +84,14 @@ pub fn load_peach_config() -> Result<PeachConfig, PeachError> {
|
||||
dyn_tsig_key_path: "".to_string(),
|
||||
dyn_enabled: false,
|
||||
ssb_admin_ids: Vec::new(),
|
||||
admin_password_hash: "".to_string(),
|
||||
// default password is `peach`
|
||||
admin_password_hash: "146".to_string(),
|
||||
temporary_password_hash: "".to_string(),
|
||||
}
|
||||
}
|
||||
// otherwise we load peach config from disk
|
||||
else {
|
||||
debug!("Loading peach config: {} exists", YAML_PATH);
|
||||
let contents = fs::read_to_string(YAML_PATH).map_err(|source| PeachError::Read {
|
||||
source,
|
||||
path: YAML_PATH.to_string(),
|
||||
|
||||
@ -61,11 +61,8 @@ pub enum PeachError {
|
||||
/// Represents a failure to parse or compile a regular expression.
|
||||
Regex(regex::Error),
|
||||
|
||||
/// Represents a failure to successfully execute an sbot command.
|
||||
SbotCli {
|
||||
/// The `stderr` output from the sbot command.
|
||||
msg: String,
|
||||
},
|
||||
/// Represents a failure to successfully execute an sbot command (via golgi).
|
||||
Sbot(String),
|
||||
|
||||
/// Represents a failure to serialize or deserialize JSON.
|
||||
SerdeJson(serde_json::error::Error),
|
||||
@ -117,7 +114,7 @@ impl std::error::Error for PeachError {
|
||||
PeachError::PasswordNotSet => None,
|
||||
PeachError::Read { ref source, .. } => Some(source),
|
||||
PeachError::Regex(_) => None,
|
||||
PeachError::SbotCli { .. } => None,
|
||||
PeachError::Sbot(_) => None,
|
||||
PeachError::SerdeJson(_) => None,
|
||||
PeachError::SerdeYaml(_) => None,
|
||||
PeachError::SsbAdminIdNotFound { .. } => None,
|
||||
@ -153,22 +150,19 @@ impl std::fmt::Display for PeachError {
|
||||
write!(f, "Date/time parse error: {}", path)
|
||||
}
|
||||
PeachError::PasswordIncorrect => {
|
||||
write!(f, "Password error: user-supplied password is incorrect")
|
||||
write!(f, "password is incorrect")
|
||||
}
|
||||
PeachError::PasswordMismatch => {
|
||||
write!(f, "Password error: user-supplied passwords do not match")
|
||||
write!(f, "passwords do not match")
|
||||
}
|
||||
PeachError::PasswordNotSet => {
|
||||
write!(
|
||||
f,
|
||||
"Password error: hash value in YAML configuration file is empty"
|
||||
)
|
||||
write!(f, "hash value in YAML configuration file is empty")
|
||||
}
|
||||
PeachError::Read { ref path, .. } => {
|
||||
write!(f, "Read error: {}", path)
|
||||
}
|
||||
PeachError::Regex(ref err) => err.fmt(f),
|
||||
PeachError::SbotCli { ref msg } => {
|
||||
PeachError::Sbot(ref msg) => {
|
||||
write!(f, "Sbot error: {}", msg)
|
||||
}
|
||||
PeachError::SerdeJson(ref err) => err.fmt(f),
|
||||
|
||||
@ -1,7 +1,10 @@
|
||||
use async_std::task;
|
||||
use golgi::Sbot;
|
||||
use log::debug;
|
||||
use nanorand::{Rng, WyRand};
|
||||
use sha3::{Digest, Sha3_256};
|
||||
|
||||
use crate::{config_manager, error::PeachError};
|
||||
use crate::{config_manager, error::PeachError, sbot::SbotConfig};
|
||||
|
||||
/// Returns Ok(()) if the supplied password is correct,
|
||||
/// and returns Err if the supplied password is incorrect.
|
||||
@ -102,8 +105,37 @@ using this link: http://peach.local/reset_password",
|
||||
// finally send the message to the admins
|
||||
let peach_config = config_manager::load_peach_config()?;
|
||||
for ssb_admin_id in peach_config.ssb_admin_ids {
|
||||
// TODO: replace with golgi
|
||||
//sbot_client::private_message(&msg, &ssb_admin_id)?;
|
||||
// use golgi to send a private message on scuttlebutt
|
||||
match task::block_on(publish_private_msg(&msg, &ssb_admin_id)) {
|
||||
Ok(_) => (),
|
||||
Err(e) => return Err(PeachError::Sbot(e)),
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn publish_private_msg(msg: &str, recipient: &str) -> Result<(), String> {
|
||||
// retrieve latest go-sbot configuration parameters
|
||||
let sbot_config = SbotConfig::read().ok();
|
||||
|
||||
let msg = msg.to_string();
|
||||
let recipient = vec![recipient.to_string()];
|
||||
|
||||
// initialise sbot connection with ip:port and shscap from config file
|
||||
let mut sbot_client = match sbot_config {
|
||||
// TODO: panics if we pass `Some(conf.shscap)` as second arg
|
||||
Some(conf) => {
|
||||
let ip_port = conf.lis.clone();
|
||||
Sbot::init(Some(ip_port), None)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?
|
||||
}
|
||||
None => Sbot::init(None, None).await.map_err(|e| e.to_string())?,
|
||||
};
|
||||
|
||||
debug!("Publishing a Scuttlebutt private message with temporary password");
|
||||
match sbot_client.publish_private(msg, recipient).await {
|
||||
Ok(_) => Ok(()),
|
||||
Err(e) => Err(format!("Failed to publish private message: {}", e)),
|
||||
}
|
||||
}
|
||||
|
||||
@ -38,8 +38,7 @@ maintenance = { status = "actively-developed" }
|
||||
base64 = "0.13.0"
|
||||
dirs = "4.0.0"
|
||||
env_logger = "0.8"
|
||||
#golgi = "0.1.0"
|
||||
golgi = { path = "../../../playground/rust/golgi" }
|
||||
golgi = { git = "https://git.coopcloud.tech/golgi-ssb/golgi.git" }
|
||||
lazy_static = "1.4.0"
|
||||
log = "0.4"
|
||||
nest = "1.0.0"
|
||||
|
||||
@ -1,18 +1,27 @@
|
||||
# peach-web
|
||||
|
||||
[](https://travis-ci.com/peachcloud/peach-web) 
|
||||

|
||||
|
||||
## Web Interface for PeachCloud
|
||||
|
||||
**peach-web** provides a web interface for the PeachCloud device.
|
||||
|
||||
Initial development is focused on administration of the device itself, beginning with networking functionality, with SSB-related administration to be integrated at a later stage.
|
||||
The web interface is primarily designed as a means of managing a Scuttlebutt pub. As such, it exposes the following features:
|
||||
|
||||
The peach-web stack currently consists of [Rocket](https://rocket.rs/) (Rust web framework), [Tera](http://tera.netlify.com/) (Rust template engine), HTML and CSS.
|
||||
- Create a Scuttlebutt profile
|
||||
- Follow, unfollow, block and unblock peers
|
||||
- Generate pub invite codes
|
||||
- Configure the sbot (hops, log directory, LAN discovery etc.)
|
||||
- Send private messages
|
||||
- Stop, start and restart the sbot
|
||||
|
||||
Additional features are focused on administration of the device itself. This includes networking functionality and device statistics.
|
||||
|
||||
The peach-web stack currently consists of [Rocket](https://rocket.rs/) (Rust web framework), [Tera](http://tera.netlify.com/) (Rust template engine), HTML and CSS. Scuttlebutt functionality is provided by [golgi](http://golgi.mycelial.technology).
|
||||
|
||||
_Note: This is a work-in-progress._
|
||||
|
||||
### Setup
|
||||
## Setup
|
||||
|
||||
Clone the `peach-workspace` repo:
|
||||
|
||||
@ -35,9 +44,9 @@ Run the binary:
|
||||
|
||||
`./target/release/peach-web`
|
||||
|
||||
### Environment
|
||||
## Environment
|
||||
|
||||
**Deployment Profile**
|
||||
### Deployment Profile
|
||||
|
||||
The web application deployment profile can be configured with the `ROCKET_ENV` environment variable:
|
||||
|
||||
@ -47,17 +56,17 @@ Default configuration parameters are defined in `Rocket.toml`. This file defines
|
||||
|
||||
Read the [Rocket Environment Configurations docs](https://rocket.rs/v0.5-rc/guide/configuration/#environment-variables) for further information.
|
||||
|
||||
**Configuration Mode**
|
||||
### Configuration Mode
|
||||
|
||||
The web application can be run with a minimal set of routes and functionality (PeachPub - a simple sbot manager) or with the full-suite of capabilities, including network management and access to device statistics (PeachCloud). The mode is enabled by default (as defined in `Rocket.toml`) but can be overwritten using the `ROCKET_STANDALONE_MODE` environment variable: `true` or `false`. If the variable is unset or the value is incorrectly set, the application defaults to standalone mode.
|
||||
|
||||
**Authentication**
|
||||
### Authentication
|
||||
|
||||
Authentication is disabled in `debug` mode and enabled by default when running the application in `release` mode. It can be disabled by setting the `ROCKET_DISABLE_AUTH` environment variable to `true`:
|
||||
|
||||
`export ROCKET_DISABLE_AUTH=true`
|
||||
|
||||
**Logging**
|
||||
### Logging
|
||||
|
||||
Logging is made available with `env_logger`:
|
||||
|
||||
@ -65,7 +74,7 @@ Logging is made available with `env_logger`:
|
||||
|
||||
Other logging levels include `debug`, `warn` and `error`.
|
||||
|
||||
### Debian Packaging
|
||||
## Debian Packaging
|
||||
|
||||
A `systemd` service file and Debian maintainer scripts are included in the `debian` directory, allowing `peach-web` to be easily bundled as a Debian package (`.deb`). The `cargo-deb` [crate](https://crates.io/crates/cargo-deb) can be used to achieve this.
|
||||
|
||||
@ -97,17 +106,17 @@ Remove configuration files (not removed with `apt-get remove`):
|
||||
|
||||
`sudo apt-get purge peach-web`
|
||||
|
||||
### Design
|
||||
## Design
|
||||
|
||||
`peach-web` is built on the Rocket webserver and Tera templating engine. It presents a web interface for interacting with the device. HTML is rendered server-side. Request handlers call `peach-` libraries and serve HTML and assets. Each Tera template is passed a context object. In the case of Rust, this object is a `struct` and must implement `Serialize`. The fields of the context object are available in the context of the template to be rendered.
|
||||
|
||||
### Configuration
|
||||
## Configuration
|
||||
|
||||
Configuration variables are stored in /var/lib/peachcloud/config.yml.
|
||||
Peach-web also updates this file when changes are made to configurations via
|
||||
the web interface. peach-web has no database, so all configurations are stored in this file.
|
||||
|
||||
#### Dynamic DNS Configuration
|
||||
### Dynamic DNS Configuration
|
||||
|
||||
Most users will want to use the default PeachCloud dynamic dns server.
|
||||
If the config dyn_use_custom_server=false, then default values will be used.
|
||||
@ -115,6 +124,6 @@ If the config dyn_use_custom_server=true, then a value must also be set for dyn_
|
||||
This value is the URL of the instance of peach-dyndns-server that requests will be sent to for domain registration.
|
||||
Using a custom value can here can be useful for testing.
|
||||
|
||||
### Licensing
|
||||
## Licensing
|
||||
|
||||
AGPL-3.0
|
||||
|
||||
@ -3,11 +3,7 @@
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use rocket::{
|
||||
form::FromForm,
|
||||
serde::{Deserialize, Serialize},
|
||||
UriDisplayQuery,
|
||||
};
|
||||
use rocket::{form::FromForm, serde::Serialize, UriDisplayQuery};
|
||||
|
||||
use peach_network::{
|
||||
network,
|
||||
@ -36,12 +32,12 @@ pub fn ap_state() -> String {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, FromForm, UriDisplayQuery)]
|
||||
#[derive(Debug, FromForm, UriDisplayQuery)]
|
||||
pub struct Ssid {
|
||||
pub ssid: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, FromForm)]
|
||||
#[derive(Debug, FromForm)]
|
||||
pub struct WiFi {
|
||||
pub ssid: String,
|
||||
pub pass: String,
|
||||
|
||||
@ -62,11 +62,11 @@ impl StatusContext {
|
||||
// retrieve go-sbot systemd process status
|
||||
let sbot_status = SbotStatus::read()?;
|
||||
|
||||
// retrieve latest go-sbot configuration parameters
|
||||
let sbot_config = SbotConfig::read().ok();
|
||||
|
||||
// we only want to try and interact with the sbot if it's active
|
||||
if sbot_status.state == Some("active".to_string()) {
|
||||
// retrieve latest go-sbot configuration parameters
|
||||
let sbot_config = SbotConfig::read().ok();
|
||||
|
||||
let mut sbot_client = init_sbot_with_config(&sbot_config).await?;
|
||||
|
||||
// retrieve the local id
|
||||
@ -80,14 +80,13 @@ impl StatusContext {
|
||||
|
||||
// assign the sequence number of the latest msg
|
||||
context.latest_seq = Some(msgs[0].sequence);
|
||||
|
||||
context.sbot_config = sbot_config;
|
||||
} else {
|
||||
// the sbot is not currently active; return a helpful message
|
||||
context.flash_name = Some("warning".to_string());
|
||||
context.flash_msg = Some("The Sbot is currently inactive. As a result, status data cannot be retrieved. Visit the Scuttlebutt settings menu to start the Sbot and then try again".to_string());
|
||||
}
|
||||
|
||||
context.sbot_config = sbot_config;
|
||||
context.sbot_status = Some(sbot_status);
|
||||
|
||||
Ok(context)
|
||||
@ -425,98 +424,88 @@ impl ProfileContext {
|
||||
// retrieve go-sbot systemd process status
|
||||
let sbot_status = SbotStatus::read()?;
|
||||
|
||||
// we only want to try and interact with the sbot if it's active
|
||||
if sbot_status.state == Some("active".to_string()) {
|
||||
// retrieve latest go-sbot configuration parameters
|
||||
let sbot_config = SbotConfig::read().ok();
|
||||
// retrieve latest go-sbot configuration parameters
|
||||
let sbot_config = SbotConfig::read().ok();
|
||||
|
||||
let mut sbot_client = init_sbot_with_config(&sbot_config).await?;
|
||||
let mut sbot_client = init_sbot_with_config(&sbot_config).await?;
|
||||
|
||||
let local_id = sbot_client.whoami().await?;
|
||||
let local_id = sbot_client.whoami().await?;
|
||||
|
||||
// if an ssb_id has been provided to the context builder, we assume that
|
||||
// the profile info being retrieved is for a peer (ie. not for our local
|
||||
// profile)
|
||||
let id = if ssb_id.is_some() {
|
||||
// we are not dealing with the local profile
|
||||
context.is_local_profile = false;
|
||||
// if an ssb_id has been provided to the context builder, we assume that
|
||||
// the profile info being retrieved is for a peer (ie. not for our local
|
||||
// profile)
|
||||
let id = if ssb_id.is_some() {
|
||||
// we are not dealing with the local profile
|
||||
context.is_local_profile = false;
|
||||
|
||||
// we're safe to unwrap here because we know it's `Some(id)`
|
||||
let peer_id = ssb_id.unwrap();
|
||||
// we're safe to unwrap here because we know it's `Some(id)`
|
||||
let peer_id = ssb_id.unwrap();
|
||||
|
||||
// determine relationship between peer and local id
|
||||
let follow_query = RelationshipQuery {
|
||||
source: local_id.clone(),
|
||||
dest: peer_id.clone(),
|
||||
};
|
||||
|
||||
// query follow state
|
||||
context.following = match sbot_client.friends_is_following(follow_query).await {
|
||||
Ok(following) if following == "true" => Some(true),
|
||||
Ok(following) if following == "false" => Some(false),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
// TODO: i don't like that we have to instantiate the same query object
|
||||
// twice. see if we can streamline this in golgi
|
||||
let block_query = RelationshipQuery {
|
||||
source: local_id.clone(),
|
||||
dest: peer_id.clone(),
|
||||
};
|
||||
|
||||
// query block state
|
||||
context.blocking = match sbot_client.friends_is_blocking(block_query).await {
|
||||
Ok(blocking) if blocking == "true" => Some(true),
|
||||
Ok(blocking) if blocking == "false" => Some(false),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
peer_id
|
||||
} else {
|
||||
// if an ssb_id has not been provided, retrieve the local id using whoami
|
||||
context.is_local_profile = true;
|
||||
|
||||
local_id
|
||||
// determine relationship between peer and local id
|
||||
let follow_query = RelationshipQuery {
|
||||
source: local_id.clone(),
|
||||
dest: peer_id.clone(),
|
||||
};
|
||||
|
||||
// TODO: add relationship state context if not local profile
|
||||
// ie. lookup is_following and is_blocking, set context accordingly
|
||||
// query follow state
|
||||
context.following = match sbot_client.friends_is_following(follow_query).await {
|
||||
Ok(following) if following == "true" => Some(true),
|
||||
Ok(following) if following == "false" => Some(false),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
// retrieve the profile info for the given id
|
||||
let info = sbot_client.get_profile_info(&id).await?;
|
||||
// set each context field accordingly
|
||||
for (key, val) in info {
|
||||
match key.as_str() {
|
||||
"name" => context.name = Some(val),
|
||||
"description" => context.description = Some(val),
|
||||
"image" => context.image = Some(val),
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
// TODO: i don't like that we have to instantiate the same query object
|
||||
// twice. see if we can streamline this in golgi
|
||||
let block_query = RelationshipQuery {
|
||||
source: local_id.clone(),
|
||||
dest: peer_id.clone(),
|
||||
};
|
||||
|
||||
// assign the ssb public key to the context
|
||||
// (could be for the local profile or a peer)
|
||||
context.id = Some(id);
|
||||
// query block state
|
||||
context.blocking = match sbot_client.friends_is_blocking(block_query).await {
|
||||
Ok(blocking) if blocking == "true" => Some(true),
|
||||
Ok(blocking) if blocking == "false" => Some(false),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
// determine the path to the blob defined by the value of `context.image`
|
||||
if let Some(ref blob_id) = context.image {
|
||||
context.blob_path = match blobs::get_blob_path(&blob_id) {
|
||||
Ok(path) => {
|
||||
// if we get the path, check if the blob is in the blobstore.
|
||||
// this allows us to default to a placeholder image in the template
|
||||
if let Ok(exists) = utils::blob_is_stored_locally(&path).await {
|
||||
context.blob_exists = exists
|
||||
};
|
||||
|
||||
Some(path)
|
||||
}
|
||||
Err(_) => None,
|
||||
}
|
||||
}
|
||||
peer_id
|
||||
} else {
|
||||
// the sbot is not currently active; return a helpful message
|
||||
context.flash_name = Some("warning".to_string());
|
||||
context.flash_msg = Some("The Sbot is currently inactive. As a result, profile data cannot be retrieved. Visit the Scuttlebutt settings menu to start the Sbot and then try again".to_string());
|
||||
// if an ssb_id has not been provided, retrieve the local id using whoami
|
||||
context.is_local_profile = true;
|
||||
|
||||
local_id
|
||||
};
|
||||
|
||||
// retrieve the profile info for the given id
|
||||
let info = sbot_client.get_profile_info(&id).await?;
|
||||
// set each context field accordingly
|
||||
for (key, val) in info {
|
||||
match key.as_str() {
|
||||
"name" => context.name = Some(val),
|
||||
"description" => context.description = Some(val),
|
||||
"image" => context.image = Some(val),
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
// assign the ssb public key to the context
|
||||
// (could be for the local profile or a peer)
|
||||
context.id = Some(id);
|
||||
|
||||
// determine the path to the blob defined by the value of `context.image`
|
||||
if let Some(ref blob_id) = context.image {
|
||||
context.blob_path = match blobs::get_blob_path(&blob_id) {
|
||||
Ok(path) => {
|
||||
// if we get the path, check if the blob is in the blobstore.
|
||||
// this allows us to default to a placeholder image in the template
|
||||
if let Ok(exists) = utils::blob_is_stored_locally(&path).await {
|
||||
context.blob_exists = exists
|
||||
};
|
||||
|
||||
Some(path)
|
||||
}
|
||||
Err(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
context.sbot_status = Some(sbot_status);
|
||||
@ -564,22 +553,15 @@ impl PrivateContext {
|
||||
// retrieve go-sbot systemd process status
|
||||
let sbot_status = SbotStatus::read()?;
|
||||
|
||||
// we only want to try and interact with the sbot if it's active
|
||||
if sbot_status.state == Some("active".to_string()) {
|
||||
// retrieve latest go-sbot configuration parameters
|
||||
let sbot_config = SbotConfig::read().ok();
|
||||
// retrieve latest go-sbot configuration parameters
|
||||
let sbot_config = SbotConfig::read().ok();
|
||||
|
||||
let mut sbot_client = init_sbot_with_config(&sbot_config).await?;
|
||||
let mut sbot_client = init_sbot_with_config(&sbot_config).await?;
|
||||
|
||||
context.recipient_id = recipient_id;
|
||||
context.recipient_id = recipient_id;
|
||||
|
||||
let local_id = sbot_client.whoami().await?;
|
||||
context.id = Some(local_id);
|
||||
} else {
|
||||
// the sbot is not currently active; return a helpful message
|
||||
context.flash_name = Some("warning".to_string());
|
||||
context.flash_msg = Some("The Sbot is currently inactive. As a result, private messages cannot be published. Visit the Scuttlebutt settings menu to start the Sbot and then try again".to_string());
|
||||
}
|
||||
let local_id = sbot_client.whoami().await?;
|
||||
context.id = Some(local_id);
|
||||
|
||||
context.sbot_status = Some(sbot_status);
|
||||
|
||||
|
||||
@ -34,6 +34,7 @@ use std::{process, sync::RwLock};
|
||||
|
||||
use lazy_static::lazy_static;
|
||||
use log::{debug, error, info};
|
||||
use peach_lib::{config_manager, config_manager::YAML_PATH as PEACH_CONFIG};
|
||||
use rocket::{fairing::AdHoc, serde::Deserialize, Build, Rocket};
|
||||
|
||||
use utils::Theme;
|
||||
@ -90,6 +91,18 @@ async fn main() {
|
||||
// initialize logger
|
||||
env_logger::init();
|
||||
|
||||
// check if /var/lib/peachcloud/config.yml exists
|
||||
if !std::path::Path::new(PEACH_CONFIG).exists() {
|
||||
info!("PeachCloud configuration file not found; loading default values");
|
||||
// since we're in the intialisation phase, panic if the loading fails
|
||||
let config =
|
||||
config_manager::load_peach_config().expect("peachcloud configuration loading failed");
|
||||
|
||||
info!("Saving default PeachCloud configuration values to file");
|
||||
// this ensures a config file is created if it does not already exist
|
||||
config_manager::save_peach_config(config).expect("peachcloud configuration saving failed");
|
||||
}
|
||||
|
||||
// initialize rocket
|
||||
let rocket = init_rocket();
|
||||
|
||||
|
||||
@ -26,14 +26,11 @@ pub fn mount_peachpub_routes(rocket: Rocket<Build>) -> Rocket<Build> {
|
||||
.mount(
|
||||
"/",
|
||||
routes![
|
||||
help,
|
||||
guide,
|
||||
home,
|
||||
login,
|
||||
login_post,
|
||||
logout,
|
||||
reboot_cmd,
|
||||
shutdown_cmd,
|
||||
power_menu,
|
||||
settings_menu,
|
||||
set_theme,
|
||||
],
|
||||
@ -43,7 +40,6 @@ pub fn mount_peachpub_routes(rocket: Rocket<Build>) -> Rocket<Build> {
|
||||
routes![
|
||||
admin_menu,
|
||||
configure_admin,
|
||||
add_admin,
|
||||
add_admin_post,
|
||||
delete_admin_post,
|
||||
change_password,
|
||||
@ -101,6 +97,7 @@ pub fn mount_peachpub_routes(rocket: Rocket<Build>) -> Rocket<Build> {
|
||||
/// required to run a complete PeachCloud build.
|
||||
pub fn mount_peachcloud_routes(rocket: Rocket<Build>) -> Rocket<Build> {
|
||||
mount_peachpub_routes(rocket)
|
||||
.mount("/", routes![reboot_cmd, shutdown_cmd, power_menu,])
|
||||
.mount(
|
||||
"/settings/network",
|
||||
routes![
|
||||
|
||||
@ -6,7 +6,6 @@ use rocket::{
|
||||
post,
|
||||
request::{self, FlashMessage, FromRequest, Request},
|
||||
response::{Flash, Redirect},
|
||||
serde::Deserialize,
|
||||
};
|
||||
use rocket_dyn_templates::{tera::Context, Template};
|
||||
|
||||
@ -89,17 +88,14 @@ pub fn login(flash: Option<FlashMessage>) -> Template {
|
||||
Template::render("login", &context.into_json())
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, FromForm)]
|
||||
#[derive(Debug, FromForm)]
|
||||
pub struct LoginForm {
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
/// Takes in a LoginForm and returns Ok(()) if username and password
|
||||
/// are correct to authenticate with peach-web.
|
||||
/// Takes in a LoginForm and returns Ok(()) if the password is correct.
|
||||
///
|
||||
/// Note: currently there is only one user, and the username should always
|
||||
/// be "admin".
|
||||
/// Note: there is currently only one user, therefore we don't need a username.
|
||||
pub fn verify_login_form(login_form: LoginForm) -> Result<(), PeachError> {
|
||||
password_utils::verify_password(&login_form.password)
|
||||
}
|
||||
@ -117,13 +113,14 @@ pub fn login_post(login_form: Form<LoginForm>, cookies: &CookieJar<'_>) -> Templ
|
||||
|
||||
TemplateOrRedirect::Redirect(Redirect::to("/"))
|
||||
}
|
||||
Err(_) => {
|
||||
Err(e) => {
|
||||
let err_msg = format!("Invalid password: {}", e);
|
||||
// if unsuccessful login, render /login page again
|
||||
let mut context = Context::new();
|
||||
context.insert("back", &Some("/".to_string()));
|
||||
context.insert("title", &Some("Login".to_string()));
|
||||
context.insert("flash_name", &("error".to_string()));
|
||||
context.insert("flash_msg", &("Invalid password".to_string()));
|
||||
context.insert("flash_msg", &(err_msg));
|
||||
|
||||
TemplateOrRedirect::Template(Template::render("login", &context.into_json()))
|
||||
}
|
||||
@ -142,7 +139,7 @@ pub fn logout(cookies: &CookieJar<'_>) -> Flash<Redirect> {
|
||||
|
||||
// HELPERS AND ROUTES FOR /reset_password
|
||||
|
||||
#[derive(Debug, Deserialize, FromForm)]
|
||||
#[derive(Debug, FromForm)]
|
||||
pub struct ResetPasswordForm {
|
||||
pub temporary_password: String,
|
||||
pub new_password1: String,
|
||||
@ -198,7 +195,7 @@ pub fn reset_password_post(reset_password_form: Form<ResetPasswordForm>) -> Temp
|
||||
let (flash_name, flash_msg) = match save_reset_password_form(reset_password_form.into_inner()) {
|
||||
Ok(_) => (
|
||||
"success".to_string(),
|
||||
"New password is now saved. Return home to login".to_string(),
|
||||
"New password has been saved. Return home to login".to_string(),
|
||||
),
|
||||
Err(err) => (
|
||||
"error".to_string(),
|
||||
@ -266,9 +263,9 @@ pub fn send_password_reset_post() -> Template {
|
||||
|
||||
// HELPERS AND ROUTES FOR /settings/change_password
|
||||
|
||||
#[derive(Debug, Deserialize, FromForm)]
|
||||
#[derive(Debug, FromForm)]
|
||||
pub struct PasswordForm {
|
||||
pub old_password: String,
|
||||
pub current_password: String,
|
||||
pub new_password1: String,
|
||||
pub new_password2: String,
|
||||
}
|
||||
@ -277,9 +274,9 @@ pub struct PasswordForm {
|
||||
pub fn save_password_form(password_form: PasswordForm) -> Result<(), PeachWebError> {
|
||||
info!(
|
||||
"change password!: {} {} {}",
|
||||
password_form.old_password, password_form.new_password1, password_form.new_password2
|
||||
password_form.current_password, password_form.new_password1, password_form.new_password2
|
||||
);
|
||||
password_utils::verify_password(&password_form.old_password)?;
|
||||
password_utils::verify_password(&password_form.current_password)?;
|
||||
// if the previous line did not throw an error, then the old password is correct
|
||||
password_utils::validate_new_passwords(
|
||||
&password_form.new_password1,
|
||||
@ -321,7 +318,7 @@ pub fn change_password_post(password_form: Form<PasswordForm>, _auth: Authentica
|
||||
let (flash_name, flash_msg) = match save_password_form(password_form.into_inner()) {
|
||||
Ok(_) => (
|
||||
"success".to_string(),
|
||||
"New password is now saved".to_string(),
|
||||
"New password has been saved".to_string(),
|
||||
),
|
||||
Err(err) => (
|
||||
"error".to_string(),
|
||||
|
||||
@ -29,17 +29,17 @@ pub fn home(_auth: Authenticated, config: &State<RocketConfig>) -> Template {
|
||||
Template::render("home", &context.into_json())
|
||||
}
|
||||
|
||||
// HELPERS AND ROUTES FOR /help
|
||||
// HELPERS AND ROUTES FOR /guide
|
||||
|
||||
#[get("/help")]
|
||||
pub fn help(flash: Option<FlashMessage>) -> Template {
|
||||
#[get("/guide")]
|
||||
pub fn guide(flash: Option<FlashMessage>) -> Template {
|
||||
// retrieve current ui theme
|
||||
let theme = utils::get_theme();
|
||||
|
||||
let mut context = Context::new();
|
||||
context.insert("theme", &theme);
|
||||
context.insert("back", &Some("/".to_string()));
|
||||
context.insert("title", &Some("Help".to_string()));
|
||||
context.insert("title", &Some("Guide".to_string()));
|
||||
|
||||
// check to see if there is a flash message to display
|
||||
if let Some(flash) = flash {
|
||||
@ -47,5 +47,5 @@ pub fn help(flash: Option<FlashMessage>) -> Template {
|
||||
context.insert("flash_msg", &Some(flash.message().to_string()));
|
||||
};
|
||||
|
||||
Template::render("help", &context.into_json())
|
||||
Template::render("guide", &context.into_json())
|
||||
}
|
||||
|
||||
@ -156,7 +156,7 @@ pub async fn create_invite(invite: Form<Invite>, _auth: Authenticated) -> Flash<
|
||||
Ok(false) => {
|
||||
return Flash::warning(
|
||||
Redirect::to(url),
|
||||
"The Sbot is currently inactive. As a result, new posts cannot be published. Visit the Scuttlebutt settings menu to start the Sbot and then try again",
|
||||
"The Sbot is inactive. New invite codes cannot be generated. Visit the Scuttlebutt settings menu to start the Sbot and then try again",
|
||||
)
|
||||
}
|
||||
// failed to retrieve go-sbot systemd process status
|
||||
@ -173,34 +173,53 @@ pub async fn private(
|
||||
flash: Option<FlashMessage<'_>>,
|
||||
_auth: Authenticated,
|
||||
) -> Template {
|
||||
if let Some(ref key) = public_key {
|
||||
// `url_decode` replaces '+' with ' ', so we need to revert that
|
||||
public_key = Some(key.replace(' ', "+"));
|
||||
}
|
||||
// display a helpful message if the sbot is inactive
|
||||
if let Ok(false) = is_sbot_active() {
|
||||
// retrieve current ui theme
|
||||
let theme = utils::get_theme();
|
||||
|
||||
// build the private context object
|
||||
let context = PrivateContext::build(public_key).await;
|
||||
let mut context = Context::new();
|
||||
context.insert("theme", &theme);
|
||||
context.insert("back", &Some("/".to_string()));
|
||||
context.insert("title", &Some("Private Messages".to_string()));
|
||||
context.insert(
|
||||
"unavailable_msg",
|
||||
&Some("Private messages cannot be published.".to_string()),
|
||||
);
|
||||
|
||||
match context {
|
||||
// we were able to build the context without errors
|
||||
Ok(mut context) => {
|
||||
// check to see if there is a flash message to display
|
||||
if let Some(flash) = flash {
|
||||
// add flash message contents to the context object
|
||||
context.flash_name = Some(flash.kind().to_string());
|
||||
context.flash_msg = Some(flash.message().to_string());
|
||||
};
|
||||
|
||||
Template::render("scuttlebutt/private", &context)
|
||||
// render the "sbot is inactive" template
|
||||
return Template::render("scuttlebutt/inactive", &context.into_json());
|
||||
// otherwise, build the full context and render the private message template
|
||||
} else {
|
||||
if let Some(ref key) = public_key {
|
||||
// `url_decode` replaces '+' with ' ', so we need to revert that
|
||||
public_key = Some(key.replace(' ', "+"));
|
||||
}
|
||||
// an error occurred while building the context
|
||||
Err(e) => {
|
||||
// build the default context and pass along the error message
|
||||
let mut context = PrivateContext::default();
|
||||
context.flash_name = Some("error".to_string());
|
||||
context.flash_msg = Some(e.to_string());
|
||||
|
||||
Template::render("scuttlebutt/private", &context)
|
||||
// build the private context object
|
||||
let context = PrivateContext::build(public_key).await;
|
||||
|
||||
match context {
|
||||
// we were able to build the context without errors
|
||||
Ok(mut context) => {
|
||||
// check to see if there is a flash message to display
|
||||
if let Some(flash) = flash {
|
||||
// add flash message contents to the context object
|
||||
context.flash_name = Some(flash.kind().to_string());
|
||||
context.flash_msg = Some(flash.message().to_string());
|
||||
};
|
||||
|
||||
Template::render("scuttlebutt/private", &context)
|
||||
}
|
||||
// an error occurred while building the context
|
||||
Err(e) => {
|
||||
// build the default context and pass along the error message
|
||||
let mut context = PrivateContext::default();
|
||||
context.flash_name = Some("error".to_string());
|
||||
context.flash_msg = Some(e.to_string());
|
||||
|
||||
Template::render("scuttlebutt/private", &context)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -256,7 +275,7 @@ pub async fn private_post(private: Form<Private>, _auth: Authenticated) -> Flash
|
||||
Ok(false) => {
|
||||
return Flash::warning(
|
||||
Redirect::to(url),
|
||||
"The Sbot is currently inactive. As a result, new private message cannot be published. Visit the Scuttlebutt settings menu to start the Sbot and then try again",
|
||||
"The Sbot is inactive. New private message cannot be published. Visit the Scuttlebutt settings menu to start the Sbot and then try again",
|
||||
);
|
||||
}
|
||||
// failed to retrieve go-sbot systemd process status
|
||||
@ -335,12 +354,24 @@ pub fn peers(flash: Option<FlashMessage>, _auth: Authenticated) -> Template {
|
||||
context.insert("title", &Some("Scuttlebutt Peers"));
|
||||
context.insert("back", &Some("/"));
|
||||
|
||||
// check to see if there is a flash message to display
|
||||
if let Some(flash) = flash {
|
||||
// add flash message contents to the context object
|
||||
context.insert("flash_name", &Some(flash.kind().to_string()));
|
||||
context.insert("flash_msg", &Some(flash.message().to_string()));
|
||||
};
|
||||
// display a helpful message if the sbot is inactive
|
||||
if let Ok(false) = is_sbot_active() {
|
||||
context.insert(
|
||||
"unavailable_msg",
|
||||
&Some("Social lists and interactions are unavailable.".to_string()),
|
||||
);
|
||||
|
||||
// render the "sbot is inactive" template
|
||||
return Template::render("scuttlebutt/inactive", &context.into_json());
|
||||
} else {
|
||||
context.insert("sbot_state", &Some("active".to_string()));
|
||||
// check to see if there is a flash message to display
|
||||
if let Some(flash) = flash {
|
||||
// add flash message contents to the context object
|
||||
context.insert("flash_name", &Some(flash.kind().to_string()));
|
||||
context.insert("flash_msg", &Some(flash.message().to_string()));
|
||||
};
|
||||
}
|
||||
|
||||
Template::render("scuttlebutt/peers", &context.into_json())
|
||||
}
|
||||
@ -388,7 +419,7 @@ pub async fn publish(post: Form<Post>, _auth: Authenticated) -> Flash<Redirect>
|
||||
Ok(false) => {
|
||||
return Flash::warning(
|
||||
Redirect::to(url),
|
||||
"The Sbot is currently inactive. As a result, new posts cannot be published. Visit the Scuttlebutt settings menu to start the Sbot and then try again",
|
||||
"The Sbot is inactive. New posts cannot be published. Visit the Scuttlebutt settings menu to start the Sbot and then try again",
|
||||
);
|
||||
}
|
||||
Err(e) => return Flash::error(Redirect::to(url), e),
|
||||
@ -431,7 +462,7 @@ pub async fn follow(peer: Form<Peer>, _auth: Authenticated) -> Flash<Redirect> {
|
||||
Ok(false) => {
|
||||
return Flash::warning(
|
||||
Redirect::to(url),
|
||||
"The Sbot is currently inactive. As a result, follow messages cannot be published. Visit the Scuttlebutt settings menu to start the Sbot and then try again",
|
||||
"The Sbot is inactive. Follow messages cannot be published. Visit the Scuttlebutt settings menu to start the Sbot and then try again",
|
||||
);
|
||||
}
|
||||
Err(e) => return Flash::error(Redirect::to(url), e),
|
||||
@ -476,7 +507,7 @@ pub async fn unfollow(peer: Form<Peer>, _auth: Authenticated) -> Flash<Redirect>
|
||||
Ok(false) => {
|
||||
return Flash::warning(
|
||||
Redirect::to(url),
|
||||
"The Sbot is currently inactive. As a result, follow messages cannot be published. Visit the Scuttlebutt settings menu to start the Sbot and then try again",
|
||||
"The Sbot is inactive. Follow messages cannot be published. Visit the Scuttlebutt settings menu to start the Sbot and then try again",
|
||||
);
|
||||
}
|
||||
Err(e) => return Flash::error(Redirect::to(url), e),
|
||||
@ -519,7 +550,7 @@ pub async fn block(peer: Form<Peer>, _auth: Authenticated) -> Flash<Redirect> {
|
||||
Ok(false) => {
|
||||
return Flash::warning(
|
||||
Redirect::to(url),
|
||||
"The Sbot is currently inactive. As a result, follow messages cannot be published. Visit the Scuttlebutt settings menu to start the Sbot and then try again",
|
||||
"The Sbot is inactive. Follow messages cannot be published. Visit the Scuttlebutt settings menu to start the Sbot and then try again",
|
||||
);
|
||||
}
|
||||
Err(e) => return Flash::error(Redirect::to(url), e),
|
||||
@ -563,7 +594,7 @@ pub async fn unblock(peer: Form<Peer>, _auth: Authenticated) -> Flash<Redirect>
|
||||
Ok(false) => {
|
||||
return Flash::warning(
|
||||
Redirect::to(url),
|
||||
"The Sbot is currently inactive. As a result, follow messages cannot be published. Visit the Scuttlebutt settings menu to start the Sbot and then try again",
|
||||
"The Sbot is inactive. Follow messages cannot be published. Visit the Scuttlebutt settings menu to start the Sbot and then try again",
|
||||
);
|
||||
}
|
||||
Err(e) => return Flash::error(Redirect::to(url), e),
|
||||
@ -582,41 +613,59 @@ pub async fn profile(
|
||||
flash: Option<FlashMessage<'_>>,
|
||||
_auth: Authenticated,
|
||||
) -> Template {
|
||||
if let Some(ref key) = public_key {
|
||||
// `url_decode` replaces '+' with ' ', so we need to revert that
|
||||
public_key = Some(key.replace(' ', "+"));
|
||||
}
|
||||
// display a helpful message if the sbot is inactive
|
||||
if let Ok(false) = is_sbot_active() {
|
||||
// retrieve current ui theme
|
||||
let theme = utils::get_theme();
|
||||
|
||||
// build the profile context object
|
||||
let context = ProfileContext::build(public_key).await;
|
||||
let mut context = Context::new();
|
||||
context.insert("theme", &theme);
|
||||
context.insert("back", &Some("/".to_string()));
|
||||
context.insert("title", &Some("Profile".to_string()));
|
||||
context.insert(
|
||||
"unavailable_msg",
|
||||
&Some("Profile data cannot be retrieved.".to_string()),
|
||||
);
|
||||
|
||||
match context {
|
||||
// we were able to build the context without errors
|
||||
Ok(mut context) => {
|
||||
// check to see if there is a flash message to display
|
||||
if let Some(flash) = flash {
|
||||
// add flash message contents to the context object
|
||||
context.flash_name = Some(flash.kind().to_string());
|
||||
context.flash_msg = Some(flash.message().to_string());
|
||||
};
|
||||
|
||||
Template::render("scuttlebutt/profile", &context)
|
||||
// render the "sbot is inactive" template
|
||||
return Template::render("scuttlebutt/inactive", &context.into_json());
|
||||
} else {
|
||||
if let Some(ref key) = public_key {
|
||||
// `url_decode` replaces '+' with ' ', so we need to revert that
|
||||
public_key = Some(key.replace(' ', "+"));
|
||||
}
|
||||
// an error occurred while building the context
|
||||
Err(e) => {
|
||||
// build the default context and pass along the error message
|
||||
let mut context = ProfileContext::default();
|
||||
|
||||
// flash name and msg will be `Some` if the sbot is inactive (in
|
||||
// that case, they are set by the context builder).
|
||||
// otherwise, we need to assign the name and returned error msg
|
||||
// to the flash.
|
||||
if context.flash_name.is_none() || context.flash_msg.is_none() {
|
||||
context.flash_name = Some("error".to_string());
|
||||
context.flash_msg = Some(e.to_string());
|
||||
// build the profile context object
|
||||
let context = ProfileContext::build(public_key).await;
|
||||
|
||||
match context {
|
||||
// we were able to build the context without errors
|
||||
Ok(mut context) => {
|
||||
// check to see if there is a flash message to display
|
||||
if let Some(flash) = flash {
|
||||
// add flash message contents to the context object
|
||||
context.flash_name = Some(flash.kind().to_string());
|
||||
context.flash_msg = Some(flash.message().to_string());
|
||||
};
|
||||
|
||||
Template::render("scuttlebutt/profile", &context)
|
||||
}
|
||||
// an error occurred while building the context
|
||||
Err(e) => {
|
||||
// build the default context and pass along the error message
|
||||
let mut context = ProfileContext::default();
|
||||
|
||||
Template::render("scuttlebutt/profile", &context)
|
||||
// flash name and msg will be `Some` if the sbot is inactive (in
|
||||
// that case, they are set by the context builder).
|
||||
// otherwise, we need to assign the name and returned error msg
|
||||
// to the flash.
|
||||
if context.flash_name.is_none() || context.flash_msg.is_none() {
|
||||
context.flash_name = Some("error".to_string());
|
||||
context.flash_msg = Some(e.to_string());
|
||||
}
|
||||
|
||||
Template::render("scuttlebutt/profile", &context)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -627,31 +676,49 @@ pub async fn profile(
|
||||
/// for the local Scuttlebutt profile.
|
||||
#[get("/profile/update")]
|
||||
pub async fn update_profile(flash: Option<FlashMessage<'_>>, _auth: Authenticated) -> Template {
|
||||
// build the profile context object
|
||||
let context = ProfileContext::build(None).await;
|
||||
// display a helpful message if the sbot is inactive
|
||||
if let Ok(false) = is_sbot_active() {
|
||||
// retrieve current ui theme
|
||||
let theme = utils::get_theme();
|
||||
|
||||
match context {
|
||||
// we were able to build the context without errors
|
||||
Ok(mut context) => {
|
||||
context.back = Some("/scuttlebutt/profile".to_string());
|
||||
let mut context = Context::new();
|
||||
context.insert("theme", &theme);
|
||||
context.insert("back", &Some("/".to_string()));
|
||||
context.insert("title", &Some("Profile".to_string()));
|
||||
context.insert(
|
||||
"unavailable_msg",
|
||||
&Some("Profile data cannot be retrieved.".to_string()),
|
||||
);
|
||||
|
||||
// check to see if there is a flash message to display
|
||||
if let Some(flash) = flash {
|
||||
// add flash message contents to the context object
|
||||
context.flash_name = Some(flash.kind().to_string());
|
||||
context.flash_msg = Some(flash.message().to_string());
|
||||
};
|
||||
// render the "sbot is inactive" template
|
||||
return Template::render("scuttlebutt/inactive", &context.into_json());
|
||||
} else {
|
||||
// build the profile context object
|
||||
let context = ProfileContext::build(None).await;
|
||||
|
||||
Template::render("scuttlebutt/update_profile", &context)
|
||||
}
|
||||
// an error occurred while building the context
|
||||
Err(e) => {
|
||||
// build the default context and pass along the error message
|
||||
let mut context = ProfileContext::default();
|
||||
context.flash_name = Some("error".to_string());
|
||||
context.flash_msg = Some(e.to_string());
|
||||
match context {
|
||||
// we were able to build the context without errors
|
||||
Ok(mut context) => {
|
||||
context.back = Some("/scuttlebutt/profile".to_string());
|
||||
|
||||
Template::render("scuttlebutt/update_profile", &context)
|
||||
// check to see if there is a flash message to display
|
||||
if let Some(flash) = flash {
|
||||
// add flash message contents to the context object
|
||||
context.flash_name = Some(flash.kind().to_string());
|
||||
context.flash_msg = Some(flash.message().to_string());
|
||||
};
|
||||
|
||||
Template::render("scuttlebutt/update_profile", &context)
|
||||
}
|
||||
// an error occurred while building the context
|
||||
Err(e) => {
|
||||
// build the default context and pass along the error message
|
||||
let mut context = ProfileContext::default();
|
||||
context.flash_name = Some("error".to_string());
|
||||
context.flash_msg = Some(e.to_string());
|
||||
|
||||
Template::render("scuttlebutt/update_profile", &context)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -772,7 +839,7 @@ pub async fn update_profile_post(
|
||||
Ok(false) => {
|
||||
return Flash::warning(
|
||||
Redirect::to(url),
|
||||
"The Sbot is currently inactive. As a result, profile data cannot be updated. Visit the Scuttlebutt settings menu to start the Sbot and then try again",
|
||||
"The Sbot is inactive. Profile data cannot be updated. Visit the Scuttlebutt settings menu to start the Sbot and then try again",
|
||||
);
|
||||
}
|
||||
Err(e) => return Flash::error(Redirect::to(url), e),
|
||||
|
||||
@ -3,7 +3,6 @@ use rocket::{
|
||||
get, post,
|
||||
request::FlashMessage,
|
||||
response::{Flash, Redirect},
|
||||
serde::Deserialize,
|
||||
uri,
|
||||
};
|
||||
use rocket_dyn_templates::{tera::Context, Template};
|
||||
@ -77,50 +76,31 @@ pub fn configure_admin(flash: Option<FlashMessage>, _auth: Authenticated) -> Tem
|
||||
|
||||
// HELPERS AND ROUTES FOR /settings/admin/add
|
||||
|
||||
#[derive(Debug, Deserialize, FromForm)]
|
||||
#[derive(Debug, FromForm)]
|
||||
pub struct AddAdminForm {
|
||||
pub ssb_id: String,
|
||||
}
|
||||
|
||||
pub fn save_add_admin_form(admin_form: AddAdminForm) -> Result<(), PeachWebError> {
|
||||
let _result = config_manager::add_ssb_admin_id(&admin_form.ssb_id)?;
|
||||
|
||||
// if the previous line didn't throw an error then it was a success
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[get("/add")]
|
||||
pub fn add_admin(flash: Option<FlashMessage>, _auth: Authenticated) -> Template {
|
||||
// retrieve current ui theme
|
||||
let theme = utils::get_theme();
|
||||
|
||||
let mut context = Context::new();
|
||||
context.insert("theme", &theme);
|
||||
context.insert("back", &Some("/settings/admin/configure".to_string()));
|
||||
context.insert("title", &Some("Add Admin".to_string()));
|
||||
|
||||
// check to see if there is a flash message to display
|
||||
if let Some(flash) = flash {
|
||||
context.insert("flash_name", &Some(flash.kind().to_string()));
|
||||
context.insert("flash_msg", &Some(flash.message().to_string()));
|
||||
};
|
||||
|
||||
// template_dir is set in Rocket.toml
|
||||
Template::render("settings/admin/add_admin", &context.into_json())
|
||||
}
|
||||
|
||||
#[post("/add", data = "<add_admin_form>")]
|
||||
pub fn add_admin_post(add_admin_form: Form<AddAdminForm>, _auth: Authenticated) -> Flash<Redirect> {
|
||||
let result = save_add_admin_form(add_admin_form.into_inner());
|
||||
let url = uri!("/settings/admin/configure");
|
||||
match result {
|
||||
Ok(_) => Flash::success(Redirect::to(url), "Successfully added new admin"),
|
||||
Err(_) => Flash::error(Redirect::to(url), "Failed to add new admin"),
|
||||
Ok(_) => Flash::success(Redirect::to(url), "Added SSB administrator"),
|
||||
Err(e) => Flash::error(Redirect::to(url), format!("Failed to add new admin: {}", e)),
|
||||
}
|
||||
}
|
||||
|
||||
// HELPERS AND ROUTES FOR /settings/admin/delete
|
||||
|
||||
#[derive(Debug, Deserialize, FromForm)]
|
||||
#[derive(Debug, FromForm)]
|
||||
pub struct DeleteAdminForm {
|
||||
pub ssb_id: String,
|
||||
}
|
||||
@ -131,9 +111,12 @@ pub fn delete_admin_post(
|
||||
_auth: Authenticated,
|
||||
) -> Flash<Redirect> {
|
||||
let result = config_manager::delete_ssb_admin_id(&delete_admin_form.ssb_id);
|
||||
let url = uri!(configure_admin);
|
||||
let url = uri!("/settings/admin", configure_admin);
|
||||
match result {
|
||||
Ok(_) => Flash::success(Redirect::to(url), "Successfully removed admin id"),
|
||||
Err(_) => Flash::error(Redirect::to(url), "Failed to remove admin id"),
|
||||
Ok(_) => Flash::success(Redirect::to(url), "Removed SSB administrator"),
|
||||
Err(e) => Flash::error(
|
||||
Redirect::to(url),
|
||||
format!("Failed to remove admin id: {}", e),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,7 +3,6 @@ use rocket::{
|
||||
form::{Form, FromForm},
|
||||
get, post,
|
||||
request::FlashMessage,
|
||||
serde::Deserialize,
|
||||
};
|
||||
use rocket_dyn_templates::Template;
|
||||
|
||||
@ -19,7 +18,7 @@ use crate::{
|
||||
utils,
|
||||
};
|
||||
|
||||
#[derive(Debug, Deserialize, FromForm)]
|
||||
#[derive(Debug, FromForm)]
|
||||
pub struct DnsForm {
|
||||
pub external_domain: String,
|
||||
pub enable_dyndns: bool,
|
||||
|
||||
@ -4,7 +4,6 @@ use rocket::{
|
||||
get, post,
|
||||
request::FlashMessage,
|
||||
response::{Flash, Redirect},
|
||||
serde::Deserialize,
|
||||
uri, UriDisplayQuery,
|
||||
};
|
||||
use rocket_dyn_templates::{tera::Context, Template};
|
||||
@ -21,12 +20,12 @@ use crate::{
|
||||
|
||||
// STRUCTS USED BY NETWORK ROUTES
|
||||
|
||||
#[derive(Debug, Deserialize, FromForm, UriDisplayQuery)]
|
||||
#[derive(Debug, FromForm, UriDisplayQuery)]
|
||||
pub struct Ssid {
|
||||
pub ssid: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, FromForm)]
|
||||
#[derive(Debug, FromForm)]
|
||||
pub struct WiFi {
|
||||
pub ssid: String,
|
||||
pub pass: String,
|
||||
|
||||
@ -10,14 +10,13 @@ use rocket::{
|
||||
get, post,
|
||||
request::FlashMessage,
|
||||
response::{Flash, Redirect},
|
||||
serde::Deserialize,
|
||||
};
|
||||
use rocket_dyn_templates::{tera::Context, Template};
|
||||
|
||||
use crate::routes::authentication::Authenticated;
|
||||
use crate::utils;
|
||||
|
||||
#[derive(Debug, Deserialize, FromForm)]
|
||||
#[derive(Debug, FromForm)]
|
||||
pub struct SbotConfigForm {
|
||||
/// Directory path for the log and indexes.
|
||||
repo: String,
|
||||
|
||||
@ -476,13 +476,7 @@ fn scuttlebutt_settings_menu_html() {
|
||||
assert_eq!(response.content_type(), Some(ContentType::HTML));
|
||||
let body = response.into_string().unwrap();
|
||||
assert!(body.contains("Scuttlebutt Settings"));
|
||||
assert!(body.contains("Set Network Key"));
|
||||
assert!(body.contains("Set Replication Hops"));
|
||||
assert!(body.contains("Remove Blocked Feeds"));
|
||||
assert!(body.contains("Set Database Directory"));
|
||||
assert!(body.contains("Check Filesystem"));
|
||||
assert!(body.contains("Repair Filesystem"));
|
||||
assert!(body.contains("Restart Sbot"));
|
||||
assert!(body.contains("Configure Sbot"));
|
||||
}
|
||||
|
||||
// STATUS HTML ROUTES
|
||||
|
||||
@ -170,6 +170,7 @@
|
||||
|
||||
--background: var(--moon-gray);
|
||||
--button-background: var(--light-gray);
|
||||
--button-border: var(--near-black);
|
||||
--circle-background: var(--light-gray);
|
||||
--circle-small-hover-background: var(--white);
|
||||
|
||||
@ -180,6 +181,8 @@
|
||||
|
||||
--capsule-background: var(--light-gray);
|
||||
|
||||
--list-background: var(--light-gray);
|
||||
|
||||
--text-color-normal: var(--near-black);
|
||||
--text-color-gray: var(--mid-gray);
|
||||
--text-color-light-gray: var(--moon-gray);
|
||||
@ -191,20 +194,15 @@
|
||||
|
||||
/* DARK THEME VARIABLES */
|
||||
|
||||
/*--background-dark: var(--dark-gray);*/
|
||||
--background-dark: #222;
|
||||
|
||||
--button-background-dark: var(--mid-gray);
|
||||
/*--capsule-background-dark: var(--light-gray);*/
|
||||
--button-border-dark: var(--silver);
|
||||
|
||||
--capsule-background-dark: #333;
|
||||
|
||||
--circle-background-dark: var(--silver);
|
||||
--circle-small-hover-background-dark: var(--moon-gray);
|
||||
|
||||
--list-background-dark: #333;
|
||||
}
|
||||
/* we need to add shades for each accent colour
|
||||
*
|
||||
* --info-100
|
||||
* --info-200
|
||||
* --info-300 -> var(--blue)
|
||||
* --info-400
|
||||
* --info-500
|
||||
*/
|
||||
|
||||
@ -186,7 +186,7 @@ body {
|
||||
*/
|
||||
|
||||
.button {
|
||||
border: 1px solid var(--near-black);
|
||||
border: 1px solid var(--button-border);
|
||||
border-radius: var(--border-radius-2);
|
||||
/* Needed to render inputs & buttons of equal width */
|
||||
-moz-box-sizing: border-box;
|
||||
@ -199,7 +199,8 @@ body {
|
||||
text-decoration: none;
|
||||
font-size: var(--font-size-5);
|
||||
font-family: var(--sans-serif);
|
||||
width: 80%;
|
||||
/* width: 80%; */
|
||||
width: 16rem;
|
||||
margin-top: 5px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
@ -316,6 +317,12 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 600px) {
|
||||
.card-wide {
|
||||
width: 30rem;
|
||||
}
|
||||
}
|
||||
|
||||
.card-container {
|
||||
justify-content: center;
|
||||
padding: 0.5rem;
|
||||
@ -338,7 +345,7 @@ body {
|
||||
color: var(--text-color);
|
||||
margin: 0;
|
||||
font-size: var(--font-size-5);
|
||||
padding-bottom: 0.3rem;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.container {
|
||||
@ -607,11 +614,13 @@ html {
|
||||
|
||||
--background: var(--background);
|
||||
--button-background: var(--button-background);
|
||||
--button-border: var(--button-border);
|
||||
--button-text-color: var(--text-color-normal);
|
||||
--button-text-hover-color: var(--text-color-normal);
|
||||
--circle-background: var(--circle-background);
|
||||
--circle-small-hover-background: var(--circle-small-hover-background);
|
||||
--icon-color: var(--icon-normal);
|
||||
--list-background: var(--list-background);
|
||||
--nav-icon-color: var(--nav-icon-color);
|
||||
--text-color: var(--text-color-normal);
|
||||
}
|
||||
@ -619,12 +628,14 @@ html {
|
||||
html[data-theme='dark'] {
|
||||
--background: var(--background-dark);
|
||||
--button-background: var(--button-background-dark);
|
||||
--button-border: var(--button-border-dark);
|
||||
--button-text-color: var(--text-color-light);
|
||||
--button-text-hover-color: var(--text-color-normal);
|
||||
--capsule-background: var(--capsule-background-dark);
|
||||
--circle-background: var(--circle-background-dark);
|
||||
--circle-small-hover-background: var(--circle-small-hover-background-dark);
|
||||
--icon-color: var(--icon-light);
|
||||
--list-background: var(--list-background-dark);
|
||||
--nav-icon-color: var(--nav-icon-color-light);
|
||||
--text-color: var(--text-color-light);
|
||||
--text-color-gray: var(--text-color-light-gray);
|
||||
@ -720,7 +731,7 @@ form {
|
||||
-moz-box-sizing: border-box;
|
||||
-webkit-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
margin-top: 0.5rem;
|
||||
/* margin-top: 0.5rem; */
|
||||
margin-bottom: 1rem;
|
||||
padding-left: 5px;
|
||||
line-height: 1.5rem;
|
||||
@ -738,6 +749,7 @@ form {
|
||||
|
||||
.message-input {
|
||||
height: 7rem;
|
||||
margin-top: 0.5rem;
|
||||
overflow: auto;
|
||||
resize: vertical;
|
||||
width: 100%;
|
||||
@ -770,7 +782,6 @@ form {
|
||||
}
|
||||
|
||||
.label-medium {
|
||||
color: var(--text-color);
|
||||
font-size: var(--font-size-3);
|
||||
display: block;
|
||||
}
|
||||
@ -796,7 +807,11 @@ form {
|
||||
|
||||
.link {
|
||||
text-decoration: none;
|
||||
/* color: var(--font-near-black); */
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/*
|
||||
@ -805,14 +820,20 @@ form {
|
||||
|
||||
.list {
|
||||
padding-left: 0;
|
||||
margin-left: 0;
|
||||
max-width: var(--max-width-6);
|
||||
width: 90%;
|
||||
border: 1px solid var(--light-silver);
|
||||
border-radius: var(--border-radius-2);
|
||||
list-style-type: none;
|
||||
font-family: var(--sans-serif);
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 600px) {
|
||||
.list {
|
||||
width: 100%;
|
||||
max-width: var(--max-width-6);
|
||||
}
|
||||
}
|
||||
|
||||
.list-container {
|
||||
width: var(--max-width-5);
|
||||
}
|
||||
@ -825,6 +846,7 @@ form {
|
||||
}
|
||||
|
||||
.list-item {
|
||||
background-color: var(--list-background);
|
||||
display: grid;
|
||||
padding: 1rem;
|
||||
border-bottom-color: var(--light-silver);
|
||||
|
||||
33
peach-web/templates/guide.html.tera
Normal file
33
peach-web/templates/guide.html.tera
Normal file
@ -0,0 +1,33 @@
|
||||
{%- extends "nav" -%}
|
||||
{%- block card %}
|
||||
<!-- GUIDE -->
|
||||
<div class="card card-wide center">
|
||||
<div class="capsule capsule-container border-info">
|
||||
<!-- GETTING STARTED -->
|
||||
<details>
|
||||
<summary class="card-text link">Getting started</summary>
|
||||
<p class="card-text" style="margin-top: 1rem; margin-bottom: 1rem;">The Scuttlebutt server (sbot) will be inactive when you first run PeachCloud. This is to allow configuration parameters to be set before it is activated for the first time. Navigate to the <a href="/settings/scuttlebutt/configure" class="link font-gray">Sbot Configuration</a> page to configure your system. The default configuration will be fine for most usecases.</p>
|
||||
<p class="card-text" style="margin-top: 1rem; margin-bottom: 1rem;">Once the configuration is set, navigate to the <strong><a href="/settings/scuttlebutt" class="link font-gray">Scuttlebutt settings menu</a></strong> to start the sbot. If the server starts successfully, you will see a green smiley face on the home page. If the face is orange and sleeping, that means the sbot is still inactive (ie. the process is not running). If the face is red and dead, that means the sbot failed to start - indicated an error. For now, the best way to gain insight into the problem is to check the systemd log. Open a terminal and enter: <code>systemctl --user status go-sbot.service</code>. The log output may give some clues about the source of the error.</p>
|
||||
</details>
|
||||
<!-- BUG REPORTS -->
|
||||
<details>
|
||||
<summary class="card-text link">Submit a bug report</summary>
|
||||
<p class="card-text" style="margin-top: 1rem; margin-bottom: 1rem;">Bug reports can be submitted by filing an issue on the peach-workspace git repo. Before filing a report, first check to see if an issue already exists for the bug you've encountered. If not, you're invited to submit a new report; the template will guide you through several questions.</p>
|
||||
</details>
|
||||
<!-- REQUEST SUPPORT -->
|
||||
<details>
|
||||
<summary class="card-text link">Share feedback & request support</summary>
|
||||
<p class="card-text" style="margin-top: 1rem; margin-bottom: 1rem;">You're invited to share your thoughts and experiences of PeachCloud in the #peachcloud channel on Scuttlebutt. The channel is also a good place to ask for help.</p>
|
||||
<p class="card-text" style="margin-top: 1rem; margin-bottom: 1rem;">Alternatively, we have a <strong><a href="https://matrix.to/#/#peachcloud:matrix.org" class="link font-gray">Matrix channel</a></strong> for discussion about PeachCloud and you can also reach out to @glyph <strong><a href="mailto:glyph@mycelial.technology" class="link font-gray">via email</a></strong>.</p>
|
||||
</details>
|
||||
<!-- CONTRIBUTE -->
|
||||
<details>
|
||||
<summary class="card-text link">Contribute to PeachCloud</summary>
|
||||
<p class="card-text" style="margin-top: 1rem; margin-bottom: 1rem;">PeachCloud is free, open-source software and relies on donations and grants to fund develop. Donations can be made on our <strong><a href="https://opencollective.com/peachcloud" class="link font-gray">Open Collective</a></strong> page.</p>
|
||||
<p class="card-text" style="margin-top: 1rem; margin-bottom: 1rem;">Programmers, designers, artists and writers are also welcome to contribute to the project. Please visit the <strong><a href="https://git.coopcloud.tech/PeachCloud/peach-workspace" class="link font-gray">main PeachCloud git repository</a></strong> to find out more details or contact the team via Scuttlebutt, Matrix or email.</p>
|
||||
</details>
|
||||
</div>
|
||||
<!-- FLASH MESSAGE -->
|
||||
{% include "snippets/flash_message" %}
|
||||
</div>
|
||||
{%- endblock card -%}
|
||||
@ -1,10 +0,0 @@
|
||||
{%- extends "nav" -%}
|
||||
{%- block card %}
|
||||
<!-- HELP MENU -->
|
||||
<div class="card center">
|
||||
<div class="capsule capsule-container border-info">
|
||||
<!-- FLASH MESSAGE -->
|
||||
{% include "snippets/flash_message" %}
|
||||
</div>
|
||||
</div>
|
||||
{%- endblock card -%}
|
||||
@ -52,14 +52,14 @@
|
||||
</a>
|
||||
<!-- bottom-middle -->
|
||||
<!-- PEACHCLOUD GUIDEBOOK LINK AND ICON -->
|
||||
<a class="bottom-middle" href="/help" title="Help Menu">
|
||||
<a class="bottom-middle" href="/guide" title="Guide">
|
||||
<div class="circle circle-small border-circle-small border-info">
|
||||
<img class="icon-medium" src="/icons/book.svg">
|
||||
</div>
|
||||
</a>
|
||||
<!-- bottom-right -->
|
||||
<!-- SYSTEM SETTINGS LINK AND ICON -->
|
||||
<a class="bottom-right" href="/settings" title="Settings Menu">
|
||||
<a class="bottom-right" href="/settings" title="Settings">
|
||||
<div class="circle circle-small border-circle-small border-settings">
|
||||
<img class="icon-medium" src="/icons/cog.svg">
|
||||
</div>
|
||||
|
||||
@ -2,22 +2,19 @@
|
||||
{%- block card %}
|
||||
<!-- LOGIN FORM -->
|
||||
<div class="card center">
|
||||
<div class="card-container">
|
||||
<form id="login_form" class="center" action="/login" method="post">
|
||||
<!-- input for username -->
|
||||
<input id="username" name="username" class="center input" type="text" placeholder="Username" title="Username for authentication" autofocus/>
|
||||
<form id="login_form" class="center" action="/login" method="post">
|
||||
<div style="display: flex; flex-direction: column; margin-bottom: 1rem;">
|
||||
<!-- input for password -->
|
||||
<input id="password" name="password" class="center input" type="password" placeholder="Password" title="Password for given username"/>
|
||||
<div id="buttonDiv">
|
||||
<input id="loginUser" class="button button-primary center" title="Login" type="submit" value="Login">
|
||||
<label for="password" class="center label-small font-gray" style="width: 80%;">PASSWORD</label>
|
||||
<input id="password" name="password" class="center input" type="password" title="Password for given username"/>
|
||||
<!-- login (form submission) button -->
|
||||
<input id="loginUser" class="button button-primary center" title="Login" type="submit" value="Login">
|
||||
<div class="center-text" style="margin-top: 1rem;">
|
||||
<a href="/settings/admin/forgot_password" class="label-small link font-gray">Forgot Password?</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<!-- FLASH MESSAGE -->
|
||||
{% include "snippets/flash_message" %}
|
||||
<div class="center-text" style="margin-top: 25px;">
|
||||
<a href="/settings/admin/forgot_password" class="label-small link font-gray">Forgot Password?</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{%- endblock card -%}
|
||||
|
||||
11
peach-web/templates/scuttlebutt/inactive.html.tera
Normal file
11
peach-web/templates/scuttlebutt/inactive.html.tera
Normal file
@ -0,0 +1,11 @@
|
||||
{%- extends "nav" -%}
|
||||
{%- block card %}
|
||||
<!-- SBOT INACTIVE -->
|
||||
<div class="card center">
|
||||
<div class="capsule capsule-container border-warning center-text">
|
||||
<p class="card-text" style="font-size: var(--font-size-4);">Sbot Inactive</p>
|
||||
<p class="card-text">{{ unavailable_msg }}</p>
|
||||
<p class="card-text">Visit the <strong><a href="/settings/scuttlebutt" class="link">Scuttlebutt settings menu</a></strong> to start the Sbot and then try again.</p>
|
||||
</div>
|
||||
</div>
|
||||
{%- endblock card -%}
|
||||
@ -2,6 +2,8 @@
|
||||
{%- block card %}
|
||||
<!-- SCUTTLEBUTT PEERS -->
|
||||
<div class="card center">
|
||||
{# only render the peer menu elements if the sbot is active #}
|
||||
{%- if sbot_state == "active" %}
|
||||
<div class="card-container">
|
||||
<!-- BUTTONS -->
|
||||
<div id="buttons">
|
||||
@ -12,5 +14,8 @@
|
||||
<a id="invites" class="button button-primary center" href="/scuttlebutt/invites" title="Create invites">Invites</a>
|
||||
</div>
|
||||
</div>
|
||||
{%- endif %}
|
||||
<!-- FLASH MESSAGE -->
|
||||
{% include "snippets/flash_message" %}
|
||||
</div>
|
||||
{%- endblock card -%}
|
||||
|
||||
@ -1,24 +1,30 @@
|
||||
{%- extends "nav" -%}
|
||||
{%- block card %}
|
||||
<div class="card center">
|
||||
{%- if peers %}
|
||||
<ul class="list">
|
||||
{%- for peer in peers %}
|
||||
{%- if peers %}
|
||||
<ul class="center list">
|
||||
{%- for peer in peers %}
|
||||
{# set a fall-back value for name in case the data is unavailable #}
|
||||
{%- if not peer['name'] %}
|
||||
{%- set name = "name unavailable" %}
|
||||
{%- else %}
|
||||
{%- set name = peer['name'] %}
|
||||
{%- endif %}
|
||||
<li>
|
||||
<a class="list-item link light-bg" href="/scuttlebutt/profile?public_key={{ peer['id'] }}">
|
||||
<a class="list-item link" href="/scuttlebutt/profile?public_key={{ peer['id'] }}">
|
||||
{%- if peer['blob_path'] and peer['blob_exists'] == "true" %}
|
||||
<img id="peerImage" class="icon list-icon" src="/blob/{{ peer['blob_path'] }}" alt="{{ peer['name'] }}'s profile image">
|
||||
<img id="peerImage" class="icon list-icon" src="/blob/{{ peer['blob_path'] }}" alt="{{ name }}'s profile image">
|
||||
{%- else %}
|
||||
<img id="peerImage" class="icon list-icon" src="/icons/user.svg" alt="Placeholder profile image">
|
||||
<img id="peerImage" class="icon icon-active list-icon" src="/icons/user.svg" alt="Placeholder profile image">
|
||||
{%- endif %}
|
||||
<p id="peerName" class="font-normal list-text">{{ peer['name'] }}</p>
|
||||
<label class="label-small label-ellipsis list-label font-gray" for="peerName" title="{{ peer['name'] }}'s Public Key">{{ peer['id'] }}</label>
|
||||
<p id="peerName" class="font-normal list-text">{{ name }}</p>
|
||||
<label class="label-small label-ellipsis list-label font-gray" for="peerName" title="{{ name }}'s Public Key">{{ peer['id'] }}</label>
|
||||
</a>
|
||||
</li>
|
||||
{%- endfor %}
|
||||
</ul>
|
||||
{%- else %}
|
||||
<p>No follows found</p>
|
||||
{%- endif %}
|
||||
{%- endif %}
|
||||
</div>
|
||||
{%- endblock card -%}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{%- extends "nav" -%}
|
||||
{%- block card %}
|
||||
<!-- SCUTTLEBUTT PRIVATE MESSAGE FORM -->
|
||||
<div class="card center">
|
||||
<div class="card card-wide center">
|
||||
{# only render the private message elements if the sbot is active #}
|
||||
{%- if sbot_status and sbot_status.state == "active" %}
|
||||
<form id="sbotConfig" class="center" action="/scuttlebutt/private" method="post">
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{%- extends "nav" -%}
|
||||
{%- block card %}
|
||||
<!-- USER PROFILE -->
|
||||
<div class="card center">
|
||||
<div class="card card-wide center">
|
||||
{# only render the profile info elements if the sbot is active #}
|
||||
{%- if sbot_status and sbot_status.state == "active" %}
|
||||
<!-- PROFILE INFO BOX -->
|
||||
@ -19,7 +19,7 @@
|
||||
<img id="profilePicture" class="icon-large" src="/blob/{{ blob_path }}" title="Profile picture" alt="Profile picture">
|
||||
{% else %}
|
||||
{# render a placeholder profile picture (icon) #}
|
||||
<img id="profilePicture" class="icon" src="/icons/user.svg" title="Profile picture" alt="Profile picture">
|
||||
<img id="profilePicture" class="icon icon-active" src="/icons/user.svg" title="Profile picture" alt="Profile picture">
|
||||
{% endif %}
|
||||
<!-- name, public key & description -->
|
||||
<p id="profileName" class="card-text" title="Name">{{ name }}</p>
|
||||
@ -38,12 +38,12 @@
|
||||
<!-- TODO: each of these buttons needs to be a form with a public key -->
|
||||
<div id="buttons" style="margin-top: 2rem;">
|
||||
{% if following == false %}
|
||||
<form id="followForm" action="/scuttlebutt/follow" method="post">
|
||||
<form id="followForm" class="center" action="/scuttlebutt/follow" method="post">
|
||||
<input type="hidden" id="publicKey" name="public_key" value="{{ id }}">
|
||||
<input id="followPeer" class="button button-primary center" type="submit" title="Follow Peer" value="Follow">
|
||||
</form>
|
||||
{% elif following == true %}
|
||||
<form id="unfollowForm" action="/scuttlebutt/unfollow" method="post">
|
||||
<form id="unfollowForm" class="center" action="/scuttlebutt/unfollow" method="post">
|
||||
<input type="hidden" id="publicKey" name="public_key" value="{{ id }}">
|
||||
<input id="unfollowPeer" class="button button-primary center" type="submit" title="Unfollow Peer" value="Unfollow">
|
||||
</form>
|
||||
@ -51,19 +51,21 @@
|
||||
<p>Unable to determine follow state</p>
|
||||
{% endif %}
|
||||
{% if blocking == false %}
|
||||
<form id="blockForm" action="/scuttlebutt/block" method="post">
|
||||
<form id="blockForm" class="center" action="/scuttlebutt/block" method="post">
|
||||
<input type="hidden" id="publicKey" name="public_key" value="{{ id }}">
|
||||
<input id="blockPeer" class="button button-primary center" type="submit" title="Block Peer" value="Block">
|
||||
</form>
|
||||
{% elif blocking == true %}
|
||||
<form id="unblockForm" action="/scuttlebutt/unblock" method="post">
|
||||
<form id="unblockForm" class="center" action="/scuttlebutt/unblock" method="post">
|
||||
<input type="hidden" id="publicKey" name="public_key" value="{{ id }}">
|
||||
<input id="unblockPeer" class="button button-primary center" type="submit" title="Unblock Peer" value="Unblock">
|
||||
</form>
|
||||
{% else %}
|
||||
<p>Unable to determine block state</p>
|
||||
{% endif %}
|
||||
<a id="privateMessage" class="button button-primary center" href="/scuttlebutt/private?public_key={{ id }}" title="Private Message">Send Private Message</a>
|
||||
<form class="center">
|
||||
<a id="privateMessage" class="button button-primary center" href="/scuttlebutt/private?public_key={{ id }}" title="Private Message">Send Private Message</a>
|
||||
</form>
|
||||
</div>
|
||||
{%- endif %}
|
||||
{%- endif %}
|
||||
|
||||
@ -9,8 +9,8 @@
|
||||
</div>
|
||||
<!-- BUTTONS -->
|
||||
<input id="search" class="button button-primary center" type="submit" title="Search for peer" value="Search">
|
||||
<!-- FLASH MESSAGE -->
|
||||
{% include "snippets/flash_message" %}
|
||||
</form>
|
||||
<!-- FLASH MESSAGE -->
|
||||
{% include "snippets/flash_message" %}
|
||||
</div>
|
||||
{%- endblock card -%}
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
{# ASSIGN VARIABLES #}
|
||||
{# ---------------- #}
|
||||
<!-- SSB PROFILE UPDATE FORM -->
|
||||
<div class="card center">
|
||||
<div class="card card-wide center">
|
||||
<form id="profileInfo" class="center" enctype="multipart/form-data" action="/scuttlebutt/profile/update" method="post">
|
||||
<div style="display: flex; flex-direction: column">
|
||||
<label for="name" class="label-small font-gray">NAME</label>
|
||||
@ -11,7 +11,7 @@
|
||||
<label for="description" class="label-small font-gray">DESCRIPTION</label>
|
||||
<textarea id="description" class="message-input" style="margin-bottom: 1rem;" name="new_description" placeholder="Write a description for your profile...">{{ description }}</textarea>
|
||||
<label for="image" class="label-small font-gray">IMAGE</label>
|
||||
<input type="file" id="fileInput" name="image">
|
||||
<input type="file" id="fileInput" class="font-normal" name="image">
|
||||
</div>
|
||||
<input type="hidden" name="id" value="{{ id }}">
|
||||
<input type="hidden" name="current_name" value="{{ name }}">
|
||||
|
||||
@ -1,17 +0,0 @@
|
||||
{%- extends "nav" -%}
|
||||
{%- block card %}
|
||||
<!-- ADD ADMIN FORM -->
|
||||
<div class="card center">
|
||||
<div class="card-container">
|
||||
<form id="addAdminForm" action="/settings/admin/add" method="post">
|
||||
<input id="ssb_id" name="ssb_id" class="center input" type="text" placeholder="SSB ID" title="SSB ID of Admin" value=""/>
|
||||
<div id="buttonDiv">
|
||||
<input id="addAdmin" class="button button-primary center" title="Add" type="submit" value="Add">
|
||||
<a class="button button-secondary center" href="/settings/admin/configure" title="Cancel">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
<!-- FLASH MESSAGE -->
|
||||
{% include "snippets/flash_message" %}
|
||||
</div>
|
||||
</div>
|
||||
{%- endblock card -%}
|
||||
@ -3,13 +3,17 @@
|
||||
<!-- CHANGE PASSWORD FORM -->
|
||||
<div class="card center">
|
||||
<form id="changePassword" class="center" action="/settings/admin/change_password" method="post">
|
||||
<!-- input for current password -->
|
||||
<input id="currentPassword" class="center input" name="current_password" type="password" placeholder="Current password" title="Current password" autofocus>
|
||||
<!-- input for new password -->
|
||||
<input id="newPassword" class="center input" name="new_password1" type="password" placeholder="New password" title="New password">
|
||||
<!-- input for duplicate new password -->
|
||||
<input id="newPasswordDuplicate" class="center input" name="new_password2" type="password" placeholder="Re-enter new password" title="New password duplicate">
|
||||
<div id="buttonDiv">
|
||||
<div style="display: flex; flex-direction: column; margin-bottom: 1rem;">
|
||||
<!-- input for current password -->
|
||||
<label for="currentPassword" class="center label-small font-gray" style="width: 80%;">CURRENT PASSWORD</label>
|
||||
<input id="currentPassword" class="center input" name="current_password" type="password" title="Current password" autofocus>
|
||||
<!-- input for new password -->
|
||||
<label for="newPassword" class="center label-small font-gray" style="width: 80%;">NEW PASSWORD</label>
|
||||
<input id="newPassword" class="center input" name="new_password1" type="password" title="New password">
|
||||
<!-- input for duplicate new password -->
|
||||
<label for="newPasswordDuplicate" class="center label-small font-gray" style="width: 80%;">RE-ENTER NEW PASSWORD</label>
|
||||
<input id="newPasswordDuplicate" class="center input" name="new_password2" type="password" title="New password duplicate">
|
||||
<!-- save (form submission) button -->
|
||||
<input id="savePassword" class="button button-primary center" title="Add" type="submit" value="Save">
|
||||
<a class="button button-secondary center" href="/settings/admin" title="Cancel">Cancel</a>
|
||||
</div>
|
||||
|
||||
@ -2,25 +2,31 @@
|
||||
{%- block card %}
|
||||
<!-- CONFIGURE ADMIN PAGE -->
|
||||
<div class="card center">
|
||||
<div class="text-container">
|
||||
<h4 class="font-normal">Current Admins</h4>
|
||||
{% if not ssb_admin_ids %}
|
||||
<div class="card-text">
|
||||
There are no currently configured admins.
|
||||
</div>
|
||||
{% else %}
|
||||
{% for admin in ssb_admin_ids %}
|
||||
<div>
|
||||
<form action="/settings/admin/delete" method="post">
|
||||
<input type="hidden" name="ssb_id" value="{{admin}}"/>
|
||||
<input type="submit" value="X" title="Delete"/> <span>{{ admin }}</span>
|
||||
</form>
|
||||
<div class="capsule capsule-profile center-text font-normal border-info" style="font-family: var(--sans-serif); font-size: var(--font-size-6); margin-bottom: 1.5rem;">Administrators are identified and added by their Scuttlebutt public keys. These accounts will be sent private messages on Scuttlebutt when a password reset is requested.</div>
|
||||
{% if not ssb_admin_ids %}
|
||||
<div class="card-text">
|
||||
There are no currently configured admins.
|
||||
</div>
|
||||
{% else %}
|
||||
{% for admin in ssb_admin_ids %}
|
||||
<form class="center" action="/settings/admin/delete" method="post">
|
||||
<div class="center" style="display: flex; justify-content: space-between;">
|
||||
<input type="hidden" name="ssb_id" value="{{ admin }}"/>
|
||||
<p class="label-small label-ellipsis font-gray" style="user-select: all;">{{ admin }}</p>
|
||||
<input style="width: 30%;" type="submit" class="button button-warning" value="Delete" title="Delete SSB administrator"/>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
<a class="button button-primary center full-width" style="margin-top: 25px;" href="/settings/admin/add" title="Add Admin">Add Admin</a>
|
||||
</div>
|
||||
<!-- FLASH MESSAGE -->
|
||||
{% include "snippets/flash_message" %}
|
||||
</form>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
<form id="addAdmin" class="center" style="margin-top: 2rem;" action="/settings/admin/add" method="post">
|
||||
<div class="center" style="display: flex; flex-direction: column; margin-bottom: 2rem;" title="Public key (ID) of a desired administrator">
|
||||
<label for="publicKey" class="label-small font-gray">PUBLIC KEY</label>
|
||||
<input type="text" id="publicKey" name="ssb_id" placeholder="@xYz...=.ed25519" autofocus>
|
||||
</div>
|
||||
<!-- BUTTONS -->
|
||||
<input class="button button-primary center" type="submit" title="Add SSB administrator" value="Add Admin">
|
||||
<!-- FLASH MESSAGE -->
|
||||
{% include "snippets/flash_message" %}
|
||||
</form>
|
||||
</div>
|
||||
{%- endblock card -%}
|
||||
|
||||
@ -2,14 +2,15 @@
|
||||
{%- block card %}
|
||||
<!-- PASSWORD RESET REQUEST CARD -->
|
||||
<div class="card center">
|
||||
<div class="capsule capsule-container info-border">
|
||||
<p class="card-text">Click the button below to send a new temporary password which can be used to change your device password.
|
||||
</br></br>
|
||||
The temporary password will be sent in an SSB private message to the admin of this device.</p>
|
||||
<div class="capsule capsule-container border-info">
|
||||
<p class="card-text">Click the 'Send Temporary Password' button to send a new temporary password which can be used to change your device password.</p>
|
||||
<p class="card-text" style="margin-top: 1rem;">The temporary password will be sent in an SSB private message to the admin of this device.</p>
|
||||
<p class="card-text" style="margin-top: 1rem;">Once you have the temporary password, click the 'Set New Password' button to reach the password reset page.</p>
|
||||
</div>
|
||||
<form id="sendPasswordReset" action="/send_password_reset" method="post">
|
||||
<form id="sendPasswordReset" action="/settings/admin/send_password_reset" method="post">
|
||||
<div id="buttonDiv">
|
||||
<input class="button button-primary center" style="margin-top: 1rem;" type="submit" value="Send Password Reset" title="Send Password Reset Link"/>
|
||||
<input class="button button-primary center" style="margin-top: 1rem;" type="submit" value="Send Temporary Password" title="Send temporary password to Scuttlebutt admin"/>
|
||||
<a href="/settings/admin/reset_password" class="button button-primary center" title="Set a new password using the temporary password">Set New Password</a>
|
||||
</div>
|
||||
</form>
|
||||
<!-- FLASH MESSAGE -->
|
||||
|
||||
@ -4,8 +4,9 @@
|
||||
<div class="card center">
|
||||
<!-- BUTTONS -->
|
||||
<div id="settingsButtons">
|
||||
<a id="change" class="button button-primary center" href="/settings/admin/change_password" title="Change Password">Change Password</a>
|
||||
<a id="configure" class="button button-primary center" href="/settings/admin/configure" title="Configure Admin">Configure Admin</a>
|
||||
<a id="change" class="button button-primary center" href="/settings/admin/change_password" title="Change Password">Change Password</a>
|
||||
<a id="reset" class="button button-primary center" href="/settings/admin/forgot_password" title="Reset Password">Reset Password</a>
|
||||
</div>
|
||||
</div>
|
||||
{%- endblock card -%}
|
||||
|
||||
@ -2,41 +2,22 @@
|
||||
{%- block card %}
|
||||
<!-- RESET PASSWORD PAGE -->
|
||||
<div class="card center">
|
||||
<div class="form-container">
|
||||
<form id="changePassword" action="/reset_password" method="post">
|
||||
<div class="input-wrapper">
|
||||
<!-- input for temporary password -->
|
||||
<label id="temporary_password" class="label-small input-label font-near-black">
|
||||
<label class="label-small input-label font-gray" for="temporary_password" style="padding-top: 0.25rem;">Temporary Password</label>
|
||||
<input id="temporary_password" class="form-input" style="margin-bottom: 0;"
|
||||
name="temporary_password" type="password" title="temporary password" value="">
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="input-wrapper">
|
||||
<!-- input for new password1 -->
|
||||
<label id="new_password1" class="label-small input-label font-near-black">
|
||||
<label class="label-small input-label font-gray" for="new_password1" style="padding-top: 0.25rem;">Enter New Password</label>
|
||||
<input id="new_password1" class="form-input" style="margin-bottom: 0;"
|
||||
name="new_password1" title="new_password1" type="password" value="">
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="input-wrapper">
|
||||
<!-- input for new password2 -->
|
||||
<label id="new_password2" class="label-small input-label font-near-black">
|
||||
<label class="label-small input-label font-gray" for="new_password2" style="padding-top: 0.25rem;">Re-Enter New Password</label>
|
||||
<input id="new_password2" class="form-input" style="margin-bottom: 0;"
|
||||
name="new_password2" title="new_password2" type="password" value="">
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div id="buttonDiv">
|
||||
<input id="changePasswordButton" class="button button-primary center" title="Add" type="submit" value="Save">
|
||||
</div>
|
||||
</form>
|
||||
<!-- FLASH MESSAGE -->
|
||||
{% include "snippets/flash_message" %}
|
||||
</div>
|
||||
<form id="changePassword" class="center" action="/settings/admin/reset_password" method="post">
|
||||
<div style="display: flex; flex-direction: column; margin-bottom: 1rem;">
|
||||
<!-- input for temporary password -->
|
||||
<label class="center label-small font-gray" style="width: 80%;" for="temporary_password">TEMPORARY PASSWORD</label>
|
||||
<input id="temporary_password" class="center input" name="temporary_password" type="password" title="temporary password" value="">
|
||||
<!-- input for new password1 -->
|
||||
<label class="center label-small font-gray" style="width: 80%;" for="new_password1">NEW PASSWORD</label>
|
||||
<input id="new_password1" class="center input" name="new_password1" title="new_password1" type="password" value="">
|
||||
<!-- input for new password2 -->
|
||||
<label class="center label-small font-gray" style="width: 80%;" for="new_password2">RE-ENTER NEW PASSWORD</label>
|
||||
<input id="new_password2" class="center input" name="new_password2" title="new_password2" type="password" value="">
|
||||
<!-- save (form submission) button -->
|
||||
<input id="changePasswordButton" class="button button-primary center" title="Add" type="submit" value="Save">
|
||||
</div>
|
||||
</form>
|
||||
<!-- FLASH MESSAGE -->
|
||||
{% include "snippets/flash_message" %}
|
||||
</div>
|
||||
{%- endblock card -%}
|
||||
|
||||
@ -11,8 +11,6 @@
|
||||
{% else %}
|
||||
<a id="start" class="button button-primary center" href="/settings/scuttlebutt/start" title="Start Sbot">Start Sbot</a>
|
||||
{% endif %}
|
||||
<a id="checkFilesystem" class="button button-primary center" href="/settings/scuttlebutt/check_fs" title="Check Filesystem">Check Filesystem</a>
|
||||
<a id="removeFeeds" class="button button-primary center" href="/settings/scuttlebutt/remove_feeds" title="Remove Blocked Feeds">Remove Blocked Feeds</a>
|
||||
</div>
|
||||
<!-- FLASH MESSAGE -->
|
||||
{% include "snippets/flash_message" %}
|
||||
|
||||
@ -5,12 +5,12 @@
|
||||
{%- if sbot_status.memory -%}
|
||||
{% set mem = sbot_status.memory / 1024 / 1024 | round | int -%}
|
||||
{%- else -%}
|
||||
{% set mem = "X" -%}
|
||||
{% set mem = "0" -%}
|
||||
{%- endif -%}
|
||||
{%- if sbot_status.blobstore -%}
|
||||
{% set blobs = sbot_status.blobstore / 1024 / 1024 | round | int -%}
|
||||
{%- else -%}
|
||||
{% set blobs = "X" -%}
|
||||
{% set blobs = "0" -%}
|
||||
{%- endif -%}
|
||||
<!-- SCUTTLEBUTT STATUS -->
|
||||
<div class="card center">
|
||||
@ -54,8 +54,12 @@
|
||||
<div id="middleSection" style="margin-top: 1rem;">
|
||||
<div id="sbotInfo" class="center" style="display: flex; justify-content: space-between; width: 90%;">
|
||||
<div class="center" style="display: flex; align-items: last baseline;">
|
||||
{% if sbot_status.state == "active" %}
|
||||
<label class="card-text" style="margin-right: 5px;">{{ latest_seq }}</label>
|
||||
<label class="label-small font-gray">MESSAGES IN LOCAL DATABASE</label>
|
||||
{% else %}
|
||||
<label class="label-small font-gray">DATABASE UNAVAILABLE</label>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -65,22 +69,22 @@
|
||||
<div class="stack">
|
||||
<img class="icon icon-active" title="Hops" src="/icons/orbits.png">
|
||||
<div class="flex-grid" style="padding-top: 0.5rem;">
|
||||
<label class="label-medium" style="padding-right: 3px;" title="Replication hops">{{ sbot_config.hops }}</label>
|
||||
<label class="label-medium font-normal" style="padding-right: 3px;" title="Replication hops">{{ sbot_config.hops }}</label>
|
||||
</div>
|
||||
<label class="label-small font-gray">HOPS</label>
|
||||
</div>
|
||||
<div class="stack">
|
||||
<img class="icon icon-active" title="Blobs" src="/icons/image-file.png">
|
||||
<div class="flex-grid" style="padding-top: 0.5rem;">
|
||||
<label class="label-medium{% if sbot_status.state == "inactive" %} font-gray{% endif %}" style="padding-right: 3px;" title="Blobstore size in MB">{{ blobs }}</label>
|
||||
<label class="label-small{% if sbot_status.state == "inactive" %} font-gray{% else %} font-normal{% endif %}">MB</label>
|
||||
<label class="label-medium font-normal" style="padding-right: 3px;" title="Blobstore size in MB">{{ blobs }}</label>
|
||||
<label class="label-small font-normal">MB</label>
|
||||
</div>
|
||||
<label class="label-small font-gray">BLOBSTORE</label>
|
||||
</div>
|
||||
<div class="stack">
|
||||
<img class="icon{% if sbot_status.memory %} icon-active{% else %} icon-inactive{% endif %}" title="Memory" src="/icons/ram.png">
|
||||
<div class="flex-grid" style="padding-top: 0.5rem;">
|
||||
<label class="label-medium{% if sbot_status.state == "inactive" %} font-gray{% endif %}" style="padding-right: 3px;" title="Memory usage of the go-sbot process in MB">{{ mem }}</label>
|
||||
<label class="label-medium{% if sbot_status.state == "inactive" %} font-gray{% else %} font-normal{% endif %}" style="padding-right: 3px;" title="Memory usage of the go-sbot process in MB">{{ mem }}</label>
|
||||
<label class="label-small{% if sbot_status.state == "inactive" %} font-gray{% else %} font-normal{% endif %}">MB</label>
|
||||
</div>
|
||||
<label class="label-small font-gray">MEMORY</label>
|
||||
|
||||
Reference in New Issue
Block a user