Compare commits
No commits in common. "09d782b6a51820a3c5b61e41b3e40ec2ccb87dfb" and "ee9db5ae36c2f0da3f8e3a71ba569d61cf5fe4a0" have entirely different histories.
09d782b6a5
...
ee9db5ae36
|
@ -7,16 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
|
|
||||||
## [Unreleased](https://git.rustybever.be/Chewing_Bever/otter)
|
## [Unreleased](https://git.rustybever.be/Chewing_Bever/otter)
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
* Ability for an account to be an admin
|
|
||||||
* CLI command to toggle admin status of users
|
|
||||||
* Admin user management page
|
|
||||||
|
|
||||||
## [0.3.0](https://git.rustybever.be/Chewing_Bever/otter/src/tag/0.3.0)
|
## [0.3.0](https://git.rustybever.be/Chewing_Bever/otter/src/tag/0.3.0)
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
* Public sign-up page (disabled by default)
|
* Public sign-up page (disabled by default)
|
||||||
* Private sign-up links
|
* Private sign-up links
|
||||||
* New CLI commands
|
* New CLI commands
|
||||||
|
|
|
@ -14,8 +14,4 @@ impl<'a> AdminRepository<'a> {
|
||||||
) -> Result<Vec<models::User>, AuthErr> {
|
) -> Result<Vec<models::User>, AuthErr> {
|
||||||
self.store.paginated_users(page, filter)
|
self.store.paginated_users(page, filter)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn remove_user(&self, id: i64) -> Result<bool, AuthErr> {
|
|
||||||
self.store.remove_user(id)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -83,10 +83,6 @@ impl GpodderRepository {
|
||||||
self.store.insert_user(username, &password_hash)
|
self.store.insert_user(username, &password_hash)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update_user(&self, user: models::User) -> Result<models::User, AuthErr> {
|
|
||||||
self.store.update_user(user)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn validate_credentials(
|
pub fn validate_credentials(
|
||||||
&self,
|
&self,
|
||||||
username: &str,
|
username: &str,
|
||||||
|
|
|
@ -51,12 +51,6 @@ pub trait GpodderAuthStore {
|
||||||
/// Insert a new user into the data store
|
/// Insert a new user into the data store
|
||||||
fn insert_user(&self, username: &str, password_hash: &str) -> Result<User, AuthErr>;
|
fn insert_user(&self, username: &str, password_hash: &str) -> Result<User, AuthErr>;
|
||||||
|
|
||||||
/// Update the user with the included ID with the new values
|
|
||||||
fn update_user(&self, user: User) -> Result<User, AuthErr>;
|
|
||||||
|
|
||||||
/// Remove the user with the given ID
|
|
||||||
fn remove_user(&self, id: i64) -> Result<bool, AuthErr>;
|
|
||||||
|
|
||||||
/// Create a new session for a user with the given session ID
|
/// Create a new session for a user with the given session ID
|
||||||
///
|
///
|
||||||
/// The `last_seen` timestamp's precision should be at least accurate to the second
|
/// The `last_seen` timestamp's precision should be at least accurate to the second
|
||||||
|
|
|
@ -2,7 +2,7 @@ use diesel::prelude::*;
|
||||||
|
|
||||||
use crate::schema::*;
|
use crate::schema::*;
|
||||||
|
|
||||||
#[derive(Clone, Queryable, Selectable, AsChangeset)]
|
#[derive(Clone, Queryable, Selectable)]
|
||||||
#[diesel(table_name = users)]
|
#[diesel(table_name = users)]
|
||||||
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
|
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
|
||||||
pub struct User {
|
pub struct User {
|
||||||
|
|
|
@ -24,17 +24,6 @@ impl From<User> for gpodder::User {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<gpodder::User> for User {
|
|
||||||
fn from(value: gpodder::User) -> Self {
|
|
||||||
Self {
|
|
||||||
id: value.id,
|
|
||||||
username: value.username,
|
|
||||||
password_hash: value.password_hash,
|
|
||||||
admin: value.admin,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<SignupLink> for gpodder::SignupLink {
|
impl From<SignupLink> for gpodder::SignupLink {
|
||||||
fn from(value: SignupLink) -> Self {
|
fn from(value: SignupLink) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
@ -69,28 +58,6 @@ impl gpodder::GpodderAuthStore for SqliteRepository {
|
||||||
.map_err(DbError::from)?)
|
.map_err(DbError::from)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_user(&self, user: gpodder::User) -> Result<gpodder::User, AuthErr> {
|
|
||||||
let conn = &mut self.pool.get().map_err(DbError::from)?;
|
|
||||||
let user: User = user.into();
|
|
||||||
|
|
||||||
Ok(diesel::update(users::table.filter(users::id.eq(user.id)))
|
|
||||||
.set(&user)
|
|
||||||
.returning(User::as_returning())
|
|
||||||
.get_result(conn)
|
|
||||||
.map(gpodder::User::from)
|
|
||||||
.map_err(DbError::from)?)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn remove_user(&self, id: i64) -> Result<bool, AuthErr> {
|
|
||||||
let conn = &mut self.pool.get().map_err(DbError::from)?;
|
|
||||||
|
|
||||||
match diesel::delete(users::table.filter(users::id.eq(id))).execute(conn) {
|
|
||||||
Ok(0) => Ok(false),
|
|
||||||
Ok(_) => Ok(true),
|
|
||||||
Err(err) => Err(DbError::from(err).into()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_session(&self, session_id: i64) -> Result<Option<gpodder::models::Session>, AuthErr> {
|
fn get_session(&self, session_id: i64) -> Result<Option<gpodder::models::Session>, AuthErr> {
|
||||||
match sessions::table
|
match sessions::table
|
||||||
.inner_join(users::table)
|
.inner_join(users::table)
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use clap::{ArgAction, Args, Subcommand};
|
use clap::{Args, Subcommand};
|
||||||
|
|
||||||
use super::CliError;
|
use super::CliError;
|
||||||
|
|
||||||
|
@ -22,12 +22,6 @@ pub enum UserCommand {
|
||||||
Add { username: String, password: String },
|
Add { username: String, password: String },
|
||||||
/// Generate a signup link ID
|
/// Generate a signup link ID
|
||||||
GenerateSignupLink,
|
GenerateSignupLink,
|
||||||
/// Give or remove admin privileges to a user
|
|
||||||
SetAdmin {
|
|
||||||
username: String,
|
|
||||||
#[clap(action=ArgAction::Set)]
|
|
||||||
is_admin: bool,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Command {
|
impl Command {
|
||||||
|
@ -73,11 +67,6 @@ impl UserCommand {
|
||||||
|
|
||||||
println!("/signup/{}", link.id);
|
println!("/signup/{}", link.id);
|
||||||
}
|
}
|
||||||
Self::SetAdmin { username, is_admin } => {
|
|
||||||
let mut user = store.get_user(username)?;
|
|
||||||
user.admin = *is_admin;
|
|
||||||
store.update_user(user)?;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
@ -73,12 +73,10 @@ async fn get_index(
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
jar: CookieJar,
|
jar: CookieJar,
|
||||||
) -> AppResult<TemplateResponse<Page<View>>> {
|
) -> AppResult<TemplateResponse<Page<View>>> {
|
||||||
let user = auth::extract_session(ctx.clone(), &jar)
|
let authenticated = auth::extract_session(ctx.clone(), &jar).await?.is_some();
|
||||||
.await?
|
|
||||||
.map(|session| session.user);
|
|
||||||
|
|
||||||
Ok(View::Index
|
Ok(View::Index
|
||||||
.page(&headers)
|
.page(&headers)
|
||||||
.user(user.as_ref())
|
.authenticated(authenticated)
|
||||||
.response(&ctx.tera))
|
.response(&ctx.tera))
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,8 +30,6 @@ pub async fn get_sessions(
|
||||||
Query(page): Query<super::Pagination>,
|
Query(page): Query<super::Pagination>,
|
||||||
) -> AppResult<TemplateResponse<Page<View>>> {
|
) -> AppResult<TemplateResponse<Page<View>>> {
|
||||||
let next_page = page.next_page();
|
let next_page = page.next_page();
|
||||||
let admin = session.user.admin;
|
|
||||||
|
|
||||||
let sessions = tokio::task::spawn_blocking(move || {
|
let sessions = tokio::task::spawn_blocking(move || {
|
||||||
ctx.store
|
ctx.store
|
||||||
.user(&session.user)
|
.user(&session.user)
|
||||||
|
@ -46,7 +44,7 @@ pub async fn get_sessions(
|
||||||
Ok(View::Sessions(sessions, session.id, next_page_query)
|
Ok(View::Sessions(sessions, session.id, next_page_query)
|
||||||
.page(&headers)
|
.page(&headers)
|
||||||
.headers(&headers)
|
.headers(&headers)
|
||||||
.authenticated(true, admin)
|
.authenticated(true)
|
||||||
.response(&ctx.tera))
|
.response(&ctx.tera))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,6 @@ use crate::{
|
||||||
pub fn router(ctx: Context) -> Router<Context> {
|
pub fn router(ctx: Context) -> Router<Context> {
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/users", get(get_users))
|
.route("/users", get(get_users))
|
||||||
.route("/users/{id}", delete(delete_user))
|
|
||||||
.route_layer(axum::middleware::from_fn_with_state(
|
.route_layer(axum::middleware::from_fn_with_state(
|
||||||
ctx.clone(),
|
ctx.clone(),
|
||||||
super::auth::auth_web_middleware,
|
super::auth::auth_web_middleware,
|
||||||
|
@ -68,23 +67,6 @@ async fn get_users(
|
||||||
Ok(View::Users(users, user_id, next_page_query)
|
Ok(View::Users(users, user_id, next_page_query)
|
||||||
.page(&headers)
|
.page(&headers)
|
||||||
.headers(&headers)
|
.headers(&headers)
|
||||||
.authenticated(true, true)
|
.authenticated(true)
|
||||||
.response(&ctx.tera))
|
.response(&ctx.tera))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn delete_user(
|
|
||||||
State(ctx): State<Context>,
|
|
||||||
Extension(session): Extension<gpodder::Session>,
|
|
||||||
Path(id): Path<i64>,
|
|
||||||
) -> AppResult<()> {
|
|
||||||
let deleted =
|
|
||||||
tokio::task::spawn_blocking(move || ctx.store.admin(&session.user)?.remove_user(id))
|
|
||||||
.await
|
|
||||||
.unwrap()?;
|
|
||||||
|
|
||||||
if deleted {
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
Err(AppError::NotFound)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -11,7 +11,6 @@ pub struct Page<T> {
|
||||||
template: T,
|
template: T,
|
||||||
wrap_with_base: bool,
|
wrap_with_base: bool,
|
||||||
authenticated: bool,
|
authenticated: bool,
|
||||||
admin: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Template> Template for Page<T> {
|
impl<T: Template> Template for Page<T> {
|
||||||
|
@ -26,7 +25,6 @@ impl<T: Template> Template for Page<T> {
|
||||||
let mut ctx = tera::Context::new();
|
let mut ctx = tera::Context::new();
|
||||||
ctx.insert("inner", &inner);
|
ctx.insert("inner", &inner);
|
||||||
ctx.insert("authenticated", &self.authenticated);
|
ctx.insert("authenticated", &self.authenticated);
|
||||||
ctx.insert("admin", &self.admin);
|
|
||||||
|
|
||||||
tera.render(super::BASE_TEMPLATE, &ctx)
|
tera.render(super::BASE_TEMPLATE, &ctx)
|
||||||
} else {
|
} else {
|
||||||
|
@ -41,7 +39,6 @@ impl<T> Page<T> {
|
||||||
template,
|
template,
|
||||||
wrap_with_base: false,
|
wrap_with_base: false,
|
||||||
authenticated: false,
|
authenticated: false,
|
||||||
admin: false,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -57,23 +54,8 @@ impl<T> Page<T> {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the view's authentication level
|
pub fn authenticated(mut self, authenticated: bool) -> Self {
|
||||||
pub fn authenticated(mut self, authenticated: bool, admin: bool) -> Self {
|
|
||||||
self.authenticated = authenticated;
|
self.authenticated = authenticated;
|
||||||
self.admin = admin;
|
|
||||||
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Utility function to derive authentication level from a given user
|
|
||||||
pub fn user(mut self, user: Option<&gpodder::User>) -> Self {
|
|
||||||
if let Some(user) = user {
|
|
||||||
self.authenticated = true;
|
|
||||||
self.admin = user.admin;
|
|
||||||
} else {
|
|
||||||
self.authenticated = false;
|
|
||||||
self.admin = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,9 +23,6 @@ a:hover {
|
||||||
<ul>
|
<ul>
|
||||||
{% if authenticated %}
|
{% if authenticated %}
|
||||||
<li><a hx-get="/sessions" hx-target="#inner" hx-push-url="true">Sessions</a></li>
|
<li><a hx-get="/sessions" hx-target="#inner" hx-push-url="true">Sessions</a></li>
|
||||||
{% if admin %}
|
|
||||||
<li><a hx-get="/users" hx-target="#inner" hx-push-url="true">Users</a></li>
|
|
||||||
{% endif %}
|
|
||||||
<li><a hx-post="/logout" hx-target="#inner">Logout</a></li>
|
<li><a hx-post="/logout" hx-target="#inner">Logout</a></li>
|
||||||
{% else %}
|
{% else %}
|
||||||
<li><a hx-get="/login" hx-target="#inner" hx-push-url="true">Login</a></li>
|
<li><a hx-get="/login" hx-target="#inner" hx-push-url="true">Login</a></li>
|
||||||
|
|
|
@ -12,18 +12,12 @@
|
||||||
<table id="users">
|
<table id="users">
|
||||||
<thead>
|
<thead>
|
||||||
<th>Username</th>
|
<th>Username</th>
|
||||||
<th>Privileges</th>
|
|
||||||
<th>Action</th>
|
<th>Action</th>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{%- for user in users %}
|
{%- for user in users %}
|
||||||
<tr>
|
<tr>
|
||||||
<th>{{ user.username }}</th>
|
<th>{{ user.username }}</th>
|
||||||
<th>{%- if user.admin -%}
|
|
||||||
Admin
|
|
||||||
{%- else -%}
|
|
||||||
User
|
|
||||||
{%- endif -%}
|
|
||||||
<th>
|
<th>
|
||||||
{%- if user.id != current_user_id -%}
|
{%- if user.id != current_user_id -%}
|
||||||
<a hx-delete="/users/{{ user.id }}"
|
<a hx-delete="/users/{{ user.id }}"
|
||||||
|
|
|
@ -28,7 +28,6 @@ struct Session {
|
||||||
struct User {
|
struct User {
|
||||||
id: i64,
|
id: i64,
|
||||||
username: String,
|
username: String,
|
||||||
admin: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Template for View {
|
impl Template for View {
|
||||||
|
@ -104,7 +103,6 @@ impl From<gpodder::User> for User {
|
||||||
Self {
|
Self {
|
||||||
id: value.id,
|
id: value.id,
|
||||||
username: value.username,
|
username: value.username,
|
||||||
admin: value.admin,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue