UI
This commit is contained in:
19
assets/views/admin/about.html
Normal file
19
assets/views/admin/about.html
Normal file
@@ -0,0 +1,19 @@
|
||||
{% extends "admin/base.html" %}
|
||||
|
||||
{% block title %}Edit About{% endblock title %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Edit About</h1>
|
||||
|
||||
<form method="post" action="/admin/about">
|
||||
<label>
|
||||
Title
|
||||
<input type="text" name="title" value="{{ page.title }}" required>
|
||||
</label>
|
||||
<label>
|
||||
Content
|
||||
<textarea name="content" rows="16" required>{{ page.content }}</textarea>
|
||||
</label>
|
||||
<button type="submit">Save</button>
|
||||
</form>
|
||||
{% endblock content %}
|
||||
27
assets/views/admin/base.html
Normal file
27
assets/views/admin/base.html
Normal file
@@ -0,0 +1,27 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{% block title %}Admin{% endblock title %}</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link href="https://cdn.jsdelivr.net/npm/daisyui@4/dist/full.min.css" rel="stylesheet" type="text/css">
|
||||
<script src="https://unpkg.com/htmx.org@1.9.12"></script>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<nav>
|
||||
<a href="/admin/dashboard">Dashboard</a>
|
||||
<a href="/admin/blog/articles">Blog</a>
|
||||
<a href="/admin/about">About</a>
|
||||
<a href="/">View site</a>
|
||||
<form method="post" action="/admin/logout">
|
||||
<button type="submit">Logout</button>
|
||||
</form>
|
||||
</nav>
|
||||
</header>
|
||||
<main>
|
||||
{% block content %}{% endblock content %}
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
31
assets/views/admin/blog/edit.html
Normal file
31
assets/views/admin/blog/edit.html
Normal file
@@ -0,0 +1,31 @@
|
||||
{% extends "admin/base.html" %}
|
||||
|
||||
{% block title %}Edit Article{% endblock title %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Edit Article</h1>
|
||||
|
||||
<form method="post" action="/admin/blog/articles/{{ article.id }}">
|
||||
<label>
|
||||
Title
|
||||
<input type="text" name="title" value="{{ article.title }}" required>
|
||||
</label>
|
||||
<label>
|
||||
Excerpt
|
||||
<textarea name="excerpt" rows="4">{% if article.excerpt %}{{ article.excerpt }}{% endif %}</textarea>
|
||||
</label>
|
||||
<label>
|
||||
Content
|
||||
<textarea name="content" rows="18" required>{{ article.content }}</textarea>
|
||||
</label>
|
||||
<label>
|
||||
Featured image id
|
||||
<input type="text" name="featured_image_id" value="{% if article.featured_image_id %}{{ article.featured_image_id }}{% endif %}">
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" name="published" {% if article.published %}checked{% endif %}>
|
||||
Published
|
||||
</label>
|
||||
<button type="submit">Save</button>
|
||||
</form>
|
||||
{% endblock content %}
|
||||
36
assets/views/admin/blog/index.html
Normal file
36
assets/views/admin/blog/index.html
Normal file
@@ -0,0 +1,36 @@
|
||||
{% extends "admin/base.html" %}
|
||||
|
||||
{% block title %}Blog Articles{% endblock title %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Blog Articles</h1>
|
||||
<p><a href="/admin/blog/articles/new">New article</a></p>
|
||||
|
||||
{% if articles | length > 0 %}
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Title</th>
|
||||
<th>Status</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for article in articles %}
|
||||
<tr>
|
||||
<td>{{ article.title }}</td>
|
||||
<td>{% if article.published %}Published{% else %}Draft{% endif %}</td>
|
||||
<td>
|
||||
<a href="/admin/blog/articles/{{ article.id }}/edit">Edit</a>
|
||||
<form method="post" action="/admin/blog/articles/{{ article.id }}/delete">
|
||||
<button type="submit">Delete</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p>No articles yet.</p>
|
||||
{% endif %}
|
||||
{% endblock content %}
|
||||
31
assets/views/admin/blog/new.html
Normal file
31
assets/views/admin/blog/new.html
Normal file
@@ -0,0 +1,31 @@
|
||||
{% extends "admin/base.html" %}
|
||||
|
||||
{% block title %}New Article{% endblock title %}
|
||||
|
||||
{% block content %}
|
||||
<h1>New Article</h1>
|
||||
|
||||
<form method="post" action="/admin/blog/articles">
|
||||
<label>
|
||||
Title
|
||||
<input type="text" name="title" required>
|
||||
</label>
|
||||
<label>
|
||||
Excerpt
|
||||
<textarea name="excerpt" rows="4"></textarea>
|
||||
</label>
|
||||
<label>
|
||||
Content
|
||||
<textarea name="content" rows="18" required></textarea>
|
||||
</label>
|
||||
<label>
|
||||
Featured image id
|
||||
<input type="text" name="featured_image_id">
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" name="published">
|
||||
Published
|
||||
</label>
|
||||
<button type="submit">Create</button>
|
||||
</form>
|
||||
{% endblock content %}
|
||||
13
assets/views/admin/index.html
Normal file
13
assets/views/admin/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
{% extends "admin/base.html" %}
|
||||
|
||||
{% block title %}Admin{% endblock title %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Admin</h1>
|
||||
<p>Logged in as {{ admin.email }}</p>
|
||||
|
||||
<ul>
|
||||
<li><a href="/admin/blog/articles">Manage blog articles</a></li>
|
||||
<li><a href="/admin/about">Edit about page</a></li>
|
||||
</ul>
|
||||
{% endblock content %}
|
||||
23
assets/views/admin/login.html
Normal file
23
assets/views/admin/login.html
Normal file
@@ -0,0 +1,23 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Admin login{% endblock title %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Admin login</h1>
|
||||
|
||||
{% if error %}
|
||||
<p>{{ error }}</p>
|
||||
{% endif %}
|
||||
|
||||
<form method="post" action="/admin/login">
|
||||
<label>
|
||||
Email
|
||||
<input type="email" name="email" required>
|
||||
</label>
|
||||
<label>
|
||||
Password
|
||||
<input type="password" name="password" required>
|
||||
</label>
|
||||
<button type="submit">Login</button>
|
||||
</form>
|
||||
{% endblock content %}
|
||||
24
assets/views/base.html
Normal file
24
assets/views/base.html
Normal file
@@ -0,0 +1,24 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{% block title %}Universal Web{% endblock title %}</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link href="https://cdn.jsdelivr.net/npm/daisyui@4/dist/full.min.css" rel="stylesheet" type="text/css">
|
||||
<script src="https://unpkg.com/htmx.org@1.9.12"></script>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<nav>
|
||||
<a href="/">Home</a>
|
||||
<a href="/about">About</a>
|
||||
<a href="/blog">Blog</a>
|
||||
<a href="/admin/login">Admin</a>
|
||||
</nav>
|
||||
</header>
|
||||
<main>
|
||||
{% block content %}{% endblock content %}
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
20
assets/views/blog/index.html
Normal file
20
assets/views/blog/index.html
Normal file
@@ -0,0 +1,20 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Blog{% endblock title %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Blog</h1>
|
||||
|
||||
{% if articles | length > 0 %}
|
||||
<ul>
|
||||
{% for article in articles %}
|
||||
<li>
|
||||
<a href="/blog/{{ article.slug }}">{{ article.title }}</a>
|
||||
{% if article.excerpt %}<p>{{ article.excerpt }}</p>{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<p>No published posts yet.</p>
|
||||
{% endif %}
|
||||
{% endblock content %}
|
||||
12
assets/views/blog/show.html
Normal file
12
assets/views/blog/show.html
Normal file
@@ -0,0 +1,12 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}{{ article.title }}{% endblock title %}
|
||||
|
||||
{% block content %}
|
||||
<article>
|
||||
<h1>{{ article.title }}</h1>
|
||||
<p>Views: {{ article.view_count }}</p>
|
||||
{% if article.excerpt %}<p>{{ article.excerpt }}</p>{% endif %}
|
||||
<div>{{ article.content | linebreaksbr | safe }}</div>
|
||||
</article>
|
||||
{% endblock content %}
|
||||
23
assets/views/home/index.html
Normal file
23
assets/views/home/index.html
Normal file
@@ -0,0 +1,23 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Home{% endblock title %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Home</h1>
|
||||
|
||||
<section>
|
||||
<h2>Latest posts</h2>
|
||||
{% if articles | length > 0 %}
|
||||
<ul>
|
||||
{% for article in articles %}
|
||||
<li>
|
||||
<a href="/blog/{{ article.slug }}">{{ article.title }}</a>
|
||||
{% if article.excerpt %}<p>{{ article.excerpt }}</p>{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<p>No published posts yet.</p>
|
||||
{% endif %}
|
||||
</section>
|
||||
{% endblock content %}
|
||||
10
assets/views/pages/about.html
Normal file
10
assets/views/pages/about.html
Normal file
@@ -0,0 +1,10 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}{{ page.title }}{% endblock title %}
|
||||
|
||||
{% block content %}
|
||||
<article>
|
||||
<h1>{{ page.title }}</h1>
|
||||
<div>{{ page.content | linebreaksbr | safe }}</div>
|
||||
</article>
|
||||
{% endblock content %}
|
||||
@@ -55,6 +55,7 @@ impl Hooks for App {
|
||||
.add_route(controllers::admin::routes())
|
||||
.add_route(controllers::blog::routes())
|
||||
.add_route(controllers::pages::routes())
|
||||
.add_route(controllers::frontend::routes())
|
||||
}
|
||||
async fn connect_workers(ctx: &AppContext, queue: &Queue) -> Result<()> {
|
||||
queue.register(DownloadWorker::build(ctx)).await?;
|
||||
|
||||
@@ -34,7 +34,7 @@ fn is_admin(ctx: &AppContext, user: &users::Model) -> bool {
|
||||
admin_email(ctx).is_some_and(|email| user.email.eq_ignore_ascii_case(email))
|
||||
}
|
||||
|
||||
fn auth_cookie(token: &str, max_age_seconds: u64) -> Cookie<'static> {
|
||||
pub(crate) fn auth_cookie(token: &str, max_age_seconds: u64) -> Cookie<'static> {
|
||||
Cookie::build((AUTH_COOKIE, token.to_string()))
|
||||
.path("/")
|
||||
.http_only(true)
|
||||
@@ -43,7 +43,7 @@ fn auth_cookie(token: &str, max_age_seconds: u64) -> Cookie<'static> {
|
||||
.build()
|
||||
}
|
||||
|
||||
fn clear_auth_cookie() -> Cookie<'static> {
|
||||
pub(crate) fn clear_auth_cookie() -> Cookie<'static> {
|
||||
Cookie::build((AUTH_COOKIE, ""))
|
||||
.path("/")
|
||||
.http_only(true)
|
||||
|
||||
344
src/controllers/frontend.rs
Normal file
344
src/controllers/frontend.rs
Normal file
@@ -0,0 +1,344 @@
|
||||
use crate::{
|
||||
controllers::{admin, auth as auth_controller},
|
||||
models::{
|
||||
_entities::{blog_articles, site_pages},
|
||||
users::{self, LoginParams},
|
||||
},
|
||||
};
|
||||
use chrono::Utc;
|
||||
use loco_rs::prelude::*;
|
||||
use sea_orm::{
|
||||
ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, QueryOrder, QuerySelect, Set,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use serde_json::json;
|
||||
use uuid::Uuid;
|
||||
|
||||
const ABOUT_SLUG: &str = "about";
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct ArticleForm {
|
||||
title: String,
|
||||
content: String,
|
||||
excerpt: Option<String>,
|
||||
published: Option<String>,
|
||||
featured_image_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct AboutForm {
|
||||
title: String,
|
||||
content: String,
|
||||
}
|
||||
|
||||
fn slugify(title: &str) -> String {
|
||||
let mut slug = String::new();
|
||||
let mut last_was_dash = false;
|
||||
|
||||
for ch in title.chars().flat_map(char::to_lowercase) {
|
||||
if ch.is_ascii_alphanumeric() {
|
||||
slug.push(ch);
|
||||
last_was_dash = false;
|
||||
} else if !last_was_dash && !slug.is_empty() {
|
||||
slug.push('-');
|
||||
last_was_dash = true;
|
||||
}
|
||||
}
|
||||
|
||||
let slug = slug.trim_matches('-').to_string();
|
||||
if slug.is_empty() {
|
||||
Uuid::new_v4().to_string()
|
||||
} else {
|
||||
slug
|
||||
}
|
||||
}
|
||||
|
||||
fn published_at_for(published: bool) -> Option<chrono::DateTime<chrono::FixedOffset>> {
|
||||
published.then(|| Utc::now().into())
|
||||
}
|
||||
|
||||
fn is_checked(value: &Option<String>) -> bool {
|
||||
value.as_deref().is_some_and(|value| value == "on" || value == "true")
|
||||
}
|
||||
|
||||
fn normalize_empty(value: Option<String>) -> Option<String> {
|
||||
value.and_then(|value| {
|
||||
let value = value.trim().to_string();
|
||||
if value.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(value)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async fn about_page(ctx: &AppContext) -> Result<site_pages::Model> {
|
||||
site_pages::Entity::find()
|
||||
.filter(site_pages::Column::Slug.eq(ABOUT_SLUG))
|
||||
.one(&ctx.db)
|
||||
.await?
|
||||
.ok_or_else(|| Error::NotFound)
|
||||
}
|
||||
|
||||
async fn article_by_id(ctx: &AppContext, id: Uuid) -> Result<blog_articles::Model> {
|
||||
blog_articles::Entity::find_by_id(id)
|
||||
.one(&ctx.db)
|
||||
.await?
|
||||
.ok_or_else(|| Error::NotFound)
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
async fn home(
|
||||
ViewEngine(v): ViewEngine<TeraView>,
|
||||
State(ctx): State<AppContext>,
|
||||
) -> Result<Response> {
|
||||
let articles = blog_articles::Entity::find()
|
||||
.filter(blog_articles::Column::Published.eq(true))
|
||||
.order_by_desc(blog_articles::Column::PublishedAt)
|
||||
.limit(5)
|
||||
.all(&ctx.db)
|
||||
.await?;
|
||||
|
||||
format::view(&v, "home/index.html", json!({ "articles": articles }))
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
async fn about(
|
||||
ViewEngine(v): ViewEngine<TeraView>,
|
||||
State(ctx): State<AppContext>,
|
||||
) -> Result<Response> {
|
||||
format::view(&v, "pages/about.html", json!({ "page": about_page(&ctx).await? }))
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
async fn blog_index(
|
||||
ViewEngine(v): ViewEngine<TeraView>,
|
||||
State(ctx): State<AppContext>,
|
||||
) -> Result<Response> {
|
||||
let articles = blog_articles::Entity::find()
|
||||
.filter(blog_articles::Column::Published.eq(true))
|
||||
.order_by_desc(blog_articles::Column::PublishedAt)
|
||||
.all(&ctx.db)
|
||||
.await?;
|
||||
|
||||
format::view(&v, "blog/index.html", json!({ "articles": articles }))
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
async fn blog_show(
|
||||
ViewEngine(v): ViewEngine<TeraView>,
|
||||
Path(slug): Path<String>,
|
||||
State(ctx): State<AppContext>,
|
||||
) -> Result<Response> {
|
||||
let article = blog_articles::Entity::find()
|
||||
.filter(blog_articles::Column::Slug.eq(slug))
|
||||
.filter(blog_articles::Column::Published.eq(true))
|
||||
.one(&ctx.db)
|
||||
.await?
|
||||
.ok_or_else(|| Error::NotFound)?;
|
||||
|
||||
let mut active = article.into_active_model();
|
||||
let next_count = active.view_count.as_ref().to_owned() + 1;
|
||||
active.view_count = Set(next_count);
|
||||
let article = active.update(&ctx.db).await?;
|
||||
|
||||
format::view(&v, "blog/show.html", json!({ "article": article }))
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
async fn admin_login_page(ViewEngine(v): ViewEngine<TeraView>) -> Result<Response> {
|
||||
format::view(&v, "admin/login.html", json!({ "error": null }))
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
async fn admin_login(
|
||||
ViewEngine(v): ViewEngine<TeraView>,
|
||||
State(ctx): State<AppContext>,
|
||||
Form(params): Form<LoginParams>,
|
||||
) -> Result<Response> {
|
||||
let Ok(user) = users::Model::find_by_email(&ctx.db, ¶ms.email).await else {
|
||||
return format::view(&v, "admin/login.html", json!({ "error": "Invalid credentials" }));
|
||||
};
|
||||
|
||||
if !user.verify_password(¶ms.password) || !admin::is_admin(&ctx, &user) {
|
||||
return format::view(&v, "admin/login.html", json!({ "error": "Invalid credentials" }));
|
||||
}
|
||||
|
||||
let jwt_secret = ctx.config.get_jwt_config()?;
|
||||
let token = user
|
||||
.generate_jwt(&jwt_secret.secret, jwt_secret.expiration)
|
||||
.or_else(|_| unauthorized("unauthorized!"))?;
|
||||
|
||||
format::render()
|
||||
.cookies(&[auth_controller::auth_cookie(&token, jwt_secret.expiration)])?
|
||||
.redirect("/admin/dashboard")
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
async fn admin_logout() -> Result<Response> {
|
||||
format::render()
|
||||
.cookies(&[auth_controller::clear_auth_cookie()])?
|
||||
.redirect("/admin/login")
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
async fn admin_home(
|
||||
auth: auth::JWT,
|
||||
ViewEngine(v): ViewEngine<TeraView>,
|
||||
State(ctx): State<AppContext>,
|
||||
) -> Result<Response> {
|
||||
let admin_user = admin::current_admin(auth, &ctx).await?;
|
||||
format::view(&v, "admin/index.html", json!({ "admin": admin_user }))
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
async fn admin_about(
|
||||
auth: auth::JWT,
|
||||
ViewEngine(v): ViewEngine<TeraView>,
|
||||
State(ctx): State<AppContext>,
|
||||
) -> Result<Response> {
|
||||
admin::current_admin(auth, &ctx).await?;
|
||||
format::view(&v, "admin/about.html", json!({ "page": about_page(&ctx).await? }))
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
async fn admin_about_update(
|
||||
auth: auth::JWT,
|
||||
State(ctx): State<AppContext>,
|
||||
Form(params): Form<AboutForm>,
|
||||
) -> Result<Response> {
|
||||
admin::current_admin(auth, &ctx).await?;
|
||||
let mut page = about_page(&ctx).await?.into_active_model();
|
||||
page.title = Set(params.title);
|
||||
page.content = Set(params.content);
|
||||
page.update(&ctx.db).await?;
|
||||
format::redirect("/admin/about")
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
async fn admin_articles(
|
||||
auth: auth::JWT,
|
||||
ViewEngine(v): ViewEngine<TeraView>,
|
||||
State(ctx): State<AppContext>,
|
||||
) -> Result<Response> {
|
||||
admin::current_admin(auth, &ctx).await?;
|
||||
let articles = blog_articles::Entity::find()
|
||||
.order_by_desc(blog_articles::Column::CreatedAt)
|
||||
.all(&ctx.db)
|
||||
.await?;
|
||||
format::view(&v, "admin/blog/index.html", json!({ "articles": articles }))
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
async fn admin_article_new(
|
||||
auth: auth::JWT,
|
||||
ViewEngine(v): ViewEngine<TeraView>,
|
||||
State(ctx): State<AppContext>,
|
||||
) -> Result<Response> {
|
||||
admin::current_admin(auth, &ctx).await?;
|
||||
format::view(&v, "admin/blog/new.html", json!({}))
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
async fn admin_article_create(
|
||||
auth: auth::JWT,
|
||||
State(ctx): State<AppContext>,
|
||||
Form(params): Form<ArticleForm>,
|
||||
) -> Result<Response> {
|
||||
let admin_user = admin::current_admin(auth, &ctx).await?;
|
||||
let published = is_checked(¶ms.published);
|
||||
|
||||
blog_articles::ActiveModel {
|
||||
id: Set(Uuid::new_v4()),
|
||||
title: Set(params.title.clone()),
|
||||
slug: Set(slugify(¶ms.title)),
|
||||
content: Set(params.content),
|
||||
excerpt: Set(normalize_empty(params.excerpt)),
|
||||
published: Set(published),
|
||||
author_id: Set(admin_user.id),
|
||||
featured_image_id: Set(normalize_empty(params.featured_image_id)),
|
||||
view_count: Set(0),
|
||||
published_at: Set(published_at_for(published)),
|
||||
..Default::default()
|
||||
}
|
||||
.insert(&ctx.db)
|
||||
.await?;
|
||||
|
||||
format::redirect("/admin/blog/articles")
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
async fn admin_article_edit(
|
||||
auth: auth::JWT,
|
||||
ViewEngine(v): ViewEngine<TeraView>,
|
||||
Path(id): Path<Uuid>,
|
||||
State(ctx): State<AppContext>,
|
||||
) -> Result<Response> {
|
||||
admin::current_admin(auth, &ctx).await?;
|
||||
format::view(
|
||||
&v,
|
||||
"admin/blog/edit.html",
|
||||
json!({ "article": article_by_id(&ctx, id).await? }),
|
||||
)
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
async fn admin_article_update(
|
||||
auth: auth::JWT,
|
||||
Path(id): Path<Uuid>,
|
||||
State(ctx): State<AppContext>,
|
||||
Form(params): Form<ArticleForm>,
|
||||
) -> Result<Response> {
|
||||
admin::current_admin(auth, &ctx).await?;
|
||||
let existing = article_by_id(&ctx, id).await?;
|
||||
let was_published = existing.published;
|
||||
let published = is_checked(¶ms.published);
|
||||
|
||||
let mut article = existing.into_active_model();
|
||||
article.title = Set(params.title.clone());
|
||||
article.slug = Set(slugify(¶ms.title));
|
||||
article.content = Set(params.content);
|
||||
article.excerpt = Set(normalize_empty(params.excerpt));
|
||||
article.published = Set(published);
|
||||
article.featured_image_id = Set(normalize_empty(params.featured_image_id));
|
||||
if published && !was_published {
|
||||
article.published_at = Set(published_at_for(true));
|
||||
} else if !published {
|
||||
article.published_at = Set(None);
|
||||
}
|
||||
article.update(&ctx.db).await?;
|
||||
|
||||
format::redirect("/admin/blog/articles")
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
async fn admin_article_delete(
|
||||
auth: auth::JWT,
|
||||
Path(id): Path<Uuid>,
|
||||
State(ctx): State<AppContext>,
|
||||
) -> Result<Response> {
|
||||
admin::current_admin(auth, &ctx).await?;
|
||||
article_by_id(&ctx, id).await?.delete(&ctx.db).await?;
|
||||
format::redirect("/admin/blog/articles")
|
||||
}
|
||||
|
||||
pub fn routes() -> Routes {
|
||||
Routes::new()
|
||||
.add("/", get(home))
|
||||
.add("/about", get(about))
|
||||
.add("/blog", get(blog_index))
|
||||
.add("/blog/{slug}", get(blog_show))
|
||||
.add("/admin/login", get(admin_login_page))
|
||||
.add("/admin/login", post(admin_login))
|
||||
.add("/admin/logout", post(admin_logout))
|
||||
.add("/admin", get(admin_login_page))
|
||||
.add("/admin/dashboard", get(admin_home))
|
||||
.add("/admin/about", get(admin_about))
|
||||
.add("/admin/about", post(admin_about_update))
|
||||
.add("/admin/blog/articles", get(admin_articles))
|
||||
.add("/admin/blog/articles/new", get(admin_article_new))
|
||||
.add("/admin/blog/articles", post(admin_article_create))
|
||||
.add("/admin/blog/articles/{id}/edit", get(admin_article_edit))
|
||||
.add("/admin/blog/articles/{id}", post(admin_article_update))
|
||||
.add("/admin/blog/articles/{id}/delete", post(admin_article_delete))
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
pub mod admin;
|
||||
pub mod auth;
|
||||
pub mod blog;
|
||||
pub mod frontend;
|
||||
pub mod pages;
|
||||
|
||||
Reference in New Issue
Block a user