examples: ergonomic improvements to `session_auth_axum` (#2057)
I've always hated the get_todos function and I wanted to change it badly. Added a .env file containing the db url for sqlx-cli, and cleaned up with leptosfmt Co-authored-by: j0lol <me@j0.lol>
This commit is contained in:
parent
fcc9242a63
commit
50432e2651
|
@ -0,0 +1 @@
|
|||
DATABASE_URL=sqlite://Todos.db
|
|
@ -39,22 +39,21 @@ pub fn ErrorTemplate(
|
|||
}
|
||||
|
||||
view! {
|
||||
<h1>"Errors"</h1>
|
||||
<For
|
||||
// a function that returns the items we're iterating over; a signal is fine
|
||||
each= move || {errors.clone().into_iter().enumerate()}
|
||||
// a unique key for each item as a reference
|
||||
key=|(index, _error)| *index
|
||||
// renders each item to a view
|
||||
children=move |error| {
|
||||
let error_string = error.1.to_string();
|
||||
let error_code= error.1.status_code();
|
||||
view! {
|
||||
|
||||
<h2>{error_code.to_string()}</h2>
|
||||
<p>"Error: " {error_string}</p>
|
||||
}
|
||||
}
|
||||
/>
|
||||
<h1>"Errors"</h1>
|
||||
<For
|
||||
// a function that returns the items we're iterating over; a signal is fine
|
||||
each=move || { errors.clone().into_iter().enumerate() }
|
||||
// a unique key for each item as a reference
|
||||
key=|(index, _error)| *index
|
||||
// renders each item to a view
|
||||
children=move |error| {
|
||||
let error_string = error.1.to_string();
|
||||
let error_code = error.1.status_code();
|
||||
view! {
|
||||
<h2>{error_code.to_string()}</h2>
|
||||
<p>"Error: " {error_string}</p>
|
||||
}
|
||||
}
|
||||
/>
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,70 +15,56 @@ pub struct Todo {
|
|||
}
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "ssr")] {
|
||||
if #[cfg(feature = "ssr")] {
|
||||
|
||||
use sqlx::SqlitePool;
|
||||
use sqlx::SqlitePool;
|
||||
use futures::future::join_all;
|
||||
|
||||
pub fn pool() -> Result<SqlitePool, ServerFnError> {
|
||||
use_context::<SqlitePool>()
|
||||
.ok_or_else(|| ServerFnError::ServerError("Pool missing.".into()))
|
||||
}
|
||||
pub fn pool() -> Result<SqlitePool, ServerFnError> {
|
||||
use_context::<SqlitePool>()
|
||||
.ok_or_else(|| ServerFnError::ServerError("Pool missing.".into()))
|
||||
}
|
||||
|
||||
pub fn auth() -> Result<AuthSession, ServerFnError> {
|
||||
use_context::<AuthSession>()
|
||||
.ok_or_else(|| ServerFnError::ServerError("Auth session missing.".into()))
|
||||
}
|
||||
pub fn auth() -> Result<AuthSession, ServerFnError> {
|
||||
use_context::<AuthSession>()
|
||||
.ok_or_else(|| ServerFnError::ServerError("Auth session missing.".into()))
|
||||
}
|
||||
|
||||
#[derive(sqlx::FromRow, Clone)]
|
||||
pub struct SqlTodo {
|
||||
id: u32,
|
||||
user_id: i64,
|
||||
title: String,
|
||||
created_at: String,
|
||||
completed: bool,
|
||||
}
|
||||
#[derive(sqlx::FromRow, Clone)]
|
||||
pub struct SqlTodo {
|
||||
id: u32,
|
||||
user_id: i64,
|
||||
title: String,
|
||||
created_at: String,
|
||||
completed: bool,
|
||||
}
|
||||
|
||||
impl SqlTodo {
|
||||
pub async fn into_todo(self, pool: &SqlitePool) -> Todo {
|
||||
Todo {
|
||||
id: self.id,
|
||||
user: User::get(self.user_id, pool).await,
|
||||
title: self.title,
|
||||
created_at: self.created_at,
|
||||
completed: self.completed,
|
||||
impl SqlTodo {
|
||||
pub async fn into_todo(self, pool: &SqlitePool) -> Todo {
|
||||
Todo {
|
||||
id: self.id,
|
||||
user: User::get(self.user_id, pool).await,
|
||||
title: self.title,
|
||||
created_at: self.created_at,
|
||||
completed: self.completed,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[server(GetTodos, "/api")]
|
||||
pub async fn get_todos() -> Result<Vec<Todo>, ServerFnError> {
|
||||
use futures::TryStreamExt;
|
||||
|
||||
let pool = pool()?;
|
||||
|
||||
let mut todos = Vec::new();
|
||||
let mut rows =
|
||||
sqlx::query_as::<_, SqlTodo>("SELECT * FROM todos").fetch(&pool);
|
||||
|
||||
while let Some(row) = rows.try_next().await? {
|
||||
todos.push(row);
|
||||
}
|
||||
|
||||
// why can't we just have async closures?
|
||||
// let mut rows: Vec<Todo> = rows.iter().map(|t| async { t }).collect();
|
||||
|
||||
let mut converted_todos = Vec::with_capacity(todos.len());
|
||||
|
||||
for t in todos {
|
||||
let todo = t.into_todo(&pool).await;
|
||||
converted_todos.push(todo);
|
||||
}
|
||||
|
||||
let todos: Vec<Todo> = converted_todos;
|
||||
|
||||
Ok(todos)
|
||||
Ok(join_all(
|
||||
sqlx::query_as::<_, SqlTodo>("SELECT * FROM todos")
|
||||
.fetch_all(&pool)
|
||||
.await?
|
||||
.iter()
|
||||
.map(|todo: &SqlTodo| todo.clone().into_todo(&pool)),
|
||||
)
|
||||
.await)
|
||||
}
|
||||
|
||||
#[server(AddTodo, "/api")]
|
||||
|
@ -94,17 +80,14 @@ pub async fn add_todo(title: String) -> Result<(), ServerFnError> {
|
|||
// fake API delay
|
||||
std::thread::sleep(std::time::Duration::from_millis(1250));
|
||||
|
||||
match sqlx::query(
|
||||
Ok(sqlx::query(
|
||||
"INSERT INTO todos (title, user_id, completed) VALUES (?, ?, false)",
|
||||
)
|
||||
.bind(title)
|
||||
.bind(id)
|
||||
.execute(&pool)
|
||||
.await
|
||||
{
|
||||
Ok(_row) => Ok(()),
|
||||
Err(e) => Err(ServerFnError::ServerError(e.to_string())),
|
||||
}
|
||||
.map(|_| ())?)
|
||||
}
|
||||
|
||||
// The struct name and path prefix arguments are optional.
|
||||
|
@ -138,51 +121,71 @@ pub fn TodoApp() -> impl IntoView {
|
|||
provide_meta_context();
|
||||
|
||||
view! {
|
||||
|
||||
<Link rel="shortcut icon" type_="image/ico" href="/favicon.ico"/>
|
||||
<Stylesheet id="leptos" href="/pkg/session_auth_axum.css"/>
|
||||
<Router>
|
||||
<header>
|
||||
<A href="/"><h1>"My Tasks"</h1></A>
|
||||
<Transition
|
||||
fallback=move || view! {<span>"Loading..."</span>}
|
||||
>
|
||||
{move || {
|
||||
user.get().map(|user| match user {
|
||||
Err(e) => view! {
|
||||
<A href="/signup">"Signup"</A>", "
|
||||
<A href="/login">"Login"</A>", "
|
||||
<span>{format!("Login error: {}", e)}</span>
|
||||
}.into_view(),
|
||||
Ok(None) => view! {
|
||||
<A href="/signup">"Signup"</A>", "
|
||||
<A href="/login">"Login"</A>", "
|
||||
<span>"Logged out."</span>
|
||||
}.into_view(),
|
||||
Ok(Some(user)) => view! {
|
||||
<A href="/settings">"Settings"</A>", "
|
||||
<span>{format!("Logged in as: {} ({})", user.username, user.id)}</span>
|
||||
}.into_view()
|
||||
})
|
||||
}}
|
||||
<A href="/">
|
||||
<h1>"My Tasks"</h1>
|
||||
</A>
|
||||
<Transition fallback=move || {
|
||||
view! { <span>"Loading..."</span> }
|
||||
}>
|
||||
{move || {
|
||||
user.get()
|
||||
.map(|user| match user {
|
||||
Err(e) => {
|
||||
view! {
|
||||
<A href="/signup">"Signup"</A>
|
||||
", "
|
||||
<A href="/login">"Login"</A>
|
||||
", "
|
||||
<span>{format!("Login error: {}", e)}</span>
|
||||
}
|
||||
.into_view()
|
||||
}
|
||||
Ok(None) => {
|
||||
view! {
|
||||
<A href="/signup">"Signup"</A>
|
||||
", "
|
||||
<A href="/login">"Login"</A>
|
||||
", "
|
||||
<span>"Logged out."</span>
|
||||
}
|
||||
.into_view()
|
||||
}
|
||||
Ok(Some(user)) => {
|
||||
view! {
|
||||
<A href="/settings">"Settings"</A>
|
||||
", "
|
||||
<span>
|
||||
{format!("Logged in as: {} ({})", user.username, user.id)}
|
||||
</span>
|
||||
}
|
||||
.into_view()
|
||||
}
|
||||
})
|
||||
}}
|
||||
|
||||
</Transition>
|
||||
</header>
|
||||
<hr/>
|
||||
<main>
|
||||
<Routes>
|
||||
<Route path="" view=Todos/> //Route
|
||||
<Route path="signup" view=move || view! {
|
||||
<Signup action=signup/>
|
||||
}/>
|
||||
<Route path="login" view=move || view! {
|
||||
// Route
|
||||
<Route path="" view=Todos/>
|
||||
<Route path="signup" view=move || view! { <Signup action=signup/> }/>
|
||||
<Route path="login" view=move || view! { <Login action=login/> }/>
|
||||
<Route
|
||||
path="settings"
|
||||
view=move || {
|
||||
view! {
|
||||
<h1>"Settings"</h1>
|
||||
<Logout action=logout/>
|
||||
}
|
||||
}
|
||||
/>
|
||||
|
||||
<Login action=login />
|
||||
}/>
|
||||
<Route path="settings" view=move || view! {
|
||||
|
||||
<h1>"Settings"</h1>
|
||||
<Logout action=logout />
|
||||
}/>
|
||||
</Routes>
|
||||
</main>
|
||||
</Router>
|
||||
|
@ -202,24 +205,26 @@ pub fn Todos() -> impl IntoView {
|
|||
);
|
||||
|
||||
view! {
|
||||
|
||||
<div>
|
||||
<MultiActionForm action=add_todo>
|
||||
<label>
|
||||
"Add a Todo"
|
||||
<input type="text" name="title"/>
|
||||
</label>
|
||||
<label>"Add a Todo" <input type="text" name="title"/></label>
|
||||
<input type="submit" value="Add"/>
|
||||
</MultiActionForm>
|
||||
<Transition fallback=move || view! {<p>"Loading..."</p> }>
|
||||
<ErrorBoundary fallback=|errors| view!{ <ErrorTemplate errors=errors/>}>
|
||||
<Transition fallback=move || view! { <p>"Loading..."</p> }>
|
||||
<ErrorBoundary fallback=|errors| {
|
||||
view! { <ErrorTemplate errors=errors/> }
|
||||
}>
|
||||
{move || {
|
||||
let existing_todos = {
|
||||
move || {
|
||||
todos.get()
|
||||
todos
|
||||
.get()
|
||||
.map(move |todos| match todos {
|
||||
Err(e) => {
|
||||
view! { <pre class="error">"Server Error: " {e.to_string()}</pre>}.into_view()
|
||||
view! {
|
||||
<pre class="error">"Server Error: " {e.to_string()}</pre>
|
||||
}
|
||||
.into_view()
|
||||
}
|
||||
Ok(todos) => {
|
||||
if todos.is_empty() {
|
||||
|
@ -229,17 +234,11 @@ pub fn Todos() -> impl IntoView {
|
|||
.into_iter()
|
||||
.map(move |todo| {
|
||||
view! {
|
||||
|
||||
<li>
|
||||
{todo.title}
|
||||
": Created at "
|
||||
{todo.created_at}
|
||||
" by "
|
||||
{
|
||||
todo.user.unwrap_or_default().username
|
||||
}
|
||||
{todo.title} ": Created at " {todo.created_at} " by "
|
||||
{todo.user.unwrap_or_default().username}
|
||||
<ActionForm action=delete_todo>
|
||||
<input type="hidden" name="id" value={todo.id}/>
|
||||
<input type="hidden" name="id" value=todo.id/>
|
||||
<input type="submit" value="X"/>
|
||||
</ActionForm>
|
||||
</li>
|
||||
|
@ -252,30 +251,23 @@ pub fn Todos() -> impl IntoView {
|
|||
.unwrap_or_default()
|
||||
}
|
||||
};
|
||||
|
||||
let pending_todos = move || {
|
||||
submissions
|
||||
.get()
|
||||
.into_iter()
|
||||
.filter(|submission| submission.pending().get())
|
||||
.map(|submission| {
|
||||
view! {
|
||||
|
||||
<li class="pending">{move || submission.input.get().map(|data| data.title) }</li>
|
||||
}
|
||||
})
|
||||
.collect_view()
|
||||
.get()
|
||||
.into_iter()
|
||||
.filter(|submission| submission.pending().get())
|
||||
.map(|submission| {
|
||||
view! {
|
||||
<li class="pending">
|
||||
{move || submission.input.get().map(|data| data.title)}
|
||||
</li>
|
||||
}
|
||||
})
|
||||
.collect_view()
|
||||
};
|
||||
view! { <ul>{existing_todos} {pending_todos}</ul> }
|
||||
}}
|
||||
|
||||
view! {
|
||||
|
||||
<ul>
|
||||
{existing_todos}
|
||||
{pending_todos}
|
||||
</ul>
|
||||
}
|
||||
}
|
||||
}
|
||||
</ErrorBoundary>
|
||||
</Transition>
|
||||
</div>
|
||||
|
@ -287,25 +279,32 @@ pub fn Login(
|
|||
action: Action<Login, Result<(), ServerFnError>>,
|
||||
) -> impl IntoView {
|
||||
view! {
|
||||
|
||||
<ActionForm action=action>
|
||||
<h1>"Log In"</h1>
|
||||
<label>
|
||||
"User ID:"
|
||||
<input type="text" placeholder="User ID" maxlength="32" name="username" class="auth-input" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="User ID"
|
||||
maxlength="32"
|
||||
name="username"
|
||||
class="auth-input"
|
||||
/>
|
||||
</label>
|
||||
<br/>
|
||||
<label>
|
||||
"Password:"
|
||||
<input type="password" placeholder="Password" name="password" class="auth-input" />
|
||||
<input type="password" placeholder="Password" name="password" class="auth-input"/>
|
||||
</label>
|
||||
<br/>
|
||||
<label>
|
||||
<input type="checkbox" name="remember" class="auth-input" />
|
||||
<input type="checkbox" name="remember" class="auth-input"/>
|
||||
"Remember me?"
|
||||
</label>
|
||||
<br/>
|
||||
<button type="submit" class="button">"Log In"</button>
|
||||
<button type="submit" class="button">
|
||||
"Log In"
|
||||
</button>
|
||||
</ActionForm>
|
||||
}
|
||||
}
|
||||
|
@ -315,31 +314,42 @@ pub fn Signup(
|
|||
action: Action<Signup, Result<(), ServerFnError>>,
|
||||
) -> impl IntoView {
|
||||
view! {
|
||||
|
||||
<ActionForm action=action>
|
||||
<h1>"Sign Up"</h1>
|
||||
<label>
|
||||
"User ID:"
|
||||
<input type="text" placeholder="User ID" maxlength="32" name="username" class="auth-input" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="User ID"
|
||||
maxlength="32"
|
||||
name="username"
|
||||
class="auth-input"
|
||||
/>
|
||||
</label>
|
||||
<br/>
|
||||
<label>
|
||||
"Password:"
|
||||
<input type="password" placeholder="Password" name="password" class="auth-input" />
|
||||
<input type="password" placeholder="Password" name="password" class="auth-input"/>
|
||||
</label>
|
||||
<br/>
|
||||
<label>
|
||||
"Confirm Password:"
|
||||
<input type="password" placeholder="Password again" name="password_confirmation" class="auth-input" />
|
||||
<input
|
||||
type="password"
|
||||
placeholder="Password again"
|
||||
name="password_confirmation"
|
||||
class="auth-input"
|
||||
/>
|
||||
</label>
|
||||
<br/>
|
||||
<label>
|
||||
"Remember me?"
|
||||
<input type="checkbox" name="remember" class="auth-input" />
|
||||
"Remember me?" <input type="checkbox" name="remember" class="auth-input"/>
|
||||
</label>
|
||||
|
||||
<br/>
|
||||
<button type="submit" class="button">"Sign Up"</button>
|
||||
<button type="submit" class="button">
|
||||
"Sign Up"
|
||||
</button>
|
||||
</ActionForm>
|
||||
}
|
||||
}
|
||||
|
@ -349,10 +359,11 @@ pub fn Logout(
|
|||
action: Action<Logout, Result<(), ServerFnError>>,
|
||||
) -> impl IntoView {
|
||||
view! {
|
||||
|
||||
<div id="loginbox">
|
||||
<ActionForm action=action>
|
||||
<button type="submit" class="button">"Log Out"</button>
|
||||
<button type="submit" class="button">
|
||||
"Log Out"
|
||||
</button>
|
||||
</ActionForm>
|
||||
</div>
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue