mirror of
https://github.com/dani-garcia/vaultwarden.git
synced 2026-07-09 17:25:59 -06:00
2026.6.0 send support (#7346)
* 2026.6.0 send support * Prevent creating and editing a Send with email verification * Review fixes --------- Co-authored-by: Timshel <timshel@users.noreply.github.com>
This commit is contained in:
parent
7320a1db4b
commit
5c5e8e1a6f
72
playwright/tests/send.spec.ts
Normal file
72
playwright/tests/send.spec.ts
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
import { test, expect, type Page, type TestInfo } from '@playwright/test';
|
||||||
|
import * as OTPAuth from "otpauth";
|
||||||
|
|
||||||
|
import * as utils from "../global-utils";
|
||||||
|
import { createAccount } from './setups/user';
|
||||||
|
|
||||||
|
let users = utils.loadEnv();
|
||||||
|
|
||||||
|
test.beforeAll('Setup', async ({ browser }, testInfo: TestInfo) => {
|
||||||
|
await utils.startVault(browser, testInfo, {});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.afterAll('Teardown', async ({}) => {
|
||||||
|
utils.stopVault();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Send', async ({ browser, page }) => {
|
||||||
|
await createAccount(test, page, users.user1);
|
||||||
|
|
||||||
|
const send_url = await test.step('Create', async () => {
|
||||||
|
await page.getByRole('link', { name: 'Send' }).click();
|
||||||
|
await expect(page.locator('#main-content').getByText('Send', { exact: true })).toBeVisible();
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'New', exact: true }).click();
|
||||||
|
await page.getByRole('menuitem', { name: 'Text' }).click();
|
||||||
|
|
||||||
|
await page.getByRole('textbox', { name: 'Send name (required)' }).fill('Test');
|
||||||
|
await page.getByRole('textbox', { name: 'Text to share (required)' }).fill('test');
|
||||||
|
await page.getByRole('button', { name: 'Save' }).click();
|
||||||
|
|
||||||
|
await page.locator('footer').getByRole('button', { name: 'Copy link' }).click();
|
||||||
|
|
||||||
|
return await page.evaluate(() => navigator.clipboard.readText());
|
||||||
|
});
|
||||||
|
|
||||||
|
const context2 = await browser.newContext();
|
||||||
|
const page2 = await context2.newPage();
|
||||||
|
|
||||||
|
await test.step('View', async () => {
|
||||||
|
await page2.goto(send_url, { waitUntil: 'domcontentloaded' });
|
||||||
|
await expect(page2.getByRole('heading', { name: 'View Send' })).toBeVisible();
|
||||||
|
await expect(await page2.getByRole('paragraph').filter({ hasText: 'Test' })).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
const pwd_url = await test.step('Create with password', async () => {
|
||||||
|
await page.getByRole('link', { name: 'Send' }).click();
|
||||||
|
await expect(page.locator('#main-content').getByText('Send', { exact: true })).toBeVisible();
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'New', exact: true }).click();
|
||||||
|
await page.getByRole('menuitem', { name: 'Text' }).click();
|
||||||
|
|
||||||
|
await page.getByRole('textbox', { name: 'Send name (required)' }).fill('Password');
|
||||||
|
await page.getByRole('textbox', { name: 'Text to share (required)' }).fill('password');
|
||||||
|
await page.getByRole('combobox', { name: 'Who can view' }).click();
|
||||||
|
await page.getByText('Anyone with a password set by you').click();
|
||||||
|
await page.getByRole('textbox', { name: 'Password (required)' }).fill('password');
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Save' }).click();
|
||||||
|
await page.locator('footer').getByRole('button', { name: 'Copy link' }).click();
|
||||||
|
|
||||||
|
return await page.evaluate(() => navigator.clipboard.readText());
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step('View with password', async () => {
|
||||||
|
await page2.goto(pwd_url, { waitUntil: 'domcontentloaded' });
|
||||||
|
await expect(page2.getByRole('heading', { name: 'Enter the password to view' })).toBeVisible();
|
||||||
|
await page2.getByRole('textbox', { name: 'Password (required)' }).fill('password');
|
||||||
|
await page2.getByRole('button', { name: 'Continue' }).click();
|
||||||
|
await expect(page2.getByRole('heading', { name: 'View Send' })).toBeVisible();
|
||||||
|
await expect(await page2.getByRole('paragraph').filter({ hasText: 'Password' })).toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -12,7 +12,7 @@ use serde_json::Value;
|
|||||||
use crate::{
|
use crate::{
|
||||||
CONFIG,
|
CONFIG,
|
||||||
api::{ApiResult, EmptyResult, JsonResult, Notify, UpdateType},
|
api::{ApiResult, EmptyResult, JsonResult, Notify, UpdateType},
|
||||||
auth::{ClientIp, Headers, Host},
|
auth::{ClientIp, Headers, Host, SendHeaders},
|
||||||
config::PathType,
|
config::PathType,
|
||||||
db::{
|
db::{
|
||||||
DbConn, DbPool,
|
DbConn, DbPool,
|
||||||
@ -48,7 +48,9 @@ pub fn routes() -> Vec<rocket::Route> {
|
|||||||
post_send,
|
post_send,
|
||||||
post_send_file,
|
post_send_file,
|
||||||
post_access,
|
post_access,
|
||||||
|
post_access_legacy,
|
||||||
post_access_file,
|
post_access_file,
|
||||||
|
post_access_file_legacy,
|
||||||
put_send,
|
put_send,
|
||||||
delete_send,
|
delete_send,
|
||||||
put_remove_password,
|
put_remove_password,
|
||||||
@ -78,6 +80,7 @@ pub struct SendData {
|
|||||||
deletion_date: DateTime<Utc>,
|
deletion_date: DateTime<Utc>,
|
||||||
disabled: bool,
|
disabled: bool,
|
||||||
hide_email: Option<bool>,
|
hide_email: Option<bool>,
|
||||||
|
emails: Option<String>,
|
||||||
|
|
||||||
// Data field
|
// Data field
|
||||||
name: String,
|
name: String,
|
||||||
@ -148,6 +151,10 @@ fn create_send(data: SendData, user_id: UserId) -> ApiResult<Send> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if data.emails.is_some() {
|
||||||
|
err!("Sends with email verification is not supported");
|
||||||
|
}
|
||||||
|
|
||||||
let mut send = Send::new(data.r#type, data.name, data_str, data.key, data.deletion_date.naive_utc());
|
let mut send = Send::new(data.r#type, data.name, data_str, data.key, data.deletion_date.naive_utc());
|
||||||
send.user_uuid = Some(user_id);
|
send.user_uuid = Some(user_id);
|
||||||
send.notes = data.notes;
|
send.notes = data.notes;
|
||||||
@ -371,7 +378,7 @@ pub struct SendFileData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Api/Tools/Controllers/SendsController.cs#L195
|
// https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Api/Tools/Controllers/SendsController.cs#L195
|
||||||
#[post("/sends/<send_id>/file/<file_id>", format = "multipart/form-data", data = "<data>")]
|
#[post("/sends/<send_id>/file/<file_id>", format = "multipart/form-data", data = "<data>", rank = 2)]
|
||||||
async fn post_send_file_v2_data(
|
async fn post_send_file_v2_data(
|
||||||
send_id: SendId,
|
send_id: SendId,
|
||||||
file_id: SendFileId,
|
file_id: SendFileId,
|
||||||
@ -441,14 +448,23 @@ async fn post_send_file_v2_data(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[post("/sends/access")]
|
||||||
|
async fn post_access(headers: SendHeaders, conn: DbConn, nt: Notify<'_>) -> JsonResult {
|
||||||
|
let Some(send) = Send::find_by_uuid(&headers.send_id, &conn).await else {
|
||||||
|
err_code!(SEND_INACCESSIBLE_MSG, 404)
|
||||||
|
};
|
||||||
|
process_access(send, conn, nt).await
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct SendAccessData {
|
pub struct SendAccessData {
|
||||||
pub password: Option<String>,
|
pub password: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Legacy since web-2026.6.0
|
||||||
#[post("/sends/access/<access_id>", data = "<data>")]
|
#[post("/sends/access/<access_id>", data = "<data>")]
|
||||||
async fn post_access(
|
async fn post_access_legacy(
|
||||||
access_id: &str,
|
access_id: &str,
|
||||||
data: Json<SendAccessData>,
|
data: Json<SendAccessData>,
|
||||||
conn: DbConn,
|
conn: DbConn,
|
||||||
@ -494,6 +510,10 @@ async fn post_access(
|
|||||||
|
|
||||||
send.save(&conn).await?;
|
send.save(&conn).await?;
|
||||||
|
|
||||||
|
process_access(send, conn, nt).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn process_access(send: Send, conn: DbConn, nt: Notify<'_>) -> JsonResult {
|
||||||
nt.send_send_update(
|
nt.send_send_update(
|
||||||
UpdateType::SyncSendUpdate,
|
UpdateType::SyncSendUpdate,
|
||||||
&send,
|
&send,
|
||||||
@ -506,8 +526,23 @@ async fn post_access(
|
|||||||
Ok(Json(send.to_json_access(&conn).await))
|
Ok(Json(send.to_json_access(&conn).await))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/sends/<send_id>/access/file/<file_id>", data = "<data>")]
|
#[post("/sends/access/file/<file_id>", rank = 1)]
|
||||||
async fn post_access_file(
|
async fn post_access_file(
|
||||||
|
file_id: SendFileId,
|
||||||
|
headers: SendHeaders,
|
||||||
|
host: Host,
|
||||||
|
conn: DbConn,
|
||||||
|
nt: Notify<'_>,
|
||||||
|
) -> JsonResult {
|
||||||
|
let Some(send) = Send::find_by_uuid(&headers.send_id, &conn).await else {
|
||||||
|
err_code!(SEND_INACCESSIBLE_MSG, 404)
|
||||||
|
};
|
||||||
|
process_access_file(send, file_id, host, conn, nt).await
|
||||||
|
}
|
||||||
|
|
||||||
|
// Legacy since web-2026.6.0
|
||||||
|
#[post("/sends/<send_id>/access/file/<file_id>", data = "<data>")]
|
||||||
|
async fn post_access_file_legacy(
|
||||||
send_id: SendId,
|
send_id: SendId,
|
||||||
file_id: SendFileId,
|
file_id: SendFileId,
|
||||||
data: Json<SendAccessData>,
|
data: Json<SendAccessData>,
|
||||||
@ -551,6 +586,10 @@ async fn post_access_file(
|
|||||||
|
|
||||||
send.save(&conn).await?;
|
send.save(&conn).await?;
|
||||||
|
|
||||||
|
process_access_file(send, file_id, host, conn, nt).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn process_access_file(send: Send, file_id: SendFileId, host: Host, conn: DbConn, nt: Notify<'_>) -> JsonResult {
|
||||||
nt.send_send_update(
|
nt.send_send_update(
|
||||||
UpdateType::SyncSendUpdate,
|
UpdateType::SyncSendUpdate,
|
||||||
&send,
|
&send,
|
||||||
@ -563,7 +602,7 @@ async fn post_access_file(
|
|||||||
Ok(Json(json!({
|
Ok(Json(json!({
|
||||||
"object": "send-fileDownload",
|
"object": "send-fileDownload",
|
||||||
"id": file_id,
|
"id": file_id,
|
||||||
"url": download_url(&host, &send_id, &file_id).await?,
|
"url": download_url(&host, &send.uuid, &file_id).await?,
|
||||||
})))
|
})))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -601,6 +640,10 @@ async fn put_send(send_id: SendId, data: Json<SendData>, headers: Headers, conn:
|
|||||||
err!("Send not found", "Send send_id is invalid or does not belong to user")
|
err!("Send not found", "Send send_id is invalid or does not belong to user")
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if data.emails.is_some() {
|
||||||
|
err!("Sends with email verification is not supported");
|
||||||
|
}
|
||||||
|
|
||||||
update_send_from_data(&mut send, data, &headers, &conn, &nt, UpdateType::SyncSendUpdate).await?;
|
update_send_from_data(&mut send, data, &headers, &conn, &nt, UpdateType::SyncSendUpdate).await?;
|
||||||
|
|
||||||
Ok(Json(send.to_json()))
|
Ok(Json(send.to_json()))
|
||||||
|
|||||||
@ -31,8 +31,8 @@ use crate::{
|
|||||||
DbConn,
|
DbConn,
|
||||||
models::{
|
models::{
|
||||||
AuthRequest, AuthRequestId, Device, DeviceId, EventType, Invitation, OIDCCodeResponseError,
|
AuthRequest, AuthRequestId, Device, DeviceId, EventType, Invitation, OIDCCodeResponseError,
|
||||||
OrganizationApiKey, OrganizationId, SsoAuth, SsoUser, TwoFactor, TwoFactorIncomplete, TwoFactorType, User,
|
OrganizationApiKey, OrganizationId, SendId, SsoAuth, SsoUser, TwoFactor, TwoFactorIncomplete,
|
||||||
UserId,
|
TwoFactorType, User, UserId,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
error::MapResult,
|
error::MapResult,
|
||||||
@ -108,6 +108,19 @@ async fn login(
|
|||||||
sso_login(data, &mut user_id, &conn, &client_header.ip, client_version.as_ref()).await
|
sso_login(data, &mut user_id, &conn, &client_header.ip, client_version.as_ref()).await
|
||||||
}
|
}
|
||||||
"authorization_code" => err!("SSO sign-in is not available"),
|
"authorization_code" => err!("SSO sign-in is not available"),
|
||||||
|
"send_access" => {
|
||||||
|
check_is_some(data.client_id.as_ref(), "client_id cannot be blank")?;
|
||||||
|
check_is_some(data.send_id.as_ref(), "send_id cannot be blank")?;
|
||||||
|
|
||||||
|
let tokens = auth::SendTokens::generate_tokens(
|
||||||
|
data.send_id.as_ref().unwrap(),
|
||||||
|
data.password_hash_b64,
|
||||||
|
&client_header.ip,
|
||||||
|
&conn,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
Ok(Json(tokens.to_json()))
|
||||||
|
}
|
||||||
t => err!("Invalid type", t),
|
t => err!("Invalid type", t),
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -1144,6 +1157,10 @@ struct ConnectData {
|
|||||||
code: Option<OIDCCode>,
|
code: Option<OIDCCode>,
|
||||||
#[field(name = uncased("code_verifier"))]
|
#[field(name = uncased("code_verifier"))]
|
||||||
code_verifier: Option<OIDCCodeVerifier>,
|
code_verifier: Option<OIDCCodeVerifier>,
|
||||||
|
|
||||||
|
// Needed for send access
|
||||||
|
send_id: Option<SendId>,
|
||||||
|
password_hash_b64: Option<String>,
|
||||||
}
|
}
|
||||||
fn check_is_some<T>(value: Option<&T>, msg: &str) -> EmptyResult {
|
fn check_is_some<T>(value: Option<&T>, msg: &str) -> EmptyResult {
|
||||||
if value.is_none() {
|
if value.is_none() {
|
||||||
|
|||||||
15
src/auth.rs
15
src/auth.rs
@ -1,3 +1,8 @@
|
|||||||
|
#[path = "auth/send.rs"]
|
||||||
|
pub mod send;
|
||||||
|
pub type SendTokens = send::SendTokens;
|
||||||
|
pub type SendHeaders = send::SendHeaders;
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
env,
|
env,
|
||||||
net::IpAddr,
|
net::IpAddr,
|
||||||
@ -487,6 +492,16 @@ pub struct BasicJwtClaims {
|
|||||||
pub sub: String,
|
pub sub: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl BasicJwtClaims {
|
||||||
|
pub fn expires_in(&self) -> i64 {
|
||||||
|
self.exp - Utc::now().timestamp()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn token(&self) -> String {
|
||||||
|
encode_jwt(&self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn generate_delete_claims(uuid: String) -> BasicJwtClaims {
|
pub fn generate_delete_claims(uuid: String) -> BasicJwtClaims {
|
||||||
let time_now = Utc::now();
|
let time_now = Utc::now();
|
||||||
let expire_hours = i64::from(CONFIG.invitation_expiration_hours());
|
let expire_hours = i64::from(CONFIG.invitation_expiration_hours());
|
||||||
|
|||||||
156
src/auth/send.rs
Normal file
156
src/auth/send.rs
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
use chrono::{TimeDelta, Utc};
|
||||||
|
|
||||||
|
use rocket::request::{FromRequest, Outcome, Request};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
api::ApiResult,
|
||||||
|
auth,
|
||||||
|
auth::{BasicJwtClaims, ClientIp},
|
||||||
|
db::{
|
||||||
|
DbConn,
|
||||||
|
models::{Send, SendId},
|
||||||
|
},
|
||||||
|
error::{Error, ErrorKind},
|
||||||
|
};
|
||||||
|
|
||||||
|
fn generate_send_access_claims(send_id: &SendId) -> BasicJwtClaims {
|
||||||
|
let time_now = Utc::now();
|
||||||
|
BasicJwtClaims {
|
||||||
|
nbf: time_now.timestamp(),
|
||||||
|
exp: (time_now + TimeDelta::try_minutes(2).unwrap()).timestamp(),
|
||||||
|
iss: auth::JWT_SEND_ISSUER.to_string(),
|
||||||
|
sub: format!("{send_id}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct SendTokens {
|
||||||
|
pub access_claims: BasicJwtClaims,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SendTokens {
|
||||||
|
pub fn as_send_id(access_id: &str) -> Option<SendId> {
|
||||||
|
data_encoding::BASE64URL_NOPAD
|
||||||
|
.decode(access_id.as_bytes())
|
||||||
|
.ok()
|
||||||
|
.and_then(|uuid_vec| uuid::Uuid::from_slice(&uuid_vec).ok().map(|u| SendId::from(u.to_string())))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_json(&self) -> serde_json::Value {
|
||||||
|
json!({
|
||||||
|
"access_token": self.access_claims.token(),
|
||||||
|
"expires_in": self.access_claims.expires_in(),
|
||||||
|
"token_type": "Bearer",
|
||||||
|
"scope": "api.send.access",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn expected_error(msg: &str, error_type: &str) -> ApiResult<SendTokens> {
|
||||||
|
let err = json!({
|
||||||
|
"kind": "expected_server",
|
||||||
|
"error": "invalid_request",
|
||||||
|
"send_access_error_type": error_type,
|
||||||
|
});
|
||||||
|
|
||||||
|
Err(Error::new_msg(msg).with_kind(ErrorKind::Json(err)).silent())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn invalid_error(msg: &str, error_type: &str, silent: bool) -> ApiResult<SendTokens> {
|
||||||
|
let err = json!({
|
||||||
|
"kind": "expected_server",
|
||||||
|
"error": "invalid_grant",
|
||||||
|
"send_access_error_type": error_type,
|
||||||
|
});
|
||||||
|
|
||||||
|
Err(Error::new_msg(msg).with_kind(ErrorKind::Json(err)).with_code(404).with_silent(silent))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn generate_tokens(
|
||||||
|
access_id: &str,
|
||||||
|
password: Option<String>,
|
||||||
|
ip: &ClientIp,
|
||||||
|
conn: &DbConn,
|
||||||
|
) -> ApiResult<SendTokens> {
|
||||||
|
let Some(send_id) = Self::as_send_id(access_id) else {
|
||||||
|
return Self::invalid_error(&format!("Can't convert {access_id}"), "send_id_invalid", false);
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(mut send) = Send::find_by_uuid(&send_id, conn).await else {
|
||||||
|
return Self::invalid_error(&format!("Can't find {send_id}"), "send_id_invalid", false);
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(max_access_count) = send.max_access_count
|
||||||
|
&& send.access_count >= max_access_count
|
||||||
|
{
|
||||||
|
return Self::invalid_error(&format!("Send {send_id}, max access reached"), "send_id_invalid", true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(expiration) = send.expiration_date
|
||||||
|
&& Utc::now().naive_utc() >= expiration
|
||||||
|
{
|
||||||
|
return Self::invalid_error(&format!("Send {send_id}, expired"), "send_id_invalid", true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if Utc::now().naive_utc() >= send.deletion_date {
|
||||||
|
return Self::invalid_error(&format!("Send {send_id}, past deletion"), "send_id_invalid", true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if send.disabled {
|
||||||
|
return Self::invalid_error(&format!("Send {send_id}, disabled"), "send_id_invalid", true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if send.password_hash.is_some() {
|
||||||
|
match password {
|
||||||
|
Some(ref p) if send.check_password(p) => { /* Nothing to do here */ }
|
||||||
|
Some(_) => {
|
||||||
|
return Self::invalid_error(
|
||||||
|
&format!("Send {send_id}, Invalid password from {}", ip.ip),
|
||||||
|
"password_hash_b64_invalid",
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
None => return Self::expected_error("Password required", "password_hash_b64_required"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
send.access_count += 1;
|
||||||
|
send.save(conn).await?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
access_claims: generate_send_access_claims(&send_id),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct SendHeaders {
|
||||||
|
pub send_id: SendId,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rocket::async_trait]
|
||||||
|
impl<'r> FromRequest<'r> for SendHeaders {
|
||||||
|
type Error = &'static str;
|
||||||
|
|
||||||
|
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
|
||||||
|
let headers = request.headers();
|
||||||
|
|
||||||
|
// Get access_token
|
||||||
|
let access_token: &str = if let Some(a) = headers.get_one("Authorization") {
|
||||||
|
if let Some(split) = a.rsplit("Bearer ").next() {
|
||||||
|
split
|
||||||
|
} else {
|
||||||
|
err_handler!("No access token provided")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
err_handler!("No access token provided")
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check JWT token is valid and get send_id
|
||||||
|
let Ok(claims) = auth::decode_send(access_token) else {
|
||||||
|
err_handler!("Invalid claim")
|
||||||
|
};
|
||||||
|
|
||||||
|
Outcome::Success(SendHeaders {
|
||||||
|
send_id: claims.sub.into(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
24
src/error.rs
24
src/error.rs
@ -15,14 +15,14 @@ macro_rules! make_error {
|
|||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct ErrorEvent { pub event: EventType }
|
pub struct ErrorEvent { pub event: EventType }
|
||||||
pub struct Error { message: String, kind: ErrorKind, code: u16, event: Option<ErrorEvent> }
|
pub struct Error { message: String, kind: ErrorKind, code: u16, event: Option<ErrorEvent>, silent: bool }
|
||||||
|
|
||||||
$(impl From<$ty> for Error {
|
$(impl From<$ty> for Error {
|
||||||
fn from(err: $ty) -> Self { Error::from((stringify!($name), err)) }
|
fn from(err: $ty) -> Self { Error::from((stringify!($name), err)) }
|
||||||
})+
|
})+
|
||||||
$(impl<S: Into<String>> From<(S, $ty)> for Error {
|
$(impl<S: Into<String>> From<(S, $ty)> for Error {
|
||||||
fn from(val: (S, $ty)) -> Self {
|
fn from(val: (S, $ty)) -> Self {
|
||||||
Error { message: val.0.into(), kind: ErrorKind::$name(val.1), code: BAD_REQUEST, event: None }
|
Error { message: val.0.into(), kind: ErrorKind::$name(val.1), code: BAD_REQUEST, event: None, silent: false }
|
||||||
}
|
}
|
||||||
})+
|
})+
|
||||||
impl StdError for Error {
|
impl StdError for Error {
|
||||||
@ -172,6 +172,18 @@ impl Error {
|
|||||||
pub fn message(&self) -> &str {
|
pub fn message(&self) -> &str {
|
||||||
&self.message
|
&self.message
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn silent(mut self) -> Self {
|
||||||
|
self.silent = true;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn with_silent(mut self, silent: bool) -> Self {
|
||||||
|
self.silent = silent;
|
||||||
|
self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait MapResult<S> {
|
pub trait MapResult<S> {
|
||||||
@ -309,9 +321,11 @@ use rocket::{
|
|||||||
|
|
||||||
impl Responder<'_, 'static> for Error {
|
impl Responder<'_, 'static> for Error {
|
||||||
fn respond_to(self, _: &Request<'_>) -> response::Result<'static> {
|
fn respond_to(self, _: &Request<'_>) -> response::Result<'static> {
|
||||||
match self.kind {
|
if !self.silent {
|
||||||
ErrorKind::Empty(_) | ErrorKind::Simple(_) | ErrorKind::Compact(_) => {} // Don't print the error in this situation
|
match self.kind {
|
||||||
_ => error!(target: "error", "{self:#?}"),
|
ErrorKind::Empty(_) | ErrorKind::Simple(_) | ErrorKind::Compact(_) => {} // Don't print the error in this situation
|
||||||
|
_ => error!(target: "error", "{self:#?}"),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let code = Status::from_code(self.code).unwrap_or(Status::BadRequest);
|
let code = Status::from_code(self.code).unwrap_or(Status::BadRequest);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user