|
1 | | -use crate::config::Config; |
2 | | -use crate::constants; |
3 | | -use crate::oauth::types::roblox::{ |
4 | | - RobloxAccessTokenResponse, RobloxAuthorizedCodeRequestBody, RobloxOAuthScopes, |
5 | | - RobloxUserInfoResponse, |
| 1 | +use crate::{ |
| 2 | + cipher::{decrypt, EncryptedData}, |
| 3 | + config::Config, |
| 4 | + constants, |
| 5 | + database::wrappers::{ |
| 6 | + account_links::{ |
| 7 | + models::AccountLink, AccountLinkUserId, AccountLinksDb, DiscordMarker, RobloxMarker, |
| 8 | + }, |
| 9 | + roblox_connections::{models::NewRobloxConnection, RobloxConnectionsDb}, |
| 10 | + }, |
| 11 | + oauth::types::roblox::{ |
| 12 | + RobloxAccessTokenResponse, RobloxAuthorizedCodeRequestBody, RobloxOAuthScopes, |
| 13 | + RobloxTokenRefreshRequestBody, RobloxUserInfoResponse, |
| 14 | + }, |
| 15 | + response::ApiError, |
| 16 | + url, DbConn, |
6 | 17 | }; |
7 | | -use crate::response::ApiError; |
8 | | -use crate::url; |
9 | 18 | use rocket::http::Status; |
| 19 | +use std::sync::Arc; |
10 | 20 |
|
11 | 21 | /// Constructs the Roblox OAuth URL with the given scopes and state. |
12 | 22 | /// The scopes are joined by an encoded space (`%20`). |
@@ -97,3 +107,159 @@ pub(crate) fn get_authorized_user(access_token: &str) -> Result<RobloxUserInfoRe |
97 | 107 | ) |
98 | 108 | }) |
99 | 109 | } |
| 110 | + |
| 111 | +/// Exchanges the refresh token for an access token. |
| 112 | +/// |
| 113 | +/// # Arguments |
| 114 | +/// |
| 115 | +/// * `refresh_token` - The refresh token to exchange for an access token. |
| 116 | +/// * `cfg` - The application configuration. |
| 117 | +/// |
| 118 | +/// # Returns |
| 119 | +/// |
| 120 | +/// The [`RobloxAccessTokenResponse`] struct if the token refresh was successful, an [`ApiError`] otherwise. |
| 121 | +pub(crate) fn refresh_access_token( |
| 122 | + refresh_token: &str, |
| 123 | + cfg: &Config, |
| 124 | +) -> Result<RobloxAccessTokenResponse, ApiError> { |
| 125 | + let body = RobloxTokenRefreshRequestBody::new(refresh_token, cfg); |
| 126 | + let response = minreq::post(constants::roblox_api::TOKEN_URL) |
| 127 | + .with_header("Content-Type", "application/x-www-form-urlencoded") |
| 128 | + .with_body(body.as_query_params()) |
| 129 | + .send() |
| 130 | + .map_err(|_| ApiError::message(Status::BadGateway, "Failed to refresh access token"))?; |
| 131 | + |
| 132 | + response.json::<RobloxAccessTokenResponse>().map_err(|_| { |
| 133 | + ApiError::message( |
| 134 | + Status::InternalServerError, |
| 135 | + "Failed to parse token response", |
| 136 | + ) |
| 137 | + }) |
| 138 | +} |
| 139 | + |
| 140 | +/// Finds an account link by discord uid & roblox uid |
| 141 | +/// |
| 142 | +/// # Arguments |
| 143 | +/// |
| 144 | +/// * `db_conn` |
| 145 | +/// * `discord_uid` |
| 146 | +/// * `roblox_uid` |
| 147 | +/// |
| 148 | +/// # Returns |
| 149 | +/// |
| 150 | +/// [`AccountLink`] if the account link was found, an [`ApiError`] otherwise. |
| 151 | +pub(crate) async fn find_account_link( |
| 152 | + db_conn: &DbConn, |
| 153 | + discord_uid: String, |
| 154 | + roblox_uid: String, |
| 155 | +) -> Result<AccountLink, ApiError> { |
| 156 | + db_conn |
| 157 | + .run(|conn| { |
| 158 | + let discord_marker = AccountLinkUserId::<DiscordMarker>::new(discord_uid); |
| 159 | + let roblox_marker = AccountLinkUserId::<RobloxMarker>::new(roblox_uid); |
| 160 | + |
| 161 | + AccountLinksDb::find_one((discord_marker, roblox_marker), conn) |
| 162 | + }) |
| 163 | + .await |
| 164 | + .ok_or_else(|| { |
| 165 | + ApiError::message( |
| 166 | + Status::NotFound, |
| 167 | + "Roblox account link with the given discord_uid & roblox_uid not found", |
| 168 | + ) |
| 169 | + }) |
| 170 | +} |
| 171 | + |
| 172 | +/// Finds & decrypts the refresh token belonging to the given roblox uid |
| 173 | +/// |
| 174 | +/// # Arguments |
| 175 | +/// |
| 176 | +/// * `db_conn` |
| 177 | +/// * `roblox_uid` |
| 178 | +/// |
| 179 | +/// # Returns |
| 180 | +/// |
| 181 | +/// [`String`] if the refresh token was found, an [`ApiError`] otherwise. |
| 182 | +pub(crate) async fn get_roblox_refresh_token_by_roblox_uid( |
| 183 | + db_conn: &DbConn, |
| 184 | + roblox_uid: &str, |
| 185 | +) -> Result<String, ApiError> { |
| 186 | + let roblox_uid = Arc::<str>::from(roblox_uid); |
| 187 | + |
| 188 | + db_conn |
| 189 | + .run(move |conn| { |
| 190 | + let Some(roblox_connection) = RobloxConnectionsDb::find_one(&roblox_uid, conn) else { |
| 191 | + return Err(ApiError::message( |
| 192 | + Status::NotFound, |
| 193 | + "Roblox connection with the given uid not found", |
| 194 | + )); |
| 195 | + }; |
| 196 | + |
| 197 | + let decrypted_refresh_token = decrypt(&EncryptedData { |
| 198 | + data: roblox_connection.refresh_token, |
| 199 | + nonce: roblox_connection.refresh_token_nonce, |
| 200 | + }) |
| 201 | + .map_err(|_| { |
| 202 | + ApiError::message( |
| 203 | + Status::InternalServerError, |
| 204 | + "Failed to decrypt the Roblox refresh token", |
| 205 | + ) |
| 206 | + }); |
| 207 | + |
| 208 | + Ok(decrypted_refresh_token) |
| 209 | + }) |
| 210 | + .await? |
| 211 | +} |
| 212 | + |
| 213 | +/// Refreshes the access token belonging to the given roblox uid |
| 214 | +/// with the given refresh token |
| 215 | +/// |
| 216 | +/// # Arguments |
| 217 | +/// |
| 218 | +/// * `db_conn` |
| 219 | +/// * `roblox_uid` |
| 220 | +/// * `refresh_token` |
| 221 | +/// * `cfg` - Application config |
| 222 | +/// |
| 223 | +/// # Returns |
| 224 | +/// |
| 225 | +/// [`()`] if everything was successful, an [`ApiError`] otherwise. |
| 226 | +pub(crate) async fn update_roblox_connection_with_refresh_token( |
| 227 | + db_conn: &DbConn, |
| 228 | + roblox_uid: &str, |
| 229 | + refresh_token: &str, |
| 230 | + cfg: &Config, |
| 231 | +) -> Result<(), ApiError> { |
| 232 | + let new_token_info = refresh_access_token(refresh_token, cfg)?; |
| 233 | + let new_token_expires_at = |
| 234 | + chrono::Utc::now().naive_utc() + chrono::Duration::seconds(new_token_info.expires_in); |
| 235 | + |
| 236 | + let update_connection = NewRobloxConnection::build() |
| 237 | + .refresh_token(new_token_info.refresh_token) |
| 238 | + .access_token(new_token_info.access_token) |
| 239 | + .expires_at(new_token_expires_at) |
| 240 | + .build_update() |
| 241 | + .map_err(|_| { |
| 242 | + ApiError::message( |
| 243 | + Status::InternalServerError, |
| 244 | + "Failed to encrypt access token and/or refresh token", |
| 245 | + ) |
| 246 | + })?; |
| 247 | + |
| 248 | + let roblox_uid = Arc::<str>::from(roblox_uid); |
| 249 | + |
| 250 | + db_conn |
| 251 | + .run(move |conn| { |
| 252 | + RobloxConnectionsDb::update_one(&roblox_uid, update_connection, conn)?; |
| 253 | + |
| 254 | + diesel::result::QueryResult::Ok(()) |
| 255 | + }) |
| 256 | + .await |
| 257 | + .map_err(|_| { |
| 258 | + ApiError::message( |
| 259 | + Status::InternalServerError, |
| 260 | + "Failed to update the Roblox connection due to an error", |
| 261 | + ) |
| 262 | + })?; |
| 263 | + |
| 264 | + Ok(()) |
| 265 | +} |
0 commit comments