Browse Source

[WIP-unstable-branch] Use async branch of Rocket.

undefined
Jeb Rosen 2 months ago
parent
commit
141e531f54
15 changed files with 1096 additions and 829 deletions
  1. +910
    -669
      Cargo.lock
  2. +2
    -2
      sirus-api/Cargo.toml
  3. +1
    -1
      sirus-api/src/error.rs
  4. +4
    -4
      sirus-api/src/password.rs
  5. +7
    -6
      sirus-server/Cargo.toml
  6. +4
    -5
      sirus-server/src/content.rs
  7. +22
    -20
      sirus-server/src/guards.rs
  8. +2
    -3
      sirus-server/src/main.rs
  9. +2
    -2
      sirus-server/src/routes/admin.rs
  10. +4
    -4
      sirus-server/src/routes/catchers.rs
  11. +2
    -2
      sirus-server/src/routes/site.rs
  12. +1
    -1
      sirus-server/src/templates/mod.rs
  13. +24
    -21
      sirus-server/src/utils/etag.rs
  14. +28
    -16
      sirus-server/src/utils/limited_data.rs
  15. +83
    -73
      sirus-server/src/utils/oauth.rs

+ 910
- 669
Cargo.lock
File diff suppressed because it is too large
View File


+ 2
- 2
sirus-api/Cargo.toml View File

@@ -12,8 +12,8 @@ backtraces = []
base64 = "0.10"
chrono = "0.4"
log = "0.4"
ring = { version = "0.14", default-features = false }
ring = { version = "0.16", default-features = false }

# For trait impls
postgres-shared = { version = "0.4.2", default-features = false, optional = true }
rocket = { git = "https://github.com/jebrosen/Rocket", branch="master", optional = true }
rocket = { git = "https://github.com/jebrosen/Rocket", branch="async", optional = true }

+ 1
- 1
sirus-api/src/error.rs View File

