fix: replace username with first and last name and use user_id UUID for primary keys
Some checks failed
ci/woodpecker/pr/woodpecker Pipeline failed
ci/woodpecker/push/woodpecker Pipeline failed

This commit is contained in:
Aravinth Manivannan 2024-07-14 21:00:20 +05:30
parent 07be1ecf20
commit 50bd3db7b3
Signed by: realaravinth
GPG key ID: F8F50389936984FF
66 changed files with 719 additions and 873 deletions

View file

@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT EXISTS (\n SELECT 1\n FROM user_query\n WHERE\n username = $1\n );",
"query": "SELECT EXISTS (\n SELECT 1\n FROM user_query\n WHERE\n user_id = $1\n );",
"describe": {
"columns": [
{
@ -11,12 +11,12 @@
],
"parameters": {
"Left": [
"Text"
"Uuid"
]
},
"nullable": [
null
]
},
"hash": "ed4bd44b2a0595cd80b36ce70b30ab55af1af12c1f22a2d4a2baf8af4569cf73"
"hash": "004d12b7ccb1b21c39ef6de716953bf039bdba5096ae139be7656170ff45613f"
}

View file

@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO verification_otp (secret, created_at, purpose, username)\n VALUES ($1, $2, $3, $4);",
"query": "INSERT INTO verification_otp (secret, created_at, purpose, user_id)\n VALUES ($1, $2, $3, $4);",
"describe": {
"columns": [],
"parameters": {
@ -8,10 +8,10 @@
"Varchar",
"Timestamptz",
"Text",
"Text"
"Uuid"
]
},
"nullable": []
},
"hash": "487deedfaaf10c4ab02fc223b8fec4bf359b082331adfa788096742673168be3"
"hash": "0fbaa8084440adce8a6162d67e3e57b6062cc17bbbbeb5ea3e1e58db17ac8240"
}

View file

@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE\n user_query\n SET\n user_id = $1, version = $2, first_name = $3, email = $4,\n hashed_password = $5, is_admin = $6, is_verified = $7, deleted = $8,\n last_name=$9;",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid",
"Int8",
"Text",
"Text",
"Text",
"Bool",
"Bool",
"Bool",
"Text"
]
},
"nullable": []
},
"hash": "14e8d7a1c8f80701b76b2bac69b1ecd99f7694d620f1945ad5c4ae474a17be1b"
}

View file

@ -9,7 +9,7 @@
"Text",
"Text",
"Uuid",
"Text"
"Uuid"
]
},
"nullable": []

View file

@ -21,7 +21,7 @@
{
"ordinal": 3,
"name": "owner",
"type_info": "Text"
"type_info": "Uuid"
}
],
"parameters": {

View file

@ -1,12 +1,12 @@
{
"db_name": "PostgreSQL",
"query": "SELECT \n view_id, version\n FROM\n user_query\n WHERE\n view_id = $1;",
"query": "SELECT \n user_id, version\n FROM\n user_query\n WHERE\n user_id = $1;",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "view_id",
"type_info": "Text"
"name": "user_id",
"type_info": "Uuid"
},
{
"ordinal": 1,
@ -16,7 +16,7 @@
],
"parameters": {
"Left": [
"Text"
"Uuid"
]
},
"nullable": [
@ -24,5 +24,5 @@
false
]
},
"hash": "fa1c65d51e3e0521d6a20998f4b80b615c9e0ffe9ceda82e0bce410c9aca39a0"
"hash": "6523c83a859d7ca283d209133a10d4ac74b6b0358bdf1e17f1a54e2cc02e305b"
}

View file

@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT EXISTS (\n SELECT 1\n FROM verification_otp\n WHERE\n username = $1\n AND\n purpose = $2\n AND\n secret = $3\n );",
"query": "SELECT EXISTS (\n SELECT 1\n FROM verification_otp\n WHERE\n user_id = $1\n AND\n purpose = $2\n AND\n secret = $3\n );",
"describe": {
"columns": [
{
@ -11,7 +11,7 @@
],
"parameters": {
"Left": [
"Text",
"Uuid",
"Text",
"Text"
]
@ -20,5 +20,5 @@
null
]
},
"hash": "3edf94a78114819085b573ff51702faee1c444c24bbf6d33ec1b4b245dd9f675"
"hash": "70e6216e30f90175d4c3bad51ff51f5fa2b6f965d868b15c53264cb8cd6b4053"
}

View file

