mirror of
https://github.com/trailbaseio/trailbase.git
synced 2026-02-21 10:19:02 -06:00
Rename redirect_to query parameter to redirect_uri in accordance with RFC 6749.
Reference: https://datatracker.ietf.org/doc/html/rfc6749#appendix-A.6
This commit is contained in:
5
Makefile
5
Makefile
@@ -26,6 +26,9 @@ check:
|
||||
docker:
|
||||
docker buildx build --platform linux/arm64,linux/amd64 --output=type=registry -t trailbase/trailbase:latest .
|
||||
|
||||
openapi:
|
||||
cargo run -- openapi print > docs/openapi/schema.json
|
||||
|
||||
cloc:
|
||||
cloc --not-match-d=".*(/target|/dist|/node_modules|/vendor|.astro|.build|.venv|/traildepot|/flutter|/assets|lock|_benchmark|/bin|/obj).*" .
|
||||
|
||||
@@ -42,4 +45,4 @@ publish_crates:
|
||||
-p trailbase-js \
|
||||
-p trailbase
|
||||
|
||||
.PHONY: default format check static docker cloc publish_crates
|
||||
.PHONY: default format check static docker openapi cloc publish_crates
|
||||
|
||||
@@ -82,7 +82,7 @@
|
||||
"operationId": "change_password_handler",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "redirect_to",
|
||||
"name": "redirect_uri",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"schema": {
|
||||
@@ -126,7 +126,23 @@
|
||||
"operationId": "login_handler",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "redirect_to",
|
||||
"name": "redirect_uri",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": ["string", "null"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "response_type",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": ["string", "null"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "pkce_code_challenge",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"schema": {
|
||||
@@ -166,7 +182,7 @@
|
||||
"operationId": "logout_handler",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "redirect_to",
|
||||
"name": "redirect_uri",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"schema": {
|
||||
@@ -266,7 +282,7 @@
|
||||
"operationId": "login_with_external_auth_provider",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "redirect_to",
|
||||
"name": "redirect_uri",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"schema": {
|
||||
@@ -511,7 +527,7 @@
|
||||
"operationId": "create_record_handler",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "redirect_to",
|
||||
"name": "redirect_uri",
|
||||
"in": "query",
|
||||
"description": "Redirect user to this address upon successful record creation.\nThis only exists to support insertions via static HTML form actions.\n\nWe may want to have a different on-error redirect to better support the static HTML use-case.",
|
||||
"required": false,
|
||||
@@ -826,7 +842,7 @@
|
||||
"pkce_code_challenge": {
|
||||
"type": ["string", "null"]
|
||||
},
|
||||
"redirect_to": {
|
||||
"redirect_uri": {
|
||||
"type": ["string", "null"]
|
||||
},
|
||||
"response_type": {
|
||||
|
||||
@@ -80,7 +80,7 @@ off-site sign-in with an external provider, users are redirected back,
|
||||
1. first to your TrailBase instance at
|
||||
`<YOUR_SITE>/api/auth/v1/oauth/<PROVIDER_NAME>/callback?code=<AUTH_CODE>`
|
||||
2. and subsequently to a URI registered and provided by your app via
|
||||
`?redirect_to=my-app://callback`.
|
||||
`?redirect_uri=my-app://callback`.
|
||||
|
||||
This allows TrailBase to observe the external provider's auth code and issue
|
||||
one to your app as well.
|
||||
@@ -107,7 +107,7 @@ pass:
|
||||
|
||||
- `response_type=code`,
|
||||
- `pkce_code_challenge=<urlSafeBase64(sha256(pkce_code_verifier)))>`,
|
||||
- `redirect_to=<TARGET>`, e.g. `custom-app-scheme://callback`,
|
||||
- `redirect_uri=<TARGET>`, e.g. `custom-app-scheme://callback`,
|
||||
|
||||
as inputs to `/api/auth/v1/login` or `/api/auth/v1/oauth/<PROVIDER_NAME>/login`.
|
||||
Note that native apps will need to register the custom scheme first to receive
|
||||
|
||||
@@ -75,7 +75,7 @@ class _LoginFormState extends State<LoginFormWidget> {
|
||||
// Construct the login page url
|
||||
final url = Uri.parse('${widget.client.site()}/_/auth/login')
|
||||
.replace(queryParameters: {
|
||||
'redirect_to': redirectUri,
|
||||
'redirect_uri': redirectUri,
|
||||
'response_type': 'code',
|
||||
'pkce_code_challenge': challenge,
|
||||
});
|
||||
|
||||
@@ -47,7 +47,7 @@ export function AuthButton() {
|
||||
return (
|
||||
<Switch>
|
||||
<Match when={!user()}>
|
||||
<a href={`${HOST}/_/auth/login?redirect_to=${redirect}`}>Log in</a>
|
||||
<a href={`${HOST}/_/auth/login?redirect_uri=${redirect}`}>Log in</a>
|
||||
</Match>
|
||||
|
||||
<Match when={user()}>
|
||||
|
||||
@@ -14,7 +14,7 @@ const redirect = import.meta.env.DEV ? "http://localhost:4321/" : "/";
|
||||
|
||||
<form
|
||||
method="post"
|
||||
action={`${RECORD_API}/articles?redirect_to=${redirect}`}
|
||||
action={`${RECORD_API}/articles?redirect_uri=${redirect}`}
|
||||
enctype="multipart/form-data"
|
||||
target="_self"
|
||||
>
|
||||
|
||||
@@ -89,29 +89,3 @@ const BASE_URL = import.meta.env.BASE_URL;
|
||||
{"{% endif %}"}
|
||||
</div>
|
||||
</Form>
|
||||
|
||||
{
|
||||
// For DEV we need to fix up redirects to point back to dev server's auth APIs.
|
||||
import.meta.env.DEV && (
|
||||
<script is:inline>
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
// In source order
|
||||
const form = document.forms[0];
|
||||
const baseAction = form.dataset.base_action;
|
||||
const baseUrl = form.dataset.base_url;
|
||||
|
||||
// NOTE: Only setting redirect_to should be fine since other params such
|
||||
// as response_type are sent via the hidden form state. This is different from
|
||||
// OAuth APIs, which expect oauth_query_params.
|
||||
const redirect = urlParams.get("redirect_to");
|
||||
if (redirect) {
|
||||
form.action = `${baseAction}?redirect_to=${redirect}`;
|
||||
} else {
|
||||
const devRedirect = `http://${window.location.host}${baseUrl}/profile`;
|
||||
form.action = `${baseAction}?redirect_to=${devRedirect}`;
|
||||
}
|
||||
|
||||
console.debug(`Updated form action to ${form.action}, based on redirect: ${redirect}`);
|
||||
</script>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type LoginRequest = { email: string, password: string, redirect_to: string | null, response_type: string | null, pkce_code_challenge: string | null, };
|
||||
export type LoginRequest = { email: string, password: string, redirect_uri: string | null, response_type: string | null, pkce_code_challenge: string | null, };
|
||||
|
||||
@@ -80,11 +80,11 @@ where
|
||||
return "".to_string();
|
||||
}
|
||||
|
||||
pub fn redirect_to<T>(redirect_to: Option<T>) -> String
|
||||
pub fn redirect_uri<T>(redirect_uri: Option<T>) -> String
|
||||
where
|
||||
T: AsRef<str>,
|
||||
{
|
||||
return hidden_input("redirect_to", redirect_to);
|
||||
return hidden_input("redirect_uri", redirect_uri);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -95,20 +95,20 @@ mod tests {
|
||||
fn test_login_template_escaping() {
|
||||
let state = hidden_input("TEST", Some("FOO"));
|
||||
let alert = "<><>";
|
||||
let redirect_to = "http://localhost:42";
|
||||
let redirect_uri = "http://localhost:42";
|
||||
|
||||
let template = LoginTemplate {
|
||||
state: state.clone(),
|
||||
alert,
|
||||
enable_registration: true,
|
||||
oauth_providers: &[],
|
||||
oauth_query_params: &[("redirect_to", redirect_to)],
|
||||
oauth_query_params: &[("redirect_uri", redirect_uri)],
|
||||
}
|
||||
.render()
|
||||
.unwrap();
|
||||
|
||||
assert!(template.contains(&state), "{template}"); // Not escaped.
|
||||
assert!(!template.contains(&redirect_to), "{template}"); // Not escaped.
|
||||
assert!(!template.contains(&redirect_uri), "{template}"); // Not escaped.
|
||||
// Missing because no oauth provider given.
|
||||
assert!(!template.contains("foo=bar"), "{template}"); // Not escaped.
|
||||
assert!(!template.contains(alert), "{template}"); // Is escaped.
|
||||
@@ -122,13 +122,13 @@ mod tests {
|
||||
display_name: "Fancy Name".to_string(),
|
||||
img_name: "oidc".to_string(),
|
||||
}],
|
||||
oauth_query_params: &[("redirect_to", redirect_to), ("foo", "bar")],
|
||||
oauth_query_params: &[("redirect_uri", redirect_uri), ("foo", "bar")],
|
||||
}
|
||||
.render()
|
||||
.unwrap();
|
||||
|
||||
assert!(oauth_template.contains(&state), "{template}"); // Not escaped.
|
||||
assert!(oauth_template.contains(&redirect_to), "{template}"); // Not escaped.
|
||||
assert!(oauth_template.contains(&redirect_uri), "{template}"); // Not escaped.
|
||||
assert!(oauth_template.contains("foo=bar"), "{template}"); // Not escaped.
|
||||
}
|
||||
|
||||
|
||||
@@ -51,6 +51,7 @@ pub enum SubCommands {
|
||||
/// Export JSON Schema definitions.
|
||||
Schema(JsonSchemaArgs),
|
||||
/// Export OpenAPI definitions.
|
||||
#[command(name = "openapi")]
|
||||
OpenApi {
|
||||
#[command(subcommand)]
|
||||
cmd: Option<OpenApiSubCommands>,
|
||||
|
||||
@@ -134,7 +134,7 @@ pub async fn change_email_request_handler(
|
||||
|
||||
#[derive(Debug, Default, Deserialize, IntoParams)]
|
||||
pub(crate) struct ChangeEmailConfigQuery {
|
||||
pub redirect_to: Option<String>,
|
||||
pub redirect_uri: Option<String>,
|
||||
}
|
||||
|
||||
/// Confirm a change of email address.
|
||||
@@ -149,10 +149,10 @@ pub(crate) struct ChangeEmailConfigQuery {
|
||||
pub async fn change_email_confirm_handler(
|
||||
State(state): State<AppState>,
|
||||
Path(email_verification_code): Path<String>,
|
||||
Query(ChangeEmailConfigQuery { redirect_to }): Query<ChangeEmailConfigQuery>,
|
||||
Query(ChangeEmailConfigQuery { redirect_uri }): Query<ChangeEmailConfigQuery>,
|
||||
user: User,
|
||||
) -> Result<Redirect, AuthError> {
|
||||
validate_redirect(&state, redirect_to.as_deref())?;
|
||||
validate_redirect(&state, redirect_uri.as_deref())?;
|
||||
|
||||
if email_verification_code.len() != VERIFICATION_CODE_LENGTH {
|
||||
return Err(AuthError::BadRequest("Invalid code"));
|
||||
@@ -200,7 +200,7 @@ pub async fn change_email_confirm_handler(
|
||||
|
||||
return match rows_affected {
|
||||
0 => Err(AuthError::BadRequest("Invalid verification code")),
|
||||
1 => Ok(Redirect::to(redirect_to.as_deref().unwrap_or(PROFILE_UI))),
|
||||
1 => Ok(Redirect::to(redirect_uri.as_deref().unwrap_or(PROFILE_UI))),
|
||||
_ => panic!("emails updated for multiple users at once: {rows_affected}"),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ use crate::{app_state::AppState, auth::util::user_by_id};
|
||||
|
||||
#[derive(Debug, Default, Deserialize, IntoParams)]
|
||||
pub(crate) struct ChangePasswordQuery {
|
||||
pub redirect_to: Option<String>,
|
||||
pub redirect_uri: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Deserialize, TS, ToSchema)]
|
||||
@@ -42,11 +42,11 @@ pub struct ChangePasswordRequest {
|
||||
)]
|
||||
pub async fn change_password_handler(
|
||||
State(state): State<AppState>,
|
||||
Query(ChangePasswordQuery { redirect_to }): Query<ChangePasswordQuery>,
|
||||
Query(ChangePasswordQuery { redirect_uri }): Query<ChangePasswordQuery>,
|
||||
user: User,
|
||||
either_request: Either<ChangePasswordRequest>,
|
||||
) -> Result<Redirect, AuthError> {
|
||||
validate_redirect(&state, redirect_to.as_deref())?;
|
||||
validate_redirect(&state, redirect_uri.as_deref())?;
|
||||
|
||||
let request = match either_request {
|
||||
Either::Json(req) => req,
|
||||
@@ -98,7 +98,7 @@ pub async fn change_password_handler(
|
||||
|
||||
return match rows_affected {
|
||||
0 => Err(AuthError::BadRequest("Invalid old password")),
|
||||
1 => Ok(Redirect::to(redirect_to.as_deref().unwrap_or(PROFILE_UI))),
|
||||
1 => Ok(Redirect::to(redirect_uri.as_deref().unwrap_or(PROFILE_UI))),
|
||||
_ => panic!("password changed for multiple users at once: {rows_affected}"),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ pub struct LoginRequest {
|
||||
pub email: String,
|
||||
pub password: String,
|
||||
|
||||
pub redirect_to: Option<String>,
|
||||
pub redirect_uri: Option<String>,
|
||||
pub response_type: Option<String>,
|
||||
pub pkce_code_challenge: Option<String>,
|
||||
}
|
||||
@@ -67,7 +67,7 @@ pub(crate) async fn login_handler(
|
||||
LoginRequest {
|
||||
email,
|
||||
password,
|
||||
redirect_to,
|
||||
redirect_uri,
|
||||
response_type,
|
||||
pkce_code_challenge,
|
||||
},
|
||||
@@ -81,16 +81,16 @@ pub(crate) async fn login_handler(
|
||||
return match build_and_validate_input_params(
|
||||
&state,
|
||||
query_login_input.merge(LoginInputParams {
|
||||
redirect_to,
|
||||
redirect_uri,
|
||||
response_type,
|
||||
pkce_code_challenge,
|
||||
}),
|
||||
)? {
|
||||
LoginParams::Password { redirect_to } => {
|
||||
immediate_login(&state, &cookies, email, password, redirect_to, is_json).await
|
||||
LoginParams::Password { redirect_uri } => {
|
||||
immediate_login(&state, &cookies, email, password, redirect_uri, is_json).await
|
||||
}
|
||||
LoginParams::AuthorizationCodeFlowWithPkce {
|
||||
redirect_to,
|
||||
redirect_uri,
|
||||
pkce_code_challenge,
|
||||
} => {
|
||||
login_with_authorization_code_flow_and_pkce(
|
||||
@@ -98,7 +98,7 @@ pub(crate) async fn login_handler(
|
||||
&cookies,
|
||||
email,
|
||||
password,
|
||||
redirect_to,
|
||||
redirect_uri,
|
||||
pkce_code_challenge,
|
||||
)
|
||||
.await
|
||||
@@ -250,7 +250,7 @@ fn auth_error_to_response(err: AuthError, cookies: &Cookies, redirect: Option<St
|
||||
// error case?
|
||||
let msg = urlencode(&format!("Login Failed: {status}"));
|
||||
return if let Some(redirect) = redirect {
|
||||
Redirect::to(&format!("{LOGIN_UI}?alert={msg}&redirect_to={redirect}")).into_response()
|
||||
Redirect::to(&format!("{LOGIN_UI}?alert={msg}&redirect_uri={redirect}")).into_response()
|
||||
} else {
|
||||
Redirect::to(&format!("{LOGIN_UI}?alert={msg}")).into_response()
|
||||
};
|
||||
|
||||
@@ -18,7 +18,7 @@ use crate::auth::util::{
|
||||
|
||||
#[derive(Debug, Default, Deserialize, IntoParams)]
|
||||
pub struct LogoutQuery {
|
||||
redirect_to: Option<String>,
|
||||
redirect_uri: Option<String>,
|
||||
}
|
||||
|
||||
/// Logs out the current user and delete **all** pending sessions for that user.
|
||||
@@ -36,11 +36,11 @@ pub struct LogoutQuery {
|
||||
)]
|
||||
pub async fn logout_handler(
|
||||
State(state): State<AppState>,
|
||||
Query(LogoutQuery { redirect_to }): Query<LogoutQuery>,
|
||||
Query(LogoutQuery { redirect_uri }): Query<LogoutQuery>,
|
||||
user: Option<User>,
|
||||
cookies: Cookies,
|
||||
) -> Result<Redirect, AuthError> {
|
||||
validate_redirect(&state, redirect_to.as_deref())?;
|
||||
validate_redirect(&state, redirect_uri.as_deref())?;
|
||||
|
||||
remove_all_cookies(&cookies);
|
||||
|
||||
@@ -48,7 +48,7 @@ pub async fn logout_handler(
|
||||
delete_all_sessions_for_user(state.user_conn(), user.uuid).await?;
|
||||
}
|
||||
|
||||
return Ok(Redirect::to(redirect_to.as_deref().unwrap_or_else(|| {
|
||||
return Ok(Redirect::to(redirect_uri.as_deref().unwrap_or_else(|| {
|
||||
if state.public_dir().is_some() {
|
||||
"/"
|
||||
} else {
|
||||
|
||||
@@ -94,7 +94,7 @@ pub async fn request_email_verification_handler(
|
||||
|
||||
#[derive(Debug, Default, Deserialize, IntoParams)]
|
||||
pub(crate) struct VerifyEmailQuery {
|
||||
pub redirect_to: Option<String>,
|
||||
pub redirect_uri: Option<String>,
|
||||
}
|
||||
|
||||
/// Request a new email to verify email address.
|
||||
@@ -109,9 +109,9 @@ pub(crate) struct VerifyEmailQuery {
|
||||
pub async fn verify_email_handler(
|
||||
State(state): State<AppState>,
|
||||
Path(email_verification_code): Path<String>,
|
||||
Query(VerifyEmailQuery { redirect_to }): Query<VerifyEmailQuery>,
|
||||
Query(VerifyEmailQuery { redirect_uri }): Query<VerifyEmailQuery>,
|
||||
) -> Result<Redirect, AuthError> {
|
||||
validate_redirect(&state, redirect_to.as_deref())?;
|
||||
validate_redirect(&state, redirect_uri.as_deref())?;
|
||||
|
||||
lazy_static! {
|
||||
static ref UPDATE_CODE_QUERY: String = format!(
|
||||
@@ -134,7 +134,7 @@ pub async fn verify_email_handler(
|
||||
|
||||
return match rows_affected {
|
||||
0 => Err(AuthError::BadRequest("Invalid verification code")),
|
||||
1 => Ok(Redirect::to(redirect_to.as_deref().unwrap_or(PROFILE_UI))),
|
||||
1 => Ok(Redirect::to(redirect_uri.as_deref().unwrap_or(PROFILE_UI))),
|
||||
_ => panic!("email verification affected multiple users: {rows_affected}"),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -178,7 +178,7 @@ async fn test_auth_password_login_flow_with_pkce() {
|
||||
};
|
||||
|
||||
// Test login using the PKCE flow (?response_type="code").
|
||||
let redirect_to = "test-scheme://foo".to_string();
|
||||
let redirect_uri = "test-scheme://foo".to_string();
|
||||
let (pkce_code_challenge, pkce_code_verifier) = oauth2::PkceCodeChallenge::new_random_sha256();
|
||||
|
||||
// Missing code challenge.
|
||||
@@ -187,7 +187,7 @@ async fn test_auth_password_login_flow_with_pkce() {
|
||||
email: email.clone(),
|
||||
password: password.clone(),
|
||||
response_type: Some("code".to_string()),
|
||||
redirect_to: Some(redirect_to.clone()),
|
||||
redirect_uri: Some(redirect_uri.clone()),
|
||||
pkce_code_challenge: None,
|
||||
..Default::default()
|
||||
}))
|
||||
@@ -201,7 +201,7 @@ async fn test_auth_password_login_flow_with_pkce() {
|
||||
email: email.clone(),
|
||||
password: password.clone(),
|
||||
response_type: Some("code".to_string()),
|
||||
redirect_to: None,
|
||||
redirect_uri: None,
|
||||
pkce_code_challenge: Some(pkce_code_challenge.as_str().to_string()),
|
||||
..Default::default()
|
||||
}))
|
||||
@@ -215,7 +215,7 @@ async fn test_auth_password_login_flow_with_pkce() {
|
||||
email: email.clone(),
|
||||
password: "WRONG PASSWORD".to_string(),
|
||||
response_type: Some("code".to_string()),
|
||||
redirect_to: Some(redirect_to.clone()),
|
||||
redirect_uri: Some(redirect_uri.clone()),
|
||||
pkce_code_challenge: Some(pkce_code_challenge.as_str().to_string()),
|
||||
..Default::default()
|
||||
}))
|
||||
@@ -228,7 +228,7 @@ async fn test_auth_password_login_flow_with_pkce() {
|
||||
email: email.clone(),
|
||||
password: password.clone(),
|
||||
response_type: Some("code".to_string()),
|
||||
redirect_to: Some(redirect_to.clone()),
|
||||
redirect_uri: Some(redirect_uri.clone()),
|
||||
pkce_code_challenge: Some(pkce_code_challenge.as_str().to_string()),
|
||||
..Default::default()
|
||||
}))
|
||||
@@ -586,7 +586,7 @@ async fn test_auth_change_email_flow() {
|
||||
let _ = change_email::change_email_confirm_handler(
|
||||
State(state.clone()),
|
||||
Path(email_verification_code.clone()),
|
||||
Query(ChangeEmailConfigQuery { redirect_to: None }),
|
||||
Query(ChangeEmailConfigQuery { redirect_uri: None }),
|
||||
user.clone(),
|
||||
)
|
||||
.await
|
||||
|
||||
@@ -8,15 +8,15 @@ use crate::auth::util::validate_redirect;
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize, Serialize, IntoParams, PartialEq)]
|
||||
pub(crate) struct LoginInputParams {
|
||||
pub redirect_to: Option<String>,
|
||||
pub redirect_uri: Option<String>,
|
||||
pub response_type: Option<String>,
|
||||
pub pkce_code_challenge: Option<String>,
|
||||
}
|
||||
|
||||
impl LoginInputParams {
|
||||
pub(crate) fn merge(mut self, other: LoginInputParams) -> LoginInputParams {
|
||||
if let Some(redirect_to) = other.redirect_to {
|
||||
self.redirect_to.get_or_insert(redirect_to);
|
||||
if let Some(redirect_uri) = other.redirect_uri {
|
||||
self.redirect_uri.get_or_insert(redirect_uri);
|
||||
}
|
||||
if let Some(response_type) = other.response_type {
|
||||
self.response_type.get_or_insert(response_type);
|
||||
@@ -31,10 +31,10 @@ impl LoginInputParams {
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub(crate) enum LoginParams {
|
||||
Password {
|
||||
redirect_to: Option<String>,
|
||||
redirect_uri: Option<String>,
|
||||
},
|
||||
AuthorizationCodeFlowWithPkce {
|
||||
redirect_to: String,
|
||||
redirect_uri: String,
|
||||
pkce_code_challenge: String,
|
||||
},
|
||||
}
|
||||
@@ -45,11 +45,11 @@ pub(crate) fn build_and_validate_input_params(
|
||||
) -> Result<LoginParams, AuthError> {
|
||||
return match params.response_type.as_deref() {
|
||||
Some("code") => {
|
||||
let Some(redirect_to) = params.redirect_to else {
|
||||
return Err(AuthError::BadRequest("missing 'redirect_to'"));
|
||||
let Some(redirect_uri) = params.redirect_uri else {
|
||||
return Err(AuthError::BadRequest("missing 'redirect_uri'"));
|
||||
};
|
||||
|
||||
validate_redirect(state, Some(&redirect_to))?;
|
||||
validate_redirect(state, Some(&redirect_uri))?;
|
||||
|
||||
let Some(pkce_code_challenge) = params.pkce_code_challenge else {
|
||||
return Err(AuthError::BadRequest("missing 'pkce_code_challenge'"));
|
||||
@@ -61,7 +61,7 @@ pub(crate) fn build_and_validate_input_params(
|
||||
.map_err(|_| AuthError::BadRequest("invalid 'pkce_code_challenge'"))?;
|
||||
|
||||
Ok(LoginParams::AuthorizationCodeFlowWithPkce {
|
||||
redirect_to,
|
||||
redirect_uri,
|
||||
pkce_code_challenge,
|
||||
})
|
||||
}
|
||||
@@ -73,10 +73,10 @@ pub(crate) fn build_and_validate_input_params(
|
||||
));
|
||||
}
|
||||
|
||||
validate_redirect(state, params.redirect_to.as_deref())?;
|
||||
validate_redirect(state, params.redirect_uri.as_deref())?;
|
||||
|
||||
Ok(LoginParams::Password {
|
||||
redirect_to: params.redirect_to,
|
||||
redirect_uri: params.redirect_uri,
|
||||
})
|
||||
}
|
||||
};
|
||||
@@ -93,13 +93,13 @@ mod tests {
|
||||
|
||||
assert_eq!(
|
||||
LoginParams::AuthorizationCodeFlowWithPkce {
|
||||
redirect_to: "/redirect".to_string(),
|
||||
redirect_uri: "/redirect".to_string(),
|
||||
pkce_code_challenge: BASE64_URL_SAFE.encode("challenge"),
|
||||
},
|
||||
build_and_validate_input_params(
|
||||
&state,
|
||||
LoginInputParams {
|
||||
redirect_to: Some("/redirect".to_string()),
|
||||
redirect_uri: Some("/redirect".to_string()),
|
||||
response_type: Some("code".to_string()),
|
||||
pkce_code_challenge: Some(BASE64_URL_SAFE.encode("challenge")),
|
||||
},
|
||||
@@ -109,12 +109,12 @@ mod tests {
|
||||
|
||||
assert_eq!(
|
||||
LoginParams::Password {
|
||||
redirect_to: Some("/redirect".to_string()),
|
||||
redirect_uri: Some("/redirect".to_string()),
|
||||
},
|
||||
build_and_validate_input_params(
|
||||
&state,
|
||||
LoginInputParams {
|
||||
redirect_to: Some("/redirect".to_string()),
|
||||
redirect_uri: Some("/redirect".to_string()),
|
||||
response_type: None,
|
||||
pkce_code_challenge: None,
|
||||
},
|
||||
@@ -126,7 +126,7 @@ mod tests {
|
||||
build_and_validate_input_params(
|
||||
&state,
|
||||
LoginInputParams {
|
||||
redirect_to: Some("invalid".to_string()),
|
||||
redirect_uri: Some("invalid".to_string()),
|
||||
response_type: None,
|
||||
pkce_code_challenge: None,
|
||||
},
|
||||
|
||||
@@ -59,7 +59,7 @@ pub(crate) async fn callback_from_external_auth_provider(
|
||||
pkce_code_verifier,
|
||||
user_pkce_code_challenge,
|
||||
response_type,
|
||||
redirect_to,
|
||||
redirect_uri,
|
||||
..
|
||||
} = state
|
||||
.jwt()
|
||||
@@ -80,7 +80,7 @@ pub(crate) async fn callback_from_external_auth_provider(
|
||||
}
|
||||
|
||||
// NOTE: This was already validated in the login-handler, we're just pedantic.
|
||||
validate_redirect(&state, redirect_to.as_deref())?;
|
||||
validate_redirect(&state, redirect_uri.as_deref())?;
|
||||
|
||||
return match response_type {
|
||||
Some(ResponseType::Code) => {
|
||||
@@ -88,7 +88,7 @@ pub(crate) async fn callback_from_external_auth_provider(
|
||||
&state,
|
||||
&cookies,
|
||||
provider,
|
||||
redirect_to,
|
||||
redirect_uri,
|
||||
query.code,
|
||||
pkce_code_verifier,
|
||||
user_pkce_code_challenge,
|
||||
@@ -100,7 +100,7 @@ pub(crate) async fn callback_from_external_auth_provider(
|
||||
&state,
|
||||
&cookies,
|
||||
provider,
|
||||
redirect_to,
|
||||
redirect_uri,
|
||||
query.code,
|
||||
pkce_code_verifier,
|
||||
)
|
||||
|
||||
@@ -53,17 +53,17 @@ pub(crate) async fn login_with_external_auth_provider(
|
||||
.url();
|
||||
|
||||
let oauth_state = match login_params {
|
||||
LoginParams::Password { redirect_to } => OAuthState {
|
||||
LoginParams::Password { redirect_uri } => OAuthState {
|
||||
// Set short-lived CSRF and PkceCodeVerifier cookies for the callback.
|
||||
exp: (chrono::Utc::now() + Duration::seconds(5 * 60)).timestamp(),
|
||||
csrf_secret: csrf_state.secret().to_string(),
|
||||
pkce_code_verifier: server_pkce_code_verifier.secret().to_string(),
|
||||
redirect_to,
|
||||
redirect_uri,
|
||||
response_type: None,
|
||||
user_pkce_code_challenge: None,
|
||||
},
|
||||
LoginParams::AuthorizationCodeFlowWithPkce {
|
||||
redirect_to,
|
||||
redirect_uri,
|
||||
pkce_code_challenge,
|
||||
} => OAuthState {
|
||||
// Set short-lived CSRF and PkceCodeVerifier cookies for the callback.
|
||||
@@ -72,7 +72,7 @@ pub(crate) async fn login_with_external_auth_provider(
|
||||
pkce_code_verifier: server_pkce_code_verifier.secret().to_string(),
|
||||
user_pkce_code_challenge: Some(pkce_code_challenge),
|
||||
response_type: Some(ResponseType::Code),
|
||||
redirect_to: Some(redirect_to),
|
||||
redirect_uri: Some(redirect_uri),
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -153,12 +153,12 @@ async fn test_oauth_login_flow_without_pkce() {
|
||||
// Call TB's OAuth login handler, which will produce a redirect for users to get the external
|
||||
// auth provider's login form.
|
||||
let cookies = Cookies::default();
|
||||
let redirect_to = format!("{site_url}/login-success-welcome");
|
||||
let redirect_uri = format!("{site_url}/login-success-welcome");
|
||||
let external_redirect: Redirect = login::login_with_external_auth_provider(
|
||||
State(state.clone()),
|
||||
Path(TestOAuthProvider::NAME.to_string()),
|
||||
Query(LoginInputParams {
|
||||
redirect_to: Some(redirect_to.to_string()),
|
||||
redirect_uri: Some(redirect_uri.to_string()),
|
||||
response_type: None,
|
||||
pkce_code_challenge: None,
|
||||
}),
|
||||
@@ -174,14 +174,14 @@ async fn test_oauth_login_flow_without_pkce() {
|
||||
.unwrap();
|
||||
|
||||
// Call the fake server's auth endpoint.
|
||||
let redirect_to_external_login = get_redirect_location(external_redirect).unwrap();
|
||||
let redirect_uri_external_login = get_redirect_location(external_redirect).unwrap();
|
||||
// NOTE: The dummy implementation just pipes the input query params through. We could do the
|
||||
// following assertions equally on `redirect_to_external_login`
|
||||
let redirect_to_external_login_url = url::Url::parse(&redirect_to_external_login).unwrap();
|
||||
// following assertions equally on `redirect_uri_external_login`
|
||||
let redirect_uri_external_login_url = url::Url::parse(&redirect_uri_external_login).unwrap();
|
||||
let query_params: HashMap<Cow<'_, str>, Cow<'_, str>> =
|
||||
redirect_to_external_login_url.query_pairs().collect();
|
||||
redirect_uri_external_login_url.query_pairs().collect();
|
||||
|
||||
let auth_query: AuthQuery = reqwest::get(&redirect_to_external_login)
|
||||
let auth_query: AuthQuery = reqwest::get(&redirect_uri_external_login)
|
||||
.await
|
||||
.unwrap()
|
||||
.json()
|
||||
@@ -219,7 +219,7 @@ async fn test_oauth_login_flow_without_pkce() {
|
||||
.unwrap();
|
||||
|
||||
let location = get_redirect_location(internal_redirect).unwrap();
|
||||
assert_eq!(location, redirect_to);
|
||||
assert_eq!(location, redirect_uri);
|
||||
|
||||
// Check user exists.
|
||||
let db_user = state
|
||||
@@ -256,13 +256,13 @@ async fn test_oauth_login_flow_with_pkce() {
|
||||
// Call TB's OAuth login handler, which will produce a redirect for users to get the external
|
||||
// auth provider's login form.
|
||||
let cookies = Cookies::default();
|
||||
let redirect_to = format!("{site_url}/login-success-welcome");
|
||||
let redirect_uri = format!("{site_url}/login-success-welcome");
|
||||
let (pkce_code_challenge, pkce_code_verifier) = oauth2::PkceCodeChallenge::new_random_sha256();
|
||||
let external_redirect: Redirect = login::login_with_external_auth_provider(
|
||||
State(state.clone()),
|
||||
Path(TestOAuthProvider::NAME.to_string()),
|
||||
Query(LoginInputParams {
|
||||
redirect_to: Some(redirect_to.to_string()),
|
||||
redirect_uri: Some(redirect_uri.to_string()),
|
||||
response_type: Some("code".to_string()),
|
||||
pkce_code_challenge: Some(pkce_code_challenge.as_str().to_string()),
|
||||
}),
|
||||
@@ -278,14 +278,14 @@ async fn test_oauth_login_flow_with_pkce() {
|
||||
.unwrap();
|
||||
|
||||
// Call the fake server's auth endpoint.
|
||||
let redirect_to_external_login = get_redirect_location(external_redirect).unwrap();
|
||||
let redirect_uri_external_login = get_redirect_location(external_redirect).unwrap();
|
||||
// NOTE: The dummy implementation just pipes the input query params through. We could do the
|
||||
// following assertions equally on `redirect_to_external_login`
|
||||
let redirect_to_external_login_url = url::Url::parse(&redirect_to_external_login).unwrap();
|
||||
// following assertions equally on `redirect_uri_external_login`
|
||||
let redirect_uri_external_login_url = url::Url::parse(&redirect_uri_external_login).unwrap();
|
||||
let query_params: HashMap<Cow<'_, str>, Cow<'_, str>> =
|
||||
redirect_to_external_login_url.query_pairs().collect();
|
||||
redirect_uri_external_login_url.query_pairs().collect();
|
||||
|
||||
let auth_query: AuthQuery = reqwest::get(&redirect_to_external_login)
|
||||
let auth_query: AuthQuery = reqwest::get(&redirect_uri_external_login)
|
||||
.await
|
||||
.unwrap()
|
||||
.json()
|
||||
@@ -324,7 +324,7 @@ async fn test_oauth_login_flow_with_pkce() {
|
||||
|
||||
let location_str = get_redirect_location(internal_redirect).unwrap();
|
||||
let location = url::Url::parse(&location_str).unwrap();
|
||||
assert!(location_str.starts_with(&format!("{redirect_to}?code=")));
|
||||
assert!(location_str.starts_with(&format!("{redirect_uri}?code=")));
|
||||
|
||||
let auth_code_re = Regex::new(r"^code=(.*)$").unwrap();
|
||||
let captures = auth_code_re.captures(&location.query().unwrap()).unwrap();
|
||||
|
||||
@@ -42,7 +42,7 @@ pub(crate) struct OAuthState {
|
||||
pub response_type: Option<ResponseType>,
|
||||
|
||||
/// Redirect target.
|
||||
pub redirect_to: Option<String>,
|
||||
pub redirect_uri: Option<String>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -60,7 +60,7 @@ mod tests {
|
||||
pkce_code_verifier: "server verifier".to_string(),
|
||||
user_pkce_code_challenge: Some("client challenge".to_string()),
|
||||
response_type: Some(ResponseType::Code),
|
||||
redirect_to: Some("custom-sheme://test".to_string()),
|
||||
redirect_uri: Some("custom-sheme://test".to_string()),
|
||||
};
|
||||
|
||||
let encoded = state.jwt().encode(&oauth_state).unwrap();
|
||||
|
||||
@@ -8,7 +8,7 @@ use serde::Deserialize;
|
||||
use trailbase_assets::AssetService;
|
||||
use trailbase_assets::auth::{
|
||||
ChangeEmailTemplate, ChangePasswordTemplate, LoginTemplate, OAuthProvider, RegisterTemplate,
|
||||
ResetPasswordRequestTemplate, ResetPasswordUpdateTemplate, hidden_input, redirect_to,
|
||||
ResetPasswordRequestTemplate, ResetPasswordUpdateTemplate, hidden_input, redirect_uri,
|
||||
};
|
||||
|
||||
use crate::AppState;
|
||||
@@ -20,7 +20,7 @@ pub(crate) const PROFILE_UI: &str = "/_/auth/profile";
|
||||
|
||||
#[derive(Debug, Default, Deserialize)]
|
||||
pub struct LoginQuery {
|
||||
redirect_to: Option<String>,
|
||||
redirect_uri: Option<String>,
|
||||
response_type: Option<String>,
|
||||
pkce_code_challenge: Option<String>,
|
||||
alert: Option<String>,
|
||||
@@ -31,7 +31,7 @@ async fn ui_login_handler(
|
||||
Query(query): Query<LoginQuery>,
|
||||
user: Option<User>,
|
||||
) -> Response {
|
||||
if query.redirect_to.is_none() && user.is_some() {
|
||||
if query.redirect_uri.is_none() && user.is_some() {
|
||||
// Already logged in. Only redirect to profile-page if no explicit other redirect is provided.
|
||||
// For example, if we're already logged in the browser but want to sign-in with the browser
|
||||
// from an app, we still have to go through the motions of signing in.
|
||||
@@ -58,20 +58,20 @@ async fn ui_login_handler(
|
||||
|
||||
let form_state = indoc::formatdoc!(
|
||||
r#"
|
||||
{redirect_to}
|
||||
{redirect_uri}
|
||||
{response_type}
|
||||
{pkce_code_challenge}
|
||||
"#,
|
||||
redirect_to = hidden_input("redirect_to", query.redirect_to.as_ref()),
|
||||
redirect_uri = hidden_input("redirect_uri", query.redirect_uri.as_ref()),
|
||||
response_type = hidden_input("response_type", query.response_type.as_ref()),
|
||||
pkce_code_challenge = hidden_input("pkce_code_challenge", query.pkce_code_challenge.as_ref()),
|
||||
);
|
||||
|
||||
let oauth_query_params: Vec<(&str, &str)> = [
|
||||
query
|
||||
.redirect_to
|
||||
.redirect_uri
|
||||
.as_ref()
|
||||
.map(|r| ("redirect_to", r.as_str())),
|
||||
.map(|r| ("redirect_uri", r.as_str())),
|
||||
query
|
||||
.response_type
|
||||
.as_ref()
|
||||
@@ -106,13 +106,13 @@ async fn ui_login_handler(
|
||||
|
||||
#[derive(Debug, Default, Deserialize)]
|
||||
pub struct RegisterQuery {
|
||||
redirect_to: Option<String>,
|
||||
redirect_uri: Option<String>,
|
||||
alert: Option<String>,
|
||||
}
|
||||
|
||||
async fn ui_register_handler(Query(query): Query<RegisterQuery>) -> Response {
|
||||
let html = RegisterTemplate {
|
||||
state: redirect_to(query.redirect_to.as_ref()),
|
||||
state: redirect_uri(query.redirect_uri.as_ref()),
|
||||
alert: query.alert.as_deref().unwrap_or_default(),
|
||||
}
|
||||
.render();
|
||||
@@ -129,7 +129,7 @@ async fn ui_register_handler(Query(query): Query<RegisterQuery>) -> Response {
|
||||
|
||||
#[derive(Debug, Default, Deserialize)]
|
||||
pub struct ResetPasswordRequestQuery {
|
||||
redirect_to: Option<String>,
|
||||
redirect_uri: Option<String>,
|
||||
alert: Option<String>,
|
||||
}
|
||||
|
||||
@@ -137,7 +137,7 @@ async fn ui_reset_password_request_handler(
|
||||
Query(query): Query<ResetPasswordRequestQuery>,
|
||||
) -> Response {
|
||||
let html = ResetPasswordRequestTemplate {
|
||||
state: redirect_to(query.redirect_to.as_ref()),
|
||||
state: redirect_uri(query.redirect_uri.as_ref()),
|
||||
alert: query.alert.as_deref().unwrap_or_default(),
|
||||
}
|
||||
.render();
|
||||
@@ -154,7 +154,7 @@ async fn ui_reset_password_request_handler(
|
||||
|
||||
#[derive(Debug, Default, Deserialize)]
|
||||
pub struct ResetPasswordUpdateQuery {
|
||||
redirect_to: Option<String>,
|
||||
redirect_uri: Option<String>,
|
||||
alert: Option<String>,
|
||||
}
|
||||
|
||||
@@ -162,7 +162,7 @@ async fn ui_reset_password_update_handler(
|
||||
Query(query): Query<ResetPasswordUpdateQuery>,
|
||||
) -> Response {
|
||||
let html = ResetPasswordUpdateTemplate {
|
||||
state: redirect_to(query.redirect_to.as_ref()),
|
||||
state: redirect_uri(query.redirect_uri.as_ref()),
|
||||
alert: query.alert.as_deref().unwrap_or_default(),
|
||||
}
|
||||
.render();
|
||||
@@ -179,13 +179,13 @@ async fn ui_reset_password_update_handler(
|
||||
|
||||
#[derive(Debug, Default, Deserialize)]
|
||||
pub struct ChangePasswordQuery {
|
||||
redirect_to: Option<String>,
|
||||
redirect_uri: Option<String>,
|
||||
alert: Option<String>,
|
||||
}
|
||||
|
||||
async fn ui_change_password_handler(Query(query): Query<ChangePasswordQuery>) -> Response {
|
||||
let html = ChangePasswordTemplate {
|
||||
state: redirect_to(query.redirect_to.as_ref()),
|
||||
state: redirect_uri(query.redirect_uri.as_ref()),
|
||||
alert: query.alert.as_deref().unwrap_or_default(),
|
||||
}
|
||||
.render();
|
||||
@@ -202,17 +202,17 @@ async fn ui_change_password_handler(Query(query): Query<ChangePasswordQuery>) ->
|
||||
|
||||
#[derive(Debug, Default, Deserialize)]
|
||||
pub struct ChangeEmailQuery {
|
||||
redirect_to: Option<String>,
|
||||
redirect_uri: Option<String>,
|
||||
alert: Option<String>,
|
||||
}
|
||||
|
||||
async fn ui_change_email_handler(Query(query): Query<ChangeEmailQuery>, user: User) -> Response {
|
||||
let form_state = indoc::formatdoc!(
|
||||
r#"
|
||||
{redirect_to}
|
||||
{redirect_uri}
|
||||
{csrf_token}
|
||||
"#,
|
||||
redirect_to = hidden_input("redirect_to", query.redirect_to.as_ref()),
|
||||
redirect_uri = hidden_input("redirect_uri", query.redirect_uri.as_ref()),
|
||||
csrf_token = hidden_input("csrf_token", Some(&user.csrf_token)),
|
||||
);
|
||||
|
||||
@@ -234,12 +234,12 @@ async fn ui_change_email_handler(Query(query): Query<ChangeEmailQuery>, user: Us
|
||||
|
||||
#[derive(Debug, Default, Deserialize)]
|
||||
pub struct LogoutQuery {
|
||||
redirect_to: Option<String>,
|
||||
redirect_uri: Option<String>,
|
||||
}
|
||||
|
||||
async fn ui_logout_handler(Query(query): Query<LogoutQuery>) -> Redirect {
|
||||
if let Some(redirect_to) = query.redirect_to {
|
||||
return Redirect::to(&format!("/api/auth/v1/logout?redirect_to={redirect_to}"));
|
||||
if let Some(redirect_uri) = query.redirect_uri {
|
||||
return Redirect::to(&format!("/api/auth/v1/logout?redirect_uri={redirect_uri}"));
|
||||
}
|
||||
return Redirect::to("/api/auth/v1/logout");
|
||||
}
|
||||
@@ -337,7 +337,7 @@ mod tests {
|
||||
let body = render_html(
|
||||
&state,
|
||||
LoginQuery {
|
||||
redirect_to: None,
|
||||
redirect_uri: None,
|
||||
response_type: None,
|
||||
pkce_code_challenge: None,
|
||||
alert: None,
|
||||
@@ -360,14 +360,14 @@ mod tests {
|
||||
|
||||
{
|
||||
// Parameters: all login parameters
|
||||
let redirect_to = format!("{site_url}/login-success-welcome");
|
||||
let redirect_uri = format!("{site_url}/login-success-welcome");
|
||||
let response_type = "code";
|
||||
let pkce_code_challenge = "challenge";
|
||||
|
||||
let body = render_html(
|
||||
&state,
|
||||
LoginQuery {
|
||||
redirect_to: Some(redirect_to.clone()),
|
||||
redirect_uri: Some(redirect_uri.clone()),
|
||||
response_type: Some(response_type.to_string()),
|
||||
pkce_code_challenge: Some(pkce_code_challenge.to_string()),
|
||||
alert: None,
|
||||
@@ -381,7 +381,7 @@ mod tests {
|
||||
let form_action = captures.get(1).unwrap();
|
||||
assert_eq!(format!("/{AUTH_API_PATH}/login"), form_action.as_str());
|
||||
|
||||
assert!(body.contains(&hidden_input("redirect_to", Some(&redirect_to))));
|
||||
assert!(body.contains(&hidden_input("redirect_uri", Some(&redirect_uri))));
|
||||
assert!(body.contains(&hidden_input("response_type", Some(response_type))));
|
||||
assert!(body.contains(&hidden_input(
|
||||
"pkce_code_challenge",
|
||||
@@ -403,8 +403,8 @@ mod tests {
|
||||
let query_params: HashMap<Cow<'_, str>, Cow<'_, str>> = url.query_pairs().collect();
|
||||
|
||||
assert_eq!(
|
||||
query_params.get("redirect_to").map(|s| &**s),
|
||||
Some(redirect_to.as_str()),
|
||||
query_params.get("redirect_uri").map(|s| &**s),
|
||||
Some(redirect_uri.as_str()),
|
||||
"href: {oauth_provider}"
|
||||
);
|
||||
assert_eq!(
|
||||
|
||||
@@ -98,16 +98,16 @@ fn validate_redirect_impl(
|
||||
/// Validates up to two redirects, typically from query parameter and/or request body.
|
||||
pub(crate) fn validate_redirect(
|
||||
state: &AppState,
|
||||
redirect_to: Option<&str>,
|
||||
redirect_uri: Option<&str>,
|
||||
) -> Result<(), AuthError> {
|
||||
if let Some(redirect_to) = redirect_to {
|
||||
if let Some(redirect_uri) = redirect_uri {
|
||||
let site: &Option<url::Url> = &state.site_url();
|
||||
let custom_uri_schemes = state.access_config(|c| c.auth.custom_uri_schemes.clone());
|
||||
|
||||
validate_redirect_impl(
|
||||
site.as_ref(),
|
||||
&custom_uri_schemes,
|
||||
redirect_to,
|
||||
redirect_uri,
|
||||
state.dev_mode(),
|
||||
)?;
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ pub struct CreateRecordQuery {
|
||||
/// This only exists to support insertions via static HTML form actions.
|
||||
///
|
||||
/// We may want to have a different on-error redirect to better support the static HTML use-case.
|
||||
pub redirect_to: Option<String>,
|
||||
pub redirect_uri: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, ToSchema)]
|
||||
@@ -187,8 +187,8 @@ pub async fn create_record_handler(
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(redirect_to) = create_record_query.redirect_to {
|
||||
return Ok(Redirect::to(&redirect_to).into_response());
|
||||
if let Some(redirect_uri) = create_record_query.redirect_uri {
|
||||
return Ok(Redirect::to(&redirect_uri).into_response());
|
||||
}
|
||||
|
||||
return Ok(Json(CreateRecordResponse { ids: record_ids }).into_response());
|
||||
|
||||
Reference in New Issue
Block a user