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)
 | 
			
		||||
 | 
			
		||||
### 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)
 | 
			
		||||
 | 
			
		||||
### Added
 | 
			
		||||
 | 
			
		||||
* Public sign-up page (disabled by default)
 | 
			
		||||
* Private sign-up links
 | 
			
		||||
* New CLI commands
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -14,8 +14,4 @@ impl<'a> AdminRepository<'a> {
 | 
			
		|||
    ) -> Result<Vec<models::User>, AuthErr> {
 | 
			
		||||
        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)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn update_user(&self, user: models::User) -> Result<models::User, AuthErr> {
 | 
			
		||||
        self.store.update_user(user)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn validate_credentials(
 | 
			
		||||
        &self,
 | 
			
		||||
        username: &str,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -51,12 +51,6 @@ pub trait GpodderAuthStore {
 | 
			
		|||
    /// Insert a new user into the data store
 | 
			
		||||
    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
 | 
			
		||||
    ///
 | 
			
		||||
    /// The `last_seen` timestamp's precision should be at least accurate to the second
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,7 +2,7 @@ use diesel::prelude::*;
 | 
			
		|||
 | 
			
		||||
use crate::schema::*;
 | 
			
		||||
 | 
			
		||||
#[derive(Clone, Queryable, Selectable, AsChangeset)]
 | 
			
		||||
#[derive(Clone, Queryable, Selectable)]
 | 
			
		||||
#[diesel(table_name = users)]
 | 
			
		||||
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
 | 
			
		||||
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 {
 | 
			
		||||
    fn from(value: SignupLink) -> Self {
 | 
			
		||||
        Self {
 | 
			
		||||
| 
						 | 
				
			
			@ -69,28 +58,6 @@ impl gpodder::GpodderAuthStore for SqliteRepository {
 | 
			
		|||
            .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> {
 | 
			
		||||
        match sessions::table
 | 
			
		||||
            .inner_join(users::table)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,4 +1,4 @@
 | 
			
		|||
use clap::{ArgAction, Args, Subcommand};
 | 
			
		||||
use clap::{Args, Subcommand};
 | 
			
		||||
 | 
			
		||||
use super::CliError;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -22,12 +22,6 @@ pub enum UserCommand {
 | 
			
		|||
    Add { username: String, password: String },
 | 
			
		||||
    /// Generate a signup link ID
 | 
			
		||||
    GenerateSignupLink,
 | 
			
		||||
    /// Give or remove admin privileges to a user
 | 
			
		||||
    SetAdmin {
 | 
			
		||||
        username: String,
 | 
			
		||||
        #[clap(action=ArgAction::Set)]
 | 
			
		||||
        is_admin: bool,
 | 
			
		||||
    },
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Command {
 | 
			
		||||
| 
						 | 
				
			
			@ -73,11 +67,6 @@ impl UserCommand {
 | 
			
		|||
 | 
			
		||||
                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(())
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -73,12 +73,10 @@ async fn get_index(
 | 
			
		|||
    headers: HeaderMap,
 | 
			
		||||
    jar: CookieJar,
 | 
			
		||||
) -> AppResult<TemplateResponse<Page<View>>> {
 | 
			
		||||
    let user = auth::extract_session(ctx.clone(), &jar)
 | 
			
		||||
        .await?
 | 
			
		||||
        .map(|session| session.user);
 | 
			
		||||
    let authenticated = auth::extract_session(ctx.clone(), &jar).await?.is_some();
 | 
			
		||||
 | 
			
		||||
    Ok(View::Index
 | 
			
		||||
        .page(&headers)
 | 
			
		||||
        .user(user.as_ref())
 | 
			
		||||
        .authenticated(authenticated)
 | 
			
		||||
        .response(&ctx.tera))
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -30,8 +30,6 @@ pub async fn get_sessions(
 | 
			
		|||
    Query(page): Query<super::Pagination>,
 | 
			
		||||
) -> AppResult<TemplateResponse<Page<View>>> {
 | 
			
		||||
    let next_page = page.next_page();
 | 
			
		||||
    let admin = session.user.admin;
 | 
			
		||||
 | 
			
		||||
    let sessions = tokio::task::spawn_blocking(move || {
 | 
			
		||||
        ctx.store
 | 
			
		||||
            .user(&session.user)
 | 
			
		||||
| 
						 | 
				
			
			@ -46,7 +44,7 @@ pub async fn get_sessions(
 | 
			
		|||
    Ok(View::Sessions(sessions, session.id, next_page_query)
 | 
			
		||||
        .page(&headers)
 | 
			
		||||
        .headers(&headers)
 | 
			
		||||
        .authenticated(true, admin)
 | 
			
		||||
        .authenticated(true)
 | 
			
		||||
        .response(&ctx.tera))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -17,7 +17,6 @@ use crate::{
 | 
			
		|||
pub fn router(ctx: Context) -> Router<Context> {
 | 
			
		||||
    Router::new()
 | 
			
		||||
        .route("/users", get(get_users))
 | 
			
		||||
        .route("/users/{id}", delete(delete_user))
 | 
			
		||||
        .route_layer(axum::middleware::from_fn_with_state(
 | 
			
		||||
            ctx.clone(),
 | 
			
		||||
            super::auth::auth_web_middleware,
 | 
			
		||||
| 
						 | 
				
			
			@ -68,23 +67,6 @@ async fn get_users(
 | 
			
		|||
    Ok(View::Users(users, user_id, next_page_query)
 | 
			
		||||
        .page(&headers)
 | 
			
		||||
        .headers(&headers)
 | 
			
		||||
        .authenticated(true, true)
 | 
			
		||||
        .authenticated(true)
 | 
			
		||||
        .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,
 | 
			
		||||
    wrap_with_base: bool,
 | 
			
		||||
    authenticated: bool,
 | 
			
		||||
    admin: bool,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl<T: Template> Template for Page<T> {
 | 
			
		||||
| 
						 | 
				
			
			@ -26,7 +25,6 @@ impl<T: Template> Template for Page<T> {
 | 
			
		|||
            let mut ctx = tera::Context::new();
 | 
			
		||||
            ctx.insert("inner", &inner);
 | 
			
		||||
            ctx.insert("authenticated", &self.authenticated);
 | 
			
		||||
            ctx.insert("admin", &self.admin);
 | 
			
		||||
 | 
			
		||||
            tera.render(super::BASE_TEMPLATE, &ctx)
 | 
			
		||||
        } else {
 | 
			
		||||
| 
						 | 
				
			
			@ -41,7 +39,6 @@ impl<T> Page<T> {
 | 
			
		|||
            template,
 | 
			
		||||
            wrap_with_base: false,
 | 
			
		||||
            authenticated: false,
 | 
			
		||||
            admin: false,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -57,23 +54,8 @@ impl<T> Page<T> {
 | 
			
		|||
        self
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Set the view's authentication level
 | 
			
		||||
    pub fn authenticated(mut self, authenticated: bool, admin: bool) -> Self {
 | 
			
		||||
    pub fn authenticated(mut self, authenticated: bool) -> Self {
 | 
			
		||||
        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
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -23,9 +23,6 @@ a:hover {
 | 
			
		|||
                <ul>
 | 
			
		||||
                    {% if authenticated %}
 | 
			
		||||
                    <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>
 | 
			
		||||
                    {% else %}
 | 
			
		||||
                    <li><a hx-get="/login" hx-target="#inner" hx-push-url="true">Login</a></li>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -12,18 +12,12 @@
 | 
			
		|||
<table id="users">
 | 
			
		||||
    <thead>
 | 
			
		||||
        <th>Username</th>
 | 
			
		||||
        <th>Privileges</th>
 | 
			
		||||
        <th>Action</th>
 | 
			
		||||
    </thead>
 | 
			
		||||
    <tbody>
 | 
			
		||||
        {%- for user in users %}
 | 
			
		||||
        <tr>
 | 
			
		||||
            <th>{{ user.username }}</th>
 | 
			
		||||
            <th>{%- if user.admin -%}
 | 
			
		||||
                Admin
 | 
			
		||||
                {%- else -%}
 | 
			
		||||
                User
 | 
			
		||||
                {%- endif -%}
 | 
			
		||||
            <th>
 | 
			
		||||
                {%- if user.id != current_user_id -%}
 | 
			
		||||
                <a hx-delete="/users/{{ user.id }}"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -28,7 +28,6 @@ struct Session {
 | 
			
		|||
struct User {
 | 
			
		||||
    id: i64,
 | 
			
		||||
    username: String,
 | 
			
		||||
    admin: bool,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Template for View {
 | 
			
		||||
| 
						 | 
				
			
			@ -104,7 +103,6 @@ impl From<gpodder::User> for User {
 | 
			
		|||
        Self {
 | 
			
		||||
            id: value.id,
 | 
			
		||||
            username: value.username,
 | 
			
		||||
            admin: value.admin,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue