Merge branch 'main' into better-caching
This commit is contained in:
commit
d44c034f6a
@ -10,7 +10,7 @@ executors:
|
||||
type: boolean
|
||||
default: false
|
||||
docker:
|
||||
- image: plumeorg/plume-buildenv:v0.0.9
|
||||
- image: plumeorg/plume-buildenv:v0.2.0
|
||||
- image: <<#parameters.postgres>>circleci/postgres:9.6-alpine<</parameters.postgres>><<^parameters.postgres>>alpine:latest<</parameters.postgres>>
|
||||
environment:
|
||||
POSTGRES_USER: postgres
|
||||
|
@ -1,4 +1,4 @@
|
||||
FROM debian:stretch-20190326
|
||||
FROM debian:buster-20201117
|
||||
ENV PATH="/root/.cargo/bin:${PATH}"
|
||||
|
||||
#install native/circleci/build dependancies
|
||||
|
@ -45,3 +45,12 @@ ROCKET_ADDRESS=127.0.0.1
|
||||
#PLUME_LOGO_192=icons/trwnh/paragraphs/plumeParagraphs192.png
|
||||
#PLUME_LOGO_256=icons/trwnh/paragraphs/plumeParagraphs256.png
|
||||
#PLUME_LOGO_512=icons/trwnh/paragraphs/plumeParagraphs512.png
|
||||
|
||||
## LDAP CONFIG ##
|
||||
# the object that will be bound is "${USER_NAME_ATTR}=${username},${BASE_DN}"
|
||||
#LDAP_ADDR=ldap://127.0.0.1:1389
|
||||
#LDAP_BASE_DN="ou=users,dc=your-org,dc=eu"
|
||||
#LDAP_USER_NAME_ATTR=cn
|
||||
#LDAP_USER_MAIL_ATTR=mail
|
||||
#LDAP_TLS=false
|
||||
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -18,3 +18,4 @@ tags.*
|
||||
search_index
|
||||
.buildconfig
|
||||
__pycache__
|
||||
.vscode/
|
||||
|
158
CHANGELOG.md
Normal file
158
CHANGELOG.md
Normal file
@ -0,0 +1,158 @@
|
||||
# Changelog
|
||||
|
||||
<!-- next-header -->
|
||||
|
||||
## [Unreleased] - No release date
|
||||
|
||||
## [0.5.0] - 2020-06-21
|
||||
|
||||
### Added
|
||||
|
||||
- Email blocklisting (#718)
|
||||
- Syntax highlighting (#691)
|
||||
- Persian localization (#782)
|
||||
- Switchable tokenizer - enables Japanese full-text search (#776)
|
||||
- Make database connections configurable by environment variables (#768)
|
||||
|
||||
### Changed
|
||||
|
||||
- Display likes and boost on post cards (#744)
|
||||
- Rust 2018 (#726)
|
||||
- Bump to LLVM to 9.0.0 to fix ARM builds (#737)
|
||||
- Remove dependency on runtime-fmt (#773)
|
||||
- Drop the -alpha suffix in release names, it is implied that Plume is not stable yet because of the 0 major version (Plume 1.0.0 will be the first stable release).
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix parsing of mentions inside a Markdown code block (be430c6)
|
||||
- Fix RSS issues (#720)
|
||||
- Fix Atom feed (#764)
|
||||
- Fix default theme (#746)
|
||||
- Fix shown password on remote interact pages (#741)
|
||||
- Allow unicode hashtags (#757)
|
||||
- Fix French grammar for for 0 (#760)
|
||||
- Don't show boosts and likes for "all" and "local" in timelines (#781)
|
||||
- Fix liking and boosting posts on remote instances (#762)
|
||||
|
||||
## [0.4.0] - 2019-12-23
|
||||
|
||||
### Added
|
||||
|
||||
- Add support for generic timeline (#525)
|
||||
- Federate user deletion (#551)
|
||||
- import migrations and don't require diesel_cli for admins (#555)
|
||||
- Cache local instance (#572)
|
||||
- Initial RTL support #575 (#577)
|
||||
- Confirm deletion of blog (#602)
|
||||
- Make a distinction between moderators and admins (#619)
|
||||
- Theming (#624)
|
||||
- Add clap to plume in order to print help and version (#631)
|
||||
- Add Snapcraft metadata and install/maintenance hooks (#666)
|
||||
- Add environmental variable to control path of media (#683)
|
||||
- Add autosaving to the editor (#688)
|
||||
- CI: Upload artifacts to pull request deploy environment (#539)
|
||||
- CI: Upload artifact of wasm binary (#571)
|
||||
|
||||
### Changed
|
||||
|
||||
- Update follow_remote.rs.html grammar (#548)
|
||||
- Add some feedback when performing some actions (#552)
|
||||
- Theme update (#553)
|
||||
- Remove the new index lock tantivy uses (#556)
|
||||
- Reduce reqwest timeout to 5s (#557)
|
||||
- Improve notification management (#561)
|
||||
- Fix occurrences of 'have been' to 'has been' (#578) + Direct follow-up to #578 (#603)
|
||||
- Store password reset requests in database (#610)
|
||||
- Use futures and tokio to send activities (#620)
|
||||
- Don't ignore dotenv errors (#630)
|
||||
- Replace the input! macro with an Input builder (#646)
|
||||
- Update default license (#659)
|
||||
- Paginate the outbox responses. Fixes #669 (#681)
|
||||
- Use the "classic" editor by default (#697)
|
||||
- Fix issue #705 (#708)
|
||||
- Make comments in styleshhets a bit clearer (#545)
|
||||
- Rewrite circleci config (#558)
|
||||
- Use openssl instead of sha256sum for build.rs (#568)
|
||||
- Update dependencies (#574)
|
||||
- Refactor code to use Shrinkwraprs and diesel-derive-newtype (#598)
|
||||
- Add enum containing all successful route returns (#614)
|
||||
- Update dependencies which depended on nix -- fixes arm32 builds (#615)
|
||||
- Update some documents (#616)
|
||||
- Update dependencies (#643)
|
||||
- Make the comment syntax consistent across all CSS (#487)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Remove r (#535)
|
||||
- Fix certain improper rendering of forms (#560)
|
||||
- make hashtags work in profile summary (#562)
|
||||
- Fix some federation issue (#573)
|
||||
- Prevent comment form submit button distortion on iOS (#592)
|
||||
- Update textarea overflow to scroll (#609)
|
||||
- Fix arm builds (#612)
|
||||
- Fix theme caching (#647)
|
||||
- Fix issue #642, frontend not in English if the user language does not exist (#648)
|
||||
- Don't index drafts (#656)
|
||||
- Fill entirely user on creation (#657)
|
||||
- Delete notification on user deletion (#658)
|
||||
- Order media so that latest added are top (#660)
|
||||
- Fix logo URL (#664)
|
||||
- Snap: Ensure cargo-web doesn't erroneously adopt our workspace. (#667)
|
||||
- Snap: Another fix for building (#668)
|
||||
- Snap: Fix build for non-Tier-1 Rust platforms (#672)
|
||||
- Don't split sentences for translations (#677)
|
||||
- Escape href quotation marks (#678)
|
||||
- Re-add empty strings in translation (#682)
|
||||
- Make the search index creation during migration respect SEARCH_INDEX (#689)
|
||||
- Fix the navigation menu not opening on touch (#690)
|
||||
- Make search items optional (#693)
|
||||
- Various snap fixes (#698)
|
||||
- Fix #637 : Markdown footnotes (#700)
|
||||
- Fix lettre (#706)
|
||||
- CI: Fix Crowdin upload (#576)
|
||||
|
||||
### Removed
|
||||
|
||||
- Remove the Canapi dependency (#540)
|
||||
- Remove use of Rust in migrations (#704)
|
||||
|
||||
## [0.3.0] - 2019-04-19
|
||||
|
||||
### Added
|
||||
|
||||
- Cover for articles (#299, #387)
|
||||
- Password reset (#448)
|
||||
- New editor (#293, #458, #482, #483, #486, #530)
|
||||
- Search (#324, #375, #445)
|
||||
- Edit blogs (#460, #494, #497)
|
||||
- Hashtags in articles (#283, #295)
|
||||
- API endpoints (#245, #285, #307)
|
||||
- A bunch of new translations! (#479, #501, #506, #510, #512, #514)
|
||||
|
||||
### Changed
|
||||
|
||||
- Federation improvements (#216, #217, #357, #364, #399, #443, #446, #455, #502, #519)
|
||||
- Improved build process (#281, #374, #392, #402, #489, #498, #503, #511, #513, #515, #528)
|
||||
|
||||
### Fixes
|
||||
|
||||
- UI usability fixes (#370, #386, #401, #417, #418, #444, #452, #480, #516, #518, #522, #532)
|
||||
|
||||
## [0.2.0] - 2018-09-12
|
||||
|
||||
### Added
|
||||
|
||||
- Article publishing, or save as a draft
|
||||
- Like, or boost an article
|
||||
- Basic Markdown editor
|
||||
- Federated commenting system
|
||||
- User account creation
|
||||
- Limited federation on other platforms and subscribing to users
|
||||
- Ability to create multiple blogs
|
||||
|
||||
<!-- next-url -->
|
||||
[Unreleased]: https://github.com/Plume-org/Plume/compare/0.5.0...HEAD
|
||||
[0.5.0]: https://github.com/Plume-org/Plume/compare/0.4.0-alpha-4...0.5.0
|
||||
[0.4.0]: https://github.com/Plume-org/Plume/compare/0.3.0-alpha-2...0.4.0-alpha-4
|
||||
[0.3.0]: https://github.com/Plume-org/Plume/compare/0.2.0-alpha-1...0.3.0-alpha-2
|
||||
[0.2.0]: https://github.com/Plume-org/Plume/releases/tag/0.2.0-alpha-1
|
1952
Cargo.lock
generated
1952
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -1,7 +1,7 @@
|
||||
[package]
|
||||
authors = ["Plume contributors"]
|
||||
name = "plume"
|
||||
version = "0.4.0"
|
||||
version = "0.5.0"
|
||||
repository = "https://github.com/Plume-org/Plume"
|
||||
edition = "2018"
|
||||
|
||||
@ -20,8 +20,8 @@ heck = "0.3.0"
|
||||
lettre = "0.9.2"
|
||||
lettre_email = "0.9.2"
|
||||
num_cpus = "1.10"
|
||||
rocket = "0.4.2"
|
||||
rocket_contrib = { version = "0.4.2", features = ["json"] }
|
||||
rocket = "0.4.5"
|
||||
rocket_contrib = { version = "0.4.5", features = ["json"] }
|
||||
rocket_i18n = { git = "https://github.com/Plume-org/rocket_i18n", rev = "e922afa7c366038b3433278c03b1456b346074f2" }
|
||||
rpassword = "4.0"
|
||||
scheduled-thread-pool = "0.2.2"
|
||||
|
@ -1,4 +1,4 @@
|
||||
FROM rust:1-stretch as builder
|
||||
FROM rust:1-buster as builder
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
ca-certificates \
|
||||
@ -28,7 +28,7 @@ RUN cargo install --path ./ --force --no-default-features --features postgres
|
||||
RUN cargo install --path plume-cli --force --no-default-features --features postgres
|
||||
RUN cargo clean
|
||||
|
||||
FROM debian:stretch-slim
|
||||
FROM debian:buster-slim
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
ca-certificates \
|
||||
|
@ -1,4 +1,4 @@
|
||||
FROM rust:1-stretch
|
||||
FROM rust:1-buster
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
ca-certificates \
|
||||
|
@ -40,7 +40,7 @@ main header.article {
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: end;
|
||||
justify-content: flex-end;
|
||||
|
||||
h1, .article-info {
|
||||
text-align: center;
|
||||
@ -490,3 +490,30 @@ input:checked ~ .cw-container > .cw-text {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
|
||||
// Small screens
|
||||
@media screen and (max-width: 600px) {
|
||||
#plume-editor header {
|
||||
flex-direction: column-reverse;
|
||||
|
||||
button {
|
||||
flex: 0 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
.popup {
|
||||
top: 10vh;
|
||||
bottom: 10vh;
|
||||
left: 1vw;
|
||||
right: 1vw;
|
||||
}
|
||||
|
||||
main article {
|
||||
margin: 2.5em .5em;
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
main .article-meta > *, main .article-meta .comments, main .article-meta > .banner > * {
|
||||
margin: 0 5%;
|
||||
}
|
||||
}
|
||||
|
@ -490,6 +490,10 @@ figure {
|
||||
|
||||
/// Small screens
|
||||
@media screen and (max-width: 600px) {
|
||||
body > main > *, .h-feed > * {
|
||||
margin: 1em;
|
||||
}
|
||||
|
||||
main .article-meta {
|
||||
> *, .comments {
|
||||
margin: 0 5%;
|
||||
@ -535,7 +539,7 @@ figure {
|
||||
margin: 0;
|
||||
|
||||
& > * {
|
||||
max-width: 100%;
|
||||
max-width: 100% !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -205,6 +205,7 @@ body > header {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translate(-50%, 0);
|
||||
transform: translateZ(0);
|
||||
opacity: 0;
|
||||
font-size: 0.9em;
|
||||
white-space: nowrap;
|
||||
@ -221,3 +222,93 @@ body > header {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Small screens
|
||||
@media screen and (max-width: 600px) {
|
||||
@keyframes menuOpening {
|
||||
from {
|
||||
transform: scaleX(0);
|
||||
transform-origin: left;
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: scaleX(1);
|
||||
transform-origin: left;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
body > header {
|
||||
flex-direction: column;
|
||||
|
||||
nav#menu {
|
||||
display: inline-flex;
|
||||
z-index: 21;
|
||||
}
|
||||
|
||||
#content {
|
||||
display: none;
|
||||
appearance: none;
|
||||
text-align: center;
|
||||
z-index: 20;
|
||||
}
|
||||
}
|
||||
|
||||
body > header:focus-within #content, #content.show {
|
||||
position: fixed;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
|
||||
animation: 0.2s menuOpening;
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
transform: skewX(-10deg);
|
||||
top: 0;
|
||||
left: -20%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
z-index: -10;
|
||||
|
||||
background: $primary;
|
||||
}
|
||||
|
||||
> nav {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
|
||||
a {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin: 0;
|
||||
padding: 1rem 1.5rem;
|
||||
color: $background;
|
||||
font-size: 1.4em;
|
||||
font-weight: 300;
|
||||
|
||||
&.title { font-size: 1.8em; }
|
||||
|
||||
> *:first-child { width: 3rem; }
|
||||
> img:first-child { height: 3rem; }
|
||||
> *:last-child { margin-left: 1rem; }
|
||||
> nav hr {
|
||||
display: block;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
border: solid $background 0.1rem;
|
||||
}
|
||||
.mobile-label { display: initial; }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
"project_identifier": "plume"
|
||||
"api_key_env": CROWDIN_API_KEY
|
||||
"project_id": 352097
|
||||
"api_token_env": "CROWDIN_API_KEY"
|
||||
preserve_hierarchy: true
|
||||
files:
|
||||
- source: /po/plume/plume.pot
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "plume-api"
|
||||
version = "0.4.0"
|
||||
version = "0.5.0"
|
||||
authors = ["Plume contributors"]
|
||||
edition = "2018"
|
||||
|
||||
|
1
plume-api/release.toml
Normal file
1
plume-api/release.toml
Normal file
@ -0,0 +1 @@
|
||||
pre-release-replacements = []
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "plume-cli"
|
||||
version = "0.4.0"
|
||||
version = "0.5.0"
|
||||
authors = ["Plume contributors"]
|
||||
edition = "2018"
|
||||
|
||||
|
1
plume-cli/release.toml
Normal file
1
plume-cli/release.toml
Normal file
@ -0,0 +1 @@
|
||||
pre-release-replacements = []
|
@ -132,7 +132,7 @@ fn new<'a>(args: &ArgMatches<'a>, conn: &Connection) {
|
||||
role,
|
||||
&bio,
|
||||
email,
|
||||
User::hash_pass(&password).expect("Couldn't hash password"),
|
||||
Some(User::hash_pass(&password).expect("Couldn't hash password")),
|
||||
)
|
||||
.expect("Couldn't save new user");
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "plume-common"
|
||||
version = "0.4.0"
|
||||
version = "0.5.0"
|
||||
authors = ["Plume contributors"]
|
||||
edition = "2018"
|
||||
|
||||
@ -14,7 +14,7 @@ heck = "0.3.0"
|
||||
hex = "0.3"
|
||||
hyper = "0.12.33"
|
||||
openssl = "0.10.22"
|
||||
rocket = "0.4.0"
|
||||
rocket = "0.4.5"
|
||||
reqwest = "0.9"
|
||||
serde = "1.0"
|
||||
serde_derive = "1.0"
|
||||
|
1
plume-common/release.toml
Normal file
1
plume-common/release.toml
Normal file
@ -0,0 +1 @@
|
||||
pre-release-replacements = []
|
@ -294,8 +294,7 @@ pub fn md_to_html<'a>(
|
||||
}
|
||||
let hashtag = text_acc;
|
||||
let link = Tag::Link(
|
||||
format!("{}tag/{}", base_url, &hashtag.to_camel_case())
|
||||
.into(),
|
||||
format!("{}tag/{}", base_url, &hashtag).into(),
|
||||
hashtag.to_owned().into(),
|
||||
);
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "plume-front"
|
||||
version = "0.4.0"
|
||||
version = "0.5.0"
|
||||
authors = ["Plume contributors"]
|
||||
edition = "2018"
|
||||
|
||||
|
1
plume-front/release.toml
Normal file
1
plume-front/release.toml
Normal file
@ -0,0 +1 @@
|
||||
pre-release-replacements = []
|
@ -269,7 +269,13 @@ pub fn init() -> Result<(), EditorError> {
|
||||
let editor_button = document().create_element("a")?;
|
||||
js! { @{&editor_button}.href = "#"; }
|
||||
editor_button.add_event_listener(|_: ClickEvent| {
|
||||
window().local_storage().remove("basic-editor");
|
||||
if window()
|
||||
.local_storage()
|
||||
.insert("basic-editor", "false")
|
||||
.is_err()
|
||||
{
|
||||
console!(log, "Failed to write into local storage");
|
||||
}
|
||||
window().history().go(0).ok(); // refresh
|
||||
});
|
||||
editor_button.append_child(
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "plume-macro"
|
||||
version = "0.4.0"
|
||||
version = "0.5.0"
|
||||
authors = ["Trinity Pointard <trinity.pointard@insa-rennes.fr>"]
|
||||
edition = "2018"
|
||||
description = "Plume procedural macros"
|
||||
|
1
plume-macro/release.toml
Normal file
1
plume-macro/release.toml
Normal file
@ -0,0 +1 @@
|
||||
pre-release-replacements = []
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "plume-models"
|
||||
version = "0.4.0"
|
||||
version = "0.5.0"
|
||||
authors = ["Plume contributors"]
|
||||
edition = "2018"
|
||||
|
||||
@ -13,9 +13,10 @@ guid-create = "0.1"
|
||||
heck = "0.3.0"
|
||||
itertools = "0.8.0"
|
||||
lazy_static = "1.0"
|
||||
ldap3 = "0.7.1"
|
||||
migrations_internals= "1.4.0"
|
||||
openssl = "0.10.22"
|
||||
rocket = "0.4.0"
|
||||
rocket = "0.4.5"
|
||||
rocket_i18n = { git = "https://github.com/Plume-org/rocket_i18n", rev = "e922afa7c366038b3433278c03b1456b346074f2" }
|
||||
reqwest = "0.9"
|
||||
scheduled-thread-pool = "0.2.2"
|
||||
@ -30,7 +31,7 @@ whatlang = "0.7.1"
|
||||
shrinkwraprs = "0.2.1"
|
||||
diesel-derive-newtype = "0.1.2"
|
||||
glob = "0.3.0"
|
||||
lindera-tantivy = { version = "0.1.2", optional = true }
|
||||
lindera-tantivy = { version = "0.1.3", optional = true }
|
||||
|
||||
[dependencies.chrono]
|
||||
features = ["serde"]
|
||||
|
BIN
plume-models/plume.db-journal
Normal file
BIN
plume-models/plume.db-journal
Normal file
Binary file not shown.
BIN
plume-models/plume_tests.sqlite-journal
Normal file
BIN
plume-models/plume_tests.sqlite-journal
Normal file
Binary file not shown.
1
plume-models/release.toml
Normal file
1
plume-models/release.toml
Normal file
@ -0,0 +1 @@
|
||||
pre-release-replacements = []
|
@ -20,6 +20,7 @@ pub struct Config {
|
||||
pub logo: LogoConfig,
|
||||
pub default_theme: String,
|
||||
pub media_directory: String,
|
||||
pub ldap: Option<LdapConfig>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@ -240,6 +241,42 @@ impl SearchTokenizerConfig {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct LdapConfig {
|
||||
pub addr: String,
|
||||
pub base_dn: String,
|
||||
pub tls: bool,
|
||||
pub user_name_attr: String,
|
||||
pub mail_attr: String,
|
||||
}
|
||||
|
||||
fn get_ldap_config() -> Option<LdapConfig> {
|
||||
let addr = var("LDAP_ADDR").ok();
|
||||
let base_dn = var("LDAP_BASE_DN").ok();
|
||||
match (addr, base_dn) {
|
||||
(Some(addr), Some(base_dn)) => {
|
||||
let tls = var("LDAP_TLS").unwrap_or_else(|_| "false".to_owned());
|
||||
let tls = match tls.as_ref() {
|
||||
"1" | "true" | "TRUE" => true,
|
||||
"0" | "false" | "FALSE" => false,
|
||||
_ => panic!("Invalid LDAP configuration : tls"),
|
||||
};
|
||||
let user_name_attr = var("LDAP_USER_NAME_ATTR").unwrap_or_else(|_| "cn".to_owned());
|
||||
let mail_attr = var("LDAP_USER_MAIL_ATTR").unwrap_or_else(|_| "mail".to_owned());
|
||||
Some(LdapConfig {
|
||||
addr,
|
||||
base_dn,
|
||||
tls,
|
||||
user_name_attr,
|
||||
mail_attr,
|
||||
})
|
||||
}
|
||||
(None, None) => None,
|
||||
(_, _) => {
|
||||
panic!("Invalid LDAP configuration : both LDAP_ADDR and LDAP_BASE_DN must be set")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
pub static ref CONFIG: Config = Config {
|
||||
base_url: var("BASE_URL").unwrap_or_else(|_| format!(
|
||||
@ -267,5 +304,6 @@ lazy_static! {
|
||||
default_theme: var("DEFAULT_THEME").unwrap_or_else(|_| "default-light".to_owned()),
|
||||
media_directory: var("MEDIA_UPLOAD_DIRECTORY")
|
||||
.unwrap_or_else(|_| "static/media".to_owned()),
|
||||
ldap: get_ldap_config(),
|
||||
};
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ use activitypub::{
|
||||
};
|
||||
use chrono::{NaiveDateTime, TimeZone, Utc};
|
||||
use diesel::{self, BelongingToDsl, ExpressionMethods, QueryDsl, RunQueryDsl, SaveChangesDsl};
|
||||
use heck::{CamelCase, KebabCase};
|
||||
use heck::KebabCase;
|
||||
use plume_common::{
|
||||
activity_pub::{
|
||||
inbox::{AsObject, FromId},
|
||||
@ -622,7 +622,6 @@ impl FromId<PlumeRocket> for Post {
|
||||
let mut hashtags = md_to_html(&post.source, None, false, None)
|
||||
.2
|
||||
.into_iter()
|
||||
.map(|s| s.to_camel_case())
|
||||
.collect::<HashSet<_>>();
|
||||
if let Some(serde_json::Value::Array(tags)) = article.object_props.tag {
|
||||
for tag in tags {
|
||||
@ -762,7 +761,6 @@ impl AsObject<User, Update, &PlumeRocket> for PostUpdate {
|
||||
let mut txt_hashtags = md_to_html(&post.source, None, false, None)
|
||||
.2
|
||||
.into_iter()
|
||||
.map(|s| s.to_camel_case())
|
||||
.collect::<HashSet<_>>();
|
||||
if let Some(serde_json::Value::Array(mention_tags)) = self.tags {
|
||||
let mut mentions = vec![];
|
||||
|
@ -5,10 +5,10 @@ use crate::{
|
||||
use chrono::Datelike;
|
||||
use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl};
|
||||
use itertools::Itertools;
|
||||
use std::{cmp, fs::create_dir_all, path::Path, sync::Mutex};
|
||||
use std::{cmp, fs::create_dir_all, io, path::Path, sync::Mutex};
|
||||
use tantivy::{
|
||||
collector::TopDocs, directory::MmapDirectory, schema::*, Index, IndexReader, IndexWriter,
|
||||
ReloadPolicy, Term,
|
||||
ReloadPolicy, TantivyError, Term,
|
||||
};
|
||||
use whatlang::{detect as detect_lang, Lang};
|
||||
|
||||
@ -18,6 +18,7 @@ pub enum SearcherError {
|
||||
WriteLockAcquisitionError,
|
||||
IndexOpeningError,
|
||||
IndexEditionError,
|
||||
InvalidIndexDataError,
|
||||
}
|
||||
|
||||
pub struct Searcher {
|
||||
@ -135,7 +136,19 @@ impl Searcher {
|
||||
.reader_builder()
|
||||
.reload_policy(ReloadPolicy::Manual)
|
||||
.try_into()
|
||||
.map_err(|_| SearcherError::IndexCreationError)?,
|
||||
.map_err(|e| {
|
||||
if let TantivyError::IOError(err) = e {
|
||||
let err: io::Error = err.into();
|
||||
if err.kind() == io::ErrorKind::InvalidData {
|
||||
// Search index was created in older Tantivy format.
|
||||
SearcherError::InvalidIndexDataError
|
||||
} else {
|
||||
SearcherError::IndexCreationError
|
||||
}
|
||||
} else {
|
||||
SearcherError::IndexCreationError
|
||||
}
|
||||
})?,
|
||||
index,
|
||||
})
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
use crate::{
|
||||
ap_url, blocklisted_emails::BlocklistedEmail, blogs::Blog, db_conn::DbConn, follows::Follow,
|
||||
instance::*, medias::Media, notifications::Notification, post_authors::PostAuthor, posts::Post,
|
||||
safe_string::SafeString, schema::users, search::Searcher, timeline::Timeline, Connection,
|
||||
Error, PlumeRocket, Result, ITEMS_PER_PAGE,
|
||||
ap_url, blocklisted_emails::BlocklistedEmail, blogs::Blog, config::CONFIG, db_conn::DbConn,
|
||||
follows::Follow, instance::*, medias::Media, notifications::Notification,
|
||||
post_authors::PostAuthor, posts::Post, safe_string::SafeString, schema::users,
|
||||
search::Searcher, timeline::Timeline, Connection, Error, PlumeRocket, Result, ITEMS_PER_PAGE,
|
||||
};
|
||||
use activitypub::{
|
||||
activity::Delete,
|
||||
@ -14,6 +14,7 @@ use activitypub::{
|
||||
use bcrypt;
|
||||
use chrono::{NaiveDateTime, Utc};
|
||||
use diesel::{self, BelongingToDsl, ExpressionMethods, OptionalExtension, QueryDsl, RunQueryDsl};
|
||||
use ldap3::{LdapConn, Scope, SearchEntry};
|
||||
use openssl::{
|
||||
hash::MessageDigest,
|
||||
pkey::{PKey, Private},
|
||||
@ -292,11 +293,116 @@ impl User {
|
||||
bcrypt::hash(pass, 10).map_err(Error::from)
|
||||
}
|
||||
|
||||
pub fn auth(&self, pass: &str) -> bool {
|
||||
self.hashed_password
|
||||
.clone()
|
||||
.map(|hashed| bcrypt::verify(pass, hashed.as_ref()).unwrap_or(false))
|
||||
.unwrap_or(false)
|
||||
fn ldap_register(conn: &Connection, name: &str, password: &str) -> Result<User> {
|
||||
if CONFIG.ldap.is_none() {
|
||||
return Err(Error::NotFound);
|
||||
}
|
||||
let ldap = CONFIG.ldap.as_ref().unwrap();
|
||||
|
||||
let mut ldap_conn = LdapConn::new(&ldap.addr).map_err(|_| Error::NotFound)?;
|
||||
let ldap_name = format!("{}={},{}", ldap.user_name_attr, name, ldap.base_dn);
|
||||
let bind = ldap_conn
|
||||
.simple_bind(&ldap_name, password)
|
||||
.map_err(|_| Error::NotFound)?;
|
||||
|
||||
if bind.success().is_err() {
|
||||
return Err(Error::NotFound);
|
||||
}
|
||||
|
||||
let search = ldap_conn
|
||||
.search(
|
||||
&ldap_name,
|
||||
Scope::Base,
|
||||
"(|(objectClass=person)(objectClass=user))",
|
||||
vec![&ldap.mail_attr],
|
||||
)
|
||||
.map_err(|_| Error::NotFound)?
|
||||
.success()
|
||||
.map_err(|_| Error::NotFound)?;
|
||||
for entry in search.0 {
|
||||
let entry = SearchEntry::construct(entry);
|
||||
let email = entry.attrs.get("mail").and_then(|vec| vec.first());
|
||||
if let Some(email) = email {
|
||||
let _ = ldap_conn.unbind();
|
||||
return NewUser::new_local(
|
||||
conn,
|
||||
name.to_owned(),
|
||||
name.to_owned(),
|
||||
Role::Normal,
|
||||
"",
|
||||
email.to_owned(),
|
||||
None,
|
||||
);
|
||||
}
|
||||
}
|
||||
let _ = ldap_conn.unbind();
|
||||
Err(Error::NotFound)
|
||||
}
|
||||
|
||||
fn ldap_login(&self, password: &str) -> bool {
|
||||
if let Some(ldap) = CONFIG.ldap.as_ref() {
|
||||
let mut conn = if let Ok(conn) = LdapConn::new(&ldap.addr) {
|
||||
conn
|
||||
} else {
|
||||
return false;
|
||||
};
|
||||
let name = format!(
|
||||
"{}={},{}",
|
||||
ldap.user_name_attr, &self.username, ldap.base_dn
|
||||
);
|
||||
if let Ok(bind) = conn.simple_bind(&name, password) {
|
||||
bind.success().is_ok()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn login(conn: &Connection, ident: &str, password: &str) -> Result<User> {
|
||||
let local_id = Instance::get_local()?.id;
|
||||
let user = match User::find_by_email(conn, ident) {
|
||||
Ok(user) => Ok(user),
|
||||
_ => User::find_by_name(conn, ident, local_id),
|
||||
}
|
||||
.and_then(|u| {
|
||||
if u.instance_id == local_id {
|
||||
Ok(u)
|
||||
} else {
|
||||
Err(Error::NotFound)
|
||||
}
|
||||
});
|
||||
|
||||
match user {
|
||||
Ok(user) if user.hashed_password.is_some() => {
|
||||
if bcrypt::verify(password, user.hashed_password.as_ref().unwrap()).unwrap_or(false)
|
||||
{
|
||||
Ok(user)
|
||||
} else {
|
||||
Err(Error::NotFound)
|
||||
}
|
||||
}
|
||||
Ok(user) => {
|
||||
if user.ldap_login(password) {
|
||||
Ok(user)
|
||||
} else {
|
||||
Err(Error::NotFound)
|
||||
}
|
||||
}
|
||||
e => {
|
||||
if let Ok(user) = User::ldap_register(conn, ident, password) {
|
||||
return Ok(user);
|
||||
}
|
||||
// if no user was found, and we were unable to auto-register from ldap
|
||||
// fake-verify a password, and return an error.
|
||||
let other = User::get(&*conn, 1)
|
||||
.expect("No user is registered")
|
||||
.hashed_password;
|
||||
other.map(|pass| bcrypt::verify(password, &pass));
|
||||
e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reset_password(&self, conn: &Connection, pass: &str) -> Result<()> {
|
||||
@ -983,7 +1089,7 @@ impl NewUser {
|
||||
role: Role,
|
||||
summary: &str,
|
||||
email: String,
|
||||
password: String,
|
||||
password: Option<String>,
|
||||
) -> Result<User> {
|
||||
let (pub_key, priv_key) = gen_keypair();
|
||||
let instance = Instance::get_local()?;
|
||||
@ -1001,7 +1107,7 @@ impl NewUser {
|
||||
summary: summary.to_owned(),
|
||||
summary_html: SafeString::new(&utils::md_to_html(&summary, None, false, None).0),
|
||||
email: Some(email),
|
||||
hashed_password: Some(password),
|
||||
hashed_password: password,
|
||||
instance_id: instance.id,
|
||||
public_key: String::from_utf8(pub_key).or(Err(Error::Signature))?,
|
||||
private_key: Some(String::from_utf8(priv_key).or(Err(Error::Signature))?),
|
||||
@ -1043,7 +1149,7 @@ pub(crate) mod tests {
|
||||
Role::Admin,
|
||||
"Hello there, I'm the admin",
|
||||
"admin@example.com".to_owned(),
|
||||
"invalid_admin_password".to_owned(),
|
||||
Some("invalid_admin_password".to_owned()),
|
||||
)
|
||||
.unwrap();
|
||||
let user = NewUser::new_local(
|
||||
@ -1053,7 +1159,7 @@ pub(crate) mod tests {
|
||||
Role::Normal,
|
||||
"Hello there, I'm no one",
|
||||
"user@example.com".to_owned(),
|
||||
"invalid_user_password".to_owned(),
|
||||
Some("invalid_user_password".to_owned()),
|
||||
)
|
||||
.unwrap();
|
||||
let other = NewUser::new_local(
|
||||
@ -1063,7 +1169,7 @@ pub(crate) mod tests {
|
||||
Role::Normal,
|
||||
"Hello there, I'm someone else",
|
||||
"other@example.com".to_owned(),
|
||||
"invalid_other_password".to_owned(),
|
||||
Some("invalid_other_password".to_owned()),
|
||||
)
|
||||
.unwrap();
|
||||
vec![admin, user, other]
|
||||
@ -1082,7 +1188,7 @@ pub(crate) mod tests {
|
||||
Role::Normal,
|
||||
"Hello I'm a test",
|
||||
"test@example.com".to_owned(),
|
||||
User::hash_pass("test_password").unwrap(),
|
||||
Some(User::hash_pass("test_password").unwrap()),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
@ -1165,12 +1271,15 @@ pub(crate) mod tests {
|
||||
Role::Normal,
|
||||
"Hello I'm a test",
|
||||
"test@example.com".to_owned(),
|
||||
User::hash_pass("test_password").unwrap(),
|
||||
Some(User::hash_pass("test_password").unwrap()),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert!(test_user.auth("test_password"));
|
||||
assert!(!test_user.auth("other_password"));
|
||||
assert_eq!(
|
||||
User::login(conn, "test", "test_password").unwrap().id,
|
||||
test_user.id
|
||||
);
|
||||
assert!(User::login(conn, "test", "other_password").is_err());
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
|
17
release.toml
Normal file
17
release.toml
Normal file
@ -0,0 +1,17 @@
|
||||
# we don't have a crate yet, so
|
||||
disable-publish = true
|
||||
# change when we all have gpg keys
|
||||
sign-commit = false
|
||||
dev-version-ext = 'dev'
|
||||
# update all crates in plume at once:
|
||||
consolidate-commits = true
|
||||
|
||||
pre-release-hook = ["crowdin", "pull"]
|
||||
|
||||
pre-release-replacements = [
|
||||
{file="CHANGELOG.md", search="Unreleased", replace="[{{version}}]"},
|
||||
{file="CHANGELOG.md", search="\\.\\.\\.HEAD", replace="...{{tag_name}}", exactly=1},
|
||||
{file="CHANGELOG.md", search="No release date", replace="{{date}}"},
|
||||
{file="CHANGELOG.md", search="<!-- next-header -->", replace="<!-- next-header -->\n\n## [Unreleased] - ReleaseDate", exactly=1},
|
||||
{file="CHANGELOG.md", search="<!-- next-url -->", replace="<!-- next-url -->\n[Unreleased]: https://github.com/Plume-org/Plume/compare/{{tag_name}}...HEAD", exactly=1},
|
||||
]
|
@ -62,8 +62,7 @@ pub fn oauth(
|
||||
let conn = &*rockets.conn;
|
||||
let app = App::find_by_client_id(conn, &query.client_id)?;
|
||||
if app.client_secret == query.client_secret {
|
||||
if let Ok(user) = User::find_by_fqn(&rockets, &query.username) {
|
||||
if user.auth(&query.password) {
|
||||
if let Ok(user) = User::login(conn, &query.username, &query.password) {
|
||||
let token = ApiToken::insert(
|
||||
conn,
|
||||
NewApiToken {
|
||||
@ -81,15 +80,6 @@ pub fn oauth(
|
||||
"error": "Invalid credentials"
|
||||
})))
|
||||
}
|
||||
} else {
|
||||
// Making fake password verification to avoid different
|
||||
// response times that would make it possible to know
|
||||
// if a username is registered or not.
|
||||
User::get(conn, 1)?.auth(&query.password);
|
||||
Ok(Json(json!({
|
||||
"error": "Invalid credentials"
|
||||
})))
|
||||
}
|
||||
} else {
|
||||
Ok(Json(json!({
|
||||
"error": "Invalid client_secret"
|
||||
|
@ -1,5 +1,5 @@
|
||||
use chrono::NaiveDateTime;
|
||||
use heck::{CamelCase, KebabCase};
|
||||
use heck::KebabCase;
|
||||
use rocket_contrib::json::Json;
|
||||
|
||||
use crate::api::{authorization::*, Api};
|
||||
@ -181,7 +181,7 @@ pub fn create(
|
||||
Tag::insert(
|
||||
conn,
|
||||
NewTag {
|
||||
tag: hashtag.to_camel_case(),
|
||||
tag: hashtag,
|
||||
is_hashtag: true,
|
||||
post_id: post.id,
|
||||
},
|
||||
|
27
src/main.rs
Normal file → Executable file
27
src/main.rs
Normal file → Executable file
@ -10,6 +10,7 @@ extern crate serde_json;
|
||||
#[macro_use]
|
||||
extern crate validator_derive;
|
||||
|
||||
use chrono::Utc;
|
||||
use clap::App;
|
||||
use diesel::r2d2::ConnectionManager;
|
||||
use plume_models::{
|
||||
@ -21,6 +22,8 @@ use plume_models::{
|
||||
};
|
||||
use rocket_csrf::CsrfFairingBuilder;
|
||||
use scheduled_thread_pool::ScheduledThreadPool;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use std::process::exit;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Duration;
|
||||
@ -98,8 +101,30 @@ Then try to restart Plume.
|
||||
}
|
||||
let workpool = ScheduledThreadPool::with_name("worker {}", num_cpus::get());
|
||||
// we want a fast exit here, so
|
||||
let mut open_searcher =
|
||||
UnmanagedSearcher::open(&CONFIG.search_index, &CONFIG.search_tokenizers);
|
||||
if let Err(Error::Search(SearcherError::InvalidIndexDataError)) = open_searcher {
|
||||
if UnmanagedSearcher::create(&CONFIG.search_index, &CONFIG.search_tokenizers).is_err() {
|
||||
let current_path = Path::new(&CONFIG.search_index);
|
||||
let backup_path = format!("{}.{}", ¤t_path.display(), Utc::now().timestamp());
|
||||
let backup_path = Path::new(&backup_path);
|
||||
fs::rename(current_path, backup_path)
|
||||
.expect("main: error on backing up search index directory for recreating");
|
||||
if UnmanagedSearcher::create(&CONFIG.search_index, &CONFIG.search_tokenizers).is_ok() {
|
||||
if fs::remove_dir_all(backup_path).is_err() {
|
||||
eprintln!(
|
||||
"error on removing backup directory: {}. it remains",
|
||||
backup_path.display()
|
||||
);
|
||||
}
|
||||
} else {
|
||||
panic!("main: error on recreating search index in new index format. remove search index and run `plm search init` manually");
|
||||
}
|
||||
}
|
||||
open_searcher = UnmanagedSearcher::open(&CONFIG.search_index, &CONFIG.search_tokenizers);
|
||||
}
|
||||
#[allow(clippy::match_wild_err_arm)]
|
||||
let searcher = match UnmanagedSearcher::open(&CONFIG.search_index, &CONFIG.search_tokenizers) {
|
||||
let searcher = match open_searcher {
|
||||
Err(Error::Search(e)) => match e {
|
||||
SearcherError::WriteLockAcquisitionError => panic!(
|
||||
r#"
|
||||
|
@ -210,11 +210,19 @@ pub fn add_email_blocklist(
|
||||
form: LenientForm<NewBlocklistedEmail>,
|
||||
rockets: PlumeRocket,
|
||||
) -> Result<Flash<Redirect>, ErrorPage> {
|
||||
BlocklistedEmail::insert(&*rockets.conn, form.0)?;
|
||||
let result = BlocklistedEmail::insert(&*rockets.conn, form.0);
|
||||
|
||||
if let Err(Error::Db(_)) = result {
|
||||
Ok(Flash::error(
|
||||
Redirect::to(uri!(admin_email_blocklist: page = None)),
|
||||
i18n!(rockets.intl.catalog, "Email already blocked"),
|
||||
))
|
||||
} else {
|
||||
Ok(Flash::success(
|
||||
Redirect::to(uri!(admin_email_blocklist: page = None)),
|
||||
i18n!(rockets.intl.catalog, "Email Blocked"),
|
||||
))
|
||||
}
|
||||
}
|
||||
#[get("/admin/emails?<page>")]
|
||||
pub fn admin_email_blocklist(
|
||||
|
@ -1,5 +1,5 @@
|
||||
use chrono::Utc;
|
||||
use heck::{CamelCase, KebabCase};
|
||||
use heck::KebabCase;
|
||||
use rocket::request::LenientForm;
|
||||
use rocket::response::{Flash, Redirect};
|
||||
use rocket_i18n::I18n;
|
||||
@ -314,18 +314,17 @@ pub fn update(
|
||||
let tags = form
|
||||
.tags
|
||||
.split(',')
|
||||
.map(|t| t.trim().to_camel_case())
|
||||
.map(|t| t.trim())
|
||||
.filter(|t| !t.is_empty())
|
||||
.collect::<HashSet<_>>()
|
||||
.into_iter()
|
||||
.filter_map(|t| Tag::build_activity(t).ok())
|
||||
.filter_map(|t| Tag::build_activity(t.to_string()).ok())
|
||||
.collect::<Vec<_>>();
|
||||
post.update_tags(&conn, tags)
|
||||
.expect("post::update: tags error");
|
||||
|
||||
let hashtags = hashtags
|
||||
.into_iter()
|
||||
.map(|h| h.to_camel_case())
|
||||
.collect::<HashSet<_>>()
|
||||
.into_iter()
|
||||
.filter_map(|t| Tag::build_activity(t).ok())
|
||||
@ -489,14 +488,14 @@ pub fn create(
|
||||
let tags = form
|
||||
.tags
|
||||
.split(',')
|
||||
.map(|t| t.trim().to_camel_case())
|
||||
.map(|t| t.trim())
|
||||
.filter(|t| !t.is_empty())
|
||||
.collect::<HashSet<_>>();
|
||||
for tag in tags {
|
||||
Tag::insert(
|
||||
&*conn,
|
||||
NewTag {
|
||||
tag,
|
||||
tag: tag.to_string(),
|
||||
is_hashtag: false,
|
||||
post_id: post.id,
|
||||
},
|
||||
@ -507,7 +506,7 @@ pub fn create(
|
||||
Tag::insert(
|
||||
&*conn,
|
||||
NewTag {
|
||||
tag: hashtag.to_camel_case(),
|
||||
tag: hashtag,
|
||||
is_hashtag: true,
|
||||
post_id: post.id,
|
||||
},
|
||||
|
@ -48,38 +48,19 @@ pub fn create(
|
||||
rockets: PlumeRocket,
|
||||
) -> RespondOrRedirect {
|
||||
let conn = &*rockets.conn;
|
||||
let user = User::find_by_email(&*conn, &form.email_or_name)
|
||||
.or_else(|_| User::find_by_fqn(&rockets, &form.email_or_name));
|
||||
let mut errors = match form.validate() {
|
||||
Ok(_) => ValidationErrors::new(),
|
||||
Err(e) => e,
|
||||
};
|
||||
|
||||
let user = User::login(conn, &form.email_or_name, &form.password);
|
||||
let user_id = if let Ok(user) = user {
|
||||
if !user.auth(&form.password) {
|
||||
let mut err = ValidationError::new("invalid_login");
|
||||
err.message = Some(Cow::from("Invalid username, or password"));
|
||||
errors.add("email_or_name", err);
|
||||
String::new()
|
||||
} else {
|
||||
user.id.to_string()
|
||||
}
|
||||
} else {
|
||||
// Fake password verification, only to avoid different login times
|
||||
// that could be used to see if an email adress is registered or not
|
||||
User::get(&*conn, 1)
|
||||
.map(|u| u.auth(&form.password))
|
||||
.expect("No user is registered");
|
||||
|
||||
let mut err = ValidationError::new("invalid_login");
|
||||
err.message = Some(Cow::from("Invalid username, or password"));
|
||||
errors.add("email_or_name", err);
|
||||
String::new()
|
||||
};
|
||||
|
||||
if !errors.is_empty() {
|
||||
return render!(session::login(&rockets.to_context(), None, &*form, errors)).into();
|
||||
}
|
||||
};
|
||||
|
||||
cookies.add_private(
|
||||
Cookie::build(AUTH_COOKIE, user_id)
|
||||
|
@ -541,7 +541,7 @@ pub fn create(
|
||||
Role::Normal,
|
||||
"",
|
||||
form.email.to_string(),
|
||||
User::hash_pass(&form.password).map_err(to_validation)?,
|
||||
Some(User::hash_pass(&form.password).map_err(to_validation)?),
|
||||
).map_err(to_validation)?;
|
||||
Ok(Flash::success(
|
||||
Redirect::to(uri!(super::session::new: m = _)),
|
||||
|
@ -10,9 +10,6 @@
|
||||
@:base(ctx, tl.name.clone(), {}, {}, {
|
||||
<section class="flex wrap" dir="auto">
|
||||
<h1 class="grow">@i18n_timeline_name(ctx.1, &tl.name)</h1>
|
||||
@if ctx.clone().2.map(|u| (u.is_admin() && tl.user_id.is_none()) || Some(u.id) == tl.user_id).unwrap_or(false) {
|
||||
<a href="@uri!(timelines::edit: _id = tl.id)" class="button inline-block">@i18n!(ctx.1, "Edit")</a>
|
||||
}
|
||||
</section>
|
||||
|
||||
@tabs(&vec![(format!("{}", uri!(instance::index)), i18n!(ctx.1, "Latest articles"), false)]
|
||||
|
Loading…
Reference in New Issue
Block a user