@ -1,21 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE\n user_query\n SET\n view_id = $1, version = $2, username = $3, email = $4,\n hashed_password = $5, is_admin = $6, is_verified = $7, deleted = $8;",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Int8",
"Text",
"Text",
"Text",
"Bool",
"Bool",
"Bool"
]
},
"nullable": []
},
"hash": "750f96296e6d2c0b6d79c6f21e5b665369d5062a42c2c405f43d56a614a1e0ea"
}

View file

@ -1,21 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO user_query (\n view_id, version, username, email,\n hashed_password, is_admin, is_verified, deleted\n ) VALUES (\n $1, $2, $3, $4, $5, $6, $7, $8\n );",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Int8",
"Text",
"Text",
"Text",
"Bool",
"Bool",
"Bool"
]
},
"nullable": []
},
"hash": "7a401e98fff59eb8c0fe1744ccdea68842eaf86ba312c2eb324854041beb9de7"
}

View file

@ -1,42 +1,52 @@
{
"db_name": "PostgreSQL",
"query": "SELECT \n username, email, hashed_password, is_admin, is_verified, deleted\n FROM\n user_query\n WHERE\n view_id = $1;",
"query": "SELECT \n first_name, last_name, user_id, email, hashed_password, is_admin, is_verified, deleted\n FROM\n user_query\n WHERE\n user_id = $1;",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "username",
"name": "first_name",
"type_info": "Text"
},
{
"ordinal": 1,
"name": "email",
"name": "last_name",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "user_id",
"type_info": "Uuid"
},
{
"ordinal": 3,
"name": "email",
"type_info": "Text"
},
{
"ordinal": 4,
"name": "hashed_password",
"type_info": "Text"
},
{
"ordinal": 3,
"ordinal": 5,
"name": "is_admin",
"type_info": "Bool"
},
{
"ordinal": 4,
"ordinal": 6,
"name": "is_verified",
"type_info": "Bool"
},
{
"ordinal": 5,
"ordinal": 7,
"name": "deleted",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Text"
"Uuid"
]
},
"nullable": [
@ -45,8 +55,10 @@
false,
false,
false,
false,
false,
false
]
},
"hash": "803f868ee4a79fefdd85c5d0a034a5925c3bb7de87828095971cb19bc1fc7550"
"hash": "7a59c989d043c249cd04fe24544cf9ea55e1329ce4b53889947478c2e766ea1a"
}

View file

@ -9,7 +9,7 @@
"Text",
"Text",
"Uuid",
"Text"
"Uuid"
]
},
"nullable": []

View file

@ -1,16 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM\n verification_otp\n WHERE\n username = $1\n AND\n purpose = $2\n AND\n secret = $3;",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Text",
"Text"
]
},
"nullable": []
},
"hash": "c54d1b89ee39ceb11326e82962eef3d8f588f6252ba4bbac99fb73cdbbcd2204"
}

View file

@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT\n secret\n FROM\n verification_otp\n WHERE\n username = $1\n AND\n purpose = $2;",
"query": "SELECT\n secret\n FROM\n verification_otp\n WHERE\n user_id = $1\n AND\n purpose = $2;",
"describe": {
"columns": [
{
@ -11,7 +11,7 @@
],
"parameters": {
"Left": [
"Text",
"Uuid",
"Text"
]
},
@ -19,5 +19,5 @@
false
]
},
"hash": "7c48c7569b16e9600aa31fbf14b9ceeb195da7cde0082be861369ef4f997c534"
"hash": "ec80b5dc41697e7df7112962aba8185040fc0a101edea4b7e0c23f6468300196"
}

View file

@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO user_query (\n version, first_name, last_name, email,\n hashed_password, is_admin, is_verified, deleted, user_id\n ) VALUES (\n $1, $2, $3, $4, $5, $6, $7, $8, $9\n );",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int8",
"Text",
"Text",
"Text",
"Text",
"Bool",
"Bool",
"Bool",
"Uuid"
]
},
"nullable": []
},
"hash": "f1b7a6581e20b9f1c0dabeb7a2479d70ab005cf787a0ed13260e65f7ea949136"
}

View file

@ -0,0 +1,16 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM\n verification_otp\n WHERE\n user_id = $1\n AND\n purpose = $2\n AND\n secret = $3;",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid",
"Text",
"Text"
]
},
"nullable": []
},
"hash": "ffb4315a9b95ed39964c2a0a5a9dc45e4ab7941ced043931568023238d5127f7"
}

View file

