User Authentication and Other fixes #1

Merged
vinod merged 30 commits from user-auth into main 2023-02-05 09:01:08 +00:00
20 changed files with 702 additions and 175 deletions
Showing only changes of commit ec907e4b5f - Show all commits

621
backend/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -9,7 +9,7 @@ booksman-orm = { path = "../orm" }
booksman-search = { path = "../search" } booksman-search = { path = "../search" }
migration = { path = "../migration" } migration = { path = "../migration" }
entity = { path = "../entity" } entity = { path = "../entity" }
axum = "^0.5" axum = "^0.6"
axum-extra = { version = "^0.3", features = ["spa"] } axum-extra = { version = "^0.3", features = ["spa"] }
clap = { version = "^3", features = ["derive"] } clap = { version = "^3", features = ["derive"] }
dotenvy = "0.15.0" dotenvy = "0.15.0"
@@ -27,9 +27,15 @@ tracing = "^0.1"
tracing-subscriber = "^0.3" tracing-subscriber = "^0.3"
itertools = "0.10" itertools = "0.10"
chrono = "0.4" chrono = "0.4"
axum-login = "0.4"
#features = ["sqlite"]
[dependencies.rand]
version = "0.8.5"
features = ["min_const_gen"]
[dependencies.sea-orm] [dependencies.sea-orm]
version = "^0.9.2" # sea-orm version version = "^0.10.6" # sea-orm version
features = [ features = [
"debug-print", "debug-print",
"runtime-tokio-native-tls", "runtime-tokio-native-tls",

View File

@@ -31,6 +31,31 @@ use ::entity::entities::{book,book_author,book_person,book_place,book_subject,bo
use std::env; use std::env;
use migration::{Migrator, MigratorTrait}; use migration::{Migrator, MigratorTrait};
use chrono::Local; use chrono::Local;
use axum_login::{
axum_sessions::{async_session::MemoryStore as SessionMemoryStore, SessionLayer},
memory_store::MemoryStore as AuthMemoryStore,
secrecy::SecretVec,
AuthLayer, AuthUser, RequireAuthorizationLayer,
};
use rand::Rng;
use std::{collections::HashMap, sync::Arc};
#[derive(Debug, Default, Clone)]
struct User {
id: i64,
password_hash: String,
name: String,
}
impl AuthUser for User {
fn get_id(&self) -> String {
format!("{}", self.id)
}
fn get_password_hash(&self) -> SecretVec<u8> {
SecretVec::new(self.password_hash.clone().into())
}
}
#[derive(Deserialize, Serialize, Debug, Clone)] #[derive(Deserialize, Serialize, Debug, Clone)]
struct BookUI { struct BookUI {
@@ -206,6 +231,8 @@ struct Opt {
static_dir: String, static_dir: String,
} }
type AuthContext = axum_login::extractors::AuthContext<User, AuthMemoryStore<User>>;
#[tokio::main] #[tokio::main]
pub async fn main() { pub async fn main() {
let opt = Opt::parse(); let opt = Opt::parse();
@@ -225,6 +252,10 @@ pub async fn main() {
let meili_url = env::var("MEILI_URL").expect("MEILI_URL is not set in .env file"); let meili_url = env::var("MEILI_URL").expect("MEILI_URL is not set in .env file");
let meili_key = env::var("MEILI_KEY").expect("MEILI_KEY is not set in .env file"); let meili_key = env::var("MEILI_KEY").expect("MEILI_KEY is not set in .env file");
let secret = rand::thread_rng().gen::<[u8; 64]>();
let session_store = MemoryStore::new();
let session_layer = SessionLayer::new(session_store, &secret).with_secure(false);
let conn = Database::connect(db_url) let conn = Database::connect(db_url)
.await .await
@@ -232,8 +263,37 @@ pub async fn main() {
// Apply all pending migrations // Apply all pending migrations
Migrator::up(&conn, None).await.unwrap(); Migrator::up(&conn, None).await.unwrap();
let store = Arc::new(RwLock::new(HashMap::default()));
let users : Vec<User> = get_users_seaorm(conn);
//let user = User::get_rusty_user();
for user in users.iter() {
store.write().await.insert(user.get_id(), user);
}
let user_store = AuthMemoryStore::new(&store);
let auth_layer = AuthLayer::new(user_store, &secret);
let meili_client = Client::new(meili_url, meili_key); let meili_client = Client::new(meili_url, meili_key);
async fn login_handler(mut auth: AuthContext, Json(user_sent): Json<User>) {
auth.login(&user_sent).await.unwrap();
}
async fn register_handler(mut auth: AuthContext, Json(user_sent): Json<User>) {
// add to db
store.write().await.insert(user_sent.get_id(), user_sent);
auth.login(&user_sent).await.unwrap();
}
async fn logout_handler(mut auth: AuthContext) {
dbg!("Logging out user: {}", &auth.current_user);
auth.logout().await;
}
let app = Router::new() let app = Router::new()
.route("/api/search_openlibrary", get(search_openlibrary)) .route("/api/search_openlibrary", get(search_openlibrary))
.route("/api/create_by_isbn", get(create_by_isbn)) .route("/api/create_by_isbn", get(create_by_isbn))
@@ -242,8 +302,13 @@ pub async fn main() {
.route("/api/list_search", get(list_search_book)) .route("/api/list_search", get(list_search_book))
.route("/api/create", post(create_book)) .route("/api/create", post(create_book))
.route("/api/update", post(update_book)) .route("/api/update", post(update_book))
.route("/api/login", post(login_handler))
.route("/api/register", post(register_handler))
.route("/api/logout", post(logout_handler))
.nest("/images", get_service(ServeDir::new(images_dir)).handle_error(handle_error)) .nest("/images", get_service(ServeDir::new(images_dir)).handle_error(handle_error))
.merge(SpaRouter::new("/assets", opt.static_dir)) .merge(SpaRouter::new("/assets", opt.static_dir))
.layer(auth_layer)
.layer(session_layer)
.layer(ServiceBuilder::new().layer(TraceLayer::new_for_http())) .layer(ServiceBuilder::new().layer(TraceLayer::new_for_http()))
.layer(Extension(conn)) .layer(Extension(conn))
.layer(Extension(meili_client)) .layer(Extension(meili_client))

View File

@@ -13,4 +13,4 @@ serde = { version = "1", features = ["derive"] }
chrono = "0.4" chrono = "0.4"
[dependencies.sea-orm] [dependencies.sea-orm]
version = "^0.9.2" # sea-orm version version = "^0.10.6" # sea-orm version

View File

@@ -1,8 +1,8 @@
//! SeaORM Entity. Generated by sea-orm-codegen 0.9.3 //! `SeaORM` Entity. Generated by sea-orm-codegen 0.10.6
use sea_orm::entity::prelude::*; use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)] #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
#[sea_orm(table_name = "book")] #[sea_orm(table_name = "book")]
pub struct Model { pub struct Model {
#[sea_orm(primary_key)] #[sea_orm(primary_key)]
@@ -19,12 +19,15 @@ pub struct Model {
pub time_added: Option<String>, pub time_added: Option<String>,
pub rating: Option<i32>, pub rating: Option<i32>,
pub comments: Option<String>, pub comments: Option<String>,
pub user_id: i32,
} }
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation { pub enum Relation {
#[sea_orm(has_many = "super::book_author::Entity")] #[sea_orm(has_many = "super::book_author::Entity")]
BookAuthor, BookAuthor,
#[sea_orm(has_many = "super::book_isbn::Entity")]
BookIsbn,
#[sea_orm(has_many = "super::book_person::Entity")] #[sea_orm(has_many = "super::book_person::Entity")]
BookPerson, BookPerson,
#[sea_orm(has_many = "super::book_place::Entity")] #[sea_orm(has_many = "super::book_place::Entity")]
@@ -33,8 +36,14 @@ pub enum Relation {
BookSubject, BookSubject,
#[sea_orm(has_many = "super::book_time::Entity")] #[sea_orm(has_many = "super::book_time::Entity")]
BookTime, BookTime,
#[sea_orm(has_many = "super::book_isbn::Entity")] #[sea_orm(
BookIsbn, belongs_to = "super::user::Entity",
from = "Column::UserId",
to = "super::user::Column::Id",
on_update = "Cascade",
on_delete = "Cascade"
)]
User,
} }
impl Related<super::book_author::Entity> for Entity { impl Related<super::book_author::Entity> for Entity {
@@ -43,6 +52,12 @@ impl Related<super::book_author::Entity> for Entity {
} }
} }
impl Related<super::book_isbn::Entity> for Entity {
fn to() -> RelationDef {
Relation::BookIsbn.def()
}
}
impl Related<super::book_person::Entity> for Entity { impl Related<super::book_person::Entity> for Entity {
fn to() -> RelationDef { fn to() -> RelationDef {
Relation::BookPerson.def() Relation::BookPerson.def()
@@ -67,9 +82,9 @@ impl Related<super::book_time::Entity> for Entity {
} }
} }
impl Related<super::book_isbn::Entity> for Entity { impl Related<super::user::Entity> for Entity {
fn to() -> RelationDef { fn to() -> RelationDef {
Relation::BookIsbn.def() Relation::User.def()
} }
} }

View File

@@ -1,8 +1,8 @@
//! SeaORM Entity. Generated by sea-orm-codegen 0.9.3 //! `SeaORM` Entity. Generated by sea-orm-codegen 0.10.6
use sea_orm::entity::prelude::*; use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)] #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
#[sea_orm(table_name = "book_author")] #[sea_orm(table_name = "book_author")]
pub struct Model { pub struct Model {
#[sea_orm(primary_key)] #[sea_orm(primary_key)]

View File

@@ -1,8 +1,8 @@
//! SeaORM Entity. Generated by sea-orm-codegen 0.9.3 //! `SeaORM` Entity. Generated by sea-orm-codegen 0.10.6
use sea_orm::entity::prelude::*; use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)] #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
#[sea_orm(table_name = "book_isbn")] #[sea_orm(table_name = "book_isbn")]
pub struct Model { pub struct Model {
#[sea_orm(primary_key)] #[sea_orm(primary_key)]

View File

@@ -1,8 +1,8 @@
//! SeaORM Entity. Generated by sea-orm-codegen 0.9.3 //! `SeaORM` Entity. Generated by sea-orm-codegen 0.10.6
use sea_orm::entity::prelude::*; use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)] #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
#[sea_orm(table_name = "book_person")] #[sea_orm(table_name = "book_person")]
pub struct Model { pub struct Model {
#[sea_orm(primary_key)] #[sea_orm(primary_key)]

View File

@@ -1,8 +1,8 @@
//! SeaORM Entity. Generated by sea-orm-codegen 0.9.3 //! `SeaORM` Entity. Generated by sea-orm-codegen 0.10.6
use sea_orm::entity::prelude::*; use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)] #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
#[sea_orm(table_name = "book_place")] #[sea_orm(table_name = "book_place")]
pub struct Model { pub struct Model {
#[sea_orm(primary_key)] #[sea_orm(primary_key)]

View File

@@ -1,8 +1,8 @@
//! SeaORM Entity. Generated by sea-orm-codegen 0.9.3 //! `SeaORM` Entity. Generated by sea-orm-codegen 0.10.6
use sea_orm::entity::prelude::*; use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)] #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
#[sea_orm(table_name = "book_subject")] #[sea_orm(table_name = "book_subject")]
pub struct Model { pub struct Model {
#[sea_orm(primary_key)] #[sea_orm(primary_key)]

View File

@@ -1,8 +1,8 @@
//! SeaORM Entity. Generated by sea-orm-codegen 0.9.3 //! `SeaORM` Entity. Generated by sea-orm-codegen 0.10.6
use sea_orm::entity::prelude::*; use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)] #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
#[sea_orm(table_name = "book_time")] #[sea_orm(table_name = "book_time")]
pub struct Model { pub struct Model {
#[sea_orm(primary_key)] #[sea_orm(primary_key)]

View File

@@ -1,4 +1,4 @@
//! SeaORM Entity. Generated by sea-orm-codegen 0.9.3 //! `SeaORM` Entity. Generated by sea-orm-codegen 0.10.6
pub mod prelude; pub mod prelude;
@@ -9,3 +9,4 @@ pub mod book_person;
pub mod book_place; pub mod book_place;
pub mod book_subject; pub mod book_subject;
pub mod book_time; pub mod book_time;
pub mod user;

View File

@@ -1,4 +1,4 @@
//! SeaORM Entity. Generated by sea-orm-codegen 0.9.3 //! `SeaORM` Entity. Generated by sea-orm-codegen 0.10.6
pub use super::book::Entity as Book; pub use super::book::Entity as Book;
pub use super::book_author::Entity as BookAuthor; pub use super::book_author::Entity as BookAuthor;
@@ -7,3 +7,4 @@ pub use super::book_person::Entity as BookPerson;
pub use super::book_place::Entity as BookPlace; pub use super::book_place::Entity as BookPlace;
pub use super::book_subject::Entity as BookSubject; pub use super::book_subject::Entity as BookSubject;
pub use super::book_time::Entity as BookTime; pub use super::book_time::Entity as BookTime;
pub use super::user::Entity as User;

View File

@@ -0,0 +1,26 @@
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.10.6
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
#[sea_orm(table_name = "user")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
pub user_name: Option<String>,
pub password_hash: String,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(has_many = "super::book::Entity")]
Book,
}
impl Related<super::book::Entity> for Entity {
fn to() -> RelationDef {
Relation::Book.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@@ -13,7 +13,7 @@ async-std = { version = "^1", features = ["attributes", "tokio1"] }
chrono = "0.4" chrono = "0.4"
[dependencies.sea-orm-migration] [dependencies.sea-orm-migration]
version = "^0.9.2" version = "^0.10.6"
features = [ features = [
# Enable at least one `ASYNC_RUNTIME` and `DATABASE_DRIVER` feature if you want to run migration via CLI. # Enable at least one `ASYNC_RUNTIME` and `DATABASE_DRIVER` feature if you want to run migration via CLI.
# View the list of supported features at https://www.sea-ql.org/SeaORM/docs/install-and-config/database-and-async-runtime. # View the list of supported features at https://www.sea-ql.org/SeaORM/docs/install-and-config/database-and-async-runtime.

View File

@@ -10,6 +10,26 @@ impl MigrationTrait for Migration {
// Replace the sample below with your own migration scripts // Replace the sample below with your own migration scripts
//todo!(); //todo!();
manager
.create_table(
Table::create()
.table(User::Table)
.if_not_exists()
.col(
ColumnDef::new(User::Id)
.integer()
.not_null()
.auto_increment()
.primary_key(),
)
.col(ColumnDef::new(User::UserName).string().unique_key().not_null())
.col(ColumnDef::new(User::PasswordHash).string().not_null())
.to_owned(),
)
.await.expect("Migration failed");
manager manager
.create_table( .create_table(
Table::create() Table::create()
@@ -34,6 +54,15 @@ impl MigrationTrait for Migration {
.col(ColumnDef::new(Book::TimeAdded).time()) .col(ColumnDef::new(Book::TimeAdded).time())
.col(ColumnDef::new(Book::Rating).integer()) .col(ColumnDef::new(Book::Rating).integer())
.col(ColumnDef::new(Book::Comments).string()) .col(ColumnDef::new(Book::Comments).string())
.col(ColumnDef::new(Book::UserId).integer().not_null())
.foreign_key(
ForeignKey::create()
.name("FK_2e303c3a712662f1fc2a4d0aavc")
.from(Book::Table, Book::UserId)
.to(User::Table, User::Id)
.on_delete(ForeignKeyAction::Cascade)
.on_update(ForeignKeyAction::Cascade),
)
.to_owned(), .to_owned(),
) )
.await.expect("Migration failed"); .await.expect("Migration failed");
@@ -246,6 +275,7 @@ enum Book {
TimeAdded, TimeAdded,
Rating, Rating,
Comments, Comments,
UserId
} }
#[derive(Iden)] #[derive(Iden)]
@@ -295,3 +325,11 @@ enum BookISBN {
ISBN, ISBN,
BookId, BookId,
} }
#[derive(Iden)]
enum User {
Table,
Id,
UserName,
PasswordHash,
}

View File

@@ -10,7 +10,7 @@ entity = { path = "../entity" }
chrono = "0.4" chrono = "0.4"
[dependencies.sea-orm] [dependencies.sea-orm]
version = "^0.9.2" # sea-orm version version = "^0.10.6" # sea-orm version
features = [ features = [
"debug-print", "debug-print",
"runtime-tokio-native-tls", "runtime-tokio-native-tls",

View File

@@ -1,14 +1,37 @@
//use ::entity::entities::prelude::Book; //use ::entity::entities::prelude::Book;
//use ::entity::entities::{prelude::*, *}; //use ::entity::entities::{prelude::*, *};
use ::entity::entities::{book::Entity as Book}; use ::entity::entities::user::Entity as User;
use ::entity::entities::book::Entity as Book;
//, book_author::Entity as Author, book_person::Entity as Person, book_place::Entity as Place, book_subject::Entity as Subject, book_time::Entity as Time, book_isbn::Entity as ISBN}; //, book_author::Entity as Author, book_person::Entity as Person, book_place::Entity as Place, book_subject::Entity as Subject, book_time::Entity as Time, book_isbn::Entity as ISBN};
use ::entity::entities::{book,book_author,book_person,book_place,book_subject,book_time,book_isbn}; use ::entity::entities::{book,book_author,book_person,book_place,book_subject,book_time,book_isbn,user};
use sea_orm::*; use sea_orm::*;
//use ::entity::entities::prelude::Book; //use ::entity::entities::prelude::Book;
pub struct Mutation; pub struct Mutation;
impl Mutation { impl Mutation {
pub async fn create_user(
db: &DbConn,
form_data: user::Model,
) -> Result<InsertResult<user::ActiveModel>, DbErr> {
let record = user::ActiveModel {
user_name: Set(form_data.user_name.to_owned()),
password_hash: Set(form_data.password_hash.to_owned()),
..Default::default()
};
User::insert(record).exec(db).await
}
pub async fn delete_user(db: &DbConn, id: i32) -> Result<DeleteResult, DbErr> {
let user: user::ActiveModel = User::find_by_id(id)
.one(db)
.await?
.ok_or(DbErr::Custom("Cannot find user.".to_owned()))
.map(Into::into)?;
user.delete(db).await
}
pub async fn create_book( pub async fn create_book(
db: &DbConn, db: &DbConn,
form_data: book::Model, form_data: book::Model,
@@ -26,6 +49,7 @@ impl Mutation {
time_added: Set(form_data.time_added.to_owned()), time_added: Set(form_data.time_added.to_owned()),
rating: Set(form_data.rating.to_owned()), rating: Set(form_data.rating.to_owned()),
comments: Set(form_data.comments.to_owned()), comments: Set(form_data.comments.to_owned()),
user_id: Set(form_data.user_id.to_owned()),
..Default::default() ..Default::default()
}; };
Book::insert(record).exec(db).await Book::insert(record).exec(db).await
@@ -134,6 +158,7 @@ impl Mutation {
time_added: Set(form_data.time_added.to_owned()), time_added: Set(form_data.time_added.to_owned()),
rating: Set(form_data.rating.to_owned()), rating: Set(form_data.rating.to_owned()),
comments: Set(form_data.comments.to_owned()), comments: Set(form_data.comments.to_owned()),
user_id: Set(form_data.user_id.to_owned()),
}.update(db).await }.update(db).await
} }

View File

@@ -59,11 +59,13 @@ impl Query {
/// If ok, returns (post models, num pages). /// If ok, returns (post models, num pages).
pub async fn find_books_in_page( pub async fn find_books_in_page(
db: &DbConn, db: &DbConn,
page: usize, page: u64,
posts_per_page: usize, posts_per_page: u64,
) -> Result<(Vec<book::Model>, usize), DbErr> { userid: i32,
) -> Result<(Vec<book::Model>, u64), DbErr> {
// Setup paginator // Setup paginator
let paginator = Book::find() let paginator = Book::find()
.filter(book::Column::UserId.eq(userid))
.order_by_asc(book::Column::Id) .order_by_asc(book::Column::Id)
.paginate(db, posts_per_page); .paginate(db, posts_per_page);
let num_pages = paginator.num_pages().await?; let num_pages = paginator.num_pages().await?;
@@ -76,9 +78,10 @@ pub async fn find_books_plus_meta_in_page(
db: &DbConn, db: &DbConn,
page: usize, page: usize,
posts_per_page: usize, posts_per_page: usize,
) -> Result<(Vec<BookAndMetaV2>, usize), DbErr> { userid: i32,
) -> Result<(Vec<BookAndMetaV2>, u64), DbErr> {
// Setup paginator // Setup paginator
let books = Self::find_books_in_page(db,page,posts_per_page).await?; let books = Self::find_books_in_page(db,page.try_into().unwrap(),posts_per_page.try_into().unwrap(),userid).await?;
let book_ids: Vec<i32> = books.0.clone().into_iter().map(|b| b.id).collect(); let book_ids: Vec<i32> = books.0.clone().into_iter().map(|b| b.id).collect();
let mut resbooks: Vec<BookAndMetaV2> = Vec::with_capacity(book_ids.len()); let mut resbooks: Vec<BookAndMetaV2> = Vec::with_capacity(book_ids.len());
for book in books.0.iter() { for book in books.0.iter() {

View File

@@ -25,9 +25,9 @@ pub struct BookMeili {
pub isbn: Vec<String>, pub isbn: Vec<String>,
} }
pub async fn create_or_update_book(book: BookMeili, client: &Client) { pub async fn create_or_update_book(book: BookMeili, userid: i32, client: &Client) {
// An index is where the documents are stored. // An index is where the documents are stored.
let books = client.index("books"); let books = client.index(format!("books{}",userid));
// Add some movies in the index. If the index 'movies' does not exist, Meilisearch creates it when you first add the documents. // Add some movies in the index. If the index 'movies' does not exist, Meilisearch creates it when you first add the documents.
books.add_or_replace(&[ books.add_or_replace(&[
book book
@@ -35,16 +35,16 @@ pub async fn create_or_update_book(book: BookMeili, client: &Client) {
} }
pub async fn delete_book(bookid: i32, client: &Client) { pub async fn delete_book(bookid: i32, userid: i32, client: &Client) {
// An index is where the documents are stored. // An index is where the documents are stored.
let books = client.index("books"); let books = client.index(format!("books{}",userid));
books.delete_document(bookid).await.unwrap(); books.delete_document(bookid).await.unwrap();
} }
pub async fn search_book(search: &str, page: usize, client: &Client) -> Result<(Vec<BookMeili>, usize), meilisearch_sdk::errors::Error> { pub async fn search_book(search: &str, page: usize, userid: i32, client: &Client) -> Result<(Vec<BookMeili>, usize), meilisearch_sdk::errors::Error> {
// An index is where the documents are stored. // An index is where the documents are stored.
let books = client.index("books"); let books = client.index(format!("books{}",userid));
let results : SearchResults<BookMeili> = books.search().with_query(search).with_offset((page-1)*12) let results : SearchResults<BookMeili> = books.search().with_query(search).with_offset((page-1)*12)
.execute::<BookMeili>().await.unwrap(); .execute::<BookMeili>().await.unwrap();