Files
novem/src/api/auth.rs

109 lines
3.1 KiB
Rust
Raw Normal View History

2026-04-24 19:22:25 +01:00
use std::collections::HashMap;
2026-04-21 20:30:41 +01:00
use serde::Deserialize;
2026-05-06 01:42:38 +08:00
use crate::{api, query};
2026-04-24 19:22:25 +01:00
pub(crate) const DEVICE_LOGIN_FLOW_URL: &str = "https://github.com/login/device";
2026-04-21 20:30:41 +01:00
#[derive(Clone)]
2026-04-22 12:41:33 +01:00
pub struct CreateDeviceCode;
2026-04-21 20:30:41 +01:00
#[derive(Deserialize)]
2026-04-24 19:22:25 +01:00
pub(crate) struct DeviceCodeResponse {
pub device_code: String,
pub user_code: String,
pub vertification_uri: Option<String>,
pub expires_in: u16,
2026-04-25 00:49:50 +01:00
// minimum number of seconds between polling for access token
2026-04-24 19:22:25 +01:00
pub interval: u16,
2026-04-21 20:30:41 +01:00
}
impl query::QueryFn for CreateDeviceCode {
type Data = DeviceCodeResponse;
type Error = api::Error;
type Context = api::QueryContext;
2026-05-06 01:42:38 +08:00
fn key(&self) -> query::Key {
"auth/device_code".into()
2026-04-21 20:30:41 +01:00
}
async fn run(&self, c: &Self::Context) -> Result<Self::Data, Self::Error> {
2026-04-24 19:22:25 +01:00
let res = c
2026-04-21 20:30:41 +01:00
.http
.post(format!(
"https://github.com/login/device/code?client_id={}",
c.github.client_id
))
2026-04-24 19:22:25 +01:00
.header("Accept", "application/json")
.send()
.await?;
api::parse_response(res).await
}
}
#[derive(Clone)]
pub struct RequestAccessToken {
pub device_code: String,
}
#[derive(Deserialize)]
pub struct RequestAccessTokenResponse {
pub access_token: String,
pub token_type: String,
pub scope: String,
}
impl query::QueryFn for RequestAccessToken {
type Data = RequestAccessTokenResponse;
type Error = api::Error;
type Context = api::QueryContext;
2026-05-06 01:42:38 +08:00
fn key(&self) -> query::Key {
"auth.access_token".into()
2026-04-24 19:22:25 +01:00
}
async fn run(&self, c: &Self::Context) -> Result<Self::Data, Self::Error> {
let mut params = HashMap::new();
params.insert("client_id", c.github.client_id);
params.insert("device_code", &self.device_code);
params.insert("grant_type", "urn:ietf:params:oauth:grant-type:device_code");
let res = c
.http
.post(format!(
"https://github.com/login/oauth/access_token?client_id={}&device_code={}",
c.github.client_id, self.device_code
))
2026-04-25 00:49:50 +01:00
.header("Accept", "application/json")
2026-04-24 19:22:25 +01:00
.form(&params)
2026-04-21 20:30:41 +01:00
.send()
.await?;
2026-04-25 00:49:50 +01:00
let status = res.status();
let data = res.bytes().await?;
println!("status: {:?}, data: {:?}", status, str::from_utf8(&data));
if status.is_success() {
// for device code flow, github returns error with 200 response code
// so the body is either a valid access token or an error
let json: serde_json::Value = serde_json::from_slice(&data)?;
let maybe_error = &json["error"];
if maybe_error.is_string() {
let error = serde_json::from_value::<api::GithubError>(json)?;
Err(api::Error::Github(error))
} else {
let res = serde_json::from_value::<RequestAccessTokenResponse>(json)?;
Ok(res)
}
} else {
serde_json::from_slice::<api::GithubError>(&data)
.map_err(|e| e.into())
.and_then(|e| Err(api::Error::Github(e)))
}
2026-04-21 20:30:41 +01:00
}
}