feat: expose workspace and profile api (#32)
* feat: expose workspace and profile api * feat: add impl for client_api * feat: add test case for workspace and profile
This commit is contained in:
parent
9ac53dca8e
commit
0d59211e55
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT * FROM public.af_workspace WHERE owner_uid = $1\n ",
|
||||
"query": "\n SELECT * FROM public.af_workspace WHERE owner_uid = (\n SELECT uid FROM public.af_user WHERE uuid = $1\n )\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
|
|
@ -41,7 +41,7 @@
|
|||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
|
|
@ -54,5 +54,5 @@
|
|||
true
|
||||
]
|
||||
},
|
||||
"hash": "23697d21dc4aa7a71027ac5671d415f3ead15394a18414911475fb946a00dd01"
|
||||
"hash": "d37119628f436d151a5559e692a3d6a7a6bc3c8226631ce6a0bcd0c7f1c0a8c5"
|
||||
}
|
||||
|
|
@ -161,7 +161,7 @@ dependencies = [
|
|||
"futures-core",
|
||||
"futures-util",
|
||||
"mio",
|
||||
"socket2 0.5.3",
|
||||
"socket2 0.5.4",
|
||||
"tokio",
|
||||
"tracing",
|
||||
]
|
||||
|
|
@ -263,7 +263,7 @@ dependencies = [
|
|||
"serde_json",
|
||||
"serde_urlencoded",
|
||||
"smallvec",
|
||||
"socket2 0.5.3",
|
||||
"socket2 0.5.4",
|
||||
"time",
|
||||
"url",
|
||||
]
|
||||
|
|
@ -477,6 +477,7 @@ dependencies = [
|
|||
"tracing-log",
|
||||
"tracing-subscriber",
|
||||
"unicode-segmentation",
|
||||
"uuid",
|
||||
"validator",
|
||||
]
|
||||
|
||||
|
|
@ -824,6 +825,7 @@ dependencies = [
|
|||
"reqwest",
|
||||
"serde_json",
|
||||
"shared_entity",
|
||||
"storage",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -3105,9 +3107,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.5.3"
|
||||
version = "0.5.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2538b18701741680e0322a2302176d3253a35388e2e62f172f64f4f16605f877"
|
||||
checksum = "4031e820eb552adee9295814c0ced9e5cf38ddf1e8b7d566d6de8e2538ea989e"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys",
|
||||
|
|
@ -3371,6 +3373,7 @@ dependencies = [
|
|||
"thiserror",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"uuid",
|
||||
"validator",
|
||||
]
|
||||
|
||||
|
|
@ -3537,7 +3540,7 @@ dependencies = [
|
|||
"parking_lot",
|
||||
"pin-project-lite",
|
||||
"signal-hook-registry",
|
||||
"socket2 0.5.3",
|
||||
"socket2 0.5.4",
|
||||
"tokio-macros",
|
||||
"windows-sys",
|
||||
]
|
||||
|
|
@ -3856,6 +3859,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "79daa5ed5740825c40b389c5e50312b9c86df53fccd33f281df655642b43869d"
|
||||
dependencies = [
|
||||
"getrandom 0.2.10",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@ tracing-bunyan-formatter = "0.3.6"
|
|||
tracing-actix-web = "0.7"
|
||||
tracing-log = "0.1.1"
|
||||
sqlx = { version = "0.7", default-features = false, features = ["runtime-tokio-rustls", "macros", "postgres", "uuid", "chrono", "migrate"] }
|
||||
uuid = { version= "1.4.1", features = ["serde", "v4"] }
|
||||
|
||||
#Local crate
|
||||
token = { path = "libs/token" }
|
||||
|
|
|
|||
|
|
@ -12,3 +12,4 @@ gotrue = { path = "../gotrue" }
|
|||
infra = { path = "../infra" }
|
||||
serde_json = "1.0.105"
|
||||
shared_entity = { path = "../shared-entity" }
|
||||
storage = { path = "../storage" }
|
||||
|
|
|
|||
|
|
@ -8,6 +8,9 @@ use gotrue::models::{AccessTokenResponse, User};
|
|||
use shared_entity::error::AppError;
|
||||
|
||||
use shared_entity::server_error::ErrorCode;
|
||||
use storage::entities::AfUserProfileView;
|
||||
use storage::entities::AfWorkspace;
|
||||
use storage::entities::AfWorkspaces;
|
||||
|
||||
pub struct Client {
|
||||
http_client: reqwest::Client,
|
||||
|
|
@ -28,6 +31,32 @@ impl Client {
|
|||
self.token.as_ref()
|
||||
}
|
||||
|
||||
pub async fn profile(&self) -> Result<AfUserProfileView, AppError> {
|
||||
let url = format!("{}/api/user/profile", self.base_url);
|
||||
let resp = self
|
||||
.http_client_with_auth(Method::GET, &url)?
|
||||
.send()
|
||||
.await?;
|
||||
let profile = AppResponse::<AfUserProfileView>::from_response(resp)
|
||||
.await?
|
||||
.into_data()?
|
||||
.ok_or::<AppError>(ErrorCode::MissingPayload.into())?;
|
||||
Ok(profile)
|
||||
}
|
||||
|
||||
pub async fn workspaces(&mut self) -> Result<AfWorkspaces, AppError> {
|
||||
let url = format!("{}/api/user/workspaces", self.base_url);
|
||||
let resp = self
|
||||
.http_client_with_auth(Method::GET, &url)?
|
||||
.send()
|
||||
.await?;
|
||||
let workspaces = AppResponse::<Vec<AfWorkspace>>::from_response(resp)
|
||||
.await?
|
||||
.into_data()?
|
||||
.ok_or::<AppError>(ErrorCode::MissingPayload.into())?;
|
||||
Ok(AfWorkspaces(workspaces))
|
||||
}
|
||||
|
||||
pub async fn sign_in_password(&mut self, email: &str, password: &str) -> Result<(), AppError> {
|
||||
let url = format!("{}/api/user/sign_in/password", self.base_url);
|
||||
let payload = serde_json::json!({
|
||||
|
|
@ -72,7 +101,7 @@ impl Client {
|
|||
let new_user = AppResponse::<User>::from_response(resp)
|
||||
.await?
|
||||
.into_data()?
|
||||
.ok_or::<Error>(ErrorCode::Unhandled.into())?;
|
||||
.ok_or::<AppError>(ErrorCode::Unhandled.into())?;
|
||||
if let Some(t) = self.token.as_mut() {
|
||||
t.user = new_user;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,9 @@ pub enum ErrorCode {
|
|||
|
||||
#[error("OAuth authentication error.")]
|
||||
OAuthError = 1003,
|
||||
|
||||
#[error("Missing Payload")]
|
||||
MissingPayload = 1004,
|
||||
}
|
||||
|
||||
/// Implements conversion from `anyhow::Error` to `ErrorCode`.
|
||||
|
|
|
|||
|
|
@ -16,9 +16,11 @@ serde = { version = "1.0.130", features = ["derive"] }
|
|||
serde_json = "1.0.68"
|
||||
thiserror = "1.0.47"
|
||||
secrecy = { version = "0.8", features = ["serde"] }
|
||||
|
||||
sqlx = { version = "0.7", default-features = false, features = ["postgres", "chrono", "uuid", "macros", "runtime-tokio-rustls"] }
|
||||
tracing = { version = "0.1.37" }
|
||||
validator = { version = "0.16", features = ["validator_derive", "derive"] }
|
||||
uuid = { version = "1.4.1", features = ["serde", "v4"] }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
default = []
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ pub struct QueryCollabParams {
|
|||
pub collab_type: CollabType,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, sqlx::FromRow)]
|
||||
#[derive(Debug, Clone, sqlx::FromRow, Serialize, Deserialize)]
|
||||
pub struct AfWorkspace {
|
||||
pub workspace_id: uuid::Uuid,
|
||||
pub database_storage_id: Option<sqlx::types::uuid::Uuid>,
|
||||
|
|
@ -64,21 +64,23 @@ pub struct AfWorkspace {
|
|||
pub workspace_name: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, sqlx::FromRow)]
|
||||
#[derive(Debug, sqlx::FromRow, Deserialize, Serialize)]
|
||||
pub struct AfUserProfileView {
|
||||
pub uid: Option<i64>, // Made this field nullable based on the error
|
||||
pub uuid: Option<uuid::Uuid>, // Made this field nullable based on the error
|
||||
pub email: Option<String>, // Made this field nullable based on the error
|
||||
pub password: Option<String>, // Made this field nullable based on the error
|
||||
pub name: Option<String>, // Made this field nullable based on the error
|
||||
pub encryption_sign: Option<String>, // Made this field nullable based on the error
|
||||
pub uid: Option<i64>,
|
||||
pub uuid: Option<uuid::Uuid>,
|
||||
pub email: Option<String>,
|
||||
pub password: Option<String>,
|
||||
pub name: Option<String>,
|
||||
pub encryption_sign: Option<String>,
|
||||
pub deleted_at: Option<DateTime<Utc>>,
|
||||
pub updated_at: Option<DateTime<Utc>>,
|
||||
pub created_at: Option<DateTime<Utc>>,
|
||||
pub latest_workspace_id: Option<uuid::Uuid>,
|
||||
}
|
||||
|
||||
#[derive(Debug, sqlx::FromRow, Deserialize, Serialize)]
|
||||
pub struct AfWorkspaces(pub Vec<AfWorkspace>);
|
||||
|
||||
impl AfWorkspaces {
|
||||
pub fn get_latest(&self, profile: AfUserProfileView) -> Option<AfWorkspace> {
|
||||
match profile.latest_workspace_id {
|
||||
|
|
|
|||
|
|
@ -26,14 +26,16 @@ pub async fn create_user_if_not_exists(
|
|||
|
||||
pub async fn select_all_workspaces_owned(
|
||||
pool: &PgPool,
|
||||
owner_uid: i64,
|
||||
owner_uuid: &Uuid,
|
||||
) -> Result<Vec<AfWorkspace>, sqlx::Error> {
|
||||
sqlx::query_as!(
|
||||
AfWorkspace,
|
||||
r#"
|
||||
SELECT * FROM public.af_workspace WHERE owner_uid = $1
|
||||
SELECT * FROM public.af_workspace WHERE owner_uid = (
|
||||
SELECT uid FROM public.af_user WHERE uuid = $1
|
||||
)
|
||||
"#,
|
||||
owner_uid
|
||||
owner_uuid
|
||||
)
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
|
|
@ -41,7 +43,7 @@ pub async fn select_all_workspaces_owned(
|
|||
|
||||
pub async fn select_user_profile_view_by_uuid(
|
||||
pool: &PgPool,
|
||||
gotrue_uuid: Uuid,
|
||||
user_uuid: &Uuid,
|
||||
) -> Result<Option<AfUserProfileView>, sqlx::Error> {
|
||||
sqlx::query_as!(
|
||||
AfUserProfileView,
|
||||
|
|
@ -49,7 +51,7 @@ pub async fn select_user_profile_view_by_uuid(
|
|||
SELECT *
|
||||
FROM public.af_user_profile_view WHERE uuid = $1
|
||||
"#,
|
||||
gotrue_uuid
|
||||
user_uuid
|
||||
)
|
||||
.fetch_optional(pool)
|
||||
.await
|
||||
|
|
|
|||
|
|
@ -9,8 +9,9 @@ use crate::domain::{UserEmail, UserName, UserPassword};
|
|||
use crate::state::State;
|
||||
use gotrue::models::{AccessTokenResponse, User};
|
||||
use shared_entity::data::{AppResponse, JsonAppResponse};
|
||||
use storage::entities::{AfUserProfileView, AfWorkspaces};
|
||||
|
||||
use crate::component::auth::jwt::Authorization;
|
||||
use crate::component::auth::jwt::{Authorization, UserUuid};
|
||||
use actix_web::web::{Data, Json};
|
||||
use actix_web::HttpRequest;
|
||||
use actix_web::Result;
|
||||
|
|
@ -25,6 +26,9 @@ pub fn user_scope() -> Scope {
|
|||
.service(web::resource("/sign_out").route(web::post().to(sign_out_handler)))
|
||||
.service(web::resource("/update").route(web::post().to(update_handler)))
|
||||
|
||||
.service(web::resource("/workspaces").route(web::get().to(workspaces_handler)))
|
||||
.service(web::resource("/profile").route(web::get().to(profile_handler)))
|
||||
|
||||
// native
|
||||
.service(web::resource("/login").route(web::post().to(login_handler)))
|
||||
.service(web::resource("/logout").route(web::get().to(logout_handler)))
|
||||
|
|
@ -32,6 +36,22 @@ pub fn user_scope() -> Scope {
|
|||
.service(web::resource("/password").route(web::post().to(change_password_handler)))
|
||||
}
|
||||
|
||||
async fn profile_handler(
|
||||
uuid: UserUuid,
|
||||
state: Data<State>,
|
||||
) -> Result<JsonAppResponse<AfUserProfileView>> {
|
||||
let profile = biz::user::user_profile(&state.pg_pool, &uuid.0).await?;
|
||||
Ok(AppResponse::Ok().with_data(profile).into())
|
||||
}
|
||||
|
||||
async fn workspaces_handler(
|
||||
uuid: UserUuid,
|
||||
state: Data<State>,
|
||||
) -> Result<JsonAppResponse<AfWorkspaces>> {
|
||||
let workspaces = biz::user::user_workspaces(&state.pg_pool, &uuid.0).await?;
|
||||
Ok(AppResponse::Ok().with_data(workspaces).into())
|
||||
}
|
||||
|
||||
async fn update_handler(
|
||||
auth: Authorization,
|
||||
req: Json<LoginRequest>,
|
||||
|
|
@ -71,7 +91,13 @@ async fn sign_up_handler(
|
|||
req: Json<LoginRequest>,
|
||||
state: Data<State>,
|
||||
) -> Result<JsonAppResponse<()>> {
|
||||
biz::user::sign_up(&state.gotrue_client, &req.email, &req.password).await?;
|
||||
biz::user::sign_up(
|
||||
&state.pg_pool,
|
||||
&state.gotrue_client,
|
||||
&req.email,
|
||||
&req.password,
|
||||
)
|
||||
.await?;
|
||||
Ok(AppResponse::Ok().into())
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,15 +8,41 @@ use gotrue::{
|
|||
};
|
||||
|
||||
use shared_entity::{error::AppError, server_error};
|
||||
use storage::entities::{AfUserProfileView, AfWorkspaces};
|
||||
use validator::validate_email;
|
||||
|
||||
use crate::domain::validate_password;
|
||||
use sqlx::{types::uuid, PgPool};
|
||||
|
||||
pub async fn sign_up(gotrue_client: &Client, email: &str, password: &str) -> Result<(), AppError> {
|
||||
pub async fn user_workspaces(
|
||||
pg_pool: &PgPool,
|
||||
uuid: &uuid::Uuid,
|
||||
) -> Result<AfWorkspaces, AppError> {
|
||||
let workspaces = storage::workspace::select_all_workspaces_owned(pg_pool, uuid).await?;
|
||||
Ok(AfWorkspaces(workspaces))
|
||||
}
|
||||
|
||||
pub async fn user_profile(
|
||||
pg_pool: &PgPool,
|
||||
uuid: &uuid::Uuid,
|
||||
) -> Result<AfUserProfileView, AppError> {
|
||||
let profile = storage::workspace::select_user_profile_view_by_uuid(pg_pool, uuid)
|
||||
.await?
|
||||
.ok_or(sqlx::Error::RowNotFound)?;
|
||||
Ok(profile)
|
||||
}
|
||||
|
||||
pub async fn sign_up(
|
||||
pg_pool: &PgPool,
|
||||
gotrue_client: &Client,
|
||||
email: &str,
|
||||
password: &str,
|
||||
) -> Result<(), AppError> {
|
||||
validate_email_password(email, password)?;
|
||||
let user = gotrue_client.sign_up(email, password).await??;
|
||||
tracing::info!("user sign up: {:?}", user);
|
||||
if user.confirmed_at.is_some() {
|
||||
storage::workspace::create_user_if_not_exists(pg_pool, uuid::Uuid::from_str(&user.id)?).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,80 +10,119 @@ lazy_static::lazy_static! {
|
|||
pub static ref VALIDATION: Validation = Validation::new(Algorithm::HS256);
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct UserUuid(pub uuid::Uuid);
|
||||
|
||||
impl FromRequest for UserUuid {
|
||||
type Error = actix_web::Error;
|
||||
|
||||
type Future = std::future::Ready<Result<Self, Self::Error>>;
|
||||
|
||||
fn from_request(req: &HttpRequest, _payload: &mut Payload) -> Self::Future {
|
||||
let auth = get_auth_from_request(req);
|
||||
match auth {
|
||||
Ok(auth) => {
|
||||
let sub = auth.claims.sub.ok_or(actix_web::error::ErrorUnauthorized(
|
||||
"Invalid Authorization header, missing sub(uuid)",
|
||||
));
|
||||
match sub {
|
||||
Ok(sub) => std::future::ready(Ok(UserUuid(uuid::Uuid::parse_str(&sub).unwrap()))),
|
||||
Err(e) => std::future::ready(Err(e)),
|
||||
}
|
||||
},
|
||||
Err(e) => std::future::ready(Err(e)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Authorization {
|
||||
pub token: String,
|
||||
pub claims: GoTrueJWTClaims,
|
||||
}
|
||||
|
||||
impl Authorization {
|
||||
pub fn uuid(&self) -> Option<String> {
|
||||
self.claims.sub.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl FromRequest for Authorization {
|
||||
type Error = actix_web::Error;
|
||||
|
||||
type Future = std::future::Ready<Result<Self, Self::Error>>;
|
||||
|
||||
fn from_request(req: &HttpRequest, _payload: &mut Payload) -> Self::Future {
|
||||
let state = req.app_data::<Data<State>>().unwrap();
|
||||
let bearer = req.headers().get("Authorization");
|
||||
match bearer {
|
||||
None => std::future::ready(Err(actix_web::error::ErrorUnauthorized(
|
||||
"No Authorization header",
|
||||
))),
|
||||
Some(bearer) => {
|
||||
let bearer = bearer.to_str();
|
||||
match bearer {
|
||||
Err(e) => std::future::ready(Err(actix_web::error::ErrorUnauthorized(e))),
|
||||
Ok(bearer) => {
|
||||
let pair_opt = bearer.split_once("Bearer "); // Authorization: Bearer <token>
|
||||
match pair_opt {
|
||||
None => std::future::ready(Err(actix_web::error::ErrorUnauthorized(
|
||||
"Invalid Authorization header, missing Bearer",
|
||||
))),
|
||||
Some(pair) => {
|
||||
match GoTrueJWTClaims::verify(
|
||||
pair.1,
|
||||
state.config.gotrue.jwt_secret.expose_secret().as_bytes(),
|
||||
) {
|
||||
Err(e) => std::future::ready(Err(actix_web::error::ErrorUnauthorized(e))),
|
||||
Ok(t) => std::future::ready(Ok(Authorization {
|
||||
token: pair.1.to_string(),
|
||||
claims: t,
|
||||
})),
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
let auth = get_auth_from_request(req);
|
||||
match auth {
|
||||
Ok(auth) => std::future::ready(Ok(auth)),
|
||||
Err(e) => std::future::ready(Err(e)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_auth_from_request(req: &HttpRequest) -> Result<Authorization, actix_web::Error> {
|
||||
let state = req.app_data::<Data<State>>().unwrap();
|
||||
let bearer = req.headers().get("Authorization");
|
||||
match bearer {
|
||||
None => Err(actix_web::error::ErrorUnauthorized(
|
||||
"No Authorization header",
|
||||
)),
|
||||
Some(bearer) => {
|
||||
let bearer = bearer.to_str();
|
||||
match bearer {
|
||||
Err(e) => Err(actix_web::error::ErrorUnauthorized(e)),
|
||||
Ok(bearer) => {
|
||||
let pair_opt = bearer.split_once("Bearer "); // Authorization: Bearer <token>
|
||||
match pair_opt {
|
||||
None => Err(actix_web::error::ErrorUnauthorized(
|
||||
"Invalid Authorization header, missing Bearer",
|
||||
)),
|
||||
Some(pair) => {
|
||||
match GoTrueJWTClaims::verify(
|
||||
pair.1,
|
||||
state.config.gotrue.jwt_secret.expose_secret().as_bytes(),
|
||||
) {
|
||||
Err(e) => Err(actix_web::error::ErrorUnauthorized(e)),
|
||||
Ok(t) => Ok(Authorization {
|
||||
token: pair.1.to_string(),
|
||||
claims: t,
|
||||
}),
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct GoTrueJWTClaims {
|
||||
// JWT standard claims
|
||||
aud: Option<String>,
|
||||
exp: Option<i64>,
|
||||
jti: Option<String>,
|
||||
iat: Option<i64>,
|
||||
iss: Option<String>,
|
||||
nbf: Option<i64>,
|
||||
sub: Option<String>,
|
||||
pub aud: Option<String>,
|
||||
pub exp: Option<i64>,
|
||||
pub jti: Option<String>,
|
||||
pub iat: Option<i64>,
|
||||
pub iss: Option<String>,
|
||||
pub nbf: Option<i64>,
|
||||
pub sub: Option<String>,
|
||||
|
||||
email: String,
|
||||
phone: String,
|
||||
app_metadata: serde_json::Value,
|
||||
user_metadata: serde_json::Value,
|
||||
role: String,
|
||||
aal: Option<String>,
|
||||
amr: Option<Vec<Amr>>,
|
||||
session_id: Option<String>,
|
||||
pub email: String,
|
||||
pub phone: String,
|
||||
pub app_metadata: serde_json::Value,
|
||||
pub user_metadata: serde_json::Value,
|
||||
pub role: String,
|
||||
pub aal: Option<String>,
|
||||
pub amr: Option<Vec<Amr>>,
|
||||
pub session_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct Amr {
|
||||
method: String,
|
||||
timestamp: u64,
|
||||
provider: Option<String>,
|
||||
pub struct Amr {
|
||||
pub method: String,
|
||||
pub timestamp: u64,
|
||||
pub provider: Option<String>,
|
||||
}
|
||||
|
||||
impl GoTrueJWTClaims {
|
||||
|
|
|
|||
|
|
@ -58,5 +58,9 @@ async fn sign_in_success() {
|
|||
let token = c.token().unwrap();
|
||||
assert!(token.user.confirmed_at.is_some());
|
||||
|
||||
// TODO: check that workspace is created for user
|
||||
let workspaces = c.workspaces().await.unwrap();
|
||||
assert!(!workspaces.0.is_empty());
|
||||
let profile = c.profile().await.unwrap();
|
||||
let latest_workspace = workspaces.get_latest(profile);
|
||||
assert!(latest_workspace.is_some());
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue