First release
This commit is contained in:
111
src/auth.rs
Normal file
111
src/auth.rs
Normal file
@@ -0,0 +1,111 @@
|
||||
use crate::Db;
|
||||
|
||||
use rocket::response::Redirect;
|
||||
use rocket::request;
|
||||
use rocket::http::{Status, Cookie, CookieJar, SameSite};
|
||||
use inventur_db;
|
||||
use rocket_oauth2::{OAuth2, TokenResponse};
|
||||
use reqwest::Client;
|
||||
use rocket::serde::{Deserialize, json::Json};
|
||||
|
||||
pub struct RanderathIdentity;
|
||||
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(crate = "rocket::serde")]
|
||||
struct UnAuthUser {
|
||||
preferred_username: String,
|
||||
email: String,
|
||||
}
|
||||
|
||||
pub struct AuthUser {
|
||||
pub uid: i32,
|
||||
pub uname: String,
|
||||
pub email: String,
|
||||
}
|
||||
|
||||
#[rocket::async_trait]
|
||||
impl<'r> request::FromRequest<'r> for AuthUser {
|
||||
type Error = ();
|
||||
|
||||
async fn from_request(request: &'r request::Request<'_>) -> request::Outcome<AuthUser, ()> {
|
||||
let cookies = request
|
||||
.guard::<&CookieJar<'_>>()
|
||||
.await
|
||||
.expect("Cookie request failed...");
|
||||
let uid = cookies.get_private("uid");
|
||||
let uname = cookies.get_private("username");
|
||||
let email = cookies.get_private("email");
|
||||
if uid.is_some() && uname.is_some() && email.is_some() {
|
||||
let uid = uid.unwrap().value().parse::<i32>().unwrap();
|
||||
let uname = uname.unwrap().value().to_string();
|
||||
let email = email.unwrap().value().to_string();
|
||||
return request::Outcome::Success(AuthUser { uid, uname, email });
|
||||
}
|
||||
return request::Outcome::Forward(Status::Unauthorized);
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn login_or_register(conn: Db, access_token: &str) -> Option<inventur_db::User> {
|
||||
let ui = Client::new()
|
||||
.get("https://ldap.randerath.eu/realms/master/protocol/openid-connect/userinfo")
|
||||
.bearer_auth(access_token)
|
||||
.send()
|
||||
.await;
|
||||
if ui.is_err() {
|
||||
return None;
|
||||
}
|
||||
let ui = ui.unwrap().json::<UnAuthUser>().await;
|
||||
if ui.is_err() {
|
||||
return None;
|
||||
}
|
||||
let ui = ui.unwrap();
|
||||
let user = conn.run(move |c| inventur_db::register_or_login(c, ui.preferred_username, ui.email)).await;
|
||||
if user.is_none() {
|
||||
return None;
|
||||
}
|
||||
Some(user.unwrap())
|
||||
|
||||
}
|
||||
|
||||
#[catch(401)]
|
||||
pub async fn redirect_to_login() -> Redirect {
|
||||
Redirect::to(uri!(oauth_login()))
|
||||
}
|
||||
|
||||
#[get("/login")]
|
||||
pub fn oauth_login(oauth2: OAuth2<RanderathIdentity>, cookies: &CookieJar<'_>) -> Redirect {
|
||||
oauth2.get_redirect(cookies, &["openid"]).unwrap()
|
||||
}
|
||||
|
||||
#[get("/auth")]
|
||||
pub async fn oauth_callback(conn: Db, token: TokenResponse<RanderathIdentity>, cookies: &CookieJar<'_>) -> Result<Redirect, Status> {
|
||||
let at = token.access_token().to_string();
|
||||
let tv = token.as_value();
|
||||
cookies.add_private(
|
||||
Cookie::build(("token", at.to_string()))
|
||||
.same_site(SameSite::Lax)
|
||||
.build()
|
||||
);
|
||||
let user = login_or_register(conn, &at).await;
|
||||
if user.is_none() {
|
||||
return Err(Status::Forbidden);
|
||||
}
|
||||
let user = user.unwrap();
|
||||
|
||||
cookies.add_private(
|
||||
Cookie::build(("username", user.uname))
|
||||
.same_site(SameSite::Lax)
|
||||
.build());
|
||||
|
||||
cookies.add_private(
|
||||
Cookie::build(("email", user.email))
|
||||
.same_site(SameSite::Lax)
|
||||
.build());
|
||||
|
||||
cookies.add_private(
|
||||
Cookie::build(("uid", user.uid.to_string()))
|
||||
.same_site(SameSite::Lax)
|
||||
.build());
|
||||
Ok(Redirect::to("/"))
|
||||
}
|
||||
152
src/main.rs
152
src/main.rs
@@ -1,112 +1,74 @@
|
||||
#[macro_use] extern crate rocket;
|
||||
#[macro_use] extern crate rocket_db_pools;
|
||||
mod auth;
|
||||
mod table;
|
||||
|
||||
use auth::AuthUser;
|
||||
|
||||
// TODO: filter (ajax, spinning circle), show options as in excel
|
||||
use rocket::fs::{FileServer, relative};
|
||||
use rocket_dyn_templates::{Template, context};
|
||||
use rocket_db_pools::{Database, Connection};
|
||||
use rocket_db_pools::diesel::{QueryResult, MysqlPool, prelude::*};
|
||||
use rocket::form::Form;
|
||||
/*use dotenvy::dotenv;
|
||||
use std::env;*/
|
||||
use rocket::request;
|
||||
use rocket::response::Redirect;
|
||||
use rocket::http::{Status, Cookie, CookieJar, SameSite};
|
||||
use inventur_db;
|
||||
use rocket_sync_db_pools::{database, diesel};
|
||||
use rocket_oauth2::OAuth2;
|
||||
use std::env;
|
||||
use dotenvy::dotenv;
|
||||
use rocket::Config;
|
||||
use rocket::figment::providers::{Toml, Env, Format};
|
||||
|
||||
#[derive(Database)]
|
||||
#[database("inventur")]
|
||||
struct Db(MysqlPool);
|
||||
pub struct Db(diesel::MysqlConnection);
|
||||
|
||||
/*fn connect_db() -> MysqlConnection {
|
||||
dotenv.ok();
|
||||
let database_url = env::var("DATABASE_URL").expect("Can't find database url");
|
||||
MysqlConnection::establish(&database_url).expect_or_else(|_| panic!("Couldn't connect to database {}.", database_url));
|
||||
}*/
|
||||
|
||||
struct Owner {
|
||||
id: i64,
|
||||
email: String,
|
||||
name: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Queryable, Insertable)]
|
||||
#[diesel(table_name = users)]
|
||||
struct User {
|
||||
id: i64,
|
||||
email: String,
|
||||
}
|
||||
|
||||
diesel::table! {
|
||||
users (id) {
|
||||
id -> BigInt,
|
||||
email -> Text,
|
||||
#[get("/")]
|
||||
async fn home(conn: Db, user: AuthUser) -> Template {
|
||||
let uid = user.uid;
|
||||
let (tids, tnames) = table::get_tids(&conn, uid).await;
|
||||
let mut cols = Vec::new();
|
||||
let mut rows = Vec::new();
|
||||
for tblid in tids.clone() {
|
||||
let tbl = conn.run(move |c| inventur_db::get_table(c, tblid, uid)).await.unwrap();
|
||||
cols.push(tbl.column_names.clone());
|
||||
let mut rws : Vec<Vec<String>> = Vec::new();
|
||||
for row in tbl.rows {
|
||||
rws.push(row.cells);
|
||||
}
|
||||
rows.push(rws.clone());
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Queryable, Insertable)]
|
||||
#[diesel(table_name = jrtables)]
|
||||
struct JRTable {
|
||||
id: i64,
|
||||
name: String,
|
||||
owner_id: i64,
|
||||
}
|
||||
|
||||
diesel::table! {
|
||||
jrtables (id) {
|
||||
id -> BigInt,
|
||||
name -> Text,
|
||||
owner_id -> BigInt,
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/<tname>")]
|
||||
fn table(tname: &str) -> Template {
|
||||
let columns = ["name", "djhfae", "fsjhr"];
|
||||
let rows = [
|
||||
["1", "first", "hasdjf", "753rgf"],
|
||||
["2", "second", "7438ued", "🚀"],
|
||||
["3", "third", "", ""]
|
||||
];
|
||||
Template::render("table",
|
||||
context!{tname: tname,
|
||||
columns: columns,
|
||||
rows: rows,
|
||||
}
|
||||
Template::render("home",
|
||||
context!{
|
||||
tids: tids,
|
||||
tnames: tnames,
|
||||
columns: cols,
|
||||
rows: rows,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(FromForm)]
|
||||
struct New_table<'r> {
|
||||
name: &'r str,
|
||||
fields: Vec<&'r str>,
|
||||
#[field(name = "done")]
|
||||
complete: bool,
|
||||
}
|
||||
|
||||
#[post("/create", data="<data>")]
|
||||
fn create(data: Form<New_table<'_>>) {
|
||||
//println!("{:?}", data);
|
||||
}
|
||||
|
||||
|
||||
#[derive(FromForm)]
|
||||
struct Args <'r> {
|
||||
value: &'r str,
|
||||
}
|
||||
|
||||
#[get("/table/<tname>?filter&<column>&<mode>&<args..>")]
|
||||
async fn filter(db:Connection<Db>, tname: &str, column: &str, mode: &str, args: Args<'_>) -> &'static str {
|
||||
todo!()
|
||||
}
|
||||
|
||||
#[get("/")]
|
||||
fn index(db: Connection<Db>) -> Template {
|
||||
Template::render("test", context!{foo: 123,})
|
||||
#[get("/", rank=2)]
|
||||
async fn login_home() -> Redirect {
|
||||
Redirect::to(uri!(auth::oauth_login()))
|
||||
}
|
||||
|
||||
#[launch]
|
||||
fn rocket() -> _ {
|
||||
rocket::build()
|
||||
async fn rocket() -> _ {
|
||||
dotenv().ok();
|
||||
|
||||
let cfg = Config::figment()
|
||||
.merge(Env::prefixed("ROCKET_"));
|
||||
|
||||
|
||||
rocket::custom(cfg)
|
||||
.attach(Template::fairing())
|
||||
.attach(Db::init())
|
||||
.mount("/", FileServer::from(relative!("static")))
|
||||
.mount("/", routes![index])
|
||||
.mount("/table", routes![create, table])
|
||||
//connect_db();
|
||||
.attach(Db::fairing())
|
||||
.attach(OAuth2::<auth::RanderathIdentity>::fairing("oauth"))
|
||||
.mount("/", routes![auth::oauth_login, auth::oauth_callback, home, login_home])
|
||||
.mount("/table", routes![table::table, table::table_sec, table::edit_tname, table::create, table::import_table])
|
||||
.mount("/entry", routes![table::new_entry])
|
||||
.register("/", catchers![auth::redirect_to_login])
|
||||
.mount("/static", FileServer::from(relative!("static")))
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
// @generated automatically by Diesel CLI.
|
||||
|
||||
diesel::table! {
|
||||
users (id) {
|
||||
jrtables (id) {
|
||||
id -> Integer,
|
||||
#[max_length = 64]
|
||||
email -> Varchar,
|
||||
#[max_length = 255]
|
||||
name -> Nullable<Varchar>,
|
||||
name -> Varchar,
|
||||
num_fields -> Integer,
|
||||
}
|
||||
}
|
||||
|
||||
180
src/table.rs
Normal file
180
src/table.rs
Normal file
@@ -0,0 +1,180 @@
|
||||
use crate::auth;
|
||||
|
||||
use auth::AuthUser;
|
||||
use crate::Db;
|
||||
|
||||
use rocket_dyn_templates::{Template, context};
|
||||
use rocket::form::Form;
|
||||
use rocket::response::Redirect;
|
||||
use inventur_db;
|
||||
use rocket::serde::{Serialize, Deserialize, json::Json};
|
||||
use rocket::http::{Status, Cookie, CookieJar, SameSite};
|
||||
|
||||
|
||||
|
||||
#[derive(FromForm)]
|
||||
struct NewEntry {
|
||||
tblid: i32,
|
||||
cells: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(FromForm)]
|
||||
struct EditTname {
|
||||
tblid: i32,
|
||||
new_name: String,
|
||||
}
|
||||
|
||||
#[derive(FromForm)]
|
||||
struct NewTable {
|
||||
name: String,
|
||||
fields: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(crate = "rocket::serde")]
|
||||
struct ImportTable {
|
||||
name: String,
|
||||
columns: Vec<String>,
|
||||
rows: Vec<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(crate = "rocket::serde")]
|
||||
struct JRows {
|
||||
rows: Vec<Vec<String>>,
|
||||
}
|
||||
|
||||
pub async fn get_tids(conn: &Db, uid: i32) -> (Vec<i32>, Vec<String>) {
|
||||
let mut tids = conn.run(move |c| inventur_db::get_user_tblids(c, uid)).await;
|
||||
let tnames;
|
||||
if tids.is_none() {
|
||||
tids = Some(Vec::new());
|
||||
tnames = Some(Vec::new());
|
||||
}else {
|
||||
let tids = tids.clone().unwrap();
|
||||
tnames = conn.run(move |c| inventur_db::get_tblnames(c, tids)).await;
|
||||
}
|
||||
(tids.unwrap(), tnames.unwrap())
|
||||
}
|
||||
|
||||
#[get("/<tid>", rank=2)]
|
||||
pub async fn table_sec(conn: Db, tid: i32, user: AuthUser) -> Redirect {
|
||||
let nus : Option<usize> = None;
|
||||
let nu8 : Option<u8> = None;
|
||||
let nvi32 : Option<Vec<i32>> = None;
|
||||
let ns : Option<String> = None;
|
||||
Redirect::to(uri!("/table", table(tid, nu8, nus, nvi32, ns)))
|
||||
}
|
||||
|
||||
#[get("/<tid>?<sort_dir>&<sort_field>&<search_fields>&<search_value>")]
|
||||
pub async fn table(conn: Db, tid: i32, sort_dir: Option<u8>, sort_field: Option<usize>, search_fields: Option<Vec<i32>>, search_value: Option<String>, user: AuthUser) -> Option<Template> {
|
||||
let uid = user.uid;
|
||||
let table = conn.run(move |c| inventur_db::get_table(c, tid, uid)).await;
|
||||
if table.is_none() {
|
||||
return None;
|
||||
}
|
||||
let mut table = table.unwrap();
|
||||
let searchvalue;
|
||||
let searchfields;
|
||||
if search_value.is_some() && !search_value.clone().unwrap().eq_ignore_ascii_case("") && search_fields.is_some() {
|
||||
searchvalue = search_value.unwrap();
|
||||
searchfields = search_fields.unwrap();
|
||||
table = inventur_db::search_table(table, searchfields.clone(), searchvalue.clone());
|
||||
}else {
|
||||
if search_value.is_none() {
|
||||
searchvalue = String::new();
|
||||
} else {
|
||||
searchvalue = search_value.unwrap();
|
||||
}
|
||||
if search_fields.is_none() {
|
||||
searchfields = (0i32..(table.column_names.len() as i32)).collect();
|
||||
|
||||
} else {
|
||||
searchfields = search_fields.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
let sortdir;
|
||||
if sort_dir.is_none() || sort_dir.unwrap() > 1 {
|
||||
sortdir = 0;
|
||||
}else {
|
||||
sortdir = sort_dir.unwrap();
|
||||
}
|
||||
let sortfield;
|
||||
if sort_field.is_none() {
|
||||
sortfield = 0;
|
||||
}else {
|
||||
sortfield = sort_field.unwrap();
|
||||
}
|
||||
let table = inventur_db::sort_table(table, sortfield, sortdir);
|
||||
|
||||
let tname = table.name;
|
||||
let columns = table.column_names;
|
||||
let rows : Vec<Vec<String>>= table.rows.iter().map(|v| {let mut r = v.cells.clone(); r.insert(0, v.row_pos.to_string()); r}).collect();
|
||||
let (tids, tnames) = get_tids(&conn, uid).await;
|
||||
|
||||
Some(
|
||||
Template::render("table",
|
||||
context!{
|
||||
search_value: searchvalue,
|
||||
search_fields: searchfields,
|
||||
sort_field: sortfield,
|
||||
sort_dir: sortdir,
|
||||
tblid: tid,
|
||||
tblname: tname,
|
||||
tids: tids,
|
||||
tnames: tnames,
|
||||
columns: columns,
|
||||
rows: rows,
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
#[post("/new", data="<data>")]
|
||||
pub async fn new_entry(conn: Db, data: Form<NewEntry>, user: AuthUser) -> Result<Redirect, Status> {
|
||||
let uid = user.uid;
|
||||
let cells = data.cells.clone();
|
||||
let tblid = data.tblid;
|
||||
if conn.run(move |c| inventur_db::add_row(c, data.tblid, cells, uid)).await.is_none() {
|
||||
return Err(Status::Forbidden);
|
||||
}
|
||||
Ok(Redirect::to(uri!("/table", table_sec(tblid))))
|
||||
}
|
||||
|
||||
#[post("/name/edit", data="<data>")]
|
||||
pub async fn edit_tname(conn: Db, data: Form<EditTname>, user: AuthUser) -> Result<Redirect, Status> {
|
||||
let uid = user.uid;
|
||||
let tblid = data.tblid;
|
||||
if conn.run(move |c| inventur_db::rename_table(c, data.tblid, data.new_name.clone(), uid)).await.is_none() {
|
||||
return Err(Status::Forbidden);
|
||||
}
|
||||
Ok(Redirect::to(uri!("/table", table_sec(tblid))))
|
||||
}
|
||||
|
||||
#[post("/create", data="<data>")]
|
||||
pub async fn create(conn: Db, data: Form<NewTable>, user: AuthUser) -> Result<Redirect, Status> {
|
||||
let uid = user.uid;
|
||||
let tblid = conn.run(move |c| inventur_db::create_table(c, data.name.clone(), data.fields.clone(), uid)).await;
|
||||
if tblid.is_none() {
|
||||
return Err(Status::Forbidden);
|
||||
}
|
||||
Ok(Redirect::to(uri!("/table", table_sec(tblid.unwrap()))))
|
||||
}
|
||||
|
||||
#[post("/import", data="<data>")]
|
||||
pub async fn import_table(conn: Db, data: Json<ImportTable>, user: AuthUser) -> Result<Redirect, Status> {
|
||||
let uid = user.uid;
|
||||
let columns = data.columns.clone();
|
||||
let rows = data.rows.clone();
|
||||
let tblid = conn.run(move |c| inventur_db::create_table(c, data.name.clone(), columns, uid)).await;
|
||||
if tblid.is_none() {
|
||||
return Err(Status::UnprocessableEntity);
|
||||
}
|
||||
let tblid = tblid.unwrap();
|
||||
for row in rows {
|
||||
conn.run(move |c| inventur_db::add_row(c, tblid, row, uid)).await;
|
||||
}
|
||||
Ok(Redirect::to(uri!("/table", table_sec(tblid))))
|
||||
}
|
||||
|
||||
120
src/table.rs~
Normal file
120
src/table.rs~
Normal file
@@ -0,0 +1,120 @@
|
||||
|
||||
#[get("/<tid>")]
|
||||
async fn table(conn: Db, tid: i32, user: AuthUser) -> Option<Template> {
|
||||
let uid = user.uid;
|
||||
let table = conn.run(move |c| inventur_db::get_table(c, tid, uid)).await;
|
||||
if table.is_none() {
|
||||
return None;
|
||||
}
|
||||
let table = table.unwrap();
|
||||
let tname = table.name;
|
||||
let columns = table.column_names;
|
||||
let trows = table.rows;
|
||||
let mut rows = Vec::new();
|
||||
for trow in trows {
|
||||
rows.push(trow.cells);
|
||||
let index = rows.len()-1;
|
||||
rows[index].insert(0, trow.row_pos.to_string());
|
||||
}
|
||||
let rows = rows;
|
||||
let mut tids = conn.run(move |c| inventur_db::get_user_tblids(c, uid)).await;
|
||||
let tnames;
|
||||
if tids.is_none() {
|
||||
tids = Some(Vec::new());
|
||||
tnames = Some(Vec::new());
|
||||
}else {
|
||||
let tids = tids.clone().unwrap();
|
||||
tnames = conn.run(move |c| inventur_db::get_tblnames(c, tids)).await;
|
||||
}
|
||||
let tids = tids.unwrap();
|
||||
if tnames.is_none() {
|
||||
return None;
|
||||
}
|
||||
Some(
|
||||
Template::render("table",
|
||||
context!{
|
||||
sort_field: 0,
|
||||
sort_dir: 0,
|
||||
tblid: tid,
|
||||
tblname: tname,
|
||||
tids: tids,
|
||||
tnames: tnames,
|
||||
columns: columns,
|
||||
rows: rows,
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(FromForm)]
|
||||
struct NewEntry {
|
||||
tblid: i32,
|
||||
cells: Vec<String>,
|
||||
}
|
||||
|
||||
#[post("/new", data="<data>")]
|
||||
async fn new_entry(conn: Db, data: Form<NewEntry>, user: AuthUser) -> Result<Redirect, Status> {
|
||||
let uid = user.uid;
|
||||
let cells = data.cells.clone();
|
||||
let tblid = data.tblid;
|
||||
if conn.run(move |c| inventur_db::add_row(c, data.tblid, cells, uid)).await.is_none() {
|
||||
return Err(Status::Forbidden);
|
||||
}
|
||||
Ok(Redirect::to(uri!("/table", table(tblid))))
|
||||
}
|
||||
|
||||
#[derive(FromForm)]
|
||||
struct EditTname {
|
||||
tblid: i32,
|
||||
new_name: String,
|
||||
}
|
||||
|
||||
#[post("/name/edit", data="<data>")]
|
||||
async fn edit_tname(conn: Db, data: Form<EditTname>, user: AuthUser) -> Result<Redirect, Status> {
|
||||
let uid = user.uid;
|
||||
let tblid = data.tblid;
|
||||
if conn.run(move |c| inventur_db::rename_table(c, data.tblid, data.new_name.clone(), uid)).await.is_none() {
|
||||
return Err(Status::Forbidden);
|
||||
}
|
||||
Ok(Redirect::to(uri!("/table", table(tblid))))
|
||||
}
|
||||
|
||||
#[derive(FromForm)]
|
||||
struct NewTable {
|
||||
name: String,
|
||||
fields: Vec<String>,
|
||||
}
|
||||
|
||||
#[post("/create", data="<data>")]
|
||||
async fn create(conn: Db, data: Form<NewTable>, user: AuthUser) -> Result<Redirect, Status> {
|
||||
let uid = user.uid;
|
||||
let tblid = conn.run(move |c| inventur_db::create_table(c, data.name.clone(), data.fields.clone(), uid)).await;
|
||||
if tblid.is_none() {
|
||||
return Err(Status::Forbidden);
|
||||
}
|
||||
Ok(Redirect::to(uri!("/table", table(tblid.unwrap()))))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(crate = "rocket::serde")]
|
||||
struct ImportTable {
|
||||
name: String,
|
||||
columns: Vec<String>,
|
||||
rows: Vec<Vec<String>>,
|
||||
}
|
||||
|
||||
#[post("/import", data="<data>")]
|
||||
pub async fn import_table(conn: Db, data: Json<ImportTable>, user: AuthUser) -> Result<Redirect, Status> {
|
||||
let uid = user.uid;
|
||||
let columns = data.columns.clone();
|
||||
let rows = data.rows.clone();
|
||||
let tblid = conn.run(move |c| inventur_db::create_table(c, data.name.clone(), columns, uid)).await;
|
||||
if tblid.is_none() {
|
||||
return Err(Status::UnprocessableEntity);
|
||||
}
|
||||
let tblid = tblid.unwrap();
|
||||
for row in rows {
|
||||
conn.run(move |c| inventur_db::add_row(c, tblid, row, uid)).await;
|
||||
}
|
||||
Ok(Redirect::to(uri!("/table", table(tblid))))
|
||||
}
|
||||
Reference in New Issue
Block a user