add more routes, channel id to presets, no full paths, change email to mail

This commit is contained in:
jb-alvarado 2022-06-30 18:44:42 +02:00
parent ecccc86264
commit 7352735f15
13 changed files with 181 additions and 87 deletions

2
Cargo.lock generated
View File

@ -1027,7 +1027,7 @@ dependencies = [
[[package]]
name = "ffplayout-api"
version = "0.3.2"
version = "0.3.3"
dependencies = [
"actix-multipart",
"actix-web",

View File

@ -1,15 +1,6 @@
[workspace]
members = [
"ffplayout-api",
"ffplayout-engine",
"lib",
]
default-members = [
"ffplayout-api",
"ffplayout-engine",
]
members = ["ffplayout-api", "ffplayout-engine", "lib"]
default-members = ["ffplayout-api", "ffplayout-engine"]
[profile.release]
opt-level = 3

View File

@ -32,6 +32,9 @@ From here on all request **must** contain the authorization header:\
#### User
- **GET** `/api/user`\
Get current user, response is in JSON format
- **PUT** `/api/user/{user id}`\
JSON Data: `{"email": "<EMAIL>", "password": "<PASS>"}`
@ -48,8 +51,10 @@ JSON Data:
#### API Settings
- **GET** `/api/settings`\
Response is in JSON format
- **GET** `/api/settings/{id}`\
HEADER:
Response is in JSON format
- **PATCH** `/api/settings/{id}`\
@ -157,11 +162,17 @@ Response is in JSON format
- **DELETE** `/api/playlist/{id}/2022-06-20`\
Response is in TEXT format
#### File Operations
#### Log File
- **GET** `/api/file/{id}/browse/`\
Response is in JSON format
#### File Operations
- **GET** `/api/log/{id}(/{date})`\
Response is in TEXT format
- **POST** `/api/file/{id}/move/`\
JSON Data: `{"source": "<SOURCE>", "target": "<TARGET>"}`\
Response is in JSON format

View File

@ -4,7 +4,7 @@ description = "Rest API for ffplayout"
license = "GPL-3.0"
authors = ["Jonathan Baecker jonbae77@gmail.com"]
readme = "README.md"
version = "0.3.2"
version = "0.3.3"
edition = "2021"
[dependencies]

View File

@ -12,7 +12,7 @@ ffpapi -i
Then add an admin user:
```BASH
ffpapi -u <USERNAME> -p <PASSWORD> -e <EMAIL ADDRESS>
ffpapi -u <USERNAME> -p <PASSWORD> -m <MAIL ADDRESS>
```
Then run the API thru the systemd service, or like:

View File

@ -15,11 +15,11 @@ use utils::{
auth, db_path, init_config,
models::LoginUser,
routes::{
add_preset, add_user, del_playlist, file_browser, gen_playlist, get_playlist,
get_playout_config, get_presets, get_settings, jump_to_last, jump_to_next, login,
media_current, media_last, media_next, move_rename, patch_settings, process_control,
remove, reset_playout, save_file, save_playlist, send_text_message, update_playout_config,
update_preset, update_user,
add_preset, add_user, del_playlist, file_browser, gen_playlist, get_all_settings, get_log,
get_playlist, get_playout_config, get_presets, get_settings, get_user, jump_to_last,
jump_to_next, login, media_current, media_last, media_next, move_rename, patch_settings,
process_control, remove, reset_playout, save_file, save_playlist, send_text_message,
update_playout_config, update_preset, update_user,
},
run_args, Role,
};
@ -77,12 +77,14 @@ async fn main() -> std::io::Result<()> {
web::scope("/api")
.wrap(auth)
.service(add_user)
.service(get_user)
.service(get_playout_config)
.service(update_playout_config)
.service(add_preset)
.service(get_presets)
.service(update_preset)
.service(get_settings)
.service(get_all_settings)
.service(patch_settings)
.service(update_user)
.service(send_text_message)
@ -97,6 +99,7 @@ async fn main() -> std::io::Result<()> {
.service(save_playlist)
.service(gen_playlist)
.service(del_playlist)
.service(get_log)
.service(file_browser)
.service(move_rename)
.service(remove)

View File

@ -17,8 +17,8 @@ pub struct Args {
#[clap(short, long, help = "Create admin user")]
pub username: Option<String>,
#[clap(short, long, help = "Admin email")]
pub email: Option<String>,
#[clap(short, long, help = "Admin mail address")]
pub mail: Option<String>,
#[clap(short, long, help = "Admin password")]
pub password: Option<String>,

View File

@ -62,22 +62,22 @@ pub async fn browser(id: i64, path_obj: &PathObject) -> Result<PathObject, Servi
for path in paths {
let file_path = path.path().to_owned();
let path_str = file_path.display().to_string();
let path = file_path.clone();
// ignore hidden files/folders on unix
if path_str.contains("/.") {
if path.display().to_string().contains("/.") {
continue;
}
if file_path.is_dir() {
if let Some(ref mut folders) = obj.folders {
folders.push(path_str);
folders.push(path.file_name().unwrap().to_string_lossy().to_string());
}
} else if file_path.is_file() {
if let Some(ext) = file_extension(&file_path) {
if extensions.contains(&ext.to_string().to_lowercase()) {
if let Some(ref mut files) = obj.files {
files.push(path_str);
files.push(path.file_name().unwrap().to_string_lossy().to_string());
}
}
}

View File

@ -36,6 +36,7 @@ async fn create_schema() -> Result<SqliteQueryResult, sqlx::Error> {
CREATE TABLE IF NOT EXISTS presets
(
id INTEGER PRIMARY KEY AUTOINCREMENT,
channel_id INTEGER,
name TEXT NOT NULL,
text TEXT NOT NULL,
x TEXT NOT NULL,
@ -56,19 +57,20 @@ async fn create_schema() -> Result<SqliteQueryResult, sqlx::Error> {
preview_url TEXT NOT NULL,
config_path TEXT NOT NULL,
extra_extensions TEXT NOT NULL,
service TEXT NOT NULL,
timezone TEXT NOT NULL,
service TEXT NOT NULL,
UNIQUE(channel_name)
);
CREATE TABLE IF NOT EXISTS user
(
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT NOT NULL,
mail TEXT NOT NULL,
username TEXT NOT NULL,
password TEXT NOT NULL,
salt TEXT NOT NULL,
role_id INTEGER NOT NULL DEFAULT 2,
FOREIGN KEY (role_id) REFERENCES roles (id) ON UPDATE SET NULL ON DELETE SET NULL,
UNIQUE(email, username)
UNIQUE(mail, username)
);";
let result = sqlx::query(query).execute(&conn).await;
conn.close().await;
@ -101,17 +103,17 @@ pub async fn db_init() -> Result<&'static str, Box<dyn std::error::Error>> {
SELECT RAISE(FAIL, 'Database is already init!');
END;
INSERT INTO global(secret) VALUES($1);
INSERT INTO presets(name, text, x, y, fontsize, line_spacing, fontcolor, alpha, box, boxcolor, boxborderw)
VALUES('Default', 'Wellcome to ffplayout messenger!', '(w-text_w)/2', '(h-text_h)/2', '24', '4', '#ffffff@0xff', '1.0', '0', '#000000@0x80', '4'),
('Empty Text', '', '0', '0', '24', '4', '#000000', '0', '0', '#000000', '0'),
('Bottom Text fade in', 'The upcoming event will be delayed by a few minutes.', '(w-text_w)/2', '(h-line_h)*0.9', '24', '4', '#ffffff',
INSERT INTO presets(channel_id, name, text, x, y, fontsize, line_spacing, fontcolor, alpha, box, boxcolor, boxborderw)
VALUES('1', 'Default', 'Wellcome to ffplayout messenger!', '(w-text_w)/2', '(h-text_h)/2', '24', '4', '#ffffff@0xff', '1.0', '0', '#000000@0x80', '4'),
('1', 'Empty Text', '', '0', '0', '24', '4', '#000000', '0', '0', '#000000', '0'),
('1', 'Bottom Text fade in', 'The upcoming event will be delayed by a few minutes.', '(w-text_w)/2', '(h-line_h)*0.9', '24', '4', '#ffffff',
'ifnot(ld(1),st(1,t));if(lt(t,ld(1)+1),0,if(lt(t,ld(1)+2),(t-(ld(1)+1))/1,if(lt(t,ld(1)+8),1,if(lt(t,ld(1)+9),(1-(t-(ld(1)+8)))/1,0))))', '1', '#000000@0x80', '4'),
('Scrolling Text', 'We have a very important announcement to make.', 'ifnot(ld(1),st(1,t));if(lt(t,ld(1)+1),w+4,w-w/12*mod(t-ld(1),12*(w+tw)/w))', '(h-line_h)*0.9',
('1', 'Scrolling Text', 'We have a very important announcement to make.', 'ifnot(ld(1),st(1,t));if(lt(t,ld(1)+1),w+4,w-w/12*mod(t-ld(1),12*(w+tw)/w))', '(h-line_h)*0.9',
'24', '4', '#ffffff', '1.0', '1', '#000000@0x80', '4');
INSERT INTO roles(name) VALUES('admin'), ('user'), ('guest');
INSERT INTO settings(channel_name, preview_url, config_path, extra_extensions, service)
INSERT INTO settings(channel_name, preview_url, config_path, extra_extensions, timezone, service)
VALUES('Channel 1', 'http://localhost/live/preview.m3u8',
'/etc/ffplayout/ffplayout.yml', '.jpg,.jpeg,.png', 'ffplayout.service');";
'/etc/ffplayout/ffplayout.yml', '.jpg,.jpeg,.png', 'UTC', 'ffplayout.service');";
sqlx::query(query).bind(secret).execute(&instances).await?;
instances.close().await;
@ -143,6 +145,15 @@ pub async fn db_get_settings(id: &i64) -> Result<Settings, sqlx::Error> {
Ok(result)
}
pub async fn db_get_all_settings() -> Result<Vec<Settings>, sqlx::Error> {
let conn = db_connection().await?;
let query = "SELECT * FROM settings";
let result: Vec<Settings> = sqlx::query_as(query).fetch_all(&conn).await?;
conn.close().await;
Ok(result)
}
pub async fn db_update_settings(
id: i64,
settings: Settings,
@ -174,7 +185,16 @@ pub async fn db_role(id: &i64) -> Result<String, sqlx::Error> {
pub async fn db_login(user: &str) -> Result<User, sqlx::Error> {
let conn = db_connection().await?;
let query = "SELECT id, email, username, password, salt, role_id FROM user WHERE username = $1";
let query = "SELECT id, mail, username, password, salt, role_id FROM user WHERE username = $1";
let result: User = sqlx::query_as(query).bind(user).fetch_one(&conn).await?;
conn.close().await;
Ok(result)
}
pub async fn db_get_user(user: &str) -> Result<User, sqlx::Error> {
let conn = db_connection().await?;
let query = "SELECT id, mail, username, role_id FROM user WHERE username = $1";
let result: User = sqlx::query_as(query).bind(user).fetch_one(&conn).await?;
conn.close().await;
@ -189,9 +209,9 @@ pub async fn db_add_user(user: User) -> Result<SqliteQueryResult, sqlx::Error> {
.unwrap();
let query =
"INSERT INTO user (email, username, password, salt, role_id) VALUES($1, $2, $3, $4, $5)";
"INSERT INTO user (mail, username, password, salt, role_id) VALUES($1, $2, $3, $4, $5)";
let result = sqlx::query(query)
.bind(user.email)
.bind(user.mail)
.bind(user.username)
.bind(password_hash.to_string())
.bind(salt.to_string())
@ -212,10 +232,10 @@ pub async fn db_update_user(id: i64, fields: String) -> Result<SqliteQueryResult
Ok(result)
}
pub async fn db_get_presets() -> Result<Vec<TextPreset>, sqlx::Error> {
pub async fn db_get_presets(id: i64) -> Result<Vec<TextPreset>, sqlx::Error> {
let conn = db_connection().await?;
let query = "SELECT * FROM presets";
let result: Vec<TextPreset> = sqlx::query_as(query).fetch_all(&conn).await?;
let query = "SELECT * FROM presets WHERE channel_id = $1";
let result: Vec<TextPreset> = sqlx::query_as(query).bind(id).fetch_all(&conn).await?;
conn.close().await;
Ok(result)
@ -252,9 +272,10 @@ pub async fn db_update_preset(
pub async fn db_add_preset(preset: TextPreset) -> Result<SqliteQueryResult, sqlx::Error> {
let conn = db_connection().await?;
let query =
"INSERT INTO presets (name, text, x, y, fontsize, line_spacing, fontcolor, alpha, box, boxcolor, boxborderw)
VALUES($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)";
"INSERT INTO presets (channel_id, name, text, x, y, fontsize, line_spacing, fontcolor, alpha, box, boxcolor, boxborderw)
VALUES($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)";
let result: SqliteQueryResult = sqlx::query(query)
.bind(preset.channel_id)
.bind(preset.name)
.bind(preset.text)
.bind(preset.x)

View File

@ -1,6 +1,6 @@
use std::{
error::Error,
fs::File,
fs::{self, File},
io::{stdin, stdout, Write},
path::Path,
};
@ -129,32 +129,32 @@ pub async fn run_args(mut args: Args) -> Result<(), i32> {
args.password = password.ok();
let mut email = String::new();
print!("EMail: ");
let mut mail = String::new();
print!("Mail: ");
stdout().flush().unwrap();
stdin()
.read_line(&mut email)
.read_line(&mut mail)
.expect("Did not enter a correct name?");
if let Some('\n') = email.chars().next_back() {
email.pop();
if let Some('\n') = mail.chars().next_back() {
mail.pop();
}
if let Some('\r') = email.chars().next_back() {
email.pop();
if let Some('\r') = mail.chars().next_back() {
mail.pop();
}
args.email = Some(email);
args.mail = Some(mail);
}
if let Some(username) = args.username {
if args.email.is_none() || args.password.is_none() {
error!("Email/password missing!");
if args.mail.is_none() || args.password.is_none() {
error!("Mail/password missing!");
return Err(1);
}
let user = User {
id: 0,
email: Some(args.email.unwrap()),
mail: Some(args.mail.unwrap()),
username: username.clone(),
password: args.password.unwrap(),
salt: None,
@ -193,3 +193,30 @@ pub async fn playout_config(channel_id: &i64) -> Result<(PlayoutConfig, Settings
"Error in getting config!".to_string(),
))
}
pub async fn read_log_file(channel_id: &i64, date: &str) -> Result<String, ServiceError> {
if let Ok(settings) = db_get_settings(channel_id).await {
let mut date_str = "".to_string();
if !date.is_empty() {
date_str.push('.');
date_str.push_str(date);
}
if let Ok(config) = read_playout_config(&settings.config_path) {
let mut log_path = Path::new(&config.logging.log_path)
.join("ffplayout.log")
.display()
.to_string();
log_path.push_str(&date_str);
let file = fs::read_to_string(log_path)?;
return Ok(file);
}
}
Err(ServiceError::BadRequest(
"Requested log file not exists, or not readable.".to_string(),
))
}

View File

@ -6,7 +6,7 @@ pub struct User {
#[serde(skip_deserializing)]
pub id: i64,
#[sqlx(default)]
pub email: Option<String>,
pub mail: Option<String>,
pub username: String,
#[sqlx(default)]
#[serde(skip_serializing, default = "empty_string")]
@ -41,6 +41,7 @@ pub struct TextPreset {
#[sqlx(default)]
#[serde(skip_deserializing)]
pub id: i64,
pub channel_id: i64,
#[serde(skip_deserializing)]
pub name: String,
pub text: String,
@ -63,6 +64,7 @@ pub struct Settings {
pub preview_url: String,
pub config_path: String,
pub extra_extensions: String,
pub timezone: String,
#[sqlx(default)]
#[serde(skip_serializing, skip_deserializing)]
pub secret: String,

View File

@ -16,12 +16,12 @@ use crate::utils::{
errors::ServiceError,
files::{browser, remove_file_or_folder, rename_file, upload, MoveObject, PathObject},
handles::{
db_add_preset, db_add_user, db_get_presets, db_get_settings, db_login, db_role,
db_update_preset, db_update_settings, db_update_user,
db_add_preset, db_add_user, db_get_all_settings, db_get_presets, db_get_settings,
db_get_user, db_login, db_role, db_update_preset, db_update_settings, db_update_user,
},
models::{LoginUser, Settings, TextPreset, User},
playlist::{delete_playlist, generate_playlist, read_playlist, write_playlist},
read_playout_config, Role,
read_log_file, read_playout_config, Role,
};
use ffplayout_lib::utils::{JsonPlaylist, PlayoutConfig};
@ -32,6 +32,12 @@ struct ResponseObj<T> {
data: Option<T>,
}
#[derive(Serialize)]
struct UserObj<T> {
message: String,
user: Option<T>,
}
/// curl -X POST http://127.0.0.1:8080/auth/login/ -H "Content-Type: application/json" \
/// -d '{"username": "<USER>", "password": "<PASS>" }'
#[post("/auth/login/")]
@ -58,19 +64,17 @@ pub async fn login(credentials: web::Json<User>) -> impl Responder {
info!("user {} login, with role: {role}", credentials.username);
web::Json(ResponseObj {
web::Json(UserObj {
message: "login correct!".into(),
status: 200,
data: Some(user),
user: Some(user),
})
.customize()
.with_status(StatusCode::OK)
} else {
error!("Wrong password for {}!", credentials.username);
web::Json(ResponseObj {
web::Json(UserObj {
message: "Wrong password!".into(),
status: 403,
data: None,
user: None,
})
.customize()
.with_status(StatusCode::FORBIDDEN)
@ -78,10 +82,9 @@ pub async fn login(credentials: web::Json<User>) -> impl Responder {
}
Err(e) => {
error!("Login {} failed! {e}", credentials.username);
return web::Json(ResponseObj {
return web::Json(UserObj {
message: format!("Login {} failed!", credentials.username),
status: 400,
data: None,
user: None,
})
.customize()
.with_status(StatusCode::BAD_REQUEST);
@ -89,8 +92,22 @@ pub async fn login(credentials: web::Json<User>) -> impl Responder {
}
}
/// curl -X GET 'http://localhost:8080/api/user' --header 'Content-Type: application/json' \
/// --header 'Authorization: Bearer <TOKEN>'
#[get("/user")]
#[has_any_role("Role::Admin", "Role::User", type = "Role")]
async fn get_user(user: web::ReqData<LoginUser>) -> Result<impl Responder, ServiceError> {
match db_get_user(&user.username).await {
Ok(user) => Ok(web::Json(user)),
Err(e) => {
error!("{e}");
Err(ServiceError::InternalServerError)
}
}
}
/// curl -X PUT http://localhost:8080/api/user/1 --header 'Content-Type: application/json' \
/// --data '{"email": "<EMAIL>", "password": "<PASS>"}' --header 'Authorization: <TOKEN>'
/// --data '{"mail": "<MAIL>", "password": "<PASS>"}' --header 'Authorization: <TOKEN>'
#[put("/user/{id}")]
#[has_any_role("Role::Admin", "Role::User", type = "Role")]
async fn update_user(
@ -101,8 +118,8 @@ async fn update_user(
if id.into_inner() == user.id {
let mut fields = String::new();
if let Some(email) = data.email.clone() {
fields.push_str(format!("email = '{email}'").as_str());
if let Some(mail) = data.mail.clone() {
fields.push_str(format!("mail = '{mail}'").as_str());
}
if !data.password.is_empty() {
@ -129,7 +146,7 @@ async fn update_user(
}
/// curl -X POST 'http://localhost:8080/api/user/' --header 'Content-Type: application/json' \
/// -d '{"email": "<EMAIL>", "username": "<USER>", "password": "<PASS>", "role_id": 1}' \
/// -d '{"mail": "<MAIL>", "username": "<USER>", "password": "<PASS>", "role_id": 1}' \
/// --header 'Authorization: Bearer <TOKEN>'
#[post("/user/")]
#[has_any_role("Role::Admin", type = "Role")]
@ -148,11 +165,18 @@ async fn add_user(data: web::Json<User>) -> Result<impl Responder, ServiceError>
#[has_any_role("Role::Admin", "Role::User", type = "Role")]
async fn get_settings(id: web::Path<i64>) -> Result<impl Responder, ServiceError> {
if let Ok(settings) = db_get_settings(&id).await {
return Ok(web::Json(ResponseObj {
message: format!("Settings from {}", settings.channel_name),
status: 200,
data: Some(settings),
}));
return Ok(web::Json(settings));
}
Err(ServiceError::InternalServerError)
}
/// curl -X GET http://127.0.0.1:8080/api/settings -H "Authorization: Bearer <TOKEN>"
#[get("/settings")]
#[has_any_role("Role::Admin", type = "Role")]
async fn get_all_settings() -> Result<impl Responder, ServiceError> {
if let Ok(settings) = db_get_all_settings().await {
return Ok(web::Json(settings));
}
Err(ServiceError::InternalServerError)
@ -217,11 +241,11 @@ async fn update_playout_config(
}
/// curl -X GET http://localhost:8080/api/presets/ --header 'Content-Type: application/json' \
/// --data '{"email": "<EMAIL>", "password": "<PASS>"}' --header 'Authorization: <TOKEN>'
#[get("/presets/")]
/// --data '{"mail": "<MAIL>", "password": "<PASS>"}' --header 'Authorization: <TOKEN>'
#[get("/presets/{id}")]
#[has_any_role("Role::Admin", "Role::User", type = "Role")]
async fn get_presets() -> Result<impl Responder, ServiceError> {
if let Ok(presets) = db_get_presets().await {
async fn get_presets(id: web::Path<i64>) -> Result<impl Responder, ServiceError> {
if let Ok(presets) = db_get_presets(*id).await {
return Ok(web::Json(presets));
}
@ -423,6 +447,21 @@ pub async fn del_playlist(
}
}
/// ----------------------------------------------------------------------------
/// read log file
///
/// ----------------------------------------------------------------------------
#[get("/log/{req:.*}")]
#[has_any_role("Role::Admin", "Role::User", type = "Role")]
pub async fn get_log(req: web::Path<String>) -> Result<impl Responder, ServiceError> {
let mut segments = req.split('/');
let id: i64 = segments.next().unwrap_or_default().parse().unwrap_or(0);
let date = segments.next().unwrap_or_default();
read_log_file(&id, date).await
}
/// ----------------------------------------------------------------------------
/// file operations
///

View File

@ -43,7 +43,7 @@ pub fn send_mail(cfg: &PlayoutConfig, msg: String) {
message = message.to(r.parse().unwrap());
}
if let Ok(email) = message.body(clean_string(&msg)) {
if let Ok(mail) = message.body(clean_string(&msg)) {
let credentials =
Credentials::new(cfg.mail.sender_addr.clone(), cfg.mail.sender_pass.clone());
@ -55,9 +55,9 @@ pub fn send_mail(cfg: &PlayoutConfig, msg: String) {
let mailer = transporter.unwrap().credentials(credentials).build();
// Send the email
if let Err(e) = mailer.send(&email) {
error!("Could not send email: {:?}", e);
// Send the mail
if let Err(e) = mailer.send(&mail) {
error!("Could not send mail: {:?}", e);
}
} else {
error!("Mail Message failed!");