Compare commits

..

No commits in common. "09d782b6a51820a3c5b61e41b3e40ec2ccb87dfb" and "ee9db5ae36c2f0da3f8e3a71ba569d61cf5fe4a0" have entirely different histories.

14 changed files with 7 additions and 124 deletions

View File

@ -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

View File

@ -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)
}
} }

View File

@ -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,

View File

@ -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

View File

@ -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 {

View File

@ -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)

View File

@ -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(())

View File

@ -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))
} }

View File

@ -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))
} }

View File

@ -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)
}
}

View File

@ -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
} }

View File

@ -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>

View File

@ -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 }}"

View File

@ -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,
} }
} }
} }