chore(history): squash 100 commit(s) from 2026-09-24
- fix(backend): case-insensitive unique email, revoke PUBLIC schema access, pin Argon2id params - test(backend): assert password length cap boundary (256 ok, 257 rejected) - docs(backend): plan — typed JWT token kinds so refresh/device tokens cannot pass as access tokens - feat(backend): JWT access/refresh token issue and verify - feat(backend): accounts repository (create/find_by_email/find_by_id) - refactor(gui): LoVisualAddonManagerScreen 989→10 файлов addon/ (8.5.2) - docs(backend): plan — fix sqlx::migrate! path in integration tests - feat(backend): POST /auth/register and /auth/login - refactor(settings): SettingsPanelComponent 715→81 + 6 helpers (8.5.2) - docs(backend): plan — harden device flow (single-use codes, bounded store, 404/429) - feat(backend): OAuth device authorization grant for mod login - fix(backend): first confirm wins for device codes - docs(backend): plan — split Task 8 (refactor) and Task 9 (avatars), harden avatar handling - refactor(mixins): LocalPlayerMixin 691→111 + 4 handlers (8.5.2) - refactor(visuals): Trails 687→130 (8.5.2) - refactor(render): ItemBatchRenderer 677->100 (8.5.2) - refactor(visuals): ReimaginedVisual 674→118 + 5 helpers (8.5.2) - refactor(config): ConfigSerializer 668→91 + 4 helpers (8.5.2) - refactor(hud): DynamicIsland 661→158 + 4 helpers (8.5.2) - refactor(render): GlStencilFramebufferSupport 666→169 (8.5.2) - refactor(gui): MenuScreen 669→128 + 4 helpers (8.5.2) - refactor(render): UiStyle 644→170 + 3 helpers (8.5.2) - refactor(gui): ModuleComponent 613→98 + 4 helpers (8.5.2) - refactor(media): MediaSessionService 616→200 + 4 helpers (8.5.2) - refactor(gui): RelationsComponent 661→59 + 4 helpers (8.5.2) - chore(license): strip GPL file headers from all Java sources - refactor(aiming): PointTracker 583→168 + 2 helpers (8.5.2) - refactor(gui): LoVisualProxyManagerScreen 591→132 + 2 helpers (8.5.2) - refactor(visuals): KillEffect 588→96 + 4 helpers (8.5.2) - refactor(render): MeshBuilder +4 helpers (8.5.2) - refactor(visuals): extract WorldParticlesRender helper (8.5.2) - refactor(world): ExplosionDamageUtil 551→116 + 2 helpers (8.5.2) - refactor(visuals): TazikHat 596->179 + Model + Palette in hats/tazik (8.5.2) - chore(license): strip GPL header from remaining 30 files and make strip script variant-aware - refactor(gui): ThemeComponent 561->166 + CardRenderer + ScrollState (8.5.2) - refactor(hud): CustomHotbar 556→178 + Renderer + Selection + SelectionGradient (8.5.2) - refactor(gui): ThemeCardRenderer perf + readability polish - refactor(clickgui): CooldownRulesSetting 596->198 + Editor + DetailRenderer (8.5.2) - refactor(hud): HudNotifier 561->200 + Painter + runtime/HudNotifierRuntime (8.5.2) - refactor(theme): Themes 555->168 + impl/Transition + impl/Blending + impl/ProfileCodec (8.5.2) - refactor(theme): EditableClickGuiTheme 205->185 + JavaDoc (8.5.2) - refactor(theme): ThemeStore 491->128 + store/ThemeStoreJson + store/ThemeStoreIO (8.5.2) - refactor(clickgui): ClickGuiRenderer 604->200 compacted one-line delegators + JavaDoc (8.5.2) - refactor(mainmenu): LoVisualMainMenuScreen 551->161 + impl/Painter + impl/Renderer + impl/TextUtil (8.5.2) - refactor(clickgui): ClickGuiTextEditorState 531->187 + impl/EditorCaret + impl/EditorPainter (8.5.2) - refactor(tab): TabListModel 525->139 + model/Collector + model/Reader + model/Signature + model/TextSplitter (8.5.2) - refactor(backend): shared bearer helper and test helpers, build_app takes Config, validate JWT secret strength - refactor(module): ModuleManager 521->198 + impl/Registrar + impl/Dispatcher (8.5.2) - feat(backend): avatar upload with decode, square crop, PNG re-encode and S3 storage - refactor(clip): ClipFunction 512->146 + impl/Geometry + impl/Debug (8.5.2) - docs(backend): implementation plans for gateway (auth hardening, gRPC, rate limits) and configs-service - refactor(iris-patch): ShaderPatchEngine 499->146 + impl/Repo (8.5.2) - chore(frontend): add router, react-query, fonts and vitest; dev proxy to gateway - refactor(hud): ScriptedListHudPanel 499->158 + panel/Props + panel/Signature (8.5.2) - refactor(hud): BaseHudElement 499->199 + impl/Registry + impl/Namer + impl/Prewarm (8.5.2) - refactor(clickgui): Setting 498->170 + impl/Localization + impl/I18n (8.5.2) - refact(viewmodel): split swing animations into camera/swing package - refact(kineticlyrics): split module into stage, playback and modes - rename(holeesp): module HoleESP -> CrystalHoles - refact(crystalholes): split module into crystal scanner, renderer and safety - refact(addonmanager): split manager into lifecycle, runtime, descriptors and profiles - refact(accountconfig): split config into store, session and value helpers - refactor(render): CustomTextRenderer 229->195, extract glyph-pass into GradientTexts helper - docs(TODO): mark AddonManager split done; close 9.2 refactor gate - refactor(media): LinuxMediaSession 441->148, split reader + track/seek state - refactor(nametags): split NameTags into facade + impl helpers - refactor(clickgui): split MainSettingsComponent into facade + scroll + model - refactor(hud): split CustomBar into facade, model and BarSettings - docs(frontend): implementation plan with design system from the mod theme - feat(frontend): design tokens from the mod theme, fonts and shared UI kit - fix(accounts): run migrations on startup, offload Argon2, validate register input, JSON error shape - docs(gateway): plan note on splitting auth handlers before refresh endpoints - feat(frontend): API client with silent refresh, error descriptions and test helpers - style(mod): group compact one-line bulk query methods in ModuleManager - feat(frontend): session restore, login and registration with client-side validation - refactor(hud): split CustomHealthBar into facade + painter + script renderer - docs(mod): record the 2026-09-24 HUD/settings split wave in TODO phase 8.5 - refactor(rhi): split GlStencilShapeClipBackend into facade + native-state + pass-lifecycle helpers - refactor(rhi): split VulkanRenderStateBridge into facade + MSAA and stencil state helpers - refactor(backtrack): split BacktrackController into facade + model + impl helpers - refactor(svg): split SvgPathParser into facade + arc geometry + command/curve helpers - refactor(mixin): split ClientPacketListenerMixin into hook-only mixin + handlers - refactor(renderer3d): un-nest batch bindings + culling into sibling impl types - refactor(renderwarp): extract static factories + geometry into impl helpers - feat(backend): add common crate with shared JWT, internal gateway contract and accounts proto - refactor(guimixin): move hook bodies into handlers, keep mixin as hooks + shadows - feat(accounts): accept only gateway traffic, read identity from gateway header - refactor(cacheduiscriptruntime): extract engine, hashing and frame stats into impl - refactor(customskyboxrenderer): extract projection, shader passes and sun into impl - refactor(betterchatstoremanager): extract persistence, key/path and hover helpers into impl - feat(accounts): rotating opaque refresh tokens in httpOnly cookie, /auth/refresh and /auth/logout - refactor(targetesp): extract crystal rendering subsystem into impl/TargetEspCrystalRenderer - refactor(betterchathovercache): extract disk codec and lookup indexing into impl/ChatHoverCacheCodec - refactor(microsoftauth): split HTTP transport, device-code and Xbox flows into impl/ - refactor(pvpcooldowns): extract local item-rule engine and defaults into impl/PvpCooldownRules - refactor(lovisual): extract HUD/world render orchestration into HudRender helper - refactor(statuseffectheuristics): extract palette/inference into ParticlePalette and color utils into ParticleColors - refactor(dropesp): extract overlay/label render subsystem into impl/DropEspOverlayRenderer - refactor(proxy): extract SOCKS handshake message builders into ProxyProtocolMessages - refactor(eagleutil): promote EdgeRecovery controller and RecoveryMode to top-level class
This commit is contained in:
parent
9d08fa910a
commit
72bc4c7148
1897 changed files with 36199 additions and 39289 deletions
23
backend/common/Cargo.toml
Normal file
23
backend/common/Cargo.toml
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
[package]
|
||||
name = "common"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
axum = "0.8"
|
||||
jsonwebtoken = { version = "11", default-features = false, features = ["rust_crypto"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
chrono = "0.4"
|
||||
uuid = { version = "1", features = ["v4", "serde"] }
|
||||
subtle = "2"
|
||||
tonic = "0.14"
|
||||
tonic-prost = "0.14"
|
||||
prost = "0.14"
|
||||
|
||||
[build-dependencies]
|
||||
tonic-prost-build = "0.14"
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
|
||||
axum-test = "21"
|
||||
4
backend/common/build.rs
Normal file
4
backend/common/build.rs
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
tonic_prost_build::compile_protos("proto/accounts.proto")?;
|
||||
Ok(())
|
||||
}
|
||||
13
backend/common/proto/accounts.proto
Normal file
13
backend/common/proto/accounts.proto
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
syntax = "proto3";
|
||||
package accounts.v1;
|
||||
|
||||
// Internal-only API of accounts-service. Never exposed publicly; every call
|
||||
// must carry the x-lovisual-internal-key metadata entry.
|
||||
service AccountsInternal {
|
||||
// Resolves a mod's long-lived device token to its account and bumps
|
||||
// device_links.last_seen. UNAUTHENTICATED if the token is unknown/revoked.
|
||||
rpc AuthenticateDevice(AuthenticateDeviceRequest) returns (AuthenticateDeviceReply);
|
||||
}
|
||||
|
||||
message AuthenticateDeviceRequest { string device_token = 1; }
|
||||
message AuthenticateDeviceReply { string account_id = 1; }
|
||||
176
backend/common/src/internal.rs
Normal file
176
backend/common/src/internal.rs
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
//! Contract between the gateway and internal services. Services trust the
|
||||
//! identity header ONLY because `require_internal_key` guarantees the request
|
||||
//! came through the gateway (which strips client-supplied copies of both).
|
||||
|
||||
use axum::{
|
||||
extract::{FromRequestParts, Request, State},
|
||||
http::{request::Parts, StatusCode},
|
||||
middleware::Next,
|
||||
response::{IntoResponse, Response},
|
||||
Json,
|
||||
};
|
||||
use serde_json::json;
|
||||
use std::sync::Arc;
|
||||
use subtle::ConstantTimeEq;
|
||||
use tonic::{
|
||||
metadata::{Ascii, MetadataValue},
|
||||
service::Interceptor,
|
||||
Status,
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
pub const INTERNAL_KEY_HEADER: &str = "x-lovisual-internal-key";
|
||||
pub const ACCOUNT_ID_HEADER: &str = "x-lovisual-account-id";
|
||||
pub const DEVICE_TOKEN_PREFIX: &str = "lvd_";
|
||||
|
||||
fn keys_match(given: &[u8], expected: &[u8]) -> bool {
|
||||
given.ct_eq(expected).into()
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct InternalKey(Arc<str>);
|
||||
|
||||
impl InternalKey {
|
||||
pub fn new(key: String) -> Self {
|
||||
InternalKey(key.into())
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn require_internal_key(State(key): State<InternalKey>, req: Request, next: Next) -> Response {
|
||||
let ok = req
|
||||
.headers()
|
||||
.get(INTERNAL_KEY_HEADER)
|
||||
.is_some_and(|v| keys_match(v.as_bytes(), key.0.as_bytes()));
|
||||
if !ok {
|
||||
return (StatusCode::FORBIDDEN, Json(json!({ "error": "forbidden" }))).into_response();
|
||||
}
|
||||
next.run(req).await
|
||||
}
|
||||
|
||||
/// The authenticated account, as resolved by the gateway. Rejects with 401
|
||||
/// when the gateway forwarded the request anonymously.
|
||||
pub struct GatewayIdentity {
|
||||
pub account_id: Uuid,
|
||||
}
|
||||
|
||||
impl<S: Send + Sync> FromRequestParts<S> for GatewayIdentity {
|
||||
type Rejection = Response;
|
||||
|
||||
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
|
||||
parts
|
||||
.headers
|
||||
.get(ACCOUNT_ID_HEADER)
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.and_then(|s| Uuid::parse_str(s).ok())
|
||||
.map(|account_id| GatewayIdentity { account_id })
|
||||
.ok_or_else(|| {
|
||||
(StatusCode::UNAUTHORIZED, Json(json!({ "error": "unauthorized" }))).into_response()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Server-side tonic interceptor: rejects calls without the internal key.
|
||||
#[derive(Clone)]
|
||||
pub struct GrpcKeyCheck(Arc<str>);
|
||||
|
||||
impl GrpcKeyCheck {
|
||||
pub fn new(key: &str) -> Self {
|
||||
GrpcKeyCheck(key.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl Interceptor for GrpcKeyCheck {
|
||||
fn call(&mut self, req: tonic::Request<()>) -> Result<tonic::Request<()>, Status> {
|
||||
match req.metadata().get(INTERNAL_KEY_HEADER) {
|
||||
Some(v) if keys_match(v.as_bytes(), self.0.as_bytes()) => Ok(req),
|
||||
_ => Err(Status::permission_denied("missing or invalid internal key")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Client-side tonic interceptor: attaches the internal key to every call.
|
||||
#[derive(Clone)]
|
||||
pub struct GrpcKeyAttach(MetadataValue<Ascii>);
|
||||
|
||||
impl GrpcKeyAttach {
|
||||
pub fn new(key: &str) -> Result<Self, tonic::metadata::errors::InvalidMetadataValue> {
|
||||
Ok(GrpcKeyAttach(MetadataValue::try_from(key)?))
|
||||
}
|
||||
}
|
||||
|
||||
impl Interceptor for GrpcKeyAttach {
|
||||
fn call(&mut self, mut req: tonic::Request<()>) -> Result<tonic::Request<()>, Status> {
|
||||
req.metadata_mut().insert(INTERNAL_KEY_HEADER, self.0.clone());
|
||||
Ok(req)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use axum::{routing::get, Router};
|
||||
use axum_test::TestServer;
|
||||
|
||||
const KEY: &str = "internal-key-internal-key-internal!!";
|
||||
|
||||
fn app() -> Router {
|
||||
Router::new()
|
||||
.route("/whoami", get(|id: GatewayIdentity| async move { id.account_id.to_string() }))
|
||||
.route("/open", get(|| async { "open" }))
|
||||
.layer(axum::middleware::from_fn_with_state(InternalKey::new(KEY.into()), require_internal_key))
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn request_without_internal_key_is_forbidden() {
|
||||
let server = TestServer::new(app());
|
||||
server.get("/open").await.assert_status(axum::http::StatusCode::FORBIDDEN);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn request_with_wrong_internal_key_is_forbidden() {
|
||||
let server = TestServer::new(app());
|
||||
server
|
||||
.get("/open")
|
||||
.add_header(INTERNAL_KEY_HEADER, "nope")
|
||||
.await
|
||||
.assert_status(axum::http::StatusCode::FORBIDDEN);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn correct_key_passes_and_identity_is_read() {
|
||||
let server = TestServer::new(app());
|
||||
let id = Uuid::new_v4();
|
||||
let res = server
|
||||
.get("/whoami")
|
||||
.add_header(INTERNAL_KEY_HEADER, KEY)
|
||||
.add_header(ACCOUNT_ID_HEADER, id.to_string())
|
||||
.await;
|
||||
res.assert_status_ok();
|
||||
assert_eq!(res.text(), id.to_string());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn missing_or_garbage_identity_is_401() {
|
||||
let server = TestServer::new(app());
|
||||
server
|
||||
.get("/whoami")
|
||||
.add_header(INTERNAL_KEY_HEADER, KEY)
|
||||
.await
|
||||
.assert_status_unauthorized();
|
||||
server
|
||||
.get("/whoami")
|
||||
.add_header(INTERNAL_KEY_HEADER, KEY)
|
||||
.add_header(ACCOUNT_ID_HEADER, "not-a-uuid")
|
||||
.await
|
||||
.assert_status_unauthorized();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn grpc_key_check_accepts_only_the_right_key() {
|
||||
let mut check = GrpcKeyCheck::new(KEY);
|
||||
let mut attach = GrpcKeyAttach::new(KEY).unwrap();
|
||||
let ok = attach.call(tonic::Request::new(())).unwrap();
|
||||
assert!(check.call(ok).is_ok());
|
||||
assert!(check.call(tonic::Request::new(())).is_err());
|
||||
}
|
||||
}
|
||||
150
backend/common/src/jwt.rs
Normal file
150
backend/common/src/jwt.rs
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
use axum::http::{header::AUTHORIZATION, HeaderMap};
|
||||
use jsonwebtoken::{decode, encode, Algorithm, DecodingKey, EncodingKey, Header, Validation};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
/// Distinguishes token purposes so a long-lived refresh/device token can
|
||||
/// never be replayed as a short-lived access token (and vice versa).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum TokenType {
|
||||
Access,
|
||||
Refresh,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Claims {
|
||||
pub sub: String,
|
||||
pub exp: usize,
|
||||
pub token_type: TokenType,
|
||||
}
|
||||
|
||||
fn issue(account_id: Uuid, secret: &str, token_type: TokenType, ttl_seconds: i64) -> String {
|
||||
let exp = (chrono::Utc::now() + chrono::Duration::seconds(ttl_seconds)).timestamp() as usize;
|
||||
let claims = Claims { sub: account_id.to_string(), exp, token_type };
|
||||
encode(&Header::new(Algorithm::HS256), &claims, &EncodingKey::from_secret(secret.as_bytes()))
|
||||
.expect("encoding a well-formed Claims struct cannot fail")
|
||||
}
|
||||
|
||||
pub fn issue_access_token(account_id: Uuid, secret: &str) -> String {
|
||||
issue(account_id, secret, TokenType::Access, 15 * 60)
|
||||
}
|
||||
|
||||
/// Temporary — deleted in Task 4 once opaque refresh tokens land.
|
||||
pub fn issue_refresh_token(account_id: Uuid, secret: &str) -> String {
|
||||
issue(account_id, secret, TokenType::Refresh, 30 * 24 * 60 * 60)
|
||||
}
|
||||
|
||||
/// Returns the claims only if the signature, expiry AND token type all match.
|
||||
/// HS256 is pinned explicitly (no algorithm confusion) and expiry leeway is 0.
|
||||
pub fn verify_token(token: &str, secret: &str, expected: TokenType) -> Option<Claims> {
|
||||
let mut validation = Validation::new(Algorithm::HS256);
|
||||
validation.leeway = 0;
|
||||
validation.set_required_spec_claims(&["exp", "sub"]);
|
||||
let claims = decode::<Claims>(
|
||||
token,
|
||||
&DecodingKey::from_secret(secret.as_bytes()),
|
||||
&validation,
|
||||
)
|
||||
.ok()?
|
||||
.claims;
|
||||
(claims.token_type == expected).then_some(claims)
|
||||
}
|
||||
|
||||
/// The raw token from `Authorization: Bearer <token>`, if the header is
|
||||
/// present, valid ASCII, uses the Bearer scheme and is non-empty.
|
||||
pub fn bearer_token(headers: &HeaderMap) -> Option<&str> {
|
||||
let value = headers.get(AUTHORIZATION)?.to_str().ok()?;
|
||||
let token = value.strip_prefix("Bearer ")?.trim();
|
||||
(!token.is_empty()).then_some(token)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn access_token_round_trips() {
|
||||
let id = Uuid::new_v4();
|
||||
let token = issue_access_token(id, "test-secret");
|
||||
let claims = verify_token(&token, "test-secret", TokenType::Access).expect("should decode");
|
||||
assert_eq!(claims.sub, id.to_string());
|
||||
assert_eq!(claims.token_type, TokenType::Access);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wrong_secret_fails_verify() {
|
||||
let token = issue_access_token(Uuid::new_v4(), "test-secret");
|
||||
assert!(verify_token(&token, "other-secret", TokenType::Access).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn garbage_token_fails_verify() {
|
||||
assert!(verify_token("not.a.jwt", "test-secret", TokenType::Access).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn refresh_token_is_rejected_where_access_is_expected() {
|
||||
let token = issue_refresh_token(Uuid::new_v4(), "test-secret");
|
||||
assert!(verify_token(&token, "test-secret", TokenType::Access).is_none());
|
||||
assert!(verify_token(&token, "test-secret", TokenType::Refresh).is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn access_token_is_rejected_where_refresh_is_expected() {
|
||||
let token = issue_access_token(Uuid::new_v4(), "test-secret");
|
||||
assert!(verify_token(&token, "test-secret", TokenType::Refresh).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expired_token_fails_verify() {
|
||||
let token = issue(Uuid::new_v4(), "test-secret", TokenType::Access, -10);
|
||||
assert!(verify_token(&token, "test-secret", TokenType::Access).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn token_signed_with_other_algorithm_is_rejected() {
|
||||
let claims = Claims {
|
||||
sub: Uuid::new_v4().to_string(),
|
||||
exp: (chrono::Utc::now().timestamp() + 600) as usize,
|
||||
token_type: TokenType::Access,
|
||||
};
|
||||
let hs512 = encode(
|
||||
&Header::new(Algorithm::HS512),
|
||||
&claims,
|
||||
&EncodingKey::from_secret(b"test-secret"),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(verify_token(&hs512, "test-secret", TokenType::Access).is_none());
|
||||
}
|
||||
|
||||
use axum::http::HeaderValue;
|
||||
|
||||
fn headers_with(value: &str) -> HeaderMap {
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert(AUTHORIZATION, HeaderValue::from_str(value).unwrap());
|
||||
headers
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bearer_token_extracts_the_token() {
|
||||
let mut h = HeaderMap::new();
|
||||
h.insert(AUTHORIZATION, "Bearer abc.def".parse().unwrap());
|
||||
assert_eq!(bearer_token(&h), Some("abc.def"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bearer_token_rejects_missing_wrong_scheme_and_empty() {
|
||||
let mut h = HeaderMap::new();
|
||||
assert_eq!(bearer_token(&h), None);
|
||||
h.insert(AUTHORIZATION, "Basic abc".parse().unwrap());
|
||||
assert_eq!(bearer_token(&h), None);
|
||||
h.insert(AUTHORIZATION, "Bearer ".parse().unwrap());
|
||||
assert_eq!(bearer_token(&h), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bearer_token_rejects_garbage_headers() {
|
||||
assert_eq!(bearer_token(&headers_with("Bearer not.a.jwt")).unwrap(), "not.a.jwt");
|
||||
}
|
||||
}
|
||||
8
backend/common/src/lib.rs
Normal file
8
backend/common/src/lib.rs
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
pub mod internal;
|
||||
pub mod jwt;
|
||||
|
||||
pub mod pb {
|
||||
pub mod accounts {
|
||||
tonic::include_proto!("accounts.v1");
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue