Globální bannery
Tento commit je obsažen v:
rodič
1eeffc71c6
revize
bde9a95bd6
@ -1 +0,0 @@
|
|||||||
DROP TABLE accounts, boards, bans;
|
|
6
migrations/20231216092451_global_banners.sql
Normální soubor
6
migrations/20231216092451_global_banners.sql
Normální soubor
@ -0,0 +1,6 @@
|
|||||||
|
ALTER TABLE boards DROP COLUMN banners;
|
||||||
|
|
||||||
|
CREATE TABLE banners (
|
||||||
|
id SERIAL NOT NULL PRIMARY KEY,
|
||||||
|
banner JSONB NOT NULL
|
||||||
|
);
|
@ -31,7 +31,6 @@ pub struct ServerCfg {
|
|||||||
pub struct SiteCfg {
|
pub struct SiteCfg {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub description: String,
|
pub description: String,
|
||||||
pub site_banner: Option<String>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Clone)]
|
#[derive(Deserialize, Clone)]
|
||||||
|
64
src/db/banner.rs
Normální soubor
64
src/db/banner.rs
Normální soubor
@ -0,0 +1,64 @@
|
|||||||
|
use redis::AsyncCommands;
|
||||||
|
use sqlx::{query, query_as, types::Json};
|
||||||
|
|
||||||
|
use super::models::{Banner, File};
|
||||||
|
use crate::{ctx::Ctx, NekrochanError};
|
||||||
|
|
||||||
|
impl Banner {
|
||||||
|
pub async fn create(ctx: &Ctx, banner: File) -> Result<Self, NekrochanError> {
|
||||||
|
let banner: Self = query_as("INSERT INTO banners (banner) VALUES ($1) RETURNING *")
|
||||||
|
.bind(Json(banner))
|
||||||
|
.fetch_one(ctx.db())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
ctx.cache()
|
||||||
|
.zadd("banners", serde_json::to_string(&banner)?, banner.id)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(banner)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn read(ctx: &Ctx, id: i32) -> Result<Option<Self>, NekrochanError> {
|
||||||
|
let banners: Vec<String> = ctx.cache().zrangebyscore("banners", id, id).await?;
|
||||||
|
let json = banners.get(0);
|
||||||
|
|
||||||
|
let banner = match json {
|
||||||
|
Some(json) => Some(serde_json::from_str(json)?),
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(banner)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn read_all(ctx: &Ctx) -> Result<Vec<Self>, NekrochanError> {
|
||||||
|
let banners_str: Vec<String> = ctx.cache().zrange("banners", 0, -1).await?;
|
||||||
|
let banners_json = format!("[{}]", banners_str.join(",")); // If it works, it works
|
||||||
|
let banners = serde_json::from_str(&banners_json)?;
|
||||||
|
|
||||||
|
Ok(banners)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn read_random(ctx: &Ctx) -> Result<Option<Self>, NekrochanError> {
|
||||||
|
let banner: Option<String> = ctx.cache().zrandmember("banners", None).await?;
|
||||||
|
|
||||||
|
let banner = match banner {
|
||||||
|
Some(json) => Some(serde_json::from_str(&json)?),
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(banner)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn remove(&self, ctx: &Ctx) -> Result<(), NekrochanError> {
|
||||||
|
self.banner.delete().await;
|
||||||
|
|
||||||
|
query("DELETE FROM banners WHERE id = $1")
|
||||||
|
.bind(self.id)
|
||||||
|
.execute(ctx.db())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
ctx.cache().zrembyscore("banners", self.id, self.id).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,4 @@
|
|||||||
use captcha::{gen, Difficulty};
|
use captcha::{gen, Difficulty};
|
||||||
use rand::{seq::SliceRandom, thread_rng};
|
|
||||||
use redis::{cmd, AsyncCommands, JsonAsyncCommands};
|
use redis::{cmd, AsyncCommands, JsonAsyncCommands};
|
||||||
use sha256::digest;
|
use sha256::digest;
|
||||||
use sqlx::{query, query_as, types::Json};
|
use sqlx::{query, query_as, types::Json};
|
||||||
@ -207,12 +206,6 @@ impl Board {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Board {
|
impl Board {
|
||||||
pub fn random_banner(&self) -> Option<File> {
|
|
||||||
self.banners
|
|
||||||
.choose(&mut thread_rng())
|
|
||||||
.map(std::clone::Clone::clone)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn thread_captcha(&self) -> Option<(String, String)> {
|
pub fn thread_captcha(&self) -> Option<(String, String)> {
|
||||||
let captcha = match self.config.thread_captcha.as_str() {
|
let captcha = match self.config.thread_captcha.as_str() {
|
||||||
"easy" => gen(Difficulty::Easy),
|
"easy" => gen(Difficulty::Easy),
|
||||||
|
@ -3,7 +3,7 @@ use redis::{cmd, AsyncCommands, JsonAsyncCommands};
|
|||||||
use sha256::digest;
|
use sha256::digest;
|
||||||
use sqlx::query_as;
|
use sqlx::query_as;
|
||||||
|
|
||||||
use super::models::{Account, Board, Post};
|
use super::models::{Account, Banner, Board, Post};
|
||||||
use crate::ctx::Ctx;
|
use crate::ctx::Ctx;
|
||||||
|
|
||||||
pub async fn init_cache(ctx: &Ctx) -> Result<(), Error> {
|
pub async fn init_cache(ctx: &Ctx) -> Result<(), Error> {
|
||||||
@ -29,6 +29,16 @@ pub async fn init_cache(ctx: &Ctx) -> Result<(), Error> {
|
|||||||
ctx.cache().lpush("board_ids", &board.id).await?;
|
ctx.cache().lpush("board_ids", &board.id).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let banners: Vec<Banner> = query_as("SELECT * FROM banners")
|
||||||
|
.fetch_all(ctx.db())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
for banner in &banners {
|
||||||
|
ctx.cache()
|
||||||
|
.zadd("banners", serde_json::to_string(banner)?, banner.id)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
cmd("SORT")
|
cmd("SORT")
|
||||||
.arg("board_ids")
|
.arg("board_ids")
|
||||||
.arg("ALPHA")
|
.arg("ALPHA")
|
||||||
|
@ -3,5 +3,6 @@ pub mod models;
|
|||||||
|
|
||||||
mod account;
|
mod account;
|
||||||
mod ban;
|
mod ban;
|
||||||
|
mod banner;
|
||||||
mod board;
|
mod board;
|
||||||
mod post;
|
mod post;
|
||||||
|
@ -7,7 +7,7 @@ use sqlx::{types::Json, FromRow};
|
|||||||
|
|
||||||
use crate::cfg::BoardCfg;
|
use crate::cfg::BoardCfg;
|
||||||
|
|
||||||
#[derive(FromRow, Clone, Serialize, Deserialize)]
|
#[derive(FromRow, Serialize, Deserialize, Clone)]
|
||||||
pub struct Account {
|
pub struct Account {
|
||||||
pub username: String,
|
pub username: String,
|
||||||
pub password: String,
|
pub password: String,
|
||||||
@ -21,7 +21,6 @@ pub struct Board {
|
|||||||
pub id: String,
|
pub id: String,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub description: String,
|
pub description: String,
|
||||||
pub banners: Json<Vec<File>>,
|
|
||||||
pub config: Json<BoardCfg>,
|
pub config: Json<BoardCfg>,
|
||||||
pub created: DateTime<Utc>,
|
pub created: DateTime<Utc>,
|
||||||
}
|
}
|
||||||
@ -90,3 +89,9 @@ pub struct File {
|
|||||||
pub timestamp: i64,
|
pub timestamp: i64,
|
||||||
pub size: usize,
|
pub size: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(FromRow, Serialize, Deserialize)]
|
||||||
|
pub struct Banner {
|
||||||
|
pub id: i32,
|
||||||
|
pub banner: Json<File>,
|
||||||
|
}
|
||||||
|
@ -333,6 +333,10 @@ impl Post {
|
|||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
for post in &to_be_deleted {
|
for post in &to_be_deleted {
|
||||||
|
for file in post.files.iter() {
|
||||||
|
file.delete().await;
|
||||||
|
}
|
||||||
|
|
||||||
let id = post.id;
|
let id = post.id;
|
||||||
let url = post.post_url();
|
let url = post.post_url();
|
||||||
|
|
||||||
@ -381,7 +385,7 @@ impl Post {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn delete_files(&self, ctx: &Ctx) -> Result<(), NekrochanError> {
|
pub async fn delete_files(&self, ctx: &Ctx) -> Result<(), NekrochanError> {
|
||||||
for file in self.files.iter() {
|
for file in &self.files.0 {
|
||||||
file.delete().await;
|
file.delete().await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
15
src/files.rs
15
src/files.rs
@ -11,7 +11,7 @@ use tokio::{
|
|||||||
use crate::{
|
use crate::{
|
||||||
cfg::Cfg,
|
cfg::Cfg,
|
||||||
ctx::Ctx,
|
ctx::Ctx,
|
||||||
db::models::{Board, File, Post},
|
db::models::{Banner, Board, File, Post},
|
||||||
error::NekrochanError,
|
error::NekrochanError,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -310,13 +310,18 @@ pub async fn cleanup_files(ctx: &Ctx) -> Result<(), Error> {
|
|||||||
let mut keep = HashSet::new();
|
let mut keep = HashSet::new();
|
||||||
let mut keep_thumbs = HashSet::new();
|
let mut keep_thumbs = HashSet::new();
|
||||||
|
|
||||||
|
let banners = Banner::read_all(ctx).await?;
|
||||||
|
|
||||||
|
for banner in banners {
|
||||||
|
keep.insert(format!(
|
||||||
|
"{}.{}",
|
||||||
|
banner.banner.timestamp, banner.banner.format
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
let boards = Board::read_all(ctx).await?;
|
let boards = Board::read_all(ctx).await?;
|
||||||
|
|
||||||
for board in boards {
|
for board in boards {
|
||||||
for file in board.banners.0 {
|
|
||||||
keep.insert(format!("{}.{}", file.timestamp, file.format));
|
|
||||||
}
|
|
||||||
|
|
||||||
let posts = Post::read_all(ctx, board.id.clone()).await?;
|
let posts = Post::read_all(ctx, board.id.clone()).await?;
|
||||||
|
|
||||||
for post in posts {
|
for post in posts {
|
||||||
|
34
src/main.rs
34
src/main.rs
@ -3,11 +3,10 @@ use actix_web::{
|
|||||||
body::MessageBody,
|
body::MessageBody,
|
||||||
dev::ServiceResponse,
|
dev::ServiceResponse,
|
||||||
get,
|
get,
|
||||||
http::StatusCode,
|
http::header::{HeaderValue, CACHE_CONTROL, PRAGMA},
|
||||||
middleware::{ErrorHandlerResponse, ErrorHandlers},
|
middleware::{ErrorHandlerResponse, ErrorHandlers},
|
||||||
post,
|
|
||||||
web::Data,
|
web::Data,
|
||||||
App, HttpResponse, HttpServer, ResponseError,
|
App, HttpRequest, HttpResponse, HttpServer, ResponseError,
|
||||||
};
|
};
|
||||||
use anyhow::Error;
|
use anyhow::Error;
|
||||||
use askama::Template;
|
use askama::Template;
|
||||||
@ -15,7 +14,7 @@ use log::{error, info};
|
|||||||
use nekrochan::{
|
use nekrochan::{
|
||||||
cfg::Cfg,
|
cfg::Cfg,
|
||||||
ctx::Ctx,
|
ctx::Ctx,
|
||||||
db::cache::init_cache,
|
db::{cache::init_cache, models::Banner},
|
||||||
error::NekrochanError,
|
error::NekrochanError,
|
||||||
files::cleanup_files,
|
files::cleanup_files,
|
||||||
web::{self, template_response},
|
web::{self, template_response},
|
||||||
@ -98,8 +97,8 @@ async fn run() -> Result<(), Error> {
|
|||||||
.service(web::staff::actions::update_board_config::update_board_config)
|
.service(web::staff::actions::update_board_config::update_board_config)
|
||||||
.service(web::staff::actions::update_boards::update_boards)
|
.service(web::staff::actions::update_boards::update_boards)
|
||||||
.service(web::staff::actions::update_permissions::update_permissions)
|
.service(web::staff::actions::update_permissions::update_permissions)
|
||||||
.service(debug)
|
|
||||||
.service(favicon)
|
.service(favicon)
|
||||||
|
.service(random_banner)
|
||||||
.service(Files::new("/static", "./static"))
|
.service(Files::new("/static", "./static"))
|
||||||
.service(Files::new("/uploads", "./uploads").disable_content_disposition())
|
.service(Files::new("/uploads", "./uploads").disable_content_disposition())
|
||||||
.wrap(ErrorHandlers::new().default_handler(error_handler))
|
.wrap(ErrorHandlers::new().default_handler(error_handler))
|
||||||
@ -118,11 +117,28 @@ async fn favicon() -> Result<NamedFile, NekrochanError> {
|
|||||||
Ok(favicon)
|
Ok(favicon)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/debug")]
|
#[get("/random-banner")]
|
||||||
async fn debug(req: String) -> HttpResponse {
|
async fn random_banner(ctx: Data<Ctx>, req: HttpRequest) -> Result<HttpResponse, NekrochanError> {
|
||||||
println!("{req}");
|
let file = if let Some(banner) = Banner::read_random(&ctx).await? {
|
||||||
|
let timestamp = banner.banner.timestamp;
|
||||||
|
let format = &banner.banner.format;
|
||||||
|
|
||||||
HttpResponse::new(StatusCode::OK)
|
NamedFile::open(format!("./uploads/{timestamp}.{format}"))?
|
||||||
|
} else {
|
||||||
|
NamedFile::open("./static/default-banner.png")?
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut res = file.into_response(&req);
|
||||||
|
|
||||||
|
res.headers_mut().append(
|
||||||
|
CACHE_CONTROL,
|
||||||
|
HeaderValue::from_static("no-cache, no-store, must-revalidate"),
|
||||||
|
);
|
||||||
|
|
||||||
|
res.headers_mut()
|
||||||
|
.append(PRAGMA, HeaderValue::from_static("no-cache"));
|
||||||
|
|
||||||
|
Ok(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
|
@ -59,7 +59,7 @@ impl PermissionWrapper {
|
|||||||
self.0.contains(Permissions::Bans)
|
self.0.contains(Permissions::Bans)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn board_banners(&self) -> bool {
|
pub fn banners(&self) -> bool {
|
||||||
self.0.contains(Permissions::BoardBanners)
|
self.0.contains(Permissions::BoardBanners)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,16 +1,15 @@
|
|||||||
use actix_multipart::form::{tempfile::TempFile, text::Text, MultipartForm};
|
use actix_multipart::form::{tempfile::TempFile, MultipartForm};
|
||||||
use actix_web::{post, web::Data, HttpRequest, HttpResponse};
|
use actix_web::{post, web::Data, HttpRequest, HttpResponse};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
ctx::Ctx,
|
ctx::Ctx,
|
||||||
db::models::{Board, File},
|
db::models::{Banner, File},
|
||||||
error::NekrochanError,
|
error::NekrochanError,
|
||||||
web::tcx::account_from_auth,
|
web::tcx::account_from_auth,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(MultipartForm)]
|
#[derive(MultipartForm)]
|
||||||
pub struct AddBannersForm {
|
pub struct AddBannersForm {
|
||||||
board: Text<String>,
|
|
||||||
#[multipart(rename = "files[]")]
|
#[multipart(rename = "files[]")]
|
||||||
files: Vec<TempFile>,
|
files: Vec<TempFile>,
|
||||||
}
|
}
|
||||||
@ -23,32 +22,20 @@ pub async fn add_banners(
|
|||||||
) -> Result<HttpResponse, NekrochanError> {
|
) -> Result<HttpResponse, NekrochanError> {
|
||||||
let account = account_from_auth(&ctx, &req).await?;
|
let account = account_from_auth(&ctx, &req).await?;
|
||||||
|
|
||||||
if !(account.perms().owner() || account.perms().board_banners()) {
|
if !(account.perms().owner() || account.perms().banners()) {
|
||||||
return Err(NekrochanError::InsufficientPermissionError);
|
return Err(NekrochanError::InsufficientPermissionError);
|
||||||
}
|
}
|
||||||
|
|
||||||
let board = form.board.0;
|
|
||||||
let board = Board::read(&ctx, board.clone())
|
|
||||||
.await?
|
|
||||||
.ok_or(NekrochanError::BoardNotFound(board))?;
|
|
||||||
|
|
||||||
let mut new_banners = board.banners.0.clone();
|
|
||||||
let added_banners = form.files;
|
|
||||||
|
|
||||||
let mut cfg = ctx.cfg.clone();
|
let mut cfg = ctx.cfg.clone();
|
||||||
|
|
||||||
cfg.files.videos = false;
|
cfg.files.videos = false;
|
||||||
|
|
||||||
for banner in added_banners {
|
for file in form.files {
|
||||||
let file = File::new(&cfg, banner, false, false).await?;
|
Banner::create(&ctx, File::new(&cfg, file, false, false).await?).await?;
|
||||||
|
|
||||||
new_banners.push(file);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
board.update_banners(&ctx, new_banners).await?;
|
|
||||||
|
|
||||||
let res = HttpResponse::SeeOther()
|
let res = HttpResponse::SeeOther()
|
||||||
.append_header(("Location", format!("/staff/banners/{}", board.id).as_str()))
|
.append_header(("Location", "/staff/banners"))
|
||||||
.finish();
|
.finish();
|
||||||
|
|
||||||
Ok(res)
|
Ok(res)
|
||||||
|
@ -2,14 +2,14 @@ use actix_web::{post, web::Data, HttpRequest, HttpResponse};
|
|||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
ctx::Ctx, db::models::Board, error::NekrochanError, qsform::QsForm, web::tcx::account_from_auth,
|
ctx::Ctx, db::models::Banner, error::NekrochanError, qsform::QsForm,
|
||||||
|
web::tcx::account_from_auth,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct RemoveBannersForm {
|
pub struct RemoveBannersForm {
|
||||||
board: String,
|
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
banners: Vec<usize>,
|
banners: Vec<i32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/staff/actions/remove-banners")]
|
#[post("/staff/actions/remove-banners")]
|
||||||
@ -20,30 +20,18 @@ pub async fn remove_banners(
|
|||||||
) -> Result<HttpResponse, NekrochanError> {
|
) -> Result<HttpResponse, NekrochanError> {
|
||||||
let account = account_from_auth(&ctx, &req).await?;
|
let account = account_from_auth(&ctx, &req).await?;
|
||||||
|
|
||||||
if !(account.perms().owner() || account.perms().board_banners()) {
|
if !(account.perms().owner() || account.perms().banners()) {
|
||||||
return Err(NekrochanError::InsufficientPermissionError);
|
return Err(NekrochanError::InsufficientPermissionError);
|
||||||
}
|
}
|
||||||
|
|
||||||
let board = form.board;
|
for id in form.banners {
|
||||||
let board = Board::read(&ctx, board.clone())
|
if let Some(banner) = Banner::read(&ctx, id).await? {
|
||||||
.await?
|
banner.remove(&ctx).await?;
|
||||||
.ok_or(NekrochanError::BoardNotFound(board))?;
|
|
||||||
|
|
||||||
let old_banners = board.banners.0.clone();
|
|
||||||
let mut new_banners = Vec::new();
|
|
||||||
|
|
||||||
for (i, banner) in old_banners.into_iter().enumerate() {
|
|
||||||
if form.banners.contains(&i) {
|
|
||||||
banner.delete().await;
|
|
||||||
} else {
|
|
||||||
new_banners.push(banner);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
board.update_banners(&ctx, new_banners).await?;
|
|
||||||
|
|
||||||
let res = HttpResponse::SeeOther()
|
let res = HttpResponse::SeeOther()
|
||||||
.append_header(("Location", format!("/staff/banners/{}", board.id).as_str()))
|
.append_header(("Location", "/staff/banners"))
|
||||||
.finish();
|
.finish();
|
||||||
|
|
||||||
Ok(res)
|
Ok(res)
|
||||||
|
@ -16,7 +16,7 @@ pub struct UpdatePermissionsForm {
|
|||||||
staff_log: Option<String>,
|
staff_log: Option<String>,
|
||||||
reports: Option<String>,
|
reports: Option<String>,
|
||||||
bans: Option<String>,
|
bans: Option<String>,
|
||||||
board_banners: Option<String>,
|
banners: Option<String>,
|
||||||
board_config: Option<String>,
|
board_config: Option<String>,
|
||||||
bypass_bans: Option<String>,
|
bypass_bans: Option<String>,
|
||||||
bypass_board_lock: Option<String>,
|
bypass_board_lock: Option<String>,
|
||||||
@ -67,7 +67,7 @@ pub async fn update_permissions(
|
|||||||
permissions |= Permissions::Bans;
|
permissions |= Permissions::Bans;
|
||||||
}
|
}
|
||||||
|
|
||||||
if form.board_banners.is_some() {
|
if form.banners.is_some() {
|
||||||
permissions |= Permissions::BoardBanners;
|
permissions |= Permissions::BoardBanners;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,13 +1,9 @@
|
|||||||
use actix_web::{
|
use actix_web::{get, web::Data, HttpRequest, HttpResponse};
|
||||||
get,
|
|
||||||
web::{Data, Path},
|
|
||||||
HttpRequest, HttpResponse,
|
|
||||||
};
|
|
||||||
use askama::Template;
|
use askama::Template;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
ctx::Ctx,
|
ctx::Ctx,
|
||||||
db::models::Board,
|
db::models::Banner,
|
||||||
error::NekrochanError,
|
error::NekrochanError,
|
||||||
web::{
|
web::{
|
||||||
tcx::{account_from_auth, TemplateCtx},
|
tcx::{account_from_auth, TemplateCtx},
|
||||||
@ -19,28 +15,20 @@ use crate::{
|
|||||||
#[template(path = "staff/banners.html")]
|
#[template(path = "staff/banners.html")]
|
||||||
struct BannersTemplate {
|
struct BannersTemplate {
|
||||||
tcx: TemplateCtx,
|
tcx: TemplateCtx,
|
||||||
board: Board,
|
banners: Vec<Banner>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/staff/banners/{board}")]
|
#[get("/staff/banners")]
|
||||||
pub async fn banners(
|
pub async fn banners(ctx: Data<Ctx>, req: HttpRequest) -> Result<HttpResponse, NekrochanError> {
|
||||||
ctx: Data<Ctx>,
|
|
||||||
req: HttpRequest,
|
|
||||||
board: Path<String>,
|
|
||||||
) -> Result<HttpResponse, NekrochanError> {
|
|
||||||
let tcx = TemplateCtx::new(&ctx, &req).await?;
|
let tcx = TemplateCtx::new(&ctx, &req).await?;
|
||||||
let account = account_from_auth(&ctx, &req).await?;
|
let account = account_from_auth(&ctx, &req).await?;
|
||||||
|
|
||||||
if !(account.perms().owner() || account.perms().board_banners()) {
|
if !(account.perms().owner() || account.perms().banners()) {
|
||||||
return Err(NekrochanError::InsufficientPermissionError);
|
return Err(NekrochanError::InsufficientPermissionError);
|
||||||
}
|
}
|
||||||
|
|
||||||
let board = board.into_inner();
|
let banners = Banner::read_all(&ctx).await?;
|
||||||
let board = Board::read(&ctx, board.clone())
|
let template = BannersTemplate { tcx, banners };
|
||||||
.await?
|
|
||||||
.ok_or(NekrochanError::BoardNotFound(board))?;
|
|
||||||
|
|
||||||
let template = BannersTemplate { tcx, board };
|
|
||||||
|
|
||||||
template_response(&template)
|
template_response(&template)
|
||||||
}
|
}
|
||||||
|
@ -24,10 +24,7 @@ pub async fn boards(ctx: Data<Ctx>, req: HttpRequest) -> Result<HttpResponse, Ne
|
|||||||
let tcx = TemplateCtx::new(&ctx, &req).await?;
|
let tcx = TemplateCtx::new(&ctx, &req).await?;
|
||||||
let account = account_from_auth(&ctx, &req).await?;
|
let account = account_from_auth(&ctx, &req).await?;
|
||||||
|
|
||||||
if !(account.perms().owner()
|
if !(account.perms().owner() || account.perms().board_config() || account.perms().banners()) {
|
||||||
|| account.perms().board_config()
|
|
||||||
|| account.perms().board_banners())
|
|
||||||
{
|
|
||||||
return Err(NekrochanError::InsufficientPermissionError);
|
return Err(NekrochanError::InsufficientPermissionError);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
binární
static/banner.gif
binární
static/banner.gif
Binární soubor nebyl zobrazen.
Před Šířka: | Výška: | Velikost: 206 KiB |
binární
static/default-banner.png
Normální soubor
binární
static/default-banner.png
Normální soubor
Binární soubor nebyl zobrazen.
Za Šířka: | Výška: | Velikost: 19 KiB |
@ -8,9 +8,7 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="center">
|
<div class="center">
|
||||||
{% if let Some(banner) = board.random_banner() %}
|
<img class="banner" src="/random-banner">
|
||||||
<img class="banner" src="{{ banner.file_url() }}">
|
|
||||||
{% endif %}
|
|
||||||
<h1 class="title">Katalog (<a href="/boards/{{ board.id }}">/{{ board.id }}/</a>)</h1>
|
<h1 class="title">Katalog (<a href="/boards/{{ board.id }}">/{{ board.id }}/</a>)</h1>
|
||||||
<p class="description">{{ board.description }}</p>
|
<p class="description">{{ board.description }}</p>
|
||||||
<a href="/boards/{{ board.id }}">Index</a>
|
<a href="/boards/{{ board.id }}">Index</a>
|
||||||
|
@ -10,9 +10,7 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="center">
|
<div class="center">
|
||||||
{% if let Some(banner) = board.random_banner() %}
|
<img class="banner" src="/random-banner">
|
||||||
<img class="banner" src="{{ banner.file_url() }}">
|
|
||||||
{% endif %}
|
|
||||||
<h1 class="title">/{{ board.id }}/ - {{ board.name }}</h1>
|
<h1 class="title">/{{ board.id }}/ - {{ board.name }}</h1>
|
||||||
<p class="description">{{ board.description }}</p>
|
<p class="description">{{ board.description }}</p>
|
||||||
<a href="/boards/{{ board.id }}/catalog">Katalog</a>
|
<a href="/boards/{{ board.id }}/catalog">Katalog</a>
|
||||||
|
@ -5,9 +5,6 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="center">
|
<div class="center">
|
||||||
{% if let Some(banner) = tcx.cfg.site.site_banner %}
|
|
||||||
<img class="banner" src="{{ banner }}">
|
|
||||||
{% endif %}
|
|
||||||
<h1 class="title">{{ tcx.cfg.site.name }}</h1>
|
<h1 class="title">{{ tcx.cfg.site.name }}</h1>
|
||||||
<p class="description">{{ tcx.cfg.site.description }}</p>
|
<p class="description">{{ tcx.cfg.site.description }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
<a href="/staff/account">[Účet]</a> 
|
<a href="/staff/account">[Účet]</a> 
|
||||||
<a href="/staff/accounts">[Účty]</a> 
|
<a href="/staff/accounts">[Účty]</a> 
|
||||||
|
|
||||||
{% if perms.owner() || perms.board_config() || perms.board_banners() %}
|
{% if perms.owner() || perms.board_config() || perms.banners() %}
|
||||||
<a href="/staff/boards">[Nástěnky]</a> 
|
<a href="/staff/boards">[Nástěnky]</a> 
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
@ -11,6 +11,10 @@
|
|||||||
<a href="/staff/bans">[Bany]</a> 
|
<a href="/staff/bans">[Bany]</a> 
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{% if perms.owner() || perms.banners() %}
|
||||||
|
<a href="/staff/banners">[Bannery]</a> 
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% if perms.owner() || perms.reports() %}
|
{% if perms.owner() || perms.reports() %}
|
||||||
<a href="/staff/reports">[Hlášení]</a> 
|
<a href="/staff/reports">[Hlášení]</a> 
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -8,9 +8,7 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="center">
|
<div class="center">
|
||||||
{% if let Some(banner) = tcx.cfg.site.site_banner %}
|
<img class="banner" src="/random-banner">
|
||||||
<img class="banner" src="{{ banner }}">
|
|
||||||
{% endif %}
|
|
||||||
<h1 class="title">Katalog nadnástěnky</h1>
|
<h1 class="title">Katalog nadnástěnky</h1>
|
||||||
<p class="description">Nově naťuknutá vlákna ze všech nástěnek</p>
|
<p class="description">Nově naťuknutá vlákna ze všech nástěnek</p>
|
||||||
<a href="/overboard">Index</a>
|
<a href="/overboard">Index</a>
|
||||||
|
@ -9,9 +9,7 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="center">
|
<div class="center">
|
||||||
{% if let Some(banner) = tcx.cfg.site.site_banner %}
|
<img class="banner" src="/random-banner">
|
||||||
<img class="banner" src="{{ banner }}">
|
|
||||||
{% endif %}
|
|
||||||
<h1 class="title">Index nadnástěnky</h1>
|
<h1 class="title">Index nadnástěnky</h1>
|
||||||
<p class="description">Nově naťuknutá vlákna ze všech nástěnek</p>
|
<p class="description">Nově naťuknutá vlákna ze všech nástěnek</p>
|
||||||
<a href="/overboard/catalog">Katalog</a>
|
<a href="/overboard/catalog">Katalog</a>
|
||||||
|
@ -2,27 +2,25 @@
|
|||||||
|
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block title %}Bannery (/{{ board.id }}/){% endblock %}
|
{% block title %}Bannery{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h1 class="title">Bannery (/{{ board.id }}/)</h1>
|
<h1 class="title">Bannery</h1>
|
||||||
{% call staff_nav::staff_nav(tcx.perms) %}
|
{% call staff_nav::staff_nav(tcx.perms) %}
|
||||||
<hr>
|
<hr>
|
||||||
<h2>Bannery</h2>
|
<h2>Bannery</h2>
|
||||||
<form method="post" action="/staff/actions/remove-banners">
|
<form method="post" action="/staff/actions/remove-banners">
|
||||||
<input name="board" type="hidden" value="{{ board.id }}">
|
|
||||||
|
|
||||||
<div class="table-wrap">
|
<div class="table-wrap">
|
||||||
<table class="data-table">
|
<table class="data-table">
|
||||||
<tr>
|
<tr>
|
||||||
<th></th>
|
<th></th>
|
||||||
<th>Banner</th>
|
<th>Banner</th>
|
||||||
</tr>
|
</tr>
|
||||||
{% for banner in board.banners.0 %}
|
{% for banner in banners %}
|
||||||
<tr>
|
<tr>
|
||||||
<td><input name="banners[]" type="checkbox" value="{{ loop.index0 }}"></td>
|
<td><input name="banners[]" type="checkbox" value="{{ banner.id }}"></td>
|
||||||
<td>
|
<td>
|
||||||
<img class="banner" src="{{ banner.file_url() }}">
|
<img class="banner" src="{{ banner.banner.file_url() }}">
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@ -34,8 +32,6 @@
|
|||||||
<hr>
|
<hr>
|
||||||
<h2>Přidat bannery</h2>
|
<h2>Přidat bannery</h2>
|
||||||
<form method="post" enctype="multipart/form-data" action="/staff/actions/add-banners">
|
<form method="post" enctype="multipart/form-data" action="/staff/actions/add-banners">
|
||||||
<input name="board" type="hidden" value="{{ board.id }}">
|
|
||||||
|
|
||||||
<table class="form-table">
|
<table class="form-table">
|
||||||
<tr>
|
<tr>
|
||||||
<td class="label">Bannery</td>
|
<td class="label">Bannery</td>
|
||||||
|
@ -18,7 +18,6 @@
|
|||||||
<th>Jméno</th>
|
<th>Jméno</th>
|
||||||
<th>Popis</th>
|
<th>Popis</th>
|
||||||
<th>Vytvořena</th>
|
<th>Vytvořena</th>
|
||||||
<th>Bannery</th>
|
|
||||||
<th>Nastavení</th>
|
<th>Nastavení</th>
|
||||||
</tr>
|
</tr>
|
||||||
{% for board in boards %}
|
{% for board in boards %}
|
||||||
@ -28,7 +27,6 @@
|
|||||||
<td>{{ board.name }}</td>
|
<td>{{ board.name }}</td>
|
||||||
<td>{{ board.description }}</td>
|
<td>{{ board.description }}</td>
|
||||||
<td>{{ board.created|czech_datetime }}</td>
|
<td>{{ board.created|czech_datetime }}</td>
|
||||||
<td>{% if tcx.perms.owner() || tcx.perms.board_banners() %}<a href="/staff/banners/{{ board.id }}">[Zobrazit]</a>{% else %}-{% endif %}</td>
|
|
||||||
<td>{% if tcx.perms.owner() || tcx.perms.board_config() %}<a href="/staff/board-config/{{ board.id }}">[Zobrazit]</a>{% else %}-{% endif %}</td>
|
<td>{% if tcx.perms.owner() || tcx.perms.board_config() %}<a href="/staff/board-config/{{ board.id }}">[Zobrazit]</a>{% else %}-{% endif %}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
@ -71,10 +71,10 @@
|
|||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<td class="label">Bannery nástěnek</td>
|
<td class="label">Bannery</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="input-wrapper">
|
<div class="input-wrapper">
|
||||||
<input name="board_banners" type="checkbox"{% if account.perms().board_banners() %} checked="checked"{% endif %}{% if !tcx.perms.owner() %} disabled=""{% endif %}>
|
<input name="banners" type="checkbox"{% if account.perms().banners() %} checked="checked"{% endif %}{% if !tcx.perms.owner() %} disabled=""{% endif %}>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
@ -9,9 +9,7 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="center">
|
<div class="center">
|
||||||
{% if let Some(banner) = board.random_banner() %}
|
<img class="banner" src="/random-banner">
|
||||||
<img class="banner" src="{{ banner.file_url() }}">
|
|
||||||
{% endif %}
|
|
||||||
<h1 class="title">/{{ board.id }}/ - {{ board.name }}</h1>
|
<h1 class="title">/{{ board.id }}/ - {{ board.name }}</h1>
|
||||||
<p class="description">{{ board.description }}</p>
|
<p class="description">{{ board.description }}</p>
|
||||||
<a href="/boards/{{ board.id }}/catalog">Katalog</a>
|
<a href="/boards/{{ board.id }}/catalog">Katalog</a>
|
||||||
|
Načítá se…
Odkázat v novém úkolu
Zablokovat Uživatele