Test single api token
This commit is contained in:
parent
495746d6a1
commit
ec2f6b9d52
@ -1,4 +1,3 @@
|
||||
use actix_jwt_session::Authenticated;
|
||||
use actix_web::web::{Data, Path, ServiceConfig};
|
||||
use actix_web::{get, HttpResponse};
|
||||
use entities::api_tokens::*;
|
||||
@ -9,9 +8,8 @@ use sea_orm::*;
|
||||
use squadron_contract::{ApiTokenId, WorkspaceSlug};
|
||||
use tracing::error;
|
||||
|
||||
use crate::extractors::RequireInstanceConfigured;
|
||||
use crate::extractors::{RequireInstanceConfigured, RequireUser};
|
||||
use crate::models::{Error, JsonError};
|
||||
use crate::session::AppClaims;
|
||||
use crate::DatabaseConnection;
|
||||
|
||||
pub fn configure(_: reqwest::Client, config: &mut ServiceConfig) {
|
||||
@ -21,19 +19,11 @@ pub fn configure(_: reqwest::Client, config: &mut ServiceConfig) {
|
||||
#[get("{workspace_slug}/api-tokens/{id}")]
|
||||
async fn single_api_token(
|
||||
_: RequireInstanceConfigured,
|
||||
session: Authenticated<AppClaims>,
|
||||
user: RequireUser,
|
||||
path: Path<(WorkspaceSlug, ApiTokenId)>,
|
||||
db: Data<DatabaseConnection>,
|
||||
) -> Result<HttpResponse, JsonError> {
|
||||
let (slug, id) = path.into_inner();
|
||||
let user = entities::prelude::Users::find_by_id(session.account_id())
|
||||
.one(&**db)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("Failed to load user: {e}");
|
||||
Error::DatabaseError
|
||||
})?
|
||||
.ok_or(Error::UserRequired)?;
|
||||
|
||||
let slug = slug.as_str().to_string();
|
||||
match ApiTokens::find()
|
||||
@ -65,4 +55,249 @@ async fn single_api_token(
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod single_tests {}
|
||||
mod single_tests {
|
||||
use actix_jwt_session::{
|
||||
Hashing, JwtTtl, RefreshTtl, SessionMiddlewareFactory, JWT_COOKIE_NAME, JWT_HEADER_NAME,
|
||||
REFRESH_COOKIE_NAME, REFRESH_HEADER_NAME,
|
||||
};
|
||||
use actix_web::body::to_bytes;
|
||||
use actix_web::web::Data;
|
||||
use actix_web::{test, App};
|
||||
use reqwest::{Method, StatusCode};
|
||||
use sea_orm::Database;
|
||||
use squadron_contract::deadpool_redis;
|
||||
use tracing_test::traced_test;
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::super::*;
|
||||
use super::*;
|
||||
use crate::session;
|
||||
use crate::utils::{slugify, ApiTokenBuilder, UserExt};
|
||||
|
||||
macro_rules! create_app {
|
||||
($app: ident, $session_storage: ident, $db: ident) => {
|
||||
std::env::set_var(
|
||||
"DATABASE_URL",
|
||||
"postgres://postgres@0.0.0.0:5432/squadron_test",
|
||||
);
|
||||
let redis = deadpool_redis::Config::from_url("redis://0.0.0.0:6379")
|
||||
.create_pool(Some(deadpool_redis::Runtime::Tokio1))
|
||||
.expect("Can't connect to redis");
|
||||
let $db: sea_orm::prelude::DatabaseConnection =
|
||||
Database::connect("postgres://postgres@0.0.0.0:5432/squadron_test")
|
||||
.await
|
||||
.expect("Failed to connect to database");
|
||||
let ($session_storage, factory) =
|
||||
SessionMiddlewareFactory::<session::AppClaims>::build_ed_dsa()
|
||||
.with_redis_pool(redis.clone())
|
||||
// Check if header "Authorization" exists and contains Bearer with encoded JWT
|
||||
.with_jwt_header(JWT_HEADER_NAME)
|
||||
// Check if cookie JWT exists and contains encoded JWT
|
||||
.with_jwt_cookie(JWT_COOKIE_NAME)
|
||||
.with_refresh_header(REFRESH_HEADER_NAME)
|
||||
// Check if cookie JWT exists and contains encoded JWT
|
||||
.with_refresh_cookie(REFRESH_COOKIE_NAME)
|
||||
.with_jwt_json(&["access_token"])
|
||||
.finish();
|
||||
let $db = Data::new($db.clone());
|
||||
ensure_instance($db.clone()).await;
|
||||
let $app = test::init_service(
|
||||
App::new()
|
||||
.app_data(Data::new($session_storage.clone()))
|
||||
.app_data($db.clone())
|
||||
.app_data(Data::new(redis))
|
||||
.wrap(actix_web::middleware::NormalizePath::trim())
|
||||
.wrap(actix_web::middleware::Logger::default())
|
||||
.wrap(factory)
|
||||
.service(single_api_token),
|
||||
)
|
||||
.await;
|
||||
};
|
||||
}
|
||||
|
||||
async fn ensure_instance(db: Data<DatabaseConnection>) {
|
||||
use entities::instances::*;
|
||||
use entities::prelude::Instances;
|
||||
use sea_orm::*;
|
||||
|
||||
if Instances::find().count(&**db).await.unwrap() > 0 {
|
||||
return;
|
||||
}
|
||||
ActiveModel {
|
||||
instance_name: Set("Plan Free".into()),
|
||||
is_telemetry_enabled: Set(true),
|
||||
is_support_required: Set(true),
|
||||
is_setup_done: Set(true),
|
||||
is_signup_screen_visited: Set(true),
|
||||
is_verified: Set(true),
|
||||
user_count: Set(0),
|
||||
instance_id: Set(random_hex(12)),
|
||||
api_key: Set(random_hex(8)),
|
||||
version: Set(env!("CARGO_PKG_VERSION").to_string()),
|
||||
last_checked_at: Set(chrono::Utc::now().fixed_offset()),
|
||||
..Default::default()
|
||||
}
|
||||
.save(&**db)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
async fn create_user(
|
||||
db: Data<DatabaseConnection>,
|
||||
user_name: &str,
|
||||
pass: &str,
|
||||
) -> entities::users::Model {
|
||||
use entities::users::*;
|
||||
use sea_orm::*;
|
||||
|
||||
if let Ok(Some(user)) = Users::find()
|
||||
.filter(Column::Email.eq(format!("{user_name}@example.com")))
|
||||
.one(&**db)
|
||||
.await
|
||||
{
|
||||
return user;
|
||||
}
|
||||
|
||||
let pass = Hashing::encrypt(pass).unwrap();
|
||||
|
||||
Users::insert(ActiveModel {
|
||||
password: Set(pass),
|
||||
email: Set(Some(format!("{user_name}@example.com"))),
|
||||
display_name: Set(user_name.to_string()),
|
||||
username: Set(Uuid::new_v4().to_string()),
|
||||
first_name: Set("".to_string()),
|
||||
last_name: Set("".to_string()),
|
||||
last_location: Set("".to_string()),
|
||||
created_location: Set("".to_string()),
|
||||
is_password_autoset: Set(false),
|
||||
token: Set(Uuid::new_v4().to_string()),
|
||||
billing_address_country: Set("".to_string()),
|
||||
user_timezone: Set("UTC".to_string()),
|
||||
last_login_ip: Set("0.0.0.0".to_string()),
|
||||
last_login_medium: Set("None".to_string()),
|
||||
last_logout_ip: Set("0.0.0.0".to_string()),
|
||||
last_login_uagent: Set("test".to_string()),
|
||||
is_active: Set(true),
|
||||
avatar: Set("".to_string()),
|
||||
..Default::default()
|
||||
})
|
||||
.exec_with_returning(&**db)
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
async fn ensure_workspace(
|
||||
db: Data<DatabaseConnection>,
|
||||
name: &str,
|
||||
user: &entities::users::Model,
|
||||
) -> entities::workspaces::Model {
|
||||
use entities::prelude::*;
|
||||
use entities::workspaces::*;
|
||||
|
||||
let slug = slugify(name);
|
||||
if let Ok(Some(m)) = Workspaces::find()
|
||||
.filter(Column::Slug.eq(slug))
|
||||
.one(&**db)
|
||||
.await
|
||||
{
|
||||
return m;
|
||||
}
|
||||
|
||||
Workspaces::insert(user.new_workspace(name))
|
||||
.exec_with_returning(&**db)
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
async fn ensure_api_token(
|
||||
db: Data<DatabaseConnection>,
|
||||
label: &str,
|
||||
desc: &str,
|
||||
user: &entities::users::Model,
|
||||
workspace: &entities::workspaces::Model,
|
||||
) -> entities::api_tokens::Model {
|
||||
use entities::api_tokens::*;
|
||||
use entities::prelude::*;
|
||||
|
||||
if let Ok(Some(m)) = ApiTokens::find()
|
||||
.filter(
|
||||
Column::Label.eq(label).and(
|
||||
Column::Description.eq(desc).and(
|
||||
Column::WorkspaceId
|
||||
.eq(workspace.id)
|
||||
.and(Column::UserId.eq(user.id)),
|
||||
),
|
||||
),
|
||||
)
|
||||
.one(&**db)
|
||||
.await
|
||||
{
|
||||
return m;
|
||||
}
|
||||
|
||||
ApiTokens::insert(
|
||||
ApiTokenBuilder::new(label, desc, user, Some(workspace)).into_active_model(),
|
||||
)
|
||||
.exec_with_returning(&**db)
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[traced_test]
|
||||
#[actix_web::test]
|
||||
async fn valid() {
|
||||
create_app!(app, session, db);
|
||||
let user = create_user(db.clone(), "valid_single_api_token", "qweQWE123!@#").await;
|
||||
let workspace = ensure_workspace(db.clone(), "Foo bar%&^", &user).await;
|
||||
let api_token = ensure_api_token(
|
||||
db.clone(),
|
||||
"valid search single api token",
|
||||
"",
|
||||
&user,
|
||||
&workspace,
|
||||
)
|
||||
.await;
|
||||
|
||||
let uri = format!(
|
||||
"http://example.com/{}/api-tokens/{}",
|
||||
workspace.slug, api_token.id
|
||||
);
|
||||
eprintln!("URI: {uri:?}");
|
||||
|
||||
let pair = session
|
||||
.store(
|
||||
AppClaims {
|
||||
expiration_time: (chrono::Utc::now() + chrono::Duration::days(100))
|
||||
.timestamp_millis(),
|
||||
issued_at: 0,
|
||||
subject: "999999999".into(),
|
||||
audience: session::Audience::Web,
|
||||
jwt_id: Uuid::new_v4(),
|
||||
account_id: user.id,
|
||||
not_before: 0,
|
||||
},
|
||||
JwtTtl::new(actix_jwt_session::Duration::days(9999)),
|
||||
RefreshTtl::new(actix_jwt_session::Duration::days(9999)),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let req = test::TestRequest::default()
|
||||
.insert_header((JWT_HEADER_NAME, pair.jwt.encode().unwrap()))
|
||||
.uri(&uri)
|
||||
.method(Method::GET)
|
||||
.to_request();
|
||||
|
||||
let resp = test::call_service(&app, req).await;
|
||||
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
|
||||
let body = resp.into_body();
|
||||
let json: serde_json::Value =
|
||||
serde_json::from_slice(&to_bytes(body).await.unwrap()[..]).unwrap();
|
||||
|
||||
let expected: serde_json::Value =
|
||||
serde_json::from_str(&serde_json::to_string(&api_token).unwrap()).unwrap();
|
||||
|
||||
assert_eq!(json, expected,);
|
||||
}
|
||||
}
|
||||
|
@ -156,7 +156,9 @@ mod tests {
|
||||
.app_data($db.clone())
|
||||
.app_data(Data::new(redis))
|
||||
.app_data(Data::new(EventBusClient::new_succ_mock()))
|
||||
.app_data(Data::new(PasswordResetSecret::new("ahsdhy9asd".to_string())))
|
||||
.app_data(Data::new(PasswordResetSecret::new(
|
||||
"ahsdhy9asd".to_string(),
|
||||
)))
|
||||
.wrap(actix_web::middleware::NormalizePath::trim())
|
||||
.wrap(actix_web::middleware::Logger::default())
|
||||
.wrap(factory)
|
||||
|
@ -19,7 +19,7 @@ use sea_orm::*;
|
||||
use tracing::error;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::http::{AuthError, OAuthError};
|
||||
use crate::http::{random_hex, AuthError, OAuthError};
|
||||
use crate::models::Error;
|
||||
|
||||
#[macro_export]
|
||||
@ -392,3 +392,117 @@ pub mod pass_reset_token {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait UserExt {
|
||||
fn new_workspace(&self, name: &str) -> entities::workspaces::ActiveModel;
|
||||
}
|
||||
|
||||
impl UserExt for entities::users::Model {
|
||||
fn new_workspace(&self, name: &str) -> entities::workspaces::ActiveModel {
|
||||
entities::workspaces::ActiveModel::new_workspace(self.id, name)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait WorkspaceExt {
|
||||
fn new_workspace(owner_id: Uuid, name: &str) -> entities::workspaces::ActiveModel {
|
||||
use sea_orm::*;
|
||||
|
||||
entities::workspaces::ActiveModel {
|
||||
name: Set(name.to_string()),
|
||||
slug: Set(slugify(name)),
|
||||
owner_id: Set(owner_id),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl WorkspaceExt for entities::workspaces::ActiveModel {}
|
||||
|
||||
pub struct ApiTokenBuilder {
|
||||
pub created_at: Option<chrono::DateTime<chrono::FixedOffset>>,
|
||||
pub updated_at: Option<chrono::DateTime<chrono::FixedOffset>>,
|
||||
|
||||
pub token: String,
|
||||
pub label: String,
|
||||
pub user_type: i16,
|
||||
pub created_by_id: Option<Uuid>,
|
||||
pub updated_by_id: Option<Uuid>,
|
||||
pub user_id: Uuid,
|
||||
pub workspace_id: Option<Uuid>,
|
||||
pub description: String,
|
||||
pub expired_at: Option<chrono::DateTime<chrono::FixedOffset>>,
|
||||
pub is_active: bool,
|
||||
pub last_used: Option<chrono::DateTime<chrono::FixedOffset>>,
|
||||
}
|
||||
|
||||
impl ApiTokenBuilder {
|
||||
pub fn new(
|
||||
label: &str,
|
||||
desc: &str,
|
||||
user: &entities::users::Model,
|
||||
workspace: Option<&entities::workspaces::Model>,
|
||||
) -> Self {
|
||||
let now = chrono::Utc::now().fixed_offset();
|
||||
Self {
|
||||
token: random_hex(60),
|
||||
created_at: Some(now),
|
||||
updated_at: Some(now),
|
||||
label: label.to_string(),
|
||||
user_type: if user.is_bot { 1 } else { 0 },
|
||||
created_by_id: Some(user.id),
|
||||
updated_by_id: Some(user.id),
|
||||
user_id: user.id,
|
||||
workspace_id: workspace.map(|w| w.id),
|
||||
description: desc.to_string(),
|
||||
expired_at: None,
|
||||
is_active: true,
|
||||
last_used: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_active_model(self) -> entities::api_tokens::ActiveModel {
|
||||
use sea_orm::*;
|
||||
|
||||
let ApiTokenBuilder {
|
||||
label,
|
||||
created_at,
|
||||
updated_at,
|
||||
token,
|
||||
user_type,
|
||||
created_by_id,
|
||||
updated_by_id,
|
||||
user_id,
|
||||
workspace_id,
|
||||
description,
|
||||
expired_at,
|
||||
is_active,
|
||||
last_used,
|
||||
} = self;
|
||||
entities::api_tokens::ActiveModel {
|
||||
label: Set(label),
|
||||
created_at: created_at.map(Set).unwrap_or(NotSet),
|
||||
updated_at: updated_at.map(Set).unwrap_or(NotSet),
|
||||
token: Set(token),
|
||||
user_type: Set(user_type),
|
||||
created_by_id: Set(created_by_id),
|
||||
updated_by_id: Set(updated_by_id),
|
||||
user_id: Set(user_id),
|
||||
workspace_id: Set(workspace_id),
|
||||
description: Set(description),
|
||||
expired_at: Set(expired_at),
|
||||
is_active: Set(is_active),
|
||||
last_used: Set(last_used),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn slugify(s: &str) -> String {
|
||||
s.chars()
|
||||
.filter_map(|c| match c {
|
||||
_ if c.is_whitespace() => Some('-'),
|
||||
_ if c.is_ascii_alphanumeric() => Some(c.to_ascii_lowercase()),
|
||||
_ => None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
@ -91,10 +91,6 @@ CREATE TABLE users (
|
||||
use_case text
|
||||
);
|
||||
|
||||
--
|
||||
-- Name: workspaces; Type: TABLE; Schema: public; Owner: plane
|
||||
--
|
||||
|
||||
CREATE TABLE workspaces (
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
|
||||
|
6
web/architecture.svg
Normal file
6
web/architecture.svg
Normal file
@ -0,0 +1,6 @@
|
||||
<svg height="600" width="600" xmlns="http://www.w3.org/2000/svg">
|
||||
<g>
|
||||
<rect x="20" y="20" width="100" height="60" fill="rgb(94, 235, 244)" />
|
||||
<text x="20" y="100">API Server</text>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 217 B |
24
web/index.html
Normal file
24
web/index.html
Normal file
@ -0,0 +1,24 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
</head>
|
||||
<body>
|
||||
<base href="/" />
|
||||
<main>
|
||||
<header style="display:flex;position:sticky;top:0;left:0;height:2rem;width:100%;">
|
||||
<div style="display:flex;">
|
||||
<picture>
|
||||
<img src="/logo.svg" style="height:36px;margin-right:1rem;" />
|
||||
</picture>
|
||||
<b style="height:2rem;line-height:2rem;text-align:center;display:block;">Squadron</b>
|
||||
</div>
|
||||
</header>
|
||||
<app style="display:block;position:relative;margin-top:2rem;">
|
||||
<section>
|
||||
<img src="/architecture.svg" />
|
||||
</section>
|
||||
</app>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
18
web/logo.svg
Normal file
18
web/logo.svg
Normal file
@ -0,0 +1,18 @@
|
||||
<svg height="560" width="560" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<g id="jet">
|
||||
<polygon points="200,60 260,150 140,150" />
|
||||
<polygon points="200,10 225,190 175,190" />
|
||||
</g>
|
||||
</defs>
|
||||
<circle r="260" cx="280" cy="280" fill="rgb(94, 235, 244)" />
|
||||
<g transform="translate(0 90) rotate(-45 0 0)" transform-box="fill-box" transform-origin="center" style="fill:#00569d;stroke:none;stroke-width:0">
|
||||
<g transform="translate(30, 30)"><use href="#jet" /></g>
|
||||
<g transform="translate(130, -90)"><use href="#jet" /></g>
|
||||
<g transform="translate(230, 30)"><use href="#jet" /></g>
|
||||
|
||||
<g transform="translate(130, 190)"><use href="#jet" /></g>
|
||||
<g transform="translate(300, 190)"><use href="#jet" /></g>
|
||||
<g transform="translate(-50, 190)"><use href="#jet" /></g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 785 B |
Loading…
Reference in New Issue
Block a user