1use std::collections::BTreeMap;
6
7use color_eyre::eyre::{eyre, Context, ContextCompat};
8use http::StatusCode;
9use jwt::{Claims, RegisteredClaims, SignWithKey, VerifyWithKey};
10use serde::Deserialize;
11
12use crate::env::EnvVars;
13
14#[derive(Clone)]
15pub struct Auth {
17 pub jwt: String,
18 pub username: String,
19}
20
21pub async fn verify_token(
25 token: &str,
26 env_vars: &EnvVars,
27) -> Result<Auth, color_eyre::eyre::Error> {
28 let jwt_key = env_vars.get_jwt_key()?;
29 let claims: Result<Claims, _> = token.verify_with_key(&jwt_key);
30
31 let claims = claims.map_err(|_| eyre!("Claims not found on the JWT."))?;
32 let username = claims
33 .private
34 .get("username")
35 .ok_or(eyre!("Username not in the claims."))?;
36 let username = username
37 .as_str()
38 .ok_or(eyre!("Username is not a string."))?;
39
40 Ok(Auth {
41 jwt: token.to_owned(),
42 username: username.to_owned(),
43 })
44}
45
46async fn generate_token(
48 username: &str,
49 env_vars: &EnvVars,
50) -> Result<String, color_eyre::eyre::Error> {
51 let jwt_key = env_vars.get_jwt_key()?;
52
53 let expiration = chrono::Utc::now()
54 .checked_add_days(chrono::naive::Days::new(7)) .context("Error: error setting JWT expiry date")?
56 .timestamp()
57 .unsigned_abs();
58
59 let mut private_claims = BTreeMap::new();
60 private_claims.insert(
61 "username".into(),
62 serde_json::Value::String(username.into()),
63 );
64
65 let claims = Claims {
66 registered: RegisteredClaims {
67 audience: None,
68 issued_at: None,
69 issuer: None,
70 subject: None,
71 not_before: None,
72 json_web_token_id: None,
73 expiration: Some(expiration),
74 },
75 private: private_claims,
76 };
77
78 Ok(claims.sign_with_key(&jwt_key)?)
79}
80
81#[derive(Deserialize)]
82struct GithubAccessTokenResponse {
83 access_token: String,
84}
85
86#[derive(Deserialize)]
87struct GithubUserResponse {
88 login: String,
89}
90
91#[derive(Deserialize)]
92struct GithubMembershipResponse {
93 state: String,
94}
95
96pub async fn authenticate_user(
103 code: &String,
104 env_vars: &EnvVars,
105) -> Result<Option<String>, color_eyre::eyre::Error> {
106 let client = reqwest::Client::new();
107
108 let response = client
110 .get(format!(
111 "https://github.com/login/oauth/access_token?client_id={}&client_secret={}&code={}",
112 env_vars.gh_client_id, env_vars.gh_client_secret, code
113 ))
114 .header("Accept", "application/json")
115 .send()
116 .await
117 .context("Error getting access token from Github.")?;
118
119 if response.status() != StatusCode::OK {
120 tracing::error!(
121 "Github OAuth error getting access token: {}",
122 response.text().await?
123 );
124
125 return Err(eyre!("Github API response error."));
126 }
127
128 let access_token =
129 serde_json::from_slice::<GithubAccessTokenResponse>(&response.bytes().await?)
130 .context("Error parsing access token response.")?
131 .access_token;
132
133 let response = client
135 .get("https://api.github.com/user")
136 .header("Authorization", format!("Bearer {}", access_token))
137 .header("User-Agent", "bruh") .send()
139 .await
140 .context("Error fetching user's username.")?;
141
142 if response.status() != StatusCode::OK {
143 tracing::error!(
144 "Github OAuth error getting username: {}",
145 response.text().await?
146 );
147
148 return Err(eyre!("Github API response error."));
149 }
150
151 let username = serde_json::from_slice::<GithubUserResponse>(&response.bytes().await?)
152 .context("Error parsing username API response.")?
153 .login;
154
155 if env_vars
157 .gh_admin_usernames
158 .split(",")
159 .any(|x| x == username)
160 {
161 return Ok(Some(generate_token(&username, env_vars).await?));
162 }
163
164 println!(
166 "https://api.github.com/orgs/{}/teams/{}/memberships/{}",
167 env_vars.gh_org_name, env_vars.gh_org_team_slug, username
168 );
169
170 let response = client
171 .get(format!(
172 "https://api.github.com/orgs/{}/teams/{}/memberships/{}",
173 env_vars.gh_org_name, env_vars.gh_org_team_slug, username
174 ))
175 .header(
176 "Authorization",
177 format!("Bearer {}", env_vars.gh_org_admin_token),
178 )
179 .header("User-Agent", "bruh why is this required")
180 .send()
181 .await
182 .context("Error getting user's team membership")?;
183
184 if response.status() != StatusCode::OK {
185 tracing::error!(
186 "Github OAuth error getting membership status: {}",
187 response.text().await?
188 );
189
190 return Err(eyre!("Github API response error."));
191 }
192
193 let state = serde_json::from_slice::<GithubMembershipResponse>(&response.bytes().await?)
194 .context("Error parsing membership API response.")?
195 .state;
196
197 if state != "active" {
198 Ok(None)
199 } else {
200 Ok(Some(generate_token(&username, env_vars).await?))
201 }
202}