estrogen for vencord
This commit is contained in:
commit
a3dab56ddb
7 changed files with 2484 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
/target
|
||||||
|
.envrc
|
1909
Cargo.lock
generated
Normal file
1909
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
29
Cargo.toml
Normal file
29
Cargo.toml
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
[package]
|
||||||
|
name = "e4vc"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow = "1.0.72"
|
||||||
|
axum = { version = "0.6.20", features = ["http2", "headers", "macros"] }
|
||||||
|
base64 = "0.21.2"
|
||||||
|
bb8 = "0.8.1"
|
||||||
|
bb8-redis = "0.13.1"
|
||||||
|
blake3 = "1.4.1"
|
||||||
|
hex = "0.4.3"
|
||||||
|
hyper = "0.14.27"
|
||||||
|
lazy_static = "1.4.0"
|
||||||
|
redis = { version = "0.23.2", features = ["tokio-comp"] }
|
||||||
|
reqwest = { version = "0.11.18", default-features = false, features = ["rustls-tls", "json"] }
|
||||||
|
sentry = { version = "0.31.5", default-features = false, features = ["backtrace", "contexts", "debug-images", "panic", "reqwest", "rustls", "tracing", "anyhow", "tower", "tower-http"] }
|
||||||
|
sentry-tracing = "*"
|
||||||
|
sentry-tower = "*"
|
||||||
|
serde = { version = "1.0.183", features = ["derive"] }
|
||||||
|
serde_json = "1.0.104"
|
||||||
|
time = "0.3.25"
|
||||||
|
tokio = { version = "1.31.0", features = ["macros", "rt-multi-thread"] }
|
||||||
|
tower-http = { version = "0.4.3", features = ["cors"] }
|
||||||
|
tracing = "0.1.37"
|
||||||
|
tracing-subscriber = "0.3.17"
|
24
flake.lock
Normal file
24
flake.lock
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1691421349,
|
||||||
|
"narHash": "sha256-RRJyX0CUrs4uW4gMhd/X4rcDG8PTgaaCQM5rXEJOx6g=",
|
||||||
|
"path": "/nix/store/24jqzdczdyg04kkgd71wvydc9yxr5ndh-source",
|
||||||
|
"rev": "011567f35433879aae5024fc6ec53f2a0568a6c4",
|
||||||
|
"type": "path"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"id": "nixpkgs",
|
||||||
|
"type": "indirect"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": "nixpkgs"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
60
flake.nix
Normal file
60
flake.nix
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
{
|
||||||
|
inputs.nixpkgs.url = "nixpkgs";
|
||||||
|
|
||||||
|
outputs = {
|
||||||
|
self,
|
||||||
|
nixpkgs,
|
||||||
|
...
|
||||||
|
}: let
|
||||||
|
version = builtins.substring 0 7 self.lastModifiedDate;
|
||||||
|
|
||||||
|
systems = [
|
||||||
|
"x86_64-linux"
|
||||||
|
"aarch64-linux"
|
||||||
|
"x86_64-darwin"
|
||||||
|
"aarch64-darwin"
|
||||||
|
];
|
||||||
|
|
||||||
|
forAllSystems = nixpkgs.lib.genAttrs systems;
|
||||||
|
nixpkgsFor = forAllSystems (system: import nixpkgs {inherit system;});
|
||||||
|
|
||||||
|
packageFn = pkgs:
|
||||||
|
pkgs.rustPlatform.buildRustPackage {
|
||||||
|
pname = "e4vc";
|
||||||
|
inherit version;
|
||||||
|
|
||||||
|
src = builtins.path {
|
||||||
|
name = "source";
|
||||||
|
path = ./.;
|
||||||
|
};
|
||||||
|
|
||||||
|
cargoSha256 = "sha256-6aA95Zm1FGp8xiv55QSBZFfmBXWz5pygc/8uQHvy3tM=";
|
||||||
|
};
|
||||||
|
in rec {
|
||||||
|
packages = forAllSystems (s: let
|
||||||
|
pkgs = nixpkgsFor.${s};
|
||||||
|
in rec {
|
||||||
|
e4vc = packageFn pkgs;
|
||||||
|
default = e4vc;
|
||||||
|
});
|
||||||
|
|
||||||
|
devShells = forAllSystems (s: let
|
||||||
|
pkgs = nixpkgsFor.${s};
|
||||||
|
inherit (pkgs) mkShell;
|
||||||
|
in {
|
||||||
|
default = mkShell {
|
||||||
|
packages = with pkgs; [rustc cargo rustfmt];
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
dockerImage = forAllSystems (s: let
|
||||||
|
pkgs = nixpkgsFor.${s};
|
||||||
|
in pkgs.dockerTools.buildImage {
|
||||||
|
name = "e4vc";
|
||||||
|
tag = "latest";
|
||||||
|
config = {
|
||||||
|
Cmd = ["${packageFn pkgs}/bin/e4vc"];
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
14
fly.toml
Normal file
14
fly.toml
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
#:schema https://json.schemastore.org/fly.json
|
||||||
|
|
||||||
|
app = "e4vc"
|
||||||
|
primary_region = "nrt"
|
||||||
|
|
||||||
|
[build]
|
||||||
|
image = "e4vc:latest"
|
||||||
|
|
||||||
|
[http_service]
|
||||||
|
internal_port = 8080
|
||||||
|
force_https = true
|
||||||
|
auto_stop_machines = true
|
||||||
|
auto_start_machines = true
|
||||||
|
min_machines_running = 0
|
446
src/main.rs
Normal file
446
src/main.rs
Normal file
|
@ -0,0 +1,446 @@
|
||||||
|
use std::fmt::Debug;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use axum::{
|
||||||
|
async_trait,
|
||||||
|
extract::{FromRef, FromRequestParts, Query, RawBody, State},
|
||||||
|
http::{header::AUTHORIZATION, request::Parts, HeaderValue, StatusCode},
|
||||||
|
response::{IntoResponse, Response},
|
||||||
|
routing::{delete, get, post},
|
||||||
|
Json, Router,
|
||||||
|
};
|
||||||
|
use base64::{engine::general_purpose as base64_engines, Engine};
|
||||||
|
use bb8::Pool;
|
||||||
|
use bb8_redis::RedisConnectionManager;
|
||||||
|
use hex::FromHex;
|
||||||
|
use hyper::{
|
||||||
|
body::to_bytes,
|
||||||
|
header::{CONTENT_TYPE, ETAG, IF_NONE_MATCH},
|
||||||
|
Method,
|
||||||
|
};
|
||||||
|
use serde::Deserialize;
|
||||||
|
use serde_json::json;
|
||||||
|
use time::OffsetDateTime;
|
||||||
|
use tower_http::cors::CorsLayer;
|
||||||
|
use tracing::{error, instrument, trace};
|
||||||
|
use tracing_subscriber::prelude::*;
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
#[repr(transparent)]
|
||||||
|
struct VencloudianIfNoneMatch(pub Option<u64>);
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl<S> FromRequestParts<S> for VencloudianIfNoneMatch
|
||||||
|
where
|
||||||
|
S: Send + Sync + Debug,
|
||||||
|
{
|
||||||
|
type Rejection = (StatusCode, Json<serde_json::Value>);
|
||||||
|
|
||||||
|
#[instrument]
|
||||||
|
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
|
||||||
|
Ok(Self(
|
||||||
|
parts
|
||||||
|
.headers
|
||||||
|
.get(IF_NONE_MATCH)
|
||||||
|
.and_then(|header| header.to_str().ok())
|
||||||
|
.and_then(|header| header.parse().ok()),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
#[repr(transparent)]
|
||||||
|
struct AuthenticatedUser(pub u64);
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl<S> FromRequestParts<S> for AuthenticatedUser
|
||||||
|
where
|
||||||
|
S: Send + Sync + Debug,
|
||||||
|
{
|
||||||
|
type Rejection = (StatusCode, Json<serde_json::Value>);
|
||||||
|
|
||||||
|
#[instrument]
|
||||||
|
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
|
||||||
|
if let Some(authorization) = parts.headers.get(AUTHORIZATION) {
|
||||||
|
let authorization = base64_engines::STANDARD.decode(authorization).or(Err((
|
||||||
|
StatusCode::UNAUTHORIZED,
|
||||||
|
Json(json!({
|
||||||
|
"error": "Unauthorized"
|
||||||
|
})),
|
||||||
|
)))?;
|
||||||
|
let authorization = String::from_utf8(authorization).or(Err((
|
||||||
|
StatusCode::UNAUTHORIZED,
|
||||||
|
Json(json!({
|
||||||
|
"error": "Unauthorized"
|
||||||
|
})),
|
||||||
|
)))?;
|
||||||
|
let (mac, user_id) = authorization.split_once(':').ok_or((
|
||||||
|
StatusCode::UNAUTHORIZED,
|
||||||
|
Json(json!({
|
||||||
|
"error": "Unauthorized"
|
||||||
|
})),
|
||||||
|
))?;
|
||||||
|
let mac: [u8; 32] = base64_engines::STANDARD_NO_PAD
|
||||||
|
.decode(mac)
|
||||||
|
.or(Err((
|
||||||
|
StatusCode::UNAUTHORIZED,
|
||||||
|
Json(json!({
|
||||||
|
"error": "Unauthorized"
|
||||||
|
})),
|
||||||
|
)))?
|
||||||
|
.try_into()
|
||||||
|
.or(Err((
|
||||||
|
StatusCode::UNAUTHORIZED,
|
||||||
|
Json(json!({
|
||||||
|
"error": "Unauthorized"
|
||||||
|
})),
|
||||||
|
)))?;
|
||||||
|
let user_id: u64 = user_id.parse().or(Err((
|
||||||
|
StatusCode::UNAUTHORIZED,
|
||||||
|
Json(json!({
|
||||||
|
"error": "Unauthorized"
|
||||||
|
})),
|
||||||
|
)))?;
|
||||||
|
let correct_mac = blake3::keyed_hash(&MAC_SECRET, &user_id.to_le_bytes());
|
||||||
|
if correct_mac == mac {
|
||||||
|
trace!("Authentication successful for: {}", user_id);
|
||||||
|
Ok(Self(user_id))
|
||||||
|
} else {
|
||||||
|
Err((
|
||||||
|
StatusCode::UNAUTHORIZED,
|
||||||
|
Json(json!({
|
||||||
|
"error": "Unauthorized"
|
||||||
|
})),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Err((
|
||||||
|
StatusCode::UNAUTHORIZED,
|
||||||
|
Json(json!({
|
||||||
|
"error": "Unauthorized"
|
||||||
|
})),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct RedisConnection(bb8::PooledConnection<'static, RedisConnectionManager>);
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl FromRequestParts<AppState> for RedisConnection {
|
||||||
|
type Rejection = WrappedError;
|
||||||
|
|
||||||
|
#[instrument]
|
||||||
|
async fn from_request_parts(
|
||||||
|
parts: &mut Parts,
|
||||||
|
state: &AppState,
|
||||||
|
) -> Result<Self, Self::Rejection> {
|
||||||
|
Ok(Self(state.redis_conn_pool.get_owned().await?))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct WrappedError(anyhow::Error);
|
||||||
|
|
||||||
|
impl IntoResponse for WrappedError {
|
||||||
|
fn into_response(self) -> Response {
|
||||||
|
error!("{:?}", self.0);
|
||||||
|
(
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
Json(json!({
|
||||||
|
"error": "Internal Error"
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<E> From<E> for WrappedError
|
||||||
|
where
|
||||||
|
E: Into<anyhow::Error>,
|
||||||
|
{
|
||||||
|
fn from(value: E) -> Self {
|
||||||
|
Self(value.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lazy_static::lazy_static! {
|
||||||
|
static ref DISCORD_ORIGINS: [HeaderValue; 3] = [
|
||||||
|
"https://discord.com".parse().unwrap(),
|
||||||
|
"https://ptb.discord.com".parse().unwrap(),
|
||||||
|
"https://canary.discord.com".parse().unwrap(),
|
||||||
|
];
|
||||||
|
static ref DISCORD_CLIENT_ID: String = std::env::var("E4VC_DISCORD_CLIENT_ID")
|
||||||
|
.expect("environment variable E4VC_DISCORD_CLIENT_ID should be set");
|
||||||
|
static ref DISCORD_CLIENT_SECRET: String = std::env::var("E4VC_DISCORD_CLIENT_SECRET")
|
||||||
|
.expect("environment variable E4VC_DISCORD_CLIENT_SECRET should be set");
|
||||||
|
static ref DISCORD_REDIRECT_URL: String = std::env::var("E4VC_DISCORD_REDIRECT_URL")
|
||||||
|
.expect("environment variable E4VC_DISCORD_REDIRECT_URL should be set");
|
||||||
|
static ref MAC_SECRET: [u8; 32] = <[u8; 32]>::from_hex(
|
||||||
|
std::env::var("E4VC_MAC_SECRET").expect("environment variable E4VC_MAC_SECRET should be set"),
|
||||||
|
)
|
||||||
|
.expect("environment variable E4VC_MAC_SECRET should be 32 hex-encoded bytes");
|
||||||
|
}
|
||||||
|
|
||||||
|
const DISCORD_OAUTH_TOKEN_URL: &str = "https://discord.com/api/oauth2/token";
|
||||||
|
const DISCORD_SELF_INFO_URL: &str = "https://discord.com/api/users/@me";
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, FromRef)]
|
||||||
|
struct AppState {
|
||||||
|
http_client: reqwest::Client,
|
||||||
|
redis_conn_pool: Pool<RedisConnectionManager>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> anyhow::Result<()> {
|
||||||
|
let _guard = sentry::init(
|
||||||
|
std::env::var("E4VC_SENTRY_URL")
|
||||||
|
.expect("environment variable E4VC_SENTRY_URL should be set"),
|
||||||
|
);
|
||||||
|
|
||||||
|
tracing_subscriber::registry()
|
||||||
|
.with(tracing_subscriber::fmt::layer())
|
||||||
|
.with(sentry_tracing::layer())
|
||||||
|
.init();
|
||||||
|
|
||||||
|
let client = RedisConnectionManager::new(
|
||||||
|
std::env::var("E4VC_REDIS_URL").expect("environment variable E4VC_REDIS_URL should be set"),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let pool = bb8::Pool::builder().build(client).await?;
|
||||||
|
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
|
||||||
|
let app = Router::new()
|
||||||
|
.route("/", post(|| async { "Hello!" }))
|
||||||
|
.route(
|
||||||
|
"/v1",
|
||||||
|
get(|| async { Json(json!({ "ping": "pong" })) }).layer(
|
||||||
|
CorsLayer::new()
|
||||||
|
.allow_origin(DISCORD_ORIGINS.clone())
|
||||||
|
.allow_methods([Method::GET, Method::HEAD])
|
||||||
|
.allow_headers([AUTHORIZATION]),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/v1/",
|
||||||
|
delete(delete_data).layer(
|
||||||
|
CorsLayer::new()
|
||||||
|
.allow_origin(DISCORD_ORIGINS.clone())
|
||||||
|
.allow_methods([Method::DELETE])
|
||||||
|
.allow_headers([AUTHORIZATION]),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/v1/oauth/settings",
|
||||||
|
get(|| async {
|
||||||
|
Json(json!({
|
||||||
|
"clientId": *DISCORD_CLIENT_ID,
|
||||||
|
"redirectUri": *DISCORD_REDIRECT_URL
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
.layer(
|
||||||
|
CorsLayer::new()
|
||||||
|
.allow_origin(DISCORD_ORIGINS.clone())
|
||||||
|
.allow_methods([Method::GET, Method::HEAD])
|
||||||
|
.allow_headers([AUTHORIZATION]),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/v1/oauth/callback",
|
||||||
|
get(handle_discord_callback).layer(
|
||||||
|
CorsLayer::new()
|
||||||
|
.allow_origin(DISCORD_ORIGINS.clone())
|
||||||
|
.allow_methods([Method::GET])
|
||||||
|
.allow_headers([AUTHORIZATION]),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/v1/settings",
|
||||||
|
get(get_data)
|
||||||
|
.head(get_data_head)
|
||||||
|
.put(set_data)
|
||||||
|
.delete(delete_data)
|
||||||
|
.layer(
|
||||||
|
CorsLayer::new()
|
||||||
|
.allow_origin(DISCORD_ORIGINS.clone())
|
||||||
|
.allow_methods([Method::GET, Method::HEAD, Method::PUT, Method::DELETE])
|
||||||
|
.allow_headers([AUTHORIZATION, CONTENT_TYPE, IF_NONE_MATCH]),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.layer(sentry_tower::NewSentryLayer::<_>::new_from_top())
|
||||||
|
.layer(sentry_tower::SentryHttpLayer::with_transaction())
|
||||||
|
.with_state(AppState {
|
||||||
|
http_client: client,
|
||||||
|
redis_conn_pool: pool,
|
||||||
|
});
|
||||||
|
|
||||||
|
axum::Server::bind(&"0.0.0.0:8080".parse().unwrap())
|
||||||
|
.serve(app.into_make_service())
|
||||||
|
.await?;
|
||||||
|
drop(_guard);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct CodeParam {
|
||||||
|
code: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct TokenResponse {
|
||||||
|
access_token: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct UserInfo {
|
||||||
|
id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument]
|
||||||
|
async fn handle_discord_callback(
|
||||||
|
Query(CodeParam { code }): Query<CodeParam>,
|
||||||
|
State(client): State<reqwest::Client>,
|
||||||
|
) -> Result<(StatusCode, Json<serde_json::Value>), WrappedError> {
|
||||||
|
let token_response = client
|
||||||
|
.post(DISCORD_OAUTH_TOKEN_URL)
|
||||||
|
.form(&json!({
|
||||||
|
"client_id": *DISCORD_CLIENT_ID,
|
||||||
|
"client_secret": *DISCORD_CLIENT_SECRET,
|
||||||
|
"grant_type": "authorization_code",
|
||||||
|
"code": code,
|
||||||
|
"redirect_uri": *DISCORD_REDIRECT_URL,
|
||||||
|
"scope": "identify"
|
||||||
|
}))
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
if token_response.status() != StatusCode::OK {
|
||||||
|
return Ok((
|
||||||
|
StatusCode::UNAUTHORIZED,
|
||||||
|
Json(json!({
|
||||||
|
"error": "Failed to authorize with Discord"
|
||||||
|
})),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let TokenResponse { access_token } = token_response.json().await?;
|
||||||
|
let UserInfo { id } = client
|
||||||
|
.get(DISCORD_SELF_INFO_URL)
|
||||||
|
.bearer_auth(access_token)
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.json()
|
||||||
|
.await?;
|
||||||
|
let id: u64 = id.parse()?;
|
||||||
|
let mac: [u8; 32] = blake3::keyed_hash(&MAC_SECRET, &id.to_le_bytes()).into();
|
||||||
|
let encoded = base64_engines::STANDARD_NO_PAD.encode(mac);
|
||||||
|
Ok((
|
||||||
|
StatusCode::OK,
|
||||||
|
Json(json!({
|
||||||
|
"secret": encoded
|
||||||
|
})),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(conn))]
|
||||||
|
async fn delete_data(
|
||||||
|
AuthenticatedUser(id): AuthenticatedUser,
|
||||||
|
RedisConnection(mut conn): RedisConnection,
|
||||||
|
) -> Result<StatusCode, WrappedError> {
|
||||||
|
redis::cmd("HDEL")
|
||||||
|
.arg(id)
|
||||||
|
.arg("last_modified")
|
||||||
|
.arg("settings")
|
||||||
|
.query_async(&mut *conn)
|
||||||
|
.await?;
|
||||||
|
Ok(StatusCode::NO_CONTENT)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(conn))]
|
||||||
|
async fn set_data(
|
||||||
|
AuthenticatedUser(id): AuthenticatedUser,
|
||||||
|
RedisConnection(mut conn): RedisConnection,
|
||||||
|
RawBody(body): RawBody,
|
||||||
|
) -> Result<Json<serde_json::Value>, WrappedError> {
|
||||||
|
let now = (OffsetDateTime::now_utc().unix_timestamp_nanos() / 1000) as u64;
|
||||||
|
let body = to_bytes(body).await?;
|
||||||
|
redis::cmd("HSET")
|
||||||
|
.arg(id)
|
||||||
|
.arg("last_modified")
|
||||||
|
.arg(now)
|
||||||
|
.arg("settings")
|
||||||
|
.arg(&*body)
|
||||||
|
.query_async(&mut *conn)
|
||||||
|
.await?;
|
||||||
|
Ok(Json(json!({
|
||||||
|
"written": now
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(conn))]
|
||||||
|
async fn get_data(
|
||||||
|
AuthenticatedUser(id): AuthenticatedUser,
|
||||||
|
RedisConnection(mut conn): RedisConnection,
|
||||||
|
VencloudianIfNoneMatch(tag): VencloudianIfNoneMatch,
|
||||||
|
) -> Result<Response, WrappedError> {
|
||||||
|
if let (Some(last_modified), Some(data)) = redis::cmd("HMGET")
|
||||||
|
.arg(id)
|
||||||
|
.arg("last_modified")
|
||||||
|
.arg("settings")
|
||||||
|
.query_async::<_, (Option<u64>, Option<Vec<u8>>)>(&mut *conn)
|
||||||
|
.await?
|
||||||
|
{
|
||||||
|
if let Some(tag) = tag {
|
||||||
|
if tag == last_modified {
|
||||||
|
return Ok((
|
||||||
|
StatusCode::NOT_MODIFIED,
|
||||||
|
[(ETAG, last_modified.to_string())],
|
||||||
|
)
|
||||||
|
.into_response());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok((
|
||||||
|
StatusCode::OK,
|
||||||
|
[
|
||||||
|
(ETAG, last_modified.to_string()),
|
||||||
|
(CONTENT_TYPE, "application/octet-stream".to_string()),
|
||||||
|
],
|
||||||
|
data,
|
||||||
|
)
|
||||||
|
.into_response())
|
||||||
|
} else {
|
||||||
|
Ok(StatusCode::NOT_FOUND.into_response())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(conn))]
|
||||||
|
async fn get_data_head(
|
||||||
|
AuthenticatedUser(id): AuthenticatedUser,
|
||||||
|
RedisConnection(mut conn): RedisConnection,
|
||||||
|
VencloudianIfNoneMatch(tag): VencloudianIfNoneMatch,
|
||||||
|
) -> Result<Response, WrappedError> {
|
||||||
|
if let Some(last_modified) = redis::cmd("HGET")
|
||||||
|
.arg(id)
|
||||||
|
.arg("last_modified")
|
||||||
|
.query_async::<_, Option<u64>>(&mut *conn)
|
||||||
|
.await?
|
||||||
|
{
|
||||||
|
if let Some(tag) = tag {
|
||||||
|
if tag == last_modified {
|
||||||
|
return Ok((
|
||||||
|
StatusCode::NOT_MODIFIED,
|
||||||
|
[(ETAG, last_modified.to_string())],
|
||||||
|
)
|
||||||
|
.into_response());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok((
|
||||||
|
StatusCode::OK,
|
||||||
|
[
|
||||||
|
(ETAG, last_modified.to_string()),
|
||||||
|
(CONTENT_TYPE, "application/octet-stream".to_string()),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.into_response())
|
||||||
|
} else {
|
||||||
|
Ok(StatusCode::NOT_FOUND.into_response())
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue