Compare commits

..
16 changed files with 288 additions and 62 deletions

View file

@ -39,6 +39,11 @@ Compared to lldap, llldap:
We may explore in the future:
- domain mapping (eg. .org/.fr/.onion)
- optional complete logging of all activity
- 2-factor auth
- Shamir's secret sharing social account recovery
- Permission to create invites on an account
- i18n domains (punycode)
- i18n user identifiers (TODO: check compatibility with other tools)
- RTL languages

3
assets/css/bulma.min.css vendored Normal file

File diff suppressed because one or more lines are too long

12
assets/css/fork-awesome.min.css vendored Executable file

File diff suppressed because one or more lines are too long

Binary file not shown.

8
docs/groups.md Normal file
View file

@ -0,0 +1,8 @@
# Groups
> [!WARNING]
> This is a design document and does not necessarily reflect the current state
> of the implementation.
WIP: A group is only valid on a single domain, except for global service groups (eg. `admins`). A group
has permissions to perform some actions on a domain.

View file

@ -7,6 +7,29 @@ use serde::Deserialize;
use crate::db::{DatabaseInterface, Operation};
use crate::http::{HttpSession, HttpState};
pub async fn list_domains(State(state): State<HttpState>, session: HttpSession) -> Response {
let domains = match state.db.domains_user_can_see(&session.user).await {
Ok(domains) => domains,
Err(e) => {
return format!("Database error: {e}").into_response();
}
};
let ctx = context! {
domains,
user => session.user,
can_create_domain => session.user.can_create_domain(),
};
let page = state
.templates
.get_template("domains.html")
.unwrap()
.render(ctx)
.unwrap();
(StatusCode::OK, Html(page)).into_response()
}
pub async fn get_domain(
State(state): State<HttpState>,
session: HttpSession,

View file

@ -104,6 +104,7 @@ pub async fn http_listen(listener: Listener, db: Database) {
.route("/login", post(login::post_login))
.route("/logout", get(logout::logout))
.route("/domain/{domain}", get(domain::get_domain))
.route("/domain", get(domain::list_domains))
.route("/domain", post(domain::create_domain))
.route("/user", post(user::create_user))
.with_state(HttpState::new(db));

58
templates/app.html Normal file
View file

@ -0,0 +1,58 @@
{% extends 'base.html' %}
{% block header %}
<nav class="navbar" role="navigation" aria-label="main navigation">
<div class="navbar-brand">
<a class="navbar-item" href="/"><img src="/assets/img/logo.png"></a>
<a role="button" class="navbar-burger" aria-label="menu" aria-expanded="false" data-target="navMenu">
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
</a>
</div>
<div id="navMenu" class="navbar-menu">
<div class="navbar-start">
<a href="/domain" class="navbar-item">
<i class="fa fa-university" aria-hidden="true"></i>
Domains
</a>
</div>
<div class="navbar-end">
<div class="navbar-item">
<div class="buttons">
<a href="/logout" class="button is-danger">
<strong>Logout</strong>
</a>
</div>
</div>
</div>
</div>
</nav>
{% endblock header %}
{% block js %}
<script>
document.addEventListener('DOMContentLoaded', () => {
// Get all "navbar-burger" elements
const $navbarBurgers = Array.prototype.slice.call(document.querySelectorAll('.navbar-burger'), 0);
// Add a click event on each of them
$navbarBurgers.forEach( el => {
el.addEventListener('click', () => {
// Get the target from the "data-target" attribute
const target = el.dataset.target;
const $target = document.getElementById(target);
// Toggle the "is-active" class on both the "navbar-burger" and the "navbar-menu"
el.classList.toggle('is-active');
$target.classList.toggle('is-active');
});
});
});
</script>
{% endblock js %}

View file

@ -1,11 +1,22 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>LLLDAP</title>
{% block stylesheets %}
<link rel="stylesheet" href="/assets/css/bulma.min.css">
<link rel="stylesheet" href="/assets/css/fork-awesome.min.css">
<link rel="stylesheet" href="/assets/css/main.css">
{% endblock stylesheets %}
</head>
<body>
{% block main %}
{% endblock %}
{% block header %}
{% endblock header %}
{% block main %}
{% endblock main %}
{% block js %}
{% endblock js %}
</body>
</html>

View file

@ -1,30 +1,48 @@
{% extends 'base.html' %}
{% extends 'app.html' %}
{% block main %}
<main id="center">
<img src="/assets/img/logo.png" id="logo">
<p style="text-align: center;">You are logged in as {{ user.username }}</p>
<div id="login-line">
<a href="/logout" id="submit-login" value="Logout">Logout</a>
</div>
{% if can_create_user %}
<div>
<h2>Create user</h2>
<form action="/user" method="POST">
<input type="hidden" name="domain" value="{{ domain.name }}">
<input type="text" name="username" placeholder="username">
<input type="password" name="password" placeholder="password">
<button type="submit" class="button is-info">Create</button>
</form>
<main>
<section class="section">
<div class="container is-widescreen mt-5">
<div class="card">
<header class="card-header">
<p class="card-header-title">{{ domain.name }}</p>
</header>
<div class="card-content">
<div class="content">
<p><i class="fa fa-user mr-3" aria-hidden="true"></i>{{ users|length() }} users</p>
</div>
</div>
</div>
</div>
{% endif %}
<div>
<h2>Users on {{ domain.name }}</h2>
<ul>
{% for user in users %}
<li>{{ user.mail }}</li>
{% endfor %}
</ul>
</div>
{% if can_create_user %}
{% include 'partials/create_user_form.html' %}
{% endif %}
<div class="container is-widescreen mt-5">
<article class="message is-info">
<header class="message-header">
<p class="card-header-title">List of all users on this domain</p>
</header>
<div class="message-body">
<table class="table is-fullwidth is-striped">
<thead>
<tr>
<th>Username</th>
<th>Role</th>
</tr>
</thead>
<tbody>
{% for user in users %}
<tr>
<td>{{ user.username }}</td>
<td>{{ user.role }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</article>
</div>
</section>
</main>
{% endblock %}

38
templates/domains.html Normal file
View file

@ -0,0 +1,38 @@
{% extends 'app.html' %}
{% block main %}
<main>
<section class="section">
{% if can_create_domain %}
{% include 'partials/create_domain_form.html' %}
{% endif %}
<div class="container is-widescreen mt-5">
<article class="message is-info">
<header class="message-header">
{% if can_create_domain %}
<p class="card-header-title">List of all domains on this server</p>
{% else %}
<p class="card-header-title">List of your domains</p>
{% endif %}
</header>
<div class="message-body">
<table class="table is-fullwidth is-striped">
<thead>
<tr>
<th>Domain</th>
</tr>
</thead>
<tbody>
{% for domain in domains %}
<tr>
<td><a href="/domain/{{ domain.name }}">{{ domain.name }}</a></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</article>
</div>
</section>
</main>
{% endblock %}

View file

@ -1,38 +1,14 @@
{% extends 'base.html' %}
{% extends 'app.html' %}
{% block main %}
<main id="center">
<img src="/assets/img/logo.png" id="logo">
<p style="text-align: center;">You are logged in as {{ user.username }}</p>
<div id="login-line">
<a href="/logout" id="submit-login" value="Logout">Logout</a>
</div>
{% if can_create_domain %}
<div>
<h2>Create domain</h2>
<form action="/domain" method="POST">
<input type="text" name="domainname" placeholder="example.com">
<button type="submit" class="button is-info">Create</button>
</form>
</div>
{% endif %}
<div>
<h2>Active domains you can see</h2>
<ul>
{% for domain in domains %}
<li><a href="/domain/{{ domain.name }}">{{ domain.name }}</a></li>
{% endfor %}
</ul>
</div>
{% if other_users %}
<div>
<h2>Other users you have permission to see on your own domain</h2>
<ul>
{% for user in other_users %}
<li>{{ user.mail }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
<main>
<section class="section">
<h1 class="title has-text-centered">Welcome to llldap, {{ user.username }}</h1>
{% if can_create_domain %}
{% include 'partials/create_domain_form.html' %}
{% endif %}
{% if domains|length() > 1 %}
{% include 'partials/create_user_form.html' %}
{% endif %}
</section>
</main>
{% endblock %}

View file

@ -1,4 +1,7 @@
{% extends 'base.html' %}
{% block stylesheets %}
<link rel="stylesheet" href="/assets/css/login.css">
{% endblock stylesheets %}
{% block main %}
<main id="login-screen">
<img src="/assets/img/logo.png" id="logo">

View file

@ -0,0 +1,19 @@
<div class="container is-widescreen mt-5">
<article class="message is-info">
<div class="message-header">
<p><i class="fa fa-university mr-3" aria-hidden="true"></i>Create domain</p>
</div>
<div class="message-body">
<form action="/domain" method="POST">
<div class="field">
<label class="label">Domain name</label>
<div class="control">
<input name="domainname" class="input" type="text" placeholder="example.com">
</div>
</div>
<button type="submit" class="button is-info is-link">Create</button>
</form>
</div>
</article>
</div>

View file

@ -0,0 +1,51 @@
<div class="container is-widescreen mt-5">
<article class="message is-info">
<div class="message-header">
<p><i class="fa fa-user mr-3" aria-hidden="true"></i>Create user</p>
</div>
<div class="message-body">
<form action="/user" method="POST">
{% if domain %}
<input name="domain" type="hidden" value="{{ domain.name }}">
{% else %}
<div class="field is-horizontal">
<div class="field-label is-flex-grow-0 is-align-content-center">
<label class="label">Domain</label>
</div>
<div class="field-body">
<div class="field">
<div class="control">
<div class="select">
<select name="domain" autocomplete="{{ domains|map(attribute = 'name')|join(' ') }}">
{% for domain in domains %}
<option>{{ domain.name }}</option>
{% endfor %}
</select>
</div>
</div>
</div>
</div>
</div>
{% endif %}
<div class="field">
<div class="control has-icons-left">
<input name="username" class="input" type="text" placeholder="myuser">
<span class="icon is-small is-left">
<i class="fa fa-user"></i>
</span>
</div>
</div>
<div class="field">
<div class="control has-icons-left">
<input name="password" class="input" type="password" placeholder="password">
<span class="icon is-small is-left">
<i class="fa fa-lock"></i>
</span>
</div>
</div>
<button type="submit" class="button is-info is-link">Create</button>
</form>
</div>
</article>
</div>