Skip to content

Commit

Permalink
feat: use redirect in url instead
Browse files Browse the repository at this point in the history
  • Loading branch information
speed2exe committed Oct 4, 2024
1 parent 6dc9366 commit a4bc00b
Show file tree
Hide file tree
Showing 9 changed files with 74 additions and 49 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions admin_frontend/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,4 @@ human_bytes = "0.4.3"
rand = "0.8.5"
sha2 = "0.10.8"
base64 = "0.22.1"
urlencoding = "2.1.3"
6 changes: 6 additions & 0 deletions admin_frontend/src/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use serde::Deserialize;
pub struct WebApiLoginRequest {
pub email: String,
pub password: String,
pub redirect_to: Option<String>,
}

#[derive(Deserialize)]
Expand Down Expand Up @@ -82,3 +83,8 @@ pub struct OAuthRedirectToken {
pub redirect_uri: Option<String>,
pub code_verifier: Option<String>,
}

#[derive(Debug, Deserialize)]
pub struct LoginParams {
pub redirect_to: Option<String>,
}
50 changes: 20 additions & 30 deletions admin_frontend/src/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -137,23 +137,22 @@ impl FromRequestParts<AppState> for UserSession {
parts: &mut Parts,
state: &AppState,
) -> Result<Self, Self::Rejection> {
let mut cookie_jar = CookieJar::from_request_parts(parts, state)
let cookie_jar = CookieJar::from_request_parts(parts, state)
.await
.map_err(|e| {
tracing::error!("failed to get cookie jar");
SessionRejection::new_no_cookie(SessionRejectionKind::CookieError(e.to_string()))
SessionRejection::new(SessionRejectionKind::CookieError(e.to_string()), None)
})?;

let session =
match get_session_from_store(&cookie_jar, &state.session_store, &state.gotrue_client).await {
Ok(session) => session,
Err(err) => {
if let Some(original_url) = parts.extensions.get::<OriginalUri>() {
let mut pre_login_cookie = Cookie::new("pre_login_url", original_url.to_string());
pre_login_cookie.set_path("/");
cookie_jar = cookie_jar.add(pre_login_cookie);
};
return Err(SessionRejection::new(cookie_jar, err));
let original_url = parts
.extensions
.get::<OriginalUri>()
.map(|uri| urlencoding::encode(&uri.to_string()).to_string());
return Err(SessionRejection::new(err, original_url));
},
};
Ok(session)
Expand Down Expand Up @@ -227,36 +226,28 @@ fn get_session_expiration(access_token: &str) -> Option<u64> {

#[derive(Clone, Debug)]
pub struct SessionRejection {
pub cookie_jar: CookieJar,
pub session_rejection: SessionRejectionKind,
pub kind: SessionRejectionKind,
pub redirect_url: Option<String>,
}

impl SessionRejection {
fn new_no_cookie(kind: SessionRejectionKind) -> Self {
Self {
cookie_jar: CookieJar::new(),
session_rejection: kind,
}
}

fn new(cookie_jar: CookieJar, kind: SessionRejectionKind) -> Self {
Self {
cookie_jar,
session_rejection: kind,
}
fn new(kind: SessionRejectionKind, redirect_url: Option<String>) -> Self {
Self { kind, redirect_url }
}
}

impl IntoResponse for SessionRejection {
fn into_response(self) -> axum::response::Response {
tracing::info!("session rejection: {:?}", self);
match self.session_rejection {
SessionRejectionKind::Redirect(url) => {
(self.cookie_jar, Redirect::temporary(&url)).into_response()
},
x => {
tracing::info!("session rejection: {:?}", x);
(self.cookie_jar, Redirect::temporary("/web/login")).into_response()
match self.kind {
any => {
tracing::info!("session rejection: {:?}", any);
match self.redirect_url {
Some(url) => {
Redirect::temporary(&format!("/web/login?redirect_to={}", url)).into_response()
},
None => Redirect::temporary("/web/login").into_response(),
}
},
}
}
Expand All @@ -268,7 +259,6 @@ pub enum SessionRejectionKind {
SessionNotFound,
CookieError(String),
RefreshTokenError(String),
Redirect(String),
}

impl ToRedisArgs for UserSession {
Expand Down
1 change: 1 addition & 0 deletions admin_frontend/src/templates.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ pub struct ChangePassword;
#[template(path = "pages/login.html")]
pub struct Login<'a> {
pub oauth_providers: &'a [&'a str],
pub redirect_to: Option<&'a str>,
}

#[derive(Template)]
Expand Down
47 changes: 32 additions & 15 deletions admin_frontend/src/web_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ use crate::ext::api::{
verify_token_cloud,
};
use crate::models::{
OAuthRedirect, OAuthRedirectToken, WebApiAdminCreateUserRequest, WebApiChangePasswordRequest,
WebApiCreateSSOProviderRequest, WebApiInviteUserRequest, WebApiPutUserRequest,
LoginParams, OAuthRedirect, OAuthRedirectToken, WebApiAdminCreateUserRequest,
WebApiChangePasswordRequest, WebApiCreateSSOProviderRequest, WebApiInviteUserRequest,
WebApiPutUserRequest,
};
use crate::response::WebApiResponse;
use crate::session::{self, new_session_cookie, CodeSession, UserSession};
Expand Down Expand Up @@ -316,6 +317,7 @@ async fn login_refresh_handler(
State(state): State<AppState>,
jar: CookieJar,
Path(refresh_token): Path<String>,
Query(login): Query<LoginParams>,
) -> Result<(CookieJar, HeaderMap, WebApiResponse<()>), WebApiError<'static>> {
let token = state
.gotrue_client
Expand All @@ -334,7 +336,7 @@ async fn login_refresh_handler(
))
.await?;

session_login(State(state), token, jar).await
session_login(State(state), token, jar, login.redirect_to.as_deref()).await
}

// login and set the cookie
Expand All @@ -344,8 +346,14 @@ async fn sign_in_handler(
jar: CookieJar,
Form(param): Form<WebApiLoginRequest>,
) -> Result<(CookieJar, HeaderMap, WebApiResponse<()>), WebApiError<'static>> {
if param.password.is_empty() {
let res = send_magic_link(State(state), &param.email).await?;
let WebApiLoginRequest {
email,
password,
redirect_to,
} = param;

if password.is_empty() {
let res = send_magic_link(State(state), &email).await?;
return Ok((CookieJar::new(), HeaderMap::new(), res));
}

Expand All @@ -354,13 +362,13 @@ async fn sign_in_handler(
.gotrue_client
.token(&gotrue::grant::Grant::Password(
gotrue::grant::PasswordGrant {
email: param.email.to_owned(),
password: param.password.to_owned(),
email: email.to_owned(),
password: password.to_owned(),
},
))
.await?;

session_login(State(state), token, jar).await
session_login(State(state), token, jar, redirect_to.as_deref()).await
}

async fn oauth_redirect_handler(
Expand Down Expand Up @@ -423,11 +431,11 @@ async fn oauth_redirect_handler(
)
.await?;

let url = format!(
let ext_url = format!(
"{}?code={}&state={}",
oauth_redirect.redirect_uri, code, oauth_redirect.state,
);

let url = format!("/web/login?redirect_to={}", urlencoding::encode(&ext_url));
let resp = Redirect::to(&url).into_response();
Ok(resp)
}
Expand Down Expand Up @@ -496,19 +504,27 @@ async fn sign_up_handler(
jar: CookieJar,
Form(param): Form<WebApiLoginRequest>,
) -> Result<(CookieJar, HeaderMap, WebApiResponse<()>), WebApiError<'static>> {
if param.password.is_empty() {
let res = send_magic_link(State(state), &param.email).await?;
let WebApiLoginRequest {
email,
password,
redirect_to,
} = param;

if password.is_empty() {
let res = send_magic_link(State(state), &email).await?;
return Ok((CookieJar::new(), HeaderMap::new(), res));
}

let sign_up_res = state
.gotrue_client
.sign_up(&param.email, &param.password, Some("/"))
.sign_up(&email, &password, Some("/"))
.await?;

match sign_up_res {
// when GOTRUE_MAILER_AUTOCONFIRM=true, auto sign in
SignUpResponse::Authenticated(token) => session_login(State(state), token, jar).await,
SignUpResponse::Authenticated(token) => {
session_login(State(state), token, jar, redirect_to.as_deref()).await
},
SignUpResponse::NotAuthenticated(user) => {
info!("user signed up and not authenticated: {:?}", user);
Ok((
Expand Down Expand Up @@ -555,6 +571,7 @@ async fn session_login(
State(state): State<AppState>,
token: GotrueTokenResponse,
jar: CookieJar,
redirect_to: Option<&str>,
) -> Result<(CookieJar, HeaderMap, WebApiResponse<()>), WebApiError<'static>> {
verify_token_cloud(
token.access_token.as_str(),
Expand All @@ -571,7 +588,7 @@ async fn session_login(

Ok((
jar.add(new_session_cookie(new_session_id)),
htmx_redirect("/web/home"),
htmx_redirect(redirect_to.unwrap_or("/web/home")),
().into(),
))
}
Expand Down
9 changes: 7 additions & 2 deletions admin_frontend/src/web_app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use crate::ext::api::{
get_user_workspace_limit, get_user_workspace_usages, get_user_workspaces, get_workspace_members,
verify_token_cloud,
};
use crate::models::{OAuthLoginAction, WebAppOAuthLoginRequest};
use crate::models::{LoginParams, OAuthLoginAction, WebAppOAuthLoginRequest};
use crate::session::{self, new_session_cookie, UserSession};
use askama::Template;
use axum::extract::{Path, Query, State};
Expand Down Expand Up @@ -345,11 +345,16 @@ async fn user_user_handler(
render_template(templates::UserDetails { user: &user })
}

async fn login_handler(State(state): State<AppState>) -> Result<Html<String>, WebAppError> {
async fn login_handler(
State(state): State<AppState>,
Query(login): Query<LoginParams>,
) -> Result<Html<String>, WebAppError> {
let redirect_to = login.redirect_to;
let external = state.gotrue_client.settings().await?.external;
let oauth_providers = external.oauth_providers();
render_template(templates::Login {
oauth_providers: &oauth_providers,
redirect_to: redirect_to.as_deref(),
})
}

Expand Down
6 changes: 5 additions & 1 deletion admin_frontend/templates/pages/login.html
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ <h2 style="padding: 16px;">AppFlowy Cloud</h2>
<h3>Email Login</h3>
<form>
<table style="width: 100%">
{% if let Some(redirect_to) = redirect_to %}
<input type="hidden" name="redirect_to" value="{{ redirect_to }}">
{% endif %}

<tr>
<td>Email</td>
<td>
Expand Down Expand Up @@ -92,7 +96,7 @@ <h3>OAuth Login</h3>
<div id="oauth-container">
{% for provider in oauth_providers %}
<div class="oauth-icon">
<a href="/gotrue/authorize?provider={{ provider|escape }}&redirect_to=/web/login">
<a href="/gotrue/authorize?provider={{ provider|escape }}&redirect_to={{ redirect_to|default("/web/login")|escape }}">
<div
hx-get="../assets/{{ provider|escape }}/logo.html"
hx-trigger="load"
Expand Down
2 changes: 1 addition & 1 deletion tests/workspace/import_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ async fn import_zip(name: &str) -> (TestClient, String) {
);

// after the import task is completed, the new workspace should be visible
let mut workspaces = client.api_client.get_workspaces().await.unwrap();
let workspaces = client.api_client.get_workspaces().await.unwrap();
assert_eq!(workspaces.len(), 2);

let imported_workspace = workspaces
Expand Down

0 comments on commit a4bc00b

Please sign in to comment.