Skip to content

Commit

Permalink
feat(term): support google login
Browse files Browse the repository at this point in the history
  • Loading branch information
ymgyt committed Mar 17, 2024
1 parent c7c81fd commit a55c310
Show file tree
Hide file tree
Showing 16 changed files with 405 additions and 164 deletions.
2 changes: 1 addition & 1 deletion crates/synd_term/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ synd-feed = { path = "../synd_feed", version = "0.1.5" }
synd-o11y = { path = "../synd_o11y", version = "0.1.4" }

anyhow = { workspace = true }
chrono = { workspace = true, features = ["std"] }
chrono = { workspace = true, features = ["std", "now"] }
clap = { workspace = true, features = ["derive", "string", "color", "suggestions", "wrap_help", "env", "std"] }
crossterm = { version = "0.27.0", features = ["event-stream"] }
directories = "5.0.1"
Expand Down
46 changes: 46 additions & 0 deletions crates/synd_term/src/application/authenticator.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
use synd_auth::{
device_flow::{provider, DeviceFlow},
jwt,
};

pub struct DeviceFlows {
pub github: DeviceFlow<provider::Github>,
pub google: DeviceFlow<provider::Google>,
}

pub struct JwtService {
pub google: jwt::google::JwtService,
}

impl JwtService {
pub fn new() -> Self {
Self {
google: jwt::google::JwtService::default(),
}
}
}

pub struct Authenticator {
pub device_flows: DeviceFlows,
pub jwt_decoders: JwtService,
}

impl Authenticator {
pub fn new() -> Self {
Self {
device_flows: DeviceFlows {
github: DeviceFlow::new(provider::Github::default()),
google: DeviceFlow::new(provider::Google::default()),
},
jwt_decoders: JwtService::new(),
}
}

#[must_use]
pub fn with_device_flows(self, device_flows: DeviceFlows) -> Self {
Self {
device_flows,
..self
}
}
}
143 changes: 111 additions & 32 deletions crates/synd_term/src/application/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use crossterm::event::{Event as CrosstermEvent, KeyCode, KeyEvent, KeyEventKind}
use futures_util::{FutureExt, Stream, StreamExt};
use ratatui::{style::palette::tailwind, widgets::Widget};
use synd_auth::device_flow::{
github::DeviceFlow, DeviceAccessTokenResponse, DeviceAuthorizationResponse,
self, DeviceAccessTokenResponse, DeviceAuthorizationResponse, DeviceFlow,
};
use tokio::time::{Instant, Sleep};

Expand Down Expand Up @@ -33,6 +33,9 @@ pub use in_flight::{InFlight, RequestId, RequestSequence};
mod input_parser;
use input_parser::InputParser;

mod authenticator;
pub use authenticator::{Authenticator, DeviceFlows, JwtService};

enum Screen {
Login,
Browse,
Expand All @@ -52,15 +55,13 @@ pub enum ListAction {
pub struct Config {
pub idle_timer_interval: Duration,
pub throbber_timer_interval: Duration,
pub github_device_flow: DeviceFlow,
}

impl Default for Config {
fn default() -> Self {
Self {
idle_timer_interval: Duration::from_secs(250),
throbber_timer_interval: Duration::from_millis(250),
github_device_flow: DeviceFlow::new(config::github::CLIENT_ID),
}
}
}
Expand All @@ -71,6 +72,7 @@ pub struct Application {
jobs: Jobs,
components: Components,
interactor: Interactor,
authenticator: Authenticator,
in_flight: InFlight,
theme: Theme,
idle_timer: Pin<Box<Sleep>>,
Expand All @@ -94,6 +96,7 @@ impl Application {
jobs: Jobs::new(),
components: Components::new(),
interactor: Interactor::new(),
authenticator: Authenticator::new(),
in_flight: InFlight::new().with_throbber_timer_interval(config.throbber_timer_interval),
theme: Theme::with_palette(&tailwind::BLUE),
idle_timer: Box::pin(tokio::time::sleep(config.idle_timer_interval)),
Expand All @@ -110,6 +113,18 @@ impl Application {
Self { theme, ..self }
}

#[must_use]
pub fn with_authenticator(self, authenticator: Authenticator) -> Self {
Self {
authenticator,
..self
}
}

pub fn jwt_decoders(&self) -> &JwtService {
&self.authenticator.jwt_decoders
}

pub fn set_credential(&mut self, cred: Credential) {
self.client.set_credential(cred);
self.components.auth.authenticated();
Expand Down Expand Up @@ -215,12 +230,42 @@ impl Application {
Command::Idle => {
self.handle_idle();
}
Command::Authenticate(method) => self.authenticate(method),
Command::DeviceAuthorizationFlow(device_authorization) => {
self.device_authorize_flow(device_authorization);
Command::Authenticate(provider) => match provider {
AuthenticationProvider::Github => {
self.authenticate(provider, self.authenticator.device_flows.github.clone());
}
AuthenticationProvider::Google => {
self.authenticate(provider, self.authenticator.device_flows.google.clone());
}
},
Command::MoveAuthenticationProvider(direction) => {
self.components.auth.move_selection(&direction);
self.should_render = true;
}
Command::CompleteDevieAuthorizationFlow(device_access_token) => {
self.complete_device_authroize_flow(device_access_token);
Command::DeviceAuthorizationFlow {
provider,
device_authorization,
} => match provider {
AuthenticationProvider::Github => {
self.device_authorize_flow(
provider,
self.authenticator.device_flows.github.clone(),
device_authorization,
);
}
AuthenticationProvider::Google => {
self.device_authorize_flow(
provider,
self.authenticator.device_flows.google.clone(),
device_authorization,
);
}
},
Command::CompleteDevieAuthorizationFlow {
provider,
device_access_token,
} => {
self.complete_device_authroize_flow(provider, device_access_token);
}
Command::MoveTabSelection(direction) => {
match self.components.tabs.move_selection(&direction) {
Expand Down Expand Up @@ -342,6 +387,8 @@ impl Application {
message,
request_seq,
} => {
tracing::error!("{message}");

if let Some(request_seq) = request_seq {
self.in_flight.remove(request_seq);
}
Expand Down Expand Up @@ -389,6 +436,12 @@ impl Application {
));
};
}
KeyCode::Char('j') => {
return Some(Command::MoveAuthenticationProvider(Direction::Down))
}
KeyCode::Char('k') => {
return Some(Command::MoveAuthenticationProvider(Direction::Up))
}
_ => {}
},
Screen::Browse => match key.code {
Expand Down Expand Up @@ -661,38 +714,45 @@ impl Application {
}

impl Application {
#[tracing::instrument(skip(self))]
fn authenticate(&mut self, provider: AuthenticationProvider) {
#[tracing::instrument(skip(self, device_flow))]
fn authenticate<P>(&mut self, provider: AuthenticationProvider, device_flow: DeviceFlow<P>)
where
P: device_flow::Provider + Sync + Send + 'static,
{
tracing::info!("Start authenticate");
match provider {
AuthenticationProvider::Github => {
let device_flow = self.config.github_device_flow.clone();
let fut = async move {
match device_flow.device_authorize_request().await {
Ok(res) => Ok(Command::DeviceAuthorizationFlow(res)),
Err(err) => Ok(Command::HandleError {
message: format!("{err}"),
request_seq: None,
}),
}
}
.boxed();
self.jobs.futures.push(fut);

let fut = async move {
match device_flow.device_authorize_request().await {
Ok(device_authorization) => Ok(Command::DeviceAuthorizationFlow {
provider,
device_authorization,
}),
Err(err) => Ok(Command::HandleError {
message: format!("{err}"),
request_seq: None,
}),
}
}
.boxed();
self.jobs.futures.push(fut);
}

fn device_authorize_flow(&mut self, device_authorization: DeviceAuthorizationResponse) {
fn device_authorize_flow<P>(
&mut self,
provider: AuthenticationProvider,
device_flow: DeviceFlow<P>,
device_authorization: DeviceAuthorizationResponse,
) where
P: device_flow::Provider + Sync + Send + 'static,
{
self.components
.auth
.set_device_authorization_response(device_authorization.clone());
self.should_render = true;

// try to open input screen in the browser
self.interactor
.open_browser(device_authorization.verification_uri.to_string());
.open_browser(device_authorization.verification_uri().to_string());

let device_flow = self.config.github_device_flow.clone();
let fut = async move {
match device_flow
.poll_device_access_token(
Expand All @@ -701,20 +761,39 @@ impl Application {
)
.await
{
Ok(res) => Ok(Command::CompleteDevieAuthorizationFlow(res)),
Ok(device_access_token) => Ok(Command::CompleteDevieAuthorizationFlow {
provider,
device_access_token,
}),
Err(err) => Ok(Command::HandleError {
message: format!("{err}"),
request_seq: None,
}),
}
}
.boxed();

self.jobs.futures.push(fut);
}

fn complete_device_authroize_flow(&mut self, device_access_token: DeviceAccessTokenResponse) {
let auth = Credential::Github {
access_token: device_access_token.access_token,
fn complete_device_authroize_flow(
&mut self,
provider: AuthenticationProvider,
device_access_token: DeviceAccessTokenResponse,
) {
// TODO: remove
tracing::info!("{device_access_token:?}");

let auth = match provider {
AuthenticationProvider::Github => Credential::Github {
access_token: device_access_token.access_token,
},
AuthenticationProvider::Google => Credential::Google {
id_token: device_access_token.id_token.expect("id token not found"),
refresh_token: device_access_token
.refresh_token
.expect("refresh token not found"),
},
};

// should test with tmp file?
Expand Down

0 comments on commit a55c310

Please sign in to comment.