@ -17,17 +17,16 @@ CREATE TABLE IF NOT EXISTS events
CREATE TABLE IF NOT EXISTS user_query
(
view_id text NOT NULL,
version bigint CHECK (version >= 0) NOT NULL,
username TEXT NOT NULL UNIQUE,
first_name TEXT NOT NULL,
last_name TEXT NOT NULL,
user_id UUID NOT NULL UNIQUE,
email TEXT NOT NULL UNIQUE,
hashed_password TEXT NOT NULL,
is_admin BOOLEAN NOT NULL DEFAULT FALSE,
is_verified BOOLEAN NOT NULL DEFAULT FALSE,
deleted BOOLEAN NOT NULL DEFAULT FALSE,
PRIMARY KEY (view_id)
PRIMARY KEY (user_id)
);
CREATE UNIQUE INDEX IF NOT EXISTS user_username_index ON user_query (username);

View file

@ -6,6 +6,6 @@ CREATE TABLE IF NOT EXISTS verification_otp (
secret VARCHAR(32) NOT NULL UNIQUE,
created_at timestamp with time zone DEFAULT (CURRENT_TIMESTAMP),
purpose TEXT NOT NULL,
username TEXT NOT NULL,
user_id UUID NOT NULL,
ID SERIAL PRIMARY KEY NOT NULL
);

View file

@ -15,8 +15,3 @@ CREATE TABLE IF NOT EXISTS cqrs_inventory_category_query
PRIMARY KEY (category_id)
);
CREATE UNIQUE INDEX IF NOT EXISTS
cqrs_inventory_store_query_category_id_index
ON
cqrs_inventory_category_query (category_id);

View file

@ -8,11 +8,9 @@ CREATE TABLE IF NOT EXISTS cqrs_inventory_store_query
name TEXT NOT NULL,
address TEXT,
owner TEXT NOT NULL,
owner UUID NOT NULL,
store_id UUID NOT NULL UNIQUE,
deleted BOOLEAN NOT NULL DEFAULT FALSE,
PRIMARY KEY (store_id)
);
CREATE UNIQUE INDEX IF NOT EXISTS store_store_id_index ON cqrs_inventory_store_query (store_id);

View file

