Skip to content

Commit 0158c53

Browse files
committed
feat: refresh token on failure
1 parent 6cec7ca commit 0158c53

4 files changed

Lines changed: 172 additions & 36 deletions

File tree

lib/error.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,12 @@ pub enum Error {
5050
Url(String),
5151
}
5252

53+
impl Error {
54+
pub fn is_refreshable_auth_error(&self) -> bool {
55+
matches!(self, Error::Api { status, .. } if status.as_u16() == 401)
56+
}
57+
}
58+
5359
impl From<url::ParseError> for Error {
5460
fn from(value: url::ParseError) -> Self {
5561
Self::Url(value.to_string())

src/app.rs

Lines changed: 54 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,66 @@
1+
use crate::Command;
12
use crate::config::Config;
2-
use puddle::RaindropClient;
3+
use puddle::{Error, RaindropClient};
34

45
pub(crate) struct CliApp {
6+
pub(crate) config: Config,
57
pub(crate) client: RaindropClient,
68
}
79

810
impl CliApp {
911
pub(crate) fn new() -> Result<Self, Box<dyn std::error::Error>> {
1012
let config = Config::load()?;
13+
let client = RaindropClient::builder()
14+
.access_token(config.values().access_token.clone())
15+
.build()?;
1116

12-
Ok(Self {
13-
client: RaindropClient::new(config.access_token)?,
14-
})
17+
Ok(Self { config, client })
18+
}
19+
20+
pub(crate) async fn run_command(
21+
&mut self,
22+
command: Command,
23+
) -> Result<(), Box<dyn std::error::Error>> {
24+
let mut attempted = false;
25+
26+
loop {
27+
let result = match command.clone() {
28+
Command::List(args) => self.list(args).await,
29+
Command::Get(args) => self.get(args).await,
30+
Command::Create(args) => self.create(args).await,
31+
Command::Update(args) => self.update(args).await,
32+
Command::Delete(args) => self.delete(args).await,
33+
Command::UploadFile(args) => self.upload_file(args).await,
34+
Command::UploadCover(args) => self.upload_cover(args).await,
35+
Command::CreateMany(args) => self.create_many(args).await,
36+
Command::UpdateMany(args) => self.update_many(args).await,
37+
Command::DeleteMany(args) => self.delete_many(args).await,
38+
Command::Export(args) => self.export(args).await,
39+
Command::Collections { command } => self.run_collections(command).await,
40+
Command::Tags { command } => self.run_tags(command).await,
41+
Command::Me => self.user_me().await,
42+
Command::Filters { command } => self.run_filters(command).await,
43+
Command::Setup | Command::Config => unreachable!(),
44+
};
45+
46+
match result {
47+
Ok(()) => return Ok(()),
48+
Err(err)
49+
if !attempted
50+
&& err
51+
.as_ref()
52+
.downcast_ref::<Error>()
53+
.is_some_and(Error::is_refreshable_auth_error) =>
54+
{
55+
self.config = self.config.clone().refresh_access_token().await?;
56+
self.client = RaindropClient::builder()
57+
.access_token(self.config.values().access_token.clone())
58+
.build()?;
59+
60+
attempted = true;
61+
}
62+
Err(err) => return Err(err),
63+
}
64+
}
1565
}
1666
}

src/config.rs

Lines changed: 108 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,32 +2,116 @@ use crate::constants::{
22
ENV_ACCESS_TOKEN, ENV_CLIENT_ID, ENV_CLIENT_SECRET, ENV_REDIRECT_URI, ENV_REFRESH_TOKEN,
33
TOML_ACCESS_TOKEN, TOML_CLIENT_ID, TOML_CLIENT_SECRET, TOML_REDIRECT_URI, TOML_REFRESH_TOKEN,
44
};
5+
use puddle::auth::oauth;
56
use std::env;
67
use std::fs;
78
use std::io;
89
use std::path::{Path, PathBuf};
910
use toml::Value;
1011

11-
#[derive(Debug)]
12-
pub struct Config {
12+
#[derive(Debug, Clone, PartialEq, Eq)]
13+
pub(crate) struct ConfigValues {
1314
pub client_id: String,
1415
pub client_secret: String,
1516
pub redirect_uri: String,
1617
pub access_token: String,
1718
pub refresh_token: String,
1819
}
1920

21+
#[derive(Debug, Clone, PartialEq, Eq)]
22+
pub(crate) enum Config {
23+
Env(ConfigValues),
24+
File { path: PathBuf, values: ConfigValues },
25+
}
26+
2027
impl Config {
21-
pub fn load() -> io::Result<Self> {
22-
if let Some(config) = Self::from_env()? {
23-
return Ok(config);
28+
pub(crate) fn load() -> io::Result<Self> {
29+
if let Some(values) = ConfigValues::from_env()? {
30+
return Ok(Self::Env(values));
31+
}
32+
33+
let path = global_config_path()?;
34+
let values = ConfigValues::from_path(&path)?;
35+
36+
Ok(Self::File { path, values })
37+
}
38+
39+
pub(crate) fn values(&self) -> &ConfigValues {
40+
match self {
41+
Self::Env(values) => values,
42+
Self::File { values, .. } => values,
43+
}
44+
}
45+
46+
pub(crate) fn with_refreshed_tokens(
47+
&self,
48+
access_token: String,
49+
refresh_token: Option<String>,
50+
) -> Self {
51+
match self {
52+
Self::Env(values) => {
53+
Self::Env(values.with_refreshed_tokens(access_token, refresh_token))
54+
}
55+
Self::File { path, values } => Self::File {
56+
path: path.clone(),
57+
values: values.with_refreshed_tokens(access_token, refresh_token),
58+
},
59+
}
60+
}
61+
62+
pub(crate) fn persist(&self) -> io::Result<()> {
63+
if let Self::File { path, values } = self {
64+
let mut table = toml::Table::new();
65+
66+
table.insert(
67+
TOML_CLIENT_ID.to_string(),
68+
Value::String(values.client_id.clone()),
69+
);
70+
table.insert(
71+
TOML_CLIENT_SECRET.to_string(),
72+
Value::String(values.client_secret.clone()),
73+
);
74+
table.insert(
75+
TOML_REDIRECT_URI.to_string(),
76+
Value::String(values.redirect_uri.clone()),
77+
);
78+
table.insert(
79+
TOML_ACCESS_TOKEN.to_string(),
80+
Value::String(values.access_token.clone()),
81+
);
82+
table.insert(
83+
TOML_REFRESH_TOKEN.to_string(),
84+
Value::String(values.refresh_token.clone()),
85+
);
86+
87+
let content = toml::to_string_pretty(&table).map_err(|e| {
88+
io::Error::other(format!("failed to serialize global config toml: {e}"))
89+
})?;
90+
fs::write(path, format!("{content}\n"))?;
2491
}
2592

26-
let config_path = global_config_path()?;
27-
Self::from_path(&config_path)
93+
Ok(())
94+
}
95+
96+
pub(crate) async fn refresh_access_token(self) -> Result<Self, Box<dyn std::error::Error>> {
97+
let values = self.values();
98+
let token = oauth::TokenRequestBuilder::refresh(
99+
&values.client_id,
100+
&values.client_secret,
101+
&values.refresh_token,
102+
)
103+
.send()
104+
.await?;
105+
106+
let config = self.with_refreshed_tokens(token.access_token, token.refresh_token);
107+
config.persist()?;
108+
109+
Ok(config)
28110
}
111+
}
29112

30-
pub fn from_path(path: impl AsRef<Path>) -> io::Result<Self> {
113+
impl ConfigValues {
114+
pub(crate) fn from_path(path: impl AsRef<Path>) -> io::Result<Self> {
31115
let path = path.as_ref();
32116
let content = fs::read_to_string(path).map_err(|err| {
33117
io::Error::new(
@@ -51,10 +135,10 @@ impl Config {
51135
})
52136
}
53137

54-
pub fn write_to_global_path(&self) -> io::Result<()> {
138+
pub(crate) fn write_to_global_path(&self) -> io::Result<()> {
55139
let path = global_config_path()?;
56-
57140
let mut table = toml::Table::new();
141+
58142
table.insert(
59143
TOML_CLIENT_ID.to_string(),
60144
Value::String(self.client_id.clone()),
@@ -84,6 +168,20 @@ impl Config {
84168
Ok(())
85169
}
86170

171+
pub(crate) fn with_refreshed_tokens(
172+
&self,
173+
access_token: String,
174+
refresh_token: Option<String>,
175+
) -> Self {
176+
Self {
177+
client_id: self.client_id.clone(),
178+
client_secret: self.client_secret.clone(),
179+
redirect_uri: self.redirect_uri.clone(),
180+
access_token,
181+
refresh_token: refresh_token.unwrap_or_else(|| self.refresh_token.clone()),
182+
}
183+
}
184+
87185
fn from_env() -> io::Result<Option<Self>> {
88186
let entries = [
89187
ENV_CLIENT_ID,

src/main.rs

Lines changed: 4 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ mod tags;
99
mod user;
1010

1111
use crate::app::CliApp;
12-
use crate::config::{Config, global_config_path};
12+
use crate::config::{ConfigValues, global_config_path};
1313
use crate::constants::{
1414
AUTHORIZE_URL_BASE, DEFAULT_OAUTH_DEBUG_REDIRECT_URI, RAINDROP_INTEGRATIONS_URI,
1515
};
@@ -94,26 +94,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
9494
println!("{}", path.display());
9595
}
9696
command => {
97-
let app = CliApp::new()?;
98-
99-
match command {
100-
Command::List(args) => app.list(args).await?,
101-
Command::Get(args) => app.get(args).await?,
102-
Command::Create(args) => app.create(args).await?,
103-
Command::Update(args) => app.update(args).await?,
104-
Command::Delete(args) => app.delete(args).await?,
105-
Command::UploadFile(args) => app.upload_file(args).await?,
106-
Command::UploadCover(args) => app.upload_cover(args).await?,
107-
Command::CreateMany(args) => app.create_many(args).await?,
108-
Command::UpdateMany(args) => app.update_many(args).await?,
109-
Command::DeleteMany(args) => app.delete_many(args).await?,
110-
Command::Export(args) => app.export(args).await?,
111-
Command::Collections { command } => app.run_collections(command).await?,
112-
Command::Tags { command } => app.run_tags(command).await?,
113-
Command::Me => app.user_me().await?,
114-
Command::Filters { command } => app.run_filters(command).await?,
115-
Command::Setup | Command::Config => unreachable!(),
116-
}
97+
let mut app = CliApp::new()?;
98+
app.run_command(command).await?;
11799
}
118100
}
119101

@@ -149,7 +131,7 @@ async fn run_setup() -> Result<(), Box<dyn std::error::Error>> {
149131
.refresh_token
150132
.ok_or_else(|| io::Error::other("missing refresh token in OAuth response"))?;
151133

152-
let config = Config {
134+
let config = ConfigValues {
153135
client_id,
154136
client_secret,
155137
redirect_uri,

0 commit comments

Comments
 (0)