@@ -126,7 +126,7 @@ impl From<postgres_shared::error::Error> for Error {

#[cfg(feature = "rocket")]
impl<'r> rocket::response::Responder<'r> for Error {
fn respond_to(self, req: &rocket::request::Request<'_>) -> rocket::response::Result<'r> {
fn respond_to(self, req: &'r rocket::request::Request<'_>) -> rocket::response::ResultFuture<'r> {
rocket::response::Debug(self).respond_to(req)
}
}


+ 4
- 4
sirus-api/src/password.rs View File

@@ -3,7 +3,6 @@ use std::str::FromStr;

use base64;

use ring::digest;
use ring::pbkdf2;
use ring::rand::{SecureRandom, SystemRandom};

@@ -221,7 +220,8 @@ fn base64_encode<T: ?Sized + AsRef<[u8]>>(input: &T) -> String {
}

const SALT_LEN: usize = 16;
const DIGEST_LEN: usize = digest::SHA256_OUTPUT_LEN;
static DIGEST: pbkdf2::Algorithm = pbkdf2::PBKDF2_HMAC_SHA256;
const DIGEST_LEN: usize = ring::digest::SHA256_OUTPUT_LEN;
const ITERATIONS: u32 = 100_000;
pub fn hash(password: &str) -> String {
let iters = ITERATIONS;
@@ -232,7 +232,7 @@ pub fn hash(password: &str) -> String {
.fill(&mut salt)
.expect("generate random salt");
pbkdf2::derive(
&digest::SHA256,
DIGEST,
std::num::NonZeroU32::new(iters).expect("iterations is not zero"),
&salt,
password.as_bytes(),
@@ -268,7 +268,7 @@ pub fn verify(attempt: &str, hash_str: &str) -> bool {
let salt = base64::decode(&hashed.salt?).ok()?;
let digest = base64::decode(&hashed.hash?).ok()?;

pbkdf2::verify(&digest::SHA256, iters, &salt, attempt.as_bytes(), &digest).ok()
pbkdf2::verify(DIGEST, iters, &salt, attempt.as_bytes(), &digest).ok()
}

_verify(attempt, hash_str).is_some()


+ 7
- 6
sirus-server/Cargo.toml View File

@@ -14,12 +14,13 @@ sirus-api = { path = "../sirus-api", features = ["rocket"] }
# web server / core
maud = "0.21"
quick-xml = "0.16.1"
rocket = { git = "https://github.com/jebrosen/Rocket", branch="master" }
rocket = { git = "https://github.com/jebrosen/Rocket", branch="async" }
tokio = "0.2.0"

# auth
hyper = "0.10"
hyper-sync-rustls = "0.3.0-rc.3"
rocket_oauth2 = { git = "https://git.jebrosen.com/jeb/rocket_oauth2", branch = "experimental" }
hyper = "0.13.0"
hyper-rustls = { git = "https://github.com/jebrosen/hyper-rustls", branch = "for-rocket_oauth2" }
rocket_oauth2 = { git = "https://git.jebrosen.com/jeb/rocket_oauth2", branch = "async" }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

@@ -35,6 +36,6 @@ sirus-postgres = { path = "../sirus-postgres", optional = true }

[dependencies.rocket_contrib]
git = "https://github.com/jebrosen/Rocket"
branch = "master"
branch = "async"
default_features = false
features = ["compression", "databases", "helmet"]
features = ["databases", "helmet"]

+ 4
- 5
sirus-server/src/content.rs View File

@@ -1,8 +1,7 @@
use std::ffi::OsStr;
use std::path::Path;

use rocket::http::hyper::header::{CacheControl, CacheDirective};
use rocket::http::ContentType;
use rocket::http::{ContentType, Header};
use rocket::Responder;

const CACHE_TIME_STATIC: u32 = 7 * 24 * 3600; // one week
@@ -18,7 +17,7 @@ enum ContentBytes {
#[derive(Responder)]
pub struct ContentFile {
data: ContentBytes,
cache: CacheControl,
cache: Header<'static>,
content_type: ContentType,
}

@@ -33,7 +32,7 @@ impl ContentFile {
pub fn known(bytes: &'static [u8], path: &Path) -> ContentFile {
ContentFile {
data: ContentBytes::Borrowed(bytes),
cache: CacheControl(vec![CacheDirective::MaxAge(CACHE_TIME_STATIC)]),
cache: Header::new("Cache-Control", format!("max-age={}", CACHE_TIME_STATIC)),
content_type: content_type_from_path(path),
}
}
@@ -41,7 +40,7 @@ impl ContentFile {
pub fn dynamic(bytes: Vec<u8>, path: &Path) -> ContentFile {
ContentFile {
data: ContentBytes::Owned(bytes),
cache: CacheControl(vec![CacheDirective::MaxAge(CACHE_TIME_DYNAMIC)]),
cache: Header::new("Cache-Control", format!("max-age={}", CACHE_TIME_DYNAMIC)),
content_type: content_type_from_path(path),
}
}


+ 22
- 20
sirus-server/src/guards.rs View File

@@ -1,6 +1,6 @@
use rocket::http::Cookies;
use rocket::request::{self, FromRequest};
use rocket::{Outcome, Request};
use rocket::request::{self, FromRequest, FromRequestAsync, FromRequestFuture};
use rocket::{try_outcome, Outcome, Request};

use crate::storage::Connection;
use crate::AUTH_TIMEOUT;
@@ -27,30 +27,32 @@ impl<'a> AdminExt for Option<Admin> {
}
}

impl FromRequest<'_, '_> for Admin {
impl<'a, 'r> FromRequestAsync<'a, 'r> for Admin {
type Error = ();

fn from_request(request: &Request<'_>) -> request::Outcome<Self, Self::Error> {
let mut cookies = request.guard::<Cookies<'_>>().expect("request cookies");
if let Some(cookie) = cookies.get_private("session") {
let mut split = cookie.value().split(';');
if let Some(username) = split.next() {
let conn = rocket::try_outcome!(request.guard::<Connection>());
if let Ok(Some(User {
username,
is_admin: true,
})) = conn.get_user(username)
{
if let Some(timestamp) = split.next().and_then(|s| s.parse::<i64>().ok()) {
if Timestamp::now().to_i64() - timestamp < AUTH_TIMEOUT {
return Outcome::Success(Admin(ApiAdmin::new_unchecked(username)));
fn from_request(request: &'a Request<'r>) -> FromRequestFuture<'a, Self, Self::Error> {
Box::pin(async move {
let mut cookies = request.guard::<Cookies<'_>>().expect("request cookies");
if let Some(cookie) = cookies.get_private("session") {
let mut split = cookie.value().split(';');
if let Some(username) = split.next() {
let conn = try_outcome!(Connection::from_request(request).await);
if let Ok(Some(User {
username,
is_admin: true,
})) = conn.get_user(username)
{
if let Some(timestamp) = split.next().and_then(|s| s.parse::<i64>().ok()) {
if Timestamp::now().to_i64() - timestamp < AUTH_TIMEOUT {
return Outcome::Success(Admin(ApiAdmin::new_unchecked(username)));
}
}
}
}
}
};
};

Outcome::Forward(())
Outcome::Forward(())
})
}
}



+ 2
- 3
sirus-server/src/main.rs View File

@@ -19,7 +19,6 @@ mod utils {
}

use rocket::fairing::AdHoc;
use rocket_contrib::compression::Compression;
use rocket_contrib::helmet;

use crate::storage::{Connection, NextConnection};
@@ -80,7 +79,6 @@ fn main() {
}))
.attach(utils::oauth::fairing())
.attach(utils::etag::ETag::fairing())
.attach(Compression::fairing())
.attach(
helmet::SpaceHelmet::new()
.enable(helmet::NoSniff::Enable)
@@ -88,5 +86,6 @@ fn main() {
.enable(helmet::XssFilter::EnableBlock)
.enable(helmet::Referrer::StrictOriginWhenCrossOrigin),
)
.launch();
.launch()
.expect("server exited unexpectedly");
}

+ 2
- 2
sirus-server/src/routes/admin.rs View File

@@ -243,7 +243,7 @@ fn add_user(
}

#[put("/upload/<path..>", data = "<data>", rank = 1)]
fn admin_upload_file(
async fn admin_upload_file(
admin: Admin,
conn: Connection,
upload_limit: ContentUploadLimit,
@@ -252,7 +252,7 @@ fn admin_upload_file(
) -> Result<(), Error> {
let path_str = path.to_str().ok_or_else(|| Error::invalid_input("path"))?;
assert!(upload_limit.0 < usize::max_value() as u64);
let bytes = data.read_all(upload_limit.0 as usize)?;
let bytes = data.read_all(upload_limit.0 as usize).await?;
conn.upsert_content(
&admin,
NewContent {


+ 4
- 4
sirus-server/src/routes/catchers.rs View File

@@ -1,5 +1,5 @@
use rocket::catch;
use rocket::request::Request;
use rocket::request::{FromRequestAsync, Request};

use crate::storage::Connection;
use crate::templates::{err_404, SitePage, E500};
@@ -9,9 +9,9 @@ pub fn catchers() -> Vec<::rocket::Catcher> {
}

#[catch(404)]
pub fn e404(req: &Request<'_>) -> SitePage {
let pagetree = req
.guard::<Connection>()
pub async fn e404(req: &Request<'_>) -> SitePage {
let pagetree = Connection::from_request(req)
.await
.succeeded()
.and_then(|conn| conn.get_pagetree(None).ok())
.unwrap_or_default();


+ 2
- 2
sirus-server/src/routes/site.rs View File

@@ -22,10 +22,10 @@ pub fn index(conn: Connection, admin: Option<Admin>) -> Result<Custom<SitePage>,
}

#[get("/static/<path..>")]
pub fn static_file(
pub fn static_file<'r>(
path: std::path::PathBuf,
conn: Connection,
) -> Result<Option<impl Responder<'static>>, Error> {
) -> Result<Option<impl Responder<'r>>, Error> {
let path_str = path.to_string_lossy();

Ok(match conn.get_content(&path_str)? {


+ 1
- 1
sirus-server/src/templates/mod.rs View File

@@ -72,7 +72,7 @@ impl E500 {
macro_rules! impl_maud_responders {
($($types:ident),* $(,)*) => {
$(impl<'r> rocket::response::Responder<'r> for $types {
fn respond_to(self, req: &rocket::Request<'_>) -> Result<rocket::Response<'r>, rocket::http::Status> {
fn respond_to(self, req: &'r rocket::Request<'_>) -> rocket::response::ResultFuture<'r> {
rocket::response::Content(rocket::http::ContentType::HTML, self.render().into_string()).respond_to(req)
}
})*


+ 24
- 21
sirus-server/src/utils/etag.rs View File

@@ -1,11 +1,9 @@
use std::io::Cursor;
use std::str::FromStr;

use etag::EntityTag;

use rocket::fairing::{AdHoc, Fairing};
use rocket::http::hyper::header;
use rocket::http::{Method, Status};
use rocket::http::{Method, Status, Header};
use rocket::request::Request;
use rocket::response::{Body, Responder, Response};

@@ -38,14 +36,16 @@ impl ETag<()> {
#[inline]
pub fn fairing_with_max_length(max_length: u64) -> impl Fairing {
AdHoc::on_response("ETag", move |req, res| {
ETag::apply_etag(req, res, max_length);
ETag::handle_conditional_get(req, res);
Box::pin(async move {
ETag::apply_etag(req, res, max_length).await;
ETag::handle_conditional_get(req, res);
})
})
}

/// Calculates the ETag for `response` if it is shorter than `max_length`,
/// and stores it in the ETag header.
fn apply_etag(request: &Request<'_>, response: &mut Response<'_>, max_length: u64) {
async fn apply_etag(request: &Request<'_>, response: &mut Response<'_>, max_length: u64) {
// Ignore responses that already have an ETag.
if response.headers().contains("ETag") {
return;
@@ -66,12 +66,13 @@ impl ETag<()> {
// Pull the body out of the response to hash it.
let body_bytes = response
.body_bytes()
.await
.expect("body already known to be Some");
let etag = EntityTag::from_hash(&body_bytes);

// Set the ETag header on the response.
let calculated_tag = header::EntityTag::new(true, etag.tag().to_string());
response.set_header(header::ETag(calculated_tag));
let calculated_tag = format!("W/\"{}\"", etag.tag());
response.set_header(Header::new("ETag", calculated_tag[2..].to_string()));

// Restore the response body.
response.set_sized_body(Cursor::new(body_bytes));
@@ -90,7 +91,6 @@ impl ETag<()> {
let etag = match response
.headers()
.get_one("ETag")
.and_then(|s| s.parse().ok())
{
Some(e) => e,
None => return,
@@ -98,12 +98,13 @@ impl ETag<()> {

// Check for an If-None-Match header.
let matched = match request.headers().get_one("If-None-Match") {
Some(match_str) => match_str.split(',').map(str::trim).any(|val| {
// Compare the requested ETag(s) against the response's ETag.
// '*' is not supported because it is not useful for GET or HEAD requests.
let match_tag = header::EntityTag::from_str(val);
match_tag.map(|mt| mt.weak_eq(&etag)).unwrap_or(false)
}),
Some(match_str) => {
match_str.split(',').map(str::trim).any(|val| {
// Compare the requested ETag(s) against the response's ETag.
// '*' is not supported because it is not useful for GET or HEAD requests.
(val == etag || val == &etag[2..])
})
},
None => false,
};

@@ -135,11 +136,13 @@ impl<'r, R: Responder<'r>> ETag<R> {
}
}

impl<'r, R: Responder<'r>> Responder<'r> for ETag<R> {
fn respond_to(self, request: &Request<'_>) -> rocket::response::Result<'r> {
let mut response = self.responder.respond_to(request)?;
ETag::apply_etag(request, &mut response, self.max_length);
ETag::handle_conditional_get(request, &mut response);
Ok(response)
impl<'r, R: Responder<'r> + Send + 'r> Responder<'r> for ETag<R> {
fn respond_to(self, request: &'r Request<'_>) -> rocket::response::ResultFuture<'r> {
Box::pin(async move {
let mut response = self.responder.respond_to(request).await?;
ETag::apply_etag(request, &mut response, self.max_length).await;
ETag::handle_conditional_get(request, &mut response);
Ok(response)
})
}
}

+ 28
- 16
sirus-server/src/utils/limited_data.rs View File

@@ -1,17 +1,21 @@
use std::io::{self, ErrorKind, Read};
use std::io::{self, ErrorKind};
use std::pin::Pin;

use rocket::data::{self, Data, FromDataSimple};
use tokio::io::{AsyncRead, AsyncReadExt};
use std::task::{Context, Poll};

use rocket::data::{Data, FromDataFuture, FromDataSimple};
use rocket::request::Request;
use rocket::Outcome;

/// LimitedReader reads a limited number of bytes of a data stream. Unlike
/// [Read::take], this type returns an error when too much data has been read.
/// LimitedReader reads a limited number of bytes of a data stream.
/// This type returns an error when too much data has been read.
struct LimitedReader<R> {
inner: R,
remaining: usize,
}

impl<R: Read> LimitedReader<R> {
impl<R> LimitedReader<R> {
fn new(inner: R, limit: usize) -> Self {
LimitedReader {
inner,
@@ -20,11 +24,19 @@ impl<R: Read> LimitedReader<R> {
}
}

impl<R: Read> Read for LimitedReader<R> {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
let count = self.inner.read(buf)?;
impl<R: AsyncRead + Unpin> AsyncRead for LimitedReader<R> {
fn poll_read(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut [u8],
) -> Poll<io::Result<usize>> {
let count = match Pin::new(&mut self.inner).poll_read(cx, buf) {
Poll::Pending => return Poll::Pending,
Poll::Ready(Err(e)) => return Poll::Ready(Err(e)),
Poll::Ready(Ok(count)) => count,
};

match self.remaining.checked_sub(count) {
Poll::Ready(match self.remaining.checked_sub(count) {
Some(r) => {
self.remaining = r;
Ok(count)
@@ -33,7 +45,7 @@ impl<R: Read> Read for LimitedReader<R> {
ErrorKind::InvalidData,
LimitedDataError::TooBig,
)),
}
})
}
}

@@ -45,13 +57,13 @@ pub struct LimitedData {
}

impl LimitedData {
/// Returns a `Read` adaptor over the data stream. The `Read`
/// Returns an `AsyncRead` adaptor over the data stream. The `AsyncRead`
/// implementation will return an [io::Error] if more than `max` bytes were
/// provided.
///
/// The Content-Length header, if present, is used as a hint for the buffer
/// size and is checked against `max` before data is read.
pub fn into_read(self, max: usize) -> Result<impl Read, io::Error> {
pub fn into_read(self, max: usize) -> Result<impl AsyncRead, io::Error> {
if let Some(size) = self.size {
if size > max {
return Err(io::Error::new(
@@ -69,10 +81,10 @@ impl LimitedData {
///
/// The Content-Length header, if present, is used as a hint for the buffer
/// size and is checked against `max` before data is read.
pub fn read_all(self, max: usize) -> Result<Vec<u8>, io::Error> {
pub async fn read_all(self, max: usize) -> io::Result<Vec<u8>> {
let mut reader = self.into_read(max)?;
let mut bytes = Vec::with_capacity(max);
io::copy(&mut reader, &mut bytes)?;
reader.read_to_end(&mut bytes).await?;
Ok(bytes)
}
}
@@ -80,13 +92,13 @@ impl LimitedData {
impl FromDataSimple for LimitedData {
type Error = ();

fn from_data(req: &Request<'_>, data: Data) -> data::Outcome<Self, Self::Error> {
fn from_data(req: &Request<'_>, data: Data) -> FromDataFuture<'static, Self, Self::Error> {
let size = req
.headers()
.get_one("Content-Length")
.and_then(|h| h.parse::<usize>().ok());

Outcome::Success(LimitedData { size, data })
Box::pin(async move { Outcome::Success(LimitedData { size, data }) })
}
}



+ 83
- 73
sirus-server/src/utils/oauth.rs View File

@@ -1,16 +1,11 @@
use std::io::Read;

use hyper::{
header::{Accept, Authorization, Bearer, Headers},
net::HttpsConnector,
Client,
};
use hyper::Client;
use log::error;
use rocket::fairing::{AdHoc, Fairing};
use rocket::http::{Cookie, Cookies, Status};
use rocket::request::{Request, State};
use rocket::request::{FromRequestAsync, Request, State};
use rocket::response::{Responder, ResultFuture};
use rocket::uri;
use rocket_oauth2::hyper_sync_rustls_adapter::HyperSyncRustlsAdapter;
use rocket_oauth2::hyper_rustls_adapter::HyperRustlsAdapter;
use rocket_oauth2::{OAuth2, TokenResponse};
use serde::Deserialize;

@@ -30,7 +25,7 @@ pub fn fairing() -> impl Fairing {
};

Ok(rocket.manage(nextcloud_root).attach(OAuth2::fairing(
HyperSyncRustlsAdapter,
HyperRustlsAdapter,
nextcloud_auth_callback,
"nextcloud",
"/admin/auth/nextcloud",
@@ -52,68 +47,83 @@ struct OcsData {
id: String,
}

fn nextcloud_auth_callback(request: &Request<'_>, token: TokenResponse) -> AuthResult {
let nc_root = request
.guard::<State<'_, NextcloudRoot>>()
.expect("nextcloud root");

let https = HttpsConnector::new(hyper_sync_rustls::TlsClient::new());

let client = Client::with_connector(https);

let url = format!("{}/ocs/v1.php/cloud/user", nc_root.0);
let mut headers = Headers::new();
headers.set(Accept::json());
headers.set(Authorization(Bearer {
token: token.access_token().to_string(),
}));
headers.set_raw("OCS-APIRequest", vec![b"true".to_vec()]);

let req = client.get(&url).headers(headers);

let response = match req.send() {
Ok(res) => res,
Err(e) => {
error!("Error retrieving user details: {}", e);
return AuthResult::error(Status::InternalServerError);
}
};

let cloud_user_res = serde_json::from_reader(response.take(2 * 1024 * 1024));

let user_id = match cloud_user_res {
Ok(cur @ CloudUserResponse { .. }) => cur.ocs.data.id,
Err(e) => {
error!("Error parsing user details response: {}", e);
return AuthResult::error(Status::InternalServerError);
}
};

let conn = match request.guard::<Connection>().succeeded() {
Some(c) => c,
None => {
error!("Error connecting to database to look up user");
return AuthResult::error(Status::ServiceUnavailable);
}
};

let username = match conn.get_external_user("nextcloud", &user_id) {
Ok(Some(User {
username,
is_admin: true,
})) => username,
_ => {
return AuthResult::invalid(uri!(
"/admin",
crate::routes::admin::login_get: dest = _
fn nextcloud_auth_callback<'r>(
request: &'r Request<'_>,
token: TokenResponse,
) -> ResultFuture<'r> {
Box::pin(async move {
async {
let nc_root = request
.guard::<State<'_, NextcloudRoot>>()
.expect("nextcloud root");

let https = hyper_rustls::HttpsConnector::new();

let client = Client::builder().build(https);

let url = format!("{}/ocs/v1.php/cloud/user", nc_root.0);

let req = hyper::Request::get(&url)
.header("Accept", "application/json")
.header("Authorization", format!("Bearer {}", token.access_token()))
.header("OCS-APIRequest", "true")
.body(hyper::Body::empty())
.expect("valid request");

let response = match client.request(req).await {
Ok(res) => res,
Err(e) => {
error!("Error retrieving user details: {}", e);
return AuthResult::error(Status::InternalServerError);
}
};

// TODO: Unlimited
let body = match hyper::body::to_bytes(response.into_body()).await {
Ok(bytes) => bytes,
Err(e) => {
error!("Error retrieveing body: {}", e);
return AuthResult::error(Status::InternalServerError);
}
};

let cloud_user_res = serde_json::from_slice(&body);

let user_id = match cloud_user_res {
Ok(cur @ CloudUserResponse { .. }) => cur.ocs.data.id,
Err(e) => {
error!("Error parsing user details response: {}", e);
return AuthResult::error(Status::InternalServerError);
}
};

let conn = match Connection::from_request(request).await.succeeded() {
Some(c) => c,
None => {
error!("Error connecting to database to look up user");
return AuthResult::error(Status::ServiceUnavailable);
}
};

let username = match conn.get_external_user("nextcloud", &user_id) {
Ok(Some(User {
username,
is_admin: true,
})) => username,
_ => {
return AuthResult::invalid(uri!(
"/admin",
crate::routes::admin::login_get: dest = _
));
}
};

let mut cookies = request.guard::<Cookies<'_>>().expect("request cookies");
cookies.add_private(Cookie::new(
"session",
format!("{};{}", username, Timestamp::now().to_i64()),
));
}
};

let mut cookies = request.guard::<Cookies<'_>>().expect("request cookies");
cookies.add_private(Cookie::new(
"session",
format!("{};{}", username, Timestamp::now().to_i64()),
));
AuthResult::success(uri!(crate::routes::site::index))
AuthResult::success(uri!(crate::routes::site::index))
}.await.respond_to(request).await
})
}

Loading…
Cancel
Save