@ -11,12 +11,12 @@ use crate::identity::application::port::output::db::{create_verification_secret:
impl CreateVerificationSecretOutDBPort for DBOutPostgresAdapter {
async fn create_verification_secret(&self, msg: CreateSecretMsg) -> OutDBPortResult<()> {
sqlx::query!(
"INSERT INTO verification_otp (secret, created_at, purpose, username)
"INSERT INTO verification_otp (secret, created_at, purpose, user_id)
VALUES ($1, $2, $3, $4);",
&msg.secret,
OffsetDateTime::now_utc(),
REGISTRATION_SECRET_PURPOSE,
&msg.username,
&msg.user_id,
)
.execute(&self.pool)
.await?;
@ -26,6 +26,8 @@ impl CreateVerificationSecretOutDBPort for DBOutPostgresAdapter {
#[cfg(test)]
mod tests {
use crate::utils::uuid::tests::UUID;
use super::*;
#[actix_rt::test]
@ -40,7 +42,7 @@ mod tests {
let msg = CreateSecretMsgBuilder::default()
.secret("secret".into())
.username("username".into())
.user_id(UUID.clone())
.build()
.unwrap();

View file

@ -12,12 +12,12 @@ impl DeleteVerificationSecretOutDBPort for DBOutPostgresAdapter {
"DELETE FROM
verification_otp
WHERE
username = $1
user_id = $1
AND
purpose = $2
AND
secret = $3;",
msg.username,
msg.user_id,
REGISTRATION_SECRET_PURPOSE,
msg.secret,
)
@ -30,13 +30,16 @@ impl DeleteVerificationSecretOutDBPort for DBOutPostgresAdapter {
#[cfg(test)]
mod tests {
use super::*;
use crate::identity::application::port::output::db::{
create_verification_secret::*, verification_secret_exists::*,
use crate::{
identity::application::port::output::db::{
create_verification_secret::*, verification_secret_exists::*,
},
utils::uuid::tests::UUID,
};
#[actix_rt::test]
async fn test_postgres_delete_verification_secret() {
let username = "batman";
let user_id = UUID;
let secret = "bsdasdf";
let settings = crate::settings::tests::get_settings().await;
settings.create_db().await;
@ -46,7 +49,7 @@ mod tests {
.unwrap(),
);
let msg = VerifySecretExistsMsgBuilder::default()
.username(username.into())
.user_id(user_id.clone())
.secret(secret.into())
.build()
.unwrap();
@ -56,7 +59,7 @@ mod tests {
let create_msg = CreateSecretMsgBuilder::default()
.secret(secret.into())
.username(username.into())
.user_id(user_id.clone())
.build()
.unwrap();
db.create_verification_secret(create_msg).await.unwrap();

View file

@ -33,6 +33,8 @@ impl EmailExistsOutDBPort for DBOutPostgresAdapter {
mod tests {
use super::*;
use crate::utils::uuid::tests::*;
#[actix_rt::test]
async fn test_postgres_email_exists() {
let email = "foo@exmaple.com";
@ -49,12 +51,13 @@ mod tests {
sqlx::query!(
"INSERT INTO user_query
(view_id, version, username, email, hashed_password)
VALUES ($1, $2, $3, $4, $5);",
"1",
(user_id, version, first_name, email, hashed_password, last_name)
VALUES ($1, $2, $3, $4, $5, $6);",
&UUID,
1,
"foo",
email,
"passwd",
"passwd"
)
.execute(&db.pool)

View file

@ -2,13 +2,15 @@
//
// SPDX-License-Identifier: AGPL-3.0-or-later
use uuid::Uuid;
use super::errors::*;
use super::DBOutPostgresAdapter;
use crate::identity::application::port::output::db::{errors::*, get_verification_secret::*};
#[async_trait::async_trait]
impl GetVerificationSecretOutDBPort for DBOutPostgresAdapter {
async fn get_verification_secret(&self, username: &str) -> OutDBPortResult<String> {
async fn get_verification_secret(&self, user_id: &Uuid) -> OutDBPortResult<String> {
struct Secret {
secret: String,
}
@ -19,10 +21,10 @@ impl GetVerificationSecretOutDBPort for DBOutPostgresAdapter {
FROM
verification_otp
WHERE
username = $1
user_id = $1
AND
purpose = $2;",
username,
user_id,
REGISTRATION_SECRET_PURPOSE,
)
.fetch_one(&self.pool)
@ -35,11 +37,14 @@ impl GetVerificationSecretOutDBPort for DBOutPostgresAdapter {
#[cfg(test)]
mod tests {
use super::*;
use crate::identity::application::port::output::db::create_verification_secret::*;
use crate::{
identity::application::port::output::db::create_verification_secret::*,
utils::uuid::tests::UUID,
};
#[actix_rt::test]
async fn test_postgres_get_verification_secret() {
let username = "batman";
let user_id = UUID;
let secret = "bsdasdf";
let settings = crate::settings::tests::get_settings().await;
settings.create_db().await;
@ -49,19 +54,19 @@ mod tests {
.unwrap(),
);
assert_eq!(
db.get_verification_secret(username).await.err(),
db.get_verification_secret(&user_id).await.err(),
Some(OutDBPortError::VerificationOTPSecretNotFound)
);
let create_msg = CreateSecretMsgBuilder::default()
.secret(secret.into())
.username(username.into())
.user_id(user_id.clone())
.build()
.unwrap();
db.create_verification_secret(create_msg).await.unwrap();
assert_eq!(db.get_verification_secret(username).await.unwrap(), secret);
assert_eq!(db.get_verification_secret(&user_id).await.unwrap(), secret);
settings.drop_db().await;
}

View file

@ -14,8 +14,8 @@ pub mod delete_verification_secret;
pub mod email_exists;
mod errors;
pub mod get_verification_secret;
pub mod user_id_exists;
pub mod user_view;
pub mod username_exists;
pub mod verification_secret_exists;
#[derive(Clone)]

View file

@ -2,22 +2,24 @@
//
// SPDX-License-Identifier: AGPL-3.0-or-later
use uuid::Uuid;
use super::DBOutPostgresAdapter;
use crate::identity::application::port::output::db::{
errors::*, username_exists::UsernameExistsOutDBPort,
errors::*, user_id_exists::UserIDExistsOutDBPort,
};
#[async_trait::async_trait]
impl UsernameExistsOutDBPort for DBOutPostgresAdapter {
async fn username_exists(&self, username: &str) -> OutDBPortResult<bool> {
impl UserIDExistsOutDBPort for DBOutPostgresAdapter {
async fn user_id_exists(&self, user_id: &Uuid) -> OutDBPortResult<bool> {
let res = sqlx::query!(
"SELECT EXISTS (
SELECT 1
FROM user_query
WHERE
username = $1
user_id = $1
);",
username
user_id,
)
.fetch_one(&self.pool)
.await?;
@ -33,9 +35,12 @@ impl UsernameExistsOutDBPort for DBOutPostgresAdapter {
mod tests {
use super::*;
use crate::utils::uuid::tests::UUID;
use crate::identity::domain::aggregate::*;
#[actix_rt::test]
async fn test_postgres_username_exists() {
let username = "foo@exmaple.com";
async fn test_postgres_user_id_exists() {
let settings = crate::settings::tests::get_settings().await;
settings.create_db().await;
let db = super::DBOutPostgresAdapter::new(
@ -44,25 +49,28 @@ mod tests {
.unwrap(),
);
let user = User::default();
// state doesn't exist
assert!(!db.username_exists(username).await.unwrap());
assert!(!db.user_id_exists(&UUID).await.unwrap());
sqlx::query!(
"INSERT INTO user_query
(view_id, version, username, email, hashed_password)
VALUES ($1, $2, $3, $4, $5);",
"1",
(version, user_id, email, hashed_password, first_name, last_name)
VALUES ($1, $2, $3, $4, $5, $6);",
1,
username,
"foo",
"passwd"
UUID,
user.email(),
user.hashed_password(),
user.first_name(),
user.last_name(),
)
.execute(&db.pool)
.await
.unwrap();
// state exists
assert!(db.username_exists(username).await.unwrap());
assert!(db.user_id_exists(&UUID).await.unwrap());
settings.drop_db().await;
}

View file

@ -6,18 +6,24 @@ use async_trait::async_trait;
use cqrs_es::persist::GenericQuery;
use cqrs_es::persist::{PersistenceError, ViewContext, ViewRepository};
use cqrs_es::{EventEnvelope, Query, View};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use super::errors::*;
use super::DBOutPostgresAdapter;
use crate::identity::application::services::events::UserEvent;
use crate::identity::domain::aggregate::User;
use serde::{Deserialize, Serialize};
use crate::utils::parse_aggregate_id::parse_aggregate_id;
pub const NEW_USER_NON_UUID: &str = "new_user_non_uuid-asdfa";
// The view for a User query, for a standard http application this should
// be designed to reflect the response dto that will be returned to a user.
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct UserView {
username: String,
first_name: String,
last_name: String,
user_id: Uuid,
email: String,
hashed_password: String,
is_admin: bool,
@ -32,12 +38,14 @@ impl View<User> for UserView {
fn update(&mut self, event: &EventEnvelope<User>) {
match &event.payload {
UserEvent::UserRegistered(val) => {
self.username = val.username().into();
self.email = val.email().into();
self.hashed_password = val.hashed_password().into();
self.is_admin = val.is_admin().to_owned();
self.is_verified = val.is_verified().to_owned();
self.deleted = false;
self.first_name = val.first_name().into();
self.last_name = val.last_name().into();
self.user_id = val.user_id().clone();
}
UserEvent::UserDeleted => self.deleted = true,
UserEvent::Loggedin(_) => (),
@ -58,16 +66,21 @@ impl View<User> for UserView {
#[async_trait]
impl ViewRepository<UserView, User> for DBOutPostgresAdapter {
async fn load(&self, view_id: &str) -> Result<Option<UserView>, PersistenceError> {
async fn load(&self, user_id: &str) -> Result<Option<UserView>, PersistenceError> {
let user_id = match parse_aggregate_id(user_id, NEW_USER_NON_UUID)? {
Some((val, _)) => return Ok(Some(val)),
None => Uuid::parse_str(user_id).unwrap(),
};
let res = sqlx::query_as!(
UserView,
"SELECT
username, email, hashed_password, is_admin, is_verified, deleted
first_name, last_name, user_id, email, hashed_password, is_admin, is_verified, deleted
FROM
user_query
WHERE
view_id = $1;",
view_id
user_id = $1;",
user_id
)
.fetch_one(&self.pool)
.await
@ -77,17 +90,22 @@ impl ViewRepository<UserView, User> for DBOutPostgresAdapter {
async fn load_with_context(
&self,
view_id: &str,
user_id: &str,
) -> Result<Option<(UserView, ViewContext)>, PersistenceError> {
let user_id = match parse_aggregate_id(user_id, NEW_USER_NON_UUID)? {
Some(val) => return Ok(Some(val)),
None => Uuid::parse_str(user_id).unwrap(),
};
let res = sqlx::query_as!(
UserView,
"SELECT
username, email, hashed_password, is_admin, is_verified, deleted
first_name, last_name, user_id, email, hashed_password, is_admin, is_verified, deleted
FROM
user_query
WHERE
view_id = $1;",
view_id
user_id = $1;",
user_id
)
.fetch_one(&self.pool)
.await
@ -95,24 +113,24 @@ impl ViewRepository<UserView, User> for DBOutPostgresAdapter {
struct Context {
version: i64,
view_id: String,
user_id: String,
}
let ctx = sqlx::query_as!(
Context,
"SELECT
view_id, version
user_id, version
FROM
user_query
WHERE
view_id = $1;",
view_id
user_id = $1;",
user_id
)
.fetch_one(&self.pool)
.await
.map_err(PostgresAggregateError::from)?;
let view_context = ViewContext::new(ctx.view_id, ctx.version);
let view_context = ViewContext::new(ctx.user_id.to_string(), ctx.version);
Ok(Some((res, view_context)))
}
@ -126,19 +144,20 @@ impl ViewRepository<UserView, User> for DBOutPostgresAdapter {
let version = context.version + 1;
sqlx::query!(
"INSERT INTO user_query (
view_id, version, username, email,
hashed_password, is_admin, is_verified, deleted
version, first_name, last_name, email,
hashed_password, is_admin, is_verified, deleted, user_id
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8
$1, $2, $3, $4, $5, $6, $7, $8, $9
);",
context.view_instance_id,
version,
view.username,
view.first_name,
view.last_name,
view.email,
view.hashed_password,
view.is_admin,
view.is_verified,
view.deleted,
view.user_id,
)
.execute(&self.pool)
.await
@ -150,16 +169,18 @@ impl ViewRepository<UserView, User> for DBOutPostgresAdapter {
"UPDATE
user_query
SET
view_id = $1, version = $2, username = $3, email = $4,
hashed_password = $5, is_admin = $6, is_verified = $7, deleted = $8;",
context.view_instance_id,
user_id = $1, version = $2, first_name = $3, email = $4,
hashed_password = $5, is_admin = $6, is_verified = $7, deleted = $8,
last_name=$9;",
view.user_id,
version,
view.username,
view.first_name,
view.email,
view.hashed_password,
view.is_admin,
view.is_verified,
view.deleted,
view.last_name,
)
.execute(&self.pool)
.await

View file

@ -16,13 +16,13 @@ impl VerificationSecretExistsOutDBPort for DBOutPostgresAdapter {
SELECT 1
FROM verification_otp
WHERE
username = $1
user_id = $1
AND
purpose = $2
AND
secret = $3
);",
msg.username,
msg.user_id,
REGISTRATION_SECRET_PURPOSE,
msg.secret,
)
@ -39,11 +39,14 @@ impl VerificationSecretExistsOutDBPort for DBOutPostgresAdapter {
#[cfg(test)]
mod tests {
use super::*;
use crate::identity::application::port::output::db::create_verification_secret::*;
use crate::{
identity::application::port::output::db::create_verification_secret::*,
utils::uuid::tests::UUID,
};
#[actix_rt::test]
async fn test_postgres_verification_secret_exists() {
let username = "batman";
let user_id = UUID;
let secret = "bsdasdf";
let settings = crate::settings::tests::get_settings().await;
settings.create_db().await;
@ -53,7 +56,7 @@ mod tests {
.unwrap(),
);
let msg = VerifySecretExistsMsgBuilder::default()
.username(username.into())
.user_id(user_id.clone())
.secret(secret.into())
.build()
.unwrap();
@ -63,7 +66,7 @@ mod tests {
let create_msg = CreateSecretMsgBuilder::default()
.secret(secret.into())
.username(username.into())
.user_id(user_id.clone())
.build()
.unwrap();
db.create_verification_secret(create_msg).await.unwrap();

View file

@ -12,17 +12,17 @@ impl AccountValidationLinkOutMailerPort for LettreMailer {
async fn account_validation_link(
&self,
to: &str,
username: &str,
first_name: &str,
validation_secret: &str,
) -> OutMailerPortResult<()> {
let email = Message::builder()
.from(self.from.parse().unwrap())
.reply_to(self.reply_to.parse().unwrap())
.to(format!("{username} <{to}>").parse().unwrap())
.to(format!("{first_name} <{to}>").parse().unwrap())
.subject("Please verify your account on Vanikam") // TODO: use better title
.header(ContentType::TEXT_PLAIN)
.body(format!(
r#"Hello {username},
r#"Hello {first_name},
Please click here to verify your Vanikam account: {validation_secret}
Warm regards,
Vanikam Admin

View file

@ -1,325 +0,0 @@
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
use async_trait::async_trait;
use cqrs_es::Aggregate;
use crate::identity::application::services::errors::*;
use crate::identity::application::services::events::UserEvent;
use crate::identity::application::services::UserCommand;
use crate::identity::application::services::UserServicesInterface;
use crate::identity::domain::aggregate::User;
use crate::identity::domain::aggregate::UserBuilder;
#[async_trait]
impl Aggregate for User {
type Command = UserCommand;
type Event = UserEvent;
type Error = IdentityError;
type Services = std::sync::Arc<dyn UserServicesInterface>;
// This identifier should be unique to the system.
fn aggregate_type() -> String {
"account".to_string()
}
// The aggregate logic goes here. Note that this will be the _bulk_ of a CQRS system
// so expect to use helper functions elsewhere to keep the code clean.
async fn handle(
&self,
command: Self::Command,
services: &Self::Services,
) -> Result<Vec<Self::Event>, Self::Error> {
match command {
UserCommand::RegisterUser(cmd) => {
let res = services.register_user().register_user(cmd).await?;
Ok(vec![UserEvent::UserRegistered(res)])
}
UserCommand::DeleteUser(cmd) => {
services.delete_user().delete_user(cmd).await;
Ok(vec![UserEvent::UserDeleted])
}
UserCommand::Login(cmd) => {
let res = services.login().login(cmd).await;
Ok(vec![UserEvent::Loggedin(res)])
}
UserCommand::UpdatePassword(cmd) => {
let res = services.update_password().update_password(cmd).await;
Ok(vec![UserEvent::PasswordUpdated(res)])
}
UserCommand::UpdateEmail(cmd) => {
let res = services.update_email().update_email(cmd).await?;
Ok(vec![UserEvent::EmailUpdated(res)])
}
UserCommand::MarkUserVerified(cmd) => {
services
.mark_user_verified()
.mark_user_verified(cmd)
.await?;
Ok(vec![UserEvent::UserVerified])
}
UserCommand::SetAdmin(cmd) => {
let res = services.set_user_admin().set_user_admin(cmd).await;
Ok(vec![UserEvent::UserPromotedToAdmin(res)])
}
UserCommand::ResendVerificationEmail(cmd) => {
services
.resend_verification_email()
.resend_verification_email(cmd)
.await?;
Ok(vec![UserEvent::VerificationEmailResent])
}
}
}
fn apply(&mut self, event: Self::Event) {
match event {
UserEvent::UserRegistered(e) => {
*self = UserBuilder::default()
.username(e.username().into())
.email(e.email().into())
.hashed_password(e.hashed_password().into())
.is_admin(e.is_admin().to_owned())
.email_verified(e.email_verified().to_owned())
.is_verified(e.is_verified().to_owned())
.deleted(false)
.build()
.unwrap();
}
UserEvent::UserDeleted => {
self.set_deleted(true);
}
UserEvent::Loggedin(_) => (),
UserEvent::PasswordUpdated(_) => (),
UserEvent::EmailUpdated(e) => {
self.set_email(e.new_email().into());
self.set_email_verified(false);
}
UserEvent::UserVerified => {
self.set_is_verified(true);
self.set_email_verified(true);
}
UserEvent::UserPromotedToAdmin(_) => {
self.set_is_admin(true);
}
UserEvent::VerificationEmailResent => (),
}
}
}
//// The aggregate tests are the most important part of a CQRS system.
//// The simplicity and flexibility of these tests are a good part of what
//// makes an event sourced system so friendly to changing business requirements.
//#[cfg(test)]
//mod aggregate_tests {
// use async_trait::async_trait;
// use std::sync::Mutex;
//
// use cqrs_es::test::TestFramework;
//
// use crate::domain::aggregate::User;
// use crate::domain::commands::UserCommand;
// use crate::domain::events::UserEvent;
// use crate::services::{AtmError, UserApi, UserServices, CheckingError};
//
// // A test framework that will apply our events and command
// // and verify that the logic works as expected.
// type AccountTestFramework = TestFramework<User>;
//
// #[test]
// fn test_deposit_money() {
// let expected = UserEvent::CustomerDepositedMoney {
// amount: 200.0,
// balance: 200.0,
// };
// let command = UserCommand::DepositMoney { amount: 200.0 };
// let services = UserServices::new(Box::new(MockUserServices::default()));
// // Obtain a new test framework
// AccountTestFramework::with(services)
// // In a test case with no previous events
// .given_no_previous_events()
// // Wnen we fire this command
// .when(command)
// // then we expect these results
// .then_expect_events(vec![expected]);
// }
//
// #[test]
// fn test_deposit_money_with_balance() {
// let previous = UserEvent::CustomerDepositedMoney {
// amount: 200.0,
// balance: 200.0,
// };
// let expected = UserEvent::CustomerDepositedMoney {
// amount: 200.0,
// balance: 400.0,
// };
// let command = UserCommand::DepositMoney { amount: 200.0 };
// let services = UserServices::new(Box::new(MockUserServices::default()));
//
// AccountTestFramework::with(services)
// // Given this previously applied event
// .given(vec![previous])
// // When we fire this command
// .when(command)
// // Then we expect this resultant event
// .then_expect_events(vec![expected]);
// }
//
// #[test]
// fn test_withdraw_money() {
// let previous = UserEvent::CustomerDepositedMoney {
// amount: 200.0,
// balance: 200.0,
// };
// let expected = UserEvent::CustomerWithdrewCash {
// amount: 100.0,
// balance: 100.0,
// };
// let services = MockUserServices::default();
// services.set_atm_withdrawal_response(Ok(()));
// let command = UserCommand::WithdrawMoney {
// amount: 100.0,
// atm_id: "ATM34f1ba3c".to_string(),
// };
//
// AccountTestFramework::with(UserServices::new(Box::new(services)))
// .given(vec![previous])
// .when(command)
// .then_expect_events(vec![expected]);
// }
//
// #[test]
// fn test_withdraw_money_client_error() {
// let previous = UserEvent::CustomerDepositedMoney {
// amount: 200.0,
// balance: 200.0,
// };
// let services = MockUserServices::default();
// services.set_atm_withdrawal_response(Err(AtmError));
// let command = UserCommand::WithdrawMoney {
// amount: 100.0,
// atm_id: "ATM34f1ba3c".to_string(),
// };
//
// let services = UserServices::new(Box::new(services));
// AccountTestFramework::with(services)
// .given(vec![previous])
// .when(command)
// .then_expect_error_message("atm rule violation");
// }
//
// #[test]
// fn test_withdraw_money_funds_not_available() {
// let command = UserCommand::WithdrawMoney {
// amount: 200.0,
// atm_id: "ATM34f1ba3c".to_string(),
// };
//
// let services = UserServices::new(Box::new(MockUserServices::default()));
// AccountTestFramework::with(services)
// .given_no_previous_events()
// .when(command)
// // Here we expect an error rather than any events
// .then_expect_error_message("funds not available")
// }
//
// #[test]
// fn test_wrote_check() {
// let previous = UserEvent::CustomerDepositedMoney {
// amount: 200.0,
// balance: 200.0,
// };
// let expected = UserEvent::CustomerWroteCheck {
// check_number: "1170".to_string(),
// amount: 100.0,
// balance: 100.0,
// };
// let services = MockUserServices::default();
// services.set_validate_check_response(Ok(()));
// let services = UserServices::new(Box::new(services));
// let command = UserCommand::WriteCheck {
// check_number: "1170".to_string(),
// amount: 100.0,
// };
//
// AccountTestFramework::with(services)
// .given(vec![previous])
// .when(command)
// .then_expect_events(vec![expected]);
// }
//
// #[test]
// fn test_wrote_check_bad_check() {
// let previous = UserEvent::CustomerDepositedMoney {
// amount: 200.0,
// balance: 200.0,
// };
// let services = MockUserServices::default();
// services.set_validate_check_response(Err(CheckingError));
// let services = UserServices::new(Box::new(services));
// let command = UserCommand::WriteCheck {
// check_number: "1170".to_string(),
// amount: 100.0,
// };
//
// AccountTestFramework::with(services)
// .given(vec![previous])
// .when(command)
// .then_expect_error_message("check invalid");
// }
//
// #[test]
// fn test_wrote_check_funds_not_available() {
// let command = UserCommand::WriteCheck {
// check_number: "1170".to_string(),
// amount: 100.0,
// };
//
// let services = UserServices::new(Box::new(MockUserServices::default()));
// AccountTestFramework::with(services)
// .given_no_previous_events()
// .when(command)
// .then_expect_error_message("funds not available")
// }
//
// pub struct MockUserServices {
// atm_withdrawal_response: Mutex<Option<Result<(), AtmError>>>,
// validate_check_response: Mutex<Option<Result<(), CheckingError>>>,
// }
//
// impl Default for MockUserServices {
// fn default() -> Self {
// Self {
// atm_withdrawal_response: Mutex::new(None),
// validate_check_response: Mutex::new(None),
// }
// }
// }
//
// impl MockUserServices {
// fn set_atm_withdrawal_response(&self, response: Result<(), AtmError>) {
// *self.atm_withdrawal_response.lock().unwrap() = Some(response);
// }
// fn set_validate_check_response(&self, response: Result<(), CheckingError>) {
// *self.validate_check_response.lock().unwrap() = Some(response);
// }
// }
//
// #[async_trait]
// impl UserApi for MockUserServices {
// async fn atm_withdrawal(&self, _atm_id: &str, _amount: f64) -> Result<(), AtmError> {
// self.atm_withdrawal_response.lock().unwrap().take().unwrap()