Compare commits
361 Commits
ssb_status
...
main
Author | SHA1 | Date |
---|---|---|
glyph | 6cc8faa0c3 | |
glyph | 6028e07bde | |
glyph | ebc7b9d417 | |
glyph | b8ff944377 | |
glyph | 8cbb295c3a | |
glyph | 7d5d6bcc1f | |
glyph | 8c3a92aa88 | |
glyph | cfe270a995 | |
glyph | 2eca779208 | |
glyph | a1b16f8d38 | |
glyph | 3bf095e148 | |
glyph | d9167a2cd6 | |
glyph | 4e7fbd5fdf | |
glyph | 0fab57d94f | |
glyph | 441d2a6a3b | |
glyph | 52e0aff4d1 | |
glyph | 24ceedbb9d | |
glyph | d3ab490c05 | |
glyph | 1e7a54b728 | |
glyph | 3eab3e3687 | |
glyph | 8b0381ead1 | |
glyph | e91c40355a | |
glyph | 8cd8ee5dd6 | |
glyph | 24deb4601a | |
glyph | fedf2855ed | |
glyph | 0814eedf13 | |
glyph | 4fb4ea2f9c | |
glyph | 8e283fbc6e | |
glyph | bdd3b7ab9b | |
glyph | 4f36f61128 | |
glyph | acab30acce | |
glyph | 61ef909ed3 | |
glyph | 97030fbfbf | |
glyph | b6cd54142c | |
glyph | 67f33385e5 | |
glyph | a9bcc267a2 | |
glyph | a513b7aa5b | |
glyph | 1a7bd7987b | |
glyph | c5c0bb91e4 | |
glyph | 5a50730435 | |
glyph | 86b4714274 | |
glyph | d5a2390e29 | |
notplants | c83a22461d | |
notplants | 40bd1e48f1 | |
glyph | 6407495292 | |
notplants | 03ac890793 | |
notplants | bc0c0fca7f | |
glyph | 05c1577f2a | |
glyph | add169db07 | |
glyph | fcb17d6802 | |
notplants | fc50bb5ee5 | |
notplants | 29f5ad0e84 | |
notplants | cb09d6c3e9 | |
notplants | 01138eef35 | |
notplants | 2637b28380 | |
notplants | 03a0a51f4d | |
mycognosist | eddb167c4c | |
mycognosist | 9704269c8a | |
notplants | 466db8ceea | |
notplants | 90badbfe30 | |
notplants | 7489916d5f | |
notplants | 7daab74b37 | |
notplants | 58bf306d3b | |
notplants | bdac23092a | |
notplants | f1ab2caa08 | |
notplants | 1fab4f3c43 | |
notplants | 8dcd594dd7 | |
notplants | fcaa9e29c4 | |
notplants | c6fc5c2992 | |
notplants | 1258a3697d | |
notplants | 7e94135839 | |
notplants | f002f5cf3e | |
notplants | fba1e91d8b | |
notplants | 6621a09ec9 | |
glyph | 170b037248 | |
glyph | 251aaf9237 | |
glyph | 123ebc06cc | |
glyph | 9ce27d17c5 | |
glyph | 2a8cf4ecfb | |
glyph | d1a55e29d7 | |
glyph | 4568577f81 | |
glyph | ab0e27c14d | |
glyph | 65b5f95a90 | |
glyph | a60d892e95 | |
glyph | 5bd8a68ddf | |
glyph | a6f52ce384 | |
glyph | c71cc3992d | |
glyph | 56fafc8d67 | |
glyph | 414508f8ff | |
glyph | 6ad5c620c1 | |
glyph | 76d5e6a355 | |
glyph | 11e94fa421 | |
glyph | 216b60b86a | |
glyph | a70f5e227d | |
glyph | cddcb8f9bd | |
glyph | 8b33f8c174 | |
glyph | 1b43dc8b18 | |
glyph | 2d7b74d377 | |
notplants | 6b34864289 | |
notplants | 87ad2439b9 | |
notplants | 5838faf128 | |
notplants | 9c6fa00ec7 | |
notplants | a81b8b42cf | |
notplants | cdcff3475c | |
notplants | 077c2a9178 | |
notplants | 8b0b872d21 | |
glyph | 218a70b8f8 | |
glyph | 50dcb2cf9e | |
glyph | e1877b5024 | |
mycognosist | 0923c24693 | |
mycognosist | f3d4ba9fe5 | |
notplants | 16e6d42f87 | |
notplants | 3493e5adb9 | |
notplants | 5147eed497 | |
notplants | 9f6ba14123 | |
notplants | 21fb29c322 | |
notplants | 6c9e5fd3fd | |
notplants | 3adb226969 | |
notplants | 92f516b161 | |
notplants | 543470b949 | |
notplants | 6434471599 | |
notplants | 56c142a387 | |
notplants | 7deaa00d6e | |
glyph | bf7f2c8e31 | |
glyph | dc79833e2b | |
glyph | 039cbcdfc4 | |
glyph | 4cd2692b9a | |
glyph | 3e5e7e0f7c | |
glyph | b0b79fef24 | |
glyph | 98497fa5ae | |
notplants | e6fd9a48cf | |
notplants | 8960df6635 | |
notplants | 781af460ae | |
notplants | 4a08e4ed6d | |
notplants | 908d265de6 | |
notplants | 8202d4af5f | |
notplants | 5ea6a86700 | |
notplants | 99fd3be4ad | |
notplants | e041e1c7f9 | |
notplants | e10777a5a5 | |
notplants | 288941e8a3 | |
notplants | f96c950aa6 | |
glyph | 827ccbd4dc | |
glyph | c21e2d090c | |
notplants | bab33b602a | |
notplants | b84e470a42 | |
notplants | 97680f9010 | |
notplants | ab0401e555 | |
notplants | 810a97db8a | |
notplants | 610d60d989 | |
notplants | f4c1bc1169 | |
notplants | 3ae182caa9 | |
notplants | 8a6ad4ad61 | |
notplants | 600f9c58bf | |
notplants | 2540a77af1 | |
notplants | 5b86f754f4 | |
notplants | 29804b0dce | |
notplants | e2ac5de6e4 | |
notplants | d03de8cf5d | |
notplants | 03720a7338 | |
notplants | cf9c0c7eca | |
notplants | f764acc2df | |
notplants | a347e4726d | |
notplants | 3d3006049b | |
notplants | 2adb3006fe | |
notplants | 64b5929e5c | |
notplants | 5629a048a1 | |
glyph | 713c3da4cc | |
glyph | 92c7d7daa9 | |
glyph | 5a95ade8b9 | |
glyph | 00d33c2c69 | |
glyph | 126609a605 | |
glyph | e4078bd1ba | |
notplants | 315b04a63e | |
notplants | 1866e289a6 | |
notplants | bff86a490b | |
notplants | 65d5352c85 | |
notplants | df3b4b8858 | |
notplants | 2f1535fbee | |
notplants | b75aadd62d | |
glyph | 4662b15ba3 | |
glyph | abde4ce1b4 | |
glyph | c792aea2f6 | |
glyph | b158fba147 | |
glyph | da8d8f0ec3 | |
glyph | 271aa14322 | |
glyph | d31825f688 | |
glyph | defb8f5f09 | |
glyph | 27e9a8295c | |
glyph | 9ad580b86f | |
glyph | a76ec08da6 | |
glyph | cf64cd9c76 | |
glyph | 169149d607 | |
glyph | f1eaa07f7b | |
glyph | 52c3e88b44 | |
glyph | e659102495 | |
glyph | 57b1a786a4 | |
glyph | fded48908d | |
glyph | 46ded85feb | |
glyph | f29659669c | |
glyph | d6695b291d | |
glyph | aefa1525fb | |
glyph | 367f0307b6 | |
glyph | b1c5c701e5 | |
glyph | 2ae9cb5c48 | |
glyph | 30aff5d7ac | |
glyph | 25e3a145fc | |
glyph | 0bfad25d3d | |
glyph | 952951515b | |
glyph | c65f568e40 | |
glyph | 979ec4eb64 | |
glyph | d9019d6a4b | |
glyph | 07147f8a4f | |
glyph | 5fc0094146 | |
glyph | 50afb61955 | |
glyph | 928afb35d3 | |
glyph | 1bdacf3632 | |
glyph | deaedc4428 | |
glyph | 4a94f14dc5 | |
glyph | b78fafe84d | |
glyph | 5d37c12913 | |
glyph | 3a05396afb | |
glyph | 41bd39d422 | |
glyph | 77c1ccb1c7 | |
glyph | 7d9bc2d7cd | |
glyph | b20822a644 | |
glyph | 65f0ac7630 | |
glyph | 703f35d8b1 | |
glyph | 084af1b486 | |
glyph | 3e918f66cf | |
glyph | 98121f4922 | |
glyph | e19fa0f99d | |
glyph | 3a7b499742 | |
glyph | 85231a20c7 | |
glyph | 602c6a90f1 | |
glyph | 34b4cbff32 | |
glyph | 112cfca67b | |
glyph | a379de179d | |
glyph | 0353586705 | |
glyph | 4e8d93c388 | |
glyph | 6db5e7c169 | |
glyph | 60539adf41 | |
glyph | e8b9cb2cc1 | |
glyph | cad3fc94c8 | |
glyph | 976fac973d | |
glyph | cd7c2bc230 | |
glyph | 40c4f8aaf2 | |
glyph | 70f7ad0dc6 | |
glyph | 31628a7155 | |
glyph | 3c49c067dd | |
glyph | 729580729c | |
glyph | 59739cf6e5 | |
glyph | 7fe919d9a1 | |
glyph | 7cdf8c553d | |
glyph | fe04195030 | |
glyph | 8455e8089c | |
glyph | 97206e0573 | |
glyph | 7acf6ef395 | |
glyph | 3828998769 | |
glyph | 440d6f9bd5 | |
glyph | 59a6c7fdca | |
glyph | 3a4b0ffffd | |
glyph | 447f81a41c | |
glyph | fadad1c30b | |
glyph | 6395fb05e3 | |
glyph | d652f1a020 | |
glyph | 7c98cfcd5d | |
glyph | 4a1d3e81c1 | |
glyph | 5a07eda910 | |
glyph | 580771ebf2 | |
glyph | c794d398b8 | |
glyph | 4d06eb167f | |
glyph | eba15605c2 | |
glyph | 07c18ea64d | |
glyph | ec288658f3 | |
glyph | 6b145d66f8 | |
glyph | 23d6870f77 | |
glyph | b7cf3c1aab | |
glyph | 5b70353d6f | |
glyph | 67c727716c | |
glyph | 5ab47cf742 | |
glyph | b092f1e1c4 | |
glyph | 983aa0689c | |
glyph | 1a8ac3f57f | |
glyph | af34829cb0 | |
glyph | 824cbdbc0c | |
glyph | 84656ff251 | |
glyph | 6cdd6dc41b | |
glyph | 3572fd4e7b | |
glyph | 7fdf88eaa8 | |
glyph | 10049f0bc6 | |
glyph | 486518002d | |
glyph | 3991af11c7 | |
glyph | 59ef5960a4 | |
glyph | 69a8cc262e | |
glyph | 1479a65d59 | |
glyph | ffe190148d | |
glyph | b1724f6eb4 | |
glyph | 9a07ab3ac0 | |
glyph | 06a55ade06 | |
glyph | 6cba477f15 | |
glyph | 020d18731b | |
glyph | a38394054d | |
glyph | 814162ce7d | |
glyph | 03028a2278 | |
glyph | e10468c337 | |
glyph | 436a516c3e | |
glyph | 786e3f41d9 | |
glyph | a491892bd9 | |
glyph | 02a1078ece | |
glyph | 6d2502257d | |
glyph | ebbcc35fbb | |
glyph | e3eb3be2e3 | |
glyph | 799d9de001 | |
glyph | e05de8284d | |
glyph | 9013ccb3d6 | |
glyph | a37288225a | |
glyph | 4665a9e6fa | |
glyph | 1a3ddccbd6 | |
glyph | fe1da62058 | |
glyph | 68c926609e | |
glyph | 17ea3e7f44 | |
glyph | a174027ff5 | |
glyph | f459fe47d1 | |
glyph | 4709ec77f9 | |
glyph | 4e6bb15a23 | |
glyph | 62191e5509 | |
glyph | da976ff4fe | |
glyph | 0737c435a8 | |
glyph | 435e819648 | |
glyph | 8f49fa55ad | |
glyph | f6292407d0 | |
glyph | dfc173d941 | |
glyph | a46b58b206 | |
glyph | 33604ac0dc | |
glyph | f0d972f46b | |
glyph | 89b502be25 | |
glyph | 90a90096f4 | |
glyph | 46926bf468 | |
glyph | d801c957bd | |
glyph | 3397e5eb75 | |
glyph | e474ea519f | |
glyph | f715644e25 | |
glyph | c7cc310a32 | |
glyph | 4470f949bd | |
glyph | 8e5c29ca6d | |
glyph | 00554706cb | |
glyph | 6d9ced5ebc | |
glyph | abda4373ae | |
glyph | e718889485 | |
glyph | b7ec1a42be | |
glyph | 445c05e3ee | |
glyph | 476eaa540e | |
glyph | 6f03063f8d | |
glyph | de9b8f5d73 | |
glyph | 51eff6a298 | |
glyph | 1c90e45f11 | |
glyph | 178af281ed | |
glyph | e1aa7b1bb6 | |
glyph | 816d6c8a73 | |
glyph | b098f73a5f |
|
@ -0,0 +1,33 @@
|
|||
kind: pipeline
|
||||
type: docker
|
||||
name: test-on-amd64
|
||||
|
||||
platform:
|
||||
arch: amd64
|
||||
|
||||
steps:
|
||||
- name: rustfmt
|
||||
image: rust:buster
|
||||
commands:
|
||||
- rustup component add rustfmt
|
||||
- cargo fmt --check
|
||||
|
||||
- name: clippy
|
||||
image: rust:buster
|
||||
commands:
|
||||
- rustup component add clippy
|
||||
- cargo clippy -- -D warnings
|
||||
|
||||
- name: test
|
||||
image: rust:buster
|
||||
commands:
|
||||
- cargo test
|
||||
|
||||
- name: build
|
||||
image: rust:buster
|
||||
commands:
|
||||
- cargo build
|
||||
|
||||
trigger:
|
||||
event:
|
||||
- pull_request
|
File diff suppressed because it is too large
Load Diff
|
@ -1,7 +1,5 @@
|
|||
[workspace]
|
||||
|
||||
members = [
|
||||
"peach-buttons",
|
||||
"peach-oled",
|
||||
"peach-lib",
|
||||
"peach-config",
|
||||
|
@ -13,3 +11,4 @@ members = [
|
|||
"peach-jsonrpc-server",
|
||||
"peach-dyndns-updater"
|
||||
]
|
||||
|
||||
|
|
17
README.md
17
README.md
|
@ -4,6 +4,8 @@ _Better [Scuttlebutt](https://scuttlebutt.nz) cloud infrastructure as a hardware
|
|||
|
||||
[**_Support us on OpenCollective!_**](https://opencollective.com/peachcloud)
|
||||
|
||||
[![Build Status](https://build.coopcloud.tech/api/badges/PeachCloud/peach-workspace/status.svg?ref=refs/heads/main)](https://build.coopcloud.tech/PeachCloud/peach-workspace)
|
||||
|
||||
## Background
|
||||
|
||||
- April 2018 project proposal: [`%HqwAsltORROCh4uyOq6iV+SsqU3OuNUevnq+5dwCqVI=.sha256`](https://viewer.scuttlebot.io/%25HqwAsltORROCh4uyOq6iV%2BSsqU3OuNUevnq%2B5dwCqVI%3D.sha256)
|
||||
|
@ -43,6 +45,19 @@ _Better [Scuttlebutt](https://scuttlebutt.nz) cloud infrastructure as a hardware
|
|||
- [peach-patterns](https://github.com/peachcloud/peach-patterns) - Pattern library for the PeachCloud UI design system
|
||||
- [peach-web](https://github.com/peachcloud/peach-web) - A web interface for monitoring and interacting with the PeachCloud device
|
||||
|
||||
## Continuous Integration
|
||||
|
||||
[Drone CI](https://docs.drone.io/) is used to provide continuous integration for this workspace. The configuration file can be found in `.drone.yml` in the root of this repository. It is currently configured to run `cargo fmt`, `cargo clippy`, `cargo test` and `cargo build` on every `pull request` event. The pipeline runs on the AMD64 Debian Buster image from the official Rust Docker image repository.
|
||||
|
||||
The status of the current and previous CI builds can be viewed via the [Drone CI Build UI](https://build.coopcloud.tech/PeachCloud/peach-workspace) (kindly hosted by Co-op Cloud).
|
||||
|
||||
Adding `[CI SKIP]` to the end of a commit message results in the CI checks being skipped for the next event. For example:
|
||||
|
||||
```
|
||||
git commit -m "update readme [CI SKIP]"
|
||||
git push origin main
|
||||
```
|
||||
|
||||
## Developer Diaries
|
||||
|
||||
- [@ahdinosaur](https://github.com/ahdinosaur): `@6ilZq3kN0F+dXFHAPjAwMm87JEb/VdB+LC9eIMW3sa0=.ed25519`
|
||||
|
@ -56,4 +71,4 @@ _Better [Scuttlebutt](https://scuttlebutt.nz) cloud infrastructure as a hardware
|
|||
- [GitHub](https://github.com/peachcloud)
|
||||
- [Twitter](https://twitter.com/peachcloudorg)
|
||||
- [Email](mailto:peachcloudorg@gmail.com)
|
||||
- [OpenCollective](https://opencollective.com/peachcloud)
|
||||
- [OpenCollective](https://opencollective.com/peachcloud)
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
---
|
||||
|
||||
name: "Bug Report Template"
|
||||
about: "This template is for submitting bugs."
|
||||
title: "[BUG] "
|
||||
ref: "main"
|
||||
labels:
|
||||
|
||||
- bug
|
||||
- "help needed"
|
||||
|
||||
---
|
||||
|
||||
> Please fill out the sections below.
|
||||
> Be kind and objective when writing in text.
|
||||
> Thanks for the report! :)
|
||||
|
||||
**Brief description of the bug:**
|
||||
|
||||
|
||||
**Steps to reproduce the bug:**
|
||||
|
||||
|
||||
**Expected behaviour:**
|
||||
|
||||
|
||||
**Technical details:**
|
||||
|
||||
_Is peach-web running on an x86-64 or arm64 machine?_
|
||||
|
||||
|
||||
_What operating system distribution is it running on?_
|
||||
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
---
|
||||
|
||||
name: "Feature Suggestion Template"
|
||||
about: "This template is for submitting feature suggestions."
|
||||
title: "[FEATURE] "
|
||||
ref: "main"
|
||||
labels:
|
||||
|
||||
- enhancement
|
||||
|
||||
---
|
||||
|
||||
**Brief description of the feature you'd like to suggest:**
|
||||
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "peach-config"
|
||||
version = "0.1.17"
|
||||
version = "0.1.27"
|
||||
authors = ["Andrew Reid <gnomad@cryptolab.net>", "Max Fowler <max@mfowler.info>"]
|
||||
edition = "2018"
|
||||
description = "Command line tool for installing, updating and configuring PeachCloud"
|
||||
|
@ -37,3 +37,5 @@ log = "0.4"
|
|||
lazy_static = "1.4.0"
|
||||
peach-lib = { path = "../peach-lib" }
|
||||
rpassword = "5.0"
|
||||
golgi = { git = "https://git.coopcloud.tech/golgi-ssb/golgi.git" }
|
||||
async-std = "1.10.0"
|
||||
|
|
|
@ -10,7 +10,7 @@ pub const SERVICES: [&str; 8] = [
|
|||
"peach-buttons",
|
||||
"peach-oled",
|
||||
"peach-dyndns-updater",
|
||||
"peach-go-sbot",
|
||||
"go-sbot",
|
||||
"peach-config",
|
||||
];
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
#![allow(clippy::nonstandard_macro_braces)]
|
||||
use golgi::error::GolgiError;
|
||||
use peach_lib::error::PeachError;
|
||||
pub use snafu::ResultExt;
|
||||
use snafu::Snafu;
|
||||
|
@ -35,6 +36,14 @@ pub enum PeachConfigError {
|
|||
ChangePasswordError { source: PeachError },
|
||||
#[snafu(display("Entered passwords did not match. Please try again."))]
|
||||
InvalidPassword,
|
||||
#[snafu(display("Error in peach lib: {}", source))]
|
||||
PeachLibError { source: PeachError },
|
||||
#[snafu(display("Error in golgi: {}", source))]
|
||||
Golgi { source: GolgiError },
|
||||
#[snafu(display("{}", message))]
|
||||
CmdInputError { message: String },
|
||||
#[snafu(display("{}", message))]
|
||||
WaitForSbotError { message: String },
|
||||
}
|
||||
|
||||
impl From<std::io::Error> for PeachConfigError {
|
||||
|
@ -51,3 +60,15 @@ impl From<serde_json::Error> for PeachConfigError {
|
|||
PeachConfigError::SerdeError { source: err }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PeachError> for PeachConfigError {
|
||||
fn from(err: PeachError) -> PeachConfigError {
|
||||
PeachConfigError::PeachLibError { source: err }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<GolgiError> for PeachConfigError {
|
||||
fn from(err: GolgiError) -> PeachConfigError {
|
||||
PeachConfigError::Golgi { source: err }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,40 +1,32 @@
|
|||
use regex::Regex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use snafu::ResultExt;
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
|
||||
use crate::constants::HARDWARE_CONFIG_FILE;
|
||||
use crate::constants::{HARDWARE_CONFIG_FILE, SERVICES};
|
||||
use crate::error::{FileReadError, FileWriteError, PeachConfigError};
|
||||
use crate::utils::get_output;
|
||||
use crate::RtcOption;
|
||||
|
||||
/// Helper function which returns the version of a package currently installed,
|
||||
/// as an Ok(String) if found, and as an Err if not found
|
||||
pub fn get_package_version_number(package: &str) -> Result<String, PeachConfigError> {
|
||||
let version = get_output(&["dpkg-query", "--showformat=${Version}", "--show", package])?;
|
||||
Ok(version)
|
||||
}
|
||||
|
||||
/// Returns a HashMap<String, String> of all the peach-packages which are currently installed
|
||||
/// mapped to their version number e.g. { "peach-probe": "1.2.0", "peach-network": "1.4.0" }
|
||||
pub fn get_currently_installed_microservices() -> Result<HashMap<String, String>, PeachConfigError>
|
||||
{
|
||||
// gets a list of all packages currently installed with dpkg
|
||||
let packages = get_output(&["dpkg", "-l"])?;
|
||||
|
||||
// this regex matches packages which contain the word peach in them
|
||||
// and has two match groups
|
||||
// 1. the first match group gets the package name
|
||||
// 2. the second match group gets the version number of the package
|
||||
let re: Regex = Regex::new(r"\S+\s+(\S*peach\S+)\s+(\S+).*\n").unwrap();
|
||||
|
||||
// the following iterator, iterates through the captures matched via the regex
|
||||
// and for each capture, creates a value in the hash map,
|
||||
// which maps the name of the package, to its version number
|
||||
// e.g. { "peach-probe": "1.2.0", "peach-network": "1.4.0" }
|
||||
let peach_packages: HashMap<String, String> = re
|
||||
.captures_iter(&packages)
|
||||
.filter_map(|cap| {
|
||||
let groups = (cap.get(1), cap.get(2));
|
||||
match groups {
|
||||
(Some(package), Some(version)) => {
|
||||
Some((package.as_str().to_string(), version.as_str().to_string()))
|
||||
}
|
||||
_ => None,
|
||||
// gets a list of all packages currently installed with dpkg-query
|
||||
let peach_packages: HashMap<String, String> = SERVICES
|
||||
.iter()
|
||||
.filter_map(|service| {
|
||||
let version = get_package_version_number(service);
|
||||
match version {
|
||||
Ok(v) => Some((service.to_string(), v)),
|
||||
Err(_) => None,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
|
|
@ -2,12 +2,15 @@ mod change_password;
|
|||
mod constants;
|
||||
mod error;
|
||||
mod generate_manifest;
|
||||
mod publish_address;
|
||||
mod set_permissions;
|
||||
mod setup_networking;
|
||||
mod setup_peach;
|
||||
mod setup_peach_deb;
|
||||
mod status;
|
||||
mod update;
|
||||
mod utils;
|
||||
mod wait_for_sbot;
|
||||
|
||||
use clap::arg_enum;
|
||||
use log::error;
|
||||
|
@ -44,12 +47,25 @@ enum PeachConfig {
|
|||
Update(UpdateOpts),
|
||||
|
||||
/// Changes the password for the peach-web interface
|
||||
#[structopt(name = "changepassword")]
|
||||
#[structopt(name = "change-password")]
|
||||
ChangePassword(ChangePasswordOpts),
|
||||
|
||||
/// Updates file permissions on PeachCloud device
|
||||
#[structopt(name = "permissions")]
|
||||
SetPermissions,
|
||||
|
||||
/// Returns sbot id if sbot is running
|
||||
#[structopt(name = "whoami")]
|
||||
WhoAmI,
|
||||
|
||||
/// Publish domain and port.
|
||||
/// It takes an address argument of the form host:port
|
||||
#[structopt(name = "publish-address")]
|
||||
PublishAddress(PublishAddressOpts),
|
||||
|
||||
/// Wait for a successful connection to sbot
|
||||
#[structopt(name = "wait-for-sbot")]
|
||||
WaitForSbot,
|
||||
}
|
||||
|
||||
#[derive(StructOpt, Debug)]
|
||||
|
@ -90,6 +106,13 @@ pub struct ChangePasswordOpts {
|
|||
password: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(StructOpt, Debug)]
|
||||
pub struct PublishAddressOpts {
|
||||
/// Specify address in the form domain:port
|
||||
#[structopt(short, long)]
|
||||
address: String,
|
||||
}
|
||||
|
||||
arg_enum! {
|
||||
/// enum options for real-time clock choices
|
||||
#[derive(Debug)]
|
||||
|
@ -102,7 +125,7 @@ arg_enum! {
|
|||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
async fn run() {
|
||||
// initialize the logger
|
||||
env_logger::init();
|
||||
|
||||
|
@ -155,6 +178,42 @@ fn main() {
|
|||
)
|
||||
}
|
||||
},
|
||||
PeachConfig::WhoAmI => match status::whoami().await {
|
||||
Ok(sbot_id) => {
|
||||
println!("{:?}", sbot_id);
|
||||
{}
|
||||
}
|
||||
Err(err) => {
|
||||
error!("sbot whoami encountered an error: {}", err)
|
||||
}
|
||||
},
|
||||
PeachConfig::PublishAddress(opts) => {
|
||||
match publish_address::publish_address(opts.address).await {
|
||||
Ok(_) => {}
|
||||
Err(err) => {
|
||||
error!(
|
||||
"peach-config encountered an error during publish address: {}",
|
||||
err
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
PeachConfig::WaitForSbot => match wait_for_sbot::wait_for_sbot().await {
|
||||
Ok(sbot_id) => {
|
||||
println!("connected with sbot and found sbot_id: {:?}", sbot_id)
|
||||
}
|
||||
Err(err) => {
|
||||
error!("peach-config did not successfully connect to sbot: {}", err)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Enable an async main function and execute the `run()` function,
|
||||
// catching any errors and printing them to `stderr` before exiting the
|
||||
// process.
|
||||
#[async_std::main]
|
||||
async fn main() {
|
||||
run().await;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
use crate::error::PeachConfigError;
|
||||
use golgi::kuska_ssb::api::dto::content::PubAddress;
|
||||
use golgi::messages::SsbMessageContent;
|
||||
use peach_lib::sbot::init_sbot;
|
||||
|
||||
/// Utility function to publish the address (domain:port) of the pub
|
||||
/// publishing the address causes the domain and port to be used for invite generation,
|
||||
/// and also gossips this pub address to their peers
|
||||
pub async fn publish_address(address: String) -> Result<(), PeachConfigError> {
|
||||
// split address into domain:port
|
||||
let split: Vec<&str> = address.split(':').collect();
|
||||
let (domain, port): (&str, &str) = (split[0], split[1]);
|
||||
// convert port to u16
|
||||
let port_as_u16: u16 = port
|
||||
.parse()
|
||||
.map_err(|_err| PeachConfigError::CmdInputError {
|
||||
message: "Failure to parse domain and port. Address must be of the format host:port."
|
||||
.to_string(),
|
||||
})?;
|
||||
// publish address
|
||||
let mut sbot = init_sbot().await?;
|
||||
let pub_id = sbot.whoami().await?;
|
||||
// Compose a `pub` address type message.
|
||||
let pub_address_msg = SsbMessageContent::Pub {
|
||||
address: Some(PubAddress {
|
||||
// Host name (can be an IP address if onboarding over WiFi).
|
||||
host: Some(domain.to_string()),
|
||||
// Port.
|
||||
port: port_as_u16,
|
||||
// Public key.
|
||||
key: pub_id,
|
||||
}),
|
||||
};
|
||||
// Publish the `pub` address message.
|
||||
let _pub_msg_ref = sbot.publish(pub_address_msg).await?;
|
||||
Ok(())
|
||||
}
|
|
@ -1,21 +1,30 @@
|
|||
use lazy_static::lazy_static;
|
||||
|
||||
use peach_lib::config_manager;
|
||||
|
||||
use crate::error::PeachConfigError;
|
||||
use crate::utils::cmd;
|
||||
|
||||
/// All configs are stored in this folder, and should be read/writeable by peach group
|
||||
/// so they can be read and written by all PeachCloud services.
|
||||
pub const CONFIGS_DIR: &str = "/var/lib/peachcloud";
|
||||
pub const PEACH_WEB_DIR: &str = "/usr/share/peach-web";
|
||||
lazy_static! {
|
||||
pub static ref PEACH_CONFIGDIR: String = config_manager::get_config_value("PEACH_CONFIGDIR")
|
||||
.expect("Failed to load config value for PEACH_CONFIGDIR");
|
||||
pub static ref PEACH_WEBDIR: String = config_manager::get_config_value("PEACH_WEBDIR")
|
||||
.expect("Failed to load config value for PEACH_WEBDIR");
|
||||
pub static ref PEACH_HOMEDIR: String = config_manager::get_config_value("PEACH_HOMEDIR")
|
||||
.expect("Failed to load config value for PEACH_HOMEDIR");
|
||||
}
|
||||
|
||||
/// Utility function to set correct file permissions on the PeachCloud device.
|
||||
/// Accidentally changing file permissions is a fairly common thing to happen,
|
||||
/// so this is a useful CLI function for quickly correcting anything that may be out of order.
|
||||
pub fn set_permissions() -> Result<(), PeachConfigError> {
|
||||
println!("[ UPDATING FILE PERMISSIONS ON PEACHCLOUD DEVICE ]");
|
||||
cmd(&["chmod", "-R", "u+rwX,g+rwX", CONFIGS_DIR])?;
|
||||
cmd(&["chown", "-R", "peach", CONFIGS_DIR])?;
|
||||
cmd(&["chgrp", "-R", "peach", CONFIGS_DIR])?;
|
||||
cmd(&["chmod", "-R", "u+rwX,g+rwX", PEACH_WEB_DIR])?;
|
||||
cmd(&["chown", "-R", "peach-web:peach", PEACH_WEB_DIR])?;
|
||||
cmd(&["chmod", "-R", "u+rwX,g+rwX", &PEACH_CONFIGDIR])?;
|
||||
cmd(&["chown", "-R", "peach:peach", &PEACH_CONFIGDIR])?;
|
||||
cmd(&["chmod", "-R", "u+rwX,g+rwX", &PEACH_WEBDIR])?;
|
||||
cmd(&["chown", "-R", "peach:peach", &PEACH_WEBDIR])?;
|
||||
cmd(&["chmod", "-R", "u+rwX,g+rwX", &PEACH_HOMEDIR])?;
|
||||
cmd(&["chown", "-R", "peach:peach", &PEACH_HOMEDIR])?;
|
||||
println!("[ PERMISSIONS SUCCESSFULLY UPDATED ]");
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ use std::fs;
|
|||
|
||||
use crate::error::{FileWriteError, PeachConfigError};
|
||||
use crate::generate_manifest::save_hardware_config;
|
||||
use crate::set_permissions::set_permissions;
|
||||
use crate::setup_networking::configure_networking;
|
||||
use crate::setup_peach_deb::setup_peach_deb;
|
||||
use crate::update::update_microservices;
|
||||
|
@ -239,6 +240,9 @@ pub fn setup_peach(
|
|||
info!("[ SAVING LOG OF HARDWARE CONFIGURATIONS ]");
|
||||
save_hardware_config(i2c, rtc)?;
|
||||
|
||||
info!("[ SETTING FILE PERMISSIONS ]");
|
||||
set_permissions()?;
|
||||
|
||||
info!("[ PEACHCLOUD SETUP COMPLETE ]");
|
||||
info!("[ ------------------------- ]");
|
||||
info!("[ please reboot your device ]");
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
use crate::error::PeachConfigError;
|
||||
use peach_lib::sbot::init_sbot;
|
||||
|
||||
/// Utility function to check if sbot is running via the whoami method
|
||||
pub async fn whoami() -> Result<String, PeachConfigError> {
|
||||
let mut sbot = init_sbot().await?;
|
||||
let sbot_id = sbot.whoami().await?;
|
||||
Ok(sbot_id)
|
||||
}
|
|
@ -47,8 +47,8 @@ pub fn update_microservices() -> Result<(), PeachConfigError> {
|
|||
cmd(&["apt-get", "update"])?;
|
||||
// filter out peach-config from list of services
|
||||
let services_to_update: Vec<&str> = SERVICES
|
||||
.to_vec()
|
||||
.into_iter()
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|&x| x != "peach-config")
|
||||
.collect();
|
||||
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
use std::{thread, time};
|
||||
|
||||
use crate::error::PeachConfigError;
|
||||
use peach_lib::sbot::init_sbot;
|
||||
|
||||
static MAX_NUM_ATTEMPTS: u8 = 10;
|
||||
|
||||
/// Utility function to wait for a successful whoami call with sbot
|
||||
/// After each attempt to call whoami it waits 2 seconds,
|
||||
/// and if after MAX_NUM_ATTEMPTS (10) there is no successful whoami call
|
||||
/// it returns an Error. Otherwise it returns Ok(sbot_id).
|
||||
pub async fn wait_for_sbot() -> Result<String, PeachConfigError> {
|
||||
let mut num_attempts = 0;
|
||||
let mut whoami = None;
|
||||
|
||||
while num_attempts < MAX_NUM_ATTEMPTS {
|
||||
let mut sbot = None;
|
||||
|
||||
let sbot_res = init_sbot().await;
|
||||
match sbot_res {
|
||||
Ok(sbot_instance) => {
|
||||
sbot = Some(sbot_instance);
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!("failed to connect to sbot: {:?}", err);
|
||||
}
|
||||
}
|
||||
|
||||
if sbot.is_some() {
|
||||
let sbot_id_res = sbot.unwrap().whoami().await;
|
||||
match sbot_id_res {
|
||||
Ok(sbot_id) => {
|
||||
whoami = Some(sbot_id);
|
||||
break;
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!("whoami failed: {:?}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!("trying to connect to sbot again {:?}", num_attempts);
|
||||
num_attempts += 1;
|
||||
|
||||
let sleep_duration = time::Duration::from_secs(2);
|
||||
thread::sleep(sleep_duration);
|
||||
}
|
||||
|
||||
whoami.ok_or(PeachConfigError::WaitForSbotError {
|
||||
message: "Failed to find sbot_id after 10 attempts".to_string(),
|
||||
})
|
||||
}
|
|
@ -1,19 +1,24 @@
|
|||
[package]
|
||||
name = "peach-lib"
|
||||
version = "1.3.2"
|
||||
version = "1.3.4"
|
||||
authors = ["Andrew Reid <glyph@mycelial.technology>"]
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
chrono = "0.4.19"
|
||||
fslock="0.1.6"
|
||||
async-std = "1.10"
|
||||
chrono = "0.4"
|
||||
dirs = "4.0"
|
||||
fslock="0.1"
|
||||
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"
|
||||
jsonrpc-core = "8.0"
|
||||
log = "0.4"
|
||||
nanorand = "0.6.1"
|
||||
nanorand = { version = "0.6", features = ["getrandom"] }
|
||||
regex = "1"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
serde_yaml = "0.8"
|
||||
sha3 = "0.10.0"
|
||||
toml = "0.5"
|
||||
sha3 = "0.10"
|
||||
lazy_static = "1.4"
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
[target.aarch64-unknown-linux-gnu]
|
||||
linker = "aarch64-linux-gnu-gcc"
|
||||
objcopy = { path ="aarch64-linux-gnu-objcopy" }
|
||||
strip = { path ="aarch64-linux-gnu-strip" }
|
|
@ -1,12 +0,0 @@
|
|||
[package]
|
||||
name = "debug"
|
||||
version = "0.1.0"
|
||||
authors = ["notplants <mfowler.email@gmail.com>"]
|
||||
edition = "2018"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
peach-lib = { path = "../" }
|
||||
env_logger = "0.6"
|
||||
chrono = "0.4.19"
|
|
@ -1,65 +0,0 @@
|
|||
use peach_lib::dyndns_client::{dyndns_update_ip, register_domain, is_dns_updater_online, log_successful_nsupdate, get_num_seconds_since_successful_dns_update };
|
||||
use peach_lib::password_utils::{verify_password, set_new_password, verify_temporary_password, set_new_temporary_password, send_password_reset};
|
||||
use peach_lib::config_manager::{add_ssb_admin_id, delete_ssb_admin_id};
|
||||
use peach_lib::sbot_client;
|
||||
use std::process;
|
||||
use chrono::prelude::*;
|
||||
|
||||
|
||||
fn main() {
|
||||
// initalize the logger
|
||||
env_logger::init();
|
||||
//
|
||||
// println!("Hello, world its debug!");
|
||||
// let result = set_new_password("password3");
|
||||
// println!("result: {:?}", result);
|
||||
//
|
||||
// let result = verify_password("password1");
|
||||
// println!("result should be error: {:?}", result);
|
||||
//
|
||||
// let result = verify_password("password3");
|
||||
// println!("result should be ok: {:?}", result);
|
||||
//
|
||||
//
|
||||
// println!("Testing temporary passwords");
|
||||
// let result = set_new_temporary_password("abcd");
|
||||
// println!("result: {:?}", result);
|
||||
//
|
||||
// let result = verify_temporary_password("password1");
|
||||
// println!("result should be error: {:?}", result);
|
||||
//
|
||||
// let result = verify_temporary_password("abcd");
|
||||
// println!("result should be ok: {:?}", result);
|
||||
//
|
||||
let result = send_password_reset();
|
||||
println!("send password reset result should be ok: {:?}", result);
|
||||
|
||||
// sbot_client::post("hi cat");
|
||||
// let result = sbot_client::whoami();
|
||||
// let result = sbot_client::create_invite(50);
|
||||
// let result = sbot_client::post("is this working");
|
||||
// println!("result: {:?}", result);
|
||||
// let result = sbot_client::post("nice we have contact");
|
||||
// let result = sbot_client::update_pub_name("vermont-pub");
|
||||
// let result = sbot_client::private_message("this is a private message", "@LZx+HP6/fcjUm7vef2eaBKAQ9gAKfzmrMVGzzdJiQtA=.ed25519");
|
||||
// println!("result: {:?}", result);
|
||||
|
||||
// let result = send_password_reset();
|
||||
// let result = add_ssb_admin_id("xyzdab");
|
||||
// println!("result: {:?}", result);
|
||||
// let result = delete_ssb_admin_id("xyzdab");
|
||||
// println!("result: {:?}", result);
|
||||
// let result = delete_ssb_admin_id("ab");
|
||||
// println!("result: {:?}", result);
|
||||
|
||||
//// let result = log_successful_nsupdate();
|
||||
//// let result = get_num_seconds_since_successful_dns_update();
|
||||
// let is_online = is_dns_updater_online();
|
||||
// println!("is online: {:?}", is_online);
|
||||
//
|
||||
//// let result = get_last_successful_dns_update();
|
||||
//// println!("result: {:?}", result);
|
||||
//// register_domain("newquarter299.dyn.peachcloud.org");
|
||||
// let result = dyndns_update_ip();
|
||||
// println!("result: {:?}", result);
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
use peach_lib::config_manager::{get_config_value, save_config_value};
|
||||
|
||||
fn main() {
|
||||
println!("Running example of PeachCloud configuration management");
|
||||
let v = get_config_value("ADDR").unwrap();
|
||||
println!("ADDR: {}", v);
|
||||
|
||||
save_config_value("ADDR", "1.1.1.1");
|
||||
let v = get_config_value("ADDR").unwrap();
|
||||
println!("ADDR: {}", v);
|
||||
}
|
|
@ -1,167 +1,280 @@
|
|||
//! 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.
|
||||
//!
|
||||
//! Config values are looked up from three locations in this order by key name:
|
||||
//! 1. from environmental variables
|
||||
//! 2. from a configuration file
|
||||
//! 3. from default values
|
||||
//!
|
||||
//! The configuration file is located at: "/var/lib/peachcloud/config.yml"
|
||||
//! unless its path is configured by setting PEACH_CONFIG_PATH env variable.
|
||||
|
||||
use std::fs;
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
use std::{env, fs};
|
||||
|
||||
use fslock::LockFile;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use lazy_static::lazy_static;
|
||||
use log::debug;
|
||||
|
||||
use crate::error::PeachError;
|
||||
|
||||
// main configuration file
|
||||
pub const YAML_PATH: &str = "/var/lib/peachcloud/config.yml";
|
||||
|
||||
// lock file (used to avoid race conditions during config reading & writing)
|
||||
pub const LOCK_FILE_PATH: &str = "/var/lib/peachcloud/config.lock";
|
||||
|
||||
// default values
|
||||
pub const DEFAULT_DYN_SERVER_ADDRESS: &str = "http://dynserver.dyn.peachcloud.org";
|
||||
pub const DEFAULT_DYN_NAMESERVER: &str = "ns.peachcloud.org";
|
||||
|
||||
// we make use of Serde default values in order to make PeachCloud
|
||||
// robust and keep running even with a not fully complete config.yml
|
||||
// main type which represents all peachcloud configurations
|
||||
#[derive(Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct PeachConfig {
|
||||
#[serde(default)]
|
||||
pub external_domain: String,
|
||||
#[serde(default)]
|
||||
pub dyn_domain: String,
|
||||
#[serde(default)]
|
||||
pub dyn_dns_server_address: String,
|
||||
#[serde(default)]
|
||||
pub dyn_use_custom_server: bool,
|
||||
#[serde(default)]
|
||||
pub dyn_nameserver: String,
|
||||
#[serde(default)]
|
||||
pub dyn_tsig_key_path: String,
|
||||
#[serde(default)] // default is false
|
||||
pub dyn_enabled: bool,
|
||||
#[serde(default)] // default is empty vector
|
||||
pub ssb_admin_ids: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub admin_password_hash: String,
|
||||
#[serde(default)]
|
||||
pub temporary_password_hash: String,
|
||||
// load path to main configuration file
|
||||
// from PEACH_CONFIG_PATH if that environment variable is set
|
||||
// or using the default value if not set
|
||||
pub const DEFAULT_YAML_PATH: &str = "/var/lib/peachcloud/config.yml";
|
||||
lazy_static! {
|
||||
static ref CONFIG_PATH: String = {
|
||||
if let Ok(val) = env::var("PEACH_CONFIG_PATH") {
|
||||
val
|
||||
}
|
||||
else {
|
||||
DEFAULT_YAML_PATH.to_string()
|
||||
}
|
||||
};
|
||||
// lock file (used to avoid race conditions during config reading & writing)
|
||||
// the lock file path is the config file path + ".lock"
|
||||
static ref LOCK_FILE_PATH: String = format!("{}.lock", *CONFIG_PATH);
|
||||
}
|
||||
|
||||
// helper functions for serializing and deserializing PeachConfig from disc
|
||||
fn save_peach_config(peach_config: PeachConfig) -> Result<PeachConfig, PeachError> {
|
||||
// Default values for PeachCloud configs which are used for any key which is not set
|
||||
// via an environment variable or in a saved configuration file.
|
||||
pub fn get_peach_config_defaults() -> HashMap<String, String> {
|
||||
let peach_config_defaults: HashMap<&str, &str> = HashMap::from([
|
||||
("STANDALONE_MODE", "true"),
|
||||
("DISABLE_AUTH", "false"),
|
||||
("ADDR", "127.0.0.1"),
|
||||
("PORT", "8000"),
|
||||
("EXTERNAL_DOMAIN", ""),
|
||||
("DYN_DOMAIN", ""),
|
||||
(
|
||||
"DYN_DNS_SERVER_ADDRESS",
|
||||
"http://dynserver.dyn.peachcloud.org",
|
||||
),
|
||||
("DYN_USE_CUSTOM_SERVER", "true"),
|
||||
("DYN_TSIG_KEY_PATH", ""),
|
||||
("DYN_NAMESERVER", "ns.peachcloud.org"),
|
||||
("DYN_ENABLED", "false"),
|
||||
("SSB_ADMIN_IDS", ""),
|
||||
("ADMIN_PASSWORD_HASH", "47"),
|
||||
("TEMPORARY_PASSWORD_HASH", ""),
|
||||
("GO_SBOT_DATADIR", "/home/peach/.ssb-go"),
|
||||
("GO_SBOT_SERVICE", "go-sbot.service"),
|
||||
("PEACH_CONFIGDIR", "/var/lib/peachcloud"),
|
||||
("PEACH_HOMEDIR", "/home/peach"),
|
||||
("PEACH_WEBDIR", "/usr/share/peach-web"),
|
||||
]);
|
||||
// convert HashMap<&str, &str> to HashMap<String, String> and return
|
||||
let pc_defaults: HashMap<String, String> = peach_config_defaults
|
||||
.iter()
|
||||
.map(|(key, val)| (key.to_string(), val.to_string()))
|
||||
.collect();
|
||||
pc_defaults
|
||||
}
|
||||
|
||||
// primary interface for getting config values
|
||||
// Config values are looked up from three locations in this order by key name:
|
||||
// 1. from environmental variables
|
||||
// 2. from a configuration file
|
||||
// 3. from default values
|
||||
pub fn get_config_value(key: &str) -> Result<String, PeachError> {
|
||||
// first check if there is an environmental variable set
|
||||
if let Ok(val) = env::var(key) {
|
||||
Ok(val)
|
||||
} else {
|
||||
// then check if a value is set in the config file
|
||||
let peach_config_on_disc = load_peach_config_from_disc()?;
|
||||
let val = peach_config_on_disc.get(key);
|
||||
// if no value is found in the config file, then get the default value
|
||||
match val {
|
||||
// return config value
|
||||
Some(v) => Ok(v.to_string()),
|
||||
// get default value
|
||||
None => {
|
||||
match get_peach_config_defaults().get(key) {
|
||||
Some(v) => Ok(v.to_string()),
|
||||
// if this key was not found in the defaults, then it was an invalid key
|
||||
None => Err(PeachError::InvalidKey {
|
||||
key: key.to_string(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// helper function to load PeachCloud configuration file saved to disc
|
||||
pub fn load_peach_config_from_disc() -> Result<HashMap<String, String>, PeachError> {
|
||||
let peach_config_exists = std::path::Path::new(CONFIG_PATH.as_str()).exists();
|
||||
// if config file does not exist, return an emtpy HashMap
|
||||
if !peach_config_exists {
|
||||
let peach_config: HashMap<String, String> = HashMap::new();
|
||||
Ok(peach_config)
|
||||
}
|
||||
// otherwise we load peach config from disk
|
||||
else {
|
||||
debug!("Loading peach config: {} exists", CONFIG_PATH.as_str());
|
||||
let contents =
|
||||
fs::read_to_string(CONFIG_PATH.as_str()).map_err(|source| PeachError::Read {
|
||||
source,
|
||||
path: CONFIG_PATH.to_string(),
|
||||
})?;
|
||||
let peach_config: HashMap<String, String> = serde_yaml::from_str(&contents)?;
|
||||
Ok(peach_config)
|
||||
}
|
||||
}
|
||||
|
||||
// helper function to save PeachCloud configuration file to disc
|
||||
// takes in a Hashmap<String, String> and saves the whole HashMap as a yaml file
|
||||
// with the keys in alphabetical order
|
||||
pub fn save_peach_config_to_disc(
|
||||
peach_config: HashMap<String, String>,
|
||||
) -> Result<HashMap<String, String>, PeachError> {
|
||||
// use a file lock to avoid race conditions while saving config
|
||||
let mut lock = LockFile::open(LOCK_FILE_PATH)?;
|
||||
let mut lock = LockFile::open(&*LOCK_FILE_PATH).map_err(|source| PeachError::Read {
|
||||
source,
|
||||
path: LOCK_FILE_PATH.to_string(),
|
||||
})?;
|
||||
lock.lock()?;
|
||||
|
||||
let yaml_str = serde_yaml::to_string(&peach_config)?;
|
||||
// first convert Hashmap to BTreeMap (so that keys are saved in deterministic alphabetical order)
|
||||
let ordered: BTreeMap<_, _> = peach_config.iter().collect();
|
||||
// then serialize BTreeMap as yaml
|
||||
let yaml_str = serde_yaml::to_string(&ordered)?;
|
||||
|
||||
fs::write(YAML_PATH, yaml_str).map_err(|source| PeachError::Write {
|
||||
// write yaml to file
|
||||
fs::write(CONFIG_PATH.as_str(), yaml_str).map_err(|source| PeachError::Write {
|
||||
source,
|
||||
path: YAML_PATH.to_string(),
|
||||
path: CONFIG_PATH.to_string(),
|
||||
})?;
|
||||
|
||||
// unlock file lock
|
||||
lock.unlock()?;
|
||||
|
||||
// return peach_config
|
||||
// return modified HashMap
|
||||
Ok(peach_config)
|
||||
}
|
||||
|
||||
pub fn load_peach_config() -> Result<PeachConfig, PeachError> {
|
||||
let peach_config_exists = std::path::Path::new(YAML_PATH).exists();
|
||||
// helper functions for serializing and deserializing PeachConfig values from disc
|
||||
pub fn save_config_value(key: &str, value: &str) -> Result<HashMap<String, String>, PeachError> {
|
||||
// get current config from disc
|
||||
let mut peach_config = load_peach_config_from_disc()?;
|
||||
|
||||
let peach_config: PeachConfig = if !peach_config_exists {
|
||||
PeachConfig {
|
||||
external_domain: "".to_string(),
|
||||
dyn_domain: "".to_string(),
|
||||
dyn_dns_server_address: DEFAULT_DYN_SERVER_ADDRESS.to_string(),
|
||||
dyn_use_custom_server: false,
|
||||
dyn_nameserver: DEFAULT_DYN_NAMESERVER.to_string(),
|
||||
dyn_tsig_key_path: "".to_string(),
|
||||
dyn_enabled: false,
|
||||
ssb_admin_ids: Vec::new(),
|
||||
admin_password_hash: "".to_string(),
|
||||
temporary_password_hash: "".to_string(),
|
||||
}
|
||||
}
|
||||
// otherwise we load peach config from disk
|
||||
else {
|
||||
let contents = fs::read_to_string(YAML_PATH).map_err(|source| PeachError::Read {
|
||||
source,
|
||||
path: YAML_PATH.to_string(),
|
||||
})?;
|
||||
serde_yaml::from_str(&contents)?
|
||||
};
|
||||
// insert new key/value
|
||||
peach_config.insert(key.to_string(), value.to_string());
|
||||
|
||||
Ok(peach_config)
|
||||
// save the modified hashmap to disc
|
||||
save_peach_config_to_disc(peach_config)
|
||||
}
|
||||
|
||||
// interfaces for setting specific config values
|
||||
// set all dyn configuration values at once
|
||||
pub fn set_peach_dyndns_config(
|
||||
dyn_domain: &str,
|
||||
dyn_dns_server_address: &str,
|
||||
dyn_tsig_key_path: &str,
|
||||
dyn_enabled: bool,
|
||||
) -> Result<PeachConfig, PeachError> {
|
||||
let mut peach_config = load_peach_config()?;
|
||||
peach_config.dyn_domain = dyn_domain.to_string();
|
||||
peach_config.dyn_dns_server_address = dyn_dns_server_address.to_string();
|
||||
peach_config.dyn_tsig_key_path = dyn_tsig_key_path.to_string();
|
||||
peach_config.dyn_enabled = dyn_enabled;
|
||||
save_peach_config(peach_config)
|
||||
) -> Result<HashMap<String, String>, PeachError> {
|
||||
let mut peach_config = load_peach_config_from_disc()?;
|
||||
let dyn_enabled_str = match dyn_enabled {
|
||||
true => "true",
|
||||
false => "false",
|
||||
};
|
||||
peach_config.insert("DYN_DOMAIN".to_string(), dyn_domain.to_string());
|
||||
peach_config.insert(
|
||||
"DYN_DNS_SERVER_ADDRESS".to_string(),
|
||||
dyn_dns_server_address.to_string(),
|
||||
);
|
||||
peach_config.insert(
|
||||
"DYN_TSIG_KEY_PATH".to_string(),
|
||||
dyn_tsig_key_path.to_string(),
|
||||
);
|
||||
peach_config.insert("DYN_ENABLED".to_string(), dyn_enabled_str.to_string());
|
||||
save_peach_config_to_disc(peach_config)
|
||||
}
|
||||
|
||||
pub fn set_external_domain(new_external_domain: &str) -> Result<PeachConfig, PeachError> {
|
||||
let mut peach_config = load_peach_config()?;
|
||||
peach_config.external_domain = new_external_domain.to_string();
|
||||
save_peach_config(peach_config)
|
||||
pub fn set_external_domain(
|
||||
new_external_domain: &str,
|
||||
) -> Result<HashMap<String, String>, PeachError> {
|
||||
save_config_value("EXTERNAL_DOMAIN", new_external_domain)
|
||||
}
|
||||
|
||||
pub fn get_peachcloud_domain() -> Result<Option<String>, PeachError> {
|
||||
let peach_config = load_peach_config()?;
|
||||
if !peach_config.external_domain.is_empty() {
|
||||
Ok(Some(peach_config.external_domain))
|
||||
} else if !peach_config.dyn_domain.is_empty() {
|
||||
Ok(Some(peach_config.dyn_domain))
|
||||
let external_domain = get_config_value("EXTERNAL_DOMAIN")?;
|
||||
let dyn_domain = get_config_value("DYN_DOMAIN")?;
|
||||
if !external_domain.is_empty() {
|
||||
Ok(Some(external_domain))
|
||||
} else if !dyn_domain.is_empty() {
|
||||
Ok(Some(dyn_domain))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_dyndns_server_address() -> Result<String, PeachError> {
|
||||
let peach_config = load_peach_config()?;
|
||||
// if the user is using a custom dyn server then load the address from the config
|
||||
if peach_config.dyn_use_custom_server {
|
||||
Ok(peach_config.dyn_dns_server_address)
|
||||
}
|
||||
// otherwise hardcode the address
|
||||
else {
|
||||
Ok(DEFAULT_DYN_SERVER_ADDRESS.to_string())
|
||||
get_config_value("DYN_DNS_SERVER_ADDRESS")
|
||||
}
|
||||
|
||||
pub fn set_dyndns_enabled_value(
|
||||
enabled_value: bool,
|
||||
) -> Result<HashMap<String, String>, PeachError> {
|
||||
match enabled_value {
|
||||
true => save_config_value("DYN_ENABLED", "true"),
|
||||
false => save_config_value("DYN_ENABLED", "false"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_dyndns_enabled_value(enabled_value: bool) -> Result<PeachConfig, PeachError> {
|
||||
let mut peach_config = load_peach_config()?;
|
||||
peach_config.dyn_enabled = enabled_value;
|
||||
save_peach_config(peach_config)
|
||||
pub fn get_dyndns_enabled_value() -> Result<bool, PeachError> {
|
||||
let val = get_config_value("DYN_ENABLED")?;
|
||||
Ok(val == "true")
|
||||
}
|
||||
|
||||
pub fn add_ssb_admin_id(ssb_id: &str) -> Result<PeachConfig, PeachError> {
|
||||
let mut peach_config = load_peach_config()?;
|
||||
peach_config.ssb_admin_ids.push(ssb_id.to_string());
|
||||
save_peach_config(peach_config)
|
||||
pub fn set_admin_password_hash(
|
||||
password_hash: String,
|
||||
) -> Result<HashMap<String, String>, PeachError> {
|
||||
save_config_value("ADMIN_PASSWORD_HASH", &password_hash)
|
||||
}
|
||||
|
||||
pub fn delete_ssb_admin_id(ssb_id: &str) -> Result<PeachConfig, PeachError> {
|
||||
let mut peach_config = load_peach_config()?;
|
||||
let mut ssb_admin_ids = peach_config.ssb_admin_ids;
|
||||
pub fn get_admin_password_hash() -> Result<String, PeachError> {
|
||||
let admin_password_hash = get_config_value("ADMIN_PASSWORD_HASH")?;
|
||||
if !admin_password_hash.is_empty() {
|
||||
Ok(admin_password_hash)
|
||||
} else {
|
||||
Err(PeachError::PasswordNotSet)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_temporary_password_hash(
|
||||
password_hash: String,
|
||||
) -> Result<HashMap<String, String>, PeachError> {
|
||||
save_config_value("TEMPORARY_PASSWORD_HASH", &password_hash)
|
||||
}
|
||||
|
||||
pub fn get_temporary_password_hash() -> Result<String, PeachError> {
|
||||
let admin_password_hash = get_config_value("TEMPORARY_PASSWORD_HASH")?;
|
||||
if !admin_password_hash.is_empty() {
|
||||
Ok(admin_password_hash)
|
||||
} else {
|
||||
Err(PeachError::PasswordNotSet)
|
||||
}
|
||||
}
|
||||
|
||||
// add ssb_id to vector of admin ids and save new value for SSB_ADMIN_IDS
|
||||
pub fn add_ssb_admin_id(ssb_id: &str) -> Result<Vec<String>, PeachError> {
|
||||
let mut ssb_admin_ids = get_ssb_admin_ids()?;
|
||||
ssb_admin_ids.push(ssb_id.to_string());
|
||||
save_ssb_admin_ids(ssb_admin_ids)
|
||||
}
|
||||
|
||||
// remove ssb_id from vector of admin ids if found and save new value for SSB_ADMIN_IDS
|
||||
// if value is not found then return an error
|
||||
pub fn delete_ssb_admin_id(ssb_id: &str) -> Result<Vec<String>, PeachError> {
|
||||
let mut ssb_admin_ids = get_ssb_admin_ids()?;
|
||||
let index_result = ssb_admin_ids.iter().position(|x| *x == ssb_id);
|
||||
match index_result {
|
||||
Some(index) => {
|
||||
ssb_admin_ids.remove(index);
|
||||
peach_config.ssb_admin_ids = ssb_admin_ids;
|
||||
save_peach_config(peach_config)
|
||||
save_ssb_admin_ids(ssb_admin_ids)
|
||||
}
|
||||
None => Err(PeachError::SsbAdminIdNotFound {
|
||||
id: ssb_id.to_string(),
|
||||
|
@ -169,32 +282,16 @@ pub fn delete_ssb_admin_id(ssb_id: &str) -> Result<PeachConfig, PeachError> {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn set_admin_password_hash(password_hash: &str) -> Result<PeachConfig, PeachError> {
|
||||
let mut peach_config = load_peach_config()?;
|
||||
peach_config.admin_password_hash = password_hash.to_string();
|
||||
save_peach_config(peach_config)
|
||||
// looks up the String value for SSB_ADMIN_IDS and converts it into a Vec<String>
|
||||
pub fn get_ssb_admin_ids() -> Result<Vec<String>, PeachError> {
|
||||
let ssb_admin_ids_str = get_config_value("SSB_ADMIN_IDS")?;
|
||||
let ssb_admin_ids: Vec<String> = serde_json::from_str(&ssb_admin_ids_str)?;
|
||||
Ok(ssb_admin_ids)
|
||||
}
|
||||
|
||||
pub fn get_admin_password_hash() -> Result<String, PeachError> {
|
||||
let peach_config = load_peach_config()?;
|
||||
if !peach_config.admin_password_hash.is_empty() {
|
||||
Ok(peach_config.admin_password_hash)
|
||||
} else {
|
||||
Err(PeachError::PasswordNotSet)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_temporary_password_hash(password_hash: &str) -> Result<PeachConfig, PeachError> {
|
||||
let mut peach_config = load_peach_config()?;
|
||||
peach_config.temporary_password_hash = password_hash.to_string();
|
||||
save_peach_config(peach_config)
|
||||
}
|
||||
|
||||
pub fn get_temporary_password_hash() -> Result<String, PeachError> {
|
||||
let peach_config = load_peach_config()?;
|
||||
if !peach_config.temporary_password_hash.is_empty() {
|
||||
Ok(peach_config.temporary_password_hash)
|
||||
} else {
|
||||
Err(PeachError::PasswordNotSet)
|
||||
}
|
||||
// takes in a Vec<String> and saves SSB_ADMIN_IDS as a json string representation of this vec
|
||||
pub fn save_ssb_admin_ids(ssb_admin_ids: Vec<String>) -> Result<Vec<String>, PeachError> {
|
||||
let ssb_admin_ids_as_json_str = serde_json::to_string(&ssb_admin_ids)?;
|
||||
save_config_value("SSB_ADMIN_IDS", &ssb_admin_ids_as_json_str)?;
|
||||
Ok(ssb_admin_ids)
|
||||
}
|
||||
|
|
|
@ -18,7 +18,9 @@ use jsonrpc_client_http::HttpTransport;
|
|||
use log::{debug, info};
|
||||
use regex::Regex;
|
||||
|
||||
use crate::config_manager::get_dyndns_server_address;
|
||||
use crate::config_manager::{
|
||||
get_config_value, get_dyndns_enabled_value, get_dyndns_server_address,
|
||||
};
|
||||
use crate::{config_manager, error::PeachError};
|
||||
|
||||
/// constants for dyndns configuration
|
||||
|
@ -99,7 +101,7 @@ pub fn is_domain_available(domain: &str) -> std::result::Result<bool, PeachError
|
|||
/// Helper function to get public ip address of PeachCloud device.
|
||||
fn get_public_ip_address() -> Result<String, PeachError> {
|
||||
// TODO: consider other ways to get public IP address
|
||||
let output = Command::new("/usr/bin/curl").arg("ifconfig.me").output()?;
|
||||
let output = Command::new("curl").arg("ifconfig.me").output()?;
|
||||
let command_output = String::from_utf8(output.stdout)?;
|
||||
Ok(command_output)
|
||||
}
|
||||
|
@ -107,7 +109,11 @@ fn get_public_ip_address() -> Result<String, PeachError> {
|
|||
/// Reads dyndns configurations from config.yml
|
||||
/// and then uses nsupdate to update the IP address for the configured domain
|
||||
pub fn dyndns_update_ip() -> Result<bool, PeachError> {
|
||||
let peach_config = config_manager::load_peach_config()?;
|
||||
let dyn_tsig_key_path = get_config_value("DYN_TSIG_KEY_PATH")?;
|
||||
let dyn_enabled = get_dyndns_enabled_value()?;
|
||||
let dyn_domain = get_config_value("DYN_DOMAIN")?;
|
||||
let dyn_dns_server_address = get_config_value("DYN_DNS_SERVER_ADDRESS")?;
|
||||
let dyn_nameserver = get_config_value("DYN_NAMESERVER")?;
|
||||
info!(
|
||||
"Using config:
|
||||
dyn_tsig_key_path: {:?}
|
||||
|
@ -116,22 +122,15 @@ pub fn dyndns_update_ip() -> Result<bool, PeachError> {
|
|||
dyn_enabled: {:?}
|
||||
dyn_nameserver: {:?}
|
||||
",
|
||||
peach_config.dyn_tsig_key_path,
|
||||
peach_config.dyn_domain,
|
||||
peach_config.dyn_dns_server_address,
|
||||
peach_config.dyn_enabled,
|
||||
peach_config.dyn_nameserver,
|
||||
dyn_tsig_key_path, dyn_domain, dyn_dns_server_address, dyn_enabled, dyn_nameserver,
|
||||
);
|
||||
if !peach_config.dyn_enabled {
|
||||
if !dyn_enabled {
|
||||
info!("dyndns is not enabled, not updating");
|
||||
Ok(false)
|
||||
} else {
|
||||
// call nsupdate passing appropriate configs
|
||||
let mut nsupdate_command = Command::new("/usr/bin/nsupdate");
|
||||
nsupdate_command
|
||||
.arg("-k")
|
||||
.arg(&peach_config.dyn_tsig_key_path)
|
||||
.arg("-v");
|
||||
let mut nsupdate_command = Command::new("nsupdate");
|
||||
nsupdate_command.arg("-k").arg(&dyn_tsig_key_path).arg("-v");
|
||||
// pass nsupdate commands via stdin
|
||||
let public_ip_address = get_public_ip_address()?;
|
||||
info!("found public ip address: {}", public_ip_address);
|
||||
|
@ -142,9 +141,9 @@ pub fn dyndns_update_ip() -> Result<bool, PeachError> {
|
|||
update delete {DOMAIN} A
|
||||
update add {DOMAIN} 30 A {PUBLIC_IP_ADDRESS}
|
||||
send",
|
||||
NAMESERVER = peach_config.dyn_nameserver,
|
||||
ZONE = peach_config.dyn_domain,
|
||||
DOMAIN = peach_config.dyn_domain,
|
||||
NAMESERVER = dyn_nameserver,
|
||||
ZONE = dyn_domain,
|
||||
DOMAIN = dyn_domain,
|
||||
PUBLIC_IP_ADDRESS = public_ip_address,
|
||||
);
|
||||
info!("ns_commands: {:?}", ns_commands);
|
||||
|
@ -217,8 +216,7 @@ pub fn get_num_seconds_since_successful_dns_update() -> Result<Option<i64>, Peac
|
|||
/// and has successfully run recently (in the last six minutes)
|
||||
pub fn is_dns_updater_online() -> Result<bool, PeachError> {
|
||||
// first check if it is enabled in peach-config
|
||||
let peach_config = config_manager::load_peach_config()?;
|
||||
let is_enabled = peach_config.dyn_enabled;
|
||||
let is_enabled = get_dyndns_enabled_value()?;
|
||||
// then check if it has successfully run within the last 6 minutes (60*6 seconds)
|
||||
let num_seconds_since_successful_update = get_num_seconds_since_successful_dns_update()?;
|
||||
let ran_recently: bool = match num_seconds_since_successful_update {
|
||||
|
@ -248,8 +246,7 @@ pub fn get_dyndns_subdomain(dyndns_full_domain: &str) -> Option<String> {
|
|||
|
||||
// helper function which checks if a dyndns domain is new
|
||||
pub fn check_is_new_dyndns_domain(dyndns_full_domain: &str) -> Result<bool, PeachError> {
|
||||
let peach_config = config_manager::load_peach_config()?;
|
||||
let previous_dyndns_domain = peach_config.dyn_domain;
|
||||
let previous_dyndns_domain = get_config_value("DYN_DOMAIN")?;
|
||||
Ok(dyndns_full_domain != previous_dyndns_domain)
|
||||
}
|
||||
|
||||
|
|
|
@ -2,11 +2,21 @@
|
|||
|
||||
//! Error handling for various aspects of the PeachCloud system, including the network, OLED, stats and dyndns JSON-RPC clients, as well as the configuration manager, sbot client and password utilities.
|
||||
|
||||
use golgi::GolgiError;
|
||||
use std::{io, str, string};
|
||||
|
||||
/// This type represents all possible errors that can occur when interacting with the PeachCloud library.
|
||||
#[derive(Debug)]
|
||||
pub enum PeachError {
|
||||
/// Represents looking up a Config value with a non-existent key
|
||||
InvalidKey {
|
||||
/// the key value which was invalid
|
||||
key: String,
|
||||
},
|
||||
|
||||
/// Represents a failure to determine the path of the user's home directory.
|
||||
HomeDir,
|
||||
|
||||
/// Represents all other cases of `std::io::Error`.
|
||||
Io(io::Error),
|
||||
|
||||
|
@ -58,15 +68,18 @@ 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),
|
||||
|
||||
/// Represents a failure to deserialize TOML.
|
||||
TomlDeser(toml::de::Error),
|
||||
|
||||
/// Represents a failure to serialize TOML.
|
||||
TomlSer(toml::ser::Error),
|
||||
|
||||
/// Represents a failure to serialize or deserialize YAML.
|
||||
SerdeYaml(serde_yaml::Error),
|
||||
|
||||
|
@ -87,14 +100,19 @@ pub enum PeachError {
|
|||
Write {
|
||||
/// The underlying source of the error.
|
||||
source: io::Error,
|
||||
/// The file path for the write attemp.
|
||||
/// The file path for the write attempt.
|
||||
path: String,
|
||||
},
|
||||
|
||||
/// Represents a Golgi error
|
||||
Golgi(GolgiError),
|
||||
}
|
||||
|
||||
impl std::error::Error for PeachError {
|
||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||
match *self {
|
||||
PeachError::HomeDir => None,
|
||||
PeachError::InvalidKey { .. } => None,
|
||||
PeachError::Io(_) => None,
|
||||
PeachError::JsonRpcClientCore(_) => None,
|
||||
PeachError::JsonRpcCore(_) => None,
|
||||
|
@ -107,13 +125,16 @@ 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,
|
||||
PeachError::TomlDeser(_) => None,
|
||||
PeachError::TomlSer(_) => None,
|
||||
PeachError::Utf8ToStr(_) => None,
|
||||
PeachError::Utf8ToString(_) => None,
|
||||
PeachError::Write { ref source, .. } => Some(source),
|
||||
PeachError::Golgi(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -121,6 +142,15 @@ impl std::error::Error for PeachError {
|
|||
impl std::fmt::Display for PeachError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
match *self {
|
||||
PeachError::InvalidKey { ref key } => {
|
||||
write!(f, "Invalid key in config lookup for key: {}", key)
|
||||
}
|
||||
PeachError::HomeDir => {
|
||||
write!(
|
||||
f,
|
||||
"Unable to determine the path of the user's home directory"
|
||||
)
|
||||
}
|
||||
PeachError::Io(ref err) => err.fmt(f),
|
||||
PeachError::JsonRpcClientCore(ref err) => err.fmt(f),
|
||||
PeachError::JsonRpcCore(ref err) => {
|
||||
|
@ -135,22 +165,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),
|
||||
|
@ -158,11 +185,14 @@ impl std::fmt::Display for PeachError {
|
|||
PeachError::SsbAdminIdNotFound { ref id } => {
|
||||
write!(f, "Config error: SSB admin ID `{}` not found", id)
|
||||
}
|
||||
PeachError::TomlDeser(ref err) => err.fmt(f),
|
||||
PeachError::TomlSer(ref err) => err.fmt(f),
|
||||
PeachError::Utf8ToStr(ref err) => err.fmt(f),
|
||||
PeachError::Utf8ToString(ref err) => err.fmt(f),
|
||||
PeachError::Write { ref path, .. } => {
|
||||
write!(f, "Write error: {}", path)
|
||||
}
|
||||
PeachError::Golgi(ref err) => err.fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -209,6 +239,18 @@ impl From<serde_yaml::Error> for PeachError {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<toml::de::Error> for PeachError {
|
||||
fn from(err: toml::de::Error) -> PeachError {
|
||||
PeachError::TomlDeser(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<toml::ser::Error> for PeachError {
|
||||
fn from(err: toml::ser::Error) -> PeachError {
|
||||
PeachError::TomlSer(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<str::Utf8Error> for PeachError {
|
||||
fn from(err: str::Utf8Error) -> PeachError {
|
||||
PeachError::Utf8ToStr(err)
|
||||
|
@ -220,3 +262,9 @@ impl From<string::FromUtf8Error> for PeachError {
|
|||
PeachError::Utf8ToString(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<GolgiError> for PeachError {
|
||||
fn from(err: GolgiError) -> PeachError {
|
||||
PeachError::Golgi(err)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ pub mod error;
|
|||
pub mod network_client;
|
||||
pub mod oled_client;
|
||||
pub mod password_utils;
|
||||
pub mod sbot_client;
|
||||
pub mod sbot;
|
||||
pub mod stats_client;
|
||||
|
||||
// re-export error types
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
use async_std::task;
|
||||
use golgi::{sbot::Keystore, Sbot};
|
||||
use log::debug;
|
||||
use nanorand::{Rng, WyRand};
|
||||
use sha3::{Digest, Sha3_256};
|
||||
|
||||
use crate::{config_manager, error::PeachError, sbot_client};
|
||||
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.
|
||||
|
@ -30,7 +33,7 @@ pub fn validate_new_passwords(new_password1: &str, new_password2: &str) -> Resul
|
|||
/// Sets a new password for the admin user
|
||||
pub fn set_new_password(new_password: &str) -> Result<(), PeachError> {
|
||||
let new_password_hash = hash_password(new_password);
|
||||
config_manager::set_admin_password_hash(&new_password_hash)?;
|
||||
config_manager::set_admin_password_hash(new_password_hash)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -50,7 +53,7 @@ pub fn hash_password(password: &str) -> String {
|
|||
/// which can be used to reset the permanent password
|
||||
pub fn set_new_temporary_password(new_password: &str) -> Result<(), PeachError> {
|
||||
let new_password_hash = hash_password(new_password);
|
||||
config_manager::set_temporary_password_hash(&new_password_hash)?;
|
||||
config_manager::set_temporary_password_hash(new_password_hash)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -83,7 +86,7 @@ pub fn send_password_reset() -> Result<(), PeachError> {
|
|||
"Your new temporary password is: {}
|
||||
|
||||
If you are on the same WiFi network as your PeachCloud device you can reset your password \
|
||||
using this link: http://peach.local/reset_password",
|
||||
using this link: http://peach.local/auth/reset",
|
||||
temporary_password
|
||||
);
|
||||
// if there is an external domain, then include remote link in message
|
||||
|
@ -92,7 +95,7 @@ using this link: http://peach.local/reset_password",
|
|||
Some(domain) => {
|
||||
format!(
|
||||
"\n\nOr if you are on a different WiFi network, you can reset your password \
|
||||
using the the following link: {}/reset_password",
|
||||
using the the following link: {}/auth/reset",
|
||||
domain
|
||||
)
|
||||
}
|
||||
|
@ -100,9 +103,41 @@ using this link: http://peach.local/reset_password",
|
|||
};
|
||||
msg += &remote_link;
|
||||
// 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 {
|
||||
sbot_client::private_message(&msg, &ssb_admin_id)?;
|
||||
let ssb_admin_ids = config_manager::get_ssb_admin_ids()?;
|
||||
for ssb_admin_id in ssb_admin_ids {
|
||||
// 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(Keystore::GoSbot, Some(ip_port), None)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?
|
||||
}
|
||||
None => Sbot::init(Keystore::GoSbot, 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)),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,267 @@
|
|||
//! Data types and associated methods for monitoring and configuring go-sbot.
|
||||
|
||||
use std::{fs, fs::File, io, io::Write, path::PathBuf, process::Command, str};
|
||||
|
||||
use golgi::{sbot::Keystore, Sbot};
|
||||
use log::debug;
|
||||
|
||||
use crate::config_manager;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::error::PeachError;
|
||||
|
||||
/* HELPER FUNCTIONS */
|
||||
|
||||
// iterate over the given directory path to determine the size of the directory
|
||||
fn dir_size(path: impl Into<PathBuf>) -> io::Result<u64> {
|
||||
fn dir_size(mut dir: fs::ReadDir) -> io::Result<u64> {
|
||||
dir.try_fold(0, |acc, file| {
|
||||
let file = file?;
|
||||
let size = match file.metadata()? {
|
||||
data if data.is_dir() => dir_size(fs::read_dir(file.path())?)?,
|
||||
data => data.len(),
|
||||
};
|
||||
Ok(acc + size)
|
||||
})
|
||||
}
|
||||
|
||||
dir_size(fs::read_dir(path.into())?)
|
||||
}
|
||||
|
||||
/* SBOT-RELATED TYPES AND METHODS */
|
||||
|
||||
/// go-sbot process status.
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct SbotStatus {
|
||||
/// Current process state.
|
||||
pub state: Option<String>,
|
||||
/// Current process boot state.
|
||||
pub boot_state: Option<String>,
|
||||
/// Current process memory usage in bytes.
|
||||
pub memory: Option<u32>,
|
||||
/// Uptime for the process (if state is `active`).
|
||||
pub uptime: Option<String>,
|
||||
/// Downtime for the process (if state is `inactive`).
|
||||
pub downtime: Option<String>,
|
||||
/// Size of the blobs directory in bytes.
|
||||
pub blobstore: Option<u64>,
|
||||
}
|
||||
|
||||
/// Default builder for `SbotStatus`.
|
||||
impl Default for SbotStatus {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
state: None,
|
||||
boot_state: None,
|
||||
memory: None,
|
||||
uptime: None,
|
||||
downtime: None,
|
||||
blobstore: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SbotStatus {
|
||||
/// Retrieve statistics for the go-sbot systemd process by querying `systemctl`.
|
||||
pub fn read() -> Result<Self, PeachError> {
|
||||
let mut status = SbotStatus::default();
|
||||
|
||||
// note this command does not need to be run as sudo
|
||||
// because non-privileged users are able to run systemctl show
|
||||
let info_output = Command::new("systemctl")
|
||||
.arg("show")
|
||||
.arg(config_manager::get_config_value("GO_SBOT_SERVICE")?)
|
||||
.arg("--no-page")
|
||||
.output()?;
|
||||
|
||||
let service_info = std::str::from_utf8(&info_output.stdout)?;
|
||||
|
||||
for line in service_info.lines() {
|
||||
if line.starts_with("ActiveState=") {
|
||||
if let Some(state) = line.strip_prefix("ActiveState=") {
|
||||
status.state = Some(state.to_string())
|
||||
}
|
||||
} else if line.starts_with("MemoryCurrent=") {
|
||||
if let Some(memory) = line.strip_prefix("MemoryCurrent=") {
|
||||
status.memory = memory.parse().ok()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// note this command does not need to be run as sudo
|
||||
// because non-privileged users are able to run systemctl status
|
||||
let status_output = Command::new("systemctl")
|
||||
.arg("status")
|
||||
.arg(config_manager::get_config_value("GO_SBOT_SERVICE")?)
|
||||
.output()?;
|
||||
|
||||
let service_status = str::from_utf8(&status_output.stdout)?;
|
||||
//.map_err(PeachError::Utf8ToStr)?;
|
||||
|
||||
for line in service_status.lines() {
|
||||
// example of the output line we're looking for:
|
||||
// `Loaded: loaded (/home/glyph/.config/systemd/user/go-sbot.service; enabled; vendor
|
||||
// preset: enabled)`
|
||||
if line.contains("Loaded:") {
|
||||
let before_boot_state = line.find(';');
|
||||
let after_boot_state = line.rfind(';');
|
||||
if let (Some(start), Some(end)) = (before_boot_state, after_boot_state) {
|
||||
// extract the enabled / disabled from the `Loaded: ...` line
|
||||
// using the index of the first ';' + 2 and the last ';'
|
||||
status.boot_state = Some(line[start + 2..end].to_string());
|
||||
}
|
||||
// example of the output line we're looking for here:
|
||||
// `Active: active (running) since Mon 2022-01-24 16:22:51 SAST; 4min 14s ago`
|
||||
} else if line.contains("Active:") {
|
||||
let before_time = line.find(';');
|
||||
let after_time = line.find(" ago");
|
||||
if let (Some(start), Some(end)) = (before_time, after_time) {
|
||||
// extract the uptime / downtime from the `Active: ...` line
|
||||
// using the index of ';' + 2 and the index of " ago"
|
||||
let time = Some(&line[start + 2..end]);
|
||||
// if service is active then the `time` reading is uptime
|
||||
if status.state == Some("active".to_string()) {
|
||||
status.uptime = time.map(|t| t.to_string())
|
||||
// if service is inactive then the `time` reading is downtime
|
||||
} else if status.state == Some("inactive".to_string()) {
|
||||
status.downtime = time.map(|t| t.to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// get path to blobstore
|
||||
let blobstore_path = format!(
|
||||
"{}/blobs/sha256",
|
||||
config_manager::get_config_value("GO_SBOT_DATADIR")?
|
||||
);
|
||||
|
||||
// determine the size of the blobstore directory in bytes
|
||||
status.blobstore = dir_size(blobstore_path).ok();
|
||||
|
||||
Ok(status)
|
||||
}
|
||||
}
|
||||
|
||||
/// go-sbot configuration parameters.
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(default)]
|
||||
pub struct SbotConfig {
|
||||
// TODO: maybe define as a Path type?
|
||||
/// Directory path for the log and indexes.
|
||||
pub repo: String,
|
||||
/// Directory path for writing debug output.
|
||||
pub debugdir: String,
|
||||
/// Secret-handshake app-key (aka. network key).
|
||||
pub shscap: String,
|
||||
/// HMAC hash used to sign messages.
|
||||
pub hmac: String,
|
||||
/// Replication hops (1: friends, 2: friends of friends).
|
||||
pub hops: u8,
|
||||
/// Address to listen on.
|
||||
pub lis: String,
|
||||
/// Address to listen on for WebSocket connections.
|
||||
pub wslis: String,
|
||||
/// Address to for metrics and pprof HTTP server.
|
||||
pub debuglis: String,
|
||||
/// Enable sending local UDP broadcasts.
|
||||
pub localadv: bool,
|
||||
/// Enable listening for UDP broadcasts and connecting.
|
||||
pub localdiscov: bool,
|
||||
/// Enable syncing by using epidemic-broadcast-trees (EBT).
|
||||
#[serde(rename(serialize = "enable_ebt", deserialize = "enable-ebt"))]
|
||||
pub enable_ebt: bool,
|
||||
/// Bypass graph auth and fetch remote's feed (useful for pubs that are restoring their data
|
||||
/// from peer; user beware - caveats about).
|
||||
pub promisc: bool,
|
||||
/// Disable the UNIX socket RPC interface.
|
||||
pub nounixsock: bool,
|
||||
/// Attempt to repair the filesystem before starting.
|
||||
pub repair: bool,
|
||||
}
|
||||
|
||||
/// Default configuration values for go-sbot.
|
||||
impl Default for SbotConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
repo: ".ssb-go".to_string(),
|
||||
debugdir: "".to_string(),
|
||||
shscap: "1KHLiKZvAvjbY1ziZEHMXawbCEIM6qwjCDm3VYRan/s=".to_string(),
|
||||
hmac: "".to_string(),
|
||||
hops: 1,
|
||||
lis: ":8008".to_string(),
|
||||
wslis: ":8989".to_string(),
|
||||
debuglis: "localhost:6078".to_string(),
|
||||
localadv: false,
|
||||
localdiscov: false,
|
||||
enable_ebt: false,
|
||||
promisc: false,
|
||||
nounixsock: false,
|
||||
repair: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SbotConfig {
|
||||
/// Read the go-sbot `config.toml` file from file and deserialize into `SbotConfig`.
|
||||
pub fn read() -> Result<Self, PeachError> {
|
||||
// determine path of user's go-sbot config.toml
|
||||
let config_path = format!(
|
||||
"{}/config.toml",
|
||||
config_manager::get_config_value("GO_SBOT_DATADIR")?
|
||||
);
|
||||
|
||||
let config_contents = fs::read_to_string(config_path)?;
|
||||
|
||||
let config: SbotConfig = toml::from_str(&config_contents)?;
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
/// Write the given `SbotConfig` to the go-sbot `config.toml` file.
|
||||
pub fn write(config: SbotConfig) -> Result<(), PeachError> {
|
||||
let repo_comment = "# For details about go-sbot configuration, please visit the repo: https://github.com/cryptoscope/ssb\n".to_string();
|
||||
|
||||
// convert the provided `SbotConfig` instance to a string
|
||||
let config_string = toml::to_string(&config)?;
|
||||
|
||||
// determine path of user's go-sbot config.toml
|
||||
let config_path = format!(
|
||||
"{}/config.toml",
|
||||
config_manager::get_config_value("GO_SBOT_DATADIR")?
|
||||
);
|
||||
|
||||
// open config file for writing
|
||||
let mut file = File::create(config_path)?;
|
||||
|
||||
// write the repo comment to file
|
||||
write!(file, "{}", repo_comment)?;
|
||||
|
||||
// write the config string to file
|
||||
write!(file, "{}", config_string)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Initialise an sbot client
|
||||
pub async fn init_sbot() -> Result<Sbot, PeachError> {
|
||||
// read sbot config from config.toml
|
||||
let sbot_config = SbotConfig::read().ok();
|
||||
|
||||
debug!("Initialising an sbot client with configuration parameters");
|
||||
// initialise sbot connection with ip:port and shscap from config file
|
||||
let key_path = format!(
|
||||
"{}/secret",
|
||||
config_manager::get_config_value("GO_SBOT_DATADIR")?
|
||||
);
|
||||
let 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(Keystore::CustomGoSbot(key_path), Some(ip_port), None).await?
|
||||
}
|
||||
None => Sbot::init(Keystore::CustomGoSbot(key_path), None, None).await?,
|
||||
};
|
||||
Ok(sbot_client)
|
||||
}
|
|
@ -1,111 +0,0 @@
|
|||
//! Interfaces for monitoring and configuring go-sbot using sbotcli.
|
||||
|
||||
use std::process::Command;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::error::PeachError;
|
||||
|
||||
pub fn is_sbot_online() -> Result<bool, PeachError> {
|
||||
let output = Command::new("/usr/bin/systemctl")
|
||||
.arg("status")
|
||||
.arg("peach-go-sbot")
|
||||
.output()?;
|
||||
let status = output.status;
|
||||
// returns true if the service had an exist status of 0 (is running)
|
||||
let is_running = status.success();
|
||||
Ok(is_running)
|
||||
}
|
||||
|
||||
/// currently go-sbotcli determines where the working directory is
|
||||
/// using the home directory of th user that invokes it
|
||||
/// this could be changed to be supplied as CLI arg
|
||||
/// but for now all sbotcli commands must first become peach-go-sbot before running
|
||||
/// the sudoers file is configured to allow this to happen without a password
|
||||
pub fn sbotcli_command() -> Command {
|
||||
let mut command = Command::new("sudo");
|
||||
command
|
||||
.arg("-u")
|
||||
.arg("peach-go-sbot")
|
||||
.arg("/usr/bin/sbotcli");
|
||||
command
|
||||
}
|
||||
|
||||
pub fn post(msg: &str) -> Result<(), PeachError> {
|
||||
let mut command = sbotcli_command();
|
||||
let output = command.arg("publish").arg("post").arg(msg).output()?;
|
||||
if output.status.success() {
|
||||
Ok(())
|
||||
} else {
|
||||
let stderr = std::str::from_utf8(&output.stderr)?;
|
||||
Err(PeachError::SbotCli {
|
||||
msg: format!("Error making ssb post: {}", stderr),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct WhoAmIValue {
|
||||
id: String,
|
||||
}
|
||||
|
||||
pub fn whoami() -> Result<String, PeachError> {
|
||||
let mut command = sbotcli_command();
|
||||
let output = command.arg("call").arg("whoami").output()?;
|
||||
let text_output = std::str::from_utf8(&output.stdout)?;
|
||||
let value: WhoAmIValue = serde_json::from_str(text_output)?;
|
||||
let id = value.id;
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
pub fn create_invite(uses: i32) -> Result<String, PeachError> {
|
||||
let mut command = sbotcli_command();
|
||||
let output = command
|
||||
.arg("invite")
|
||||
.arg("create")
|
||||
.arg("--uses")
|
||||
.arg(uses.to_string())
|
||||
.output()?;
|
||||
let text_output = std::str::from_utf8(&output.stdout)?;
|
||||
let output = text_output.replace('\n', "");
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
pub fn update_pub_name(new_name: &str) -> Result<(), PeachError> {
|
||||
let pub_ssb_id = whoami()?;
|
||||
let mut command = sbotcli_command();
|
||||
let output = command
|
||||
.arg("publish")
|
||||
.arg("about")
|
||||
.arg("--name")
|
||||
.arg(new_name)
|
||||
.arg(pub_ssb_id)
|
||||
.output()?;
|
||||
if output.status.success() {
|
||||
Ok(())
|
||||
} else {
|
||||
let stderr = std::str::from_utf8(&output.stderr)?;
|
||||
Err(PeachError::SbotCli {
|
||||
msg: format!("Error updating pub name: {}", stderr),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn private_message(msg: &str, recipient: &str) -> Result<(), PeachError> {
|
||||
let mut command = sbotcli_command();
|
||||
let output = command
|
||||
.arg("publish")
|
||||
.arg("post")
|
||||
.arg("--recps")
|
||||
.arg(recipient)
|
||||
.arg(msg)
|
||||
.output()?;
|
||||
if output.status.success() {
|
||||
Ok(())
|
||||
} else {
|
||||
let stderr = std::str::from_utf8(&output.stderr)?;
|
||||
Err(PeachError::SbotCli {
|
||||
msg: format!("Error sending ssb private message: {}", stderr),
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "peach-network"
|
||||
version = "0.4.2"
|
||||
version = "0.5.0"
|
||||
authors = ["Andrew Reid <glyph@mycelial.technology>"]
|
||||
edition = "2021"
|
||||
description = "Query and configure network interfaces."
|
||||
|
|
|
@ -6,7 +6,7 @@ use std::num::ParseIntError;
|
|||
use io::Error as IoError;
|
||||
use probes::ProbeError;
|
||||
use regex::Error as RegexError;
|
||||
use wpactrl::WpaError;
|
||||
use wpactrl::Error as WpaError;
|
||||
|
||||
/// Custom error type encapsulating all possible errors when querying
|
||||
/// network interfaces and modifying their state.
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
//! access point credentials to `wpa_supplicant-<wlan_iface>.conf`.
|
||||
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fs::OpenOptions,
|
||||
io::prelude::*,
|
||||
process::{Command, Stdio},
|
||||
|
@ -22,6 +23,7 @@ use std::{
|
|||
};
|
||||
|
||||
use probes::network;
|
||||
use wpactrl::Client as WpaClient;
|
||||
|
||||
#[cfg(feature = "miniserde_support")]
|
||||
use miniserde::{Deserialize, Serialize};
|
||||
|
@ -105,8 +107,86 @@ pub struct Traffic {
|
|||
pub transmitted: u64,
|
||||
}
|
||||
|
||||
/// Access point data including state and signal strength.
|
||||
#[derive(Debug)]
|
||||
#[cfg_attr(feature = "miniserde_support", derive(Serialize, Deserialize))]
|
||||
#[cfg_attr(feature = "serde_support", derive(Serialize, Deserialize))]
|
||||
pub struct AccessPoint {
|
||||
/// Access point data retrieved via scan.
|
||||
pub detail: Option<Scan>,
|
||||
/// Current state of the access point (e.g. "Available" or "Out of range").
|
||||
pub state: String,
|
||||
/// Signal strength of the access point as a percentage.
|
||||
pub signal: Option<i32>,
|
||||
}
|
||||
|
||||
impl AccessPoint {
|
||||
fn available(detail: Option<Scan>, signal: Option<i32>) -> AccessPoint {
|
||||
AccessPoint {
|
||||
detail,
|
||||
state: String::from("Available"),
|
||||
signal,
|
||||
}
|
||||
}
|
||||
|
||||
fn saved() -> AccessPoint {
|
||||
AccessPoint {
|
||||
detail: None,
|
||||
state: String::from("Out of range"),
|
||||
signal: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* GET - Methods for retrieving data */
|
||||
|
||||
/// Retrieve combined list of available (in-range) and saved wireless access
|
||||
/// points for a given network interface.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `iface` - A string slice holding the name of a wireless network interface
|
||||
///
|
||||
/// If the list results include one or more access points for the given network
|
||||
/// interface, an `Ok` `Result` type is returned containing `HashMap<String,
|
||||
/// AccessPoint>`.
|
||||
///
|
||||
/// Each entry in the returned `HashMap` contains an SSID (`String`) and
|
||||
/// `AccessPoint` `struct`. If no access points are found, an empty `HashMap`
|
||||
/// is returned in the `Result`. In the event of an error, a `NetworkError`
|
||||
/// is returned in the `Result`.
|
||||
pub fn all_networks(iface: &str) -> Result<HashMap<String, AccessPoint>, NetworkError> {
|
||||
let mut wlan_networks = HashMap::new();
|
||||
|
||||
if let Ok(Some(networks)) = available_networks(iface) {
|
||||
for ap in networks {
|
||||
let ssid = ap.ssid.clone();
|
||||
|
||||
let rssi = ap.signal_level.clone();
|
||||
// parse the string to a signed integer (for math)
|
||||
let rssi_parsed = rssi.parse::<i32>().unwrap();
|
||||
// perform rssi (dBm) to quality (%) conversion
|
||||
let quality_percent = 2 * (rssi_parsed + 100);
|
||||
|
||||
let ap_detail = AccessPoint::available(Some(ap), Some(quality_percent));
|
||||
wlan_networks.insert(ssid, ap_detail);
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(Some(networks)) = saved_networks() {
|
||||
for saved_ssid in networks {
|
||||
if !wlan_networks.contains_key(&saved_ssid) {
|
||||
let ssid = saved_ssid.clone();
|
||||
|
||||
let ap_detail = AccessPoint::saved();
|
||||
wlan_networks.insert(ssid, ap_detail);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(wlan_networks)
|
||||
}
|
||||
|
||||
/// Retrieve list of available wireless access points for a given network
|
||||
/// interface.
|
||||
///
|
||||
|
@ -121,7 +201,7 @@ pub struct Traffic {
|
|||
/// In the event of an error, a `NetworkError` is returned in the `Result`.
|
||||
pub fn available_networks(iface: &str) -> Result<Option<Vec<Scan>>, NetworkError> {
|
||||
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
|
||||
let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?;
|
||||
let mut wpa = WpaClient::builder().ctrl_path(wpa_path).open()?;
|
||||
wpa.request("SCAN")?;
|
||||
let networks = wpa.request("SCAN_RESULTS")?;
|
||||
let mut scan = Vec::new();
|
||||
|
@ -173,7 +253,7 @@ pub fn available_networks(iface: &str) -> Result<Option<Vec<Scan>>, NetworkError
|
|||
/// event of an error, a `NetworkError` is returned in the `Result`.
|
||||
pub fn id(iface: &str, ssid: &str) -> Result<Option<String>, NetworkError> {
|
||||
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
|
||||
let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?;
|
||||
let mut wpa = WpaClient::builder().ctrl_path(wpa_path).open()?;
|
||||
let networks = wpa.request("LIST_NETWORKS")?;
|
||||
let mut id = Vec::new();
|
||||
for network in networks.lines() {
|
||||
|
@ -232,7 +312,7 @@ pub fn ip(iface: &str) -> Result<Option<String>, NetworkError> {
|
|||
/// `Result`.
|
||||
pub fn rssi(iface: &str) -> Result<Option<String>, NetworkError> {
|
||||
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
|
||||
let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?;
|
||||
let mut wpa = WpaClient::builder().ctrl_path(wpa_path).open()?;
|
||||
let status = wpa.request("SIGNAL_POLL")?;
|
||||
let rssi = utils::regex_finder(r"RSSI=(.*)\n", &status)?;
|
||||
|
||||
|
@ -259,7 +339,7 @@ pub fn rssi(iface: &str) -> Result<Option<String>, NetworkError> {
|
|||
/// the `Result`.
|
||||
pub fn rssi_percent(iface: &str) -> Result<Option<String>, NetworkError> {
|
||||
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
|
||||
let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?;
|
||||
let mut wpa = WpaClient::builder().ctrl_path(wpa_path).open()?;
|
||||
let status = wpa.request("SIGNAL_POLL")?;
|
||||
let rssi = utils::regex_finder(r"RSSI=(.*)\n", &status)?;
|
||||
|
||||
|
@ -291,7 +371,7 @@ pub fn rssi_percent(iface: &str) -> Result<Option<String>, NetworkError> {
|
|||
/// is returned in the `Result`. In the event of an error, a `NetworkError` is
|
||||
/// returned in the `Result`.
|
||||
pub fn saved_networks() -> Result<Option<Vec<String>>, NetworkError> {
|
||||
let mut wpa = wpactrl::WpaCtrl::builder().open()?;
|
||||
let mut wpa = WpaClient::builder().open()?;
|
||||
let networks = wpa.request("LIST_NETWORKS")?;
|
||||
let mut ssids = Vec::new();
|
||||
for network in networks.lines() {
|
||||
|
@ -323,7 +403,7 @@ pub fn saved_networks() -> Result<Option<Vec<String>>, NetworkError> {
|
|||
/// returned in the `Result`.
|
||||
pub fn ssid(iface: &str) -> Result<Option<String>, NetworkError> {
|
||||
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
|
||||
let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?;
|
||||
let mut wpa = WpaClient::builder().ctrl_path(wpa_path).open()?;
|
||||
let status = wpa.request("STATUS")?;
|
||||
|
||||
// pass the regex pattern and status output to the regex finder
|
||||
|
@ -379,7 +459,7 @@ pub fn state(iface: &str) -> Result<Option<String>, NetworkError> {
|
|||
/// a `NetworkError` is returned in the `Result`.
|
||||
pub fn status(iface: &str) -> Result<Option<Status>, NetworkError> {
|
||||
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
|
||||
let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?;
|
||||
let mut wpa = WpaClient::builder().ctrl_path(wpa_path).open()?;
|
||||
let wpa_status = wpa.request("STATUS")?;
|
||||
|
||||
// pass the regex pattern and status output to the regex finder
|
||||
|
@ -579,7 +659,7 @@ pub fn check_iface(wlan_iface: &str, ap_iface: &str) -> Result<(), NetworkError>
|
|||
/// is returned in the `Result`.
|
||||
pub fn connect(id: &str, iface: &str) -> Result<(), NetworkError> {
|
||||
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
|
||||
let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?;
|
||||
let mut wpa = WpaClient::builder().ctrl_path(wpa_path).open()?;
|
||||
let select = format!("SELECT {}", id);
|
||||
wpa.request(&select)?;
|
||||
Ok(())
|
||||
|
@ -598,7 +678,7 @@ pub fn connect(id: &str, iface: &str) -> Result<(), NetworkError> {
|
|||
/// returned in the `Result`.
|
||||
pub fn delete(id: &str, iface: &str) -> Result<(), NetworkError> {
|
||||
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
|
||||
let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?;
|
||||
let mut wpa = WpaClient::builder().ctrl_path(wpa_path).open()?;
|
||||
let remove = format!("REMOVE_NETWORK {}", id);
|
||||
wpa.request(&remove)?;
|
||||
Ok(())
|
||||
|
@ -617,7 +697,7 @@ pub fn delete(id: &str, iface: &str) -> Result<(), NetworkError> {
|
|||
/// `Result`.
|
||||
pub fn disable(id: &str, iface: &str) -> Result<(), NetworkError> {
|
||||
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
|
||||
let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?;
|
||||
let mut wpa = WpaClient::builder().ctrl_path(wpa_path).open()?;
|
||||
let disable = format!("DISABLE_NETWORK {}", id);
|
||||
wpa.request(&disable)?;
|
||||
Ok(())
|
||||
|
@ -634,7 +714,7 @@ pub fn disable(id: &str, iface: &str) -> Result<(), NetworkError> {
|
|||
/// error, a `NetworkError` is returned in the `Result`.
|
||||
pub fn disconnect(iface: &str) -> Result<(), NetworkError> {
|
||||
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
|
||||
let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?;
|
||||
let mut wpa = WpaClient::builder().ctrl_path(wpa_path).open()?;
|
||||
let disconnect = "DISCONNECT".to_string();
|
||||
wpa.request(&disconnect)?;
|
||||
Ok(())
|
||||
|
@ -685,7 +765,7 @@ pub fn forget(iface: &str, ssid: &str) -> Result<(), NetworkError> {
|
|||
/// event of an error, a `NetworkError` is returned in the `Result`.
|
||||
pub fn modify(id: &str, iface: &str, pass: &str) -> Result<(), NetworkError> {
|
||||
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
|
||||
let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?;
|
||||
let mut wpa = WpaClient::builder().ctrl_path(wpa_path).open()?;
|
||||
let new_pass = format!("NEW_PASSWORD {} {}", id, pass);
|
||||
wpa.request(&new_pass)?;
|
||||
Ok(())
|
||||
|
@ -702,7 +782,7 @@ pub fn modify(id: &str, iface: &str, pass: &str) -> Result<(), NetworkError> {
|
|||
/// error, a `NetworkError` is returned in the `Result`.
|
||||
pub fn reassociate(iface: &str) -> Result<(), NetworkError> {
|
||||
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
|
||||
let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?;
|
||||
let mut wpa = WpaClient::builder().ctrl_path(wpa_path).open()?;
|
||||
wpa.request("REASSOCIATE")?;
|
||||
Ok(())
|
||||
}
|
||||
|
@ -714,7 +794,7 @@ pub fn reassociate(iface: &str) -> Result<(), NetworkError> {
|
|||
/// `Result` type is returned. In the event of an error, a `NetworkError` is
|
||||
/// returned in the `Result`.
|
||||
pub fn reconfigure() -> Result<(), NetworkError> {
|
||||
let mut wpa = wpactrl::WpaCtrl::builder().open()?;
|
||||
let mut wpa = WpaClient::builder().open()?;
|
||||
wpa.request("RECONFIGURE")?;
|
||||
Ok(())
|
||||
}
|
||||
|
@ -730,7 +810,7 @@ pub fn reconfigure() -> Result<(), NetworkError> {
|
|||
/// event of an error, a `NetworkError` is returned in the `Result`.
|
||||
pub fn reconnect(iface: &str) -> Result<(), NetworkError> {
|
||||
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
|
||||
let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?;
|
||||
let mut wpa = WpaClient::builder().ctrl_path(wpa_path).open()?;
|
||||
wpa.request("DISCONNECT")?;
|
||||
wpa.request("RECONNECT")?;
|
||||
Ok(())
|
||||
|
@ -742,7 +822,7 @@ pub fn reconnect(iface: &str) -> Result<(), NetworkError> {
|
|||
/// `wpa_supplicant.conf` file, an `Ok` `Result` type is returned. In the
|
||||
/// event of an error, a `NetworkError` is returned in the `Result`.
|
||||
pub fn save() -> Result<(), NetworkError> {
|
||||
let mut wpa = wpactrl::WpaCtrl::builder().open()?;
|
||||
let mut wpa = WpaClient::builder().open()?;
|
||||
wpa.request("SAVE_CONFIG")?;
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "peach-stats"
|
||||
version = "0.2.0"
|
||||
version = "0.3.1"
|
||||
authors = ["Andrew Reid <glyph@mycelial.technology>"]
|
||||
edition = "2018"
|
||||
description = "Query system statistics. Provides a wrapper around the probes and systemstat crates."
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
# peach-stats
|
||||
|
||||
![Generic badge](https://img.shields.io/badge/version-0.2.0-<COLOR>.svg)
|
||||
![Generic badge](https://img.shields.io/badge/version-0.3.0-<COLOR>.svg)
|
||||
|
||||
System statistics library for PeachCloud. Provides a wrapper around the [probes](https://crates.io/crates/probes) and [systemstat](https://crates.io/crates/systemstat) crates.
|
||||
|
||||
Currently offers the following statistics and associated data structures:
|
||||
Currently offers the following system statistics and associated data structures:
|
||||
|
||||
- CPU: `user`, `system`, `nice`, `idle` (as values or percentages)
|
||||
- Disk usage: `filesystem`, `one_k_blocks`, `one_k_blocks_used`,
|
||||
|
@ -13,10 +13,14 @@ Currently offers the following statistics and associated data structures:
|
|||
- Memory: `total`, `free`, `used`
|
||||
- Uptime: `seconds`
|
||||
|
||||
As well as the following go-sbot process statistics:
|
||||
|
||||
- Sbot: `state`, `memory`, `uptime`, `downtime`
|
||||
|
||||
## Example Usage
|
||||
|
||||
```rust
|
||||
use peach_stats::{stats, StatsError};
|
||||
use peach_stats::{sbot, stats, StatsError};
|
||||
|
||||
fn main() -> Result<(), StatsError> {
|
||||
let cpu = stats::cpu_stats()?;
|
||||
|
@ -25,6 +29,7 @@ fn main() -> Result<(), StatsError> {
|
|||
let load = stats::load_average()?;
|
||||
let mem = stats::mem_stats()?;
|
||||
let uptime = stats::uptime()?;
|
||||
let sbot_process = sbot::sbot_stats()?;
|
||||
|
||||
// do things with the retrieved values...
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
//! Custom error type for `peach-stats`.
|
||||
|
||||
use probes::ProbeError;
|
||||
use std::{error, fmt, io::Error as IoError};
|
||||
use std::{error, fmt, io::Error as IoError, str::Utf8Error};
|
||||
|
||||
/// Custom error type encapsulating all possible errors when retrieving system
|
||||
/// statistics.
|
||||
|
@ -17,6 +17,10 @@ pub enum StatsError {
|
|||
MemStat(ProbeError),
|
||||
/// Failed to retrieve system uptime.
|
||||
Uptime(IoError),
|
||||
/// Systemctl command returned an error.
|
||||
Systemctl(IoError),
|
||||
/// Failed to interpret sequence of `u8` as a string.
|
||||
Utf8String(Utf8Error),
|
||||
}
|
||||
|
||||
impl error::Error for StatsError {}
|
||||
|
@ -39,6 +43,12 @@ impl fmt::Display for StatsError {
|
|||
StatsError::Uptime(ref source) => {
|
||||
write!(f, "Failed to retrieve system uptime: {}", source)
|
||||
}
|
||||
StatsError::Systemctl(ref source) => {
|
||||
write!(f, "Systemctl command returned an error: {}", source)
|
||||
}
|
||||
StatsError::Utf8String(ref source) => {
|
||||
write!(f, "Failed to convert stdout to string: {}", source)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -43,6 +43,7 @@
|
|||
//! ```
|
||||
|
||||
pub mod error;
|
||||
pub mod sbot;
|
||||
pub mod stats;
|
||||
|
||||
pub use crate::error::StatsError;
|
||||
|
|
|
@ -0,0 +1,111 @@
|
|||
//! Systemd go-sbot process statistics retrieval functions and associated data types.
|
||||
|
||||
use std::{process::Command, str};
|
||||
|
||||
#[cfg(feature = "miniserde_support")]
|
||||
use miniserde::{Deserialize, Serialize};
|
||||
|
||||
#[cfg(feature = "serde_support")]
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::StatsError;
|
||||
|
||||
/// go-sbot process statistics.
|
||||
#[derive(Debug)]
|
||||
#[cfg_attr(feature = "miniserde_support", derive(Serialize, Deserialize))]
|
||||
#[cfg_attr(feature = "serde_support", derive(Serialize, Deserialize))]
|
||||
pub struct SbotStat {
|
||||
/// Current process state.
|
||||
pub state: Option<String>,
|
||||
/// Current process boot state.
|
||||
pub boot_state: Option<String>,
|
||||
/// Current process memory usage in bytes.
|
||||
pub memory: Option<u32>,
|
||||
/// Uptime for the process (if state is `active`).
|
||||
pub uptime: Option<String>,
|
||||
/// Downtime for the process (if state is `inactive`).
|
||||
pub downtime: Option<String>,
|
||||
}
|
||||
|
||||
impl SbotStat {
|
||||
/// Default builder for `SbotStat`.
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
state: None,
|
||||
boot_state: None,
|
||||
memory: None,
|
||||
uptime: None,
|
||||
downtime: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Retrieve statistics for the go-sbot systemd process by querying `systemctl`.
|
||||
pub fn sbot_stats() -> Result<SbotStat, StatsError> {
|
||||
let mut status = SbotStat::default();
|
||||
|
||||
let info_output = Command::new("sudo")
|
||||
.arg("systemctl")
|
||||
.arg("show")
|
||||
.arg("go-sbot.service")
|
||||
.arg("--no-page")
|
||||
.output()
|
||||
.map_err(StatsError::Systemctl)?;
|
||||
|
||||
let service_info = std::str::from_utf8(&info_output.stdout).map_err(StatsError::Utf8String)?;
|
||||
|
||||
for line in service_info.lines() {
|
||||
if line.starts_with("ActiveState=") {
|
||||
if let Some(state) = line.strip_prefix("ActiveState=") {
|
||||
status.state = Some(state.to_string())
|
||||
}
|
||||
} else if line.starts_with("MemoryCurrent=") {
|
||||
if let Some(memory) = line.strip_prefix("MemoryCurrent=") {
|
||||
status.memory = memory.parse().ok()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let status_output = Command::new("sudo")
|
||||
.arg("systemctl")
|
||||
.arg("status")
|
||||
.arg("go-sbot.service")
|
||||
.output()
|
||||
.map_err(StatsError::Systemctl)?;
|
||||
|
||||
let service_status = str::from_utf8(&status_output.stdout).map_err(StatsError::Utf8String)?;
|
||||
|
||||
for line in service_status.lines() {
|
||||
// example of the output line we're looking for:
|
||||
// `Loaded: loaded (/home/glyph/.config/systemd/user/go-sbot.service; enabled; vendor
|
||||
// preset: enabled)`
|
||||
if line.contains("Loaded:") {
|
||||
let before_boot_state = line.find(';');
|
||||
let after_boot_state = line.rfind(';');
|
||||
if let (Some(start), Some(end)) = (before_boot_state, after_boot_state) {
|
||||
// extract the enabled / disabled from the `Loaded: ...` line
|
||||
// using the index of the first ';' + 2 and the last ';'
|
||||
status.boot_state = Some(line[start + 2..end].to_string());
|
||||
}
|
||||
// example of the output line we're looking for here:
|
||||
// `Active: active (running) since Mon 2022-01-24 16:22:51 SAST; 4min 14s ago`
|
||||
} else if line.contains("Active:") {
|
||||
let before_time = line.find(';');
|
||||
let after_time = line.find(" ago");
|
||||
if let (Some(start), Some(end)) = (before_time, after_time) {
|
||||
// extract the uptime / downtime from the `Active: ...` line
|
||||
// using the index of ';' + 2 and the index of " ago"
|
||||
let time = Some(&line[start + 2..end]);
|
||||
// if service is active then the `time` reading is uptime
|
||||
if status.state == Some("active".to_string()) {
|
||||
status.uptime = time.map(|t| t.to_string())
|
||||
// if service is inactive then the `time` reading is downtime
|
||||
} else if status.state == Some("inactive".to_string()) {
|
||||
status.downtime = time.map(|t| t.to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(status)
|
||||
}
|
|
@ -1,8 +1,5 @@
|
|||
*.bak
|
||||
static/icons/optimized/*
|
||||
api_docs.md
|
||||
js_docs.md
|
||||
hashmap_notes
|
||||
notes
|
||||
target
|
||||
**/*.rs.bk
|
||||
leftovers
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
[package]
|
||||
name = "peach-web"
|
||||
version = "0.5.0"
|
||||
authors = ["Andrew Reid <gnomad@cryptolab.net>"]
|
||||
version = "0.6.21"
|
||||
authors = ["Andrew Reid <gnomad@cryptolab.net>", "Max Fowler <max@mfowler.info>"]
|
||||
edition = "2018"
|
||||
description = "peach-web is a web application which provides a web interface for monitoring and interacting with the PeachCloud device. This allows administration of the single-board computer (ie. Raspberry Pi) running PeachCloud, as well as the ssb-server and related plugins."
|
||||
homepage = "https://opencollective.com/peachcloud"
|
||||
|
@ -21,8 +21,6 @@ maintainer-scripts="debian"
|
|||
systemd-units = { unit-name = "peach-web" }
|
||||
assets = [
|
||||
["target/release/peach-web", "/usr/bin/", "755"],
|
||||
["Rocket.toml", "/usr/share/peach-web/Rocket.toml", "644"],
|
||||
["templates/**/*", "/usr/share/peach-web/templates/", "644"],
|
||||
["static/*", "/usr/share/peach-web/static/", "644"],
|
||||
["static/css/*", "/usr/share/peach-web/static/css/", "644"],
|
||||
["static/icons/*", "/usr/share/peach-web/static/icons/", "644"],
|
||||
|
@ -35,18 +33,20 @@ travis-ci = { repository = "peachcloud/peach-web", branch = "master" }
|
|||
maintenance = { status = "actively-developed" }
|
||||
|
||||
[dependencies]
|
||||
async-std = "1.10"
|
||||
base64 = "0.13"
|
||||
chrono = "0.4"
|
||||
dirs = "4.0"
|
||||
env_logger = "0.8"
|
||||
futures = "0.3"
|
||||
golgi = { git = "https://git.coopcloud.tech/golgi-ssb/golgi.git" }
|
||||
lazy_static = "1.4"
|
||||
log = "0.4"
|
||||
nest = "1.0.0"
|
||||
maud = "0.23"
|
||||
peach-lib = { path = "../peach-lib" }
|
||||
peach-network = { path = "../peach-network", features = ["serde_support"] }
|
||||
peach-stats = { path = "../peach-stats", features = ["serde_support"] }
|
||||
rocket = { version = "0.5.0-rc.1", features = ["json", "secrets"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
tera = { version = "1.12.1", features = ["builtins"] }
|
||||
xdg = "2.2.0"
|
||||
|
||||
[dependencies.rocket_dyn_templates]
|
||||
version = "0.1.0-rc.1"
|
||||
features = ["tera"]
|
||||
peach-network = { path = "../peach-network" }
|
||||
peach-stats = { path = "../peach-stats" }
|
||||
rouille = { version = "3.5", default-features = false }
|
||||
temporary = "0.6"
|
||||
vnstat_parse = "0.1.0"
|
||||
xdg = "2.2"
|
||||
|
|
|
@ -1,18 +1,27 @@
|
|||
# peach-web
|
||||
|
||||
[![Build Status](https://travis-ci.com/peachcloud/peach-web.svg?branch=master)](https://travis-ci.com/peachcloud/peach-web) ![Generic badge](https://img.shields.io/badge/version-0.5.0-<COLOR>.svg)
|
||||
![Generic badge](https://img.shields.io/badge/version-0.6.18-<COLOR>.svg)
|
||||
|
||||
## 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 [Rouille](https://crates.io/crates/rouille) (Rust web framework), [Maud](https://maud.lambda.xyz/) (Rust template engine), HTML, CSS and a tiny bit of JS. Scuttlebutt functionality is provided by [golgi](http://golgi.mycelial.technology).
|
||||
|
||||
_Note: This is a work-in-progress._
|
||||
|
||||
### Setup
|
||||
## Setup
|
||||
|
||||
Clone the `peach-workspace` repo:
|
||||
|
||||
|
@ -23,41 +32,58 @@ Move into the repo and compile:
|
|||
`cd peach-workspace/peach-web`
|
||||
`cargo build --release`
|
||||
|
||||
Run the tests:
|
||||
|
||||
`ROCKET_DISABLE_AUTH=true ROCKET_STANDALONE_MODE=false cargo test`
|
||||
|
||||
Move back to the `peach-workspace` directory:
|
||||
|
||||
`cd ..`
|
||||
|
||||
Run the binary:
|
||||
|
||||
`./target/release/peach-web`
|
||||
`../target/release/peach-web`
|
||||
|
||||
### Environment
|
||||
## Development Setup
|
||||
|
||||
**Deployment Profile**
|
||||
In order to test `peach-web` on a development machine you will need to have a running instance of `go-sbot` (please see the [go-sbot README](https://github.com/cryptoscope/ssb) for installation details). The `GO_SBOT_DATADIR` environment variable or corresponding config variable must be set to `/home/<user>/.ssb-go` and the `PEACH_HOMEDIR` variable must be set to `/home/<user>`. See the Configuration section below for more details.
|
||||
|
||||
The web application deployment profile can be configured with the `ROCKET_ENV` environment variable:
|
||||
The `go-sbot` process must be managed by `systemd` in order for it to be controlled via the `peach-web` web interface. Here is a basic `go-sbot.service` file:
|
||||
|
||||
`export ROCKET_ENV=stage`
|
||||
```
|
||||
[Unit]
|
||||
Description=GoSSB server.
|
||||
|
||||
Default configuration parameters are defined in `Rocket.toml`. This file defines a set of default parameters, some of which are overwritten when running in `debug` mode (ie. `cargo run` or `cargo build`) or `release` mode (ie. `cargo run --release` or `cargo build --release`).
|
||||
[Service]
|
||||
ExecStart=/usr/bin/go-sbot
|
||||
Environment="LIBRARIAN_WRITEALL=0"
|
||||
Restart=always
|
||||
|
||||
Read the [Rocket Environment Configurations docs](https://rocket.rs/v0.5-rc/guide/configuration/#environment-variables) for further information.
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
**Configuration Mode**
|
||||
And a `sudoers` rule must be created to allow the `go-sbot.service` state to be modified without requiring a password. Here is an example `/etc/sudoers.d/peach-web` file:
|
||||
|
||||
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.
|
||||
```
|
||||
# Control go-sbot service without sudo passworkd
|
||||
|
||||
**Authentication**
|
||||
<user> ALL=(ALL) NOPASSWD: /bin/systemctl start go-sbot.service, /bin/systemctl restart go-sbot.service, /bin/systemctl stop go-sbot.service, /bin/systemctl enable go-sbot.service, /bin/systemctl disable go-sbot.service
|
||||
```
|
||||
|
||||
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`:
|
||||
## Configuration
|
||||
|
||||
`export ROCKET_DISABLE_AUTH=true`
|
||||
By default, configuration variables are stored in `/var/lib/peachcloud/config.yml`. The variables in the file are updated by `peach-web` when changes are made to configurations via the web interface. Since `peach-web` has no database, all configurations are stored in this file.
|
||||
|
||||
**Logging**
|
||||
A non-default configuration directory can be defined via the `PEACH_CONFIGDIR` environment variable or corresponding key in the `config.yml` file.
|
||||
|
||||
### 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 application runs in PeachPub mode by default. The complete PeachCloud mode will be available once a large refactor is complete; it is not currently in working order so it's best to stick with PeachPub for now.
|
||||
|
||||
The running mode can be defined by setting the `STANDALONE_MODE` environment variable (`true` for PeachPub or `false` for PeachCloud). Alternatively, the desired mode can be set by modifying the PeachCloud configuration file.
|
||||
|
||||
### Authentication
|
||||
|
||||
Authentication is enabled by default when running the application. It can be disabled by setting the `DISABLE_AUTH` environment variable to `true`:
|
||||
|
||||
`export DISABLE_AUTH=true`
|
||||
|
||||
### Logging
|
||||
|
||||
Logging is made available with `env_logger`:
|
||||
|
||||
|
@ -65,7 +91,15 @@ Logging is made available with `env_logger`:
|
|||
|
||||
Other logging levels include `debug`, `warn` and `error`.
|
||||
|
||||
### Debian Packaging
|
||||
### 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.
|
||||
If the config dyn_use_custom_server=true, then a value must also be set for dyn_dns_server_address (e.g. "http://peachdynserver.commoninternet.net").
|
||||
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.
|
||||
|
||||
## 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,24 +131,10 @@ 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.
|
||||
`peach-web` has been designed with simplicity and resource minimalism in mind. Both the dependencies used by the project, as well as the code itself, reflect these design priorities. The Rouille micro-web-framework and Maud templating engine have been used to present a web interface for interacting with the device. HTML is rendered server-side and request handlers call `peach-` libraries and serve HTML and assets. The optimised binary for `peach-web` can be compiled on a RPi 3 B+ in approximately 30 minutes.
|
||||
|
||||
### 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
|
||||
|
||||
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.
|
||||
If the config dyn_use_custom_server=true, then a value must also be set for dyn_dns_server_address (e.g. "http://peachdynserver.commoninternet.net").
|
||||
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
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
[default]
|
||||
secret_key = "VYVUDivXvu8g6llxeJd9F92pMfocml5xl/Jjv5Sk4yw="
|
||||
disable_auth = false
|
||||
standalone_mode = true
|
||||
|
||||
[debug]
|
||||
template_dir = "templates/"
|
||||
disable_auth = true
|
||||
|
||||
[release]
|
||||
template_dir = "templates/"
|
|
@ -1,14 +1,10 @@
|
|||
[Unit]
|
||||
Description=Rocket web application for serving the PeachCloud web interface.
|
||||
Description=Rouille web application for serving the PeachCloud web interface.
|
||||
|
||||
[Service]
|
||||
User=peach-web
|
||||
Group=www-data
|
||||
User=peach
|
||||
Group=peach
|
||||
WorkingDirectory=/usr/share/peach-web
|
||||
Environment="ROCKET_ENV=prod"
|
||||
Environment="ROCKET_ADDRESS=127.0.0.1"
|
||||
Environment="ROCKET_PORT=3000"
|
||||
Environment="ROCKET_LOG=critical"
|
||||
Environment="RUST_LOG=info"
|
||||
ExecStart=/usr/bin/peach-web
|
||||
Restart=always
|
||||
|
|
|
@ -2,8 +2,7 @@
|
|||
set -e
|
||||
|
||||
# create user which peach-web runs as
|
||||
adduser --quiet --system peach-web
|
||||
usermod -g peach peach-web
|
||||
id -u peach &>/dev/null || adduser --quiet peach
|
||||
|
||||
# create nginx config
|
||||
cat <<EOF > /etc/nginx/sites-enabled/default
|
||||
|
@ -15,16 +14,25 @@ server {
|
|||
rewrite ^/(.*)/$ /$1 permanent;
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
proxy_pass http://127.0.0.1:8000;
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
cat <<EOF > /etc/sudoers.d/peach-web
|
||||
# allow peach-web to run commands as peach-go-sbot without a password
|
||||
peach-web ALL=(peach-go-sbot) NOPASSWD:ALL
|
||||
# update sudoers to allow peach-web to stop and restart go-sbot.service
|
||||
mkdir -p /etc/sudoers.d/
|
||||
|
||||
SYSTEMCTL=/bin/systemctl
|
||||
START="${SYSTEMCTL} start go-sbot.service"
|
||||
RESTART="${SYSTEMCTL} restart go-sbot.service"
|
||||
STOP="${SYSTEMCTL} stop go-sbot.service"
|
||||
ENABLE="${SYSTEMCTL} enable go-sbot.service"
|
||||
DISABLE="${SYSTEMCTL} disable go-sbot.service"
|
||||
|
||||
cat <<EOF > /etc/sudoers.d/peach-web
|
||||
peach ALL=(ALL) NOPASSWD: $START, $STOP, $RESTART, $ENABLE, $DISABLE
|
||||
EOF
|
||||
chmod 0440 /etc/sudoers.d/peach-web
|
||||
|
||||
# cargo deb automatically replaces this token below, see https://github.com/mmstick/cargo-deb/blob/master/systemd.md
|
||||
#DEBHELPER#
|
|
@ -0,0 +1,31 @@
|
|||
//! Define the configuration parameters for the web application.
|
||||
//!
|
||||
//! These configs are loaded using peach-lib::config_manager which checks config keys from
|
||||
//! three sources:
|
||||
//! 1. from environmental variables
|
||||
//! 2. from a configuration file
|
||||
//! 3. from default values
|
||||
|
||||
use crate::error::PeachWebError;
|
||||
use peach_lib::config_manager::get_config_value;
|
||||
|
||||
pub struct ServerConfig {
|
||||
pub standalone_mode: bool,
|
||||
pub disable_auth: bool,
|
||||
pub addr: String,
|
||||
pub port: String,
|
||||
}
|
||||
|
||||
impl ServerConfig {
|
||||
pub fn new() -> Result<ServerConfig, PeachWebError> {
|
||||
// define default config values
|
||||
let config = ServerConfig {
|
||||
standalone_mode: get_config_value("STANDALONE_MODE")?.as_str() == "true",
|
||||
disable_auth: get_config_value("DISABLE_AUTH")?.as_str() == "true",
|
||||
addr: get_config_value("ADDR")?,
|
||||
port: get_config_value("PORT")?,
|
||||
};
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
}
|
|
@ -1,36 +0,0 @@
|
|||
use peach_lib::{config_manager, dyndns_client};
|
||||
use rocket::serde::Serialize;
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct ConfigureDNSContext {
|
||||
pub external_domain: String,
|
||||
pub dyndns_subdomain: String,
|
||||
pub enable_dyndns: bool,
|
||||
pub is_dyndns_online: bool,
|
||||
pub back: Option<String>,
|
||||
pub title: Option<String>,
|
||||
pub flash_name: Option<String>,
|
||||
pub flash_msg: Option<String>,
|
||||
}
|
||||
|
||||
impl ConfigureDNSContext {
|
||||
pub fn build() -> ConfigureDNSContext {
|
||||
// TODO: replace `unwrap` with resilient error handling
|
||||
let peach_config = config_manager::load_peach_config().unwrap();
|
||||
let dyndns_fulldomain = peach_config.dyn_domain;
|
||||
let is_dyndns_online = dyndns_client::is_dns_updater_online().unwrap();
|
||||
let dyndns_subdomain =
|
||||
dyndns_client::get_dyndns_subdomain(&dyndns_fulldomain).unwrap_or(dyndns_fulldomain);
|
||||
|
||||
ConfigureDNSContext {
|
||||
external_domain: peach_config.external_domain,
|
||||
dyndns_subdomain,
|
||||
enable_dyndns: peach_config.dyn_enabled,
|
||||
is_dyndns_online,
|
||||
back: None,
|
||||
title: None,
|
||||
flash_name: None,
|
||||
flash_msg: None,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
pub mod dns;
|
||||
pub mod network;
|
|
@ -1,398 +0,0 @@
|
|||
//! Data retrieval for the purpose of serving routes and hydrating
|
||||
//! network-related HTML templates.
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use rocket::{
|
||||
form::FromForm,
|
||||
serde::{Deserialize, Serialize},
|
||||
UriDisplayQuery,
|
||||
};
|
||||
|
||||
use peach_network::{
|
||||
network,
|
||||
network::{Scan, Status, Traffic},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
utils::{
|
||||
monitor,
|
||||
monitor::{Alert, Data, Threshold},
|
||||
},
|
||||
AP_IFACE, WLAN_IFACE,
|
||||
};
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct AccessPoint {
|
||||
pub detail: Option<Scan>,
|
||||
pub signal: Option<i32>,
|
||||
pub state: String,
|
||||
}
|
||||
|
||||
pub fn ap_state() -> String {
|
||||
match network::state(&*AP_IFACE) {
|
||||
Ok(Some(state)) => state,
|
||||
_ => "Interface unavailable".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, FromForm, UriDisplayQuery)]
|
||||
pub struct Ssid {
|
||||
pub ssid: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, FromForm)]
|
||||
pub struct WiFi {
|
||||
pub ssid: String,
|
||||
pub pass: String,
|
||||
}
|
||||
|
||||
fn convert_traffic(traffic: Traffic) -> Option<IfaceTraffic> {
|
||||
// modify traffic values & assign measurement unit
|
||||
// based on received and transmitted values
|
||||
let (rx, rx_unit) = if traffic.received > 1_047_527_424 {
|
||||
// convert to GB
|
||||
(traffic.received / 1_073_741_824, "GB".to_string())
|
||||
} else if traffic.received > 0 {
|
||||
// otherwise, convert it to MB
|
||||
((traffic.received / 1024) / 1024, "MB".to_string())
|
||||
} else {
|
||||
(0, "MB".to_string())
|
||||
};
|
||||
|
||||
let (tx, tx_unit) = if traffic.transmitted > 1_047_527_424 {
|
||||
// convert to GB
|
||||
(traffic.transmitted / 1_073_741_824, "GB".to_string())
|
||||
} else if traffic.transmitted > 0 {
|
||||
((traffic.transmitted / 1024) / 1024, "MB".to_string())
|
||||
} else {
|
||||
(0, "MB".to_string())
|
||||
};
|
||||
|
||||
Some(IfaceTraffic {
|
||||
rx,
|
||||
rx_unit,
|
||||
tx,
|
||||
tx_unit,
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct IfaceTraffic {
|
||||
pub rx: u64,
|
||||
pub rx_unit: String,
|
||||
pub tx: u64,
|
||||
pub tx_unit: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct NetworkAlertContext {
|
||||
pub alert: Alert,
|
||||
pub back: Option<String>,
|
||||
pub data_total: Option<Data>, // combined stored and current wifi traffic in bytes
|
||||
pub flash_name: Option<String>,
|
||||
pub flash_msg: Option<String>,
|
||||
pub threshold: Threshold,
|
||||
pub title: Option<String>,
|
||||
pub traffic: Option<IfaceTraffic>, // current wifi traffic in bytes (since boot)
|
||||
}
|
||||
|
||||
impl NetworkAlertContext {
|
||||
pub fn build() -> NetworkAlertContext {
|
||||
let alert = monitor::get_alerts().unwrap();
|
||||
// stored wifi data values as bytes
|
||||
let stored_traffic = monitor::get_data().unwrap();
|
||||
let threshold = monitor::get_thresholds().unwrap();
|
||||
|
||||
let (traffic, data_total) = match network::traffic(&*WLAN_IFACE) {
|
||||
// convert bytes to mb or gb and add appropriate units
|
||||
Ok(Some(t)) => {
|
||||
let current_traffic = t.received + t.transmitted;
|
||||
let traffic = convert_traffic(t);
|
||||
let total = stored_traffic.total + current_traffic;
|
||||
let data_total = Data { total };
|
||||
(traffic, Some(data_total))
|
||||
}
|
||||
_ => (None, None),
|
||||
};
|
||||
|
||||
NetworkAlertContext {
|
||||
alert,
|
||||
back: None,
|
||||
data_total,
|
||||
flash_name: None,
|
||||
flash_msg: None,
|
||||
threshold,
|
||||
title: None,
|
||||
traffic,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct NetworkDetailContext {
|
||||
pub back: Option<String>,
|
||||
pub flash_name: Option<String>,
|
||||
pub flash_msg: Option<String>,
|
||||
pub selected: Option<String>,
|
||||
pub title: Option<String>,
|
||||
pub saved_aps: Vec<String>,
|
||||
pub wlan_ip: String,
|
||||
pub wlan_networks: HashMap<String, AccessPoint>,
|
||||
pub wlan_rssi: Option<String>,
|
||||
pub wlan_ssid: String,
|
||||
pub wlan_state: String,
|
||||
pub wlan_status: Option<Status>,
|
||||
pub wlan_traffic: Option<IfaceTraffic>,
|
||||
}
|
||||
|
||||
impl NetworkDetailContext {
|
||||
pub fn build() -> NetworkDetailContext {
|
||||
let wlan_ip = match network::ip(&*WLAN_IFACE) {
|
||||
Ok(Some(ip)) => ip,
|
||||
_ => "x.x.x.x".to_string(),
|
||||
};
|
||||
|
||||
// list of networks saved in wpa_supplicant.conf
|
||||
let wlan_list = match network::saved_networks() {
|
||||
Ok(Some(ssids)) => ssids,
|
||||
_ => Vec::new(),
|
||||
};
|
||||
|
||||
// list of networks saved in wpa_supplicant.conf
|
||||
let saved_aps = wlan_list.clone();
|
||||
|
||||
let wlan_rssi = match network::rssi_percent(&*WLAN_IFACE) {
|
||||
Ok(rssi) => rssi,
|
||||
Err(_) => None,
|
||||
};
|
||||
|
||||
// list of networks currently in range (online & accessible)
|
||||
let wlan_scan = match network::available_networks(&*WLAN_IFACE) {
|
||||
Ok(Some(networks)) => networks,
|
||||
_ => Vec::new(),
|
||||
};
|
||||
|
||||
let wlan_ssid = match network::ssid(&*WLAN_IFACE) {
|
||||
Ok(Some(ssid)) => ssid,
|
||||
_ => "Not connected".to_string(),
|
||||
};
|
||||
|
||||
let wlan_state = match network::state(&*WLAN_IFACE) {
|
||||
Ok(Some(state)) => state,
|
||||
_ => "Interface unavailable".to_string(),
|
||||
};
|
||||
|
||||
let wlan_status = match network::status(&*WLAN_IFACE) {
|
||||
Ok(status) => status,
|
||||
// interface unavailable
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let wlan_traffic = match network::traffic(&*WLAN_IFACE) {
|
||||
// convert bytes to mb or gb and add appropriate units
|
||||
Ok(Some(traffic)) => convert_traffic(traffic),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
// create a hashmap to combine wlan_list & wlan_scan without repetition
|
||||
let mut wlan_networks = HashMap::new();
|
||||
|
||||
for ap in wlan_scan {
|
||||
let ssid = ap.ssid.clone();
|
||||
let rssi = ap.signal_level.clone();
|
||||
// parse the string to a signed integer (for math)
|
||||
let rssi_parsed = rssi.parse::<i32>().unwrap();
|
||||
// perform rssi (dBm) to quality (%) conversion
|
||||
let quality_percent = 2 * (rssi_parsed + 100);
|
||||
let ap_detail = AccessPoint {
|
||||
detail: Some(ap),
|
||||
state: "Available".to_string(),
|
||||
signal: Some(quality_percent),
|
||||
};
|
||||
wlan_networks.insert(ssid, ap_detail);
|
||||
}
|
||||
|
||||
for network in wlan_list {
|
||||
// avoid repetition by checking that ssid is not already in list
|
||||
if !wlan_networks.contains_key(&network) {
|
||||
let ssid = network.clone();
|
||||
let net_detail = AccessPoint {
|
||||
detail: None,
|
||||
state: "Not in range".to_string(),
|
||||
signal: None,
|
||||
};
|
||||
wlan_networks.insert(ssid, net_detail);
|
||||
}
|
||||
}
|
||||
|
||||
NetworkDetailContext {
|
||||
back: None,
|
||||
flash_name: None,
|
||||
flash_msg: None,
|
||||
selected: None,
|
||||
title: None,
|
||||
saved_aps,
|
||||
wlan_ip,
|
||||
wlan_networks,
|
||||
wlan_rssi,
|
||||
wlan_ssid,
|
||||
wlan_state,
|
||||
wlan_status,
|
||||
wlan_traffic,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct NetworkListContext {
|
||||
pub ap_state: String,
|
||||
pub back: Option<String>,
|
||||
pub flash_name: Option<String>,
|
||||
pub flash_msg: Option<String>,
|
||||
pub title: Option<String>,
|
||||
pub wlan_networks: HashMap<String, String>,
|
||||
pub wlan_ssid: String,
|
||||
}
|
||||
|
||||
impl NetworkListContext {
|
||||
pub fn build() -> NetworkListContext {
|
||||
// list of networks saved in wpa_supplicant.conf
|
||||
let wlan_list = match network::saved_networks() {
|
||||
Ok(Some(ssids)) => ssids,
|
||||
_ => Vec::new(),
|
||||
};
|
||||
|
||||
// list of networks currently in range (online & accessible)
|
||||
let wlan_scan = match network::available_networks(&*WLAN_IFACE) {
|
||||
Ok(Some(networks)) => networks,
|
||||
_ => Vec::new(),
|
||||
};
|
||||
|
||||
let wlan_ssid = match network::ssid(&*WLAN_IFACE) {
|
||||
Ok(Some(ssid)) => ssid,
|
||||
_ => "Not connected".to_string(),
|
||||
};
|
||||
|
||||
// create a hashmap to combine wlan_list & wlan_scan without repetition
|
||||
let mut wlan_networks = HashMap::new();
|
||||
for ap in wlan_scan {
|
||||
wlan_networks.insert(ap.ssid, "Available".to_string());
|
||||
}
|
||||
for network in wlan_list {
|
||||
// insert ssid (with state) only if it doesn't already exist
|
||||
wlan_networks
|
||||
.entry(network)
|
||||
.or_insert_with(|| "Not in range".to_string());
|
||||
}
|
||||
|
||||
let ap_state = match network::state(&*AP_IFACE) {
|
||||
Ok(Some(state)) => state,
|
||||
_ => "Interface unavailable".to_string(),
|
||||
};
|
||||
|
||||
NetworkListContext {
|
||||
ap_state,
|
||||
back: None,
|
||||
flash_msg: None,
|
||||
flash_name: None,
|
||||
title: None,
|
||||
wlan_networks,
|
||||
wlan_ssid,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct NetworkStatusContext {
|
||||
pub ap_ip: String,
|
||||
pub ap_ssid: String,
|
||||
pub ap_state: String,
|
||||
pub ap_traffic: Option<IfaceTraffic>,
|
||||
pub wlan_ip: String,
|
||||
pub wlan_rssi: Option<String>,
|
||||
pub wlan_ssid: String,
|
||||
pub wlan_state: String,
|
||||
pub wlan_status: Option<Status>,
|
||||
pub wlan_traffic: Option<IfaceTraffic>,
|
||||
pub flash_name: Option<String>,
|
||||
pub flash_msg: Option<String>,
|
||||
// passing in the ssid of a chosen access point
|
||||
pub selected: Option<String>,
|
||||
pub title: Option<String>,
|
||||
pub back: Option<String>,
|
||||
}
|
||||
|
||||
impl NetworkStatusContext {
|
||||
pub fn build() -> Self {
|
||||
let ap_ip = match network::ip(&*AP_IFACE) {
|
||||
Ok(Some(ip)) => ip,
|
||||
_ => "x.x.x.x".to_string(),
|
||||
};
|
||||
|
||||
let ap_ssid = match network::ssid(&*AP_IFACE) {
|
||||
Ok(Some(ssid)) => ssid,
|
||||
_ => "Not currently activated".to_string(),
|
||||
};
|
||||
|
||||
let ap_state = match network::state(&*AP_IFACE) {
|
||||
Ok(Some(state)) => state,
|
||||
_ => "Interface unavailable".to_string(),
|
||||
};
|
||||
|
||||
let ap_traffic = match network::traffic(&*AP_IFACE) {
|
||||
// convert bytes to mb or gb and add appropriate units
|
||||
Ok(Some(traffic)) => convert_traffic(traffic),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let wlan_ip = match network::ip(&*WLAN_IFACE) {
|
||||
Ok(Some(ip)) => ip,
|
||||
_ => "x.x.x.x".to_string(),
|
||||
};
|
||||
|
||||
let wlan_rssi = match network::rssi_percent(&*WLAN_IFACE) {
|
||||
Ok(rssi) => rssi,
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let wlan_ssid = match network::ssid(&*WLAN_IFACE) {
|
||||
Ok(Some(ssid)) => ssid,
|
||||
_ => "Not connected".to_string(),
|
||||
};
|
||||
|
||||
let wlan_state = match network::state(&*WLAN_IFACE) {
|
||||
Ok(Some(state)) => state,
|
||||
_ => "Interface unavailable".to_string(),
|
||||
};
|
||||
|
||||
let wlan_status = match network::status(&*WLAN_IFACE) {
|
||||
Ok(status) => status,
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let wlan_traffic = match network::traffic(&*WLAN_IFACE) {
|
||||
// convert bytes to mb or gb and add appropriate units
|
||||
Ok(Some(traffic)) => convert_traffic(traffic),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
NetworkStatusContext {
|
||||
ap_ip,
|
||||
ap_ssid,
|
||||
ap_state,
|
||||
ap_traffic,
|
||||
wlan_ip,
|
||||
wlan_rssi,
|
||||
wlan_ssid,
|
||||
wlan_state,
|
||||
wlan_status,
|
||||
wlan_traffic,
|
||||
flash_name: None,
|
||||
flash_msg: None,
|
||||
selected: None,
|
||||
title: None,
|
||||
back: None,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,5 +1,8 @@
|
|||
//! Custom error type representing all possible error variants for peach-web.
|
||||
|
||||
use std::io::Error as IoError;
|
||||
|
||||
use golgi::GolgiError;
|
||||
use peach_lib::error::PeachError;
|
||||
use peach_lib::{serde_json, serde_yaml};
|
||||
use serde_json::error::Error as JsonError;
|
||||
|
@ -8,19 +11,27 @@ use serde_yaml::Error as YamlError;
|
|||
/// Custom error type encapsulating all possible errors for the web application.
|
||||
#[derive(Debug)]
|
||||
pub enum PeachWebError {
|
||||
Json(JsonError),
|
||||
Yaml(YamlError),
|
||||
FailedToRegisterDynDomain(String),
|
||||
Golgi(GolgiError),
|
||||
HomeDir,
|
||||
Io(IoError),
|
||||
Json(JsonError),
|
||||
OsString,
|
||||
PeachLib { source: PeachError, msg: String },
|
||||
Yaml(YamlError),
|
||||
}
|
||||
|
||||
impl std::error::Error for PeachWebError {
|
||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||
match *self {
|
||||
PeachWebError::Json(ref source) => Some(source),
|
||||
PeachWebError::Yaml(ref source) => Some(source),
|
||||
PeachWebError::FailedToRegisterDynDomain(_) => None,
|
||||
PeachWebError::Golgi(ref source) => Some(source),
|
||||
PeachWebError::HomeDir => None,
|
||||
PeachWebError::Io(ref source) => Some(source),
|
||||
PeachWebError::Json(ref source) => Some(source),
|
||||
PeachWebError::OsString => None,
|
||||
PeachWebError::PeachLib { ref source, .. } => Some(source),
|
||||
PeachWebError::Yaml(ref source) => Some(source),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -28,28 +39,44 @@ impl std::error::Error for PeachWebError {
|
|||
impl std::fmt::Display for PeachWebError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
match *self {
|
||||
PeachWebError::Json(ref source) => write!(f, "Serde JSON error: {}", source),
|
||||
PeachWebError::Yaml(ref source) => write!(f, "Serde YAML error: {}", source),
|
||||
PeachWebError::FailedToRegisterDynDomain(ref msg) => {
|
||||
write!(f, "DYN DNS error: {}", msg)
|
||||
}
|
||||
PeachWebError::Golgi(ref source) => write!(f, "Golgi error: {}", source),
|
||||
PeachWebError::HomeDir => write!(
|
||||
f,
|
||||
"Filesystem error: failed to determine home directory path"
|
||||
),
|
||||
PeachWebError::Io(ref source) => write!(f, "IO error: {}", source),
|
||||
PeachWebError::Json(ref source) => write!(f, "Serde JSON error: {}", source),
|
||||
PeachWebError::OsString => write!(
|
||||
f,
|
||||
"Filesystem error: failed to convert OsString to String for go-ssb directory path"
|
||||
),
|
||||
PeachWebError::PeachLib { ref source, .. } => write!(f, "{}", source),
|
||||
PeachWebError::Yaml(ref source) => write!(f, "Serde YAML error: {}", source),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<GolgiError> for PeachWebError {
|
||||
fn from(err: GolgiError) -> PeachWebError {
|
||||
PeachWebError::Golgi(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<IoError> for PeachWebError {
|
||||
fn from(err: IoError) -> PeachWebError {
|
||||
PeachWebError::Io(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<JsonError> for PeachWebError {
|
||||
fn from(err: JsonError) -> PeachWebError {
|
||||
PeachWebError::Json(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<YamlError> for PeachWebError {
|
||||
fn from(err: YamlError) -> PeachWebError {
|
||||
PeachWebError::Yaml(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PeachError> for PeachWebError {
|
||||
fn from(err: PeachError) -> PeachWebError {
|
||||
PeachWebError::PeachLib {
|
||||
|
@ -58,3 +85,9 @@ impl From<PeachError> for PeachWebError {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<YamlError> for PeachWebError {
|
||||
fn from(err: YamlError) -> PeachWebError {
|
||||
PeachWebError::Yaml(err)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,90 +8,116 @@
|
|||
//! ## Design
|
||||
//!
|
||||
//! `peach-web` is written primarily in Rust and presents a web interface for
|
||||
//! interacting with the device. The stack currently consists of Rocket (Rust
|
||||
//! web framework), Tera (Rust template engine inspired by Jinja2 and the Django
|
||||
//! template language), HTML, CSS and JavaScript. Additional functionality is
|
||||
//! provided by JSON-RPC clients for the `peach-network` and `peach-stats`
|
||||
//! microservices.
|
||||
//!
|
||||
//! HTML is rendered server-side. Request handlers call JSON-RPC microservices
|
||||
//! and serve HTML and assets. A JSON API is exposed for remote calls and
|
||||
//! dynamic client-side content updates via vanilla JavaScript following
|
||||
//! unobstructive design principles. 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.
|
||||
//! interacting with the device. The stack currently consists of Rouille (Rust
|
||||
//! micro-web-framework), Maud (an HTML template engine for Rust), HTML and
|
||||
//! CSS.
|
||||
|
||||
#![feature(proc_macro_hygiene, decl_macro)]
|
||||
|
||||
mod context;
|
||||
mod config;
|
||||
pub mod error;
|
||||
mod router;
|
||||
pub mod routes;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
mod private_router;
|
||||
mod public_router;
|
||||
mod routes;
|
||||
mod templates;
|
||||
pub mod utils;
|
||||
|
||||
use std::process;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
sync::{Mutex, RwLock},
|
||||
};
|
||||
|
||||
use log::{debug, error, info};
|
||||
use rocket::{fairing::AdHoc, serde::Deserialize, Build, Rocket};
|
||||
use lazy_static::lazy_static;
|
||||
use log::info;
|
||||
|
||||
pub type BoxError = Box<dyn std::error::Error>;
|
||||
// crate-local dependencies
|
||||
use config::ServerConfig;
|
||||
use utils::theme::Theme;
|
||||
|
||||
/// Application configuration parameters.
|
||||
/// These values are extracted from Rocket's default configuration provider:
|
||||
/// `Config::figment()`. As such, the values are drawn from `Rocket.toml` or
|
||||
/// the TOML file path in the `ROCKET_CONFIG` environment variable. The TOML
|
||||
/// file parameters are automatically overruled by any `ROCKET_` variables
|
||||
/// which might be set.
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct RocketConfig {
|
||||
disable_auth: bool,
|
||||
standalone_mode: bool,
|
||||
// load the application configuration and create the theme switcher
|
||||
lazy_static! {
|
||||
static ref SERVER_CONFIG: ServerConfig =
|
||||
ServerConfig::new().expect("Failed to load rouille configuration values on server startup");
|
||||
static ref THEME: RwLock<Theme> = RwLock::new(Theme::Light);
|
||||
}
|
||||
|
||||
static WLAN_IFACE: &str = "wlan0";
|
||||
static AP_IFACE: &str = "ap0";
|
||||
/// Wireless interface identifier.
|
||||
pub const WLAN_IFACE: &str = "wlan0";
|
||||
|
||||
pub fn init_rocket() -> Rocket<Build> {
|
||||
info!("Initializing Rocket");
|
||||
// build a basic rocket instance
|
||||
let rocket = rocket::build();
|
||||
/// Access point interface identifier.
|
||||
pub const AP_IFACE: &str = "ap0";
|
||||
|
||||
// return the default provider figment used by `rocket::build()`
|
||||
let figment = rocket.figment();
|
||||
|
||||
// deserialize configuration parameters into our `RocketConfig` struct (defined above)
|
||||
// since we're in the intialisation phase, panic if the extraction fails
|
||||
let config: RocketConfig = figment.extract().expect("configuration extraction failed");
|
||||
|
||||
debug!("{:?}", config);
|
||||
|
||||
info!("Mounting Rocket routes");
|
||||
let mounted_rocket = if config.standalone_mode {
|
||||
router::mount_peachpub_routes(rocket)
|
||||
} else {
|
||||
router::mount_peachcloud_routes(rocket)
|
||||
};
|
||||
|
||||
info!("Attaching application configuration to managed state");
|
||||
mounted_rocket.attach(AdHoc::config::<RocketConfig>())
|
||||
/// Session data for each authenticated client.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SessionData {
|
||||
_login: String,
|
||||
}
|
||||
|
||||
/// Launch the peach-web rocket server.
|
||||
#[rocket::main]
|
||||
async fn main() {
|
||||
/// Launch the peach-web server.
|
||||
fn main() {
|
||||
// initialize logger
|
||||
env_logger::init();
|
||||
|
||||
// initialize rocket
|
||||
let rocket = init_rocket();
|
||||
// set ip address / hostname and port for the webserver
|
||||
// defaults to "127.0.0.1:8000"
|
||||
let addr_and_port = format!("{}:{}", SERVER_CONFIG.addr, SERVER_CONFIG.port);
|
||||
|
||||
// launch rocket
|
||||
info!("Launching Rocket");
|
||||
if let Err(e) = rocket.launch().await {
|
||||
error!("Error in Rocket application: {}", e);
|
||||
process::exit(1);
|
||||
}
|
||||
// store the session data for each session and a hashmap that associates
|
||||
// each session id with the data
|
||||
// note: we are storing this data in memory. all sessions are erased when
|
||||
// the program is restarted.
|
||||
let sessions_storage: Mutex<HashMap<String, SessionData>> = Mutex::new(HashMap::new());
|
||||
|
||||
info!("Launching web server on {}", addr_and_port);
|
||||
|
||||
// the `start_server` starts listening forever on the given address
|
||||
rouille::start_server(addr_and_port, move |request| {
|
||||
// assign a unique id to each client (appends a cookie to the response
|
||||
// with a name of "SID" and a duration of one hour (3600 seconds)
|
||||
rouille::session::session(request, "SID", 3600, |session| {
|
||||
// if the "DISABLE_AUTH" env var is true, authenticate the session
|
||||
let mut session_data = if SERVER_CONFIG.disable_auth {
|
||||
Some(SessionData {
|
||||
_login: "success".to_string(),
|
||||
})
|
||||
// if the client already has an identifier from a previous request,
|
||||
// try to load the existing session data. if successful, make a
|
||||
// copy of the data in order to avoid locking the session for too
|
||||
// long
|
||||
} else if session.client_has_sid() {
|
||||
sessions_storage.lock().unwrap().get(session.id()).cloned()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// pass the request to the public router
|
||||
//
|
||||
// the public router includes authentication-related routes which
|
||||
// do not require the user to be authenticated (ie. login and reset
|
||||
// password)
|
||||
//
|
||||
// if the user is already authenticated, their request will be
|
||||
// passed to the private router by public_router::handle_route()
|
||||
//
|
||||
// we pass a mutable reference to the `Option<SessionData>` so that
|
||||
// the function is free to modify it
|
||||
let response = public_router::handle_route(request, &mut session_data);
|
||||
|
||||
// since the function call to `handle_route` can modify the session
|
||||
// data, we have to store it back in the `sessions_storage` after
|
||||
// the request has been handled
|
||||
if let Some(data) = session_data {
|
||||
sessions_storage
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert(session.id().to_owned(), data);
|
||||
} else if session.client_has_sid() {
|
||||
// if the content of the `Option` was erased (ie. due to
|
||||
// deauthentication on logout), remove the session from the
|
||||
// storage. this is only done if the client already has an
|
||||
// identifier, otherwise calling `session.id()` will assign one
|
||||
sessions_storage.lock().unwrap().remove(session.id());
|
||||
}
|
||||
|
||||
response
|
||||
})
|
||||
});
|
||||
}
|
||||
|
|
|
@ -0,0 +1,279 @@
|
|||
use rouille::{router, Request, Response};
|
||||
|
||||
use crate::{
|
||||
routes, templates,
|
||||
utils::{cookie::CookieResponse, flash::FlashResponse},
|
||||
SessionData,
|
||||
};
|
||||
|
||||
// TODO: add mount_peachcloud_routes()
|
||||
// https://github.com/tomaka/rouille/issues/232#issuecomment-919225104
|
||||
|
||||
/// Define the PeachPub router.
|
||||
///
|
||||
/// Takes an incoming request and matches on the defined routes,
|
||||
/// returning either a template or a redirect.
|
||||
///
|
||||
/// All of these routes require the user to be authenticated. See the
|
||||
/// `public_router` for publically-accessible, authentication-related routes.
|
||||
///
|
||||
/// Excludes settings and status routes related to networking and the device
|
||||
/// (memory, hard disk, CPU etc.).
|
||||
pub fn mount_peachpub_routes(
|
||||
request: &Request,
|
||||
session_data: &mut Option<SessionData>,
|
||||
) -> Response {
|
||||
router!(request,
|
||||
(GET) (/) => {
|
||||
Response::html(routes::home::build_template())
|
||||
// reset the back_url cookie each time we visit the homepage
|
||||
.reset_cookie("back_url")
|
||||
},
|
||||
|
||||
(GET) (/auth/change) => {
|
||||
// build the html template
|
||||
Response::html(routes::authentication::change::build_template(request))
|
||||
// reset the flash msg cookies in the response object
|
||||
.reset_flash()
|
||||
},
|
||||
|
||||
(POST) (/auth/change) => {
|
||||
routes::authentication::change::handle_form(request)
|
||||
},
|
||||
|
||||
(GET) (/auth/logout) => {
|
||||
routes::authentication::logout::deauthenticate(session_data)
|
||||
},
|
||||
|
||||
(GET) (/guide) => {
|
||||
Response::html(routes::guide::build_template())
|
||||
},
|
||||
|
||||
(POST) (/scuttlebutt/block) => {
|
||||
routes::scuttlebutt::block::handle_form(request)
|
||||
},
|
||||
|
||||
(GET) (/scuttlebutt/blocks) => {
|
||||
Response::html(routes::scuttlebutt::blocks::build_template())
|
||||
// add a back_url cookie to allow the path of the back button
|
||||
// to be set correctly on the /scuttlebutt/profile page
|
||||
.add_cookie("back_url=/scuttlebutt/blocks")
|
||||
},
|
||||
|
||||
(POST) (/scuttlebutt/follow) => {
|
||||
routes::scuttlebutt::follow::handle_form(request)
|
||||
},
|
||||
|
||||
(GET) (/scuttlebutt/follows) => {
|
||||
Response::html(routes::scuttlebutt::follows::build_template())
|
||||
// add a back_url cookie to allow the path of the back button
|
||||
// to be set correctly on the /scuttlebutt/profile page
|
||||
.add_cookie("back_url=/scuttlebutt/follows")
|
||||
},
|
||||
|
||||
(GET) (/scuttlebutt/friends) => {
|
||||
Response::html(routes::scuttlebutt::friends::build_template())
|
||||
// add a back_url cookie to allow the path of the back button
|
||||
// to be set correctly on the /scuttlebutt/profile page
|
||||
.add_cookie("back_url=/scuttlebutt/friends")
|
||||
},
|
||||
|
||||
(GET) (/scuttlebutt/invites) => {
|
||||
Response::html(routes::scuttlebutt::invites::build_template(request))
|
||||
.reset_flash()
|
||||
},
|
||||
|
||||
(POST) (/scuttlebutt/invites) => {
|
||||
routes::scuttlebutt::invites::handle_form(request)
|
||||
},
|
||||
|
||||
(GET) (/scuttlebutt/peers) => {
|
||||
Response::html(routes::scuttlebutt::peers::build_template())
|
||||
},
|
||||
|
||||
(GET) (/scuttlebutt/private) => {
|
||||
Response::html(routes::scuttlebutt::private::build_template(request, None))
|
||||
},
|
||||
|
||||
(POST) (/scuttlebutt/private) => {
|
||||
routes::scuttlebutt::private::handle_form(request)
|
||||
},
|
||||
|
||||
(GET) (/scuttlebutt/private/{ssb_id: String}) => {
|
||||
Response::html(routes::scuttlebutt::private::build_template(request, Some(ssb_id)))
|
||||
},
|
||||
|
||||
(GET) (/scuttlebutt/profile) => {
|
||||
Response::html(routes::scuttlebutt::profile::build_template(request, None))
|
||||
.reset_flash()
|
||||
},
|
||||
|
||||
(GET) (/scuttlebutt/profile/update) => {
|
||||
Response::html(routes::scuttlebutt::profile_update::build_template(request))
|
||||
.reset_flash()
|
||||
},
|
||||
|
||||
(POST) (/scuttlebutt/profile/update) => {
|
||||
routes::scuttlebutt::profile_update::handle_form(request)
|
||||
},
|
||||
|
||||
(GET) (/scuttlebutt/profile/{ssb_id: String}) => {
|
||||
Response::html(routes::scuttlebutt::profile::build_template(request, Some(ssb_id)))
|
||||
},
|
||||
|
||||
(POST) (/scuttlebutt/publish) => {
|
||||
routes::scuttlebutt::publish::handle_form(request)
|
||||
},
|
||||
|
||||
(GET) (/scuttlebutt/search) => {
|
||||
Response::html(routes::scuttlebutt::search::build_template(request))
|
||||
.reset_flash()
|
||||
},
|
||||
|
||||
(POST) (/scuttlebutt/search) => {
|
||||
routes::scuttlebutt::search::handle_form(request)
|
||||
// add a back_url cookie to allow the path of the back button
|
||||
// to be set correctly on the /scuttlebutt/profile page
|
||||
.add_cookie("back_url=/scuttlebutt/search")
|
||||
},
|
||||
|
||||
(POST) (/scuttlebutt/unblock) => {
|
||||
routes::scuttlebutt::unblock::handle_form(request)
|
||||
},
|
||||
|
||||
(POST) (/scuttlebutt/unfollow) => {
|
||||
routes::scuttlebutt::unfollow::handle_form(request)
|
||||
},
|
||||
|
||||
(GET) (/settings) => {
|
||||
Response::html(routes::settings::menu::build_template())
|
||||
},
|
||||
|
||||
(GET) (/settings/admin) => {
|
||||
Response::html(routes::settings::admin::menu::build_template())
|
||||
},
|
||||
|
||||
(POST) (/settings/admin/add) => {
|
||||
routes::settings::admin::add::handle_form(request)
|
||||
},
|
||||
|
||||
(GET) (/settings/admin/configure) => {
|
||||
Response::html(routes::settings::admin::configure::build_template(request))
|
||||
.reset_flash()
|
||||
},
|
||||
|
||||
(POST) (/settings/admin/delete) => {
|
||||
routes::settings::admin::delete::handle_form(request)
|
||||
},
|
||||
|
||||
(GET) (/settings/power) => {
|
||||
Response::html(routes::settings::power::menu::build_template(request))
|
||||
},
|
||||
|
||||
(GET) (/settings/power/reboot) => {
|
||||
routes::settings::power::reboot::handle_reboot()
|
||||
},
|
||||
|
||||
(GET) (/settings/power/shutdown) => {
|
||||
routes::settings::power::shutdown::handle_shutdown()
|
||||
},
|
||||
|
||||
(GET) (/settings/scuttlebutt) => {
|
||||
Response::html(routes::settings::scuttlebutt::menu::build_template(request))
|
||||
.reset_flash()
|
||||
},
|
||||
|
||||
(GET) (/settings/scuttlebutt/restart) => {
|
||||
routes::settings::scuttlebutt::restart::restart_sbot()
|
||||
},
|
||||
|
||||
(GET) (/settings/scuttlebutt/start) => {
|
||||
routes::settings::scuttlebutt::start::start_sbot()
|
||||
},
|
||||
|
||||
(GET) (/settings/scuttlebutt/stop) => {
|
||||
routes::settings::scuttlebutt::stop::stop_sbot()
|
||||
},
|
||||
|
||||
(GET) (/settings/scuttlebutt/configure) => {
|
||||
Response::html(routes::settings::scuttlebutt::configure::build_template(request))
|
||||
.reset_flash()
|
||||
},
|
||||
|
||||
(POST) (/settings/scuttlebutt/configure) => {
|
||||
routes::settings::scuttlebutt::configure::handle_form(request, false)
|
||||
},
|
||||
|
||||
(POST) (/settings/scuttlebutt/configure/restart) => {
|
||||
routes::settings::scuttlebutt::configure::handle_form(request, true)
|
||||
},
|
||||
|
||||
(GET) (/settings/scuttlebutt/configure/default) => {
|
||||
routes::settings::scuttlebutt::default::write_config()
|
||||
},
|
||||
|
||||
(GET) (/settings/network) => {
|
||||
Response::html(routes::settings::network::menu::build_template(request)).reset_flash()
|
||||
},
|
||||
|
||||
(GET) (/settings/network/dns) => {
|
||||
Response::html(routes::settings::network::configure_dns::build_template(request)).reset_flash()
|
||||
},
|
||||
|
||||
(POST) (/settings/network/dns) => {
|
||||
routes::settings::network::configure_dns::handle_form(request)
|
||||
},
|
||||
|
||||
(GET) (/settings/network/wifi) => {
|
||||
Response::html(routes::settings::network::list_aps::build_template())
|
||||
},
|
||||
|
||||
(GET) (/settings/network/wifi/add) => {
|
||||
Response::html(routes::settings::network::add_ap::build_template(request, None)).reset_flash()
|
||||
},
|
||||
|
||||
(POST) (/settings/network/wifi/add) => {
|
||||
routes::settings::network::add_ap::handle_form(request)
|
||||
},
|
||||
|
||||
(GET) (/settings/network/wifi/add/{ssid: String}) => {
|
||||
Response::html(routes::settings::network::add_ap::build_template(request, Some(ssid))).reset_flash()
|
||||
},
|
||||
|
||||
(GET) (/settings/network/wifi/modify) => {
|
||||
Response::html(routes::settings::network::modify_ap::build_template(request, None)).reset_flash()
|
||||
},
|
||||
|
||||
(POST) (/settings/network/wifi/modify) => {
|
||||
routes::settings::network::modify_ap::handle_form(request)
|
||||
},
|
||||
|
||||
(GET) (/settings/network/wifi/modify/{ssid: String}) => {
|
||||
Response::html(routes::settings::network::modify_ap::build_template(request, Some(ssid))).reset_flash()
|
||||
},
|
||||
|
||||
(GET) (/settings/network/wifi/{ssid: String}) => {
|
||||
Response::html(routes::settings::network::ap_details::build_template(request, ssid))
|
||||
},
|
||||
|
||||
(GET) (/settings/theme/{theme: String}) => {
|
||||
routes::settings::theme::set_theme(theme)
|
||||
},
|
||||
|
||||
(GET) (/status) => {
|
||||
Response::html(routes::status::device::build_template())
|
||||
},
|
||||
|
||||
(GET) (/status/scuttlebutt) => {
|
||||
Response::html(routes::status::scuttlebutt::build_template()).add_cookie("back_url=/status/scuttlebutt")
|
||||
},
|
||||
|
||||
(GET) (/status/network) => {
|
||||
Response::html(routes::status::network::build_template())
|
||||
},
|
||||
|
||||
// render the not_found template and set a 404 status code if none of
|
||||
// the other blocks matches the request
|
||||
_ => Response::html(templates::not_found::build_template()).with_status_code(404)
|
||||
)
|
||||
}
|
|
@ -0,0 +1,103 @@
|
|||
use log::{error, info};
|
||||
use rouille::{router, Request, Response};
|
||||
|
||||
use crate::{
|
||||
private_router, routes,
|
||||
utils::{flash::FlashResponse, sbot},
|
||||
SessionData,
|
||||
};
|
||||
|
||||
/// Request handler.
|
||||
///
|
||||
/// Mount the fileservers for static assets and define the
|
||||
/// publically-accessible routes (including per-route handlers). Includes
|
||||
/// logging of all incoming requests.
|
||||
///
|
||||
/// If the request is for a private route (ie. a route requiring successful
|
||||
/// authentication to view), check the authentication status of the user
|
||||
/// by querying the `session_data`. If the user is authenticated, pass their
|
||||
/// request to the private router. Otherwise, redirect them to the login page.
|
||||
pub fn handle_route(request: &Request, session_data: &mut Option<SessionData>) -> Response {
|
||||
// static file server
|
||||
// matches on assets in the `static` directory
|
||||
let static_response = rouille::match_assets(request, "static");
|
||||
if static_response.is_success() {
|
||||
return static_response;
|
||||
}
|
||||
|
||||
// set the `.ssb-go` path in order to mount the blob fileserver
|
||||
let ssb_path = sbot::get_go_ssb_path().expect("define ssb-go dir path");
|
||||
let blobstore = format!("{}/blobs/sha256", ssb_path);
|
||||
|
||||
// blobstore file server
|
||||
// removes the /blob url prefix and serves blobs from blobstore
|
||||
// matches on assets in the `static` directory
|
||||
if let Some(request) = request.remove_prefix("/blob") {
|
||||
return rouille::match_assets(&request, &blobstore);
|
||||
}
|
||||
|
||||
// get the current time (for logging purposes)
|
||||
let now = chrono::Utc::now().format("%Y-%m-%d %H:%M:%S%.6f");
|
||||
|
||||
// define the success logger for incoming requests
|
||||
let log_ok = |req: &Request, _resp: &Response, _elap: std::time::Duration| {
|
||||
info!("{} {} {}", now, req.method(), req.raw_url());
|
||||
};
|
||||
|
||||
// define the error logger for incoming requests
|
||||
let log_err = |req: &Request, _elap: std::time::Duration| {
|
||||
error!(
|
||||
"{} Handler panicked: {} {}",
|
||||
now,
|
||||
req.method(),
|
||||
req.raw_url()
|
||||
);
|
||||
};
|
||||
|
||||
// instantiate request logging
|
||||
rouille::log_custom(request, log_ok, log_err, || {
|
||||
// handle the routes which are always accessible (ie. whether logged-in
|
||||
// or not)
|
||||
router!(request,
|
||||
(GET) (/auth/forgot) => {
|
||||
Response::html(routes::authentication::forgot::build_template(request))
|
||||
.reset_flash()
|
||||
},
|
||||
|
||||
(GET) (/auth/login) => {
|
||||
Response::html(routes::authentication::login::build_template(request))
|
||||
.reset_flash()
|
||||
},
|
||||
|
||||
(POST) (/auth/login) => {
|
||||
routes::authentication::login::handle_form(request, session_data)
|
||||
},
|
||||
|
||||
(GET) (/auth/reset) => {
|
||||
Response::html(routes::authentication::reset::build_template(request))
|
||||
.reset_flash()
|
||||
},
|
||||
|
||||
(POST) (/auth/reset) => {
|
||||
routes::authentication::reset::handle_form(request)
|
||||
},
|
||||
|
||||
(POST) (/auth/temporary) => {
|
||||
routes::authentication::temporary::handle_form()
|
||||
},
|
||||
|
||||
_ => {
|
||||
// now that we handled all the routes that are accessible in all
|
||||
// circumstances, we check that the user is logged in before proceeding
|
||||
if let Some(_session) = session_data.as_ref() {
|
||||
// logged in:
|
||||
// mount the routes which require authentication to view
|
||||
private_router::mount_peachpub_routes(request, session_data)
|
||||
} else {
|
||||
// not logged in:
|
||||
Response::redirect_303("/auth/login")
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
|
@ -1,95 +0,0 @@
|
|||
use rocket::{catchers, fs::FileServer, routes, Build, Rocket};
|
||||
use rocket_dyn_templates::Template;
|
||||
|
||||
use crate::routes::{
|
||||
authentication::*,
|
||||
catchers::*,
|
||||
index::*,
|
||||
scuttlebutt::*,
|
||||
settings::{admin::*, dns::*, menu::*, network::*, scuttlebutt::*},
|
||||
status::{device::*, network::*, scuttlebutt::*},
|
||||
};
|
||||
|
||||
/// Create a Rocket instance and mount PeachPub routes, fileserver and
|
||||
/// catchers. This gives us everything we need to run PeachPub and excludes
|
||||
/// settings and status routes related to networking and the device (memory,
|
||||
/// hard disk, CPU etc.).
|
||||
pub fn mount_peachpub_routes(rocket: Rocket<Build>) -> Rocket<Build> {
|
||||
rocket
|
||||
.mount(
|
||||
"/",
|
||||
routes![
|
||||
help,
|
||||
home,
|
||||
login,
|
||||
login_post,
|
||||
logout,
|
||||
reboot_cmd,
|
||||
shutdown_cmd,
|
||||
power_menu,
|
||||
settings_menu,
|
||||
],
|
||||
)
|
||||
.mount(
|
||||
"/settings/admin",
|
||||
routes![
|
||||
admin_menu,
|
||||
configure_admin,
|
||||
add_admin,
|
||||
add_admin_post,
|
||||
delete_admin_post,
|
||||
change_password,
|
||||
change_password_post,
|
||||
reset_password,
|
||||
reset_password_post,
|
||||
forgot_password_page,
|
||||
send_password_reset_post,
|
||||
],
|
||||
)
|
||||
.mount(
|
||||
"/settings/scuttlebutt",
|
||||
routes![ssb_settings_menu, configure_sbot],
|
||||
)
|
||||
.mount(
|
||||
"/scuttlebutt",
|
||||
routes![
|
||||
peers, friends, follows, followers, blocks, profile, private, follow, unfollow,
|
||||
block, publish,
|
||||
],
|
||||
)
|
||||
.mount("/status", routes![scuttlebutt_status])
|
||||
.mount("/", FileServer::from("static"))
|
||||
.register("/", catchers![not_found, internal_error, forbidden])
|
||||
.attach(Template::fairing())
|
||||
}
|
||||
|
||||
/// Create a Rocket instance with PeachPub routes, fileserver and catchers by
|
||||
/// calling `mount_peachpub_routes()` and then mount all additional routes
|
||||
/// required to run a complete PeachCloud build.
|
||||
pub fn mount_peachcloud_routes(rocket: Rocket<Build>) -> Rocket<Build> {
|
||||
mount_peachpub_routes(rocket)
|
||||
.mount(
|
||||
"/settings/network",
|
||||
routes![
|
||||
add_credentials,
|
||||
connect_wifi,
|
||||
configure_dns,
|
||||
configure_dns_post,
|
||||
disconnect_wifi,
|
||||
deploy_ap,
|
||||
deploy_client,
|
||||
forget_wifi,
|
||||
network_home,
|
||||
add_ssid,
|
||||
add_wifi,
|
||||
network_detail,
|
||||
wifi_list,
|
||||
wifi_password,
|
||||
wifi_set_password,
|
||||
wifi_usage,
|
||||
wifi_usage_alerts,
|
||||
wifi_usage_reset,
|
||||
],
|
||||
)
|
||||
.mount("/status", routes![device_status, network_status])
|
||||
}
|
|
@ -1,320 +0,0 @@
|
|||
use log::info;
|
||||
use rocket::{
|
||||
form::{Form, FromForm},
|
||||
get,
|
||||
http::{Cookie, CookieJar, Status},
|
||||
post,
|
||||
request::{self, FlashMessage, FromRequest, Request},
|
||||
response::{Flash, Redirect},
|
||||
serde::Deserialize,
|
||||
};
|
||||
use rocket_dyn_templates::{tera::Context, Template};
|
||||
|
||||
use peach_lib::{error::PeachError, password_utils};
|
||||
|
||||
use crate::error::PeachWebError;
|
||||
use crate::utils::TemplateOrRedirect;
|
||||
//use crate::DisableAuth;
|
||||
use crate::RocketConfig;
|
||||
|
||||
// HELPERS AND STRUCTS FOR AUTHENTICATION WITH COOKIES
|
||||
|
||||
pub const AUTH_COOKIE_KEY: &str = "peachweb_auth";
|
||||
pub const ADMIN_USERNAME: &str = "admin";
|
||||
|
||||
/// Note: Currently we use an empty struct for the Authenticated request guard
|
||||
/// because there is only one user to be authenticated, and no data needs to be stored here.
|
||||
/// In a multi-user authentication scheme, we would store the user_id in this struct,
|
||||
/// and retrieve the correct user via the user_id stored in the cookie.
|
||||
pub struct Authenticated;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum LoginError {
|
||||
UserNotLoggedIn,
|
||||
}
|
||||
|
||||
/// Request guard which returns an empty Authenticated struct from the request
|
||||
/// if and only if the user has a cookie which proves they are authenticated with peach-web.
|
||||
///
|
||||
/// Note that cookies.get_private uses encryption, which means that this private cookie
|
||||
/// cannot be inspected, tampered with, or manufactured by clients.
|
||||
#[rocket::async_trait]
|
||||
impl<'r> FromRequest<'r> for Authenticated {
|
||||
type Error = LoginError;
|
||||
|
||||
async fn from_request(req: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
|
||||
// retrieve auth state from managed state (returns `Option<bool>`).
|
||||
// this value is read from the Rocket.toml config file on start-up
|
||||
let authentication_is_disabled: bool = *req
|
||||
.rocket()
|
||||
.state::<RocketConfig>()
|
||||
.map(|config| (&config.disable_auth))
|
||||
.unwrap_or(&false);
|
||||
|
||||
if authentication_is_disabled {
|
||||
let auth = Authenticated {};
|
||||
request::Outcome::Success(auth)
|
||||
} else {
|
||||
let authenticated = req
|
||||
.cookies()
|
||||
.get_private(AUTH_COOKIE_KEY)
|
||||
.and_then(|cookie| cookie.value().parse().ok())
|
||||
.map(|_value: String| Authenticated {});
|
||||
match authenticated {
|
||||
Some(auth) => request::Outcome::Success(auth),
|
||||
None => request::Outcome::Failure((Status::Forbidden, LoginError::UserNotLoggedIn)),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// HELPERS AND ROUTES FOR /login
|
||||
|
||||
#[get("/login")]
|
||||
pub fn login(flash: Option<FlashMessage>) -> Template {
|
||||
let mut context = Context::new();
|
||||
context.insert("back", &Some("/".to_string()));
|
||||
context.insert("title", &Some("Login".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::render("login", &context.into_json())
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, 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.
|
||||
///
|
||||
/// Note: currently there is only one user, and the username should always
|
||||
/// be "admin".
|
||||
pub fn verify_login_form(login_form: LoginForm) -> Result<(), PeachError> {
|
||||
password_utils::verify_password(&login_form.password)
|
||||
}
|
||||
|
||||
#[post("/login", data = "<login_form>")]
|
||||
pub fn login_post(login_form: Form<LoginForm>, cookies: &CookieJar<'_>) -> TemplateOrRedirect {
|
||||
match verify_login_form(login_form.into_inner()) {
|
||||
Ok(_) => {
|
||||
// if successful login, add a cookie indicating the user is authenticated
|
||||
// and redirect to home page
|
||||
// NOTE: since we currently have just one user, the value of the cookie
|
||||
// is just admin (this is arbitrary).
|
||||
// If we had multiple users, we could put the user_id here.
|
||||
cookies.add_private(Cookie::new(AUTH_COOKIE_KEY, ADMIN_USERNAME));
|
||||
|
||||
TemplateOrRedirect::Redirect(Redirect::to("/"))
|
||||
}
|
||||
Err(_) => {
|
||||
// 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()));
|
||||
|
||||
TemplateOrRedirect::Template(Template::render("login", &context.into_json()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// HELPERS AND ROUTES FOR /logout
|
||||
|
||||
#[get("/logout")]
|
||||
pub fn logout(cookies: &CookieJar<'_>) -> Flash<Redirect> {
|
||||
// logout authenticated user
|
||||
info!("Attempting deauthentication of user.");
|
||||
cookies.remove_private(Cookie::named(AUTH_COOKIE_KEY));
|
||||
Flash::success(Redirect::to("/login"), "Logged out")
|
||||
}
|
||||
|
||||
// HELPERS AND ROUTES FOR /reset_password
|
||||
|
||||
#[derive(Debug, Deserialize, FromForm)]
|
||||
pub struct ResetPasswordForm {
|
||||
pub temporary_password: String,
|
||||
pub new_password1: String,
|
||||
pub new_password2: String,
|
||||
}
|
||||
|
||||
/// Verify, validate and save the submitted password. This function is publicly exposed for users who have forgotten their password.
|
||||
pub fn save_reset_password_form(password_form: ResetPasswordForm) -> Result<(), PeachWebError> {
|
||||
info!(
|
||||
"reset password!: {} {} {}",
|
||||
password_form.temporary_password, password_form.new_password1, password_form.new_password2
|
||||
);
|
||||
password_utils::verify_temporary_password(&password_form.temporary_password)?;
|
||||
// if the previous line did not throw an error, then the secret_link is correct
|
||||
password_utils::validate_new_passwords(
|
||||
&password_form.new_password1,
|
||||
&password_form.new_password2,
|
||||
)?;
|
||||
// if the previous line did not throw an error, then the new password is valid
|
||||
password_utils::set_new_password(&password_form.new_password1)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Password reset request handler. This route is used by a user who is not logged in
|
||||
/// and is specifically for users who have forgotten their password.
|
||||
#[get("/reset_password")]
|
||||
pub fn reset_password(flash: Option<FlashMessage>) -> Template {
|
||||
let mut context = Context::new();
|
||||
context.insert("back", &Some("/".to_string()));
|
||||
context.insert("title", &Some("Reset Password".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::render("settings/admin/reset_password", &context.into_json())
|
||||
}
|
||||
|
||||
/// Password reset form request handler. This route is used by a user who is not logged in
|
||||
/// and is specifically for users who have forgotten their password.
|
||||
#[post("/reset_password", data = "<reset_password_form>")]
|
||||
pub fn reset_password_post(reset_password_form: Form<ResetPasswordForm>) -> Template {
|
||||
let mut context = Context::new();
|
||||
context.insert("back", &Some("/".to_string()));
|
||||
context.insert("title", &Some("Reset Password".to_string()));
|
||||
|
||||
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(),
|
||||
),
|
||||
Err(err) => (
|
||||
"error".to_string(),
|
||||
format!("Failed to reset password: {}", err),
|
||||
),
|
||||
};
|
||||
|
||||
context.insert("flash_name", &Some(flash_name));
|
||||
context.insert("flash_msg", &Some(flash_msg));
|
||||
|
||||
Template::render("settings/admin/reset_password", &context.into_json())
|
||||
}
|
||||
|
||||
// HELPERS AND ROUTES FOR /send_password_reset
|
||||
|
||||
/// Page for users who have forgotten their password.
|
||||
/// This route is used by a user who is not logged in
|
||||
/// to initiate the sending of a new password reset.
|
||||
#[get("/forgot_password")]
|
||||
pub fn forgot_password_page(flash: Option<FlashMessage>) -> Template {
|
||||
let mut context = Context::new();
|
||||
context.insert("back", &Some("/".to_string()));
|
||||
context.insert("title", &Some("Send Password Reset".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("settings/admin/forgot_password", &context.into_json())
|
||||
}
|
||||
|
||||
/// Send password reset request handler. This route is used by a user who is not logged in
|
||||
/// and is specifically for users who have forgotten their password. A successful request results
|
||||
/// in a Scuttlebutt private message being sent to the account of the device admin.
|
||||
#[post("/send_password_reset")]
|
||||
pub fn send_password_reset_post() -> Template {
|
||||
info!("++ send password reset post");
|
||||
let mut context = Context::new();
|
||||
context.insert("back", &Some("/".to_string()));
|
||||
context.insert("title", &Some("Send Password Reset".to_string()));
|
||||
|
||||
let (flash_name, flash_msg) = match password_utils::send_password_reset() {
|
||||
Ok(_) => (
|
||||
"success".to_string(),
|
||||
"A password reset link has been sent to the admin of this device".to_string(),
|
||||
),
|
||||
Err(err) => (
|
||||
"error".to_string(),
|
||||
format!("Failed to send password reset link: {}", err),
|
||||
),
|
||||
};
|
||||
|
||||
context.insert("flash_name", &Some(flash_name));
|
||||
context.insert("flash_msg", &Some(flash_msg));
|
||||
|
||||
Template::render("settings/admin/forgot_password", &context.into_json())
|
||||
}
|
||||
|
||||
// HELPERS AND ROUTES FOR /settings/change_password
|
||||
|
||||
#[derive(Debug, Deserialize, FromForm)]
|
||||
pub struct PasswordForm {
|
||||
pub old_password: String,
|
||||
pub new_password1: String,
|
||||
pub new_password2: String,
|
||||
}
|
||||
|
||||
/// Password save form request handler. This function is for use by a user who is already logged in to change their password.
|
||||
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_utils::verify_password(&password_form.old_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,
|
||||
&password_form.new_password2,
|
||||
)?;
|
||||
// if the previous line did not throw an error, then the new password is valid
|
||||
password_utils::set_new_password(&password_form.new_password1)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Change password request handler. This is used by a user who is already logged in.
|
||||
#[get("/change_password")]
|
||||
pub fn change_password(flash: Option<FlashMessage>, _auth: Authenticated) -> Template {
|
||||
let mut context = Context::new();
|
||||
context.insert("back", &Some("/settings/admin".to_string()));
|
||||
context.insert("title", &Some("Change Password".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("settings/admin/change_password", &context.into_json())
|
||||
}
|
||||
|
||||
/// Change password form request handler. This route is used by a user who is already logged in.
|
||||
#[post("/change_password", data = "<password_form>")]
|
||||
pub fn change_password_post(password_form: Form<PasswordForm>, _auth: Authenticated) -> Template {
|
||||
let mut context = Context::new();
|
||||
context.insert("back", &Some("/settings/admin".to_string()));
|
||||
context.insert("title", &Some("Change Password".to_string()));
|
||||
|
||||
let (flash_name, flash_msg) = match save_password_form(password_form.into_inner()) {
|
||||
Ok(_) => (
|
||||
"success".to_string(),
|
||||
"New password is now saved".to_string(),
|
||||
),
|
||||
Err(err) => (
|
||||
"error".to_string(),
|
||||
format!("Failed to save new password: {}", err),
|
||||
),
|
||||
};
|
||||
|
||||
context.insert("flash_name", &Some(flash_name));
|
||||
context.insert("flash_msg", &Some(flash_msg));
|
||||
|
||||
Template::render("settings/admin/change_password", &context.into_json())
|
||||
}
|
|
@ -0,0 +1,116 @@
|
|||
use log::info;
|
||||
use maud::{html, PreEscaped};
|
||||
use peach_lib::password_utils;
|
||||
use rouille::{post_input, try_or_400, Request, Response};
|
||||
|
||||
use crate::{
|
||||
error::PeachWebError,
|
||||
templates,
|
||||
utils::{
|
||||
flash::{FlashRequest, FlashResponse},
|
||||
theme,
|
||||
},
|
||||
};
|
||||
|
||||
// HELPER AND ROUTES FOR /auth/change (GET and POST)
|
||||
|
||||
/// Password change form template builder.
|
||||
pub fn build_template(request: &Request) -> PreEscaped<String> {
|
||||
// check for flash cookies; will be (None, None) if no flash cookies are found
|
||||
let (flash_name, flash_msg) = request.retrieve_flash();
|
||||
|
||||
let form_template = html! {
|
||||
(PreEscaped("<!-- CHANGE PASSWORD FORM -->"))
|
||||
div class="card center" {
|
||||
form id="changePassword" class="center" action="/auth/change" method="post" {
|
||||
div style="display: flex; flex-direction: column; margin-bottom: 1rem;" {
|
||||
(PreEscaped("<!-- input for current password -->"))
|
||||
label for="currentPassword" class="center label-small font-gray" style="width: 80%;" { "CURRENT PASSWORD" }
|
||||
input id="currentPassword" class="center input" name="current_password" type="password" title="Current password" autofocus;
|
||||
(PreEscaped("<!-- input for new password -->"))
|
||||
label for="newPassword" class="center label-small font-gray" style="width: 80%;" { "NEW PASSWORD" }
|
||||
input id="newPassword" class="center input" name="new_password1" type="password" title="New password";
|
||||
(PreEscaped("<!-- input for duplicate new password -->"))
|
||||
label for="newPasswordDuplicate" class="center label-small font-gray" style="width: 80%;" { "RE-ENTER NEW PASSWORD" }
|
||||
input id="newPasswordDuplicate" class="center input" name="new_password2" type="password" title="New password duplicate";
|
||||
(PreEscaped("<!-- 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" }
|
||||
}
|
||||
}
|
||||
// render flash message if cookies were found in the request
|
||||
@if let (Some(name), Some(msg)) = (flash_name, flash_msg) {
|
||||
(PreEscaped("<!-- FLASH MESSAGE -->"))
|
||||
(templates::flash::build_template(name, msg))
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// wrap the nav bars around the settings menu template content
|
||||
// parameters are template, title and back url
|
||||
let body =
|
||||
templates::nav::build_template(form_template, "Change Password", Some("/settings/admin"));
|
||||
|
||||
// query the current theme so we can pass it into the base template builder
|
||||
let theme = theme::get_theme();
|
||||
|
||||
// render the base template with the provided body
|
||||
templates::base::build_template(body, theme)
|
||||
}
|
||||
|
||||
/// Verify, validate and set a new password, overwriting the current password.
|
||||
pub fn save_password(
|
||||
current_password: &str,
|
||||
new_password1: &str,
|
||||
new_password2: &str,
|
||||
) -> Result<(), PeachWebError> {
|
||||
info!(
|
||||
"Attempting password change: {} {} {}",
|
||||
current_password, new_password1, new_password2
|
||||
);
|
||||
|
||||
// check that the supplied value matches the actual current password
|
||||
password_utils::verify_password(current_password)?;
|
||||
|
||||
// ensure that both new_password values match
|
||||
password_utils::validate_new_passwords(new_password1, new_password2)?;
|
||||
|
||||
// hash the password and save the hash to file
|
||||
password_utils::set_new_password(new_password1)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Parse current and new passwords from the submitted form, save the new
|
||||
/// password hash to file (`/var/lib/peachcloud/config.yml`) and redirect
|
||||
/// to the change password form URL.
|
||||
pub fn handle_form(request: &Request) -> Response {
|
||||
// query the request body for form data
|
||||
// return a 400 error if the admin_id field is missing
|
||||
let data = try_or_400!(post_input!(request, {
|
||||
current_password: String,
|
||||
new_password1: String,
|
||||
new_password2: String,
|
||||
}));
|
||||
|
||||
// save submitted admin id to file
|
||||
// match on the result and set flash name and msg accordingly
|
||||
let (flash_name, flash_msg) = match save_password(
|
||||
&data.current_password,
|
||||
&data.new_password1,
|
||||
&data.new_password2,
|
||||
) {
|
||||
Ok(_) => (
|
||||
// <cookie-name>=<cookie-value>
|
||||
"flash_name=success".to_string(),
|
||||
"flash_msg=New password has been saved".to_string(),
|
||||
),
|
||||
Err(err) => (
|
||||
"flash_name=error".to_string(),
|
||||
format!("flash_msg=Failed to save new password: {}", err),
|
||||
),
|
||||
};
|
||||
|
||||
// set the flash cookie headers and redirect to the change password page
|
||||
Response::redirect_303("/auth/change").add_flash(flash_name, flash_msg)
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
use maud::{html, PreEscaped};
|
||||
use rouille::Request;
|
||||
|
||||
use crate::{
|
||||
templates,
|
||||
utils::{flash::FlashRequest, theme},
|
||||
};
|
||||
|
||||
// ROUTE: /auth/forgot
|
||||
|
||||
/// Forgot password template builder.
|
||||
pub fn build_template(request: &Request) -> PreEscaped<String> {
|
||||
// check for flash cookies; will be (None, None) if no flash cookies are found
|
||||
let (flash_name, flash_msg) = request.retrieve_flash();
|
||||
|
||||
let password_reset_template = html! {
|
||||
(PreEscaped("<!-- PASSWORD RESET REQUEST CARD -->"))
|
||||
div class="card center" {
|
||||
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 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 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."
|
||||
}
|
||||
}
|
||||
form id="sendPasswordReset" action="/auth/temporary" method="post" {
|
||||
div id="buttonDiv" {
|
||||
input class="button button-primary center" style="margin-top: 1rem;" type="submit" value="Send Temporary Password" title="Send temporary password to Scuttlebutt admin(s)";
|
||||
a href="/auth/reset" class="button button-primary center" title="Set a new password using the temporary password" {
|
||||
"Set New Password"
|
||||
}
|
||||
}
|
||||
}
|
||||
// render flash message if cookies were found in the request
|
||||
@if let (Some(name), Some(msg)) = (flash_name, flash_msg) {
|
||||
(PreEscaped("<!-- FLASH MESSAGE -->"))
|
||||
(templates::flash::build_template(name, msg))
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// wrap the nav bars around the settings menu template content
|
||||
// parameters are template, title and back url
|
||||
let body =
|
||||
templates::nav::build_template(password_reset_template, "Send Password Reset", Some("/"));
|
||||
|
||||
// query the current theme so we can pass it into the base template builder
|
||||
let theme = theme::get_theme();
|
||||
|
||||
// render the base template with the provided body
|
||||
templates::base::build_template(body, theme)
|
||||
}
|
|
@ -0,0 +1,87 @@
|
|||
use log::debug;
|
||||
use maud::{html, PreEscaped};
|
||||
use peach_lib::password_utils;
|
||||
use rouille::{post_input, try_or_400, Request, Response};
|
||||
|
||||
use crate::{
|
||||
templates,
|
||||
utils::{
|
||||
flash::{FlashRequest, FlashResponse},
|
||||
theme,
|
||||
},
|
||||
SessionData,
|
||||
};
|
||||
|
||||
// HELPER AND ROUTES FOR /auth/login (GET and POST)
|
||||
|
||||
/// Login form template builder.
|
||||
pub fn build_template(request: &Request) -> PreEscaped<String> {
|
||||
// check for flash cookies; will be (None, None) if no flash cookies are found
|
||||
let (flash_name, flash_msg) = request.retrieve_flash();
|
||||
|
||||
let form_template = html! {
|
||||
(PreEscaped("<!-- LOGIN FORM -->"))
|
||||
div class="card center" {
|
||||
form id="login_form" class="center" action="/auth/login" method="post" {
|
||||
div style="display: flex; flex-direction: column; margin-bottom: 1rem;" {
|
||||
(PreEscaped("<!-- input for password -->"))
|
||||
label for="password" class="center label-small font-gray" style="width: 80%;" { "PASSWORD" }
|
||||
input id="password" name="password" class="center input" type="password" title="Password for given username" autofocus;
|
||||
(PreEscaped("<!-- 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="/auth/forgot" class="label-small link font-gray" { "Forgot Password?" }
|
||||
}
|
||||
}
|
||||
}
|
||||
// render flash message if cookies were found in the request
|
||||
@if let (Some(name), Some(msg)) = (flash_name, flash_msg) {
|
||||
(PreEscaped("<!-- FLASH MESSAGE -->"))
|
||||
(templates::flash::build_template(name, msg))
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// wrap the nav bars around the settings menu template content
|
||||
// parameters are template, title and back url
|
||||
let body = templates::nav::build_template(form_template, "Login", Some("/"));
|
||||
|
||||
// query the current theme so we can pass it into the base template builder
|
||||
let theme = theme::get_theme();
|
||||
|
||||
// render the base template with the provided body
|
||||
templates::base::build_template(body, theme)
|
||||
}
|
||||
|
||||
/// Parse and verify the submitted password. If verification succeeds, set the
|
||||
/// auth session cookie and redirect to the home page. If not, set a flash
|
||||
/// message and redirect to the login page.
|
||||
pub fn handle_form(request: &Request, session_data: &mut Option<SessionData>) -> Response {
|
||||
// query the request body for form data
|
||||
// return a 400 error if the admin_id field is missing
|
||||
let data = try_or_400!(post_input!(request, { password: String }));
|
||||
|
||||
match password_utils::verify_password(&data.password) {
|
||||
Ok(_) => {
|
||||
debug!("Successful login attempt");
|
||||
// if password verification is successful, write to `session_data`
|
||||
// to authenticate the user
|
||||
*session_data = Some(SessionData {
|
||||
_login: "success".to_string(),
|
||||
});
|
||||
|
||||
Response::redirect_303("/")
|
||||
}
|
||||
Err(err) => {
|
||||
debug!("Unsuccessful login attempt");
|
||||
let err_msg = format!("Invalid password: {}", err);
|
||||
let (flash_name, flash_msg) = (
|
||||
"flash_name=error".to_string(),
|
||||
format!("flash_msg={}", err_msg),
|
||||
);
|
||||
|
||||
// if unsuccessful login, render /login page again
|
||||
Response::redirect_303("/auth/login").add_flash(flash_name, flash_msg)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
use log::info;
|
||||
use rouille::Response;
|
||||
|
||||
use crate::{utils::flash::FlashResponse, SessionData};
|
||||
|
||||
// ROUTE: /auth/logout (GET)
|
||||
|
||||
/// Deauthenticate the logged-in user by erasing the session data.
|
||||
/// Redirect to the login page.
|
||||
pub fn deauthenticate(session_data: &mut Option<SessionData>) -> Response {
|
||||
info!("Attempting deauthentication of user.");
|
||||
|
||||
// erase the content of `session_data` to deauthenticate the user
|
||||
*session_data = None;
|
||||
|
||||
let (flash_name, flash_msg) = (
|
||||
"flash_name=success".to_string(),
|
||||
"flash_msg=Logged out".to_string(),
|
||||
);
|
||||
|
||||
// set the flash cookie headers and redirect to the login page
|
||||
Response::redirect_303("/auth/login".to_string()).add_flash(flash_name, flash_msg)
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
pub mod change;
|
||||
pub mod forgot;
|
||||
pub mod login;
|
||||
pub mod logout;
|
||||
pub mod reset;
|
||||
pub mod temporary;
|
|
@ -0,0 +1,114 @@
|
|||
use log::info;
|
||||
use maud::{html, PreEscaped};
|
||||
use peach_lib::password_utils;
|
||||
use rouille::{post_input, try_or_400, Request, Response};
|
||||
|
||||
use crate::{
|
||||
error::PeachWebError,
|
||||
templates,
|
||||
utils::{
|
||||
flash::{FlashRequest, FlashResponse},
|
||||
theme,
|
||||
},
|
||||
};
|
||||
|
||||
// HELPER AND ROUTES FOR /auth/reset (GET and POST)
|
||||
|
||||
/// Password reset form template builder.
|
||||
pub fn build_template(request: &Request) -> PreEscaped<String> {
|
||||
// check for flash cookies; will be (None, None) if no flash cookies are found
|
||||
let (flash_name, flash_msg) = request.retrieve_flash();
|
||||
|
||||
let form_template = html! {
|
||||
(PreEscaped("<!-- RESET PASSWORD PAGE -->"))
|
||||
div class="card center" {
|
||||
form id="resetPassword" class="center" action="/auth/reset" method="post" {
|
||||
div style="display: flex; flex-direction: column; margin-bottom: 1rem;" {
|
||||
(PreEscaped("<!-- input for temporary password -->"))
|
||||
label for="temporaryPassword" class="center label-small font-gray" style="width: 80%;" { "TEMPORARY PASSWORD" }
|
||||
input id="temporaryPassword" class="center input" name="temporary_password" type="password" title="Temporary password" autofocus;
|
||||
(PreEscaped("<!-- input for new password1 -->"))
|
||||
label for="newPassword" class="center label-small font-gray" style="width: 80%;" { "NEW PASSWORD" }
|
||||
input id="newPassword" class="center input" name="new_password1" type="password" title="New password";
|
||||
(PreEscaped("<!-- input for duplicate new password -->"))
|
||||
label for="newPasswordDuplicate" class="center label-small font-gray" style="width: 80%;" { "RE-ENTER NEW PASSWORD" }
|
||||
input id="newPasswordDuplicate" class="center input" name="new_password2" type="password" title="New password duplicate";
|
||||
(PreEscaped("<!-- 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" }
|
||||
}
|
||||
}
|
||||
// render flash message if cookies were found in the request
|
||||
@if let (Some(name), Some(msg)) = (flash_name, flash_msg) {
|
||||
(PreEscaped("<!-- FLASH MESSAGE -->"))
|
||||
(templates::flash::build_template(name, msg))
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// wrap the nav bars around the settings menu template content
|
||||
// parameters are template, title and back url
|
||||
let body =
|
||||
templates::nav::build_template(form_template, "Reset Password", Some("/settings/admin"));
|
||||
|
||||
// query the current theme so we can pass it into the base template builder
|
||||
let theme = theme::get_theme();
|
||||
|
||||
// render the base template with the provided body
|
||||
templates::base::build_template(body, theme)
|
||||
}
|
||||
|
||||
/// Verify, validate and set a new password, overwriting the current password.
|
||||
pub fn save_password(
|
||||
temporary_password: &str,
|
||||
new_password1: &str,
|
||||
new_password2: &str,
|
||||
) -> Result<(), PeachWebError> {
|
||||
info!(
|
||||
"Attempting password reset: {} {} {}",
|
||||
temporary_password, new_password1, new_password2
|
||||
);
|
||||
|
||||
// check that the supplied value matches the actual temporary password
|
||||
password_utils::verify_temporary_password(temporary_password)?;
|
||||
|
||||
// ensure that both new_password values match
|
||||
password_utils::validate_new_passwords(new_password1, new_password2)?;
|
||||
|
||||
// hash the password and save the hash to file
|
||||
password_utils::set_new_password(new_password1)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Parse temporary and new passwords from the submitted form, save the new
|
||||
/// password hash to file (`/var/lib/peachcloud/config.yml`) and redirect
|
||||
/// to the reset password form URL.
|
||||
pub fn handle_form(request: &Request) -> Response {
|
||||
// query the request body for form data
|
||||
// return a 400 error if the admin_id field is missing
|
||||
let data = try_or_400!(post_input!(request, {
|
||||
temporary_password: String,
|
||||
new_password1: String,
|
||||
new_password2: String,
|
||||
}));
|
||||
|
||||
// save submitted admin id to file
|
||||
let (flash_name, flash_msg) = match save_password(
|
||||
&data.temporary_password,
|
||||
&data.new_password1,
|
||||
&data.new_password2,
|
||||
) {
|
||||
Ok(_) => (
|
||||
"flash_name=success".to_string(),
|
||||
"flash_msg=New password has been saved. Return home to login".to_string(),
|
||||
),
|
||||
Err(err) => (
|
||||
"flash_name=error".to_string(),
|
||||
format!("flash_msg=Failed to reset password: {}", err),
|
||||
),
|
||||
};
|
||||
|
||||
// redirect to the configure admin page
|
||||
Response::redirect_303("/auth/reset").add_flash(flash_name, flash_msg)
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
use log::debug;
|
||||
use peach_lib::password_utils;
|
||||
use rouille::Response;
|
||||
|
||||
use crate::utils::flash::FlashResponse;
|
||||
|
||||
// ROUTE: /auth/temporary (POST)
|
||||
|
||||
/// Send a temporary password as a Scuttlebutt private message to the admin(s).
|
||||
///
|
||||
/// This route is used by a user who is not logged in and is specifically for
|
||||
/// users who have forgotten their password. A successful request results
|
||||
/// in a Scuttlebutt private message being sent to the account of the device
|
||||
/// admin.
|
||||
///
|
||||
/// Redirects to the Send Password Reset page a flash message describing the
|
||||
/// outcome of the action (may be successful or unsuccessful).
|
||||
pub fn handle_form() -> Response {
|
||||
// save submitted admin id to file
|
||||
let (flash_name, flash_msg) = match password_utils::send_password_reset() {
|
||||
Ok(_) => {
|
||||
debug!("Sent temporary password to device admin(s)");
|
||||
(
|
||||
"flash_name=success".to_string(),
|
||||
"flash_msg=A temporary password has been sent to the admin(s) of this device"
|
||||
.to_string(),
|
||||
)
|
||||
}
|
||||
Err(err) => {
|
||||
debug!(
|
||||
"Received an error while trying to send temporary password to device admin(s): {}",
|
||||
err
|
||||
);
|
||||
(
|
||||
"error".to_string(),
|
||||
format!("Failed to send temporary password: {}", err),
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
Response::redirect_303("/auth/forgot").add_flash(flash_name, flash_msg)
|
||||
}
|
|
@ -1,60 +0,0 @@
|
|||
use log::debug;
|
||||
use rocket::catch;
|
||||
use rocket::response::Redirect;
|
||||
use rocket_dyn_templates::Template;
|
||||
use serde::Serialize;
|
||||
|
||||
// HELPERS AND ROUTES FOR 404 ERROR
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct ErrorContext {
|
||||
pub back: Option<String>,
|
||||
pub flash_name: Option<String>,
|
||||
pub flash_msg: Option<String>,
|
||||
pub title: Option<String>,
|
||||
}
|
||||
|
||||
impl ErrorContext {
|
||||
pub fn build() -> ErrorContext {
|
||||
ErrorContext {
|
||||
back: None,
|
||||
flash_name: None,
|
||||
flash_msg: None,
|
||||
title: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[catch(404)]
|
||||
pub fn not_found() -> Template {
|
||||
debug!("404 Page Not Found");
|
||||
let mut context = ErrorContext::build();
|
||||
context.back = Some("/".to_string());
|
||||
context.title = Some("404: Page Not Found".to_string());
|
||||
context.flash_name = Some("error".to_string());
|
||||
context.flash_msg = Some("No resource found for given URL".to_string());
|
||||
|
||||
Template::render("catchers/not_found", context)
|
||||
}
|
||||
|
||||
// HELPERS AND ROUTES FOR 500 ERROR
|
||||
|
||||
#[catch(500)]
|
||||
pub fn internal_error() -> Template {
|
||||
debug!("500 Internal Server Error");
|
||||
let mut context = ErrorContext::build();
|
||||
context.back = Some("/".to_string());
|
||||
context.title = Some("500: Internal Server Error".to_string());
|
||||
context.flash_name = Some("error".to_string());
|
||||
context.flash_msg = Some("Internal server error".to_string());
|
||||
|
||||
Template::render("catchers/internal_error", context)
|
||||
}
|
||||
|
||||
// HELPERS AND ROUTES FOR 403 FORBIDDEN
|
||||
|
||||
#[catch(403)]
|
||||
pub fn forbidden() -> Redirect {
|
||||
debug!("403 Forbidden");
|
||||
Redirect::to("/login")
|
||||
}
|
|
@ -0,0 +1,106 @@
|
|||
use maud::{html, PreEscaped};
|
||||
|
||||
use crate::{templates, utils::theme};
|
||||
|
||||
/// Guide template builder.
|
||||
pub fn build_template() -> PreEscaped<String> {
|
||||
// render the guide template html
|
||||
let guide_template = html! {
|
||||
(PreEscaped("<!-- GUIDE -->"))
|
||||
div class="card card-wide center" {
|
||||
div class="capsule capsule-container border-info" {
|
||||
(PreEscaped("<!-- GETTING STARTED -->"))
|
||||
details {
|
||||
summary class="card-text link" { "Getting started" }
|
||||
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 "
|
||||
strong {
|
||||
a href="/settings/scuttlebutt/configure" class="link font-gray" {
|
||||
"Sbot Configuration"
|
||||
}
|
||||
}
|
||||
" page to configure your system. The default configuration will be fine for most usecases."
|
||||
}
|
||||
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"
|
||||
}
|
||||
}
|
||||
" 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 status go-sbot.service" }
|
||||
". The log output may give some clues about the source of the error."
|
||||
}
|
||||
}
|
||||
(PreEscaped("<!-- BUG REPORTS -->"))
|
||||
details {
|
||||
summary class="card-text link" { "Submit a bug report" }
|
||||
p class="card-text" style="margin-top: 1rem; margin-bottom: 1rem;" {
|
||||
"Bug reports can be submitted by "
|
||||
strong {
|
||||
a href="https://git.coopcloud.tech/PeachCloud/peach-workspace/issues/new?template=BUG_TEMPLATE.md" class="link font-gray" {
|
||||
"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."
|
||||
}
|
||||
}
|
||||
(PreEscaped("<!-- REQUEST SUPPORT -->"))
|
||||
details {
|
||||
summary class="card-text link" { "Share feedback & request support" }
|
||||
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 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"
|
||||
}
|
||||
}
|
||||
" 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"
|
||||
}
|
||||
}
|
||||
"."
|
||||
}
|
||||
}
|
||||
(PreEscaped("<!-- CONTRIBUTE -->"))
|
||||
details {
|
||||
summary class="card-text link" { "Contribute to PeachCloud" }
|
||||
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"
|
||||
}
|
||||
}
|
||||
" page."
|
||||
}
|
||||
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"
|
||||
}
|
||||
}
|
||||
" to find out more details or contact the team via Scuttlebutt, Matrix or email."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// wrap the nav bars around the home template content
|
||||
// title is "" and back button link is `None` because this is the homepage
|
||||
let body = templates::nav::build_template(guide_template, "Guide", Some("/"));
|
||||
|
||||
// query the current theme so we can pass it into the base template builder
|
||||
let theme = theme::get_theme();
|
||||
|
||||
// render the base template with the provided body
|
||||
templates::base::build_template(body, theme)
|
||||
}
|
|
@ -0,0 +1,119 @@
|
|||
use maud::{html, PreEscaped};
|
||||
use peach_lib::sbot::SbotStatus;
|
||||
|
||||
use crate::{templates, utils::theme, SERVER_CONFIG};
|
||||
|
||||
/// Read the state of the go-sbot process and define status-related
|
||||
/// elements accordingly.
|
||||
fn render_status_elements<'a>() -> (&'a str, &'a str, &'a str) {
|
||||
// retrieve go-sbot systemd process status
|
||||
let sbot_status = SbotStatus::read();
|
||||
|
||||
// conditionally render the center circle class, center circle text and
|
||||
// status circle class color based on the go-sbot process state
|
||||
if let Ok(status) = sbot_status {
|
||||
if status.state == Some("active".to_string()) {
|
||||
("circle-success", "^_^", "border-success")
|
||||
} else if status.state == Some("inactive".to_string()) {
|
||||
("circle-warning", "z_z", "border-warning")
|
||||
} else {
|
||||
("circle-error", "x_x", "border-danger")
|
||||
}
|
||||
} else {
|
||||
("circle-error", "x_x", "border-danger")
|
||||
}
|
||||
}
|
||||
|
||||
/// Render the URL for the status element (icon / link).
|
||||
///
|
||||
/// If the application is running in standalone mode then the element links
|
||||
/// directly to the Scuttlebutt status page. If not, it links to the device
|
||||
/// status page.
|
||||
fn render_status_url<'a>() -> &'a str {
|
||||
if SERVER_CONFIG.standalone_mode {
|
||||
"/status/scuttlebutt"
|
||||
} else {
|
||||
"/status"
|
||||
}
|
||||
}
|
||||
|
||||
/// Home template builder.
|
||||
pub fn build_template() -> PreEscaped<String> {
|
||||
let (circle_color, center_circle_text, circle_border) = render_status_elements();
|
||||
let status_url = render_status_url();
|
||||
|
||||
// render the home template html
|
||||
let home_template = html! {
|
||||
(PreEscaped("<!-- RADIAL MENU -->"))
|
||||
div class="grid" {
|
||||
(PreEscaped("<!-- top-left -->"))
|
||||
(PreEscaped("<!-- PEERS LINK AND ICON -->"))
|
||||
a class="top-left" href="/scuttlebutt/peers" title="Scuttlebutt Peers" {
|
||||
div class="circle circle-small border-circle-small border-ssb" {
|
||||
img class="icon-medium" src="/icons/users.svg";
|
||||
}
|
||||
}
|
||||
(PreEscaped("<!-- top-middle -->"))
|
||||
(PreEscaped("<!-- CURRENT USER LINK AND ICON -->"))
|
||||
a class="top-middle" href="/scuttlebutt/profile" title="Profile" {
|
||||
div class="circle circle-small border-circle-small border-ssb" {
|
||||
img class="icon-medium" src="/icons/user.svg";
|
||||
}
|
||||
}
|
||||
(PreEscaped("<!-- top-right -->"))
|
||||
(PreEscaped("<!-- MESSAGES LINK AND ICON -->"))
|
||||
a class="top-right" href="/scuttlebutt/private" title="Private Messages" {
|
||||
div class="circle circle-small border-circle-small border-ssb" {
|
||||
img class="icon-medium" src="/icons/envelope.svg";
|
||||
}
|
||||
}
|
||||
(PreEscaped("<!-- middle -->"))
|
||||
a class="middle" {
|
||||
div class={ "circle circle-large " (circle_color) } {
|
||||
p style="font-size: 4rem; color: var(--near-black);" {
|
||||
(center_circle_text)
|
||||
}
|
||||
}
|
||||
}
|
||||
(PreEscaped("<!-- bottom-left -->"))
|
||||
(PreEscaped("<!-- SYSTEM STATUS LINK AND ICON -->"))
|
||||
a class="bottom-left" href=(status_url) title="Status" {
|
||||
div class={ "circle circle-small border-circle-small " (circle_border) } {
|
||||
img class="icon-medium" src="/icons/heart-pulse.svg";
|
||||
}
|
||||
}
|
||||
/*
|
||||
TODO: render the path of the status circle button based on the mode
|
||||
{%- if standalone_mode == true -%}
|
||||
<a class="bottom-left" href="/status/scuttlebutt" title="Status">
|
||||
{% else -%}
|
||||
<a class="bottom-left" href="/status" title="Status">
|
||||
{%- endif -%}
|
||||
*/
|
||||
(PreEscaped("<!-- bottom-middle -->"))
|
||||
(PreEscaped("<!-- PEACHCLOUD GUIDEBOOK LINK AND ICON -->"))
|
||||
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";
|
||||
}
|
||||
}
|
||||
(PreEscaped("<!-- bottom-right -->"))
|
||||
(PreEscaped("<!-- SYSTEM SETTINGS LINK AND ICON -->"))
|
||||
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";
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// wrap the nav bars around the home template content
|
||||
// title is "" and back button link is `None` because this is the homepage
|
||||
let body = templates::nav::build_template(home_template, "", None);
|
||||
|
||||
// query the current theme so we can pass it into the base template builder
|
||||
let theme = theme::get_theme();
|
||||
|
||||
// render the base template with the provided body
|
||||
templates::base::build_template(body, theme)
|
||||
}
|
|
@ -1,37 +0,0 @@
|
|||
use rocket::{get, request::FlashMessage, State};
|
||||
use rocket_dyn_templates::{tera::Context, Template};
|
||||
|
||||
use crate::routes::authentication::Authenticated;
|
||||
use crate::RocketConfig;
|
||||
|
||||
// HELPERS AND ROUTES FOR / (HOME PAGE)
|
||||
|
||||
#[get("/")]
|
||||
pub fn home(_auth: Authenticated, config: &State<RocketConfig>) -> Template {
|
||||
let mut context = Context::new();
|
||||
context.insert("flash_name", &None::<()>);
|
||||
context.insert("flash_msg", &None::<()>);
|
||||
context.insert("title", &None::<()>);
|
||||
|
||||
// pass in mode from managed state so we can define appropriate urls in template
|
||||
context.insert("standalone_mode", &config.standalone_mode);
|
||||
|
||||
Template::render("home", &context.into_json())
|
||||
}
|
||||
|
||||
// HELPERS AND ROUTES FOR /help
|
||||
|
||||
#[get("/help")]
|
||||
pub fn help(flash: Option<FlashMessage>) -> Template {
|
||||
let mut context = Context::new();
|
||||
context.insert("back", &Some("/".to_string()));
|
||||
context.insert("title", &Some("Help".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::render("help", &context.into_json())
|
||||
}
|
|
@ -1,6 +1,8 @@
|
|||
pub mod authentication;
|
||||
pub mod catchers;
|
||||
pub mod index;
|
||||
//pub mod catchers;
|
||||
//pub mod index;
|
||||
pub mod guide;
|
||||
pub mod home;
|
||||
pub mod scuttlebutt;
|
||||
pub mod settings;
|
||||
pub mod status;
|
||||
|
|
|
@ -1,365 +0,0 @@
|
|||
//! Routes for Scuttlebutt related functionality.
|
||||
|
||||
use rocket::{
|
||||
form::{Form, FromForm},
|
||||
get, post,
|
||||
request::FlashMessage,
|
||||
response::{Flash, Redirect},
|
||||
serde::{Deserialize, Serialize},
|
||||
uri,
|
||||
};
|
||||
use rocket_dyn_templates::Template;
|
||||
|
||||
use crate::routes::authentication::Authenticated;
|
||||
|
||||
// HELPERS AND ROUTES FOR /private
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct PrivateContext {
|
||||
pub back: Option<String>,
|
||||
pub flash_name: Option<String>,
|
||||
pub flash_msg: Option<String>,
|
||||
pub title: Option<String>,
|
||||
}
|
||||
|
||||
impl PrivateContext {
|
||||
pub fn build() -> PrivateContext {
|
||||
PrivateContext {
|
||||
back: None,
|
||||
flash_name: None,
|
||||
flash_msg: None,
|
||||
title: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A private message composition and publication page.
|
||||
#[get("/private")]
|
||||
pub fn private(flash: Option<FlashMessage>, _auth: Authenticated) -> Template {
|
||||
let mut context = PrivateContext::build();
|
||||
context.back = Some("/".to_string());
|
||||
context.title = Some("Private Messages".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());
|
||||
};
|
||||
Template::render("scuttlebutt/messages", &context)
|
||||
}
|
||||
|
||||
// HELPERS AND ROUTES FOR /peers
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct PeerContext {
|
||||
pub back: Option<String>,
|
||||
pub flash_name: Option<String>,
|
||||
pub flash_msg: Option<String>,
|
||||
pub title: Option<String>,
|
||||
}
|
||||
|
||||
impl PeerContext {
|
||||
pub fn build() -> PeerContext {
|
||||
PeerContext {
|
||||
back: None,
|
||||
flash_name: None,
|
||||
flash_msg: None,
|
||||
title: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A peer menu which allows navigating to lists of friends, follows, followers and blocks.
|
||||
#[get("/peers")]
|
||||
pub fn peers(flash: Option<FlashMessage>, _auth: Authenticated) -> Template {
|
||||
let mut context = PeerContext::build();
|
||||
context.back = Some("/".to_string());
|
||||
context.title = Some("Scuttlebutt Peers".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());
|
||||
};
|
||||
Template::render("scuttlebutt/peers", &context)
|
||||
}
|
||||
|
||||
// HELPERS AND ROUTES FOR /post/publish
|
||||
|
||||
#[derive(Debug, Deserialize, FromForm)]
|
||||
pub struct Post {
|
||||
pub text: String,
|
||||
}
|
||||
|
||||
/// Publish a public Scuttlebutt post. Redirects to profile page of the PeachCloud local identity with a flash message describing the outcome of the action (may be successful or unsuccessful).
|
||||
#[post("/publish", data = "<post>")]
|
||||
pub fn publish(
|
||||
post: Form<Post>,
|
||||
flash: Option<FlashMessage>,
|
||||
_auth: Authenticated,
|
||||
) -> Flash<Redirect> {
|
||||
let post_text = &post.text;
|
||||
// perform the sbotcli publish action using post_text
|
||||
// if successful, redirect to home profile page and flash "success"
|
||||
// if error, redirect to home profile page and flash "error"
|
||||
// redirect to the profile template without public key ("home" / local profile)
|
||||
let pub_key: std::option::Option<&str> = None;
|
||||
let profile_url = uri!(profile(pub_key));
|
||||
// consider adding the message reference to the flash message (or render it in the template for
|
||||
// `profile`
|
||||
Flash::success(Redirect::to(profile_url), "Published public post")
|
||||
}
|
||||
|
||||
// HELPERS AND ROUTES FOR /follow
|
||||
|
||||
#[derive(Debug, Deserialize, FromForm)]
|
||||
pub struct PublicKey {
|
||||
pub key: String,
|
||||
}
|
||||
|
||||
/// Follow a Scuttlebutt profile specified by the given public key. Redirects to the appropriate profile page with a flash message describing the outcome of the action (may be successful or unsuccessful).
|
||||
#[post("/follow", data = "<pub_key>")]
|
||||
pub fn follow(
|
||||
pub_key: Form<PublicKey>,
|
||||
flash: Option<FlashMessage>,
|
||||
_auth: Authenticated,
|
||||
) -> Flash<Redirect> {
|
||||
let public_key = &pub_key.key;
|
||||
// perform the sbotcli follow action using &pub_key.0
|
||||
// if successful, redirect to profile page with provided public key and flash "success"
|
||||
// if error, redirect to profile page with provided public key and flash "error"
|
||||
// redirect to the profile template with provided public key
|
||||
let profile_url = uri!(profile(Some(public_key)));
|
||||
let success_msg = format!("Followed {}", public_key);
|
||||
Flash::success(Redirect::to(profile_url), success_msg)
|
||||
}
|
||||
|
||||
// HELPERS AND ROUTES FOR /unfollow
|
||||
|
||||
/// Unfollow a Scuttlebutt profile specified by the given public key. Redirects to the appropriate profile page with a flash message describing the outcome of the action (may be successful or unsuccessful).
|
||||
#[post("/unfollow", data = "<pub_key>")]
|
||||
pub fn unfollow(
|
||||
pub_key: Form<PublicKey>,
|
||||
flash: Option<FlashMessage>,
|
||||
_auth: Authenticated,
|
||||
) -> Flash<Redirect> {
|
||||
let public_key = &pub_key.key;
|
||||
// perform the sbotcli unfollow action using &pub_key.0
|
||||
// if successful, redirect to profile page with provided public key and flash "success"
|
||||
// if error, redirect to profile page with provided public key and flash "error"
|
||||
// redirect to the profile template with provided public key
|
||||
let profile_url = uri!(profile(Some(public_key)));
|
||||
let success_msg = format!("Unfollowed {}", public_key);
|
||||
Flash::success(Redirect::to(profile_url), success_msg)
|
||||
}
|
||||
|
||||
// HELPERS AND ROUTES FOR /block
|
||||
|
||||
/// Block a Scuttlebutt profile specified by the given public key. Redirects to the appropriate profile page with a flash message describing the outcome of the action (may be successful or unsuccessful).
|
||||
#[post("/block", data = "<pub_key>")]
|
||||
pub fn block(
|
||||
pub_key: Form<PublicKey>,
|
||||
flash: Option<FlashMessage>,
|
||||
_auth: Authenticated,
|
||||
) -> Flash<Redirect> {
|
||||
let public_key = &pub_key.key;
|
||||
// perform the sbotcli block action using &pub_key.0
|
||||
// if successful, redirect to profile page with provided public key and flash "success"
|
||||
// if error, redirect to profile page with provided public key and flash "error"
|
||||
// redirect to the profile template with provided public key
|
||||
let profile_url = uri!(profile(Some(public_key)));
|
||||
let success_msg = format!("Blocked {}", public_key);
|
||||
Flash::success(Redirect::to(profile_url), success_msg)
|
||||
}
|
||||
|
||||
// HELPERS AND ROUTES FOR /profile
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct ProfileContext {
|
||||
pub back: Option<String>,
|
||||
pub flash_name: Option<String>,
|
||||
pub flash_msg: Option<String>,
|
||||
pub title: Option<String>,
|
||||
}
|
||||
|
||||
impl ProfileContext {
|
||||
pub fn build() -> ProfileContext {
|
||||
ProfileContext {
|
||||
back: None,
|
||||
flash_name: None,
|
||||
flash_msg: None,
|
||||
title: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A Scuttlebutt profile, specified by a public key. It may be our own profile or the profile of a peer. If not public key query parameter is provided, the local profile is displayed (ie. the profile of the public key associated with the local PeachCloud device).
|
||||
#[get("/profile?<pub_key>")]
|
||||
pub fn profile(
|
||||
pub_key: Option<&str>,
|
||||
flash: Option<FlashMessage>,
|
||||
_auth: Authenticated,
|
||||
) -> Template {
|
||||
let mut context = ProfileContext::build();
|
||||
context.back = Some("/".to_string());
|
||||
context.title = Some("Profile".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());
|
||||
};
|
||||
Template::render("scuttlebutt/profile", &context)
|
||||
}
|
||||
|
||||
// HELPERS AND ROUTES FOR /friends
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct FriendsContext {
|
||||
pub back: Option<String>,
|
||||
pub flash_name: Option<String>,
|
||||
pub flash_msg: Option<String>,
|
||||
pub title: Option<String>,
|
||||
}
|
||||
|
||||
impl FriendsContext {
|
||||
pub fn build() -> FriendsContext {
|
||||
FriendsContext {
|
||||
back: None,
|
||||
flash_name: None,
|
||||
flash_msg: None,
|
||||
title: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A list of friends (mutual follows), with each list item displaying the name, image and public
|
||||
/// key of the peer.
|
||||
#[get("/friends")]
|
||||
pub fn friends(flash: Option<FlashMessage>, _auth: Authenticated) -> Template {
|
||||
let mut context = FriendsContext::build();
|
||||
context.back = Some("/scuttlebutt/peers".to_string());
|
||||
context.title = Some("Friends".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());
|
||||
};
|
||||
Template::render("scuttlebutt/peers_list", &context)
|
||||
}
|
||||
|
||||
// HELPERS AND ROUTES FOR /follows
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct FollowsContext {
|
||||
pub back: Option<String>,
|
||||
pub flash_name: Option<String>,
|
||||
pub flash_msg: Option<String>,
|
||||
pub title: Option<String>,
|
||||
}
|
||||
|
||||
impl FollowsContext {
|
||||
pub fn build() -> FollowsContext {
|
||||
FollowsContext {
|
||||
back: None,
|
||||
flash_name: None,
|
||||
flash_msg: None,
|
||||
title: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A list of follows (peers we follow who do not follow us), with each list item displaying the name, image and public
|
||||
/// key of the peer.
|
||||
#[get("/follows")]
|
||||
pub fn follows(flash: Option<FlashMessage>, _auth: Authenticated) -> Template {
|
||||
let mut context = FollowsContext::build();
|
||||
context.back = Some("/scuttlebutt/peers".to_string());
|
||||
context.title = Some("Follows".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());
|
||||
};
|
||||
Template::render("scuttlebutt/peers_list", &context)
|
||||
}
|
||||
|
||||
// HELPERS AND ROUTES FOR /followers
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct FollowersContext {
|
||||
pub back: Option<String>,
|
||||
pub flash_name: Option<String>,
|
||||
pub flash_msg: Option<String>,
|
||||
pub title: Option<String>,
|
||||
}
|
||||
|
||||
impl FollowersContext {
|
||||
pub fn build() -> FollowersContext {
|
||||
FollowersContext {
|
||||
back: None,
|
||||
flash_name: None,
|
||||
flash_msg: None,
|
||||
title: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A list of followers (peers who follow us but who we do not follow), with each list item displaying the name, image and public
|
||||
/// key of the peer.
|
||||
#[get("/followers")]
|
||||
pub fn followers(flash: Option<FlashMessage>, _auth: Authenticated) -> Template {
|
||||
let mut context = FollowersContext::build();
|
||||
context.back = Some("/scuttlebutt/peers".to_string());
|
||||
context.title = Some("Followers".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());
|
||||
};
|
||||
Template::render("scuttlebutt/peers_list", &context)
|
||||
}
|
||||
|
||||
// HELPERS AND ROUTES FOR /blocks
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct BlocksContext {
|
||||
pub back: Option<String>,
|
||||
pub flash_name: Option<String>,
|
||||
pub flash_msg: Option<String>,
|
||||
pub title: Option<String>,
|
||||
}
|
||||
|
||||
impl BlocksContext {
|
||||
pub fn build() -> BlocksContext {
|
||||
BlocksContext {
|
||||
back: None,
|
||||
flash_name: None,
|
||||
flash_msg: None,
|
||||
title: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A list of blocks (peers we've blocked previously), with each list item displaying the name, image and public
|
||||
/// key of the peer.
|
||||
#[get("/blocks")]
|
||||
pub fn blocks(flash: Option<FlashMessage>, _auth: Authenticated) -> Template {
|
||||
let mut context = BlocksContext::build();
|
||||
context.back = Some("/scuttlebutt/peers".to_string());
|
||||
context.title = Some("Blocks".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());
|
||||
};
|
||||
Template::render("scuttlebutt/peers_list", &context)
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
use peach_lib::sbot::SbotStatus;
|
||||
use rouille::{post_input, try_or_400, Request, Response};
|
||||
|
||||
use crate::utils::{flash::FlashResponse, sbot};
|
||||
|
||||
// ROUTE: /scuttlebutt/block
|
||||
|
||||
/// Block a Scuttlebutt profile specified by the given public key.
|
||||
///
|
||||
/// Parse the public key from the submitted form and publish a contact message.
|
||||
/// Redirect to the appropriate profile page with a flash message describing
|
||||
/// the outcome of the action (may be successful or unsuccessful).
|
||||
pub fn handle_form(request: &Request) -> Response {
|
||||
// query the request body for form data
|
||||
// return a 400 error if the admin_id field is missing
|
||||
let data = try_or_400!(post_input!(request, {
|
||||
public_key: String,
|
||||
}));
|
||||
|
||||
let (flash_name, flash_msg) = match SbotStatus::read() {
|
||||
Ok(status) if status.state == Some("active".to_string()) => {
|
||||
match sbot::block_peer(&data.public_key) {
|
||||
Ok(success_msg) => (
|
||||
"flash_name=success".to_string(),
|
||||
format!("flash_msg={}", success_msg),
|
||||
),
|
||||
Err(error_msg) => (
|
||||
"flash_name=error".to_string(),
|
||||
format!("flash_msg={}", error_msg),
|
||||
),
|
||||
}
|
||||
}
|
||||
_ => (
|
||||
"flash_name=warning".to_string(),
|
||||
"Social interactions are unavailable.".to_string(),
|
||||
),
|
||||
};
|
||||
|
||||
let url = format!("/scuttlebutt/profile/{}", data.public_key);
|
||||
|
||||
Response::redirect_303(url).add_flash(flash_name, flash_msg)
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
use maud::PreEscaped;
|
||||
|
||||
use crate::{
|
||||
templates,
|
||||
utils::{sbot, theme},
|
||||
};
|
||||
|
||||
// ROUTE: /scuttlebutt/blocks
|
||||
|
||||
/// Scuttlebutt blocks list template builder.
|
||||
pub fn build_template() -> PreEscaped<String> {
|
||||
// retrieve the list of blocked peers
|
||||
match sbot::get_blocks_list() {
|
||||
// populate the peers_list template with blocks and render it
|
||||
Ok(blocks) => templates::peers_list::build_template(blocks, "Blocks"),
|
||||
Err(e) => {
|
||||
// render the sbot error template with the error message
|
||||
let error_template = templates::error::build_template(e.to_string());
|
||||
// wrap the nav bars around the error template content
|
||||
let body = templates::nav::build_template(error_template, "Blocks", Some("/"));
|
||||
|
||||
// query the current theme so we can pass it into the base template builder
|
||||
let theme = theme::get_theme();
|
||||
|
||||
// render the base template with the provided body
|
||||
templates::base::build_template(body, theme)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
use peach_lib::sbot::SbotStatus;
|
||||
use rouille::{post_input, try_or_400, Request, Response};
|
||||
|
||||
use crate::utils::{flash::FlashResponse, sbot};
|
||||
|
||||
// ROUTE: /scuttlebutt/follow
|
||||
|
||||
/// Follow a Scuttlebutt profile specified by the given public key.
|
||||
///
|
||||
/// Parse the public key from the submitted form and publish a contact message.
|
||||
/// Redirect to the appropriate profile page with a flash message describing
|
||||
/// the outcome of the action (may be successful or unsuccessful).
|
||||
pub fn handle_form(request: &Request) -> Response {
|
||||
// query the request body for form data
|
||||
// return a 400 error if the admin_id field is missing
|
||||
let data = try_or_400!(post_input!(request, {
|
||||
public_key: String,
|
||||
}));
|
||||
|
||||
let (flash_name, flash_msg) = match SbotStatus::read() {
|
||||
Ok(status) if status.state == Some("active".to_string()) => {
|
||||
match sbot::follow_peer(&data.public_key) {
|
||||
Ok(success_msg) => (
|
||||
"flash_name=success".to_string(),
|
||||
format!("flash_msg={}", success_msg),
|
||||
),
|
||||
Err(error_msg) => (
|
||||
"flash_name=error".to_string(),
|
||||
format!("flash_msg={}", error_msg),
|
||||
),
|
||||
}
|
||||
}
|
||||
_ => (
|
||||
"flash_name=warning".to_string(),
|
||||
"Social interactions are unavailable.".to_string(),
|
||||
),
|
||||
};
|
||||
|
||||
let url = format!("/scuttlebutt/profile/{}", data.public_key);
|
||||
|
||||
Response::redirect_303(url).add_flash(flash_name, flash_msg)
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
use maud::PreEscaped;
|
||||
|
||||
use crate::{
|
||||
templates,
|
||||
utils::{sbot, theme},
|
||||
};
|
||||
|
||||
// ROUTE: /scuttlebutt/follows
|
||||
|
||||
/// Scuttlebutt follows list template builder.
|
||||
pub fn build_template() -> PreEscaped<String> {
|
||||
// retrieve the list of follows
|
||||
match sbot::get_follows_list() {
|
||||
// populate the peers_list template with follows
|
||||
Ok(follows) => templates::peers_list::build_template(follows, "Follows"),
|
||||
Err(e) => {
|
||||
// render the sbot error template with the error message
|
||||
let error_template = templates::error::build_template(e.to_string());
|
||||
// wrap the nav bars around the error template content
|
||||
let body = templates::nav::build_template(error_template, "Follows", Some("/"));
|
||||
|
||||
// query the current theme so we can pass it into the base template builder
|
||||
let theme = theme::get_theme();
|
||||
|
||||
// render the base template with the provided body
|
||||
templates::base::build_template(body, theme)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
use maud::PreEscaped;
|
||||
|
||||
use crate::{
|
||||
templates,
|
||||
utils::{sbot, theme},
|
||||
};
|
||||
|
||||
// ROUTE: /scuttlebutt/friends
|
||||
|
||||
/// Scuttlebutt friends list template builder.
|
||||
pub fn build_template() -> PreEscaped<String> {
|
||||
// retrieve the list of friends
|
||||
match sbot::get_friends_list() {
|
||||
// populate the peers_list template with friends and render it
|
||||
Ok(friends) => templates::peers_list::build_template(friends, "Friends"),
|
||||
Err(e) => {
|
||||
// render the sbot error template with the error message
|
||||
let error_template = templates::error::build_template(e.to_string());
|
||||
// wrap the nav bars around the error template content
|
||||
let body = templates::nav::build_template(error_template, "Friends", Some("/"));
|
||||
|
||||
// query the current theme so we can pass it into the base template builder
|
||||
let theme = theme::get_theme();
|
||||
|
||||
// render the base template with the provided body
|
||||
templates::base::build_template(body, theme)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,97 @@
|
|||
use maud::{html, Markup, PreEscaped};
|
||||
use peach_lib::sbot::SbotStatus;
|
||||
use rouille::{post_input, try_or_400, Request, Response};
|
||||
|
||||
use crate::{
|
||||
templates,
|
||||
utils::{
|
||||
flash::{FlashRequest, FlashResponse},
|
||||
sbot, theme,
|
||||
},
|
||||
};
|
||||
|
||||
// ROUTE: /scuttlebutt/invites
|
||||
|
||||
/// Render the invite form template.
|
||||
fn invite_form_template(
|
||||
flash_name: Option<&str>,
|
||||
flash_msg: Option<&str>,
|
||||
invite_code: Option<&str>,
|
||||
) -> Markup {
|
||||
html! {
|
||||
(PreEscaped("<!-- SCUTTLEBUTT INVITE FORM -->"))
|
||||
div class="card center" {
|
||||
form id="invites" class="center" action="/scuttlebutt/invites" method="post" {
|
||||
div class="center" style="width: 80%;" {
|
||||
label for="inviteUses" class="label-small font-gray" title="Number of times the invite code can be reused" { "USES" }
|
||||
input type="number" id="inviteUses" name="uses" min="1" max="150" size="3" value="1";
|
||||
@if let Some(code) = invite_code {
|
||||
p class="card-text" style="margin-top: 1rem; user-select: all;" title="Invite code" {
|
||||
(code)
|
||||
}
|
||||
}
|
||||
}
|
||||
(PreEscaped("<!-- BUTTONS -->"))
|
||||
input id="createInvite" class="button button-primary center" style="margin-top: 1rem;" type="submit" title="Create a new invite code" value="Create";
|
||||
a id="cancel" class="button button-secondary center" href="/scuttlebutt/peers" title="Cancel" { "Cancel" }
|
||||
}
|
||||
// render flash message if cookies were found in the request
|
||||
@if let (Some(name), Some(msg)) = (flash_name, flash_msg) {
|
||||
// avoid displaying the invite code-containing flash msg
|
||||
@if name != "code" {
|
||||
(PreEscaped("<!-- FLASH MESSAGE -->"))
|
||||
(templates::flash::build_template(name, msg))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Scuttlebutt invite template builder.
|
||||
pub fn build_template(request: &Request) -> PreEscaped<String> {
|
||||
// check for flash cookies; will be (None, None) if no flash cookies are found
|
||||
let (flash_name, flash_msg) = request.retrieve_flash();
|
||||
|
||||
// if flash_name is "code" then flash_msg will be an invite code
|
||||
let invite_code = if flash_name == Some("code") {
|
||||
flash_msg
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let invite_form_template = match SbotStatus::read() {
|
||||
// only render the invite form template if the sbot is active
|
||||
Ok(status) if status.state == Some("active".to_string()) => {
|
||||
html! { (invite_form_template(flash_name, flash_msg, invite_code)) }
|
||||
}
|
||||
_ => {
|
||||
// the sbot is not active; render a message instead of the invite form
|
||||
templates::inactive::build_template("Invite creation is unavailable.")
|
||||
}
|
||||
};
|
||||
|
||||
let body =
|
||||
templates::nav::build_template(invite_form_template, "Invites", Some("/scuttlebutt/peers"));
|
||||
|
||||
// query the current theme so we can pass it into the base template builder
|
||||
let theme = theme::get_theme();
|
||||
|
||||
templates::base::build_template(body, theme)
|
||||
}
|
||||
|
||||
/// Parse the invite uses data and attempt to generate an invite code.
|
||||
pub fn handle_form(request: &Request) -> Response {
|
||||
// query the request body for form data
|
||||
// return a 400 error if the admin_id field is missing
|
||||
let data = try_or_400!(post_input!(request, {
|
||||
// the number of times the invite code can be used
|
||||
uses: u16,
|
||||
}));
|
||||
|
||||
let (flash_name, flash_msg) = match sbot::create_invite(data.uses) {
|
||||
Ok(code) => ("flash_name=code".to_string(), format!("flash_msg={}", code)),
|
||||
Err(e) => ("flash_name=error".to_string(), format!("flash_msg={}", e)),
|
||||
};
|
||||
|
||||
Response::redirect_303("/scuttlebutt/invites").add_flash(flash_name, flash_msg)
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
pub mod block;
|
||||
pub mod blocks;
|
||||
pub mod follow;
|
||||
pub mod follows;
|
||||
pub mod friends;
|
||||
pub mod invites;
|
||||
pub mod peers;
|
||||
pub mod private;
|
||||
pub mod profile;
|
||||
pub mod profile_update;
|
||||
pub mod publish;
|
||||
pub mod search;
|
||||
pub mod unblock;
|
||||
pub mod unfollow;
|
|
@ -0,0 +1,45 @@
|
|||
use maud::{html, PreEscaped};
|
||||
use peach_lib::sbot::SbotStatus;
|
||||
|
||||
use crate::{templates, utils::theme};
|
||||
|
||||
/// Scuttlebutt peer menu template builder.
|
||||
///
|
||||
/// A peer menu which allows navigating to lists of friends, follows, followers
|
||||
/// and blocks, as well as accessing the invite creation form.
|
||||
pub fn build_template() -> PreEscaped<String> {
|
||||
let menu_template = match SbotStatus::read() {
|
||||
Ok(status) if status.state == Some("active".to_string()) => {
|
||||
// render the scuttlebutt peers menu
|
||||
html! {
|
||||
(PreEscaped("<!-- SCUTTLEBUTT PEERS -->"))
|
||||
div class="card center" {
|
||||
div class="card-container" {
|
||||
(PreEscaped("<!-- BUTTONS -->"))
|
||||
div id="buttons" {
|
||||
a id="search" class="button button-primary center" href="/scuttlebutt/search" title="Search for a peer" { "Search" }
|
||||
a id="friends" class="button button-primary center" href="/scuttlebutt/friends" title="List friends" { "Friends" }
|
||||
a id="follows" class="button button-primary center" href="/scuttlebutt/follows" title="List follows" { "Follows" }
|
||||
a id="blocks" class="button button-primary center" href="/scuttlebutt/blocks" title="List blocks" { "Blocks" }
|
||||
a id="invites" class="button button-primary center" href="/scuttlebutt/invites" title="Create invites" { "Invites" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// the sbot is not active; render a message instead of the menu
|
||||
templates::inactive::build_template("Social lists and interactions are unavailable.")
|
||||
}
|
||||
};
|
||||
|
||||
// wrap the nav bars around the settings menu template content
|
||||
// parameters are template, title and back url
|
||||
let body = templates::nav::build_template(menu_template, "Scuttlebutt Peers", Some("/"));
|
||||
|
||||
// query the current theme so we can pass it into the base template builder
|
||||
let theme = theme::get_theme();
|
||||
|
||||
// render the base template with the provided body
|
||||
templates::base::build_template(body, theme)
|
||||
}
|
|
@ -0,0 +1,131 @@
|
|||
use maud::{html, Markup, PreEscaped};
|
||||
use peach_lib::sbot::SbotStatus;
|
||||
use rouille::{post_input, try_or_400, Request, Response};
|
||||
|
||||
use crate::{
|
||||
templates,
|
||||
utils::{
|
||||
flash::{FlashRequest, FlashResponse},
|
||||
sbot, theme,
|
||||
},
|
||||
};
|
||||
|
||||
// ROUTE: /scuttlebutt/private
|
||||
|
||||
fn public_key_input_template(ssb_id: &Option<String>) -> Markup {
|
||||
match ssb_id {
|
||||
Some(id) => {
|
||||
html! { input type="text" id="publicKey" name="recipient" placeholder="@xYz...=.ed25519" value=(id); }
|
||||
}
|
||||
// render the input with autofocus if no ssb_id has been provided
|
||||
None => {
|
||||
html! { input type="text" id="publicKey" name="recipient" placeholder="@xYz...=.ed25519" autofocus; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn private_message_textarea_template(ssb_id: &Option<String>) -> Markup {
|
||||
match ssb_id {
|
||||
Some(_) => {
|
||||
html! { textarea id="privatePost" class="center input message-input" name="text" title="Compose a private message" placeholder="Write a private message..." autofocus { "" } }
|
||||
}
|
||||
// render the textarea with autofocus if an ssb_id has been provided
|
||||
None => {
|
||||
html! { textarea id="privatePost" class="center input message-input" name="text" title="Compose a private message" placeholder="Write a private message..." { "" } }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Scuttlebutt private message template builder.
|
||||
///
|
||||
/// Render a form for publishing a provate message. The recipient input field
|
||||
/// is populated with the provided ssb_id. If no recipient is provided, the
|
||||
/// template autofocuses on the recipient input field.
|
||||
pub fn build_template(request: &Request, ssb_id: Option<String>) -> PreEscaped<String> {
|
||||
// check for flash cookies; will be (None, None) if no flash cookies are found
|
||||
let (flash_name, flash_msg) = request.retrieve_flash();
|
||||
|
||||
let profile_template = match SbotStatus::read() {
|
||||
// only render the private message elements if the sbot is active
|
||||
Ok(status) if status.state == Some("active".to_string()) => {
|
||||
// retrieve the local public key (set to blank if an error is returned)
|
||||
let local_id = match sbot::get_local_id() {
|
||||
Ok(id) => id,
|
||||
Err(_) => "".to_string(),
|
||||
};
|
||||
|
||||
html! {
|
||||
(PreEscaped("<!-- SCUTTLEBUTT PRIVATE MESSAGE FORM -->"))
|
||||
div class="card card-wide center" {
|
||||
form id="sbotConfig" class="center" action="/scuttlebutt/private" method="post" {
|
||||
div class="center" style="display: flex; flex-direction: column; margin-bottom: 1rem;" title="Public key (ID) of the peer being written to" {
|
||||
label for="publicKey" class="label-small font-gray" {
|
||||
"PUBLIC KEY"
|
||||
}
|
||||
(public_key_input_template(&ssb_id))
|
||||
}
|
||||
(PreEscaped("<!-- input for message contents -->"))
|
||||
(private_message_textarea_template(&ssb_id))
|
||||
(PreEscaped("<!-- hidden input field to pass the public key of the local peer -->"))
|
||||
input type="hidden" id="localId" name="id" value=(local_id);
|
||||
(PreEscaped("<!-- BUTTONS -->"))
|
||||
input id="publish" class="button button-primary center" type="submit" style="margin-top: 1rem;" title="Publish private message to peer" value="Publish";
|
||||
}
|
||||
// render flash message if cookies were found in the request
|
||||
@if let (Some(name), Some(msg)) = (flash_name, flash_msg) {
|
||||
(PreEscaped("<!-- FLASH MESSAGE -->"))
|
||||
(templates::flash::build_template(name, msg))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => templates::inactive::build_template("Private messaging is unavailable."),
|
||||
};
|
||||
|
||||
let body = templates::nav::build_template(profile_template, "Profile", Some("/"));
|
||||
|
||||
// query the current theme so we can pass it into the base template builder
|
||||
let theme = theme::get_theme();
|
||||
|
||||
templates::base::build_template(body, theme)
|
||||
}
|
||||
|
||||
/// Publish a private message.
|
||||
///
|
||||
/// Parse the public key and private message text from the submitted form
|
||||
/// and publish the message. Set a flash message communicating the outcome
|
||||
/// of the publishing attempt and redirect to the private message page.
|
||||
pub fn handle_form(request: &Request) -> Response {
|
||||
// query the request body for form data
|
||||
// return a 400 error if the admin_id field is missing
|
||||
let data = try_or_400!(post_input!(request, {
|
||||
id: String,
|
||||
text: String,
|
||||
recipient: String
|
||||
}));
|
||||
|
||||
// now we need to add the local id to the recipients vector,
|
||||
// otherwise the local id will not be able to read the message.
|
||||
let recipients = vec![data.id, data.recipient];
|
||||
|
||||
let (flash_name, flash_msg) = match SbotStatus::read() {
|
||||
Ok(status) if status.state == Some("active".to_string()) => {
|
||||
match sbot::publish_private_msg(data.text, recipients) {
|
||||
Ok(success_msg) => (
|
||||
"flash_name=success".to_string(),
|
||||
format!("flash_msg={}", success_msg),
|
||||
),
|
||||
Err(error_msg) => (
|
||||
"flash_name=error".to_string(),
|
||||
format!("flash_msg={}", error_msg),
|
||||
),
|
||||
}
|
||||
}
|
||||
_ => (
|
||||
"flash_name=warning".to_string(),
|
||||
"Private messaging is unavailable.".to_string(),
|
||||
),
|
||||
};
|
||||
|
||||
Response::redirect_303("/scuttlebutt/private").add_flash(flash_name, flash_msg)
|
||||
}
|
|
@ -0,0 +1,191 @@
|
|||
use maud::{html, Markup, PreEscaped};
|
||||
use peach_lib::sbot::SbotStatus;
|
||||
use rouille::Request;
|
||||
|
||||
use crate::{
|
||||
templates,
|
||||
utils::{cookie::CookieRequest, flash::FlashRequest, sbot, sbot::Profile, theme},
|
||||
};
|
||||
|
||||
// ROUTE: /scuttlebutt/profile
|
||||
|
||||
fn public_post_form_template() -> Markup {
|
||||
html! {
|
||||
(PreEscaped("<!-- PUBLIC POST FORM -->"))
|
||||
form id="postForm" class="center" action="/scuttlebutt/publish" method="post" {
|
||||
(PreEscaped("<!-- input for message contents -->"))
|
||||
textarea id="publicPost" class="center input message-input" name="text" title="Compose Public Post" placeholder="Write a public post..." { }
|
||||
input id="publishPost" class="button button-primary center" title="Publish" type="submit" value="Publish";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn profile_info_box_template(profile: &Profile) -> Markup {
|
||||
html! {
|
||||
(PreEscaped("<!-- PROFILE INFO BOX -->"))
|
||||
div class="capsule capsule-profile border-ssb" title="Scuttlebutt account profile information" {
|
||||
@if profile.is_local_profile {
|
||||
(PreEscaped("<!-- edit profile button -->"))
|
||||
a class="nav-icon-right" href="/scuttlebutt/profile/update" title="Edit your profile" {
|
||||
img id="editProfile" class="icon-small icon-active" src="/icons/pencil.svg" alt="Edit";
|
||||
}
|
||||
}
|
||||
// render the profile bio: picture, id, name, image & description
|
||||
(profile_bio_template(profile))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn profile_bio_template(profile: &Profile) -> Markup {
|
||||
html! {
|
||||
(PreEscaped("<!-- PROFILE BIO -->"))
|
||||
(PreEscaped("<!-- profile picture -->"))
|
||||
// only try to render profile pic if we have the blob
|
||||
@match &profile.blob_path {
|
||||
Some(blob_path) if profile.blob_exists => {
|
||||
img id="profilePicture" class="icon-large" src={ "/blob/" (blob_path) } title="Profile picture" alt="Profile picture";
|
||||
},
|
||||
_ => {
|
||||
// use a placeholder image if we don't have the blob
|
||||
img id="peerImage" class="icon icon-active list-icon" src="/icons/user.svg" alt="Placeholder profile image";
|
||||
}
|
||||
}
|
||||
(PreEscaped("<!-- name, public key & description -->"))
|
||||
p id="profileName" class="card-text" title="Name" {
|
||||
@if let Some(name) = &profile.name {
|
||||
(name)
|
||||
} @else {
|
||||
i { "Name is unavailable or has not been set" }
|
||||
}
|
||||
}
|
||||
label class="label-small label-ellipsis font-gray" style="user-select: all;" for="profileName" title="Public Key" {
|
||||
@if let Some(id) = &profile.id {
|
||||
(id)
|
||||
} @else {
|
||||
"Public key unavailable"
|
||||
}
|
||||
}
|
||||
p id="profileDescription" style="margin-top: 1rem" class="card-text" title="Description" {
|
||||
@if let Some(description) = &profile.description {
|
||||
(description)
|
||||
} @else {
|
||||
i { "Description is unavailable or has not been set" }
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
fn social_interaction_buttons_template(profile: &Profile) -> Markup {
|
||||
html! {
|
||||
(PreEscaped("<!-- BUTTONS -->"))
|
||||
div id="buttons" style="margin-top: 2rem;" {
|
||||
@match (profile.following, &profile.id) {
|
||||
(Some(false), Some(ssb_id)) => {
|
||||
form id="followForm" class="center" action="/scuttlebutt/follow" method="post" {
|
||||
// url encode the ssb_id value
|
||||
input type="hidden" id="publicKey" name="public_key" value=(ssb_id.replace('/', "%2F"));
|
||||
input id="followPeer" class="button button-primary center" type="submit" title="Follow Peer" value="Follow";
|
||||
}
|
||||
},
|
||||
(Some(true), Some(ssb_id)) => {
|
||||
form id="unfollowForm" class="center" action="/scuttlebutt/unfollow" method="post" {
|
||||
// url encode the ssb_id value
|
||||
input type="hidden" id="publicKey" name="public_key" value=(ssb_id.replace('/', "%2F"));
|
||||
input id="unfollowPeer" class="button button-primary center" type="submit" title="Unfollow Peer" value="Unfollow";
|
||||
}
|
||||
},
|
||||
_ => p { "Unable to determine follow state" }
|
||||
}
|
||||
@match (profile.blocking, &profile.id) {
|
||||
(Some(false), Some(ssb_id)) => {
|
||||
form id="blockForm" class="center" action="/scuttlebutt/block" method="post" {
|
||||
// url encode the ssb_id value
|
||||
input type="hidden" id="publicKey" name="public_key" value=(ssb_id.replace('/', "%2F"));
|
||||
input id="blockPeer" class="button button-primary center" type="submit" title="Block Peer" value="Block";
|
||||
}
|
||||
},
|
||||
(Some(true), Some(ssb_id)) => {
|
||||
form id="unblockForm" class="center" action="/scuttlebutt/unblock" method="post" {
|
||||
// url encode the ssb_id value
|
||||
input type="hidden" id="publicKey" name="public_key" value=(ssb_id.replace('/', "%2F"));
|
||||
input id="unblockPeer" class="button button-primary center" type="submit" title="Unblock Peer" value="Unblock";
|
||||
}
|
||||
},
|
||||
_ => p { "Unable to determine block state" }
|
||||
}
|
||||
@if let Some(ssb_id) = &profile.id {
|
||||
form class="center" {
|
||||
// url encode the ssb_id
|
||||
a id="privateMessage" class="button button-primary center" href={ "/scuttlebutt/private/" (ssb_id.replace('/', "%2F")) } title="Private Message" {
|
||||
"Send Private Message"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Scuttlebutt profile template builder.
|
||||
///
|
||||
/// Render a Scuttlebutt profile, either for the local profile or for a peer
|
||||
/// specified by a public key. If the public key query parameter is not
|
||||
/// provided, the local profile is displayed (ie. the profile of the public key
|
||||
/// associated with the local PeachCloud device).
|
||||
pub fn build_template(request: &Request, ssb_id: Option<String>) -> PreEscaped<String> {
|
||||
// check for flash cookies; will be (None, None) if no flash cookies are found
|
||||
let (flash_name, flash_msg) = request.retrieve_flash();
|
||||
|
||||
let profile_template = match SbotStatus::read() {
|
||||
Ok(status) if status.state == Some("active".to_string()) => {
|
||||
// TODO: validate ssb_id and return error template
|
||||
|
||||
// retrieve the profile info
|
||||
match sbot::get_profile_info(ssb_id) {
|
||||
Ok(profile) => {
|
||||
// render the profile template
|
||||
html! {
|
||||
(PreEscaped("<!-- SSB PROFILE -->"))
|
||||
div class="card card-wide center" {
|
||||
// render profile info box
|
||||
(profile_info_box_template(&profile))
|
||||
@if profile.is_local_profile {
|
||||
// render the public post form template
|
||||
(public_post_form_template())
|
||||
} @else {
|
||||
// render follow / unfollow, block / unblock and
|
||||
// private message buttons
|
||||
(social_interaction_buttons_template(&profile))
|
||||
}
|
||||
// render flash message if cookies were found in the request
|
||||
@if let (Some(name), Some(msg)) = (flash_name, flash_msg) {
|
||||
(PreEscaped("<!-- FLASH MESSAGE -->"))
|
||||
(templates::flash::build_template(name, msg))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
// render the sbot error template with the error message
|
||||
templates::error::build_template(e.to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => templates::inactive::build_template("Profile is unavailable."),
|
||||
};
|
||||
|
||||
// a request to /scuttlebutt/profile can originate via the Friends,
|
||||
// Follows or Blocks menu - as well as the Search page and Homepage.
|
||||
// therefore, we check to see if the `back_url` cookie has been set
|
||||
// and assign the path of the back button accordingly.
|
||||
// for example, if the request has come via the Friends menu then the
|
||||
// `back_url` cookie will be set with a value of "/scuttlebutt/friends".
|
||||
let back_url = request.retrieve_cookie("back_url").or(Some("/"));
|
||||
|
||||
let body = templates::nav::build_template(profile_template, "Profile", back_url);
|
||||
|
||||
// query the current theme so we can pass it into the base template builder
|
||||
let theme = theme::get_theme();
|
||||
|
||||
templates::base::build_template(body, theme)
|
||||
}
|
|
@ -0,0 +1,173 @@
|
|||
use maud::{html, PreEscaped};
|
||||
use peach_lib::sbot::SbotStatus;
|
||||
use rouille::{input::post::BufferedFile, post_input, try_or_400, Request, Response};
|
||||
|
||||
use crate::{
|
||||
templates,
|
||||
utils::{
|
||||
flash::{FlashRequest, FlashResponse},
|
||||
sbot,
|
||||
sbot::Profile,
|
||||
theme,
|
||||
},
|
||||
};
|
||||
|
||||
// ROUTE: /scuttlebutt/profile/update
|
||||
|
||||
fn parse_profile_info(profile: Profile) -> (String, String, String) {
|
||||
let id = match profile.id {
|
||||
Some(id) => id,
|
||||
_ => "Public key unavailable".to_string(),
|
||||
};
|
||||
|
||||
let name = match profile.name {
|
||||
Some(name) => name,
|
||||
_ => "Name unavailable".to_string(),
|
||||
};
|
||||
|
||||
let description = match profile.description {
|
||||
Some(description) => description,
|
||||
_ => "Description unavailable".to_string(),
|
||||
};
|
||||
|
||||
(id, name, description)
|
||||
}
|
||||
|
||||
/// Scuttlebutt profile update template builder.
|
||||
///
|
||||
/// Serve a form for the purpose of updating the name, description and picture
|
||||
/// for the local Scuttlebutt profile.
|
||||
pub fn build_template(request: &Request) -> PreEscaped<String> {
|
||||
// check for flash cookies; will be (None, None) if no flash cookies are found
|
||||
let (flash_name, flash_msg) = request.retrieve_flash();
|
||||
|
||||
let profile_update_template = match SbotStatus::read() {
|
||||
Ok(status) if status.state == Some("active".to_string()) => {
|
||||
// retrieve the local profile info
|
||||
match sbot::get_profile_info(None) {
|
||||
Ok(profile) => {
|
||||
let (id, name, description) = parse_profile_info(profile);
|
||||
|
||||
// render the scuttlebutt profile update form
|
||||
html! {
|
||||
(PreEscaped("<!-- SSB PROFILE UPDATE FORM -->"))
|
||||
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"
|
||||
}
|
||||
input style="margin-bottom: 1rem;" type="text" id="name" name="new_name" placeholder="Choose a name for your profile..." value=(name);
|
||||
label for="description" class="label-small font-gray" {
|
||||
"DESCRIPTION"
|
||||
}
|
||||
textarea id="description" class="message-input" style="margin-bottom: 1rem;" name="new_description" placeholder="Write a description for your profile..." {
|
||||
(description)
|
||||
}
|
||||
label for="image" class="label-small font-gray" {
|
||||
"IMAGE"
|
||||
}
|
||||
input type="file" id="fileInput" class="font-normal" name="image";
|
||||
}
|
||||
input type="hidden" name="id" value=(id);
|
||||
input type="hidden" name="current_name" value=(name);
|
||||
input type="hidden" name="current_description" value=(description);
|
||||
div id="buttonDiv" style="margin-top: 2rem;" {
|
||||
input id="updateProfile" class="button button-primary center" title="Publish" type="submit" value="Publish";
|
||||
a class="button button-secondary center" href="/scuttlebutt/profile" title="Cancel" { "Cancel" }
|
||||
|
||||
}
|
||||
}
|
||||
// render flash message if cookies were found in the request
|
||||
@if let (Some(name), Some(msg)) = (flash_name, flash_msg) {
|
||||
(PreEscaped("<!-- FLASH MESSAGE -->"))
|
||||
(templates::flash::build_template(name, msg))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
// render the sbot error template with the error message
|
||||
templates::error::build_template(e.to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// the sbot is not active; render a message instead of the form
|
||||
templates::inactive::build_template("Profile is unavailable.")
|
||||
}
|
||||
};
|
||||
|
||||
let body = templates::nav::build_template(
|
||||
profile_update_template,
|
||||
"Profile",
|
||||
Some("/scuttlebutt/profile"),
|
||||
);
|
||||
|
||||
// query the current theme so we can pass it into the base template builder
|
||||
let theme = theme::get_theme();
|
||||
|
||||
templates::base::build_template(body, theme)
|
||||
}
|
||||
|
||||
/// Update the name, description and picture for the local Scuttlebutt profile.
|
||||
///
|
||||
/// Redirects to profile page of the PeachCloud local identity with a flash
|
||||
/// message describing the outcome of the action (may be successful or
|
||||
/// unsuccessful).
|
||||
pub fn handle_form(request: &Request) -> Response {
|
||||
// query the request body for form data
|
||||
// return a 400 error if the admin_id field is missing
|
||||
let data = try_or_400!(post_input!(request, {
|
||||
id: String,
|
||||
current_name: String,
|
||||
current_description: String,
|
||||
new_name: Option<String>,
|
||||
new_description: Option<String>,
|
||||
image: Option<BufferedFile>,
|
||||
}));
|
||||
|
||||
let (flash_name, flash_msg) = match SbotStatus::read() {
|
||||
Ok(status) if status.state == Some("active".to_string()) => {
|
||||
// we can't pass `data` into the function (due to macro creation)
|
||||
// so we pass in each individual value instead
|
||||
match sbot::update_profile_info(
|
||||
data.current_name,
|
||||
data.current_description,
|
||||
data.new_name,
|
||||
data.new_description,
|
||||
data.image,
|
||||
) {
|
||||
Ok(success_msg) => (
|
||||
"flash_name=success".to_string(),
|
||||
format!("flash_msg={}", success_msg),
|
||||
),
|
||||
Err(error_msg) => (
|
||||
"flash_name=error".to_string(),
|
||||
format!("flash_msg={}", error_msg),
|
||||
),
|
||||
}
|
||||
}
|
||||
_ => (
|
||||
"flash_name=warning".to_string(),
|
||||
"Profile is unavailable.".to_string(),
|
||||
),
|
||||
};
|
||||
|
||||
Response::redirect_303("/scuttlebutt/profile/update").add_flash(flash_name, flash_msg)
|
||||
}
|
||||
|
||||
/*
|
||||
match sbot::validate_public_key(&data.public_key) {
|
||||
Ok(_) => {
|
||||
let url = format!("/scuttlebutt/profile?={}", &data.public_key);
|
||||
Response::redirect_303(url)
|
||||
}
|
||||
Err(err) => {
|
||||
let (flash_name, flash_msg) =
|
||||
("flash_name=error".to_string(), format!("flash_msg={}", err));
|
||||
Response::redirect_303("/scuttlebutt/search").add_flash(flash_name, flash_msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
|
@ -0,0 +1,41 @@
|
|||
use peach_lib::sbot::SbotStatus;
|
||||
use rouille::{post_input, try_or_400, Request, Response};
|
||||
|
||||
use crate::utils::{flash::FlashResponse, sbot};
|
||||
|
||||
// ROUTE: /scuttlebutt/publish
|
||||
|
||||
/// Publish a public Scuttlebutt post.
|
||||
///
|
||||
/// Parse the post text from the submitted form and publish the message.
|
||||
/// Redirect to the profile page of the PeachCloud local identity with a flash
|
||||
/// message describing the outcome of the action (may be successful or
|
||||
/// unsuccessful).
|
||||
pub fn handle_form(request: &Request) -> Response {
|
||||
// query the request body for form data
|
||||
// return a 400 error if the admin_id field is missing
|
||||
let data = try_or_400!(post_input!(request, {
|
||||
text: String,
|
||||
}));
|
||||
|
||||
let (flash_name, flash_msg) = match SbotStatus::read() {
|
||||
Ok(status) if status.state == Some("active".to_string()) => {
|
||||
match sbot::publish_public_post(data.text) {
|
||||
Ok(success_msg) => (
|
||||
"flash_name=success".to_string(),
|
||||
format!("flash_msg={}", success_msg),
|
||||
),
|
||||
Err(error_msg) => (
|
||||
"flash_name=error".to_string(),
|
||||
format!("flash_msg={}", error_msg),
|
||||
),
|
||||
}
|
||||
}
|
||||
_ => (
|
||||
"flash_name=warning".to_string(),
|
||||
"Public posting is unavailable.".to_string(),
|
||||
),
|
||||
};
|
||||
|
||||
Response::redirect_303("/scuttlebutt/profile").add_flash(flash_name, flash_msg)
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
use maud::{html, PreEscaped};
|
||||
use rouille::{post_input, try_or_400, Request, Response};
|
||||
|
||||
use crate::{
|
||||
templates,
|
||||
utils::{
|
||||
flash::{FlashRequest, FlashResponse},
|
||||
sbot, theme,
|
||||
},
|
||||
};
|
||||
|
||||
// ROUTE: /scuttlebutt/search
|
||||
|
||||
/// Scuttlebutt peer search template builder.
|
||||
pub fn build_template(request: &Request) -> PreEscaped<String> {
|
||||
// check for flash cookies; will be (None, None) if no flash cookies are found
|
||||
let (flash_name, flash_msg) = request.retrieve_flash();
|
||||
|
||||
let search_template = html! {
|
||||
(PreEscaped("<!-- PEER SEARCH FORM -->"))
|
||||
div class="card center" {
|
||||
form id="sbotConfig" class="center" action="/scuttlebutt/search" method="post" {
|
||||
div class="center" style="display: flex; flex-direction: column; margin-bottom: 2rem;" title="Public key (ID) of a peer" {
|
||||
label for="publicKey" class="label-small font-gray" { "PUBLIC KEY" }
|
||||
input type="text" id="publicKey" name="public_key" placeholder="@xYz...=.ed25519" autofocus;
|
||||
}
|
||||
(PreEscaped("<!-- BUTTONS -->"))
|
||||
input id="search" class="button button-primary center" type="submit" title="Search for peer" value="Search";
|
||||
// render flash message if cookies were found in the request
|
||||
@if let (Some(name), Some(msg)) = (flash_name, flash_msg) {
|
||||
(PreEscaped("<!-- FLASH MESSAGE -->"))
|
||||
(templates::flash::build_template(name, msg))
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let body =
|
||||
templates::nav::build_template(search_template, "Search", Some("/scuttlebutt/peers"));
|
||||
|
||||
// query the current theme so we can pass it into the base template builder
|
||||
let theme = theme::get_theme();
|
||||
|
||||
templates::base::build_template(body, theme)
|
||||
}
|
||||
|
||||
/// Parse the public key, verify that it's valid and then redirect to the
|
||||
/// profile of the given key.
|
||||
///
|
||||
/// If the public key is invalid, set an error flash message and redirect.
|
||||
pub fn handle_form(request: &Request) -> Response {
|
||||
// query the request body for form data
|
||||
// return a 400 error if the admin_id field is missing
|
||||
let data = try_or_400!(post_input!(request, {
|
||||
public_key: String,
|
||||
}));
|
||||
|
||||
match sbot::validate_public_key(&data.public_key) {
|
||||
Ok(_) => {
|
||||
let url = format!("/scuttlebutt/profile/{}", &data.public_key);
|
||||
Response::redirect_303(url)
|
||||
}
|
||||
Err(err) => {
|
||||
let (flash_name, flash_msg) =
|
||||
("flash_name=error".to_string(), format!("flash_msg={}", err));
|
||||
Response::redirect_303("/scuttlebutt/search").add_flash(flash_name, flash_msg)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
use peach_lib::sbot::SbotStatus;
|
||||
use rouille::{post_input, try_or_400, Request, Response};
|
||||
|
||||
use crate::utils::{flash::FlashResponse, sbot};
|
||||
|
||||
// ROUTE: /scuttlebutt/unblock
|
||||
|
||||
/// Unblock a Scuttlebutt profile specified by the given public key.
|
||||
///
|
||||
/// Parse the public key from the submitted form and publish a contact message.
|
||||
/// Redirect to the appropriate profile page with a flash message describing
|
||||
/// the outcome of the action (may be successful or unsuccessful).
|
||||
pub fn handle_form(request: &Request) -> Response {
|
||||
// query the request body for form data
|
||||
// return a 400 error if the admin_id field is missing
|
||||
let data = try_or_400!(post_input!(request, {
|
||||
public_key: String,
|
||||
}));
|
||||
|
||||
let (flash_name, flash_msg) = match SbotStatus::read() {
|
||||
Ok(status) if status.state == Some("active".to_string()) => {
|
||||
match sbot::unblock_peer(&data.public_key) {
|
||||
Ok(success_msg) => (
|
||||
"flash_name=success".to_string(),
|
||||
format!("flash_msg={}", success_msg),
|
||||
),
|
||||
Err(error_msg) => (
|
||||
"flash_name=error".to_string(),
|
||||
format!("flash_msg={}", error_msg),
|
||||
),
|
||||
}
|
||||
}
|
||||
_ => (
|
||||
"flash_name=warning".to_string(),
|
||||
"Social interactions are unavailable.".to_string(),
|
||||
),
|
||||
};
|
||||
|
||||
let url = format!("/scuttlebutt/profile/{}", data.public_key);
|
||||
|
||||
Response::redirect_303(url).add_flash(flash_name, flash_msg)
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
use peach_lib::sbot::SbotStatus;
|
||||
use rouille::{post_input, try_or_400, Request, Response};
|
||||
|
||||
use crate::utils::{flash::FlashResponse, sbot};
|
||||
|
||||
// ROUTE: /scuttlebutt/unfollow
|
||||
|
||||
/// Unfollow a Scuttlebutt profile specified by the given public key.
|
||||
///
|
||||
/// Parse the public key from the submitted form and publish a contact message.
|
||||
/// Redirect to the appropriate profile page with a flash message describing
|
||||
/// the outcome of the action (may be successful or unsuccessful).
|
||||
pub fn handle_form(request: &Request) -> Response {
|
||||
// query the request body for form data
|
||||
// return a 400 error if the admin_id field is missing
|
||||
let data = try_or_400!(post_input!(request, {
|
||||
public_key: String,
|
||||
}));
|
||||
|
||||
let (flash_name, flash_msg) = match SbotStatus::read() {
|
||||
Ok(status) if status.state == Some("active".to_string()) => {
|
||||
match sbot::unfollow_peer(&data.public_key) {
|
||||
Ok(success_msg) => (
|
||||
"flash_name=success".to_string(),
|
||||
format!("flash_msg={}", success_msg),
|
||||
),
|
||||
Err(error_msg) => (
|
||||
"flash_name=error".to_string(),
|
||||
format!("flash_msg={}", error_msg),
|
||||
),
|
||||
}
|
||||
}
|
||||
_ => (
|
||||
"flash_name=warning".to_string(),
|
||||
"Social interactions are unavailable.".to_string(),
|
||||
),
|
||||
};
|
||||
|
||||
let url = format!("/scuttlebutt/profile/{}", data.public_key);
|
||||
|
||||
Response::redirect_303(url).add_flash(flash_name, flash_msg)
|
||||
}
|
|
@ -1,126 +0,0 @@
|
|||
use rocket::{
|
||||
form::{Form, FromForm},
|
||||
get, post,
|
||||
request::FlashMessage,
|
||||
response::{Flash, Redirect},
|
||||
serde::Deserialize,
|
||||
uri,
|
||||
};
|
||||
use rocket_dyn_templates::{tera::Context, Template};
|
||||
|
||||
use peach_lib::config_manager;
|
||||
|
||||
use crate::error::PeachWebError;
|
||||
use crate::routes::authentication::Authenticated;
|
||||
|
||||
// HELPERS AND ROUTES FOR /settings/admin
|
||||
|
||||
/// Administrator settings menu.
|
||||
#[get("/")]
|
||||
pub fn admin_menu(flash: Option<FlashMessage>, _auth: Authenticated) -> Template {
|
||||
let mut context = Context::new();
|
||||
context.insert("back", &Some("/settings".to_string()));
|
||||
context.insert("title", &Some("Administrator Settings".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::render("settings/admin/menu", &context.into_json())
|
||||
}
|
||||
|
||||
// HELPERS AND ROUTES FOR /settings/admin/configure
|
||||
|
||||
/// View and delete currently configured admin.
|
||||
#[get("/configure")]
|
||||
pub fn configure_admin(flash: Option<FlashMessage>, _auth: Authenticated) -> Template {
|
||||
let mut context = Context::new();
|
||||
context.insert("back", &Some("/settings/admin".to_string()));
|
||||
context.insert("title", &Some("Configure 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()));
|
||||
};
|
||||
|
||||
// load the peach configuration vector
|
||||
match config_manager::load_peach_config() {
|
||||
Ok(config) => {
|
||||
// retrieve the vector of ssb admin ids
|
||||
let ssb_admin_ids = config.ssb_admin_ids;
|
||||
context.insert("ssb_admin_ids", &ssb_admin_ids);
|
||||
}
|
||||
// if load fails, overwrite the flash_name and flash_msg
|
||||
Err(e) => {
|
||||
context.insert("flash_name", &Some("error".to_string()));
|
||||
context.insert(
|
||||
"flash_msg",
|
||||
&Some(format!("Failed to load Peach config: {}", e)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Template::render("settings/admin/configure_admin", &context.into_json())
|
||||
}
|
||||
|
||||
// HELPERS AND ROUTES FOR /settings/admin/add
|
||||
|
||||
#[derive(Debug, Deserialize, 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 {
|
||||
let mut context = Context::new();
|
||||
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"),
|
||||
}
|
||||
}
|
||||
|
||||
// HELPERS AND ROUTES FOR /settings/admin/delete
|
||||
|
||||
#[derive(Debug, Deserialize, FromForm)]
|
||||
pub struct DeleteAdminForm {
|
||||
pub ssb_id: String,
|
||||
}
|
||||
|
||||
#[post("/delete", data = "<delete_admin_form>")]
|
||||
pub fn delete_admin_post(
|
||||
delete_admin_form: Form<DeleteAdminForm>,
|
||||
_auth: Authenticated,
|
||||
) -> Flash<Redirect> {
|
||||
let result = config_manager::delete_ssb_admin_id(&delete_admin_form.ssb_id);
|
||||
let url = uri!(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"),
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
use peach_lib::config_manager;
|
||||
use rouille::{post_input, try_or_400, Request, Response};
|
||||
|
||||
use crate::utils::flash::FlashResponse;
|
||||
|
||||
// HELPER AND ROUTES FOR /settings/admin/add
|
||||
|
||||
/// Parse an `admin_id` from the submitted form, save it to file
|
||||
/// (`/var/lib/peachcloud/config.yml`) and redirect to the administrator
|
||||
/// configuration URL.
|
||||
pub fn handle_form(request: &Request) -> Response {
|
||||
// query the request body for form data
|
||||
// return a 400 error if the admin_id field is missing
|
||||
let data = try_or_400!(post_input!(request, {
|
||||
// the public key of a desired administrator
|
||||
ssb_id: String,
|
||||
}));
|
||||
|
||||
// TODO: verify that the given ssb_id is valid
|
||||
|
||||
// save submitted admin id to file
|
||||
let (flash_name, flash_msg) = match config_manager::add_ssb_admin_id(&data.ssb_id) {
|
||||
Ok(_) => (
|
||||
"flash_name=success".to_string(),
|
||||
"flash_msg=Added SSB administrator".to_string(),
|
||||
),
|
||||
Err(err) => (
|
||||
"flash_name=error".to_string(),
|
||||
format!("flash_msg=Failed to add new administrator: {}", err),
|
||||
),
|
||||
};
|
||||
|
||||
// redirect to the configure admin page
|
||||
Response::redirect_303("/settings/admin/configure").add_flash(flash_name, flash_msg)
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
use maud::{html, PreEscaped};
|
||||
use peach_lib::config_manager;
|
||||
use rouille::Request;
|
||||
|
||||
use crate::{
|
||||
templates,
|
||||
utils::{flash::FlashRequest, theme},
|
||||
};
|
||||
|
||||
/// Administrator settings menu template builder.
|
||||
pub fn build_template(request: &Request) -> PreEscaped<String> {
|
||||
// check for flash cookies; will be (None, None) if no flash cookies are found
|
||||
let (mut flash_name, mut flash_msg) = request.retrieve_flash();
|
||||
|
||||
// attempt to load peachcloud config file
|
||||
let ssb_admins = match config_manager::get_ssb_admin_ids() {
|
||||
Ok(ssb_admin_ids) => Some(ssb_admin_ids),
|
||||
// note: this will overwrite any received flash cookie values
|
||||
// TODO: find a way to include the `err` in the flash_msg
|
||||
// currently produces an error because we end up with Some(String)
|
||||
// instead of Some(str)
|
||||
Err(_err) => {
|
||||
flash_name = Some("flash_name=error");
|
||||
flash_msg = Some("flash_msg=Failed to read PeachCloud configuration file");
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
let menu_template = html! {
|
||||
(PreEscaped("<!-- CONFIGURE ADMIN PAGE -->"))
|
||||
div class="card center" {
|
||||
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."
|
||||
}
|
||||
@if let Some(ref ssb_admin_ids) = ssb_admins {
|
||||
@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) }
|
||||
input style="width: 30%;" type="submit" class="button button-warning" value="Delete" title="Delete SSB administrator";
|
||||
}
|
||||
}
|
||||
}
|
||||
} @else {
|
||||
div class="card-text" {
|
||||
"There are no currently configured admins."
|
||||
}
|
||||
}
|
||||
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" }
|
||||
input type="text" id="publicKey" name="ssb_id" placeholder="@xYz...=.ed25519" autofocus;
|
||||
}
|
||||
(PreEscaped("<!-- BUTTONS -->"))
|
||||
input class="button button-primary center" type="submit" title="Add SSB administrator" value="Add Admin";
|
||||
}
|
||||
// render flash message if cookies were found in the request
|
||||
@if let (Some(name), Some(msg)) = (flash_name, flash_msg) {
|
||||
(PreEscaped("<!-- FLASH MESSAGE -->"))
|
||||
(templates::flash::build_template(name, msg))
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// wrap the nav bars around the settings menu template content
|
||||
// parameters are template, title and back url
|
||||
let body = templates::nav::build_template(
|
||||
menu_template,
|
||||
"Configure Administrators",
|
||||
Some("/settings/admin"),
|
||||
);
|
||||
|
||||
// query the current theme so we can pass it into the base template builder
|
||||
let theme = theme::get_theme();
|
||||
|
||||
// render the base template with the provided body
|
||||
templates::base::build_template(body, theme)
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
use peach_lib::config_manager;
|
||||
use rouille::{post_input, try_or_400, Request, Response};
|
||||
|
||||
use crate::utils::flash::FlashResponse;
|
||||
|
||||
// HELPERS AND ROUTES FOR /settings/admin/delete
|
||||
|
||||
/// Parse an `admin_id` from the submitted form, delete it from file
|
||||
/// (`/var/lib/peachcloud/config.yml`) and redirect to the administrator
|
||||
/// configuration URL.
|
||||
pub fn handle_form(request: &Request) -> Response {
|
||||
// query the request body for form data
|
||||
// return a 400 error if the admin_id field is missing
|
||||
let data = try_or_400!(post_input!(request, {
|
||||
// the public key of a desired administrator
|
||||
ssb_id: String,
|
||||
}));
|
||||
|
||||
// remove submitted admin id from file
|
||||
// match on the result and set flash name and msg accordingly
|
||||
let (flash_name, flash_msg) = match config_manager::delete_ssb_admin_id(&data.ssb_id) {
|
||||
Ok(_) => (
|
||||
// <cookie-name>=<cookie-value>
|
||||
"flash_name=success".to_string(),
|
||||
"flash_msg=Removed SSB administrator".to_string(),
|
||||
),
|
||||
Err(err) => (
|
||||
"flash_name=error".to_string(),
|
||||
format!("flash_msg=Failed to remove administrator: {}", err),
|
||||
),
|
||||
};
|
||||
|
||||
// set the flash cookie headers and redirect to the configure admin page
|
||||
Response::redirect_303("/settings/admin/configure").add_flash(flash_name, flash_msg)
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
use maud::{html, PreEscaped};
|
||||
|
||||
use crate::{templates, utils::theme};
|
||||
|
||||
/// Administrator settings menu template builder.
|
||||
pub fn build_template() -> PreEscaped<String> {
|
||||
let menu_template = html! {
|
||||
(PreEscaped("<!-- ADMIN SETTINGS MENU -->"))
|
||||
div class="card center" {
|
||||
(PreEscaped("<!-- BUTTONS -->"))
|
||||
div id="settingsButtons" {
|
||||
a id="configure" class="button button-primary center" href="/settings/admin/configure" title="Configure Admin" { "Configure Admin" }
|
||||
a id="change" class="button button-primary center" href="/auth/change" title="Change Password" { "Change Password" }
|
||||
a id="reset" class="button button-primary center" href="/auth/reset" title="Reset Password" { "Reset Password" }
|
||||
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// wrap the nav bars around the settings menu template content
|
||||
// parameters are template, title and back url
|
||||
let body =
|
||||
templates::nav::build_template(menu_template, "Administrator Settings", Some("/settings"));
|
||||
|
||||
// query the current theme so we can pass it into the base template builder
|
||||
let theme = theme::get_theme();
|
||||
|
||||
// render the base template with the provided body
|
||||
templates::base::build_template(body, theme)
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
pub mod add;
|
||||
pub mod configure;
|
||||
pub mod delete;
|
||||
pub mod menu;
|
|
@ -3,7 +3,6 @@ use rocket::{
|
|||
form::{Form, FromForm},
|
||||
get, post,
|
||||
request::FlashMessage,
|
||||
serde::Deserialize,
|
||||
};
|
||||
use rocket_dyn_templates::Template;
|
||||
|
||||
|
@ -16,9 +15,10 @@ use peach_lib::{
|
|||
|
||||
use crate::{
|
||||
context::dns::ConfigureDNSContext, error::PeachWebError, routes::authentication::Authenticated,
|
||||
utils,
|
||||
};
|
||||
|
||||
#[derive(Debug, Deserialize, FromForm)]
|
||||
#[derive(Debug, FromForm)]
|
||||
pub struct DnsForm {
|
||||
pub external_domain: String,
|
||||
pub enable_dyndns: bool,
|
||||
|
@ -76,11 +76,14 @@ pub fn save_dns_configuration(dns_form: DnsForm) -> Result<(), PeachWebError> {
|
|||
|
||||
#[get("/dns")]
|
||||
pub fn configure_dns(flash: Option<FlashMessage>, _auth: Authenticated) -> Template {
|
||||
let mut context = ConfigureDNSContext::build();
|
||||
// retrieve current ui theme
|
||||
let theme = utils::get_theme();
|
||||
|
||||
let mut context = ConfigureDNSContext::build();
|
||||
// set back icon link to network route
|
||||
context.back = Some("/settings/network".to_string());
|
||||
context.title = Some("Configure DNS".to_string());
|
||||
context.theme = Some(theme);
|
||||
|
||||
// check to see if there is a flash message to display
|
||||
if let Some(flash) = flash {
|
||||
|
|
|
@ -1,22 +1,35 @@
|
|||
use rocket::{get, request::FlashMessage};
|
||||
use rocket_dyn_templates::{tera::Context, Template};
|
||||
use maud::{html, PreEscaped};
|
||||
|
||||
use crate::routes::authentication::Authenticated;
|
||||
use crate::{templates, utils::theme, SERVER_CONFIG};
|
||||
|
||||
// HELPERS AND ROUTES FOR /settings
|
||||
// ROUTE: /settings
|
||||
|
||||
/// View and delete currently configured admin.
|
||||
#[get("/settings")]
|
||||
pub fn settings_menu(flash: Option<FlashMessage>, _auth: Authenticated) -> Template {
|
||||
let mut context = Context::new();
|
||||
context.insert("back", &Some("/".to_string()));
|
||||
context.insert("title", &Some("Settings".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()));
|
||||
/// Settings menu template builder.
|
||||
pub fn build_template() -> PreEscaped<String> {
|
||||
let menu_template = html! {
|
||||
(PreEscaped("<!-- SETTINGS MENU -->"))
|
||||
div class="card center" {
|
||||
(PreEscaped("<!-- BUTTONS -->"))
|
||||
div id="settingsButtons" {
|
||||
// render the network settings and power menu buttons if we're
|
||||
// not in standalone mode
|
||||
@if !SERVER_CONFIG.standalone_mode {
|
||||
a id="power" class="button button-primary center" href="/settings/power" title="Power Menu" { "Power" }
|
||||
a id="network" class="button button-primary center" href="/settings/network" title="Network Settings" { "Network" }
|
||||
}
|
||||
a id="scuttlebutt" class="button button-primary center" href="/settings/scuttlebutt" title="Scuttlebutt Settings" { "Scuttlebutt" }
|
||||
a id="admin" class="button button-primary center" href="/settings/admin" title="Administrator Settings" { "Administration" }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Template::render("settings/menu", &context.into_json())
|
||||
// wrap the nav bars around the settings menu template content
|
||||
// parameters are template, title and back url
|
||||
let body = templates::nav::build_template(menu_template, "Settings", Some("/"));
|
||||
|
||||
// query the current theme so we can pass it into the base template builder
|
||||
let theme = theme::get_theme();
|
||||
|
||||
// render the base template with the provided body
|
||||
templates::base::build_template(body, theme)
|
||||
}
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
pub mod admin;
|
||||
pub mod dns;
|
||||
//pub mod dns;
|
||||
pub mod menu;
|
||||
pub mod network;
|
||||
pub mod power;
|
||||
pub mod scuttlebutt;
|
||||
pub mod theme;
|
||||
|
|
|
@ -1,323 +0,0 @@
|
|||
use log::{debug, warn};
|
||||
use rocket::{
|
||||
form::{Form, FromForm},
|
||||
get, post,
|
||||
request::FlashMessage,
|
||||
response::{Flash, Redirect},
|
||||
serde::Deserialize,
|
||||
uri, UriDisplayQuery,
|
||||
};
|
||||
use rocket_dyn_templates::{tera::Context, Template};
|
||||
|
||||
use peach_network::network;
|
||||
|
||||
use crate::{
|
||||
context,
|
||||
context::network::{NetworkAlertContext, NetworkDetailContext, NetworkListContext},
|
||||
routes::authentication::Authenticated,
|
||||
utils::{monitor, monitor::Threshold},
|
||||
AP_IFACE, WLAN_IFACE,
|
||||
};
|
||||
|
||||
// STRUCTS USED BY NETWORK ROUTES
|
||||
|
||||
#[derive(Debug, Deserialize, FromForm, UriDisplayQuery)]
|
||||
pub struct Ssid {
|
||||
pub ssid: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, FromForm)]
|
||||
pub struct WiFi {
|
||||
pub ssid: String,
|
||||
pub pass: String,
|
||||
}
|
||||
|
||||
// HELPERS AND ROUTES FOR /settings/network/wifi/usage/reset
|
||||
|
||||
#[get("/wifi/usage/reset")]
|
||||
pub fn wifi_usage_reset(_auth: Authenticated) -> Flash<Redirect> {
|
||||
let url = uri!(wifi_usage);
|
||||
match monitor::reset_data() {
|
||||
Ok(_) => Flash::success(Redirect::to(url), "Reset stored network traffic total"),
|
||||
Err(_) => Flash::error(
|
||||
Redirect::to(url),
|
||||
"Failed to reset stored network traffic total",
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
#[post("/wifi/connect", data = "<network>")]
|
||||
pub fn connect_wifi(network: Form<Ssid>, _auth: Authenticated) -> Flash<Redirect> {
|
||||
let ssid = &network.ssid;
|
||||
let url = uri!(network_detail(ssid = ssid));
|
||||
match network::id(&*WLAN_IFACE, ssid) {
|
||||
Ok(Some(id)) => match network::connect(&id, &*WLAN_IFACE) {
|
||||
Ok(_) => Flash::success(Redirect::to(url), "Connected to chosen network"),
|
||||
Err(_) => Flash::error(Redirect::to(url), "Failed to connect to chosen network"),
|
||||
},
|
||||
_ => Flash::error(Redirect::to(url), "Failed to retrieve the network ID"),
|
||||
}
|
||||
}
|
||||
|
||||
#[post("/wifi/disconnect", data = "<network>")]
|
||||
pub fn disconnect_wifi(network: Form<Ssid>, _auth: Authenticated) -> Flash<Redirect> {
|
||||
let ssid = &network.ssid;
|
||||
let url = uri!(network_home);
|
||||
match network::disable(&*WLAN_IFACE, ssid) {
|
||||
Ok(_) => Flash::success(Redirect::to(url), "Disconnected from WiFi network"),
|
||||
Err(_) => Flash::error(Redirect::to(url), "Failed to disconnect from WiFi network"),
|
||||
}
|
||||
}
|
||||
|
||||
#[post("/wifi/forget", data = "<network>")]
|
||||
pub fn forget_wifi(network: Form<Ssid>, _auth: Authenticated) -> Flash<Redirect> {
|
||||
let ssid = &network.ssid;
|
||||
let url = uri!(network_home);
|
||||
match network::forget(&*WLAN_IFACE, ssid) {
|
||||
Ok(_) => Flash::success(Redirect::to(url), "WiFi credentials removed"),
|
||||
Err(_) => Flash::error(
|
||||
Redirect::to(url),
|
||||
"Failed to remove WiFi credentials".to_string(),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/wifi/modify?<ssid>")]
|
||||
pub fn wifi_password(ssid: &str, flash: Option<FlashMessage>, _auth: Authenticated) -> Template {
|
||||
let mut context = Context::new();
|
||||
context.insert("back", &Some("/settings/network/wifi".to_string()));
|
||||
context.insert("title", &Some("Update WiFi Password".to_string()));
|
||||
context.insert("selected", &Some(ssid.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("settings/network/modify_ap", &context.into_json())
|
||||
}
|
||||
|
||||
#[post("/wifi/modify", data = "<wifi>")]
|
||||
pub fn wifi_set_password(wifi: Form<WiFi>, _auth: Authenticated) -> Flash<Redirect> {
|
||||
let ssid = &wifi.ssid;
|
||||
let pass = &wifi.pass;
|
||||
let url = uri!(network_detail(ssid = ssid));
|
||||
match network::update(&*WLAN_IFACE, ssid, pass) {
|
||||
Ok(_) => Flash::success(Redirect::to(url), "WiFi password updated".to_string()),
|
||||
Err(_) => Flash::error(
|
||||
Redirect::to(url),
|
||||
"Failed to update WiFi password".to_string(),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
// HELPERS AND ROUTES FOR /settings/network
|
||||
|
||||
#[get("/")]
|
||||
pub fn network_home(flash: Option<FlashMessage>, _auth: Authenticated) -> Template {
|
||||
// assign context
|
||||
let mut context = Context::new();
|
||||
context.insert("back", &Some("/settings"));
|
||||
context.insert("title", &Some("Network Configuration"));
|
||||
context.insert("ap_state", &context::network::ap_state());
|
||||
|
||||
// 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_dir is set in Rocket.toml
|
||||
Template::render("settings/network/menu", &context.into_json())
|
||||
}
|
||||
|
||||
// HELPERS AND ROUTES FOR /settings/network/ap/activate
|
||||
|
||||
#[get("/ap/activate")]
|
||||
pub fn deploy_ap(_auth: Authenticated) -> Flash<Redirect> {
|
||||
// activate the wireless access point
|
||||
debug!("Activating WiFi access point.");
|
||||
match network::start_iface_service(&*AP_IFACE) {
|
||||
Ok(_) => Flash::success(
|
||||
Redirect::to("/settings/network"),
|
||||
"Activated WiFi access point",
|
||||
),
|
||||
Err(_) => Flash::error(
|
||||
Redirect::to("/settings/network"),
|
||||
"Failed to activate WiFi access point",
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
// HELPERS AND ROUTES FOR /settings/network/wifi
|
||||
|
||||
#[get("/wifi")]
|
||||
pub fn wifi_list(flash: Option<FlashMessage>, _auth: Authenticated) -> Template {
|
||||
// assign context through context_builder call
|
||||
let mut context = NetworkListContext::build();
|
||||
context.back = Some("/settings/network".to_string());
|
||||
context.title = Some("WiFi Networks".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());
|
||||
};
|
||||
|
||||
Template::render("settings/network/list_aps", &context)
|
||||
}
|
||||
|
||||
// HELPERS AND ROUTES FOR /settings/network/wifi<ssid>
|
||||
|
||||
#[get("/wifi?<ssid>")]
|
||||
pub fn network_detail(ssid: &str, flash: Option<FlashMessage>, _auth: Authenticated) -> Template {
|
||||
let mut context = NetworkDetailContext::build();
|
||||
context.back = Some("/settings/network/wifi".to_string());
|
||||
context.title = Some("WiFi Network".to_string());
|
||||
context.selected = Some(ssid.to_string());
|
||||
|
||||
if let Some(flash) = flash {
|
||||
context.flash_name = Some(flash.kind().to_string());
|
||||
context.flash_msg = Some(flash.message().to_string());
|
||||
};
|
||||
|
||||
Template::render("settings/network/ap_details", &context)
|
||||
}
|
||||
|
||||
// HELPERS AND ROUTES FOR /settings/network/wifi/activate
|
||||
|
||||
#[get("/wifi/activate")]
|
||||
pub fn deploy_client(_auth: Authenticated) -> Flash<Redirect> {
|
||||
// activate the wireless client
|
||||
debug!("Activating WiFi client mode.");
|
||||
match network::start_iface_service(&*WLAN_IFACE) {
|
||||
Ok(_) => Flash::success(Redirect::to("/settings/network"), "Activated WiFi client"),
|
||||
Err(_) => Flash::error(
|
||||
Redirect::to("/settings/network"),
|
||||
"Failed to activate WiFi client",
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
// HELPERS AND ROUTES FOR /settings/network/wifi/add
|
||||
|
||||
#[get("/wifi/add")]
|
||||
pub fn add_wifi(flash: Option<FlashMessage>, _auth: Authenticated) -> Template {
|
||||
let mut context = Context::new();
|
||||
context.insert("back", &Some("/settings/network".to_string()));
|
||||
context.insert("title", &Some("Add WiFi Network".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("settings/network/add_ap", &context.into_json())
|
||||
}
|
||||
|
||||
#[get("/wifi/add?<ssid>")]
|
||||
pub fn add_ssid(ssid: &str, flash: Option<FlashMessage>, _auth: Authenticated) -> Template {
|
||||
let mut context = Context::new();
|
||||
context.insert("back", &Some("/settings/network".to_string()));
|
||||
context.insert("title", &Some("Add WiFi Network".to_string()));
|
||||
context.insert("selected", &Some(ssid.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("settings/network/add_ap", &context.into_json())
|
||||
}
|
||||
|
||||
#[post("/wifi/add", data = "<wifi>")]
|
||||
pub fn add_credentials(wifi: Form<WiFi>, _auth: Authenticated) -> Template {
|
||||
let mut context = Context::new();
|
||||
context.insert("back", &Some("/settings/network".to_string()));
|
||||
context.insert("title", &Some("Add WiFi Network".to_string()));
|
||||
|
||||
// check if the credentials already exist for this access point
|
||||
// note: this is nicer but it's an unstable feature:
|
||||
// if check_saved_aps(&wifi.ssid).contains(true)
|
||||
// use unwrap_or instead, set value to false if err is returned
|
||||
//let creds_exist = network::saved_networks(&wifi.ssid).unwrap_or(false);
|
||||
let creds_exist = match network::saved_networks() {
|
||||
Ok(Some(networks)) => networks.contains(&wifi.ssid),
|
||||
_ => false,
|
||||
};
|
||||
|
||||
// if credentials not found, generate and write wifi config to wpa_supplicant
|
||||
let (flash_name, flash_msg) = if creds_exist {
|
||||
(
|
||||
"error".to_string(),
|
||||
"Network credentials already exist for this access point".to_string(),
|
||||
)
|
||||
} else {
|
||||
match network::add(&*WLAN_IFACE, &wifi.ssid, &wifi.pass) {
|
||||
Ok(_) => {
|
||||
debug!("Added WiFi credentials.");
|
||||
// force reread of wpa_supplicant.conf file with new credentials
|
||||
match network::reconfigure() {
|
||||
Ok(_) => debug!("Successfully reconfigured wpa_supplicant"),
|
||||
Err(_) => warn!("Failed to reconfigure wpa_supplicant"),
|
||||
}
|
||||
("success".to_string(), "Added WiFi credentials".to_string())
|
||||
}
|
||||
Err(e) => {
|
||||
debug!("Failed to add WiFi credentials.");
|
||||
("error".to_string(), format!("{}", e))
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
context.insert("flash_name", &Some(flash_name));
|
||||
context.insert("flash_msg", &Some(flash_msg));
|
||||
|
||||
Template::render("settings/network/add_ap", &context.into_json())
|
||||
}
|
||||
|
||||
// HELPERS AND ROUTES FOR WIFI USAGE
|
||||
|
||||
#[get("/wifi/usage")]
|
||||
pub fn wifi_usage(flash: Option<FlashMessage>, _auth: Authenticated) -> Template {
|
||||
let mut context = NetworkAlertContext::build();
|
||||
// set back icon link to network route
|
||||
context.back = Some("/settings/network".to_string());
|
||||
context.title = Some("Network Data Usage".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());
|
||||
};
|
||||
// template_dir is set in Rocket.toml
|
||||
Template::render("settings/network/data_usage_limits", &context)
|
||||
}
|
||||
|
||||
#[post("/wifi/usage", data = "<thresholds>")]
|
||||
pub fn wifi_usage_alerts(thresholds: Form<Threshold>, _auth: Authenticated) -> Flash<Redirect> {
|
||||
match monitor::update_store(thresholds.into_inner()) {
|
||||
Ok(_) => {
|
||||
debug!("WiFi data usage thresholds updated.");
|
||||
Flash::success(
|
||||
Redirect::to("/settings/network/wifi/usage"),
|
||||
"Updated alert thresholds and flags",
|
||||
)
|
||||
}
|
||||
Err(_) => {
|
||||
warn!("Failed to update WiFi data usage thresholds.");
|
||||
Flash::error(
|
||||
Redirect::to("/settings/network/wifi/usage"),
|
||||
"Failed to update alert thresholds and flags",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,97 @@
|
|||
use maud::{html, Markup, PreEscaped};
|
||||
use peach_network::network;
|
||||
use rouille::{post_input, try_or_400, Request, Response};
|
||||
|
||||
use crate::{
|
||||
templates,
|
||||
utils::{
|
||||
flash::{FlashRequest, FlashResponse},
|
||||
theme,
|
||||
},
|
||||
WLAN_IFACE,
|
||||
};
|
||||
|
||||
// ROUTE: /settings/network/wifi/add
|
||||
|
||||
fn render_ssid_input(selected_ap: Option<String>) -> Markup {
|
||||
html! {
|
||||
(PreEscaped("<!-- input for network ssid -->"))
|
||||
input id="ssid" name="ssid" class="center input" type="text" placeholder="SSID" title="Network name (SSID) for WiFi access point" value=[selected_ap] autofocus;
|
||||
}
|
||||
}
|
||||
|
||||
fn render_password_input() -> Markup {
|
||||
html! {
|
||||
(PreEscaped("<!-- input for network password -->"))
|
||||
input id="pass" name="pass" class="center input" type="password" placeholder="Password" title="Password for WiFi access point";
|
||||
}
|
||||
}
|
||||
|
||||
fn render_buttons() -> Markup {
|
||||
html! {
|
||||
(PreEscaped("<!-- BUTTONS -->"))
|
||||
div id="buttons" {
|
||||
input id="addWifi" class="button button-primary center" title="Add" type="submit" value="Add";
|
||||
a class="button button-secondary center" href="/settings/network" title="Cancel" { "Cancel" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// WiFi access point credentials form template builder.
|
||||
pub fn build_template(request: &Request, selected_ap: Option<String>) -> PreEscaped<String> {
|
||||
let (flash_name, flash_msg) = request.retrieve_flash();
|
||||
|
||||
let form_template = html! {
|
||||
(PreEscaped("<!-- WIFI ADD CREDENTIALS FORM -->"))
|
||||
div class="card center" {
|
||||
form id="wifiCreds" action="/settings/network/wifi/add" method="post" {
|
||||
(render_ssid_input(selected_ap))
|
||||
(render_password_input())
|
||||
(render_buttons())
|
||||
}
|
||||
@if let (Some(name), Some(msg)) = (flash_name, flash_msg) {
|
||||
(PreEscaped("<!-- FLASH MESSAGE -->"))
|
||||
(templates::flash::build_template(name, msg))
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let body = templates::nav::build_template(
|
||||
form_template,
|
||||
"Add WiFi Network",
|
||||
Some("/settings/network"),
|
||||
);
|
||||
|
||||
let theme = theme::get_theme();
|
||||
|
||||
templates::base::build_template(body, theme)
|
||||
}
|
||||
|
||||
/// Parse the SSID and password for an access point and save the new credentials.
|
||||
pub fn handle_form(request: &Request) -> Response {
|
||||
let data = try_or_400!(post_input!(request, {
|
||||
ssid: String,
|
||||
pass: String,
|
||||
}));
|
||||
|
||||
let (name, msg) = match network::add(WLAN_IFACE, &data.ssid, &data.pass) {
|
||||
Ok(_) => match network::reconfigure() {
|
||||
Ok(_) => ("success".to_string(), "Added WiFi credentials".to_string()),
|
||||
Err(err) => (
|
||||
"error".to_string(),
|
||||
format!(
|
||||
"Added WiFi credentials but failed to reconfigure interface: {}",
|
||||
err
|
||||
),
|
||||
),
|
||||
},
|
||||
Err(err) => (
|
||||
"error".to_string(),
|
||||
format!("Failed to add WiFi credentials for {}: {}", &data.ssid, err),
|
||||
),
|
||||
};
|
||||
|
||||
let (flash_name, flash_msg) = (format!("flash_name={}", name), format!("flash_msg={}", msg));
|
||||
|
||||
Response::redirect_303("/settings/network/wifi/add").add_flash(flash_name, flash_msg)
|
||||
}
|
|
@ -0,0 +1,197 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use maud::{html, Markup, PreEscaped};
|
||||
use peach_network::{network, network::AccessPoint, NetworkError};
|
||||
use rouille::Request;
|
||||
|
||||
use crate::{
|
||||
templates,
|
||||
utils::{flash::FlashRequest, theme},
|
||||
WLAN_IFACE,
|
||||
};
|
||||
|
||||
// ROUTE: /settings/network/wifi?<ssid>
|
||||
|
||||
fn render_network_status_icon(ssid: &str, wlan_ssid: &str, ap_state: &str) -> Markup {
|
||||
let status_label_value = if ssid == wlan_ssid {
|
||||
"CONNECTED"
|
||||
} else if ap_state == "Available" {
|
||||
"AVAILABLE"
|
||||
} else {
|
||||
"NOT IN RANGE"
|
||||
};
|
||||
|
||||
html! {
|
||||
(PreEscaped("<!-- NETWORK STATUS ICON -->"))
|
||||
div class="grid-column-1" {
|
||||
img id="wifiIcon" class="center icon" src="/icons/wifi.svg" alt="WiFi icon";
|
||||
label class="center label-small font-gray" for="wifiIcon" title="Access Point Status" { (status_label_value) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn render_network_detailed_info(ssid: &str, ap_protocol: &str, ap_signal: Option<i32>) -> Markup {
|
||||
let ap_signal_value = match ap_signal {
|
||||
Some(signal) => signal.to_string(),
|
||||
None => "Unknown".to_string(),
|
||||
};
|
||||
|
||||
html! {
|
||||
(PreEscaped("<!-- NETWORK DETAILED INFO -->"))
|
||||
div class="grid-column-2" {
|
||||
label class="label-small font-gray" for="netSsid" title="WiFi network SSID" { "SSID" };
|
||||
p id="netSsid" class="card-text" title="SSID" { (ssid) }
|
||||
label class="label-small font-gray" for="netSec" title="Security protocol" { "SECURITY" };
|
||||
p id="netSec" class="card-text" title={ "Security protocol in use by " (ssid) } { (ap_protocol) }
|
||||
label class="label-small font-gray" for="netSig" title="Signal Strength" { "SIGNAL" };
|
||||
p id="netSig" class="card-text" title="Signal strength of WiFi access point" { (ap_signal_value) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn render_disconnect_form(ssid: &str) -> Markup {
|
||||
html! {
|
||||
form id="wifiDisconnect" action="/settings/network/wifi/disconnect" method="post" {
|
||||
(PreEscaped("<!-- hidden element: allows ssid to be sent in request -->"))
|
||||
input id="disconnectSsid" name="ssid" type="text" value=(ssid) style="display: none;";
|
||||
input id="disconnectWifi" class="button button-warning center" title="Disconnect from Network" type="submit" value="Disconnect";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn render_connect_form(ssid: &str) -> Markup {
|
||||
html! {
|
||||
form id="wifiConnect" action="/settings/network/wifi/connect" method="post" {
|
||||
(PreEscaped("<!-- hidden element: allows ssid to be sent in request -->"))
|
||||
input id="connectSsid" name="ssid" type="text" value=(ssid) style="display: none;";
|
||||
input id="connectWifi" class="button button-primary center" title="Connect to Network" type="submit" value="Connect";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn render_forget_form(ssid: &str) -> Markup {
|
||||
html! {
|
||||
form id="wifiForget" action="/settings/network/wifi/forget" method="post" {
|
||||
(PreEscaped("<!-- hidden element: allows ssid to be sent in request -->"))
|
||||
input id="forgetSsid" name="ssid" type="text" value=(ssid) style="display: none;";
|
||||
input id="forgetWifi" class="button button-warning center" title="Forget Network" type="submit" value="Forget";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn render_buttons(
|
||||
selected_ap: &str,
|
||||
wlan_ssid: &str,
|
||||
ap: &AccessPoint,
|
||||
saved_wifi_networks: Vec<String>,
|
||||
) -> Markup {
|
||||
html! {
|
||||
(PreEscaped("<!-- BUTTONS -->"))
|
||||
div id="buttons" {
|
||||
@if wlan_ssid == selected_ap {
|
||||
(render_disconnect_form(selected_ap))
|
||||
}
|
||||
@if saved_wifi_networks.contains(&selected_ap.to_string()) {
|
||||
@if wlan_ssid != selected_ap && ap.state == "Available" {
|
||||
(render_connect_form(selected_ap))
|
||||
}
|
||||
a class="button button-primary center" href={ "/settings/network/wifi/modify?ssid=" (selected_ap) } { "Modify" }
|
||||
(render_forget_form(selected_ap))
|
||||
} @else {
|
||||
// display the Add button if AP creds not already in saved
|
||||
// networks list
|
||||
a class="button button-primary center" href={ "/settings/network/wifi/add?ssid=" (selected_ap) } { "Add" }
|
||||
}
|
||||
a class="button button-secondary center" href="/settings/network/wifi" title="Cancel" { "Cancel" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Retrieve the list of all saved and in-range networks (including SSID and
|
||||
/// AP details for each network), the list of all saved networks (SSIDs only)
|
||||
/// and the SSID for the WiFi interface.
|
||||
fn retrieve_network_data() -> (
|
||||
Result<HashMap<String, AccessPoint>, NetworkError>,
|
||||
Vec<String>,
|
||||
String,
|
||||
) {
|
||||
let all_wifi_networks = network::all_networks(WLAN_IFACE);
|
||||
let saved_wifi_networks = match network::saved_networks() {
|
||||
Ok(Some(ssids)) => ssids,
|
||||
_ => Vec::new(),
|
||||
};
|
||||
let wlan_ssid = match network::ssid(WLAN_IFACE) {
|
||||
Ok(Some(ssid)) => ssid,
|
||||
_ => String::from("Not connected"),
|
||||
};
|
||||
|
||||
(all_wifi_networks, saved_wifi_networks, wlan_ssid)
|
||||
}
|
||||
|
||||
/// WiFi access point (AP) template builder.
|
||||
///
|
||||
/// Render a UI card with details about the selected access point, including
|
||||
/// the connection state, security protocol being used, the SSID and the
|
||||
/// signal strength. Buttons are also rendering based on the state of the
|
||||
/// access point and whether or not credentials for the AP have previously
|
||||
/// been saved.
|
||||
///
|
||||
/// If the AP is available (ie. in-range) then a Connect button is rendered.
|
||||
/// A Disconnect button is rendered if the WiFi client is currently
|
||||
/// connected to the AP.
|
||||
///
|
||||
/// If credentials have not previously been saved for the AP, an Add button is
|
||||
/// rendered. Forget and Modify buttons are rendered if credentials for the AP
|
||||
/// have previously been saved.
|
||||
pub fn build_template(request: &Request, selected_ap: String) -> PreEscaped<String> {
|
||||
let (flash_name, flash_msg) = request.retrieve_flash();
|
||||
|
||||
let (all_wifi_networks, saved_wifi_networks, wlan_ssid) = retrieve_network_data();
|
||||
|
||||
let network_info_box_class = if selected_ap == wlan_ssid {
|
||||
"two-grid capsule success-border"
|
||||
} else {
|
||||
"two-grid capsule"
|
||||
};
|
||||
|
||||
let network_list_template = html! {
|
||||
(PreEscaped("<!-- NETWORK CARD -->"))
|
||||
div class="card center" {
|
||||
@if let Ok(wlan_networks) = all_wifi_networks {
|
||||
// select only the access point we are interested in displaying
|
||||
@if let Some((ssid, ap)) = wlan_networks.get_key_value(&selected_ap) {
|
||||
@let ap_protocol = match &ap.detail {
|
||||
Some(detail) => detail.protocol.clone(),
|
||||
None => "None".to_string()
|
||||
};
|
||||
(PreEscaped("<!-- NETWORK INFO BOX -->"))
|
||||
div class=(network_info_box_class) title="PeachCloud network mode and status" {
|
||||
(PreEscaped("<!-- left column -->"))
|
||||
(render_network_status_icon(ssid, &wlan_ssid, &ap.state))
|
||||
(PreEscaped("<!-- right column -->"))
|
||||
(render_network_detailed_info(ssid, &ap_protocol, ap.signal))
|
||||
}
|
||||
(render_buttons(ssid, &wlan_ssid, ap, saved_wifi_networks))
|
||||
} @else {
|
||||
p class="card-text list-item" { (selected_ap) " not found in saved or in-range networks" }
|
||||
}
|
||||
} @else {
|
||||
p class="card-text list-item" { "No saved or in-range networks found" }
|
||||
}
|
||||
@if let (Some(name), Some(msg)) = (flash_name, flash_msg) {
|
||||
(PreEscaped("<!-- FLASH MESSAGE -->"))
|
||||
(templates::flash::build_template(name, msg))
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let body = templates::nav::build_template(
|
||||
network_list_template,
|
||||
"WiFi Networks",
|
||||
Some("/settings/network"),
|
||||
);
|
||||
|
||||
let theme = theme::get_theme();
|
||||
|
||||
templates::base::build_template(body, theme)
|
||||
}
|
|
@ -0,0 +1,201 @@
|
|||
use log::info;
|
||||
use maud::{html, Markup, PreEscaped};
|
||||
use peach_lib::{
|
||||
config_manager, dyndns_client,
|
||||
error::PeachError,
|
||||
jsonrpc_client_core::{Error, ErrorKind},
|
||||
jsonrpc_core::types::error::ErrorCode,
|
||||
};
|
||||
use rouille::{post_input, try_or_400, Request, Response};
|
||||
|
||||
use crate::{
|
||||
error::PeachWebError,
|
||||
templates,
|
||||
utils::{
|
||||
flash::{FlashRequest, FlashResponse},
|
||||
theme,
|
||||
},
|
||||
};
|
||||
|
||||
// ROUTE: /settings/network/dns
|
||||
|
||||
fn render_dyndns_status_indicator() -> Markup {
|
||||
let (indicator_class, indicator_label) = match dyndns_client::is_dns_updater_online() {
|
||||
Ok(true) => ("success-border", "Dynamic DNS is currently online."),
|
||||
_ => (
|
||||
"warning-border",
|
||||
"Dynamic DNS is enabled but may be offline.",
|
||||
),
|
||||
};
|
||||
|
||||
html! {
|
||||
(PreEscaped("<!-- DYNDNS STATUS INDICATOR -->"))
|
||||
div id="dyndns-status-indicator" class={ "stack capsule " (indicator_class) } {
|
||||
div class="stack" {
|
||||
label class="label-small font-near-black" { (indicator_label) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn render_external_domain_input() -> Markup {
|
||||
let external_domain = config_manager::get_config_value("EXTERNAL_DOMAIN").ok();
|
||||
|
||||
html! {
|
||||
div class="input-wrapper" {
|
||||
(PreEscaped("<!-- input for externaldomain -->"))
|
||||
label id="external_domain" class="label-small input-label font-near-black" {
|
||||
label class="label-small input-label font-gray" for="external_domain" style="padding-top: 0.25rem;" { "External Domain (optional)" }
|
||||
input id="external_domain" class="form-input" style="margin-bottom: 0;" name="external_domain" type="text" title="external domain" value=[external_domain];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn render_dyndns_enabled_checkbox() -> Markup {
|
||||
let dyndns_enabled = config_manager::get_dyndns_enabled_value().unwrap_or(false);
|
||||
|
||||
html! {
|
||||
div class="input-wrapper" {
|
||||
div {
|
||||
(PreEscaped("<!-- checkbox for dyndns flag -->"))
|
||||
label class="label-small input-label font-gray" { "Enable Dynamic DNS" }
|
||||
input style="margin-left: 0px;" id="enable_dyndns" name="enable_dyndns" title="Activate dynamic DNS" type="checkbox" checked[dyndns_enabled];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn render_dynamic_domain_input() -> Markup {
|
||||
let dyndns_domain =
|
||||
config_manager::get_config_value("DYN_DOMAIN").unwrap_or_else(|_| String::from(""));
|
||||
let dyndns_subdomain =
|
||||
dyndns_client::get_dyndns_subdomain(&dyndns_domain).unwrap_or(dyndns_domain);
|
||||
|
||||
html! {
|
||||
div class="input-wrapper" {
|
||||
(PreEscaped("<!-- input for dyndns domain -->"))
|
||||
label id="cut" class="label-small input-label font-near-black" {
|
||||
label class="label-small input-label font-gray" for="cut" style="padding-top: 0.25rem;" { "Dynamic DNS Domain" }
|
||||
input id="dyndns_domain" class="alert-input" name="dynamic_domain" placeholder="" type="text" title="dyndns_domain" value=(dyndns_subdomain);
|
||||
{ ".dyn.peachcloud.org" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn render_save_button() -> Markup {
|
||||
html! {
|
||||
div id="buttonDiv" style="margin-top: 2rem;" {
|
||||
input id="configureDNSButton" class="button button-primary center" title="Add" type="submit" value="Save";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// DNS configuration form template builder.
|
||||
pub fn build_template(request: &Request) -> PreEscaped<String> {
|
||||
let (flash_name, flash_msg) = request.retrieve_flash();
|
||||
|
||||
let dyndns_enabled = config_manager::get_dyndns_enabled_value().unwrap_or(false);
|
||||
|
||||
let form_template = html! {
|
||||
(PreEscaped("<!-- CONFIGURE DNS FORM -->"))
|
||||
div class="card center" {
|
||||
@if dyndns_enabled {
|
||||
(render_dyndns_status_indicator())
|
||||
}
|
||||
form id="configureDNS" class="center" action="/settings/network/dns" method="post" {
|
||||
(render_external_domain_input())
|
||||
(render_dyndns_enabled_checkbox())
|
||||
(render_dynamic_domain_input())
|
||||
(render_save_button())
|
||||
@if let (Some(name), Some(msg)) = (flash_name, flash_msg) {
|
||||
(PreEscaped("<!-- FLASH MESSAGE -->"))
|
||||
(templates::flash::build_template(name, msg))
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let body = templates::nav::build_template(
|
||||
form_template,
|
||||
"Configure Dynamic DNS",
|
||||
Some("/settings/network"),
|
||||
);
|
||||
|
||||
let theme = theme::get_theme();
|
||||
|
||||
templates::base::build_template(body, theme)
|
||||
}
|
||||
|
||||
pub fn save_dns_configuration(
|
||||
external_domain: String,
|
||||
enable_dyndns: bool,
|
||||
dynamic_domain: String,
|
||||
) -> Result<(), PeachWebError> {
|
||||
// first save local configurations
|
||||
config_manager::set_external_domain(&external_domain)?;
|
||||
config_manager::set_dyndns_enabled_value(enable_dyndns)?;
|
||||
|
||||
let full_dynamic_domain = dyndns_client::get_full_dynamic_domain(&dynamic_domain);
|
||||
|
||||
// if dynamic dns is enabled and this is a new domain name, then register it
|
||||
if enable_dyndns && dyndns_client::check_is_new_dyndns_domain(&full_dynamic_domain)? {
|
||||
if let Err(registration_err) = dyndns_client::register_domain(&full_dynamic_domain) {
|
||||
info!("Failed to register dyndns domain: {:?}", registration_err);
|
||||
|
||||
// error message describing the failed update
|
||||
let err_msg = match registration_err {
|
||||
PeachError::JsonRpcClientCore(Error(ErrorKind::JsonRpcError(rpc_err), _)) => {
|
||||
if let ErrorCode::ServerError(-32030) = rpc_err.code {
|
||||
format!(
|
||||
"Error registering domain: {} was previously registered",
|
||||
full_dynamic_domain
|
||||
)
|
||||
} else {
|
||||
format!("Failed to register dyndns domain: {:?}", rpc_err)
|
||||
}
|
||||
}
|
||||
_ => "Failed to register dyndns domain".to_string(),
|
||||
};
|
||||
|
||||
Err(PeachWebError::FailedToRegisterDynDomain(err_msg))
|
||||
} else {
|
||||
info!("Registered new dyndns domain");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
} else {
|
||||
info!("Domain {} already registered", dynamic_domain);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse the DNS configuration parameters and apply them.
|
||||
pub fn handle_form(request: &Request) -> Response {
|
||||
let data = try_or_400!(post_input!(request, {
|
||||
external_domain: String,
|
||||
enable_dyndns: bool,
|
||||
dynamic_domain: String,
|
||||
}));
|
||||
|
||||
let (name, msg) = match save_dns_configuration(
|
||||
data.external_domain,
|
||||
data.enable_dyndns,
|
||||
data.dynamic_domain,
|
||||
) {
|
||||
Ok(_) => (
|
||||
"success".to_string(),
|
||||
"New dynamic DNS configuration is now enabled".to_string(),
|
||||
),
|
||||
Err(err) => (
|
||||
"error".to_string(),
|
||||
format!("Failed to save DNS configuration: {}", err),
|
||||
),
|
||||
};
|
||||
|
||||
let (flash_name, flash_msg) = (format!("flash_name={}", name), format!("flash_msg={}", msg));
|
||||
|
||||
Response::redirect_303("/settings/network/dns").add_flash(flash_name, flash_msg)
|
||||
}
|
|
@ -0,0 +1,164 @@
|
|||
// TODO:
|
||||
//
|
||||
// This template and associated feature set requires vnstat_parse.
|
||||
// - https://crates.io/crates/vnstat_parse
|
||||
//
|
||||
// Use the PeachCloud config system to store warning and cutoff flags,
|
||||
// as well as the associated totals (thresholds):
|
||||
//
|
||||
// - DATA_WARNING_ENABLED
|
||||
// - DATA_WARNING_LIMIT
|
||||
// - DATA_CUTOFF_ENABLED
|
||||
// - DATA_CUTOFF_LIMIT
|
||||
|
||||
use maud::{html, Markup, PreEscaped};
|
||||
use peach_network::network;
|
||||
use rouille::Request;
|
||||
use vnstat_parse::Vnstat;
|
||||
|
||||
use crate::{
|
||||
templates,
|
||||
utils::{flash::FlashRequest, theme},
|
||||
WLAN_IFACE,
|
||||
};
|
||||
|
||||
// ROUTE: /settings/network/wifi/usage
|
||||
|
||||
fn render_data_usage_total_capsule() -> Markup {
|
||||
html! {
|
||||
div class="stack capsule" style="margin-left: 2rem; margin-right: 2rem;" {
|
||||
div class="flex-grid" {
|
||||
label id="dataTotal" class="label-large" title="Data download total in MB" {
|
||||
data_total.total / 1024 / 1024 | round
|
||||
}
|
||||
label class="label-small font-near-black" { "MB" }
|
||||
}
|
||||
label class="center-text label-small font-gray" { "USAGE TOTAL" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn render_warning_threshold_icon() -> Markup {
|
||||
// threshold.warn_flag
|
||||
let warning_enabled = true;
|
||||
|
||||
let icon_class = match warning_enabled {
|
||||
true => "icon",
|
||||
false => "icon icon-inactive",
|
||||
};
|
||||
|
||||
html! {
|
||||
div class="card-container container" {
|
||||
div {
|
||||
img id="warnIcon" class=(icon_class) alt="Warning" title="Warning threshold" src="/icons/alert.svg";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn render_warning_threshold_input() -> Markup {
|
||||
// TODO: source threshold.warn value and replace below
|
||||
|
||||
html! {
|
||||
div {
|
||||
(PreEscaped("<!-- input for warning threshold -->"))
|
||||
label id="warn" class="label-small font-near-black" {
|
||||
input id="warnInput" class="alert-input" name="warn" placeholder="0" type="text" title="Warning threshold value" value="{{ threshold.warn }}" { "MB" }
|
||||
}
|
||||
label class="label-small font-gray" for="warn" style="padding-top: 0.25rem;" { "WARNING THRESHOLD" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn render_warning_threshold_checkbox() -> Markup {
|
||||
let warning_enabled = true;
|
||||
|
||||
html! {
|
||||
div {
|
||||
(PreEscaped("<!-- checkbox for warning threshold flag -->"))
|
||||
input id="warnCheck" name="warn_flag" title="Activate warning" type="checkbox" checked[warning_enabled];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn render_critical_threshold_icon() -> Markup {
|
||||
// threshold.cut_flag
|
||||
let cutoff_enabled = true;
|
||||
|
||||
let icon_class = match cutoff_enabled {
|
||||
true => "icon",
|
||||
false => "icon icon-inactive",
|
||||
};
|
||||
|
||||
html! {
|
||||
div {
|
||||
img id="cutIcon"
|
||||
class=(icon_class)
|
||||
alt="Cutoff"
|
||||
title="Cutoff threshold"
|
||||
src="/icons/scissor.svg";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn render_critical_threshold_input() -> Markup {
|
||||
// TODO: source threshold.cut value and replace below
|
||||
|
||||
html! {
|
||||
div {
|
||||
(PreEscaped("<!-- input for cutoff threshold -->"))
|
||||
label id="cut" class="label-small font-near-black"><input id="cutInput" class="alert-input" name="cut" placeholder="0" type="text" title="Critical threshold value" value="{{ threshold.cut }}" { "MB" }
|
||||
label class="label-small font-gray" for="cut" style="padding-top: 0.25rem;" { "CUTOFF THRESHOLD" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn render_critical_threshold_checkbox() -> Markup {
|
||||
// threshold.cut_flag
|
||||
let cutoff_enabled = true;
|
||||
|
||||
html! {
|
||||
div {
|
||||
(PreEscaped("<!-- checkbox for cutoff threshold flag -->"))
|
||||
input id="cutCheck" name="cut_flag" title="Activate cutoff" type="checkbox" checked[cutoff_enabled];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn render_buttons() -> Markup {
|
||||
html! {
|
||||
div id="buttonDiv" class="button-div" {
|
||||
input id="updateAlerts" class="button button-primary center" title="Update" type="submit" value="Update";
|
||||
a id="resetTotal" class="button button-warning center" href="/settings/network/wifi/usage/reset" title="Reset stored usage total to zero" { "Reset" }
|
||||
a class="button button-secondary center" href="/settings/network" title="Cancel" { "Cancel" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// WiFi data usage form template builder.
|
||||
pub fn build_template(request: &Request) -> PreEscaped<String> {
|
||||
let (flash_name, flash_msg) = request.retrieve_flash();
|
||||
|
||||
let wlan_data = Vnstat::get(WLAN_IFACE);
|
||||
|
||||
// wlan_data.all_time_total
|
||||
// wlan_data.all_time_total_unit
|
||||
|
||||
let form_template = html! {
|
||||
(PreEscaped("<!-- NETWORK DATA ALERTS FORM -->"))
|
||||
form id="wifiAlerts" action="/network/wifi/usage" class="card center" method="post" {
|
||||
(render_data_usage_total_capsule())
|
||||
(render_warning_threshold_icon())
|
||||
(render_warning_threshold_input())
|
||||
(render_warning_threshold_checkbox())
|
||||
(render_critical_threshold_icon())
|
||||
(render_critical_threshold_input())
|
||||
(render_critical_threshold_checkbox())
|
||||
(render_buttons())
|
||||
}
|
||||
@if let (Some(name), Some(msg)) = (flash_name, flash_msg) {
|
||||
(PreEscaped("<!-- FLASH MESSAGE -->"))
|
||||
(templates::flash::build_template(name, msg))
|
||||
}
|
||||
};
|
||||
}
|
|
@ -0,0 +1,106 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use maud::{html, Markup, PreEscaped};
|
||||
use peach_network::{network, network::AccessPoint};
|
||||
|
||||
use crate::{templates, utils::theme, AP_IFACE, WLAN_IFACE};
|
||||
|
||||
// ROUTE: /settings/network/wifi
|
||||
|
||||
/// Retrieve network state data required by the WiFi network list template.
|
||||
fn get_network_state_data(ap: &str, wlan: &str) -> (String, String, HashMap<String, AccessPoint>) {
|
||||
let ap_state = match network::state(ap) {
|
||||
Ok(Some(state)) => state,
|
||||
_ => "Interface unavailable".to_string(),
|
||||
};
|
||||
|
||||
let wlan_ssid = match network::ssid(wlan) {
|
||||
Ok(Some(ssid)) => ssid,
|
||||
_ => "Not connected".to_string(),
|
||||
};
|
||||
|
||||
let network_list = match network::all_networks(wlan) {
|
||||
Ok(networks) => networks,
|
||||
Err(_) => HashMap::new(),
|
||||
};
|
||||
|
||||
(ap_state, wlan_ssid, network_list)
|
||||
}
|
||||
|
||||
fn render_network_connected_elements(ssid: String) -> Markup {
|
||||
let ap_detail_url = format!("/network/wifi?ssid={}", ssid);
|
||||
|
||||
html! {
|
||||
a class="list-item link primary-bg" href=(ap_detail_url) {
|
||||
img id="netStatus" class="icon icon-active icon-medium list-icon" src="/icons/wifi.svg" alt="WiFi online";
|
||||
p class="list-text" { (ssid) }
|
||||
label class="label-small list-label font-gray" for="netStatus" title="Status" { "Connected" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn render_network_available_elements(ssid: String, ap_state: String) -> Markup {
|
||||
let ap_detail_url = format!("/network/wifi?ssid={}", ssid);
|
||||
|
||||
html! {
|
||||
a class="list-item link light-bg" href=(ap_detail_url) {
|
||||
img id="netStatus" class="icon icon-inactive icon-medium list-icon" src="/icons/wifi.svg" alt="WiFi offline";
|
||||
p class="list-text" { (ssid) }
|
||||
label class="label-small list-label font-gray" for="netStatus" title="Status" { (ap_state) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn render_network_unavailable_elements(ssid: String, ap_state: String) -> Markup {
|
||||
let ap_detail_url = format!("/network/wifi?ssid={}", ssid);
|
||||
|
||||
html! {
|
||||
a class="list-item link" href=(ap_detail_url) {
|
||||
img id="netStatus" class="icon icon-inactive icon-medium list-icon" src="/icons/wifi.svg" alt="WiFi offline";
|
||||
p class="list-text" { (ssid) }
|
||||
label class="label-small list-label font-gray" for="netStatus" title="Status" { (ap_state) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// WiFi network list template builder.
|
||||
pub fn build_template() -> PreEscaped<String> {
|
||||
let (ap_state, wlan_ssid, network_list) = get_network_state_data(AP_IFACE, WLAN_IFACE);
|
||||
|
||||
let list_template = html! {
|
||||
div class="card center" {
|
||||
div class="center list-container" {
|
||||
ul class="list" {
|
||||
@if ap_state == "up" {
|
||||
li class="list-item light-bg warning-border" {
|
||||
"Enable WiFi client mode to view saved and available networks."
|
||||
}
|
||||
} @else if network_list.is_empty() {
|
||||
li class="list-item light-bg" {
|
||||
"No saved or available networks found."
|
||||
}
|
||||
} @else {
|
||||
@for (ssid, ap) in network_list {
|
||||
li {
|
||||
@if ssid == wlan_ssid {
|
||||
(render_network_connected_elements(ssid))
|
||||
} @else if ap.state == "Available" {
|
||||
(render_network_available_elements(ssid, ap.state))
|
||||
} @else {
|
||||
(render_network_unavailable_elements(ssid, ap.state))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let body =
|
||||
templates::nav::build_template(list_template, "WiFi Networks", Some("/settings/network"));
|
||||
|
||||
let theme = theme::get_theme();
|
||||
|
||||
templates::base::build_template(body, theme)
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
use maud::{html, Markup, PreEscaped};
|
||||
use peach_network::network;
|
||||
use rouille::Request;
|
||||
|
||||
use crate::{
|
||||
templates,
|
||||
utils::{flash::FlashRequest, theme},
|
||||
AP_IFACE,
|
||||
};
|
||||
|
||||
// ROUTE: /settings/network
|
||||
|
||||
/// Read the wireless interface mode (WiFi AP or client) and selectively render
|
||||
/// the activation button for the deactivated mode.
|
||||
fn render_mode_toggle_button() -> Markup {
|
||||
match network::state(AP_IFACE) {
|
||||
Ok(Some(state)) if state == "up" => {
|
||||
html! {
|
||||
a id="connectWifi" class="button button-primary center" href="/settings/network/wifi/activate" title="Enable WiFi" { "Enable WiFi" }
|
||||
}
|
||||
}
|
||||
_ => html! {
|
||||
a id="deployAccessPoint" class="button button-primary center" href="/settings/network/ap/activate" title="Deploy Access Point" { "Deploy Access Point" }
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn render_buttons() -> Markup {
|
||||
html! {
|
||||
(PreEscaped("<!-- BUTTONS -->"))
|
||||
div id="buttons" {
|
||||
a class="button button-primary center" href="/settings/network/wifi/add" title="Add WiFi Network" { "Add WiFi Network" }
|
||||
a id="configureDNS" class="button button-primary center" href="/settings/network/dns" title="Configure DNS" { "Configure DNS" }
|
||||
(PreEscaped("<!-- if ap is up, show 'Enable WiFi' button, else show 'Deplay Access Point' -->"))
|
||||
(render_mode_toggle_button())
|
||||
a id="listWifi" class="button button-primary center" href="/settings/network/wifi" title="List WiFi Networks" { "List WiFi Networks" }
|
||||
// TODO: uncomment this once data usage feature is in place
|
||||
// a id="viewUsage" class="button button-primary center" href="/settings/network/wifi/usage" title="View Data Usage" { "View Data Usage" }
|
||||
a id="viewStatus" class="button button-primary center" href="/status/network" title="View Network Status" { "View Network Status" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Network settings menu template builder.
|
||||
pub fn build_template(request: &Request) -> PreEscaped<String> {
|
||||
let (flash_name, flash_msg) = request.retrieve_flash();
|
||||
|
||||
let menu_template = html! {
|
||||
(PreEscaped("<!-- NETWORK SETTINGS MENU -->"))
|
||||
div class="card center" {
|
||||
(render_buttons())
|
||||
// render flash message if cookies were found in the request
|
||||
@if let (Some(name), Some(msg)) = (flash_name, flash_msg) {
|
||||
(PreEscaped("<!-- FLASH MESSAGE -->"))
|
||||
(templates::flash::build_template(name, msg))
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let body = templates::nav::build_template(menu_template, "Network Settings", Some("/settings"));
|
||||
|
||||
let theme = theme::get_theme();
|
||||
|
||||
templates::base::build_template(body, theme)
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
pub mod add_ap;
|
||||
pub mod ap_details;
|
||||
pub mod configure_dns;
|
||||
// TODO: uncomment this once data usage feature is in place
|
||||
// pub mod data_usage_limits;
|
||||
pub mod list_aps;
|
||||
pub mod menu;
|
||||
pub mod modify_ap;
|
|
@ -0,0 +1,105 @@
|
|||
use maud::{html, Markup, PreEscaped};
|
||||
use peach_network::network;
|
||||
use rouille::{post_input, try_or_400, Request, Response};
|
||||
|
||||
use crate::{
|
||||
templates,
|
||||
utils::{
|
||||
flash::{FlashRequest, FlashResponse},
|
||||
theme,
|
||||
},
|
||||
WLAN_IFACE,
|
||||
};
|
||||
|
||||
// ROUTE: /settings/network/wifi/modify?<ssid>
|
||||
|
||||
fn render_ssid_input(selected_ap: Option<String>) -> Markup {
|
||||
html! {
|
||||
(PreEscaped("<!-- input for network ssid -->"))
|
||||
input id="ssid" name="ssid" class="center input" type="text" placeholder="SSID" title="Network name (SSID) for WiFi access point" value=[selected_ap] autofocus;
|
||||
}
|
||||
}
|
||||
|
||||
fn render_password_input() -> Markup {
|
||||
html! {
|
||||
(PreEscaped("<!-- input for network password -->"))
|
||||
input id="pass" name="pass" class="center input" type="password" placeholder="Password" title="Password for WiFi access point";
|
||||
}
|
||||
}
|
||||
|
||||
fn render_buttons() -> Markup {
|
||||
html! {
|
||||
(PreEscaped("<!-- BUTTONS -->"))
|
||||
div id="buttons" {
|
||||
input id="savePassword" class="button button-primary center" title="Save" type="submit" value="Save";
|
||||
a class="button button-secondary center" href="/settings/network" title="Cancel" { "Cancel" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// WiFi access point password modification form template builder.
|
||||
pub fn build_template(request: &Request, selected_ap: Option<String>) -> PreEscaped<String> {
|
||||
let (flash_name, flash_msg) = request.retrieve_flash();
|
||||
|
||||
let form_template = html! {
|
||||
(PreEscaped("<!-- NETWORK MODIFY AP PASSWORD FORM -->"))
|
||||
div class="card center" {
|
||||
form id="wifiModify" action="/settings/network/wifi/modify" method="post" {
|
||||
(render_ssid_input(selected_ap))
|
||||
(render_password_input())
|
||||
(render_buttons())
|
||||
}
|
||||
// render flash message if cookies were found in the request
|
||||
@if let (Some(name), Some(msg)) = (flash_name, flash_msg) {
|
||||
(PreEscaped("<!-- FLASH MESSAGE -->"))
|
||||
(templates::flash::build_template(name, msg))
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let body = templates::nav::build_template(
|
||||
form_template,
|
||||
"Change WiFi Password",
|
||||
Some("/settings/network"),
|
||||
);
|
||||
|
||||
let theme = theme::get_theme();
|
||||
|
||||
templates::base::build_template(body, theme)
|
||||
}
|
||||
|
||||
/// Parse the SSID and password for an access point and save the new password.
|
||||
pub fn handle_form(request: &Request) -> Response {
|
||||
let data = try_or_400!(post_input!(request, {
|
||||
ssid: String,
|
||||
pass: String,
|
||||
}));
|
||||
|
||||
let (name, msg) = match network::id(WLAN_IFACE, &data.ssid) {
|
||||
Ok(Some(id)) => match network::modify(&id, &data.ssid, &data.pass) {
|
||||
Ok(_) => ("success".to_string(), "WiFi password updated".to_string()),
|
||||
Err(err) => (
|
||||
"error".to_string(),
|
||||
format!("Failed to update WiFi password: {}", err),
|
||||
),
|
||||
},
|
||||
Ok(None) => (
|
||||
"error".to_string(),
|
||||
format!(
|
||||
"Failed to update WiFi password: no saved credentials found for network {}",
|
||||
&data.ssid
|
||||
),
|
||||
),
|
||||
Err(err) => (
|
||||
"error".to_string(),
|
||||
format!(
|
||||
"Failed to update WiFi password: no ID found for network {}: {}",
|
||||
&data.ssid, err
|
||||
),
|
||||
),
|
||||
};
|
||||
|
||||
let (flash_name, flash_msg) = (format!("flash_name={}", name), format!("flash_msg={}", msg));
|
||||
|
||||
Response::redirect_303("/settings/network/wifi/modify").add_flash(flash_name, flash_msg)
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
use maud::{html, PreEscaped};
|
||||
use rouille::Request;
|
||||
|
||||
use crate::{
|
||||
templates,
|
||||
utils::{flash::FlashRequest, theme},
|
||||
};
|
||||
|
||||
/// Power menu template builder.
|
||||
///
|
||||
/// Presents options for rebooting or shutting down the device.
|
||||
pub fn build_template(request: &Request) -> PreEscaped<String> {
|
||||
let (flash_name, flash_msg) = request.retrieve_flash();
|
||||
|
||||
let power_menu_template = html! {
|
||||
(PreEscaped("<!-- POWER MENU -->"))
|
||||
div class="card center" {
|
||||
div class="card-container" {
|
||||
div id="buttons" {
|
||||
a id="rebootBtn" class="button button-primary center" href="/reboot" title="Reboot Device" { "Reboot" }
|
||||
a id="shutdownBtn" class="button button-warning center" href="/shutdown" title="Shutdown Device" { "Shutdown" }
|
||||
a id="cancelBtn" class="button button-secondary center" href="/settings" title="Cancel" { "Cancel" }
|
||||
}
|
||||
@if let (Some(name), Some(msg)) = (flash_name, flash_msg) {
|
||||
(PreEscaped("<!-- FLASH MESSAGE -->"))
|
||||
(templates::flash::build_template(name, msg))
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let body = templates::nav::build_template(power_menu_template, "Power Menu", Some("/"));
|
||||
|
||||
let theme = theme::get_theme();
|
||||
|
||||
templates::base::build_template(body, theme)
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue