Add user-dropdown
Signed-off-by: Marcel Müller <neikos@neikos.email>
This commit is contained in:
parent
79305724f2
commit
f45a43c852
7 changed files with 107 additions and 18 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -6,4 +6,4 @@
|
||||||
/.direnv
|
/.direnv
|
||||||
|
|
||||||
# Nix
|
# Nix
|
||||||
/result*
|
result*
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ use std::sync::LazyLock;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use axum::Router;
|
use axum::Router;
|
||||||
|
use axum::extract::FromRequestParts;
|
||||||
use axum::http::StatusCode;
|
use axum::http::StatusCode;
|
||||||
use axum::response::Html;
|
use axum::response::Html;
|
||||||
use axum::response::IntoResponse;
|
use axum::response::IntoResponse;
|
||||||
|
|
@ -44,7 +45,9 @@ impl IntoResponse for AppError {
|
||||||
(
|
(
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
Html(
|
Html(
|
||||||
render_template("internal_error.tera.html", &error_context)
|
TERA.read()
|
||||||
|
.unwrap()
|
||||||
|
.render("internal_error.tera.html", &error_context)
|
||||||
.unwrap_or_else(|_| "ERROR RENDERING ERROR! FATAL".to_string()),
|
.unwrap_or_else(|_| "ERROR RENDERING ERROR! FATAL".to_string()),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
@ -59,7 +62,7 @@ async fn main() -> anyhow::Result<()> {
|
||||||
|
|
||||||
type AuthSession = axum_login::AuthSession<Backend>;
|
type AuthSession = axum_login::AuthSession<Backend>;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Debug, Clone)]
|
||||||
struct Backend {
|
struct Backend {
|
||||||
db: SqlitePool,
|
db: SqlitePool,
|
||||||
}
|
}
|
||||||
|
|
@ -150,6 +153,7 @@ async fn run() -> anyhow::Result<()> {
|
||||||
.route("/protected", get(show_protected))
|
.route("/protected", get(show_protected))
|
||||||
.route_layer(login_required!(Backend, login_url = "/login"))
|
.route_layer(login_required!(Backend, login_url = "/login"))
|
||||||
.merge(users::routes())
|
.merge(users::routes())
|
||||||
|
.route("/", get(show_index))
|
||||||
.layer(auth_layer)
|
.layer(auth_layer)
|
||||||
.layer(livereload)
|
.layer(livereload)
|
||||||
.with_state(AppState { db });
|
.with_state(AppState { db });
|
||||||
|
|
@ -194,6 +198,10 @@ async fn run() -> anyhow::Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn show_index(renderer: Renderer) -> WebResult<Html<String>> {
|
||||||
|
renderer.render_template("index.tera.html", None).map(Html)
|
||||||
|
}
|
||||||
|
|
||||||
async fn shutdown_signal(handle: AbortHandle) {
|
async fn shutdown_signal(handle: AbortHandle) {
|
||||||
let ctrl_c = async {
|
let ctrl_c = async {
|
||||||
tokio::signal::ctrl_c()
|
tokio::signal::ctrl_c()
|
||||||
|
|
@ -216,10 +224,33 @@ async fn shutdown_signal(handle: AbortHandle) {
|
||||||
handle.abort();
|
handle.abort();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn render_template(name: &str, context: &Context) -> tera::Result<String> {
|
#[derive(Debug, FromRequestParts)]
|
||||||
TERA.read().unwrap().render(name, context)
|
pub struct Renderer {
|
||||||
|
auth: AuthSession,
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn show_protected() -> Html<String> {
|
impl Renderer {
|
||||||
Html(render_template("protected.tera.html", &Context::default()).unwrap())
|
pub fn render_template(
|
||||||
|
&self,
|
||||||
|
name: &str,
|
||||||
|
context: impl Into<Option<Context>>,
|
||||||
|
) -> WebResult<String> {
|
||||||
|
let mut main_context = Context::default();
|
||||||
|
if let Some(user) = self.auth.user.as_ref() {
|
||||||
|
main_context.insert("logged_in", &true);
|
||||||
|
main_context.insert("current_user", user);
|
||||||
|
}
|
||||||
|
|
||||||
|
main_context.extend(context.into().unwrap_or_default());
|
||||||
|
|
||||||
|
Ok(TERA.read().unwrap().render(name, &main_context)?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn show_protected(renderer: Renderer) -> Html<String> {
|
||||||
|
Html(
|
||||||
|
renderer
|
||||||
|
.render_template("protected.tera.html", None)
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,15 +3,16 @@ use axum::http::StatusCode;
|
||||||
use axum::response::Html;
|
use axum::response::Html;
|
||||||
use axum::response::IntoResponse;
|
use axum::response::IntoResponse;
|
||||||
use axum::response::Redirect;
|
use axum::response::Redirect;
|
||||||
use tera::Context;
|
|
||||||
|
|
||||||
use crate::AuthSession;
|
use crate::AuthSession;
|
||||||
|
use crate::Renderer;
|
||||||
use crate::UserCredentials;
|
use crate::UserCredentials;
|
||||||
use crate::WebResult;
|
use crate::WebResult;
|
||||||
use crate::render_template;
|
|
||||||
|
|
||||||
pub async fn show_login() -> WebResult<Html<String>> {
|
pub async fn show_login(renderer: Renderer) -> WebResult<Html<String>> {
|
||||||
Ok(render_template("users/login.tera.html", &Context::default()).map(Html)?)
|
renderer
|
||||||
|
.render_template("users/login.tera.html", None)
|
||||||
|
.map(Html)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn do_login(
|
pub async fn do_login(
|
||||||
|
|
|
||||||
|
|
@ -4,15 +4,16 @@ use axum::response::Html;
|
||||||
use axum::response::IntoResponse;
|
use axum::response::IntoResponse;
|
||||||
use axum::response::Redirect;
|
use axum::response::Redirect;
|
||||||
use password_auth::generate_hash;
|
use password_auth::generate_hash;
|
||||||
use tera::Context;
|
|
||||||
|
|
||||||
use crate::AppState;
|
use crate::AppState;
|
||||||
|
use crate::Renderer;
|
||||||
use crate::UserCredentials;
|
use crate::UserCredentials;
|
||||||
use crate::WebResult;
|
use crate::WebResult;
|
||||||
use crate::render_template;
|
|
||||||
|
|
||||||
pub async fn show_register() -> WebResult<Html<String>> {
|
pub async fn show_register(renderer: Renderer) -> WebResult<Html<String>> {
|
||||||
Ok(render_template("users/register.tera.html", &Context::default()).map(Html)?)
|
renderer
|
||||||
|
.render_template("users/register.tera.html", None)
|
||||||
|
.map(Html)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn do_register(
|
pub async fn do_register(
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
|
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
|
||||||
|
<script type="module" src="https://cdn.jsdelivr.net/gh/starfederation/datastar@1.0.0-RC.7/bundles/datastar.js"></script>
|
||||||
{% block head %}
|
{% block head %}
|
||||||
<title>{% block title %}{% endblock title%} - Nixie CI</title>
|
<title>{% block title %}{% endblock title%} - Nixie CI</title>
|
||||||
{% endblock head%}
|
{% endblock head%}
|
||||||
|
|
@ -12,15 +13,32 @@
|
||||||
|
|
||||||
<body class="min-h-screen flex flex-col">
|
<body class="min-h-screen flex flex-col">
|
||||||
<nav class="bg-orange-300 px-2 py-2 inset-shadow-2xs border-b-2 border-orange-900">
|
<nav class="bg-orange-300 px-2 py-2 inset-shadow-2xs border-b-2 border-orange-900">
|
||||||
<div class="mx-auto container flex select-none">
|
<div class="mx-auto container flex select-none space-x-4">
|
||||||
<a href="/" class="outline-2 outline-gray-800 bg-gray-700 rounded text-white p-2">
|
<a href="/" class="outline-2 outline-gray-800 bg-gray-700 rounded text-white p-2">
|
||||||
<span class="text-green-200">>_</span>
|
<span class="text-green-200">>_</span>
|
||||||
Nixie CI ❄️
|
Nixie CI ❄️
|
||||||
</a>
|
</a>
|
||||||
|
<div></div>
|
||||||
|
<a href="/jobs/" class="outline-2 outline-cyan-800 bg-cyan-700 rounded text-white p-2">Jobs</a>
|
||||||
<div class="grow"></div>
|
<div class="grow"></div>
|
||||||
|
|
||||||
<a href="/login" class="outline-2 outline-r-none hover:outline-emerald-900 outline-emerald-700 p-2 select-none rounded-l bg-emerald-600 text-white">Sign in</a>
|
{% if not logged_in %}
|
||||||
<a href="/register" class="outline-2 hover:outline-emerald-900 outline-blue-700 p-2 select-none rounded-r bg-blue-600 text-white">Register</a>
|
<a href="/login" class="outline-2 outline-r-none hover:outline-emerald-900 outline-emerald-700 p-2 select-none rounded-l bg-emerald-600 text-white">Sign in</a>
|
||||||
|
<a href="/register" class="outline-2 hover:outline-emerald-900 outline-blue-700 p-2 select-none rounded-r bg-blue-600 text-white">Register</a>
|
||||||
|
{% else %}
|
||||||
|
<div class="relative flex" data-on:click__outside="$openProfileDropdown = false">
|
||||||
|
<a href="/users/{{ current_user.id }}" class="inline-block border-2 border-r-1 hover:border-emerald-900 border-emerald-700 p-2 select-none rounded-l bg-emerald-600 text-white">
|
||||||
|
{{ current_user.username }}
|
||||||
|
</a>
|
||||||
|
<button data-on:click="$openProfileDropdown = !$openProfileDropdown"
|
||||||
|
class="border-2 border-l-1 hover:border-emerald-900 border-emerald-700 p-2 select-none rounded-r bg-emerald-600 text-white cursor-pointer">▼</button>
|
||||||
|
<div data-class:hidden="!$openProfileDropdown"
|
||||||
|
class="hidden absolute top-[calc(100%+0.4rem)] right-0 bg-orange-300 p-2 rounded-b border border-orange-800 shadow-lg md:min-w-40 flex flex-col divide-y divide-orange-800">
|
||||||
|
<a class="block p-2 hover:bg-orange-800 hover:text-white" href="/users/{{ current_user.id }}">Profile</a>
|
||||||
|
<a class="block p-2 hover:bg-orange-800 hover:text-white" href="/logout">Logout</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
<main class="grow px-2 flex flex-col inset-shadow-sm">
|
<main class="grow px-2 flex flex-col inset-shadow-sm">
|
||||||
|
|
|
||||||
10
nixie-server/templates/index.tera.html
Normal file
10
nixie-server/templates/index.tera.html
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
{% extends "base.tera.html" %}
|
||||||
|
{% import "inputs.tera.html" as inputs %}
|
||||||
|
|
||||||
|
{% block title %}
|
||||||
|
Home
|
||||||
|
{% endblock title %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
Hi this is the main page
|
||||||
|
{% endblock content %}
|
||||||
28
nixie-server/templates/users/register.tera.html
Normal file
28
nixie-server/templates/users/register.tera.html
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
{% extends "base.tera.html" %}
|
||||||
|
{% import "inputs.tera.html" as inputs %}
|
||||||
|
|
||||||
|
{% block title %}
|
||||||
|
Register
|
||||||
|
{% endblock title %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="grow sm:flex justify-center items-center">
|
||||||
|
<div class="border-2 border-zinc-300 sm:rounded-2xl lg:rounded-4xl my-2 px-20 pb-14 pt-10 sm:shadow-lg space-y-4">
|
||||||
|
<h1 class="font-bold text-3xl">Login</h1>
|
||||||
|
<form action="/register" method="POST" class="space-y-4">
|
||||||
|
{{ inputs::text_input(label="Username", name="username", id="username") }}
|
||||||
|
{{ inputs::text_input(label="Password", name="password", id="password", type="password") }}
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<button type="submit" class="bg-blue-500 p-2 rounded-lg text-white">
|
||||||
|
Register
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<a href="/register" class="bg-zinc-500 p-2 rounded-lg text-white text-center">
|
||||||
|
Login
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock content %}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue