feat(cli): exego user set-password — remplace le mot de passe d'un compteUnverified
Filet de récupération pour un mdp oublié (le mdp reste le seul moyen de gérer les passkeys). Même règles que l'inscription (8 caractères min, saisie confirmée au terminal ou --password), et comme le changement depuis la page Sécurité, les sessions ouvertes ne sont pas révoquées. La saisie de mot de passe de user add est factorisée (ask_password).
b6b617b parent: 8a09936 modified
src/cli.rs +30 -8 | @@ -103,14 +103,9 @@ fn choose(matches: Vec<TeamMatch>, input: &str) -> Result<TeamMatch> { | ||
| 103 | 103 | } |
| 104 | 104 | } |
| 105 | 105 | |
| 106 | -/// `exego user add <username> --team <nom>` — création de compte côté | |
| 107 | -/// serveur, toujours rattaché à une équipe (invariant : jamais sans équipe). | |
| 108 | -pub async fn user_add(username: &str, password_arg: Option<String>, team: &str) -> Result<()> { | |
| 109 | - let username = username.trim(); | |
| 110 | - if username.is_empty() { | |
| 111 | - bail!("le nom d'utilisateur ne peut pas être vide"); | |
| 112 | - } | |
| 113 | - | |
| 106 | +/// Prend `--password` ou demande au terminal (avec confirmation) — mêmes | |
| 107 | +/// règles de longueur que l'inscription. | |
| 108 | +fn ask_password(password_arg: Option<String>) -> Result<String> { | |
| 114 | 109 | let password = match password_arg { |
| 115 | 110 | Some(p) => p, |
| 116 | 111 | None => { |
| @@ -125,6 +120,17 @@ pub async fn user_add(username: &str, password_arg: Option<String>, team: &str) | ||
| 125 | 120 | if password.len() < 8 { |
| 126 | 121 | bail!("le mot de passe doit faire au moins 8 caractères"); |
| 127 | 122 | } |
| 123 | + Ok(password) | |
| 124 | +} | |
| 125 | + | |
| 126 | +/// `exego user add <username> --team <nom>` — création de compte côté | |
| 127 | +/// serveur, toujours rattaché à une équipe (invariant : jamais sans équipe). | |
| 128 | +pub async fn user_add(username: &str, password_arg: Option<String>, team: &str) -> Result<()> { | |
| 129 | + let username = username.trim(); | |
| 130 | + if username.is_empty() { | |
| 131 | + bail!("le nom d'utilisateur ne peut pas être vide"); | |
| 132 | + } | |
| 133 | + let password = ask_password(password_arg)?; | |
| 128 | 134 | |
| 129 | 135 | let pool = connect().await?; |
| 130 | 136 | let team = pick_team(&pool, team.trim()).await?; |
| @@ -159,6 +165,22 @@ pub async fn user_add(username: &str, password_arg: Option<String>, team: &str) | ||
| 159 | 165 | Ok(()) |
| 160 | 166 | } |
| 161 | 167 | |
| 168 | +/// `exego user set-password <username>` — remplace le mot de passe d'un | |
| 169 | +/// compte. Filet de récupération (mdp oublié) : comme le changement depuis | |
| 170 | +/// la page Sécurité, il ne révoque pas les sessions ouvertes. | |
| 171 | +pub async fn user_set_password(username: &str, password_arg: Option<String>) -> Result<()> { | |
| 172 | + let username = username.trim(); | |
| 173 | + let password = ask_password(password_arg)?; | |
| 174 | + let pool = connect().await?; | |
| 175 | + let user = users::find_by_username(&pool, username) | |
| 176 | + .await? | |
| 177 | + .with_context(|| format!("l'utilisateur « {username} » n'existe pas"))?; | |
| 178 | + let hash = password::hash(&password)?; | |
| 179 | + users::update_password(&pool, user.id, &hash).await?; | |
| 180 | + println!("Mot de passe de « {username} » mis à jour."); | |
| 181 | + Ok(()) | |
| 182 | +} | |
| 183 | + | |
| 162 | 184 | /// `exego user join <username> --team <nom>` — rattache un compte |
| 163 | 185 | /// existant à une équipe (équivalent CLI d'une invitation acceptée). |
| 164 | 186 | pub async fn user_join(username: &str, team: &str) -> Result<()> { |
| @@ -103,14 +103,9 @@ fn choose(matches: Vec<TeamMatch>, input: &str) -> Result<TeamMatch> { | |||
| 103 | } | 103 | } |
| 104 | } | 104 | } |
| 105 | 105 | ||
| 106 | -/// `exego user add <username> --team <nom>` — création de compte côté | 106 | +/// Prend `--password` ou demande au terminal (avec confirmation) — mêmes |
| 107 | -/// serveur, toujours rattaché à une équipe (invariant : jamais sans équipe). | 107 | +/// règles de longueur que l'inscription. |
| 108 | -pub async fn user_add(username: &str, password_arg: Option<String>, team: &str) -> Result<()> { | 108 | +fn ask_password(password_arg: Option<String>) -> Result<String> { |
| 109 | - let username = username.trim(); | ||
| 110 | - if username.is_empty() { | ||
| 111 | - bail!("le nom d'utilisateur ne peut pas être vide"); | ||
| 112 | - } | ||
| 113 | - | ||
| 114 | let password = match password_arg { | 109 | let password = match password_arg { |
| 115 | Some(p) => p, | 110 | Some(p) => p, |
| 116 | None => { | 111 | None => { |
| @@ -125,6 +120,17 @@ pub async fn user_add(username: &str, password_arg: Option<String>, team: &str) | |||
| 125 | if password.len() < 8 { | 120 | if password.len() < 8 { |
| 126 | bail!("le mot de passe doit faire au moins 8 caractères"); | 121 | bail!("le mot de passe doit faire au moins 8 caractères"); |
| 127 | } | 122 | } |
| 123 | + Ok(password) | ||
| 124 | +} | ||
| 125 | + | ||
| 126 | +/// `exego user add <username> --team <nom>` — création de compte côté | ||
| 127 | +/// serveur, toujours rattaché à une équipe (invariant : jamais sans équipe). | ||
| 128 | +pub async fn user_add(username: &str, password_arg: Option<String>, team: &str) -> Result<()> { | ||
| 129 | + let username = username.trim(); | ||
| 130 | + if username.is_empty() { | ||
| 131 | + bail!("le nom d'utilisateur ne peut pas être vide"); | ||
| 132 | + } | ||
| 133 | + let password = ask_password(password_arg)?; | ||
| 128 | 134 | ||
| 129 | let pool = connect().await?; | 135 | let pool = connect().await?; |
| 130 | let team = pick_team(&pool, team.trim()).await?; | 136 | let team = pick_team(&pool, team.trim()).await?; |
| @@ -159,6 +165,22 @@ pub async fn user_add(username: &str, password_arg: Option<String>, team: &str) | |||
| 159 | Ok(()) | 165 | Ok(()) |
| 160 | } | 166 | } |
| 161 | 167 | ||
| 168 | +/// `exego user set-password <username>` — remplace le mot de passe d'un | ||
| 169 | +/// compte. Filet de récupération (mdp oublié) : comme le changement depuis | ||
| 170 | +/// la page Sécurité, il ne révoque pas les sessions ouvertes. | ||
| 171 | +pub async fn user_set_password(username: &str, password_arg: Option<String>) -> Result<()> { | ||
| 172 | + let username = username.trim(); | ||
| 173 | + let password = ask_password(password_arg)?; | ||
| 174 | + let pool = connect().await?; | ||
| 175 | + let user = users::find_by_username(&pool, username) | ||
| 176 | + .await? | ||
| 177 | + .with_context(|| format!("l'utilisateur « {username} » n'existe pas"))?; | ||
| 178 | + let hash = password::hash(&password)?; | ||
| 179 | + users::update_password(&pool, user.id, &hash).await?; | ||
| 180 | + println!("Mot de passe de « {username} » mis à jour."); | ||
| 181 | + Ok(()) | ||
| 182 | +} | ||
| 183 | + | ||
| 162 | /// `exego user join <username> --team <nom>` — rattache un compte | 184 | /// `exego user join <username> --team <nom>` — rattache un compte |
| 163 | /// existant à une équipe (équivalent CLI d'une invitation acceptée). | 185 | /// existant à une équipe (équivalent CLI d'une invitation acceptée). |
| 164 | pub async fn user_join(username: &str, team: &str) -> Result<()> { | 186 | pub async fn user_join(username: &str, team: &str) -> Result<()> { |
modified
src/main.rs +9 -0 | @@ -55,6 +55,12 @@ enum UserCommand { | ||
| 55 | 55 | #[arg(long)] |
| 56 | 56 | team: String, |
| 57 | 57 | }, |
| 58 | + /// Remplace le mot de passe d'un utilisateur | |
| 59 | + SetPassword { | |
| 60 | + username: String, | |
| 61 | + #[arg(long)] | |
| 62 | + password: Option<String>, | |
| 63 | + }, | |
| 58 | 64 | /// Ajoute un utilisateur existant à une équipe |
| 59 | 65 | Join { |
| 60 | 66 | username: String, |
| @@ -86,6 +92,9 @@ async fn main() -> Result<()> { | ||
| 86 | 92 | team, |
| 87 | 93 | }, |
| 88 | 94 | }) => cli::user_add(&username, password, &team).await, |
| 95 | + Some(Command::User { | |
| 96 | + command: UserCommand::SetPassword { username, password }, | |
| 97 | + }) => cli::user_set_password(&username, password).await, | |
| 89 | 98 | Some(Command::User { |
| 90 | 99 | command: UserCommand::Join { username, team }, |
| 91 | 100 | }) => cli::user_join(&username, &team).await, |
| @@ -55,6 +55,12 @@ enum UserCommand { | |||
| 55 | #[arg(long)] | 55 | #[arg(long)] |
| 56 | team: String, | 56 | team: String, |
| 57 | }, | 57 | }, |
| 58 | + /// Remplace le mot de passe d'un utilisateur | ||
| 59 | + SetPassword { | ||
| 60 | + username: String, | ||
| 61 | + #[arg(long)] | ||
| 62 | + password: Option<String>, | ||
| 63 | + }, | ||
| 58 | /// Ajoute un utilisateur existant à une équipe | 64 | /// Ajoute un utilisateur existant à une équipe |
| 59 | Join { | 65 | Join { |
| 60 | username: String, | 66 | username: String, |
| @@ -86,6 +92,9 @@ async fn main() -> Result<()> { | |||
| 86 | team, | 92 | team, |
| 87 | }, | 93 | }, |
| 88 | }) => cli::user_add(&username, password, &team).await, | 94 | }) => cli::user_add(&username, password, &team).await, |
| 95 | + Some(Command::User { | ||
| 96 | + command: UserCommand::SetPassword { username, password }, | ||
| 97 | + }) => cli::user_set_password(&username, password).await, | ||
| 89 | Some(Command::User { | 98 | Some(Command::User { |
| 90 | command: UserCommand::Join { username, team }, | 99 | command: UserCommand::Join { username, team }, |
| 91 | }) => cli::user_join(&username, &team).await, | 100 | }) => cli::user_join(&username, &team).await, |