diff --git a/Directory.Packages.props b/Directory.Packages.props index 0d8c4b53e..ce530bfea 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -19,6 +19,7 @@ + diff --git a/docker-bake.hcl b/docker-bake.hcl index 722bb3840..54f0618ce 100644 --- a/docker-bake.hcl +++ b/docker-bake.hcl @@ -46,7 +46,7 @@ target "awsecr_staging" { tags = ["${awsecr_repostory}:staging"] } -# Printing +# Production target "production" { context = "." dockerfile = "Dockerfile.production" diff --git a/sdk/js/react/src/api/catalog/products/gallery/index.ts b/sdk/js/react/src/api/catalog/products/gallery/index.ts index aa6006b19..daa90685c 100644 --- a/sdk/js/react/src/api/catalog/products/gallery/index.ts +++ b/sdk/js/react/src/api/catalog/products/gallery/index.ts @@ -8,7 +8,9 @@ export const all = async (req: allResources.Request) => await axios.get>(allResources.url(req)); export const single = async (req: singleResources.Request) => - await axios.get(singleResources.url(req)); + await axios.get(singleResources.url(req), { + headers: { 'X-Viewed-Product': req.viewed }, + }); export const sortings = async () => await axios.get(sortingsResources.url()); diff --git a/sdk/js/react/src/api/catalog/products/gallery/single.ts b/sdk/js/react/src/api/catalog/products/gallery/single.ts index c5452dc23..7db49d231 100644 --- a/sdk/js/react/src/api/catalog/products/gallery/single.ts +++ b/sdk/js/react/src/api/catalog/products/gallery/single.ts @@ -2,6 +2,7 @@ import { CategoryDto, Counts, GALLERY_BASE_PATH } from '@/api/catalog/common'; export type Request = { id: string; + viewed?: boolean; }; export type Response = { diff --git a/sdk/js/react/src/api/common/exchange-rates/common.ts b/sdk/js/react/src/api/common/exchange-rates/common.ts index f0b79a9b3..d4fe0c3ae 100644 --- a/sdk/js/react/src/api/common/exchange-rates/common.ts +++ b/sdk/js/react/src/api/common/exchange-rates/common.ts @@ -1,4 +1,4 @@ -import { Currency } from '@/types'; +import { Currency } from '@/constants'; export type ExchangeRate = { date: string; diff --git a/sdk/js/react/src/api/identity/common.ts b/sdk/js/react/src/api/identity/common.ts index 0bf3b2980..b591746f2 100644 --- a/sdk/js/react/src/api/identity/common.ts +++ b/sdk/js/react/src/api/identity/common.ts @@ -1 +1,14 @@ +export type ViewedProduct = { + id: string; + viewedAt: string; +}; + +export type Fingerprint = { + id: string; + device: string; + location?: string; + deleteAllowed: boolean; + issuedAt: string; +}; + export const IDENTITY_BASE_PATH = '/identity'; diff --git a/sdk/js/react/src/api/identity/identity/change-names.ts b/sdk/js/react/src/api/identity/identity/change-names.ts new file mode 100644 index 000000000..17a4b525c --- /dev/null +++ b/sdk/js/react/src/api/identity/identity/change-names.ts @@ -0,0 +1,9 @@ +import { IDENTITY_BASE_PATH } from '../common'; + +export type Request = { + username: string; + firstName?: string; + lastName?: string; +}; + +export const url = () => `${IDENTITY_BASE_PATH}/names`; diff --git a/sdk/js/react/src/api/identity/identity/change-username.ts b/sdk/js/react/src/api/identity/identity/change-username.ts deleted file mode 100644 index 215c3708a..000000000 --- a/sdk/js/react/src/api/identity/identity/change-username.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { IDENTITY_BASE_PATH } from '../common'; - -export type Request = { - username: string; -}; - -export const url = () => `${IDENTITY_BASE_PATH}/username`; diff --git a/sdk/js/react/src/api/identity/identity/delete-fingerprint.ts b/sdk/js/react/src/api/identity/identity/delete-fingerprint.ts new file mode 100644 index 000000000..c2056ba4c --- /dev/null +++ b/sdk/js/react/src/api/identity/identity/delete-fingerprint.ts @@ -0,0 +1,7 @@ +import { IDENTITY_BASE_PATH } from '../common'; + +export type Request = { + refreshTokenId: string; +}; + +export const url = () => `${IDENTITY_BASE_PATH}/fingerprint`; diff --git a/sdk/js/react/src/api/identity/identity/delete-viewed-product.ts b/sdk/js/react/src/api/identity/identity/delete-viewed-product.ts new file mode 100644 index 000000000..9a3225e41 --- /dev/null +++ b/sdk/js/react/src/api/identity/identity/delete-viewed-product.ts @@ -0,0 +1,7 @@ +import { IDENTITY_BASE_PATH } from '../common'; + +export type Request = { + productId: string; +}; + +export const url = () => `${IDENTITY_BASE_PATH}/viewed-product`; diff --git a/sdk/js/react/src/api/identity/identity/index.ts b/sdk/js/react/src/api/identity/identity/index.ts index 3a982ae45..07cc2e831 100644 --- a/sdk/js/react/src/api/identity/identity/index.ts +++ b/sdk/js/react/src/api/identity/identity/index.ts @@ -1,4 +1,4 @@ -import { axios } from '@/api/axios'; +import { axios, config } from '@/api/axios'; import * as authnResources from './authn'; import * as authzResources from './authz'; import * as myAccountResources from './my-account'; @@ -7,7 +7,9 @@ import * as loginResources from './login'; import * as refreshResources from './refresh'; import * as logoutResources from './logout'; import * as deleteResources from './delete'; -import * as changeUsernameResources from './change-username'; +import * as deleteViewedProductResources from './delete-viewed-product'; +import * as deleteFingerprintResources from './delete-fingerprint'; +import * as changeNamesResources from './change-names'; import * as toggleTrackViewedProductsResources from './toggle-track-viewed-products'; import * as forgotPasswordResources from './forgot-password'; import * as resetPasswordResources from './reset-password'; @@ -37,14 +39,27 @@ export const refresh = async () => export const logout = async () => await axios.post(logoutResources.url()); -export const changeUsername = async (req: changeUsernameResources.Request) => - await axios.patch(changeUsernameResources.url(), req); +export const changeUsername = async (req: changeNamesResources.Request) => + await axios.patch(changeNamesResources.url(), req); export const toggleTrackViewedProducts = async () => await axios.patch(toggleTrackViewedProductsResources.url()); export const delete_ = async () => await axios.delete(deleteResources.url()); +export const deleteViewedProduct = async ( + req: deleteViewedProductResources.Request, +) => + await axios.delete( + deleteViewedProductResources.url(), + config({ data: req }), + ); + +export const deleteFingerprint = async ( + req: deleteFingerprintResources.Request, +) => + await axios.delete(deleteFingerprintResources.url(), config({ data: req })); + export const forgotPassword = async (req: forgotPasswordResources.Request) => await axios.post(forgotPasswordResources.url(), req); diff --git a/sdk/js/react/src/api/identity/identity/my-account.ts b/sdk/js/react/src/api/identity/identity/my-account.ts index a4db8d8a8..3d4129ce3 100644 --- a/sdk/js/react/src/api/identity/identity/my-account.ts +++ b/sdk/js/react/src/api/identity/identity/my-account.ts @@ -1,4 +1,4 @@ -import { IDENTITY_BASE_PATH } from '../common'; +import { IDENTITY_BASE_PATH, ViewedProduct, Fingerprint } from '../common'; export type Response = { id: string; @@ -9,6 +9,8 @@ export type Response = { email: string; trackViewedProducts: boolean; createdAt: string; + viewedProducts: ViewedProduct[]; + fingerprints: Fingerprint[]; }; export const url = () => `${IDENTITY_BASE_PATH}/my-account`; diff --git a/sdk/js/react/src/api/identity/identity/types.ts b/sdk/js/react/src/api/identity/identity/types.ts index b4194566f..521a0d01b 100644 --- a/sdk/js/react/src/api/identity/identity/types.ts +++ b/sdk/js/react/src/api/identity/identity/types.ts @@ -1,6 +1,7 @@ +export type { ViewedProduct, Fingerprint } from '../common'; export type { Response as AuthnResponse } from './authn'; export type { Response as AuthzResponse } from './authz'; -export type { Request as ChangeUsernameRequest } from './change-username'; +export type { Request as ChangeNamesRequest } from './change-names'; export type { Request as ConfirmEmailRequest } from './confirm-email'; export type { Response as DownloadUserInfoResponse } from './download-info'; export type { Request as ForgotPasswordRequest } from './forgot-password'; diff --git a/sdk/js/react/src/constants.ts b/sdk/js/react/src/constants.ts index e8554b571..ecfb4e2e6 100644 --- a/sdk/js/react/src/constants.ts +++ b/sdk/js/react/src/constants.ts @@ -1,45 +1,39 @@ -import { Currency, Rates, Symbols } from './types'; - export const EXCHANGE_RATES = { - EUR: { rate: 1, symbol: '€', language: '' }, - USD: { rate: 1.1646, symbol: '$', language: 'en-US' }, - JPY: { rate: 173.1, symbol: '¥', language: 'ja-JP' }, - BGN: { rate: 1.9558, symbol: 'лв', language: 'bg-BG' }, - CZK: { rate: 24.485, symbol: 'Kč', language: 'cs-CZ' }, - DKK: { rate: 7.4632, symbol: 'kr', language: 'da-DK' }, - GBP: { rate: 0.8702, symbol: '£', language: 'en-GB' }, - HUF: { rate: 395.58, symbol: 'Ft', language: 'hu-HU' }, - PLN: { rate: 4.2653, symbol: 'zł', language: 'pl-PL' }, - RON: { rate: 5.0822, symbol: 'lei', language: 'ro-RO' }, - SEK: { rate: 11.003, symbol: 'kr', language: 'sv-SE' }, - CHF: { rate: 0.9366, symbol: 'CHF', language: 'fr-CH' }, - ISK: { rate: 143.6, symbol: 'Íkr', language: 'is-IS' }, - NOK: { rate: 11.674, symbol: 'kr', language: 'no-NO' }, - TRY: { rate: 47.9289, symbol: '₺', language: 'tr-TR' }, - AUD: { rate: 1.7897, symbol: 'A$', language: 'en-AU' }, - BRL: { rate: 6.3757, symbol: 'R$', language: 'pt-BR' }, - CAD: { rate: 1.6056, symbol: 'CA$', language: 'en-CA' }, - CNY: { rate: 8.3202, symbol: '¥', language: 'zh-CN' }, - HKD: { rate: 9.0915, symbol: 'HK$', language: 'zh-HK' }, - IDR: { rate: 19110.27, symbol: 'Rp', language: 'id-ID' }, - ILS: { rate: 3.9478, symbol: '₪', language: 'he-IL' }, - INR: { rate: 102.5795, symbol: '₹', language: 'hi-IN' }, - KRW: { rate: 1625.01, symbol: '₩', language: 'ko-KR' }, - MXN: { rate: 21.8726, symbol: '$', language: 'es-MX' }, - MYR: { rate: 4.9263, symbol: 'RM', language: 'ms-MY' }, - NZD: { rate: 1.9892, symbol: '$', language: 'en-NZ' }, - PHP: { rate: 66.762, symbol: '₱', language: 'fil-PH' }, - SGD: { rate: 1.5006, symbol: 'S$', language: 'en-SG' }, - THB: { rate: 37.704, symbol: '฿', language: 'th-TH' }, - ZAR: { rate: 20.6439, symbol: 'R', language: 'en-ZA' }, + EUR: { rate: 1, symbol: '€', language: '', display: true }, + USD: { rate: 1.1646, symbol: '$', language: 'en-US', display: true }, + JPY: { rate: 173.1, symbol: '¥', language: 'ja-JP', display: true }, + BGN: { rate: 1.9558, symbol: 'лв', language: 'bg-BG', display: false }, + CZK: { rate: 24.485, symbol: 'Kč', language: 'cs-CZ', display: true }, + DKK: { rate: 7.4632, symbol: 'kr', language: 'da-DK', display: true }, + GBP: { rate: 0.8702, symbol: '£', language: 'en-GB', display: true }, + HUF: { rate: 395.58, symbol: 'Ft', language: 'hu-HU', display: true }, + PLN: { rate: 4.2653, symbol: 'zł', language: 'pl-PL', display: true }, + RON: { rate: 5.0822, symbol: 'lei', language: 'ro-RO', display: true }, + SEK: { rate: 11.003, symbol: 'kr', language: 'sv-SE', display: true }, + CHF: { rate: 0.9366, symbol: 'CHF', language: 'fr-CH', display: true }, + ISK: { rate: 143.6, symbol: 'Íkr', language: 'is-IS', display: true }, + NOK: { rate: 11.674, symbol: 'kr', language: 'no-NO', display: true }, + TRY: { rate: 47.9289, symbol: '₺', language: 'tr-TR', display: true }, + AUD: { rate: 1.7897, symbol: 'A$', language: 'en-AU', display: true }, + BRL: { rate: 6.3757, symbol: 'R$', language: 'pt-BR', display: true }, + CAD: { rate: 1.6056, symbol: 'CA$', language: 'en-CA', display: true }, + CNY: { rate: 8.3202, symbol: '¥', language: 'zh-CN', display: true }, + HKD: { rate: 9.0915, symbol: 'HK$', language: 'zh-HK', display: true }, + IDR: { rate: 19110.27, symbol: 'Rp', language: 'id-ID', display: true }, + ILS: { rate: 3.9478, symbol: '₪', language: 'he-IL', display: true }, + INR: { rate: 102.5795, symbol: '₹', language: 'hi-IN', display: true }, + KRW: { rate: 1625.01, symbol: '₩', language: 'ko-KR', display: true }, + MXN: { rate: 21.8726, symbol: '$', language: 'es-MX', display: true }, + MYR: { rate: 4.9263, symbol: 'RM', language: 'ms-MY', display: true }, + NZD: { rate: 1.9892, symbol: '$', language: 'en-NZ', display: true }, + PHP: { rate: 66.762, symbol: '₱', language: 'fil-PH', display: true }, + SGD: { rate: 1.5006, symbol: 'S$', language: 'en-SG', display: true }, + THB: { rate: 37.704, symbol: '฿', language: 'th-TH', display: true }, + ZAR: { rate: 20.6439, symbol: 'R', language: 'en-ZA', display: true }, } as const; -export const CURRENCIES = Object.keys(EXCHANGE_RATES) as Currency[]; - -export const RATES: Rates = Object.fromEntries( - Object.entries(EXCHANGE_RATES).map(([k, v]) => [k, v.rate]), -) as never; +export type Currency = keyof typeof EXCHANGE_RATES; -export const SYMBOLS: Symbols = Object.fromEntries( - Object.entries(EXCHANGE_RATES).map(([k, v]) => [k, v.symbol]), -) as never; +export const CURRENCIES = Object.entries(EXCHANGE_RATES) + .filter(([, v]) => v.display) + .map(([k]) => k) as Array; diff --git a/sdk/js/react/src/hooks/index.ts b/sdk/js/react/src/hooks/index.ts index fb25256c2..9bcfb68a8 100644 --- a/sdk/js/react/src/hooks/index.ts +++ b/sdk/js/react/src/hooks/index.ts @@ -1,5 +1,6 @@ export * as queries from './queries'; export { useQuery } from './queries/useQuery'; export { useInfiniteQuery } from './queries/useInfiniteQuery'; +export { queryCall } from './queries/queryCall'; export * as mutations from './mutations'; export { useMutation } from './mutations/useMutation'; diff --git a/sdk/js/react/src/hooks/mutations/identity/identity.ts b/sdk/js/react/src/hooks/mutations/identity/identity.ts index 144226443..4355d3c7d 100644 --- a/sdk/js/react/src/hooks/mutations/identity/identity.ts +++ b/sdk/js/react/src/hooks/mutations/identity/identity.ts @@ -3,7 +3,9 @@ import { identityApi as api } from '@/api'; import { Request as Login } from '@/api/identity/identity/login'; import { Request as ForgotPassword } from '@/api/identity/identity/forgot-password'; import { Request as ResetPassword } from '@/api/identity/identity/reset-password'; -import { Request as ChangeUsername } from '@/api/identity/identity/change-username'; +import { Request as ChangeNames } from '@/api/identity/identity/change-names'; +import { Request as DeleteViewedProduct } from '@/api/identity/identity/delete-viewed-product'; +import { Request as DeleteFingerprint } from '@/api/identity/identity/delete-fingerprint'; import { Request as Register } from '@/api/identity/identity/register'; import { Request as ConfirmEmail } from '@/api/identity/identity/confirm-email'; import { Request as RetryConfirmEmail } from '@/api/identity/identity/retry-confirm-email'; @@ -22,9 +24,9 @@ export const identity = { mutationKey: [...BASE_KEY, 'refresh'], mutationFn: async () => (await api.refresh()).data, }), - changeUsername: mutationOptions({ - mutationKey: [...BASE_KEY, 'change-username'], - mutationFn: async (params: ChangeUsername) => + changeNames: mutationOptions({ + mutationKey: [...BASE_KEY, 'change-names'], + mutationFn: async (params: ChangeNames) => (await api.changeUsername(params)).data, }), toggleTrackViewedProducts: mutationOptions({ @@ -35,6 +37,16 @@ export const identity = { mutationKey: [...BASE_KEY, 'delete-my-account'], mutationFn: async () => (await api.delete_()).data, }), + deleteViewedProduct: mutationOptions({ + mutationKey: [...BASE_KEY, 'delete-viewed-product'], + mutationFn: async (params: DeleteViewedProduct) => + (await api.deleteViewedProduct(params)).data, + }), + deleteFingerprint: mutationOptions({ + mutationKey: [...BASE_KEY, 'delete-fingerprint'], + mutationFn: async (params: DeleteFingerprint) => + (await api.deleteFingerprint(params)).data, + }), forgotPassword: mutationOptions({ mutationKey: [...BASE_KEY, 'forgot-password'], mutationFn: async (params: ForgotPassword) => diff --git a/sdk/js/react/src/hooks/queries/queryCall.ts b/sdk/js/react/src/hooks/queries/queryCall.ts new file mode 100644 index 000000000..e19860ffe --- /dev/null +++ b/sdk/js/react/src/hooks/queries/queryCall.ts @@ -0,0 +1,15 @@ +import { AxiosResponse } from 'axios'; +import { FetchQueryOptions } from '@tanstack/react-query'; +import * as customcadsQueries from '.'; + +type Opts = FetchQueryOptions< + AxiosResponse, + Error, + AxiosResponse, + TKey +>; + +export const queryCall = ( + optionsSelector: (queries: typeof customcadsQueries) => Opts, + call: (options: Opts) => Promise | void>, +) => call(optionsSelector(customcadsQueries)); diff --git a/sdk/js/react/src/index.ts b/sdk/js/react/src/index.ts index ecae80f51..fa7ff4ce8 100644 --- a/sdk/js/react/src/index.ts +++ b/sdk/js/react/src/index.ts @@ -1,4 +1,3 @@ export * from './constants'; -export type { Currency, Rate, Symbol } from './types'; export * from './api'; export * from './hooks'; diff --git a/sdk/js/react/src/types.ts b/sdk/js/react/src/types.ts deleted file mode 100644 index 697d2f5ad..000000000 --- a/sdk/js/react/src/types.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { EXCHANGE_RATES } from './constants'; - -export type Currency = keyof typeof EXCHANGE_RATES; - -export type Rates = { - [C in Currency]: (typeof EXCHANGE_RATES)[C]['rate']; -}; -export type Rate = Rates[Currency]; - -export type Symbols = { - [C in Currency]: (typeof EXCHANGE_RATES)[C]['symbol']; -}; -export type Symbol = Symbols[Currency]; diff --git a/src/Modules/Accounts/API/Accounts/Endpoints/AccountsGroup.cs b/src/Modules/Accounts/API/Accounts/Endpoints/AccountsGroup.cs index 055628a99..3428c55a2 100644 --- a/src/Modules/Accounts/API/Accounts/Endpoints/AccountsGroup.cs +++ b/src/Modules/Accounts/API/Accounts/Endpoints/AccountsGroup.cs @@ -1,7 +1,7 @@ namespace CustomCADs.Modules.Accounts.API.Accounts.Endpoints; using static APIConstants; -using static DomainConstants.Roles; +using static DomainConstants.Users; public class AccountsGroup : Group { @@ -9,7 +9,7 @@ public AccountsGroup() { Configure(Paths.Accounts, x => { - x.Roles(Admin); + x.Roles(AdminRole); x.Description(opt => opt.WithTags(Tags[Paths.Accounts])); }); } diff --git a/src/Modules/Accounts/API/Roles/Endpoints/RolesGroup.cs b/src/Modules/Accounts/API/Roles/Endpoints/RolesGroup.cs index 939b315a8..7803b1a5a 100644 --- a/src/Modules/Accounts/API/Roles/Endpoints/RolesGroup.cs +++ b/src/Modules/Accounts/API/Roles/Endpoints/RolesGroup.cs @@ -1,7 +1,7 @@ namespace CustomCADs.Modules.Accounts.API.Roles.Endpoints; using static APIConstants; -using static DomainConstants.Roles; +using static DomainConstants.Users; public class RolesGroup : Group { @@ -9,7 +9,7 @@ public RolesGroup() { Configure(Paths.Roles, x => { - x.Roles(Admin); + x.Roles(AdminRole); x.Description(opt => opt.WithTags(Tags[Paths.Roles])); }); } diff --git a/src/Modules/Accounts/Application/Accounts/Commands/Internal/Delete/DeleteAccountHandler.cs b/src/Modules/Accounts/Application/Accounts/Commands/Internal/Delete/DeleteAccountHandler.cs index 2322a36fc..c64d1addc 100644 --- a/src/Modules/Accounts/Application/Accounts/Commands/Internal/Delete/DeleteAccountHandler.cs +++ b/src/Modules/Accounts/Application/Accounts/Commands/Internal/Delete/DeleteAccountHandler.cs @@ -22,8 +22,8 @@ public async Task Handle(DeleteAccountCommand req, CancellationToken ct) await uow.SaveChangesAsync(ct).ConfigureAwait(false); await raiser.RaiseApplicationEventAsync( - @event: new AccountDeletedApplicationEvent( - account.Username + @event: new AccountDeletedApplicationEvent( + account.Id ) ).ConfigureAwait(false); } diff --git a/src/Modules/Accounts/Application/Accounts/Commands/Shared/DeleteViewedProductHandler.cs b/src/Modules/Accounts/Application/Accounts/Commands/Shared/DeleteViewedProductHandler.cs new file mode 100644 index 000000000..bf1ad0a2f --- /dev/null +++ b/src/Modules/Accounts/Application/Accounts/Commands/Shared/DeleteViewedProductHandler.cs @@ -0,0 +1,14 @@ +using CustomCADs.Modules.Accounts.Domain.Repositories; +using CustomCADs.Modules.Accounts.Domain.Repositories.Writes; +using CustomCADs.Shared.Application.UseCases.Accounts.Commands; + +namespace CustomCADs.Modules.Accounts.Application.Accounts.Commands.Shared; + +public class DeleteViewedProductHandler(IAccountWrites writes, IUnitOfWork uow) : ICommandHandler +{ + public async Task Handle(DeleteViewedProductCommand req, CancellationToken ct = default) + { + await writes.UnviewProductAsync(req.CallerId, req.ProductId, ct).ConfigureAwait(false); + await uow.SaveChangesAsync(ct).ConfigureAwait(false); + } +} diff --git a/src/Modules/Accounts/Application/Accounts/Events/Application/UserEditedHandler.cs b/src/Modules/Accounts/Application/Accounts/Events/Application/UserEditedHandler.cs index 01a242781..5486874dc 100644 --- a/src/Modules/Accounts/Application/Accounts/Events/Application/UserEditedHandler.cs +++ b/src/Modules/Accounts/Application/Accounts/Events/Application/UserEditedHandler.cs @@ -13,12 +13,29 @@ public async Task HandleAsync(UserEditedApplicationEvent ae) if (ae.Username is not null) { - account.SetUsername(ae.Username); + if (ae.Username != account.Username) + { + account.SetUsername(ae.Username); + } } + if (ae.TrackViewedProducts is not null) { account.SetTrackViewedProducts(ae.TrackViewedProducts.Value); } + + if (ae.Names is not null) + { + if (ae.Names.FirstName != account.FirstName) + { + account.SetFirstName(ae.Names.FirstName); + } + if (ae.Names.LastName != account.LastName) + { + account.SetLastName(ae.Names.LastName); + } + } + await uow.SaveChangesAsync().ConfigureAwait(false); } } diff --git a/src/Modules/Accounts/Application/Accounts/Events/Application/UserViewedProductHandler.cs b/src/Modules/Accounts/Application/Accounts/Events/Application/UserViewedProductHandler.cs index d634d5405..f67e576cb 100644 --- a/src/Modules/Accounts/Application/Accounts/Events/Application/UserViewedProductHandler.cs +++ b/src/Modules/Accounts/Application/Accounts/Events/Application/UserViewedProductHandler.cs @@ -8,7 +8,7 @@ public class UserViewedProductHandler(IAccountWrites writes, IUnitOfWork uow) { public async Task HandleAsync(UserViewedProductApplicationEvent ae) { - await writes.ViewProductAsync(ae.AccountId, ae.Id).ConfigureAwait(false); + await writes.ViewProductAsync(ae.AccountId, ae.Id, ae.ViewedAt).ConfigureAwait(false); await uow.SaveChangesAsync().ConfigureAwait(false); } } diff --git a/src/Modules/Accounts/Application/Accounts/Queries/Shared/Exists/GetAccountExistsByUsernameHandler.cs b/src/Modules/Accounts/Application/Accounts/Queries/Shared/Exists/GetAccountExistsByUsernameHandler.cs new file mode 100644 index 000000000..c1997c85b --- /dev/null +++ b/src/Modules/Accounts/Application/Accounts/Queries/Shared/Exists/GetAccountExistsByUsernameHandler.cs @@ -0,0 +1,11 @@ +using CustomCADs.Modules.Accounts.Domain.Repositories.Reads; +using CustomCADs.Shared.Application.UseCases.Accounts.Queries; + +namespace CustomCADs.Modules.Accounts.Application.Accounts.Queries.Shared.Exists; + +public sealed class GetAccountExistsByUsernameHandler(IAccountReads reads) + : IQueryHandler +{ + public async Task Handle(GetAccountExistsByUsernameQuery req, CancellationToken ct) + => await reads.ExistsByUsernameAsync(req.Username, ct).ConfigureAwait(false); +} diff --git a/src/Modules/Accounts/Application/Accounts/Queries/Shared/Info/GetAccountInfoByUsernameHandler.cs b/src/Modules/Accounts/Application/Accounts/Queries/Shared/Info/GetAccountInfoByUsernameHandler.cs index 9a6da1c4b..000f4aff9 100644 --- a/src/Modules/Accounts/Application/Accounts/Queries/Shared/Info/GetAccountInfoByUsernameHandler.cs +++ b/src/Modules/Accounts/Application/Accounts/Queries/Shared/Info/GetAccountInfoByUsernameHandler.cs @@ -12,6 +12,7 @@ public async Task Handle(GetAccountInfoByUsernameQuery req, Canc ?? throw CustomNotFoundException.ByProp(nameof(req.Username), req.Username); return new( + Id: account.Id, CreatedAt: account.CreatedAt, TrackViewedProducts: account.TrackViewedProducts, FirstName: account.FirstName, diff --git a/src/Modules/Accounts/Application/Accounts/Queries/Shared/ViewedProduct/GetAccountViewedProductsByUsernameHandler.cs b/src/Modules/Accounts/Application/Accounts/Queries/Shared/ViewedProduct/GetAccountViewedProductsByUsernameHandler.cs deleted file mode 100644 index 28501e900..000000000 --- a/src/Modules/Accounts/Application/Accounts/Queries/Shared/ViewedProduct/GetAccountViewedProductsByUsernameHandler.cs +++ /dev/null @@ -1,12 +0,0 @@ -using CustomCADs.Modules.Accounts.Domain.Repositories.Reads; -using CustomCADs.Shared.Application.UseCases.Accounts.Queries; -using CustomCADs.Shared.Domain.TypedIds.Catalog; - -namespace CustomCADs.Modules.Accounts.Application.Accounts.Queries.Shared.ViewedProduct; - -public sealed class GetAccountViewedProductsByUsernameHandler(IAccountReads reads) - : IQueryHandler -{ - public async Task Handle(GetAccountViewedProductsByUsernameQuery req, CancellationToken ct) - => await reads.ViewedProductsByUsernameAsync(req.Username, ct).ConfigureAwait(false); -} diff --git a/src/Modules/Accounts/Application/Accounts/Queries/Shared/ViewedProduct/GetAccountViewedProductHandler.cs b/src/Modules/Accounts/Application/Accounts/Queries/Shared/ViewedProducts/GetAccountViewedProductHandler.cs similarity index 95% rename from src/Modules/Accounts/Application/Accounts/Queries/Shared/ViewedProduct/GetAccountViewedProductHandler.cs rename to src/Modules/Accounts/Application/Accounts/Queries/Shared/ViewedProducts/GetAccountViewedProductHandler.cs index ebad33cc8..0fa00dc42 100644 --- a/src/Modules/Accounts/Application/Accounts/Queries/Shared/ViewedProduct/GetAccountViewedProductHandler.cs +++ b/src/Modules/Accounts/Application/Accounts/Queries/Shared/ViewedProducts/GetAccountViewedProductHandler.cs @@ -2,7 +2,7 @@ using CustomCADs.Shared.Application.UseCases.Accounts.Queries; using CustomCADs.Shared.Domain.TypedIds.Catalog; -namespace CustomCADs.Modules.Accounts.Application.Accounts.Queries.Shared.ViewedProduct; +namespace CustomCADs.Modules.Accounts.Application.Accounts.Queries.Shared.ViewedProducts; public sealed class GetAccountViewedProductHandler(IAccountReads reads) : IQueryHandler diff --git a/src/Modules/Accounts/Application/Accounts/Queries/Shared/ViewedProducts/GetAccountViewedProductsByUsernameHandler.cs b/src/Modules/Accounts/Application/Accounts/Queries/Shared/ViewedProducts/GetAccountViewedProductsByUsernameHandler.cs new file mode 100644 index 000000000..eadcd6560 --- /dev/null +++ b/src/Modules/Accounts/Application/Accounts/Queries/Shared/ViewedProducts/GetAccountViewedProductsByUsernameHandler.cs @@ -0,0 +1,20 @@ +using CustomCADs.Modules.Accounts.Domain.Accounts.Entities; +using CustomCADs.Modules.Accounts.Domain.Repositories.Reads; +using CustomCADs.Shared.Application.UseCases.Accounts.Queries; + +namespace CustomCADs.Modules.Accounts.Application.Accounts.Queries.Shared.ViewedProducts; + +public sealed class GetAccountViewedProductsByUsernameHandler(IAccountReads reads) + : IQueryHandler +{ + public async Task Handle(GetAccountViewedProductsByUsernameQuery req, CancellationToken ct) + { + ViewedProduct[] viewedProducts = await reads + .ViewedProductsByUsernameAsync(req.Username, ct) + .ConfigureAwait(false); + + return [.. viewedProducts + .Select(x => new ViewedProductDto(x.ProductId, x.ViewedAt)) + ]; + } +} diff --git a/src/Modules/Accounts/Domain/Accounts/Account.cs b/src/Modules/Accounts/Domain/Accounts/Account.cs index da5489c83..55793c84d 100644 --- a/src/Modules/Accounts/Domain/Accounts/Account.cs +++ b/src/Modules/Accounts/Domain/Accounts/Account.cs @@ -2,8 +2,12 @@ namespace CustomCADs.Modules.Accounts.Domain.Accounts; -public class Account : BaseAggregateRoot +using Entities; + +public class Account : BaseAggregateRoot, ISoftDeletable { + private readonly List viewedProducts = []; + private Account() { } private Account( string role, @@ -30,6 +34,9 @@ private Account( public string RoleName { get; private set; } = string.Empty; public bool TrackViewedProducts { get; private set; } public DateTimeOffset CreatedAt { get; private set; } + public bool IsDeleted { get; private set; } + public DateTimeOffset? DeletedAt { get; private set; } + public IReadOnlyCollection ViewedProducts => viewedProducts; public static Account Create( string role, @@ -96,4 +103,18 @@ public Account SetTrackViewedProducts(bool track) TrackViewedProducts = track; return this; } + + public Account Delete() + { + IsDeleted = true; + DeletedAt = DateTimeOffset.UtcNow; + + Username = this.Id.ToString(); + Email = this.Id.ToString(); + + FirstName = null; + LastName = null; + + return this; + } } diff --git a/src/Modules/Accounts/Domain/Accounts/Entities/ViewedProduct.cs b/src/Modules/Accounts/Domain/Accounts/Entities/ViewedProduct.cs new file mode 100644 index 000000000..00be4dba1 --- /dev/null +++ b/src/Modules/Accounts/Domain/Accounts/Entities/ViewedProduct.cs @@ -0,0 +1,22 @@ +using CustomCADs.Shared.Domain.TypedIds.Catalog; + +namespace CustomCADs.Modules.Accounts.Domain.Accounts.Entities; + +public class ViewedProduct +{ + private ViewedProduct() { } + private ViewedProduct(AccountId id, ProductId productId, DateTimeOffset viewedAt) : base() + { + AccountId = id; + ProductId = productId; + ViewedAt = viewedAt; + } + + public AccountId AccountId { get; init; } + public ProductId ProductId { get; init; } + public DateTimeOffset ViewedAt { get; init; } + public Account Account { get; init; } = null!; + + public static ViewedProduct Create(AccountId id, ProductId productId, DateTimeOffset viewedAt) + => new(id, productId, viewedAt); +} diff --git a/src/Modules/Accounts/Domain/Constants.cs b/src/Modules/Accounts/Domain/Constants.cs new file mode 100644 index 000000000..63bec8b0f --- /dev/null +++ b/src/Modules/Accounts/Domain/Constants.cs @@ -0,0 +1,17 @@ +namespace CustomCADs.Modules.Accounts.Domain; + +public static class Constants +{ + public static class Roles + { + public static readonly RoleId CustomerId = RoleId.New(1); + public static readonly RoleId ContributorId = RoleId.New(2); + public static readonly RoleId DesignerId = RoleId.New(3); + public static readonly RoleId AdminId = RoleId.New(4); + + public const string CustomerDescription = "Can buy Products from the Gallery as Cart Items; Can request Customs from our Designers and contact them; Can download purchased CADs and track requested Shipments."; + public const string ContributorDescription = "Can upload 3D Models to the Gallery as Products; Can sell CADs to our Designers and contact them; Can apply to become a Designer himself."; + public const string DesignerDescription = "Can accept and work on Customers' Customs; Can validate or report Contributors' Products; Can do everything a Contributor can do."; + public const string AdminDescription = "Can access all non-sensitive info from all resources; Can ban reported resources - Customs, Products, Users, ...; Can modify Categories and Roles."; + } +} diff --git a/src/Modules/Accounts/Domain/Repositories/Reads/IAccountReads.cs b/src/Modules/Accounts/Domain/Repositories/Reads/IAccountReads.cs index 718ee3812..a187df136 100644 --- a/src/Modules/Accounts/Domain/Repositories/Reads/IAccountReads.cs +++ b/src/Modules/Accounts/Domain/Repositories/Reads/IAccountReads.cs @@ -1,4 +1,5 @@ using CustomCADs.Modules.Accounts.Domain.Accounts; +using CustomCADs.Modules.Accounts.Domain.Accounts.Entities; using CustomCADs.Shared.Domain.Querying; using CustomCADs.Shared.Domain.TypedIds.Catalog; @@ -13,6 +14,6 @@ public interface IAccountReads Task ExistsByIdAsync(AccountId id, CancellationToken ct = default); Task ExistsByUsernameAsync(string username, CancellationToken ct = default); Task ViewedProductsByIdAsync(AccountId id, CancellationToken ct = default); - Task ViewedProductsByUsernameAsync(string username, CancellationToken ct = default); + Task ViewedProductsByUsernameAsync(string username, CancellationToken ct = default); Task CountAsync(CancellationToken ct = default); } diff --git a/src/Modules/Accounts/Domain/Repositories/Writes/IAccountWrites.cs b/src/Modules/Accounts/Domain/Repositories/Writes/IAccountWrites.cs index 5ffb9d063..ec76eb4ba 100644 --- a/src/Modules/Accounts/Domain/Repositories/Writes/IAccountWrites.cs +++ b/src/Modules/Accounts/Domain/Repositories/Writes/IAccountWrites.cs @@ -6,6 +6,7 @@ namespace CustomCADs.Modules.Accounts.Domain.Repositories.Writes; public interface IAccountWrites { Task AddAsync(Account entity, CancellationToken ct = default); - Task ViewProductAsync(AccountId id, ProductId productId, CancellationToken ct = default); + Task ViewProductAsync(AccountId id, ProductId productId, DateTimeOffset viewedAt, CancellationToken ct = default); + Task UnviewProductAsync(AccountId id, ProductId productId, CancellationToken ct = default); void Remove(Account entity); } diff --git a/src/Modules/Accounts/Persistence/AccountsContext.cs b/src/Modules/Accounts/Persistence/AccountsContext.cs index 4121f53b7..34f811979 100644 --- a/src/Modules/Accounts/Persistence/AccountsContext.cs +++ b/src/Modules/Accounts/Persistence/AccountsContext.cs @@ -1,6 +1,6 @@ using CustomCADs.Modules.Accounts.Domain.Accounts; +using CustomCADs.Modules.Accounts.Domain.Accounts.Entities; using CustomCADs.Modules.Accounts.Domain.Roles; -using CustomCADs.Modules.Accounts.Persistence.ShadowEntities; using CustomCADs.Shared.Persistence; namespace CustomCADs.Modules.Accounts.Persistence; diff --git a/src/Modules/Accounts/Persistence/Configurations/Accounts/Configurations.cs b/src/Modules/Accounts/Persistence/Configurations/Accounts/Configurations.cs index 1dea47536..39ae3bc2c 100644 --- a/src/Modules/Accounts/Persistence/Configurations/Accounts/Configurations.cs +++ b/src/Modules/Accounts/Persistence/Configurations/Accounts/Configurations.cs @@ -12,6 +12,7 @@ public void Configure(EntityTypeBuilder builder) .SetStronglyTypedIds() .SetIndexes() .SetValidations() - .SetSeeding(); + .SetSeeding() + .SetFilters(); } } diff --git a/src/Modules/Accounts/Persistence/Configurations/Accounts/Utilities.cs b/src/Modules/Accounts/Persistence/Configurations/Accounts/Utilities.cs index b04bd1203..2930ead32 100644 --- a/src/Modules/Accounts/Persistence/Configurations/Accounts/Utilities.cs +++ b/src/Modules/Accounts/Persistence/Configurations/Accounts/Utilities.cs @@ -5,7 +5,6 @@ namespace CustomCADs.Modules.Accounts.Persistence.Configurations.Accounts; using static AccountConstants; -using static DomainConstants.Roles; using static DomainConstants.Users; internal static class Utilities @@ -70,19 +69,36 @@ internal EntityTypeBuilder SetValidations() .IsRequired() .HasColumnName(nameof(Account.CreatedAt)); + builder + .Property(x => x.IsDeleted) + .IsRequired() + .HasColumnName(nameof(Account.IsDeleted)); + + builder + .Property(x => x.DeletedAt) + .HasColumnName(nameof(Account.DeletedAt)); + return builder; } internal EntityTypeBuilder SetSeeding() { builder.HasData([ - Account.CreateWithId(AccountId.New(CustomerAccountId), Customer, CustomerUsername, CustomerEmail, new DateTimeOffset(2025, 05, 10, 19, 23, 12, 123, TimeSpan.FromHours(3))), - Account.CreateWithId(AccountId.New(ContributorAccountId), Contributor, ContributorUsername, ContributorEmail, new DateTimeOffset(2025, 05, 13, 17, 42, 57, 456, TimeSpan.FromHours(3))), - Account.CreateWithId(AccountId.New(DesignerAccountId), Designer, DesignerUsername, DesignerEmail, new DateTimeOffset(2025, 01, 09, 13, 15, 28, 789, TimeSpan.FromHours(3))), - Account.CreateWithId(AccountId.New(AdminAccountId), Admin, AdminUsername, AdminEmail, new DateTimeOffset(2024, 03, 17, 02, 45, 13, 000, TimeSpan.FromHours(3))), + Account.CreateWithId(CustomerAccountId, CustomerRole, CustomerUsername, CustomerEmail, new DateTimeOffset(2025, 05, 10, 19, 23, 12, 123, TimeSpan.FromHours(3))), + Account.CreateWithId(ContributorAccountId, ContributorRole, ContributorUsername, ContributorEmail, new DateTimeOffset(2025, 05, 13, 17, 42, 57, 456, TimeSpan.FromHours(3))), + Account.CreateWithId(DesignerAccountId, DesignerRole, DesignerUsername, DesignerEmail, new DateTimeOffset(2025, 01, 09, 13, 15, 28, 789, TimeSpan.FromHours(3))), + Account.CreateWithId(HeadDesignerAccountId, DesignerRole, HeadDesignerUsername, HeadDesignerEmail, new DateTimeOffset(2024, 03, 17, 02, 17, 32, 789, TimeSpan.FromHours(3))), + Account.CreateWithId(AdminAccountId, AdminRole, AdminUsername, AdminEmail, new DateTimeOffset(2024, 03, 17, 02, 45, 13, 000, TimeSpan.FromHours(3))), ]); return builder; } + + internal EntityTypeBuilder SetFilters() + { + builder.HasQueryFilter(x => !x.IsDeleted); + + return builder; + } } } diff --git a/src/Modules/Accounts/Persistence/Configurations/Roles/Utilities.cs b/src/Modules/Accounts/Persistence/Configurations/Roles/Utilities.cs index b25a57608..a7eec3902 100644 --- a/src/Modules/Accounts/Persistence/Configurations/Roles/Utilities.cs +++ b/src/Modules/Accounts/Persistence/Configurations/Roles/Utilities.cs @@ -1,10 +1,8 @@ using CustomCADs.Modules.Accounts.Domain.Roles; -using CustomCADs.Shared.Domain; using Microsoft.EntityFrameworkCore.Metadata.Builders; namespace CustomCADs.Modules.Accounts.Persistence.Configurations.Roles; -using static DomainConstants.Roles; using static RoleConstants; internal static class Utilities @@ -48,10 +46,26 @@ internal EntityTypeBuilder SetValidations() internal EntityTypeBuilder SetSeeding() { builder.HasData([ - Role.CreateWithId(RoleId.New(1), Customer, CustomerDescription), - Role.CreateWithId(RoleId.New(2), Contributor, ContributorDescription), - Role.CreateWithId(RoleId.New(3), Designer, DesignerDescription), - Role.CreateWithId(RoleId.New(4), Admin, AdminDescription), + Role.CreateWithId( + id: Domain.Constants.Roles.CustomerId, + name: Shared.Domain.DomainConstants.Users.CustomerRole, + description: Domain.Constants.Roles.CustomerDescription + ), + Role.CreateWithId( + id: Domain.Constants.Roles.ContributorId, + name: Shared.Domain.DomainConstants.Users.ContributorRole, + description: Domain.Constants.Roles.ContributorDescription + ), + Role.CreateWithId( + id: Domain.Constants.Roles.DesignerId, + name: Shared.Domain.DomainConstants.Users.DesignerRole, + description: Domain.Constants.Roles.DesignerDescription + ), + Role.CreateWithId( + id: Domain.Constants.Roles.AdminId, + name: Shared.Domain.DomainConstants.Users.AdminRole, + description: Domain.Constants.Roles.AdminDescription + ), ]); return builder; diff --git a/src/Modules/Accounts/Persistence/Configurations/ViewedProducts/Configurations.cs b/src/Modules/Accounts/Persistence/Configurations/ViewedProducts/Configurations.cs index 030e2659c..df93f1db7 100644 --- a/src/Modules/Accounts/Persistence/Configurations/ViewedProducts/Configurations.cs +++ b/src/Modules/Accounts/Persistence/Configurations/ViewedProducts/Configurations.cs @@ -1,4 +1,4 @@ -using CustomCADs.Modules.Accounts.Persistence.ShadowEntities; +using CustomCADs.Modules.Accounts.Domain.Accounts.Entities; using Microsoft.EntityFrameworkCore.Metadata.Builders; namespace CustomCADs.Modules.Accounts.Persistence.Configurations.ViewedProducts; diff --git a/src/Modules/Accounts/Persistence/Configurations/ViewedProducts/Utilities.cs b/src/Modules/Accounts/Persistence/Configurations/ViewedProducts/Utilities.cs index 5deeca6b5..b7626c271 100644 --- a/src/Modules/Accounts/Persistence/Configurations/ViewedProducts/Utilities.cs +++ b/src/Modules/Accounts/Persistence/Configurations/ViewedProducts/Utilities.cs @@ -1,4 +1,4 @@ -using CustomCADs.Modules.Accounts.Persistence.ShadowEntities; +using CustomCADs.Modules.Accounts.Domain.Accounts.Entities; using CustomCADs.Shared.Domain.TypedIds.Catalog; using Microsoft.EntityFrameworkCore.Metadata.Builders; @@ -19,7 +19,7 @@ internal EntityTypeBuilder SetForeignKeys() { builder .HasOne(x => x.Account) - .WithMany() + .WithMany(x => x.ViewedProducts) .HasForeignKey(x => x.AccountId) .OnDelete(DeleteBehavior.Cascade); diff --git a/src/Modules/Accounts/Persistence/Migrations/20260424222313_Added_HeadDesigner_Account.Designer.cs b/src/Modules/Accounts/Persistence/Migrations/20260424222313_Added_HeadDesigner_Account.Designer.cs new file mode 100644 index 000000000..abc4bc0a3 --- /dev/null +++ b/src/Modules/Accounts/Persistence/Migrations/20260424222313_Added_HeadDesigner_Account.Designer.cs @@ -0,0 +1,204 @@ +// +using System; +using CustomCADs.Modules.Accounts.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace CustomCADs.Modules.Accounts.Persistence.Migrations +{ + [DbContext(typeof(AccountsContext))] + [Migration("20260424222313_Added_HeadDesigner_Account")] + partial class Added_HeadDesigner_Account + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("Accounts") + .HasAnnotation("ProductVersion", "10.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("CustomCADs.Modules.Accounts.Domain.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("CreatedAt"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text") + .HasColumnName("Email"); + + b.Property("FirstName") + .HasMaxLength(62) + .HasColumnType("character varying(62)") + .HasColumnName("FirstName"); + + b.Property("LastName") + .HasMaxLength(62) + .HasColumnType("character varying(62)") + .HasColumnName("LastName"); + + b.Property("RoleName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("RoleName"); + + b.Property("TrackViewedProducts") + .HasColumnType("boolean") + .HasColumnName("TrackViewedProducts"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(62) + .HasColumnType("character varying(62)") + .HasColumnName("Username"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("Accounts", "Accounts"); + + b.HasData( + new + { + Id = new Guid("2da61b05-1a27-4af9-9df2-be4f1f4e835f"), + CreatedAt = new DateTimeOffset(new DateTime(2025, 5, 10, 19, 23, 12, 123, DateTimeKind.Unspecified), new TimeSpan(0, 3, 0, 0, 0)), + Email = "ivanzlatinov006@gmail.com", + RoleName = "Customer", + TrackViewedProducts = true, + Username = "For7a7a" + }, + new + { + Id = new Guid("6d963818-23dc-4e9a-aaa8-b4c77252bc97"), + CreatedAt = new DateTimeOffset(new DateTime(2025, 5, 13, 17, 42, 57, 456, DateTimeKind.Unspecified), new TimeSpan(0, 3, 0, 0, 0)), + Email = "PDMatsaliev20@codingburgas.bg", + RoleName = "Contributor", + TrackViewedProducts = true, + Username = "PDMatsaliev20" + }, + new + { + Id = new Guid("8d477999-0580-4770-8864-e9ba4bed9cd1"), + CreatedAt = new DateTimeOffset(new DateTime(2025, 1, 9, 13, 15, 28, 789, DateTimeKind.Unspecified), new TimeSpan(0, 3, 0, 0, 0)), + Email = "john.cad@gmail.com", + RoleName = "Designer", + TrackViewedProducts = true, + Username = "John_CAD" + }, + new + { + Id = new Guid("0fb3212f-7d51-4586-8fc2-0f333ec9fbc1"), + CreatedAt = new DateTimeOffset(new DateTime(2024, 3, 17, 2, 17, 32, 789, DateTimeKind.Unspecified), new TimeSpan(0, 3, 0, 0, 0)), + Email = "boriskolev2006@gmail.com", + RoleName = "Designer", + TrackViewedProducts = true, + Username = "Oracle3000" + }, + new + { + Id = new Guid("e995039c-a535-4f20-8288-7aadcb71b252"), + CreatedAt = new DateTimeOffset(new DateTime(2024, 3, 17, 2, 45, 13, 0, DateTimeKind.Unspecified), new TimeSpan(0, 3, 0, 0, 0)), + Email = "ivanangelov414@gmail.com", + RoleName = "Administrator", + TrackViewedProducts = true, + Username = "NinjataBG" + }); + }); + + modelBuilder.Entity("CustomCADs.Modules.Accounts.Domain.Roles.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Description") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("Description"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasColumnName("Name"); + + b.HasKey("Id"); + + b.ToTable("Roles", "Accounts"); + + b.HasData( + new + { + Id = 1, + Description = "Can buy Products from the Gallery as Cart Items; Can request Customs from our Designers and contact them; Can download purchased CADs and track requested Shipments.", + Name = "Customer" + }, + new + { + Id = 2, + Description = "Can upload 3D Models to the Gallery as Products; Can sell CADs to our Designers and contact them; Can apply to become a Designer himself.", + Name = "Contributor" + }, + new + { + Id = 3, + Description = "Can accept and work on Customers' Customs; Can validate or report Contributors' Products; Can do everything a Contributor can do.", + Name = "Designer" + }, + new + { + Id = 4, + Description = "Can access all non-sensitive info from all resources; Can ban reported resources - Customs, Products, Users, ...; Can modify Categories and Roles.", + Name = "Administrator" + }); + }); + + modelBuilder.Entity("CustomCADs.Modules.Accounts.Persistence.ShadowEntities.ViewedProduct", b => + { + b.Property("AccountId") + .HasColumnType("uuid"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.HasKey("AccountId", "ProductId"); + + b.ToTable("ViewedProducts", "Accounts"); + }); + + modelBuilder.Entity("CustomCADs.Modules.Accounts.Persistence.ShadowEntities.ViewedProduct", b => + { + b.HasOne("CustomCADs.Modules.Accounts.Domain.Accounts.Account", "Account") + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Account"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Modules/Accounts/Persistence/Migrations/20260424222313_Added_HeadDesigner_Account.cs b/src/Modules/Accounts/Persistence/Migrations/20260424222313_Added_HeadDesigner_Account.cs new file mode 100644 index 000000000..6a0d3505d --- /dev/null +++ b/src/Modules/Accounts/Persistence/Migrations/20260424222313_Added_HeadDesigner_Account.cs @@ -0,0 +1,46 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + + +namespace CustomCADs.Modules.Accounts.Persistence.Migrations; + +/// +public partial class Added_HeadDesigner_Account : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.UpdateData( + schema: "Accounts", + table: "Accounts", + keyColumn: "Id", + keyValue: new Guid("0fb3212f-7d51-4586-8fc2-0f333ec9fbc1"), + column: "CreatedAt", + value: new DateTimeOffset(new DateTime(2024, 3, 17, 2, 17, 32, 789, DateTimeKind.Unspecified), new TimeSpan(0, 3, 0, 0, 0))); + + migrationBuilder.InsertData( + schema: "Accounts", + table: "Accounts", + columns: new[] { "Id", "CreatedAt", "Email", "FirstName", "LastName", "RoleName", "TrackViewedProducts", "Username" }, + values: new object[] { new Guid("8d477999-0580-4770-8864-e9ba4bed9cd1"), new DateTimeOffset(new DateTime(2025, 1, 9, 13, 15, 28, 789, DateTimeKind.Unspecified), new TimeSpan(0, 3, 0, 0, 0)), "john.cad@gmail.com", null, null, "Designer", true, "John_CAD" }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DeleteData( + schema: "Accounts", + table: "Accounts", + keyColumn: "Id", + keyValue: new Guid("8d477999-0580-4770-8864-e9ba4bed9cd1")); + + migrationBuilder.UpdateData( + schema: "Accounts", + table: "Accounts", + keyColumn: "Id", + keyValue: new Guid("0fb3212f-7d51-4586-8fc2-0f333ec9fbc1"), + column: "CreatedAt", + value: new DateTimeOffset(new DateTime(2025, 1, 9, 13, 15, 28, 789, DateTimeKind.Unspecified), new TimeSpan(0, 3, 0, 0, 0))); + } +} diff --git a/src/Modules/Accounts/Persistence/Migrations/20260425173905_Implemented_Accounts_SoftDelete.Designer.cs b/src/Modules/Accounts/Persistence/Migrations/20260425173905_Implemented_Accounts_SoftDelete.Designer.cs new file mode 100644 index 000000000..9b0665c16 --- /dev/null +++ b/src/Modules/Accounts/Persistence/Migrations/20260425173905_Implemented_Accounts_SoftDelete.Designer.cs @@ -0,0 +1,217 @@ +// +using System; +using CustomCADs.Modules.Accounts.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace CustomCADs.Modules.Accounts.Persistence.Migrations +{ + [DbContext(typeof(AccountsContext))] + [Migration("20260425173905_Implemented_Accounts_SoftDelete")] + partial class Implemented_Accounts_SoftDelete + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("Accounts") + .HasAnnotation("ProductVersion", "10.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("CustomCADs.Modules.Accounts.Domain.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("CreatedAt"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("DeletedAt"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text") + .HasColumnName("Email"); + + b.Property("FirstName") + .HasMaxLength(62) + .HasColumnType("character varying(62)") + .HasColumnName("FirstName"); + + b.Property("IsDeleted") + .HasColumnType("boolean") + .HasColumnName("IsDeleted"); + + b.Property("LastName") + .HasMaxLength(62) + .HasColumnType("character varying(62)") + .HasColumnName("LastName"); + + b.Property("RoleName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("RoleName"); + + b.Property("TrackViewedProducts") + .HasColumnType("boolean") + .HasColumnName("TrackViewedProducts"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(62) + .HasColumnType("character varying(62)") + .HasColumnName("Username"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("Accounts", "Accounts"); + + b.HasData( + new + { + Id = new Guid("2da61b05-1a27-4af9-9df2-be4f1f4e835f"), + CreatedAt = new DateTimeOffset(new DateTime(2025, 5, 10, 19, 23, 12, 123, DateTimeKind.Unspecified), new TimeSpan(0, 3, 0, 0, 0)), + Email = "ivanzlatinov006@gmail.com", + IsDeleted = false, + RoleName = "Customer", + TrackViewedProducts = true, + Username = "For7a7a" + }, + new + { + Id = new Guid("6d963818-23dc-4e9a-aaa8-b4c77252bc97"), + CreatedAt = new DateTimeOffset(new DateTime(2025, 5, 13, 17, 42, 57, 456, DateTimeKind.Unspecified), new TimeSpan(0, 3, 0, 0, 0)), + Email = "PDMatsaliev20@codingburgas.bg", + IsDeleted = false, + RoleName = "Contributor", + TrackViewedProducts = true, + Username = "PDMatsaliev20" + }, + new + { + Id = new Guid("8d477999-0580-4770-8864-e9ba4bed9cd1"), + CreatedAt = new DateTimeOffset(new DateTime(2025, 1, 9, 13, 15, 28, 789, DateTimeKind.Unspecified), new TimeSpan(0, 3, 0, 0, 0)), + Email = "john.cad@gmail.com", + IsDeleted = false, + RoleName = "Designer", + TrackViewedProducts = true, + Username = "John_CAD" + }, + new + { + Id = new Guid("0fb3212f-7d51-4586-8fc2-0f333ec9fbc1"), + CreatedAt = new DateTimeOffset(new DateTime(2024, 3, 17, 2, 17, 32, 789, DateTimeKind.Unspecified), new TimeSpan(0, 3, 0, 0, 0)), + Email = "boriskolev2006@gmail.com", + IsDeleted = false, + RoleName = "Designer", + TrackViewedProducts = true, + Username = "Oracle3000" + }, + new + { + Id = new Guid("e995039c-a535-4f20-8288-7aadcb71b252"), + CreatedAt = new DateTimeOffset(new DateTime(2024, 3, 17, 2, 45, 13, 0, DateTimeKind.Unspecified), new TimeSpan(0, 3, 0, 0, 0)), + Email = "ivanangelov414@gmail.com", + IsDeleted = false, + RoleName = "Administrator", + TrackViewedProducts = true, + Username = "NinjataBG" + }); + }); + + modelBuilder.Entity("CustomCADs.Modules.Accounts.Domain.Roles.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Description") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("Description"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasColumnName("Name"); + + b.HasKey("Id"); + + b.ToTable("Roles", "Accounts"); + + b.HasData( + new + { + Id = 1, + Description = "Can buy Products from the Gallery as Cart Items; Can request Customs from our Designers and contact them; Can download purchased CADs and track requested Shipments.", + Name = "Customer" + }, + new + { + Id = 2, + Description = "Can upload 3D Models to the Gallery as Products; Can sell CADs to our Designers and contact them; Can apply to become a Designer himself.", + Name = "Contributor" + }, + new + { + Id = 3, + Description = "Can accept and work on Customers' Customs; Can validate or report Contributors' Products; Can do everything a Contributor can do.", + Name = "Designer" + }, + new + { + Id = 4, + Description = "Can access all non-sensitive info from all resources; Can ban reported resources - Customs, Products, Users, ...; Can modify Categories and Roles.", + Name = "Administrator" + }); + }); + + modelBuilder.Entity("CustomCADs.Modules.Accounts.Persistence.ShadowEntities.ViewedProduct", b => + { + b.Property("AccountId") + .HasColumnType("uuid"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.HasKey("AccountId", "ProductId"); + + b.ToTable("ViewedProducts", "Accounts"); + }); + + modelBuilder.Entity("CustomCADs.Modules.Accounts.Persistence.ShadowEntities.ViewedProduct", b => + { + b.HasOne("CustomCADs.Modules.Accounts.Domain.Accounts.Account", "Account") + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Account"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Modules/Accounts/Persistence/Migrations/20260425173905_Implemented_Accounts_SoftDelete.cs b/src/Modules/Accounts/Persistence/Migrations/20260425173905_Implemented_Accounts_SoftDelete.cs new file mode 100644 index 000000000..d3d12358a --- /dev/null +++ b/src/Modules/Accounts/Persistence/Migrations/20260425173905_Implemented_Accounts_SoftDelete.cs @@ -0,0 +1,82 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace CustomCADs.Modules.Accounts.Persistence.Migrations; + +/// +public partial class Implemented_Accounts_SoftDelete : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "DeletedAt", + schema: "Accounts", + table: "Accounts", + type: "timestamp with time zone", + nullable: true); + + migrationBuilder.AddColumn( + name: "IsDeleted", + schema: "Accounts", + table: "Accounts", + type: "boolean", + nullable: false, + defaultValue: false); + + migrationBuilder.UpdateData( + schema: "Accounts", + table: "Accounts", + keyColumn: "Id", + keyValue: new Guid("0fb3212f-7d51-4586-8fc2-0f333ec9fbc1"), + columns: new[] { "DeletedAt", "IsDeleted" }, + values: new object[] { null, false }); + + migrationBuilder.UpdateData( + schema: "Accounts", + table: "Accounts", + keyColumn: "Id", + keyValue: new Guid("2da61b05-1a27-4af9-9df2-be4f1f4e835f"), + columns: new[] { "DeletedAt", "IsDeleted" }, + values: new object[] { null, false }); + + migrationBuilder.UpdateData( + schema: "Accounts", + table: "Accounts", + keyColumn: "Id", + keyValue: new Guid("6d963818-23dc-4e9a-aaa8-b4c77252bc97"), + columns: new[] { "DeletedAt", "IsDeleted" }, + values: new object[] { null, false }); + + migrationBuilder.UpdateData( + schema: "Accounts", + table: "Accounts", + keyColumn: "Id", + keyValue: new Guid("8d477999-0580-4770-8864-e9ba4bed9cd1"), + columns: new[] { "DeletedAt", "IsDeleted" }, + values: new object[] { null, false }); + + migrationBuilder.UpdateData( + schema: "Accounts", + table: "Accounts", + keyColumn: "Id", + keyValue: new Guid("e995039c-a535-4f20-8288-7aadcb71b252"), + columns: new[] { "DeletedAt", "IsDeleted" }, + values: new object[] { null, false }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "DeletedAt", + schema: "Accounts", + table: "Accounts"); + + migrationBuilder.DropColumn( + name: "IsDeleted", + schema: "Accounts", + table: "Accounts"); + } +} diff --git a/src/Modules/Accounts/Persistence/Migrations/20260501233422_Added_ViewedProducts_ViewedAt.Designer.cs b/src/Modules/Accounts/Persistence/Migrations/20260501233422_Added_ViewedProducts_ViewedAt.Designer.cs new file mode 100644 index 000000000..9cc69e0bf --- /dev/null +++ b/src/Modules/Accounts/Persistence/Migrations/20260501233422_Added_ViewedProducts_ViewedAt.Designer.cs @@ -0,0 +1,225 @@ +// +using System; +using CustomCADs.Modules.Accounts.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace CustomCADs.Modules.Accounts.Persistence.Migrations +{ + [DbContext(typeof(AccountsContext))] + [Migration("20260501233422_Added_ViewedProducts_ViewedAt")] + partial class Added_ViewedProducts_ViewedAt + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("Accounts") + .HasAnnotation("ProductVersion", "10.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("CustomCADs.Modules.Accounts.Domain.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("CreatedAt"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("DeletedAt"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text") + .HasColumnName("Email"); + + b.Property("FirstName") + .HasMaxLength(62) + .HasColumnType("character varying(62)") + .HasColumnName("FirstName"); + + b.Property("IsDeleted") + .HasColumnType("boolean") + .HasColumnName("IsDeleted"); + + b.Property("LastName") + .HasMaxLength(62) + .HasColumnType("character varying(62)") + .HasColumnName("LastName"); + + b.Property("RoleName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("RoleName"); + + b.Property("TrackViewedProducts") + .HasColumnType("boolean") + .HasColumnName("TrackViewedProducts"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(62) + .HasColumnType("character varying(62)") + .HasColumnName("Username"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("Accounts", "Accounts"); + + b.HasData( + new + { + Id = new Guid("2da61b05-1a27-4af9-9df2-be4f1f4e835f"), + CreatedAt = new DateTimeOffset(new DateTime(2025, 5, 10, 19, 23, 12, 123, DateTimeKind.Unspecified), new TimeSpan(0, 3, 0, 0, 0)), + Email = "ivanzlatinov006@gmail.com", + IsDeleted = false, + RoleName = "Customer", + TrackViewedProducts = true, + Username = "For7a7a" + }, + new + { + Id = new Guid("6d963818-23dc-4e9a-aaa8-b4c77252bc97"), + CreatedAt = new DateTimeOffset(new DateTime(2025, 5, 13, 17, 42, 57, 456, DateTimeKind.Unspecified), new TimeSpan(0, 3, 0, 0, 0)), + Email = "PDMatsaliev20@codingburgas.bg", + IsDeleted = false, + RoleName = "Contributor", + TrackViewedProducts = true, + Username = "PDMatsaliev20" + }, + new + { + Id = new Guid("8d477999-0580-4770-8864-e9ba4bed9cd1"), + CreatedAt = new DateTimeOffset(new DateTime(2025, 1, 9, 13, 15, 28, 789, DateTimeKind.Unspecified), new TimeSpan(0, 3, 0, 0, 0)), + Email = "john.cad@gmail.com", + IsDeleted = false, + RoleName = "Designer", + TrackViewedProducts = true, + Username = "John_CAD" + }, + new + { + Id = new Guid("0fb3212f-7d51-4586-8fc2-0f333ec9fbc1"), + CreatedAt = new DateTimeOffset(new DateTime(2024, 3, 17, 2, 17, 32, 789, DateTimeKind.Unspecified), new TimeSpan(0, 3, 0, 0, 0)), + Email = "boriskolev2006@gmail.com", + IsDeleted = false, + RoleName = "Designer", + TrackViewedProducts = true, + Username = "Oracle3000" + }, + new + { + Id = new Guid("e995039c-a535-4f20-8288-7aadcb71b252"), + CreatedAt = new DateTimeOffset(new DateTime(2024, 3, 17, 2, 45, 13, 0, DateTimeKind.Unspecified), new TimeSpan(0, 3, 0, 0, 0)), + Email = "ivanangelov414@gmail.com", + IsDeleted = false, + RoleName = "Administrator", + TrackViewedProducts = true, + Username = "NinjataBG" + }); + }); + + modelBuilder.Entity("CustomCADs.Modules.Accounts.Domain.Accounts.Entities.ViewedProduct", b => + { + b.Property("AccountId") + .HasColumnType("uuid"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("ViewedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("AccountId", "ProductId"); + + b.ToTable("ViewedProducts", "Accounts"); + }); + + modelBuilder.Entity("CustomCADs.Modules.Accounts.Domain.Roles.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Description") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("Description"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasColumnName("Name"); + + b.HasKey("Id"); + + b.ToTable("Roles", "Accounts"); + + b.HasData( + new + { + Id = 1, + Description = "Can buy Products from the Gallery as Cart Items; Can request Customs from our Designers and contact them; Can download purchased CADs and track requested Shipments.", + Name = "Customer" + }, + new + { + Id = 2, + Description = "Can upload 3D Models to the Gallery as Products; Can sell CADs to our Designers and contact them; Can apply to become a Designer himself.", + Name = "Contributor" + }, + new + { + Id = 3, + Description = "Can accept and work on Customers' Customs; Can validate or report Contributors' Products; Can do everything a Contributor can do.", + Name = "Designer" + }, + new + { + Id = 4, + Description = "Can access all non-sensitive info from all resources; Can ban reported resources - Customs, Products, Users, ...; Can modify Categories and Roles.", + Name = "Administrator" + }); + }); + + modelBuilder.Entity("CustomCADs.Modules.Accounts.Domain.Accounts.Entities.ViewedProduct", b => + { + b.HasOne("CustomCADs.Modules.Accounts.Domain.Accounts.Account", "Account") + .WithMany("ViewedProducts") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("CustomCADs.Modules.Accounts.Domain.Accounts.Account", b => + { + b.Navigation("ViewedProducts"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Modules/Accounts/Persistence/Migrations/20260501233422_Added_ViewedProducts_ViewedAt.cs b/src/Modules/Accounts/Persistence/Migrations/20260501233422_Added_ViewedProducts_ViewedAt.cs new file mode 100644 index 000000000..4ff31e4ae --- /dev/null +++ b/src/Modules/Accounts/Persistence/Migrations/20260501233422_Added_ViewedProducts_ViewedAt.cs @@ -0,0 +1,30 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace CustomCADs.Modules.Accounts.Persistence.Migrations; + +/// +public partial class Added_ViewedProducts_ViewedAt : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "ViewedAt", + schema: "Accounts", + table: "ViewedProducts", + type: "timestamp with time zone", + nullable: false, + defaultValue: new DateTimeOffset(new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0))); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "ViewedAt", + schema: "Accounts", + table: "ViewedProducts"); + } +} diff --git a/src/Modules/Accounts/Persistence/Migrations/AccountsContextModelSnapshot.cs b/src/Modules/Accounts/Persistence/Migrations/AccountsContextModelSnapshot.cs index c4d019dfb..d842d34f9 100644 --- a/src/Modules/Accounts/Persistence/Migrations/AccountsContextModelSnapshot.cs +++ b/src/Modules/Accounts/Persistence/Migrations/AccountsContextModelSnapshot.cs @@ -18,7 +18,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) #pragma warning disable 612, 618 modelBuilder .HasDefaultSchema("Accounts") - .HasAnnotation("ProductVersion", "9.0.4") + .HasAnnotation("ProductVersion", "10.0.0") .HasAnnotation("Relational:MaxIdentifierLength", 63); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); @@ -33,6 +33,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("timestamp with time zone") .HasColumnName("CreatedAt"); + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("DeletedAt"); + b.Property("Email") .IsRequired() .HasColumnType("text") @@ -43,6 +47,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("character varying(62)") .HasColumnName("FirstName"); + b.Property("IsDeleted") + .HasColumnType("boolean") + .HasColumnName("IsDeleted"); + b.Property("LastName") .HasMaxLength(62) .HasColumnType("character varying(62)") @@ -79,6 +87,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) Id = new Guid("2da61b05-1a27-4af9-9df2-be4f1f4e835f"), CreatedAt = new DateTimeOffset(new DateTime(2025, 5, 10, 19, 23, 12, 123, DateTimeKind.Unspecified), new TimeSpan(0, 3, 0, 0, 0)), Email = "ivanzlatinov006@gmail.com", + IsDeleted = false, RoleName = "Customer", TrackViewedProducts = true, Username = "For7a7a" @@ -88,15 +97,27 @@ protected override void BuildModel(ModelBuilder modelBuilder) Id = new Guid("6d963818-23dc-4e9a-aaa8-b4c77252bc97"), CreatedAt = new DateTimeOffset(new DateTime(2025, 5, 13, 17, 42, 57, 456, DateTimeKind.Unspecified), new TimeSpan(0, 3, 0, 0, 0)), Email = "PDMatsaliev20@codingburgas.bg", + IsDeleted = false, RoleName = "Contributor", TrackViewedProducts = true, Username = "PDMatsaliev20" }, new { - Id = new Guid("0fb3212f-7d51-4586-8fc2-0f333ec9fbc1"), + Id = new Guid("8d477999-0580-4770-8864-e9ba4bed9cd1"), CreatedAt = new DateTimeOffset(new DateTime(2025, 1, 9, 13, 15, 28, 789, DateTimeKind.Unspecified), new TimeSpan(0, 3, 0, 0, 0)), + Email = "john.cad@gmail.com", + IsDeleted = false, + RoleName = "Designer", + TrackViewedProducts = true, + Username = "John_CAD" + }, + new + { + Id = new Guid("0fb3212f-7d51-4586-8fc2-0f333ec9fbc1"), + CreatedAt = new DateTimeOffset(new DateTime(2024, 3, 17, 2, 17, 32, 789, DateTimeKind.Unspecified), new TimeSpan(0, 3, 0, 0, 0)), Email = "boriskolev2006@gmail.com", + IsDeleted = false, RoleName = "Designer", TrackViewedProducts = true, Username = "Oracle3000" @@ -106,12 +127,29 @@ protected override void BuildModel(ModelBuilder modelBuilder) Id = new Guid("e995039c-a535-4f20-8288-7aadcb71b252"), CreatedAt = new DateTimeOffset(new DateTime(2024, 3, 17, 2, 45, 13, 0, DateTimeKind.Unspecified), new TimeSpan(0, 3, 0, 0, 0)), Email = "ivanangelov414@gmail.com", + IsDeleted = false, RoleName = "Administrator", TrackViewedProducts = true, Username = "NinjataBG" }); }); + modelBuilder.Entity("CustomCADs.Modules.Accounts.Domain.Accounts.Entities.ViewedProduct", b => + { + b.Property("AccountId") + .HasColumnType("uuid"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("ViewedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("AccountId", "ProductId"); + + b.ToTable("ViewedProducts", "Accounts"); + }); + modelBuilder.Entity("CustomCADs.Modules.Accounts.Domain.Roles.Role", b => { b.Property("Id") @@ -163,29 +201,21 @@ protected override void BuildModel(ModelBuilder modelBuilder) }); }); - modelBuilder.Entity("CustomCADs.Modules.Accounts.Persistence.ShadowEntities.ViewedProduct", b => - { - b.Property("AccountId") - .HasColumnType("uuid"); - - b.Property("ProductId") - .HasColumnType("uuid"); - - b.HasKey("AccountId", "ProductId"); - - b.ToTable("ViewedProducts", "Accounts"); - }); - - modelBuilder.Entity("CustomCADs.Modules.Accounts.Persistence.ShadowEntities.ViewedProduct", b => + modelBuilder.Entity("CustomCADs.Modules.Accounts.Domain.Accounts.Entities.ViewedProduct", b => { b.HasOne("CustomCADs.Modules.Accounts.Domain.Accounts.Account", "Account") - .WithMany() + .WithMany("ViewedProducts") .HasForeignKey("AccountId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); b.Navigation("Account"); }); + + modelBuilder.Entity("CustomCADs.Modules.Accounts.Domain.Accounts.Account", b => + { + b.Navigation("ViewedProducts"); + }); #pragma warning restore 612, 618 } } diff --git a/src/Modules/Accounts/Persistence/Repositories/Accounts/Reads.cs b/src/Modules/Accounts/Persistence/Repositories/Accounts/Reads.cs index 0af52b114..4cb45fe90 100644 --- a/src/Modules/Accounts/Persistence/Repositories/Accounts/Reads.cs +++ b/src/Modules/Accounts/Persistence/Repositories/Accounts/Reads.cs @@ -1,4 +1,5 @@ using CustomCADs.Modules.Accounts.Domain.Accounts; +using CustomCADs.Modules.Accounts.Domain.Accounts.Entities; using CustomCADs.Modules.Accounts.Domain.Repositories.Reads; using CustomCADs.Shared.Domain.Querying; using CustomCADs.Shared.Domain.TypedIds.Catalog; @@ -57,14 +58,24 @@ public async Task ExistsByUsernameAsync(string username, CancellationToken .ConfigureAwait(false); public async Task ViewedProductsByIdAsync(AccountId id, CancellationToken ct = default) - => await context.ViewedProducts - .GetViewedProductsByAccountIdAsync(id, ct) - .ConfigureAwait(false); + => [.. await context.Accounts + .Where(x => x.Id == id) + .SelectMany(x => x.ViewedProducts) + .Select(x => x.ProductId) + .ToArrayAsync(ct) + .ConfigureAwait(false) + ?? [] + ]; - public async Task ViewedProductsByUsernameAsync(string username, CancellationToken ct = default) - => await context.ViewedProducts - .GetViewedProductsByAccountUsernrameAsync(username, ct) - .ConfigureAwait(false); + public async Task ViewedProductsByUsernameAsync(string username, CancellationToken ct = default) + => [.. await context.Accounts + .Where(x => x.Username == username) + .SelectMany(x => x.ViewedProducts) + .OrderByDescending(x => x.ViewedAt) + .ToArrayAsync(ct) + .ConfigureAwait(false) + ?? [] + ]; public async Task CountAsync(CancellationToken ct = default) => await context.Accounts diff --git a/src/Modules/Accounts/Persistence/Repositories/Accounts/Utilities.cs b/src/Modules/Accounts/Persistence/Repositories/Accounts/Utilities.cs index 751a35d98..adeb6a913 100644 --- a/src/Modules/Accounts/Persistence/Repositories/Accounts/Utilities.cs +++ b/src/Modules/Accounts/Persistence/Repositories/Accounts/Utilities.cs @@ -1,8 +1,6 @@ using CustomCADs.Modules.Accounts.Domain.Accounts; using CustomCADs.Modules.Accounts.Domain.Accounts.Enums; -using CustomCADs.Modules.Accounts.Persistence.ShadowEntities; using CustomCADs.Shared.Domain.Extensions; -using CustomCADs.Shared.Domain.TypedIds.Catalog; using CustomCADs.Shared.Domain.ValueObjects; namespace CustomCADs.Modules.Accounts.Persistence.Repositories.Accounts; @@ -56,22 +54,4 @@ internal IQueryable WithSorting(Sorting? sorting = _ => query, }; } - - extension(DbSet set) - { - internal async Task GetViewedProductsByAccountIdAsync(AccountId id, CancellationToken ct = default) - => await set - .Where(x => x.AccountId == id) - .Select(x => x.ProductId) - .ToArrayAsync(ct) - .ConfigureAwait(false); - - internal async Task GetViewedProductsByAccountUsernrameAsync(string username, CancellationToken ct = default) - => await set - .Include(x => x.Account) - .Where(x => x.Account.Username == username) - .Select(x => x.ProductId) - .ToArrayAsync(ct) - .ConfigureAwait(false); - } } diff --git a/src/Modules/Accounts/Persistence/Repositories/Accounts/Writes.cs b/src/Modules/Accounts/Persistence/Repositories/Accounts/Writes.cs index efd66590a..841217d37 100644 --- a/src/Modules/Accounts/Persistence/Repositories/Accounts/Writes.cs +++ b/src/Modules/Accounts/Persistence/Repositories/Accounts/Writes.cs @@ -1,6 +1,6 @@ using CustomCADs.Modules.Accounts.Domain.Accounts; +using CustomCADs.Modules.Accounts.Domain.Accounts.Entities; using CustomCADs.Modules.Accounts.Domain.Repositories.Writes; -using CustomCADs.Modules.Accounts.Persistence.ShadowEntities; using CustomCADs.Shared.Domain.TypedIds.Catalog; namespace CustomCADs.Modules.Accounts.Persistence.Repositories.Accounts; @@ -10,12 +10,24 @@ public class Writes(AccountsContext context) : IAccountWrites public async Task AddAsync(Account entity, CancellationToken ct = default) => (await context.Accounts.AddAsync(entity, ct).ConfigureAwait(false)).Entity; - public async Task ViewProductAsync(AccountId id, ProductId productId, CancellationToken ct = default) + public async Task ViewProductAsync(AccountId id, ProductId productId, DateTimeOffset viewedAt, CancellationToken ct = default) => await context.ViewedProducts.AddAsync( - entity: ViewedProduct.Create(id, productId), + entity: ViewedProduct.Create(id, productId, viewedAt), cancellationToken: ct ).ConfigureAwait(false); + public async Task UnviewProductAsync(AccountId id, ProductId productId, CancellationToken ct = default) + { + ViewedProduct? viewedProduct = await context.ViewedProducts + .FirstOrDefaultAsync(x => x.AccountId == id && x.ProductId == productId, ct) + .ConfigureAwait(false); + + if (viewedProduct is not null) + { + context.ViewedProducts.Remove(viewedProduct); + } + } + public void Remove(Account entity) - => context.Accounts.Remove(entity); + => entity.Delete(); } diff --git a/src/Modules/Accounts/Persistence/ShadowEntities/ViewedProduct.cs b/src/Modules/Accounts/Persistence/ShadowEntities/ViewedProduct.cs deleted file mode 100644 index a4665cda6..000000000 --- a/src/Modules/Accounts/Persistence/ShadowEntities/ViewedProduct.cs +++ /dev/null @@ -1,16 +0,0 @@ -using CustomCADs.Modules.Accounts.Domain.Accounts; -using CustomCADs.Shared.Domain.TypedIds.Catalog; - -namespace CustomCADs.Modules.Accounts.Persistence.ShadowEntities; - -public class ViewedProduct -{ - private ViewedProduct() { } - - public AccountId AccountId { get; set; } - public ProductId ProductId { get; set; } - public Account Account { get; set; } = null!; - - public static ViewedProduct Create(AccountId id, ProductId productId) - => new() { AccountId = id, ProductId = productId }; -} diff --git a/src/Modules/Carts/API/ActiveCarts/Endpoints/ActiveCartsGroup.cs b/src/Modules/Carts/API/ActiveCarts/Endpoints/ActiveCartsGroup.cs index 3ca7481f8..a6d060a3f 100644 --- a/src/Modules/Carts/API/ActiveCarts/Endpoints/ActiveCartsGroup.cs +++ b/src/Modules/Carts/API/ActiveCarts/Endpoints/ActiveCartsGroup.cs @@ -1,7 +1,7 @@ namespace CustomCADs.Modules.Carts.API.ActiveCarts.Endpoints; using static APIConstants; -using static DomainConstants.Roles; +using static DomainConstants.Users; public class ActiveCartsGroup : Group { @@ -9,7 +9,7 @@ public ActiveCartsGroup() { Configure(Paths.ActiveCarts, x => { - x.Roles(Customer); + x.Roles(CustomerRole); x.Description(x => x.WithTags(Tags[Paths.ActiveCarts])); }); } diff --git a/src/Modules/Carts/API/PurchasedCarts/Endpoints/PurchasedCartsGroup.cs b/src/Modules/Carts/API/PurchasedCarts/Endpoints/PurchasedCartsGroup.cs index fd1cc71fc..4471a191a 100644 --- a/src/Modules/Carts/API/PurchasedCarts/Endpoints/PurchasedCartsGroup.cs +++ b/src/Modules/Carts/API/PurchasedCarts/Endpoints/PurchasedCartsGroup.cs @@ -1,7 +1,7 @@ namespace CustomCADs.Modules.Carts.API.PurchasedCarts.Endpoints; using static APIConstants; -using static DomainConstants.Roles; +using static DomainConstants.Users; public class PurchasedCartsGroup : Group { @@ -9,7 +9,7 @@ public PurchasedCartsGroup() { Configure(Paths.PurchasedCarts, x => { - x.Roles(Customer); + x.Roles(CustomerRole); x.Description(x => x.WithTags(Tags[Paths.PurchasedCarts])); }); } diff --git a/src/Modules/Catalog/API/Categories/Endpoints/CategoriesGroup.cs b/src/Modules/Catalog/API/Categories/Endpoints/CategoriesGroup.cs index 8b8284b94..8fd1f6d6d 100644 --- a/src/Modules/Catalog/API/Categories/Endpoints/CategoriesGroup.cs +++ b/src/Modules/Catalog/API/Categories/Endpoints/CategoriesGroup.cs @@ -1,7 +1,7 @@ namespace CustomCADs.Modules.Catalog.API.Categories.Endpoints; using static APIConstants; -using static DomainConstants.Roles; +using static DomainConstants.Users; public class CategoriesGroup : Group { @@ -9,7 +9,7 @@ public CategoriesGroup() { Configure(Paths.Categories, x => { - x.Roles(Admin); + x.Roles(AdminRole); x.Description(x => x.WithTags(Tags[Paths.Categories])); }); } diff --git a/src/Modules/Catalog/API/Products/Endpoints/Admin/AdminGroup.cs b/src/Modules/Catalog/API/Products/Endpoints/Admin/AdminGroup.cs index 93dac952f..db157542a 100644 --- a/src/Modules/Catalog/API/Products/Endpoints/Admin/AdminGroup.cs +++ b/src/Modules/Catalog/API/Products/Endpoints/Admin/AdminGroup.cs @@ -1,7 +1,7 @@ namespace CustomCADs.Modules.Catalog.API.Products.Endpoints.Admin; using static APIConstants; -using static DomainConstants.Roles; +using static DomainConstants.Users; public class AdminGroup : SubGroup { @@ -9,7 +9,7 @@ public AdminGroup() { Configure(Paths.Admin, x => { - x.Roles(Admin); + x.Roles(AdminRole); x.Description(x => x.WithTags(Tags[$"{Paths.Products}/{Paths.Admin}"])); }); } diff --git a/src/Modules/Catalog/API/Products/Endpoints/Creator/CreatorGroup.cs b/src/Modules/Catalog/API/Products/Endpoints/Creator/CreatorGroup.cs index 838c938c9..a865f1cb2 100644 --- a/src/Modules/Catalog/API/Products/Endpoints/Creator/CreatorGroup.cs +++ b/src/Modules/Catalog/API/Products/Endpoints/Creator/CreatorGroup.cs @@ -1,7 +1,7 @@ namespace CustomCADs.Modules.Catalog.API.Products.Endpoints.Creator; using static APIConstants; -using static DomainConstants.Roles; +using static DomainConstants.Users; public class CreatorGroup : SubGroup { @@ -9,7 +9,7 @@ public CreatorGroup() { Configure(Paths.Creator, x => { - x.Roles(Contributor, Designer); + x.Roles(ContributorRole, DesignerRole); x.Description(x => x.WithTags(Tags[$"{Paths.Products}/{Paths.Creator}"])); }); } diff --git a/src/Modules/Catalog/API/Products/Endpoints/Designer/DesignerGroup.cs b/src/Modules/Catalog/API/Products/Endpoints/Designer/DesignerGroup.cs index 73ecaff9f..0f94f18f5 100644 --- a/src/Modules/Catalog/API/Products/Endpoints/Designer/DesignerGroup.cs +++ b/src/Modules/Catalog/API/Products/Endpoints/Designer/DesignerGroup.cs @@ -1,7 +1,7 @@ namespace CustomCADs.Modules.Catalog.API.Products.Endpoints.Designer; using static APIConstants; -using static DomainConstants.Roles; +using static DomainConstants.Users; public class DesignerGroup : SubGroup { @@ -9,7 +9,7 @@ public DesignerGroup() { Configure(Paths.Designer, x => { - x.Roles(Designer); + x.Roles(DesignerRole); x.Description(x => x.WithTags(Tags[$"{Paths.Products}/{Paths.Designer}"])); }); } diff --git a/src/Modules/Catalog/API/Products/Endpoints/Gallery/Get/Single/GetGalleryProductEndpoint.cs b/src/Modules/Catalog/API/Products/Endpoints/Gallery/Get/Single/GetGalleryProductEndpoint.cs index c6f8ec914..104e45906 100644 --- a/src/Modules/Catalog/API/Products/Endpoints/Gallery/Get/Single/GetGalleryProductEndpoint.cs +++ b/src/Modules/Catalog/API/Products/Endpoints/Gallery/Get/Single/GetGalleryProductEndpoint.cs @@ -17,10 +17,13 @@ public override void Configure() public override async Task HandleAsync(GetGalleryProductRequest req, CancellationToken ct) { + string? viewed = HttpContext.Request.Headers["X-Viewed-Product"]; + GalleryGetProductByIdDto product = await sender.SendQueryAsync( query: new GalleryGetProductByIdQuery( Id: ProductId.New(req.Id), - CallerId: User.AccountId + CallerId: User.AccountId, + Viewed: string.Equals(viewed, "true", StringComparison.CurrentCultureIgnoreCase) ), ct: ct ).ConfigureAwait(false); diff --git a/src/Modules/Catalog/API/Products/Endpoints/Gallery/Patch/AddTag/AddProductTagEndpoint.cs b/src/Modules/Catalog/API/Products/Endpoints/Gallery/Patch/AddTag/AddProductTagEndpoint.cs index 48aeb3268..078d86b6f 100644 --- a/src/Modules/Catalog/API/Products/Endpoints/Gallery/Patch/AddTag/AddProductTagEndpoint.cs +++ b/src/Modules/Catalog/API/Products/Endpoints/Gallery/Patch/AddTag/AddProductTagEndpoint.cs @@ -2,7 +2,7 @@ namespace CustomCADs.Modules.Catalog.API.Products.Endpoints.Gallery.Patch.AddTag; -using static DomainConstants.Roles; +using static DomainConstants.Users; public class AddProductTagEndpoint(IRequestSender sender) : Endpoint @@ -11,7 +11,7 @@ public override void Configure() { Patch("tags/add"); Group(); - Roles(Admin); + Roles(AdminRole); Description(x => x .WithSummary("Add Tag") .WithDescription("Adds a Tag to a Product") diff --git a/src/Modules/Catalog/API/Products/Endpoints/Gallery/Patch/RemoveTag/RemoveProductTagEndpoint.cs b/src/Modules/Catalog/API/Products/Endpoints/Gallery/Patch/RemoveTag/RemoveProductTagEndpoint.cs index 29f13f895..564ba36c5 100644 --- a/src/Modules/Catalog/API/Products/Endpoints/Gallery/Patch/RemoveTag/RemoveProductTagEndpoint.cs +++ b/src/Modules/Catalog/API/Products/Endpoints/Gallery/Patch/RemoveTag/RemoveProductTagEndpoint.cs @@ -2,7 +2,7 @@ namespace CustomCADs.Modules.Catalog.API.Products.Endpoints.Gallery.Patch.RemoveTag; -using static DomainConstants.Roles; +using static DomainConstants.Users; public class RemoveProductTagEndpoint(IRequestSender sender) : Endpoint @@ -11,7 +11,7 @@ public override void Configure() { Patch("tags/remove"); Group(); - Roles(Admin); + Roles(AdminRole); Description(x => x .WithSummary("Remove Tag") .WithDescription("Removes a Tag from a Product") diff --git a/src/Modules/Catalog/API/Tags/Endpoints/TagGroup.cs b/src/Modules/Catalog/API/Tags/Endpoints/TagGroup.cs index 18e289920..07c0c50c1 100644 --- a/src/Modules/Catalog/API/Tags/Endpoints/TagGroup.cs +++ b/src/Modules/Catalog/API/Tags/Endpoints/TagGroup.cs @@ -1,7 +1,7 @@ namespace CustomCADs.Modules.Catalog.API.Tags.Endpoints; using static APIConstants; -using static DomainConstants.Roles; +using static DomainConstants.Users; public class TagGroup : Group { @@ -9,7 +9,7 @@ public TagGroup() { Configure(Paths.Tags, x => { - x.Roles(Admin); + x.Roles(AdminRole); x.Description(x => x.WithTags(Tags[Paths.Tags])); }); } diff --git a/src/Modules/Catalog/Application/Products/Commands/Internal/Creator/Create/CreateProductHandler.cs b/src/Modules/Catalog/Application/Products/Commands/Internal/Creator/Create/CreateProductHandler.cs index 9e89fb32e..0ec5e3060 100644 --- a/src/Modules/Catalog/Application/Products/Commands/Internal/Creator/Create/CreateProductHandler.cs +++ b/src/Modules/Catalog/Application/Products/Commands/Internal/Creator/Create/CreateProductHandler.cs @@ -59,7 +59,7 @@ public async Task Handle(CreateProductCommand req, CancellationToken ct: ct ).ConfigureAwait(false); - if (role is Roles.Designer) + if (role is Users.DesignerRole) { product.Validate(req.CallerId); } @@ -71,7 +71,7 @@ await raiser.RaiseApplicationEventAsync( TagIds: TagId.Filter(new() { [Tags.NewId] = true, - [Tags.ProfessionalId] = role is Roles.Designer, + [Tags.ProfessionalId] = role is Users.DesignerRole, [Tags.PrintableId] = await sender.SendQueryAsync( query: new IsCadPrintableByIdQuery(req.CadId), ct: ct diff --git a/src/Modules/Catalog/Application/Products/Events/Application/ProductViewed/ProductViewedApplicationEvent.cs b/src/Modules/Catalog/Application/Products/Events/Application/ProductViewed/ProductViewedApplicationEvent.cs index a0512c2b4..dfdfe9784 100644 --- a/src/Modules/Catalog/Application/Products/Events/Application/ProductViewed/ProductViewedApplicationEvent.cs +++ b/src/Modules/Catalog/Application/Products/Events/Application/ProductViewed/ProductViewedApplicationEvent.cs @@ -5,5 +5,6 @@ namespace CustomCADs.Modules.Catalog.Application.Products.Events.Application.Pro public record ProductViewedApplicationEvent( ProductId Id, - AccountId AccountId + AccountId AccountId, + DateTimeOffset ViewedAt ) : BaseApplicationEvent; diff --git a/src/Modules/Catalog/Application/Products/Events/Application/ProductViewed/ProductViewedHandler.cs b/src/Modules/Catalog/Application/Products/Events/Application/ProductViewed/ProductViewedHandler.cs index 4ac355d0f..ac559af70 100644 --- a/src/Modules/Catalog/Application/Products/Events/Application/ProductViewed/ProductViewedHandler.cs +++ b/src/Modules/Catalog/Application/Products/Events/Application/ProductViewed/ProductViewedHandler.cs @@ -11,30 +11,23 @@ public class ProductViewedHandler(IProductReads reads, IUnitOfWork uow, IRequest { public async Task HandleAsync(ProductViewedApplicationEvent ae) { - Product product = await reads.SingleByIdAsync(ae.Id).ConfigureAwait(false) - ?? throw CustomNotFoundException.ById(ae.Id); - - string username = await sender.SendQueryAsync( - query: new GetUsernameByIdQuery(ae.AccountId) + bool userAlreadyViewed = await sender.SendQueryAsync( + query: new GetAccountViewedProductQuery(ae.AccountId, ae.Id) ).ConfigureAwait(false); + if (userAlreadyViewed) return; - (DateTimeOffset _, bool UserTracksViewedProducts, string? _, string? _) = await sender.SendQueryAsync( - query: new GetAccountInfoByUsernameQuery(username) + var account = await sender.SendQueryAsync( + query: new GetAccountInfoByUsernameQuery( + Username: await sender.SendQueryAsync( + query: new GetUsernameByIdQuery(ae.AccountId) + ).ConfigureAwait(false) + ) ).ConfigureAwait(false); - if (!UserTracksViewedProducts) - { - return; - } + if (!account.TrackViewedProducts) return; - bool userAlreadyViewed = await sender.SendQueryAsync( - query: new GetAccountViewedProductQuery(ae.AccountId, ae.Id) - ).ConfigureAwait(false); - - if (userAlreadyViewed) - { - return; - } + Product product = await reads.SingleByIdAsync(ae.Id).ConfigureAwait(false) + ?? throw CustomNotFoundException.ById(ae.Id); product.AddToViewCount(); await uow.SaveChangesAsync().ConfigureAwait(false); @@ -42,7 +35,8 @@ public async Task HandleAsync(ProductViewedApplicationEvent ae) await raiser.RaiseApplicationEventAsync( @event: new UserViewedProductApplicationEvent( Id: ae.Id, - AccountId: ae.AccountId + AccountId: ae.AccountId, + ViewedAt: ae.ViewedAt ) ).ConfigureAwait(false); } diff --git a/src/Modules/Catalog/Application/Products/Policies/ProductCadUploadPolicy.cs b/src/Modules/Catalog/Application/Products/Policies/ProductCadUploadPolicy.cs index 847cca584..b18764781 100644 --- a/src/Modules/Catalog/Application/Products/Policies/ProductCadUploadPolicy.cs +++ b/src/Modules/Catalog/Application/Products/Policies/ProductCadUploadPolicy.cs @@ -5,6 +5,8 @@ namespace CustomCADs.Modules.Catalog.Application.Products.Policies; +using static DomainConstants; + public class ProductCadUploadPolicy(IRequestSender sender) : IFileUploadPolicy { public FileContextType Type => FileContextType.Product; @@ -15,7 +17,7 @@ public async Task EnsureUploadGrantedAsync(IFileUploadPolicy.FileContext query: new GetUserRoleByIdQuery(context.CallerId) ).ConfigureAwait(false); - if (role is not (DomainConstants.Roles.Contributor or DomainConstants.Roles.Designer)) + if (role is not (Users.ContributorRole or Users.DesignerRole)) { throw CustomAuthorizationException.Custom("Must be a Contributor/Designer to upload Product CADs"); } diff --git a/src/Modules/Catalog/Application/Products/Queries/Internal/Gallery/GetById/GalleryGetProductByIdHandler.cs b/src/Modules/Catalog/Application/Products/Queries/Internal/Gallery/GetById/GalleryGetProductByIdHandler.cs index d9dade8e8..44d1e294c 100644 --- a/src/Modules/Catalog/Application/Products/Queries/Internal/Gallery/GetById/GalleryGetProductByIdHandler.cs +++ b/src/Modules/Catalog/Application/Products/Queries/Internal/Gallery/GetById/GalleryGetProductByIdHandler.cs @@ -21,12 +21,13 @@ public async Task Handle(GalleryGetProductByIdQuery re throw CustomStatusException.ById(req.Id); } - if (!req.CallerId.IsEmpty()) + if (req.Viewed && !req.CallerId.IsEmpty()) { await raiser.RaiseApplicationEventAsync( @event: new ProductViewedApplicationEvent( Id: req.Id, - AccountId: req.CallerId + AccountId: req.CallerId, + ViewedAt: DateTimeOffset.UtcNow ) ).ConfigureAwait(false); } diff --git a/src/Modules/Catalog/Application/Products/Queries/Internal/Gallery/GetById/GalleryGetProductByIdQuery.cs b/src/Modules/Catalog/Application/Products/Queries/Internal/Gallery/GetById/GalleryGetProductByIdQuery.cs index 59a25f2f0..141d7861c 100644 --- a/src/Modules/Catalog/Application/Products/Queries/Internal/Gallery/GetById/GalleryGetProductByIdQuery.cs +++ b/src/Modules/Catalog/Application/Products/Queries/Internal/Gallery/GetById/GalleryGetProductByIdQuery.cs @@ -4,5 +4,6 @@ namespace CustomCADs.Modules.Catalog.Application.Products.Queries.Internal.Galle public sealed record GalleryGetProductByIdQuery( ProductId Id, - AccountId CallerId + AccountId CallerId, + bool Viewed ) : IQuery; diff --git a/src/Modules/Customs/API/Customs/Endpoints/Admins/AdminGroup.cs b/src/Modules/Customs/API/Customs/Endpoints/Admins/AdminGroup.cs index c4dd05b8c..774ae62d8 100644 --- a/src/Modules/Customs/API/Customs/Endpoints/Admins/AdminGroup.cs +++ b/src/Modules/Customs/API/Customs/Endpoints/Admins/AdminGroup.cs @@ -1,7 +1,7 @@ namespace CustomCADs.Modules.Customs.API.Customs.Endpoints.Admins; using static APIConstants; -using static DomainConstants.Roles; +using static DomainConstants.Users; public class AdminGroup : SubGroup { @@ -9,7 +9,7 @@ public AdminGroup() { Configure(Paths.Admin, x => { - x.Roles(Admin); + x.Roles(AdminRole); x.Description(x => x.WithTags(Tags[$"{Paths.Customs}/{Paths.Admin}"])); }); } diff --git a/src/Modules/Customs/API/Customs/Endpoints/Customers/CustomerGroup.cs b/src/Modules/Customs/API/Customs/Endpoints/Customers/CustomerGroup.cs index e2695418d..14d71af2b 100644 --- a/src/Modules/Customs/API/Customs/Endpoints/Customers/CustomerGroup.cs +++ b/src/Modules/Customs/API/Customs/Endpoints/Customers/CustomerGroup.cs @@ -1,7 +1,7 @@ namespace CustomCADs.Modules.Customs.API.Customs.Endpoints.Customers; using static APIConstants; -using static DomainConstants.Roles; +using static DomainConstants.Users; public class CustomerGroup : SubGroup { @@ -9,7 +9,7 @@ public CustomerGroup() { Configure(Paths.Customer, x => { - x.Roles(Customer); + x.Roles(CustomerRole); x.Description(x => x.WithTags(Tags[$"{Paths.Customs}/{Paths.Customer}"])); }); } diff --git a/src/Modules/Customs/API/Customs/Endpoints/Designer/DesignerGroup.cs b/src/Modules/Customs/API/Customs/Endpoints/Designer/DesignerGroup.cs index 074721819..2227158b5 100644 --- a/src/Modules/Customs/API/Customs/Endpoints/Designer/DesignerGroup.cs +++ b/src/Modules/Customs/API/Customs/Endpoints/Designer/DesignerGroup.cs @@ -1,7 +1,7 @@ namespace CustomCADs.Modules.Customs.API.Customs.Endpoints.Designer; using static APIConstants; -using static DomainConstants.Roles; +using static DomainConstants.Users; public class DesignerGroup : SubGroup { @@ -9,7 +9,7 @@ public DesignerGroup() { Configure(Paths.Designer, x => { - x.Roles(Designer); + x.Roles(DesignerRole); x.Description(x => x.WithTags(Tags[$"{Paths.Customs}/{Paths.Designer}"])); }); } diff --git a/src/Modules/Customs/Application/Customs/Commands/Internal/Customers/Create/CreateCustomHandler.cs b/src/Modules/Customs/Application/Customs/Commands/Internal/Customers/Create/CreateCustomHandler.cs index c8c41199c..e948fad58 100644 --- a/src/Modules/Customs/Application/Customs/Commands/Internal/Customers/Create/CreateCustomHandler.cs +++ b/src/Modules/Customs/Application/Customs/Commands/Internal/Customers/Create/CreateCustomHandler.cs @@ -45,7 +45,7 @@ await raiser.RaiseApplicationEventAsync( Link: Notifications.Links.CustomCreated, AuthorId: custom.BuyerId, ReceiverIds: [.. await sender.SendQueryAsync( - query: new GetAccountIdsByRoleQuery(DomainConstants.Roles.Designer), + query: new GetAccountIdsByRoleQuery(DomainConstants.Users.DesignerRole), ct: ct ).ConfigureAwait(false)] ) diff --git a/src/Modules/Customs/Application/Customs/Policies/CustomCadDownloadPolicy.cs b/src/Modules/Customs/Application/Customs/Policies/CustomCadDownloadPolicy.cs index 5ecaf5e8f..4b472ab16 100644 --- a/src/Modules/Customs/Application/Customs/Policies/CustomCadDownloadPolicy.cs +++ b/src/Modules/Customs/Application/Customs/Policies/CustomCadDownloadPolicy.cs @@ -21,11 +21,11 @@ public async Task EnsureDownloadGrantedAsync(IFileDownloadPolicy.FileCont switch (role) { - case DomainConstants.Roles.Customer: + case DomainConstants.Users.CustomerRole: await EnsureCustomerDownloadGrantedAsync(context).ConfigureAwait(false); break; - case DomainConstants.Roles.Designer: + case DomainConstants.Users.DesignerRole: await EnsureDesignerDownloadGrantedAsync(context).ConfigureAwait(false); break; diff --git a/src/Modules/Customs/Application/Customs/Policies/CustomCadUploadPolicy.cs b/src/Modules/Customs/Application/Customs/Policies/CustomCadUploadPolicy.cs index fc1003a21..f78ae3340 100644 --- a/src/Modules/Customs/Application/Customs/Policies/CustomCadUploadPolicy.cs +++ b/src/Modules/Customs/Application/Customs/Policies/CustomCadUploadPolicy.cs @@ -5,6 +5,8 @@ namespace CustomCADs.Modules.Customs.Application.Customs.Policies; +using static DomainConstants; + public class CustomCadUploadPolicy(IRequestSender sender) : IFileUploadPolicy { public FileContextType Type => FileContextType.Custom; @@ -15,7 +17,7 @@ public async Task EnsureUploadGrantedAsync(IFileUploadPolicy.FileContext query: new GetUserRoleByIdQuery(context.CallerId) ).ConfigureAwait(false); - if (role is not DomainConstants.Roles.Designer) + if (role is not Users.DesignerRole) { throw CustomAuthorizationException.Custom("Must be a Designer to upload a Custom's CAD."); } diff --git a/src/Modules/Delivery/API/Shipments/Endpoints/Get/Waybill/GetShipmentGetWaybillEndpoint.cs b/src/Modules/Delivery/API/Shipments/Endpoints/Get/Waybill/GetShipmentGetWaybillEndpoint.cs index cacb2b89a..c478d33df 100644 --- a/src/Modules/Delivery/API/Shipments/Endpoints/Get/Waybill/GetShipmentGetWaybillEndpoint.cs +++ b/src/Modules/Delivery/API/Shipments/Endpoints/Get/Waybill/GetShipmentGetWaybillEndpoint.cs @@ -2,7 +2,7 @@ namespace CustomCADs.Modules.Delivery.API.Shipments.Endpoints.Get.Waybill; -using static DomainConstants.Roles; +using static DomainConstants.Users; public class GetShipmentWaybillEndpoint(IRequestSender sender) : Endpoint @@ -11,7 +11,7 @@ public override void Configure() { Get("{id}/waybill"); Group(); - Roles(Designer); + Roles(DesignerRole); Description(x => x .WithSummary("Waybill") .WithDescription("Download this Shipment's waybill") diff --git a/src/Modules/Delivery/API/Shipments/Endpoints/ShipmentsGroup.cs b/src/Modules/Delivery/API/Shipments/Endpoints/ShipmentsGroup.cs index ce8965c1e..4feced555 100644 --- a/src/Modules/Delivery/API/Shipments/Endpoints/ShipmentsGroup.cs +++ b/src/Modules/Delivery/API/Shipments/Endpoints/ShipmentsGroup.cs @@ -1,7 +1,7 @@ namespace CustomCADs.Modules.Delivery.API.Shipments.Endpoints; using static APIConstants; -using static DomainConstants.Roles; +using static DomainConstants.Users; public class ShipmentsGroup : Group { @@ -9,7 +9,7 @@ public ShipmentsGroup() { Configure(Paths.Shipments, x => { - x.Roles(Customer); + x.Roles(CustomerRole); x.Description(x => x.WithTags(Tags[Paths.Shipments])); }); } diff --git a/src/Modules/Delivery/Application/Shipments/Commands/Internal/Cancel/CancelShipmentHandler.cs b/src/Modules/Delivery/Application/Shipments/Commands/Internal/Cancel/CancelShipmentHandler.cs index 635691ef4..fb73eac62 100644 --- a/src/Modules/Delivery/Application/Shipments/Commands/Internal/Cancel/CancelShipmentHandler.cs +++ b/src/Modules/Delivery/Application/Shipments/Commands/Internal/Cancel/CancelShipmentHandler.cs @@ -15,19 +15,19 @@ public async Task Handle(CancelShipmentCommand req, CancellationToken ct) { Shipment shipment = await reads.SingleByIdAsync(req.Id, track: false, ct).ConfigureAwait(false) ?? throw CustomNotFoundException.ById(req.Id); + shipment.Cancel(); - if (shipment is not { Status: ShipmentStatus.Active, Reference.Id: not null }) + if (shipment is not { Reference.Id: not null }) { throw CustomStatusException.ById(req.Id); } - shipment.Cancel(); - await uow.SaveChangesAsync(ct).ConfigureAwait(false); - await delivery.CancelAsync( shipmentId: shipment.Reference.Id, comment: req.Comment, ct: ct ).ConfigureAwait(false); + + await uow.SaveChangesAsync(ct).ConfigureAwait(false); } } diff --git a/src/Modules/Delivery/Application/Shipments/Queries/Internal/GetWaybill/GetShipmentWaybillHandler.cs b/src/Modules/Delivery/Application/Shipments/Queries/Internal/GetWaybill/GetShipmentWaybillHandler.cs index 682b11be0..f6150d019 100644 --- a/src/Modules/Delivery/Application/Shipments/Queries/Internal/GetWaybill/GetShipmentWaybillHandler.cs +++ b/src/Modules/Delivery/Application/Shipments/Queries/Internal/GetWaybill/GetShipmentWaybillHandler.cs @@ -16,8 +16,7 @@ public async Task Handle(GetShipmentWaybillQuery req, CancellationToken Shipment shipment = await reads.SingleByIdAsync(req.Id, track: false, ct: ct).ConfigureAwait(false) ?? throw CustomNotFoundException.ById(req.Id); - Guid headDesignerId = Guid.Parse(DesignerAccountId); - if (req.CallerId.Value != headDesignerId) + if (req.CallerId != HeadDesignerAccountId) { throw CustomAuthorizationException.ById(req.Id); } diff --git a/src/Modules/Files/API/Cads/Endpoints/CadsGroup.cs b/src/Modules/Files/API/Cads/Endpoints/CadsGroup.cs index 10db667c9..9a0a6c703 100644 --- a/src/Modules/Files/API/Cads/Endpoints/CadsGroup.cs +++ b/src/Modules/Files/API/Cads/Endpoints/CadsGroup.cs @@ -1,7 +1,7 @@ namespace CustomCADs.Modules.Files.API.Cads.Endpoints; using static APIConstants; -using static DomainConstants.Roles; +using static DomainConstants.Users; public class CadsGroup : Group { @@ -9,7 +9,7 @@ public CadsGroup() { Configure(Paths.Cads, x => { - x.Roles(Customer, Contributor, Designer, Admin); + x.Roles(CustomerRole, ContributorRole, DesignerRole, AdminRole); x.Description(x => x.WithTags(Tags[Paths.Cads])); }); } diff --git a/src/Modules/Files/API/Images/Endpoints/ImagesGroup.cs b/src/Modules/Files/API/Images/Endpoints/ImagesGroup.cs index 396319778..2994771a5 100644 --- a/src/Modules/Files/API/Images/Endpoints/ImagesGroup.cs +++ b/src/Modules/Files/API/Images/Endpoints/ImagesGroup.cs @@ -1,7 +1,7 @@ namespace CustomCADs.Modules.Files.API.Images.Endpoints; using static APIConstants; -using static DomainConstants.Roles; +using static DomainConstants.Users; public class ImagesGroup : Group { @@ -9,7 +9,7 @@ public ImagesGroup() { Configure(Paths.Images, x => { - x.Roles(Customer, Contributor, Designer, Admin); + x.Roles(CustomerRole, ContributorRole, DesignerRole, AdminRole); x.Description(x => x.WithTags(Tags[Paths.Images])); }); } diff --git a/src/Modules/Files/Domain/Repositories/Reads/ImageQuery.cs b/src/Modules/Files/Domain/Repositories/Reads/ImageQuery.cs deleted file mode 100644 index e868fc6d0..000000000 --- a/src/Modules/Files/Domain/Repositories/Reads/ImageQuery.cs +++ /dev/null @@ -1,8 +0,0 @@ -using CustomCADs.Shared.Domain.Querying; - -namespace CustomCADs.Modules.Files.Domain.Repositories.Reads; - -public record ImageQuery( - Pagination Pagination, - ImageId[]? Ids = null -); diff --git a/src/Modules/Files/Persistence/Configurations/Images/Utilities.cs b/src/Modules/Files/Persistence/Configurations/Images/Utilities.cs index e89fc684f..4e7934cac 100644 --- a/src/Modules/Files/Persistence/Configurations/Images/Utilities.cs +++ b/src/Modules/Files/Persistence/Configurations/Images/Utilities.cs @@ -58,7 +58,7 @@ internal EntityTypeBuilder SetValidaitons() internal EntityTypeBuilder SetSeeding() { - AccountId adminId = AccountId.New(Guid.Parse(DomainConstants.Users.AdminAccountId)); + AccountId adminId = DomainConstants.Users.AdminAccountId; builder.HasData([ Image.CreateWithId(PLA, "textures/pla.webp", "image/webp", adminId), Image.CreateWithId(ABS, "textures/abs.webp", "image/webp", adminId), diff --git a/src/Modules/Identity/API/DependencyInjection.cs b/src/Modules/Identity/API/DependencyInjection.cs index 7df22110b..aa4b4abd8 100644 --- a/src/Modules/Identity/API/DependencyInjection.cs +++ b/src/Modules/Identity/API/DependencyInjection.cs @@ -60,26 +60,16 @@ await context.HttpContext.RequestServices .GetRequiredService() .UnauthorizedResponseAsync( context: context.HttpContext, - ex: new UnauthorizedAccessException() + ex: new UnauthorizedAccessException("User needs an account to access this resource!") ).ConfigureAwait(false); }, - OnForbidden = async context => - { - await context.HttpContext.RequestServices + OnForbidden = async context => await context.HttpContext.RequestServices .GetRequiredService() .ForbiddenResponseAsync( context: context.HttpContext, - ex: new AccessViolationException() - ).ConfigureAwait(false); - }, - - OnTokenValidated = context => - { - ClaimsIdentity claimsIdentity = new(context.Principal?.Claims ?? [], AuthScheme); - context.HttpContext.User = new ClaimsPrincipal(claimsIdentity); - return Task.CompletedTask; - }, + ex: new UnauthorizedAccessException("User doesn't have permission to access this resource!") + ).ConfigureAwait(false), }; }); } diff --git a/src/Modules/Identity/API/Dtos/FingerprintResponse.cs b/src/Modules/Identity/API/Dtos/FingerprintResponse.cs new file mode 100644 index 000000000..6172672bd --- /dev/null +++ b/src/Modules/Identity/API/Dtos/FingerprintResponse.cs @@ -0,0 +1,9 @@ +namespace CustomCADs.Modules.Identity.API.Dtos; + +public record FingerprintResponse( + Guid Id, + string Device, + string? Location, + bool DeleteAllowed, + DateTimeOffset IssuedAt +); diff --git a/src/Modules/Identity/API/Dtos/ViewedProductResponse.cs b/src/Modules/Identity/API/Dtos/ViewedProductResponse.cs new file mode 100644 index 000000000..5325c19f3 --- /dev/null +++ b/src/Modules/Identity/API/Dtos/ViewedProductResponse.cs @@ -0,0 +1,3 @@ +namespace CustomCADs.Modules.Identity.API.Dtos; + +public record ViewedProductResponse(Guid Id, DateTimeOffset ViewedAt); diff --git a/src/Modules/Identity/API/Identity/Delete/DeleteAccountEndpoint.cs b/src/Modules/Identity/API/Identity/Delete/DeleteAccountEndpoint.cs index 077390dc1..ef54d3abe 100644 --- a/src/Modules/Identity/API/Identity/Delete/DeleteAccountEndpoint.cs +++ b/src/Modules/Identity/API/Identity/Delete/DeleteAccountEndpoint.cs @@ -12,7 +12,6 @@ public override void Configure() Delete(""); Group(); Description(x => x - .WithName(IdentityNames.DeleteAccount) .WithSummary("Delete") .WithDescription("Delete your account") ); @@ -21,7 +20,7 @@ public override void Configure() public override async Task HandleAsync(CancellationToken ct) { await sender.SendCommandAsync( - command: new DeleteUserCommand(Username: User.Name), + command: new DeleteUserCommand(User.AccountId), ct: ct ).ConfigureAwait(false); diff --git a/src/Modules/Identity/API/Identity/Delete/Fingerprints/DeleteFingerprintsEndpoint.cs b/src/Modules/Identity/API/Identity/Delete/Fingerprints/DeleteFingerprintsEndpoint.cs new file mode 100644 index 000000000..680e4362c --- /dev/null +++ b/src/Modules/Identity/API/Identity/Delete/Fingerprints/DeleteFingerprintsEndpoint.cs @@ -0,0 +1,33 @@ +using CustomCADs.Modules.Identity.Application.Users.Commands.Internal.Delete.Fingerprints; +using CustomCADs.Shared.Domain.TypedIds.Identity; + +namespace CustomCADs.Modules.Identity.API.Identity.Delete.Fingerprints; + +public sealed class DeleteFingerprintsEndpoint(IRequestSender sender) + : Endpoint +{ + public override void Configure() + { + Delete("fingerprint"); + Group(); + Description(x => x + .WithSummary("Remove Fingerprint") + .WithDescription("Remove a Fingerprint") + ); + } + + public override async Task HandleAsync(DeleteFingerprintsRequest req, CancellationToken ct) + { + await sender.SendCommandAsync( + command: new DeleteFingerprintCommand( + RefreshTokenId: RefreshTokenId.New(req.RefreshTokenId), + CurrentRefreshToken: HttpContext.RefreshTokenCookie, + CallerId: User.AccountId + ), + ct: ct + ).ConfigureAwait(false); + + await Send.NoContentAsync().ConfigureAwait(false); + } +} + diff --git a/src/Modules/Identity/API/Identity/Delete/Fingerprints/DeleteViewedProductRequest.cs b/src/Modules/Identity/API/Identity/Delete/Fingerprints/DeleteViewedProductRequest.cs new file mode 100644 index 000000000..9f6f22bc9 --- /dev/null +++ b/src/Modules/Identity/API/Identity/Delete/Fingerprints/DeleteViewedProductRequest.cs @@ -0,0 +1,3 @@ +namespace CustomCADs.Modules.Identity.API.Identity.Delete.Fingerprints; + +public sealed record DeleteFingerprintsRequest(Guid RefreshTokenId); diff --git a/src/Modules/Identity/API/Identity/Delete/ViewedProducts/DeleteViewedProductEndpoint.cs b/src/Modules/Identity/API/Identity/Delete/ViewedProducts/DeleteViewedProductEndpoint.cs new file mode 100644 index 000000000..95c1eed1d --- /dev/null +++ b/src/Modules/Identity/API/Identity/Delete/ViewedProducts/DeleteViewedProductEndpoint.cs @@ -0,0 +1,31 @@ +using CustomCADs.Shared.Application.UseCases.Accounts.Commands; +using CustomCADs.Shared.Domain.TypedIds.Catalog; + +namespace CustomCADs.Modules.Identity.API.Identity.Delete.ViewedProducts; + +public sealed class DeleteViewedProductEndpoint(IRequestSender sender) + : Endpoint +{ + public override void Configure() + { + Delete("viewed-product"); + Group(); + Description(x => x + .WithSummary("Remove Viewed Product") + .WithDescription("Remove a Viewed Product") + ); + } + + public override async Task HandleAsync(DeleteViewedProductRequest req, CancellationToken ct) + { + await sender.SendCommandAsync( + command: new DeleteViewedProductCommand( + ProductId: ProductId.New(req.ProductId), + CallerId: User.AccountId + ), + ct: ct + ).ConfigureAwait(false); + + await Send.NoContentAsync().ConfigureAwait(false); + } +} diff --git a/src/Modules/Identity/API/Identity/Delete/ViewedProducts/DeleteViewedProductRequest.cs b/src/Modules/Identity/API/Identity/Delete/ViewedProducts/DeleteViewedProductRequest.cs new file mode 100644 index 000000000..9f79a962f --- /dev/null +++ b/src/Modules/Identity/API/Identity/Delete/ViewedProducts/DeleteViewedProductRequest.cs @@ -0,0 +1,3 @@ +namespace CustomCADs.Modules.Identity.API.Identity.Delete.ViewedProducts; + +public sealed record DeleteViewedProductRequest(Guid ProductId); diff --git a/src/Modules/Identity/API/Identity/Get/Authentication/AuthenticationEndpoint.cs b/src/Modules/Identity/API/Identity/Get/Authentication/AuthenticationEndpoint.cs index 008027556..689fa94cc 100644 --- a/src/Modules/Identity/API/Identity/Get/Authentication/AuthenticationEndpoint.cs +++ b/src/Modules/Identity/API/Identity/Get/Authentication/AuthenticationEndpoint.cs @@ -9,7 +9,6 @@ public override void Configure() Group(); AllowAnonymous(); Description(x => x - .WithName(IdentityNames.Authentication) .WithSummary("AuthN") .WithDescription("See if you're logged in") ); diff --git a/src/Modules/Identity/API/Identity/Get/Authorization/AuthorizationEndpoint.cs b/src/Modules/Identity/API/Identity/Get/Authorization/AuthorizationEndpoint.cs index da4fae1e5..517bed875 100644 --- a/src/Modules/Identity/API/Identity/Get/Authorization/AuthorizationEndpoint.cs +++ b/src/Modules/Identity/API/Identity/Get/Authorization/AuthorizationEndpoint.cs @@ -9,7 +9,6 @@ public override void Configure() Group(); AllowAnonymous(); Description(x => x - .WithName(IdentityNames.Authorization) .WithSummary("AuthZ") .WithDescription("See what Role you're logged in with") ); diff --git a/src/Modules/Identity/API/Identity/Get/DownloadInfo/DownloadInfoEndpoint.cs b/src/Modules/Identity/API/Identity/Get/DownloadInfo/DownloadInfoEndpoint.cs index b0f2038ae..1582bb8fd 100644 --- a/src/Modules/Identity/API/Identity/Get/DownloadInfo/DownloadInfoEndpoint.cs +++ b/src/Modules/Identity/API/Identity/Get/DownloadInfo/DownloadInfoEndpoint.cs @@ -11,7 +11,6 @@ public override void Configure() Get("download-info"); Group(); Description(x => x - .WithName(IdentityNames.DownloadInfo) .WithSummary("Download Info") .WithDescription("Download all your persisted info") ); @@ -20,7 +19,7 @@ public override void Configure() public override async Task HandleAsync(CancellationToken ct) { GetUserByUsernameDto user = await sender.SendQueryAsync( - query: new GetUserByUsernameQuery(User.Name), + query: new GetUserByUsernameQuery(User.AccountId, HttpContext.RefreshTokenCookie), ct: ct ).ConfigureAwait(false); @@ -37,7 +36,8 @@ await JsonSerializer.SerializeAsync( firstName = user.FirstName, lastName = user.LastName, trackViewedProducts = user.TrackViewedProducts, - viewedProductIds = user.ViewedProductIds.Select(x => x.Value), + fingerprints = user.Fingerprints.Select(x => new { id = x.Id.Value, device = x.Device, location = x.Location, issuedAt = x.IssuedAt }), + viewedProducts = user.ViewedProducts.Select(x => new { id = x.Id.Value, viewedAt = x.ViewedAt }), }, options: new() { WriteIndented = true } ).ConfigureAwait(false); diff --git a/src/Modules/Identity/API/Identity/Get/MyAccount/MyAccountEndpoint.cs b/src/Modules/Identity/API/Identity/Get/MyAccount/MyAccountEndpoint.cs index b55e3334c..a55d5ddfb 100644 --- a/src/Modules/Identity/API/Identity/Get/MyAccount/MyAccountEndpoint.cs +++ b/src/Modules/Identity/API/Identity/Get/MyAccount/MyAccountEndpoint.cs @@ -10,7 +10,6 @@ public override void Configure() Get("my-account"); Group(); Description(x => x - .WithName(IdentityNames.MyAccount) .WithSummary("My Account") .WithDescription("See your Account's details") ); @@ -19,7 +18,7 @@ public override void Configure() public override async Task HandleAsync(CancellationToken ct) { GetUserByUsernameDto user = await sender.SendQueryAsync( - query: new GetUserByUsernameQuery(User.Name), + query: new GetUserByUsernameQuery(User.AccountId, HttpContext.RefreshTokenCookie), ct: ct ).ConfigureAwait(false); diff --git a/src/Modules/Identity/API/Identity/Get/MyAccount/MyAccountMapper.cs b/src/Modules/Identity/API/Identity/Get/MyAccount/MyAccountMapper.cs index 43cb5ec20..28a4eac76 100644 --- a/src/Modules/Identity/API/Identity/Get/MyAccount/MyAccountMapper.cs +++ b/src/Modules/Identity/API/Identity/Get/MyAccount/MyAccountMapper.cs @@ -1,4 +1,7 @@ +using CustomCADs.Modules.Identity.API.Dtos; +using CustomCADs.Modules.Identity.Application.Users.Dtos; using CustomCADs.Modules.Identity.Application.Users.Queries.Internal.GetByUsername; +using CustomCADs.Shared.Application.UseCases.Accounts.Queries; namespace CustomCADs.Modules.Identity.API.Identity.Get.MyAccount; @@ -13,6 +16,23 @@ public override MyAccountResponse FromEntity(GetUserByUsernameDto user) LastName: user.LastName, Email: user.Email.Value, TrackViewedProducts: user.TrackViewedProducts, - CreatedAt: user.CreatedAt + CreatedAt: user.CreatedAt, + ViewedProducts: [.. user.ViewedProducts.Select(ToResponse)], + Fingerprints: [.. user.Fingerprints.Select(ToResponse)] + ); + + private static ViewedProductResponse ToResponse(ViewedProductDto product) + => new( + Id: product.Id.Value, + ViewedAt: product.ViewedAt + ); + + private static FingerprintResponse ToResponse(FingerprintDto fingerprint) + => new( + Id: fingerprint.Id.Value, + Device: fingerprint.Device, + Location: fingerprint.Location, + DeleteAllowed: fingerprint.DeleteAllowed, + IssuedAt: fingerprint.IssuedAt ); } diff --git a/src/Modules/Identity/API/Identity/Get/MyAccount/MyAccountResponse.cs b/src/Modules/Identity/API/Identity/Get/MyAccount/MyAccountResponse.cs index 2d6ff7322..fcc974ab3 100644 --- a/src/Modules/Identity/API/Identity/Get/MyAccount/MyAccountResponse.cs +++ b/src/Modules/Identity/API/Identity/Get/MyAccount/MyAccountResponse.cs @@ -1,3 +1,5 @@ +using CustomCADs.Modules.Identity.API.Dtos; + namespace CustomCADs.Modules.Identity.API.Identity.Get.MyAccount; public record MyAccountResponse( @@ -8,5 +10,7 @@ public record MyAccountResponse( string? LastName, string Email, bool TrackViewedProducts, - DateTimeOffset CreatedAt + DateTimeOffset CreatedAt, + ViewedProductResponse[] ViewedProducts, + FingerprintResponse[] Fingerprints ); diff --git a/src/Modules/Identity/API/Identity/IdentityNames.cs b/src/Modules/Identity/API/Identity/IdentityNames.cs deleted file mode 100644 index a420d38bf..000000000 --- a/src/Modules/Identity/API/Identity/IdentityNames.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace CustomCADs.Modules.Identity.API.Identity; - -public static class IdentityNames -{ - public const string Register = "Register"; - public const string ConfirmEmail = "Confirm Email"; - public const string RetryConfirmEmail = "Retry Confirm Email"; - - public const string Login = "Login"; - public const string Refresh = "Refresh"; - public const string ForgotPassword = "Forgot Password"; - public const string ResetPassword = "Reset Password"; - public const string Logout = "Logout"; - public const string ChangeUsername = "Change Username"; - public const string ToggleViewedProductsTracking = "Toggle Viewed Products Tracking"; - public const string DeleteAccount = "Delete Account"; - - public const string Authentication = "Authentication"; - public const string Authorization = "Authorization"; - public const string MyAccount = "My Account"; - public const string DownloadInfo = "Download Info"; -} diff --git a/src/Modules/Identity/API/Identity/Patch/ChangeUsername/ChangeUsernameRequest.cs b/src/Modules/Identity/API/Identity/Patch/ChangeUsername/ChangeUsernameRequest.cs deleted file mode 100644 index 94d1c73ea..000000000 --- a/src/Modules/Identity/API/Identity/Patch/ChangeUsername/ChangeUsernameRequest.cs +++ /dev/null @@ -1,5 +0,0 @@ -namespace CustomCADs.Modules.Identity.API.Identity.Patch.ChangeUsername; - -public sealed record ChangeUsernameRequest( - string Username -); diff --git a/src/Modules/Identity/API/Identity/Patch/ChangeUsername/ChangeUsernameEndpoint.cs b/src/Modules/Identity/API/Identity/Patch/Names/ChangeNamesEndpoint.cs similarity index 50% rename from src/Modules/Identity/API/Identity/Patch/ChangeUsername/ChangeUsernameEndpoint.cs rename to src/Modules/Identity/API/Identity/Patch/Names/ChangeNamesEndpoint.cs index 038a31396..42716b5f7 100644 --- a/src/Modules/Identity/API/Identity/Patch/ChangeUsername/ChangeUsernameEndpoint.cs +++ b/src/Modules/Identity/API/Identity/Patch/Names/ChangeNamesEndpoint.cs @@ -1,27 +1,28 @@ using CustomCADs.Modules.Identity.Application.Users.Commands.Internal.ChangeUsername; -namespace CustomCADs.Modules.Identity.API.Identity.Patch.ChangeUsername; +namespace CustomCADs.Modules.Identity.API.Identity.Patch.Names; -public sealed class ChangeUsernameEndpoint(IRequestSender sender) - : Endpoint +public sealed class ChangeNamesEndpoint(IRequestSender sender) + : Endpoint { public override void Configure() { - Patch("username"); + Patch("names"); Group(); Description(x => x - .WithName(IdentityNames.ChangeUsername) .WithSummary("Change Username") .WithDescription("Change your Username") ); } - public override async Task HandleAsync(ChangeUsernameRequest req, CancellationToken ct) + public override async Task HandleAsync(ChangeNamesRequest req, CancellationToken ct) { await sender.SendCommandAsync( command: new ChangeUsernameCommand( - Username: User.Name, - NewUsername: req.Username + Id: User.AccountId, + Username: req.Username, + FirstName: req.FirstName, + LastName: req.LastName ), ct: ct ).ConfigureAwait(false); diff --git a/src/Modules/Identity/API/Identity/Patch/Names/ChangeUsernameRequest.cs b/src/Modules/Identity/API/Identity/Patch/Names/ChangeUsernameRequest.cs new file mode 100644 index 000000000..6187bd24e --- /dev/null +++ b/src/Modules/Identity/API/Identity/Patch/Names/ChangeUsernameRequest.cs @@ -0,0 +1,7 @@ +namespace CustomCADs.Modules.Identity.API.Identity.Patch.Names; + +public sealed record ChangeNamesRequest( + string Username, + string? FirstName, + string? LastName +); diff --git a/src/Modules/Identity/API/Identity/Patch/ToggleViewedProductsTracking/ToggleViewedProductsTrackingEndpoint.cs b/src/Modules/Identity/API/Identity/Patch/ToggleViewedProductsTracking/ToggleViewedProductsTrackingEndpoint.cs index bb216759d..9ca86b99f 100644 --- a/src/Modules/Identity/API/Identity/Patch/ToggleViewedProductsTracking/ToggleViewedProductsTrackingEndpoint.cs +++ b/src/Modules/Identity/API/Identity/Patch/ToggleViewedProductsTracking/ToggleViewedProductsTrackingEndpoint.cs @@ -10,7 +10,6 @@ public override void Configure() Patch("viewed-products"); Group(); Description(x => x - .WithName(IdentityNames.ToggleViewedProductsTracking) .WithSummary("Viewed Products Tracking") .WithDescription("Toggle whether the Products you View get Tracked") ); @@ -20,7 +19,8 @@ public override async Task HandleAsync(CancellationToken ct) { await sender.SendCommandAsync( command: new ToggleViewedProductsTrackingCommand( - Username: User.Name + Username: User.Name, + CallerId: User.AccountId ), ct: ct ).ConfigureAwait(false); diff --git a/src/Modules/Identity/API/Identity/Post/ForgotPassword/ForgotPasswordEndpoint.cs b/src/Modules/Identity/API/Identity/Post/ForgotPassword/ForgotPasswordEndpoint.cs index 878a78694..38f94c914 100644 --- a/src/Modules/Identity/API/Identity/Post/ForgotPassword/ForgotPasswordEndpoint.cs +++ b/src/Modules/Identity/API/Identity/Post/ForgotPassword/ForgotPasswordEndpoint.cs @@ -12,7 +12,6 @@ public override void Configure() Group(); AllowAnonymous(); Description(x => x - .WithName(IdentityNames.ForgotPassword) .WithSummary("Reset Password Email") .WithDescription("Receive an Email with a link to reset your Password") .WithMetadata(new SkipIdempotencyAttribute()) diff --git a/src/Modules/Identity/API/Identity/Post/Login/LoginEndpoint.cs b/src/Modules/Identity/API/Identity/Post/Login/LoginEndpoint.cs index a14147887..33558dd2c 100644 --- a/src/Modules/Identity/API/Identity/Post/Login/LoginEndpoint.cs +++ b/src/Modules/Identity/API/Identity/Post/Login/LoginEndpoint.cs @@ -1,11 +1,12 @@ -using CustomCADs.Modules.Identity.Application.Users.Commands.Internal.Login; +using CustomCADs.Modules.Identity.Application.Contracts; +using CustomCADs.Modules.Identity.Application.Users.Commands.Internal.Login; using CustomCADs.Modules.Identity.Application.Users.Dtos; using CustomCADs.Shared.API.Attributes; using Microsoft.Extensions.Options; namespace CustomCADs.Modules.Identity.API.Identity.Post.Login; -public sealed class LoginEndpoint(IRequestSender sender, IOptions settings) +public sealed class LoginEndpoint(IRequestSender sender, IFingerprintService fingerprintService, IOptions settings) : Endpoint { public override void Configure() @@ -14,7 +15,6 @@ public override void Configure() Group(); AllowAnonymous(); Description(x => x - .WithName(IdentityNames.Login) .WithSummary("Login") .WithDescription("Log in to your account") .WithMetadata(new SkipIdempotencyAttribute()) @@ -27,7 +27,8 @@ public override async Task HandleAsync(LoginRequest req, CancellationToken ct) command: new LoginUserCommand( Username: req.Username, Password: req.Password, - LongerExpireTime: req.RememberMe ?? false + LongerExpireTime: req.RememberMe ?? false, + Fingerprint: fingerprintService.GetFingerprint(HttpContext.Request.HeadersDictionary) ), ct: ct ).ConfigureAwait(false); diff --git a/src/Modules/Identity/API/Identity/Post/Logout/LogoutEndpoint.cs b/src/Modules/Identity/API/Identity/Post/Logout/LogoutEndpoint.cs index 8fa45935c..b2fe18a25 100644 --- a/src/Modules/Identity/API/Identity/Post/Logout/LogoutEndpoint.cs +++ b/src/Modules/Identity/API/Identity/Post/Logout/LogoutEndpoint.cs @@ -13,7 +13,6 @@ public override void Configure() Post("logout"); Group(); Description(x => x - .WithName(IdentityNames.Logout) .WithSummary("Log out") .WithDescription("Log out of your account") .WithMetadata(new SkipIdempotencyAttribute()) diff --git a/src/Modules/Identity/API/Identity/Post/RefreshToken/RefreshTokenEndpoint.cs b/src/Modules/Identity/API/Identity/Post/RefreshToken/RefreshTokenEndpoint.cs index 046eb6015..bfe25e195 100644 --- a/src/Modules/Identity/API/Identity/Post/RefreshToken/RefreshTokenEndpoint.cs +++ b/src/Modules/Identity/API/Identity/Post/RefreshToken/RefreshTokenEndpoint.cs @@ -1,11 +1,12 @@ -using CustomCADs.Modules.Identity.Application.Users.Commands.Internal.Refresh; +using CustomCADs.Modules.Identity.Application.Contracts; +using CustomCADs.Modules.Identity.Application.Users.Commands.Internal.Refresh; using CustomCADs.Modules.Identity.Application.Users.Dtos; using CustomCADs.Shared.API.Attributes; using Microsoft.Extensions.Options; namespace CustomCADs.Modules.Identity.API.Identity.Post.RefreshToken; -public sealed class RefreshTokenEndpoint(IRequestSender sender, IOptions settings) +public sealed class RefreshTokenEndpoint(IRequestSender sender, IFingerprintService fingerprintService, IOptions settings) : EndpointWithoutRequest { public override void Configure() @@ -14,7 +15,6 @@ public override void Configure() Group(); AllowAnonymous(); Description(x => x - .WithName(IdentityNames.Refresh) .WithSummary("Refresh") .WithDescription("Refresh your login") .WithMetadata(new SkipIdempotencyAttribute()) @@ -25,7 +25,8 @@ public override async Task HandleAsync(CancellationToken ct) { TokensDto tokens = await sender.SendCommandAsync( command: new RefreshUserCommand( - Token: HttpContext.RefreshTokenCookie + Token: HttpContext.RefreshTokenCookie, + Fingerprint: fingerprintService.GetFingerprint(HttpContext.Request.HeadersDictionary) ), ct: ct ).ConfigureAwait(false); diff --git a/src/Modules/Identity/API/Identity/Post/Register/RegisterEndpoint.cs b/src/Modules/Identity/API/Identity/Post/Register/RegisterEndpoint.cs index f0e5019c9..ae4d3a373 100644 --- a/src/Modules/Identity/API/Identity/Post/Register/RegisterEndpoint.cs +++ b/src/Modules/Identity/API/Identity/Post/Register/RegisterEndpoint.cs @@ -13,7 +13,6 @@ public override void Configure() Group(); AllowAnonymous(); Description(x => x - .WithName(IdentityNames.Register) .WithSummary("Register") .WithDescription("Register an Account") .WithMetadata(new SkipIdempotencyAttribute()) diff --git a/src/Modules/Identity/API/Identity/Post/Register/RegisterRequestValidator.cs b/src/Modules/Identity/API/Identity/Post/Register/RegisterRequestValidator.cs deleted file mode 100644 index f83271ea4..000000000 --- a/src/Modules/Identity/API/Identity/Post/Register/RegisterRequestValidator.cs +++ /dev/null @@ -1,21 +0,0 @@ -using CustomCADs.Shared.Application; -using CustomCADs.Shared.Domain; -using FluentValidation; - -namespace CustomCADs.Modules.Identity.API.Identity.Post.Register; - -using static ApplicationConstants.FluentMessages; -using static DomainConstants; - -public class RegisterRequestValidator : Validator -{ - public RegisterRequestValidator() - { - RuleFor(x => x.Role) - .Must(x => x is Roles.Customer or Roles.Contributor); - - RuleFor(x => x.ConfirmPassword) - .NotEmpty().WithMessage(RequiredError) - .Equal(x => x.Password).WithMessage("Passwords must be equal!"); - } -} diff --git a/src/Modules/Identity/API/Identity/Post/Register/RegisterValidator.cs b/src/Modules/Identity/API/Identity/Post/Register/RegisterValidator.cs new file mode 100644 index 000000000..fb313056b --- /dev/null +++ b/src/Modules/Identity/API/Identity/Post/Register/RegisterValidator.cs @@ -0,0 +1,35 @@ +using CustomCADs.Modules.Identity.Application.Contracts; +using CustomCADs.Shared.Application; +using CustomCADs.Shared.Domain; +using FluentValidation; + +namespace CustomCADs.Modules.Identity.API.Identity.Post.Register; + +using static ApplicationConstants.FluentMessages; +using static DomainConstants.Users; + +public class RegisterRequestValidator : Validator +{ + public RegisterRequestValidator() + { + RuleFor(x => x.Role) + .Must(x => x is CustomerRole or ContributorRole).WithMessage("""must be either "Customer" or "Contributor" """); + + + RuleFor(x => x) + .MustAsync( + async (command, ct) => !await Resolve() + .GetExistsByUsernameAsync(command.Username) + .ConfigureAwait(false) + ).WithMessage("Cannot register a User with a Duplicate Username") + .MustAsync( + async (command, ct) => !await Resolve() + .GetExistsByEmailAsync(command.Email) + .ConfigureAwait(false) + ).WithMessage("Cannot register a User with a Duplicate Email"); + + RuleFor(x => x.ConfirmPassword) + .NotEmpty().WithMessage(RequiredError) + .Equal(x => x.Password).WithMessage("must be equal to Password!"); + } +} diff --git a/src/Modules/Identity/API/Identity/Post/ResetPassword/ResetPasswordEndpoint.cs b/src/Modules/Identity/API/Identity/Post/ResetPassword/ResetPasswordEndpoint.cs index 105029972..f3eb214f6 100644 --- a/src/Modules/Identity/API/Identity/Post/ResetPassword/ResetPasswordEndpoint.cs +++ b/src/Modules/Identity/API/Identity/Post/ResetPassword/ResetPasswordEndpoint.cs @@ -12,7 +12,6 @@ public override void Configure() Group(); AllowAnonymous(); Description(x => x - .WithName(IdentityNames.ResetPassword) .WithSummary("Reset Password") .WithDescription("Reset your Password with the token from the email") .WithMetadata(new SkipIdempotencyAttribute()) diff --git a/src/Modules/Identity/API/Identity/Post/RetryVerifyEmail/RetryConfirmEmailEndpoint.cs b/src/Modules/Identity/API/Identity/Post/RetryVerifyEmail/RetryConfirmEmailEndpoint.cs index 490f2bc3a..9f15451c0 100644 --- a/src/Modules/Identity/API/Identity/Post/RetryVerifyEmail/RetryConfirmEmailEndpoint.cs +++ b/src/Modules/Identity/API/Identity/Post/RetryVerifyEmail/RetryConfirmEmailEndpoint.cs @@ -12,7 +12,6 @@ public override void Configure() Group(); AllowAnonymous(); Description(x => x - .WithName(IdentityNames.RetryConfirmEmail) .WithSummary("Retry Send Email") .WithDescription("Receive another verification email") .WithMetadata(new SkipIdempotencyAttribute()) diff --git a/src/Modules/Identity/API/Identity/Post/VerifyEmail/ConfirmEmailEndpoint.cs b/src/Modules/Identity/API/Identity/Post/VerifyEmail/ConfirmEmailEndpoint.cs index 77fcb07a2..bf66cb17e 100644 --- a/src/Modules/Identity/API/Identity/Post/VerifyEmail/ConfirmEmailEndpoint.cs +++ b/src/Modules/Identity/API/Identity/Post/VerifyEmail/ConfirmEmailEndpoint.cs @@ -1,11 +1,12 @@ -using CustomCADs.Modules.Identity.Application.Users.Commands.Internal.VerifyEmail; +using CustomCADs.Modules.Identity.Application.Contracts; +using CustomCADs.Modules.Identity.Application.Users.Commands.Internal.VerifyEmail; using CustomCADs.Modules.Identity.Application.Users.Dtos; using CustomCADs.Shared.API.Attributes; using Microsoft.Extensions.Options; namespace CustomCADs.Modules.Identity.API.Identity.Post.VerifyEmail; -public sealed class ConfirmEmailEndpoint(IRequestSender sender, IOptions settings) +public sealed class ConfirmEmailEndpoint(IRequestSender sender, IFingerprintService fingerprintService, IOptions settings) : Endpoint { public override void Configure() @@ -14,7 +15,6 @@ public override void Configure() Group(); AllowAnonymous(); Description(x => x - .WithName(IdentityNames.ConfirmEmail) .WithSummary("Confirm Email") .WithDescription("Confirm the verification email") .WithMetadata(new SkipIdempotencyAttribute()) @@ -26,7 +26,8 @@ public override async Task HandleAsync(ConfirmEmailRequest req, CancellationToke TokensDto tokens = await sender.SendCommandAsync( command: new VerifyUserEmailCommand( Username: req.Username, - Token: req.Token.Replace(' ', '+') + Token: req.Token.Replace(' ', '+'), + Fingerprint: fingerprintService.GetFingerprint(HttpContext.Request.HeadersDictionary) ), ct: ct ).ConfigureAwait(false); diff --git a/src/Modules/Identity/API/SSODependencyInjection.cs b/src/Modules/Identity/API/SSODependencyInjection.cs index b2cfdb80a..fd1779582 100644 --- a/src/Modules/Identity/API/SSODependencyInjection.cs +++ b/src/Modules/Identity/API/SSODependencyInjection.cs @@ -1,4 +1,5 @@ using CustomCADs.Modules.Identity.API; +using CustomCADs.Modules.Identity.Application.Contracts; using CustomCADs.Modules.Identity.Application.Users.Commands.Internal.SSO.Register; using CustomCADs.Modules.Identity.Application.Users.Dtos; using Microsoft.AspNetCore.Authentication; @@ -66,12 +67,18 @@ internal void Configure(SSOClientSettings sso, string domain, string provider, s IServiceProvider sp = ctx.Request.HttpContext.RequestServices; if (ctx.Principal is null) throw new Exception("Claims required"); - ctx.Principal.ExtractUserFromSSO(out string email, out string username); + ctx.Principal.ExtractUserFromSSO( + out string email, + out string username, + out string? firstName, + out string? lastName + ); string? role = null; ctx.Properties?.Items.TryGetValue("role", out role); IRequestSender sender = sp.GetRequiredService(); + IFingerprintService fingerprint = sp.GetRequiredService(); CookieSettings cookie = sp.GetRequiredService>().Value; ctx.HttpContext.SaveAllCookies( @@ -80,9 +87,12 @@ internal void Configure(SSOClientSettings sso, string domain, string provider, s tokens: await sender.SendCommandAsync( command: new SingleSignOnUserCommand( Role: role, + FirstName: firstName, + LastName: lastName, Username: username, Email: email, - Provider: provider + Provider: provider, + Fingerprint: fingerprint.GetFingerprint(ctx.Request.HeadersDictionary) ), ct: ct ).ConfigureAwait(false) diff --git a/src/Modules/Identity/Application/Contracts/IFingerprintService.cs b/src/Modules/Identity/Application/Contracts/IFingerprintService.cs new file mode 100644 index 000000000..bf660d8eb --- /dev/null +++ b/src/Modules/Identity/Application/Contracts/IFingerprintService.cs @@ -0,0 +1,8 @@ +namespace CustomCADs.Modules.Identity.Application.Contracts; + +using Domain.Users.ValueObjects; + +public interface IFingerprintService +{ + Fingerprint GetFingerprint(Dictionary headers); +} diff --git a/src/Modules/Identity/Application/Contracts/IUserService.cs b/src/Modules/Identity/Application/Contracts/IUserService.cs index b9a7d7870..b8b1944c3 100644 --- a/src/Modules/Identity/Application/Contracts/IUserService.cs +++ b/src/Modules/Identity/Application/Contracts/IUserService.cs @@ -6,6 +6,7 @@ namespace CustomCADs.Modules.Identity.Application.Contracts; public interface IUserService { #region GetUserByX + Task GetByAccountIdAsync(AccountId accountId); Task GetByUsernameAsync(string username); Task GetByEmailAsync(string email); Task<(User User, RefreshToken RefreshToken)> GetByRefreshTokenAsync(string token); @@ -14,20 +15,21 @@ public interface IUserService #region GetX Task GetExistsByUsernameAsync(string username); Task GetExistsByEmailAsync(string email); - Task GetAccountIdAsync(string username); + Task GetIsSSOByEmailAsync(string email); Task GetIsLockedOutAsync(string username); #endregion #region Lifecycle Task CreateAsync(User user, string password); Task CreateSSOAsync(User user, string provider); - Task DeleteAsync(string username); + Task DeleteAsync(AccountId id); #endregion #region Mutation Task CheckPasswordAsync(string username, string password); Task UpdateUsernameAsync(UserId id, string username); Task SaveRefreshTokensAsync(User user); + Task RevokeRefreshTokenAsync(RefreshTokenId refreshTokenId); Task RevokeRefreshTokenAsync(string token); #endregion diff --git a/src/Modules/Identity/Application/Extensions/CustomAuthorizationExceptionExtensions.cs b/src/Modules/Identity/Application/Extensions/CustomAuthorizationExceptionExtensions.cs new file mode 100644 index 000000000..07e93f6ac --- /dev/null +++ b/src/Modules/Identity/Application/Extensions/CustomAuthorizationExceptionExtensions.cs @@ -0,0 +1,15 @@ +using CustomCADs.Shared.Application.Exceptions; + +namespace CustomCADs.Modules.Identity.Application.Extensions; + +public static class CustomAuthorizationExceptionExtensions +{ + extension(CustomAuthorizationException ex) + { + public static CustomAuthorizationException NoRefreshToken() + => CustomAuthorizationException.Custom("No Refresh Token found."); + + public static CustomAuthorizationException RefreshTokenExpired() + => CustomAuthorizationException.Custom("Refresh Token found, but expired."); + } +} diff --git a/src/Modules/Identity/Application/Users/Commands/Internal/ChangeUsername/ChangeUsernameCommand.cs b/src/Modules/Identity/Application/Users/Commands/Internal/ChangeUsername/ChangeUsernameCommand.cs index 28d272ef6..fbe56d7e5 100644 --- a/src/Modules/Identity/Application/Users/Commands/Internal/ChangeUsername/ChangeUsernameCommand.cs +++ b/src/Modules/Identity/Application/Users/Commands/Internal/ChangeUsername/ChangeUsernameCommand.cs @@ -1,6 +1,10 @@ -namespace CustomCADs.Modules.Identity.Application.Users.Commands.Internal.ChangeUsername; +using CustomCADs.Shared.Domain.TypedIds.Accounts; + +namespace CustomCADs.Modules.Identity.Application.Users.Commands.Internal.ChangeUsername; public sealed record ChangeUsernameCommand( + AccountId Id, string Username, - string NewUsername + string? FirstName, + string? LastName ) : ICommand; diff --git a/src/Modules/Identity/Application/Users/Commands/Internal/ChangeUsername/ChangeUsernameHandler.cs b/src/Modules/Identity/Application/Users/Commands/Internal/ChangeUsername/ChangeUsernameHandler.cs index 3690a97fa..4962f973b 100644 --- a/src/Modules/Identity/Application/Users/Commands/Internal/ChangeUsername/ChangeUsernameHandler.cs +++ b/src/Modules/Identity/Application/Users/Commands/Internal/ChangeUsername/ChangeUsernameHandler.cs @@ -10,15 +10,16 @@ IEventRaiser raiser { public async Task Handle(ChangeUsernameCommand req, CancellationToken ct) { - User user = await service.GetByUsernameAsync(req.Username).ConfigureAwait(false); + User user = await service.GetByAccountIdAsync(req.Id).ConfigureAwait(false); - user.SetUsername(req.NewUsername); + user.SetUsername(req.Username); await service.UpdateUsernameAsync(user.Id, user.Username).ConfigureAwait(false); await raiser.RaiseApplicationEventAsync( @event: new UserEditedApplicationEvent( Id: user.AccountId, - Username: user.Username + Username: user.Username, + Names: new(req.FirstName, req.LastName) ) ).ConfigureAwait(false); } diff --git a/src/Modules/Identity/Application/Users/Commands/Internal/ChangeUsername/ChangeUsernameValidator.cs b/src/Modules/Identity/Application/Users/Commands/Internal/ChangeUsername/ChangeUsernameValidator.cs new file mode 100644 index 000000000..17597c5b9 --- /dev/null +++ b/src/Modules/Identity/Application/Users/Commands/Internal/ChangeUsername/ChangeUsernameValidator.cs @@ -0,0 +1,43 @@ +using CustomCADs.Shared.Application.Abstractions.Requests.Sender; +using CustomCADs.Shared.Application.Abstractions.Requests.Validator; +using CustomCADs.Shared.Application.Exceptions; +using CustomCADs.Shared.Application.UseCases.Accounts.Queries; +using FluentValidation; + +namespace CustomCADs.Modules.Identity.Application.Users.Commands.Internal.ChangeUsername; + +public sealed class ChangeUsernameValidator : CommandValidator +{ + public ChangeUsernameValidator(IRequestSender sender) + { + RuleFor(x => x) + .MustAsync(async (command, ct) => + { + bool usernameIsDuplicate = await sender.SendQueryAsync( + query: new GetAccountExistsByUsernameQuery(command.Username), + ct: ct + ).ConfigureAwait(false); + + return !usernameIsDuplicate; + }) + .WhenAsync(async (command, ct) => + { + try + { + var info = await sender.SendQueryAsync( + query: new GetAccountInfoByUsernameQuery(command.Username), + ct: ct + ).ConfigureAwait(false); + + // Don't run if the account with this username is the user's own + return info.Id != command.Id; + } + catch (Exception ex) when (ex.GetType().IsGenericType && ex.GetType().GetGenericTypeDefinition() == typeof(CustomNotFoundException<>)) + { + // Don't run if the account with this username doesn't exist + return false; + } + }) + .WithMessage("Username already taken"); + } +} diff --git a/src/Modules/Identity/Application/Users/Commands/Internal/Delete/DeleteUserCommand.cs b/src/Modules/Identity/Application/Users/Commands/Internal/Delete/DeleteUserCommand.cs index 59f0dc1f5..3fcb63cb3 100644 --- a/src/Modules/Identity/Application/Users/Commands/Internal/Delete/DeleteUserCommand.cs +++ b/src/Modules/Identity/Application/Users/Commands/Internal/Delete/DeleteUserCommand.cs @@ -1,5 +1,5 @@ +using CustomCADs.Shared.Domain.TypedIds.Accounts; + namespace CustomCADs.Modules.Identity.Application.Users.Commands.Internal.Delete; -public sealed record DeleteUserCommand( - string Username -) : ICommand; +public sealed record DeleteUserCommand(AccountId CallerId) : ICommand; diff --git a/src/Modules/Identity/Application/Users/Commands/Internal/Delete/DeleteUserHandler.cs b/src/Modules/Identity/Application/Users/Commands/Internal/Delete/DeleteUserHandler.cs index ce3546f6c..c6862c2cc 100644 --- a/src/Modules/Identity/Application/Users/Commands/Internal/Delete/DeleteUserHandler.cs +++ b/src/Modules/Identity/Application/Users/Commands/Internal/Delete/DeleteUserHandler.cs @@ -1,6 +1,5 @@ using CustomCADs.Shared.Application.Abstractions.Events; using CustomCADs.Shared.Application.Events.Identity; -using CustomCADs.Shared.Domain.TypedIds.Accounts; namespace CustomCADs.Modules.Identity.Application.Users.Commands.Internal.Delete; @@ -11,11 +10,10 @@ IEventRaiser raiser { public async Task Handle(DeleteUserCommand req, CancellationToken ct = default) { - AccountId accountId = await service.GetAccountIdAsync(req.Username).ConfigureAwait(false); - await service.DeleteAsync(req.Username).ConfigureAwait(false); + await service.DeleteAsync(req.CallerId).ConfigureAwait(false); await raiser.RaiseApplicationEventAsync( - @event: new UserDeletedApplicationEvent(accountId) + @event: new UserDeletedApplicationEvent(req.CallerId) ).ConfigureAwait(false); } } diff --git a/src/Modules/Identity/Application/Users/Commands/Internal/Delete/Fingerprints/DeleteFingerprintCommand.cs b/src/Modules/Identity/Application/Users/Commands/Internal/Delete/Fingerprints/DeleteFingerprintCommand.cs new file mode 100644 index 000000000..9fbe80cc8 --- /dev/null +++ b/src/Modules/Identity/Application/Users/Commands/Internal/Delete/Fingerprints/DeleteFingerprintCommand.cs @@ -0,0 +1,9 @@ +using CustomCADs.Shared.Domain.TypedIds.Accounts; + +namespace CustomCADs.Modules.Identity.Application.Users.Commands.Internal.Delete.Fingerprints; + +public record DeleteFingerprintCommand( + RefreshTokenId RefreshTokenId, + string? CurrentRefreshToken, + AccountId CallerId +) : ICommand; diff --git a/src/Modules/Identity/Application/Users/Commands/Internal/Delete/Fingerprints/DeleteFingerprintHandler.cs b/src/Modules/Identity/Application/Users/Commands/Internal/Delete/Fingerprints/DeleteFingerprintHandler.cs new file mode 100644 index 000000000..ded224345 --- /dev/null +++ b/src/Modules/Identity/Application/Users/Commands/Internal/Delete/Fingerprints/DeleteFingerprintHandler.cs @@ -0,0 +1,28 @@ +using CustomCADs.Modules.Identity.Application.Extensions; +using CustomCADs.Modules.Identity.Domain.Users.Entities; +using CustomCADs.Shared.Application.Exceptions; + +namespace CustomCADs.Modules.Identity.Application.Users.Commands.Internal.Delete.Fingerprints; + +public class DeleteFingerprintHandler(IUserService service) : ICommandHandler +{ + public async Task Handle(DeleteFingerprintCommand req, CancellationToken ct = default) + { + if (string.IsNullOrEmpty(req.CurrentRefreshToken)) + { + throw CustomAuthorizationException.NoRefreshToken(); + } + + (User user, RefreshToken refreshToken) = await service.GetByRefreshTokenAsync(req.CurrentRefreshToken).ConfigureAwait(false); + if (req.CallerId != user.AccountId) + { + throw CustomAuthorizationException.ById(user.AccountId); + } + + if (refreshToken.Id == req.RefreshTokenId) + { + throw new CustomException("Cannot delete User's current RefreshToken"); + } + await service.RevokeRefreshTokenAsync(req.RefreshTokenId).ConfigureAwait(false); + } +} diff --git a/src/Modules/Identity/Application/Users/Commands/Internal/Login/LoginUserCommand.cs b/src/Modules/Identity/Application/Users/Commands/Internal/Login/LoginUserCommand.cs index 7fa489722..f4e32773b 100644 --- a/src/Modules/Identity/Application/Users/Commands/Internal/Login/LoginUserCommand.cs +++ b/src/Modules/Identity/Application/Users/Commands/Internal/Login/LoginUserCommand.cs @@ -1,9 +1,11 @@ using CustomCADs.Modules.Identity.Application.Users.Dtos; +using CustomCADs.Modules.Identity.Domain.Users.ValueObjects; namespace CustomCADs.Modules.Identity.Application.Users.Commands.Internal.Login; public sealed record LoginUserCommand( string Username, string Password, - bool LongerExpireTime + bool LongerExpireTime, + Fingerprint Fingerprint ) : ICommand; diff --git a/src/Modules/Identity/Application/Users/Commands/Internal/Login/LoginUserHandler.cs b/src/Modules/Identity/Application/Users/Commands/Internal/Login/LoginUserHandler.cs index 92ede72b5..93b3b8267 100644 --- a/src/Modules/Identity/Application/Users/Commands/Internal/Login/LoginUserHandler.cs +++ b/src/Modules/Identity/Application/Users/Commands/Internal/Login/LoginUserHandler.cs @@ -38,7 +38,7 @@ public async Task Handle(LoginUserCommand req, CancellationToken ct) } RefreshToken rt = tokenService.IssueRefreshToken( - createRefreshToken: (token) => user.AddRefreshToken(token, longerSession: false) + createRefreshToken: (token) => user.AddRefreshToken(token, req.Fingerprint, longerSession: false) ); await service.SaveRefreshTokensAsync(user).ConfigureAwait(false); diff --git a/src/Modules/Identity/Application/Users/Commands/Internal/Refresh/RefreshUserCommand.cs b/src/Modules/Identity/Application/Users/Commands/Internal/Refresh/RefreshUserCommand.cs index d48057538..b57d1bb9e 100644 --- a/src/Modules/Identity/Application/Users/Commands/Internal/Refresh/RefreshUserCommand.cs +++ b/src/Modules/Identity/Application/Users/Commands/Internal/Refresh/RefreshUserCommand.cs @@ -1,6 +1,8 @@ using CustomCADs.Modules.Identity.Application.Users.Dtos; +using CustomCADs.Modules.Identity.Domain.Users.ValueObjects; namespace CustomCADs.Modules.Identity.Application.Users.Commands.Internal.Refresh; public sealed record RefreshUserCommand( - string? Token + string? Token, + Fingerprint Fingerprint ) : ICommand; diff --git a/src/Modules/Identity/Application/Users/Commands/Internal/Refresh/RefreshUserHandler.cs b/src/Modules/Identity/Application/Users/Commands/Internal/Refresh/RefreshUserHandler.cs index 0e5bcc32a..091052b5c 100644 --- a/src/Modules/Identity/Application/Users/Commands/Internal/Refresh/RefreshUserHandler.cs +++ b/src/Modules/Identity/Application/Users/Commands/Internal/Refresh/RefreshUserHandler.cs @@ -1,4 +1,5 @@ -using CustomCADs.Modules.Identity.Application.Users.Dtos; +using CustomCADs.Modules.Identity.Application.Extensions; +using CustomCADs.Modules.Identity.Application.Users.Dtos; using CustomCADs.Modules.Identity.Domain.Users.Entities; using CustomCADs.Shared.Application.Exceptions; @@ -13,20 +14,15 @@ public async Task Handle(RefreshUserCommand req, CancellationToken ct { if (string.IsNullOrEmpty(req.Token)) { - throw CustomAuthorizationException.Custom("No Refresh Token found."); + throw CustomAuthorizationException.NoRefreshToken(); } (User User, RefreshToken RefreshToken) = await service.GetByRefreshTokenAsync(req.Token).ConfigureAwait(false); if (RefreshToken.ExpiresAt < DateTime.UtcNow) { - throw CustomAuthorizationException.Custom("Refresh Token found, but expired."); + throw CustomAuthorizationException.RefreshTokenExpired(); } - RefreshToken rt = tokenService.IssueRefreshToken( - createRefreshToken: (token) => User.AddRefreshToken(token, longerSession: false) - ); - await service.SaveRefreshTokensAsync(User).ConfigureAwait(false); - - return tokenService.IssueTokens(User, rt); + return tokenService.IssueTokens(User, RefreshToken); } } diff --git a/src/Modules/Identity/Application/Users/Commands/Internal/ResetPasswordEmail/ResetPasswordEmailValidator.cs b/src/Modules/Identity/Application/Users/Commands/Internal/ResetPasswordEmail/ResetPasswordEmailValidator.cs new file mode 100644 index 000000000..1414ad7ea --- /dev/null +++ b/src/Modules/Identity/Application/Users/Commands/Internal/ResetPasswordEmail/ResetPasswordEmailValidator.cs @@ -0,0 +1,16 @@ +using CustomCADs.Shared.Application.Abstractions.Requests.Validator; +using FluentValidation; + +namespace CustomCADs.Modules.Identity.Application.Users.Commands.Internal.ResetPasswordEmail; + +public sealed class ResetPasswordEmailValidator : CommandValidator +{ + public ResetPasswordEmailValidator(IUserService service) + { + RuleFor(x => x) + .MustAsync( + async (command, ct) => !await service.GetIsSSOByEmailAsync(command.Email).ConfigureAwait(false) + ) + .WithMessage("Accounts with SSO have no Password to Reset."); + } +} diff --git a/src/Modules/Identity/Application/Users/Commands/Internal/SSO/Register/SingleSignOnUserCommand.cs b/src/Modules/Identity/Application/Users/Commands/Internal/SSO/Register/SingleSignOnUserCommand.cs index 37de01244..84946191f 100644 --- a/src/Modules/Identity/Application/Users/Commands/Internal/SSO/Register/SingleSignOnUserCommand.cs +++ b/src/Modules/Identity/Application/Users/Commands/Internal/SSO/Register/SingleSignOnUserCommand.cs @@ -1,10 +1,14 @@ using CustomCADs.Modules.Identity.Application.Users.Dtos; +using CustomCADs.Modules.Identity.Domain.Users.ValueObjects; namespace CustomCADs.Modules.Identity.Application.Users.Commands.Internal.SSO.Register; public sealed record SingleSignOnUserCommand( string? Role, + string? FirstName, + string? LastName, string Username, string Email, - string Provider + string Provider, + Fingerprint Fingerprint ) : ICommand; diff --git a/src/Modules/Identity/Application/Users/Commands/Internal/SSO/Register/SingleSignOnUserHandler.cs b/src/Modules/Identity/Application/Users/Commands/Internal/SSO/Register/SingleSignOnUserHandler.cs index 002f17a8e..8d395f757 100644 --- a/src/Modules/Identity/Application/Users/Commands/Internal/SSO/Register/SingleSignOnUserHandler.cs +++ b/src/Modules/Identity/Application/Users/Commands/Internal/SSO/Register/SingleSignOnUserHandler.cs @@ -18,7 +18,7 @@ public async Task Handle(SingleSignOnUserCommand req, CancellationTok User user = await GetUserAsync(req, ct).ConfigureAwait(false); RefreshToken rt = tokenService.IssueRefreshToken( - createRefreshToken: (token) => user.AddRefreshToken(token, longerSession: false) + createRefreshToken: (token) => user.AddRefreshToken(token, req.Fingerprint, longerSession: false) ); await service.SaveRefreshTokensAsync(user).ConfigureAwait(false); @@ -38,16 +38,16 @@ private async Task GetUserAsync(SingleSignOnUserCommand req, CancellationT (true, true) => await service.GetByUsernameAsync(req.Username).ConfigureAwait(false), }; - async Task CreateUser(SingleSignOnUserCommand req, CancellationToken ct) + async Task CreateUser(SingleSignOnUserCommand req, CancellationToken ct, string defaultRole = DomainConstants.Users.CustomerRole) { - string role = req.Role ?? DomainConstants.Roles.Customer; + string role = string.IsNullOrWhiteSpace(req.Role) ? defaultRole : req.Role; AccountId accountId = await sender.SendCommandAsync( command: new CreateAccountCommand( Role: role, Username: req.Username, Email: req.Email, - FirstName: null, - LastName: null + FirstName: req.FirstName, + LastName: req.LastName ), ct: ct ).ConfigureAwait(false); diff --git a/src/Modules/Identity/Application/Users/Commands/Internal/ToggleViewedProductsTracking/ToggleViewedProductsTrackingCommand.cs b/src/Modules/Identity/Application/Users/Commands/Internal/ToggleViewedProductsTracking/ToggleViewedProductsTrackingCommand.cs index 4b0f3e242..24cc7aea0 100644 --- a/src/Modules/Identity/Application/Users/Commands/Internal/ToggleViewedProductsTracking/ToggleViewedProductsTrackingCommand.cs +++ b/src/Modules/Identity/Application/Users/Commands/Internal/ToggleViewedProductsTracking/ToggleViewedProductsTrackingCommand.cs @@ -1,5 +1,8 @@ +using CustomCADs.Shared.Domain.TypedIds.Accounts; + namespace CustomCADs.Modules.Identity.Application.Users.Commands.Internal.ToggleViewedProductsTracking; public sealed record ToggleViewedProductsTrackingCommand( - string Username + string Username, + AccountId CallerId ) : ICommand; diff --git a/src/Modules/Identity/Application/Users/Commands/Internal/ToggleViewedProductsTracking/ToggleViewedProductsTrackingHandler.cs b/src/Modules/Identity/Application/Users/Commands/Internal/ToggleViewedProductsTracking/ToggleViewedProductsTrackingHandler.cs index 7cdaead2e..c8a2035a1 100644 --- a/src/Modules/Identity/Application/Users/Commands/Internal/ToggleViewedProductsTracking/ToggleViewedProductsTrackingHandler.cs +++ b/src/Modules/Identity/Application/Users/Commands/Internal/ToggleViewedProductsTracking/ToggleViewedProductsTrackingHandler.cs @@ -2,19 +2,16 @@ using CustomCADs.Shared.Application.Abstractions.Requests.Sender; using CustomCADs.Shared.Application.Events.Identity; using CustomCADs.Shared.Application.UseCases.Accounts.Queries; -using CustomCADs.Shared.Domain.TypedIds.Accounts; namespace CustomCADs.Modules.Identity.Application.Users.Commands.Internal.ToggleViewedProductsTracking; public sealed class ToggleViewedProductsTrackingHandler( - IUserService service, IRequestSender sender, IEventRaiser raiser ) : ICommandHandler { public async Task Handle(ToggleViewedProductsTrackingCommand req, CancellationToken ct = default) { - AccountId accountId = await service.GetAccountIdAsync(req.Username).ConfigureAwait(false); AccountInfoDto info = await sender.SendQueryAsync( query: new GetAccountInfoByUsernameQuery(req.Username), ct: ct @@ -22,7 +19,7 @@ public async Task Handle(ToggleViewedProductsTrackingCommand req, CancellationTo await raiser.RaiseApplicationEventAsync( @event: new UserEditedApplicationEvent( - Id: accountId, + Id: req.CallerId, TrackViewedProducts: !info.TrackViewedProducts ) ).ConfigureAwait(false); diff --git a/src/Modules/Identity/Application/Users/Commands/Internal/VerifyEmail/VerifyUserEmailCommand.cs b/src/Modules/Identity/Application/Users/Commands/Internal/VerifyEmail/VerifyUserEmailCommand.cs index 2718a7700..d5e3a5f7e 100644 --- a/src/Modules/Identity/Application/Users/Commands/Internal/VerifyEmail/VerifyUserEmailCommand.cs +++ b/src/Modules/Identity/Application/Users/Commands/Internal/VerifyEmail/VerifyUserEmailCommand.cs @@ -1,8 +1,10 @@ using CustomCADs.Modules.Identity.Application.Users.Dtos; +using CustomCADs.Modules.Identity.Domain.Users.ValueObjects; namespace CustomCADs.Modules.Identity.Application.Users.Commands.Internal.VerifyEmail; public sealed record VerifyUserEmailCommand( string Username, - string Token + string Token, + Fingerprint Fingerprint ) : ICommand; diff --git a/src/Modules/Identity/Application/Users/Commands/Internal/VerifyEmail/VerifyUserEmailHandler.cs b/src/Modules/Identity/Application/Users/Commands/Internal/VerifyEmail/VerifyUserEmailHandler.cs index e2d718ff4..49888f9b0 100644 --- a/src/Modules/Identity/Application/Users/Commands/Internal/VerifyEmail/VerifyUserEmailHandler.cs +++ b/src/Modules/Identity/Application/Users/Commands/Internal/VerifyEmail/VerifyUserEmailHandler.cs @@ -20,7 +20,7 @@ public async Task Handle(VerifyUserEmailCommand req, CancellationToke await service.ConfirmEmailAsync(req.Username, req.Token).ConfigureAwait(false); RefreshToken rt = tokenService.IssueRefreshToken( - createRefreshToken: (token) => user.AddRefreshToken(token, longerSession: false) + createRefreshToken: (token) => user.AddRefreshToken(token, req.Fingerprint, longerSession: false) ); await service.SaveRefreshTokensAsync(user).ConfigureAwait(false); diff --git a/src/Modules/Identity/Application/Users/Dtos/FingerprintDto.cs b/src/Modules/Identity/Application/Users/Dtos/FingerprintDto.cs new file mode 100644 index 000000000..ddf3f0dfb --- /dev/null +++ b/src/Modules/Identity/Application/Users/Dtos/FingerprintDto.cs @@ -0,0 +1,9 @@ +namespace CustomCADs.Modules.Identity.Application.Users.Dtos; + +public record FingerprintDto( + RefreshTokenId Id, + string Device, + string? Location, + bool DeleteAllowed, + DateTimeOffset IssuedAt +); diff --git a/src/Modules/Identity/Application/Users/Events/Application/Users/UserDeletedHandler.cs b/src/Modules/Identity/Application/Users/Events/Application/Users/UserDeletedHandler.cs index 71cbbc203..3b8c832ba 100644 --- a/src/Modules/Identity/Application/Users/Events/Application/Users/UserDeletedHandler.cs +++ b/src/Modules/Identity/Application/Users/Events/Application/Users/UserDeletedHandler.cs @@ -6,6 +6,6 @@ public class UserDeletedHandler(IUserService service) { public async Task HandleAsync(AccountDeletedApplicationEvent ae) { - await service.DeleteAsync(ae.Username).ConfigureAwait(false); + await service.DeleteAsync(ae.Id).ConfigureAwait(false); } } diff --git a/src/Modules/Identity/Application/Users/Queries/Internal/GetByUsername/GetUserByUsernameDto.cs b/src/Modules/Identity/Application/Users/Queries/Internal/GetByUsername/GetUserByUsernameDto.cs index f2bd49cd0..10a1a2cce 100644 --- a/src/Modules/Identity/Application/Users/Queries/Internal/GetByUsername/GetUserByUsernameDto.cs +++ b/src/Modules/Identity/Application/Users/Queries/Internal/GetByUsername/GetUserByUsernameDto.cs @@ -1,5 +1,6 @@ +using CustomCADs.Modules.Identity.Application.Users.Dtos; using CustomCADs.Modules.Identity.Domain.Users.ValueObjects; -using CustomCADs.Shared.Domain.TypedIds.Catalog; +using CustomCADs.Shared.Application.UseCases.Accounts.Queries; namespace CustomCADs.Modules.Identity.Application.Users.Queries.Internal.GetByUsername; @@ -12,5 +13,6 @@ public sealed record GetUserByUsernameDto( bool TrackViewedProducts, Email Email, DateTimeOffset CreatedAt, - ProductId[] ViewedProductIds + ViewedProductDto[] ViewedProducts, + FingerprintDto[] Fingerprints ); diff --git a/src/Modules/Identity/Application/Users/Queries/Internal/GetByUsername/GetUserByUsernameHandler.cs b/src/Modules/Identity/Application/Users/Queries/Internal/GetByUsername/GetUserByUsernameHandler.cs index 121314789..0b5b72a83 100644 --- a/src/Modules/Identity/Application/Users/Queries/Internal/GetByUsername/GetUserByUsernameHandler.cs +++ b/src/Modules/Identity/Application/Users/Queries/Internal/GetByUsername/GetUserByUsernameHandler.cs @@ -1,7 +1,8 @@ +using CustomCADs.Modules.Identity.Application.Users.Dtos; +using CustomCADs.Modules.Identity.Domain.Users.Entities; using CustomCADs.Shared.Application.Abstractions.Requests.Queries; using CustomCADs.Shared.Application.Abstractions.Requests.Sender; using CustomCADs.Shared.Application.UseCases.Accounts.Queries; -using CustomCADs.Shared.Domain.TypedIds.Catalog; namespace CustomCADs.Modules.Identity.Application.Users.Queries.Internal.GetByUsername; @@ -10,15 +11,15 @@ public sealed class GetUserByUsernameHandler(IUserService service, IRequestSende { public async Task Handle(GetUserByUsernameQuery req, CancellationToken ct = default) { - User user = await service.GetByUsernameAsync(req.Username).ConfigureAwait(false); + User user = await service.GetByAccountIdAsync(req.Id).ConfigureAwait(false); AccountInfoDto info = await sender.SendQueryAsync( - query: new GetAccountInfoByUsernameQuery(req.Username), + query: new GetAccountInfoByUsernameQuery(user.Username), ct: ct ).ConfigureAwait(false); - ProductId[] viewedProductIds = await sender.SendQueryAsync( - query: new GetAccountViewedProductsByUsernameQuery(req.Username), + ViewedProductDto[] viewedProducts = await sender.SendQueryAsync( + query: new GetAccountViewedProductsByUsernameQuery(user.Username), ct: ct ).ConfigureAwait(false); @@ -31,7 +32,20 @@ public async Task Handle(GetUserByUsernameQuery req, Cance CreatedAt: info.CreatedAt, FirstName: info.FirstName, LastName: info.LastName, - ViewedProductIds: viewedProductIds + ViewedProducts: viewedProducts, + Fingerprints: [.. user.RefreshTokens + .Select(x => ToFingerprintDto(x, x.Value == req.RefreshToken)) + .OrderByDescending(x => x.IssuedAt) + ] ); } + + private static FingerprintDto ToFingerprintDto(RefreshToken refreshToken, bool isCurrent) + => new( + Id: refreshToken.Id, + Device: refreshToken.Fingerprint.Device, + Location: refreshToken.Fingerprint.Location, + IssuedAt: refreshToken.IssuedAt, + DeleteAllowed: !isCurrent + ); } diff --git a/src/Modules/Identity/Application/Users/Queries/Internal/GetByUsername/GetUserByUsernameQuery.cs b/src/Modules/Identity/Application/Users/Queries/Internal/GetByUsername/GetUserByUsernameQuery.cs index 2ff5a2d7a..21456faec 100644 --- a/src/Modules/Identity/Application/Users/Queries/Internal/GetByUsername/GetUserByUsernameQuery.cs +++ b/src/Modules/Identity/Application/Users/Queries/Internal/GetByUsername/GetUserByUsernameQuery.cs @@ -1,7 +1,9 @@ using CustomCADs.Shared.Application.Abstractions.Requests.Queries; +using CustomCADs.Shared.Domain.TypedIds.Accounts; namespace CustomCADs.Modules.Identity.Application.Users.Queries.Internal.GetByUsername; public sealed record GetUserByUsernameQuery( - string Username + AccountId Id, + string? RefreshToken ) : IQuery; diff --git a/src/Modules/Identity/Domain/Constants.cs b/src/Modules/Identity/Domain/Constants.cs new file mode 100644 index 000000000..fe3a8d0e5 --- /dev/null +++ b/src/Modules/Identity/Domain/Constants.cs @@ -0,0 +1,15 @@ +using CustomCADs.Shared.Domain.TypedIds.Catalog; +using CustomCADs.Shared.Domain.TypedIds.Files; +using System.Text.RegularExpressions; + +namespace CustomCADs.Modules.Identity.Domain; + +public static partial class Constants +{ + public static class Tokens + { + public const int JwtDurationInMinutes = 15; + public const int RtDurationInDays = 7; + public const int LongerRtDurationInDays = 15; + } +} diff --git a/src/Modules/Identity/Domain/Users/Entities/RefreshToken.cs b/src/Modules/Identity/Domain/Users/Entities/RefreshToken.cs index 06b56b5b2..2f83d53bb 100644 --- a/src/Modules/Identity/Domain/Users/Entities/RefreshToken.cs +++ b/src/Modules/Identity/Domain/Users/Entities/RefreshToken.cs @@ -1,25 +1,27 @@ -using CustomCADs.Shared.Domain; using CustomCADs.Shared.Domain.Bases.Entities; namespace CustomCADs.Modules.Identity.Domain.Users.Entities; -using static DomainConstants.Tokens; +using ValueObjects; +using static Constants.Tokens; public class RefreshToken : BaseEntity { private RefreshToken() { } - private RefreshToken(string value, UserId userId, bool longerSession) + private RefreshToken(string value, Fingerprint fingerprint, UserId userId, bool longerSession) { Value = value; + Fingerprint = fingerprint; UserId = userId; IssuedAt = DateTimeOffset.UtcNow; ExpiresAt = IssuedAt.AddDays( longerSession ? LongerRtDurationInDays : RtDurationInDays ); } - private RefreshToken(string value, UserId userId, DateTimeOffset issuedAt, DateTimeOffset expiresAt) + private RefreshToken(string value, Fingerprint fingerprint, UserId userId, DateTimeOffset issuedAt, DateTimeOffset expiresAt) { Value = value; + Fingerprint = fingerprint; UserId = userId; IssuedAt = issuedAt; ExpiresAt = expiresAt; @@ -29,19 +31,20 @@ private RefreshToken(string value, UserId userId, DateTimeOffset issuedAt, DateT public DateTimeOffset IssuedAt { get; init; } public DateTimeOffset ExpiresAt { get; init; } public string Value { get; private set; } = string.Empty; + public Fingerprint Fingerprint { get; private set; } = new(); public UserId UserId { get; private set; } - public static RefreshToken Create(string value, UserId userId, bool longerSession) - => new(value, userId, longerSession); + public static RefreshToken Create(string value, Fingerprint fingerprint, UserId userId, bool longerSession) + => new(value, fingerprint, userId, longerSession); - public static RefreshToken Create(RefreshTokenId id, string value, UserId userId, bool longerSession) - => new(value, userId, longerSession) + public static RefreshToken Create(RefreshTokenId id, string value, Fingerprint fingerprint, UserId userId, bool longerSession) + => new(value, fingerprint, userId, longerSession) { Id = id, }; - public static RefreshToken Create(RefreshTokenId id, string value, UserId userId, DateTimeOffset issuedAt, DateTimeOffset expiresAt) - => new(value, userId, issuedAt, expiresAt) + public static RefreshToken Create(RefreshTokenId id, string value, Fingerprint fingerprint, UserId userId, DateTimeOffset issuedAt, DateTimeOffset expiresAt) + => new(value, fingerprint, userId, issuedAt, expiresAt) { Id = id, }; diff --git a/src/Modules/Identity/Domain/Users/User.cs b/src/Modules/Identity/Domain/Users/User.cs index 5ecb160df..0782242fd 100644 --- a/src/Modules/Identity/Domain/Users/User.cs +++ b/src/Modules/Identity/Domain/Users/User.cs @@ -53,9 +53,9 @@ public void SetUsername(string username) this.ValidateUsername(); } - public RefreshToken AddRefreshToken(string token, bool longerSession) + public RefreshToken AddRefreshToken(string token, Fingerprint fingerprint, bool longerSession) { - RefreshToken rt = RefreshToken.Create(token, this.Id, longerSession); + RefreshToken rt = RefreshToken.Create(token, fingerprint, this.Id, longerSession); refreshTokens.Add(rt); return rt; @@ -63,6 +63,6 @@ public RefreshToken AddRefreshToken(string token, bool longerSession) public bool RemoveRefreshToken(RefreshToken rt) { - return refreshTokens.Remove(rt); + return refreshTokens.RemoveAll(x => x.Id == rt.Id) == 1; } } diff --git a/src/Modules/Identity/Domain/Users/ValueObjects/Fingerprint.cs b/src/Modules/Identity/Domain/Users/ValueObjects/Fingerprint.cs new file mode 100644 index 000000000..d8a9a5c55 --- /dev/null +++ b/src/Modules/Identity/Domain/Users/ValueObjects/Fingerprint.cs @@ -0,0 +1,3 @@ +namespace CustomCADs.Modules.Identity.Domain.Users.ValueObjects; + +public record Fingerprint(string Device = "", string? Location = null); diff --git a/src/Modules/Identity/Infrastructure/Constants.cs b/src/Modules/Identity/Infrastructure/Constants.cs new file mode 100644 index 000000000..2400d76fe --- /dev/null +++ b/src/Modules/Identity/Infrastructure/Constants.cs @@ -0,0 +1,21 @@ +namespace CustomCADs.Modules.Identity.Infrastructure; + +public static class Constants +{ + public static class Roles + { + public static readonly Guid CustomerId = new("762ddec2-25c9-4183-9891-72a19d84a839"); + public static readonly Guid ContributorId = new("e1101e2c-32cc-456f-9c82-4f1d1a65d141"); + public static readonly Guid DesignerId = new("f3ad41d3-ee90-4988-9195-8b2a8f4f2733"); + public static readonly Guid AdminId = new("fad1b19d-5333-4633-bd84-d67c64649f65"); + } + + public static class Users + { + public static readonly Guid CustomerId = new("e38c495f-b1f3-4226-d289-08dd11623eb9"); + public static readonly Guid ContributorId = new("af840410-f3f2-4a3b-d28a-08dd11623eb9"); + public static readonly Guid DesignerId = new("a8145f5f-a3a4-4f06-9461-9f24b9f23fde"); + public static readonly Guid HeadDesignerId = new("4337a774-2c5c-4c27-d28b-08dd11623eb9"); + public static readonly Guid AdminId = new("cb7749fb-3fff-4902-d28c-08dd11623eb9"); + } +} diff --git a/src/Modules/Identity/Infrastructure/DependencyInjection.cs b/src/Modules/Identity/Infrastructure/DependencyInjection.cs index 0039dfd39..1753d329f 100644 --- a/src/Modules/Identity/Infrastructure/DependencyInjection.cs +++ b/src/Modules/Identity/Infrastructure/DependencyInjection.cs @@ -1,8 +1,9 @@ #pragma warning disable IDE0130 using CustomCADs.Modules.Identity.Application.Contracts; using CustomCADs.Modules.Identity.Infrastructure.BackgroundJobs; -using CustomCADs.Modules.Identity.Infrastructure.Identity; +using CustomCADs.Modules.Identity.Infrastructure.Fingerprints; using CustomCADs.Modules.Identity.Infrastructure.Identity.Context; +using CustomCADs.Modules.Identity.Infrastructure.Identity.Services; using CustomCADs.Modules.Identity.Infrastructure.Tokens; using Microsoft.EntityFrameworkCore; using Npgsql; @@ -25,8 +26,11 @@ public async Task UpdateIdentityContextAsync() extension(IServiceCollection services) { + public IServiceCollection AddFingerprintsService() + => services.AddScoped(); + public IServiceCollection AddTokensService() - => services.AddScoped(); + => services.AddScoped(); public IServiceCollection AddIdentityServices(string connectionString) => services @@ -36,9 +40,13 @@ public IServiceCollection AddIdentityServices(string connectionString) private IServiceCollection AddContext(string connectionString) { - services.AddDbContext(options => + services.AddSingleton( + sp => new NpgsqlDataSourceBuilder(connectionString).EnableDynamicJson().Build() + ); + + services.AddDbContext((sp, options) => options.UseNpgsql( - dataSource: new NpgsqlDataSourceBuilder(connectionString).EnableDynamicJson().Build(), + dataSource: sp.GetRequiredService(), npgsqlOptionsAction: opt => opt.MigrationsHistoryTable("__EFMigrationsHistory", IdentityContext.Schema) ) ); diff --git a/src/Modules/Identity/Infrastructure/Fingerprints/DeviceDetectorFingerprintService.cs b/src/Modules/Identity/Infrastructure/Fingerprints/DeviceDetectorFingerprintService.cs new file mode 100644 index 000000000..95ef0ce19 --- /dev/null +++ b/src/Modules/Identity/Infrastructure/Fingerprints/DeviceDetectorFingerprintService.cs @@ -0,0 +1,49 @@ +using DeviceDetectorNET; +using DeviceDetectorNET.Parser; +using DeviceDetectorNET.Results; +using DeviceDetectorNET.Results.Client; + +namespace CustomCADs.Modules.Identity.Infrastructure.Fingerprints; + +using Application.Contracts; +using Domain.Users.ValueObjects; + +public class DeviceDetectorFingerprintService : IFingerprintService +{ + public DeviceDetectorFingerprintService() + { + DeviceDetector.SetVersionTruncation(VersionTruncation.VERSION_TRUNCATION_MINOR); + } + + public Fingerprint GetFingerprint(Dictionary headers) + => new( + Device: GetDevice(headers), + Location: GetLocation(headers) + ); + + private static string GetDevice(Dictionary headers) + { + DeviceDetector detector = new( + userAgent: headers["User-Agent"], + clientHints: ClientHints.Factory(headers) + ); + detector.Parse(); + + if (detector.IsBot()) + { + IEnumerable botNames = detector.GetBot().Matches.Select(x => x.Name); + return string.Join("; ", botNames); + } + + ClientMatchResult clientInfo = detector.GetClient().Match; + OsMatchResult osInfo = detector.GetOs().Match; + + string os = $"{osInfo.Name} {osInfo.Platform}"; + string client = $"{clientInfo.Name} {clientInfo.Version}"; + + return $"{os} running {client}"; + } + + private static string? GetLocation(Dictionary headers, string locationHeaderName = "CF-IPCountry") + => headers.GetValueOrDefault(locationHeaderName); +} diff --git a/src/Modules/Identity/Infrastructure/GlobalUsings.cs b/src/Modules/Identity/Infrastructure/GlobalUsings.cs new file mode 100644 index 000000000..d01f7e4ac --- /dev/null +++ b/src/Modules/Identity/Infrastructure/GlobalUsings.cs @@ -0,0 +1 @@ +global using AppUserRole = Microsoft.AspNetCore.Identity.IdentityUserRole; diff --git a/src/Modules/Identity/Infrastructure/Identity/AppUserService.cs b/src/Modules/Identity/Infrastructure/Identity/AppUserService.cs deleted file mode 100644 index 4e84c15f3..000000000 --- a/src/Modules/Identity/Infrastructure/Identity/AppUserService.cs +++ /dev/null @@ -1,222 +0,0 @@ -using CustomCADs.Modules.Identity.Application.Contracts; -using CustomCADs.Modules.Identity.Domain.Users; -using CustomCADs.Modules.Identity.Domain.Users.Entities; -using CustomCADs.Modules.Identity.Infrastructure.Identity.ShadowEntities; -using CustomCADs.Shared.Application.Exceptions; -using CustomCADs.Shared.Domain.TypedIds.Accounts; -using CustomCADs.Shared.Domain.TypedIds.Identity; -using Microsoft.AspNetCore.Identity; -using Microsoft.EntityFrameworkCore; - -namespace CustomCADs.Modules.Identity.Infrastructure.Identity; - -public class AppUserService(UserManager manager) : IUserService -{ - #region GetUserByX - public async Task GetByUsernameAsync(string username) - { - AppUser appUser = await manager.Users - .Include(x => x.RefreshTokens) - .FirstOrDefaultAsync(x => x.UserName == (x.IsSSO ? x.Provider + '/' + username : username)) - .ConfigureAwait(false) - ?? throw CustomNotFoundException.ByProp(nameof(username), username); - - return await appUser.ToUserWithRoleAsync(manager).ConfigureAwait(false); - } - - public async Task GetByEmailAsync(string email) - { - AppUser appUser = await manager.Users - .Include(x => x.RefreshTokens) - .FirstOrDefaultAsync(x => x.Email == email) - .ConfigureAwait(false) - ?? throw CustomNotFoundException.ByProp(nameof(email), email); - - return await appUser.ToUserWithRoleAsync(manager).ConfigureAwait(false); - } - - public async Task<(User User, RefreshToken RefreshToken)> GetByRefreshTokenAsync(string token) - { - AppUser appUser = await manager.Users - .Include(x => x.RefreshTokens) - .FirstOrDefaultAsync(x => x.RefreshTokens.Any(x => x.Value == token)) - .ConfigureAwait(false) - ?? throw CustomNotFoundException.ByProp(nameof(token), token); - - return ( - User: await appUser.ToUserWithRoleAsync(manager).ConfigureAwait(false), - RefreshToken: appUser.RefreshTokens.First(x => x.Value == token).ToRefreshToken() - ); - } - #endregion - - #region GetPropertyX - public async Task GetExistsByUsernameAsync(string username) - => await manager.Users - .AnyAsync(x => x.UserName == (x.IsSSO ? x.Provider + '/' + username : username)) - .ConfigureAwait(false); - - public async Task GetExistsByEmailAsync(string email) - => await manager.Users - .AnyAsync(x => x.Email == email) - .ConfigureAwait(false); - - public async Task GetAccountIdAsync(string username) - { - AccountId accountId = await manager.Users - .Where(x => x.UserName == (x.IsSSO ? x.Provider + '/' + username : username)) - .Select(x => x.AccountId) - .FirstOrDefaultAsync() - .ConfigureAwait(false); - - if (accountId.IsEmpty()) - { - throw CustomNotFoundException.ByProp(nameof(username), username); - } - - return accountId; - } - - public async Task GetIsLockedOutAsync(string username) - { - AppUser appUser = await manager.Users - .FirstOrDefaultAsync(x => x.UserName == (x.IsSSO ? x.Provider + '/' + username : username)) - .ConfigureAwait(false) - ?? throw CustomNotFoundException.ByProp(nameof(username), username); - - bool isLockedOut = await manager.IsLockedOutAsync(appUser).ConfigureAwait(false); - if (!isLockedOut) - { - return null; - } - - return appUser.LockoutEnd; - } - #endregion - - #region Lifecycle - public async Task CreateAsync(User user, string password) - { - AppUser appUser = user.ToAppUser(); - await manager.CreateAsync(appUser, password).ConfigureAwait(false); - - IdentityResult result = await manager.AddToRoleAsync(appUser, user.Role).ConfigureAwait(false); - if (!result.Succeeded) - { - throw new CustomException($"Couldn't create an account for: {user.Username}."); - } - } - - public async Task CreateSSOAsync(User user, string provider) - { - AppUser appUser = user.ToAppUser(provider); - await manager.CreateAsync(appUser).ConfigureAwait(false); - - IdentityResult result = await manager.AddToRoleAsync(appUser, user.Role).ConfigureAwait(false); - if (!result.Succeeded) - { - throw new CustomException($"Couldn't create an account for: {user.Username}."); - } - } - - public async Task DeleteAsync(string username) - { - AppUser appUser = await manager.FindByIdAsync(username).ConfigureAwait(false) - ?? throw CustomNotFoundException.ByProp(nameof(username), username); - - await manager.DeleteAsync(appUser).ConfigureAwait(false); - } - #endregion - - #region Mutation - public async Task CheckPasswordAsync(string username, string password) - { - AppUser appUser = await manager.FindByNameAsync(username).ConfigureAwait(false) - ?? throw CustomNotFoundException.ByProp(nameof(username), username); - - bool success = await manager.CheckPasswordAsync(appUser, password).ConfigureAwait(false); - if (success) - { - await manager.ResetAccessFailedCountAsync(appUser).ConfigureAwait(false); - } - else - { - await manager.AccessFailedAsync(appUser).ConfigureAwait(false); - } - - return success; - } - - public async Task UpdateUsernameAsync(UserId id, string username) - { - AppUser appUser = await manager.FindByIdAsync(id.ToString()).ConfigureAwait(false) - ?? throw CustomNotFoundException.ByProp(nameof(id), id); - - appUser.Username = username; - await manager.UpdateAsync(appUser).ConfigureAwait(false); - } - - public async Task SaveRefreshTokensAsync(User user) - { - AppUser appUser = await manager.FindByIdAsync(user.Id.ToString()).ConfigureAwait(false) - ?? throw CustomNotFoundException.ByProp(nameof(user.Id), user.Id); - - appUser.FillRefreshTokens([.. user.RefreshTokens.Select(x => x.ToAppRefreshToken())]); - await manager.UpdateAsync(appUser).ConfigureAwait(false); - } - - public async Task RevokeRefreshTokenAsync(string token) - { - (User User, RefreshToken RefreshToken) = await GetByRefreshTokenAsync(token).ConfigureAwait(false); - User.RemoveRefreshToken(RefreshToken); - - AppUser appUser = await manager.FindByIdAsync(User.Id.ToString()).ConfigureAwait(false) - ?? throw CustomNotFoundException.ByProp(nameof(User.Id), User.Id); - - appUser.FillRefreshTokens([.. User.RefreshTokens.Select(x => x.ToAppRefreshToken())]); - await manager.UpdateAsync(appUser).ConfigureAwait(false); - } - #endregion - - #region Token Generation - public async Task GenerateEmailConfirmationTokenAsync(string username) - { - AppUser appUser = await manager.FindByNameAsync(username).ConfigureAwait(false) - ?? throw CustomNotFoundException.ByProp(nameof(username), username); - - return await manager.GenerateEmailConfirmationTokenAsync(appUser).ConfigureAwait(false); - } - - public async Task ConfirmEmailAsync(string username, string token) - { - AppUser appUser = await manager.FindByNameAsync(username).ConfigureAwait(false) - ?? throw CustomNotFoundException.ByProp(nameof(username), username); - - IdentityResult result = await manager.ConfirmEmailAsync(appUser, token).ConfigureAwait(false); - if (!result.Succeeded) - { - throw CustomAuthorizationException.Custom($"Error confirming Account: {username}'s email."); - } - } - - public async Task GeneratePasswordResetTokenAsync(string email) - { - AppUser appUser = await manager.FindByEmailAsync(email).ConfigureAwait(false) - ?? throw CustomNotFoundException.ByProp(nameof(email), email); - - return await manager.GeneratePasswordResetTokenAsync(appUser).ConfigureAwait(false); - } - - public async Task ResetPasswordAsync(string email, string token, string newPassword) - { - AppUser appUser = await manager.FindByEmailAsync(email).ConfigureAwait(false) - ?? throw CustomNotFoundException.ByProp(nameof(email), email); - - IdentityResult result = await manager.ResetPasswordAsync(appUser, token, newPassword).ConfigureAwait(false); - if (!result.Succeeded) - { - throw CustomAuthorizationException.Custom($"Failed to reset Account: {email}'s password."); - } - } - #endregion -} diff --git a/src/Modules/Identity/Infrastructure/Identity/Configurations/AppRefreshTokens/Configurations.cs b/src/Modules/Identity/Infrastructure/Identity/Configurations/AppRefreshTokens/Configurations.cs index 2d78aaf86..6b4b63da2 100644 --- a/src/Modules/Identity/Infrastructure/Identity/Configurations/AppRefreshTokens/Configurations.cs +++ b/src/Modules/Identity/Infrastructure/Identity/Configurations/AppRefreshTokens/Configurations.cs @@ -8,5 +8,6 @@ public class Configurations : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) => builder + .SetValueObjects() .SetValidations(); } diff --git a/src/Modules/Identity/Infrastructure/Identity/Configurations/AppRefreshTokens/Utilities.cs b/src/Modules/Identity/Infrastructure/Identity/Configurations/AppRefreshTokens/Utilities.cs index 21652e431..a0828aaeb 100644 --- a/src/Modules/Identity/Infrastructure/Identity/Configurations/AppRefreshTokens/Utilities.cs +++ b/src/Modules/Identity/Infrastructure/Identity/Configurations/AppRefreshTokens/Utilities.cs @@ -1,4 +1,5 @@ -using CustomCADs.Modules.Identity.Infrastructure.Identity.ShadowEntities; +using CustomCADs.Modules.Identity.Domain.Users.ValueObjects; +using CustomCADs.Modules.Identity.Infrastructure.Identity.ShadowEntities; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; @@ -8,6 +9,17 @@ internal static class Utilities { extension(EntityTypeBuilder builder) { + internal EntityTypeBuilder SetValueObjects() + { + builder.ComplexProperty(x => x.Fingerprint, builder => + { + builder.Property(x => x.Device).IsRequired().HasColumnName(nameof(Fingerprint.Device)); + builder.Property(x => x.Location).IsRequired(false).HasColumnName(nameof(Fingerprint.Location)); + }); + + return builder; + } + internal EntityTypeBuilder SetValidations() { builder.Property(x => x.Value) diff --git a/src/Modules/Identity/Infrastructure/Identity/Configurations/AppRoles/Utilities.cs b/src/Modules/Identity/Infrastructure/Identity/Configurations/AppRoles/Utilities.cs index b1f99ca73..d30541b13 100644 --- a/src/Modules/Identity/Infrastructure/Identity/Configurations/AppRoles/Utilities.cs +++ b/src/Modules/Identity/Infrastructure/Identity/Configurations/AppRoles/Utilities.cs @@ -1,11 +1,8 @@ using CustomCADs.Modules.Identity.Infrastructure.Identity.ShadowEntities; -using CustomCADs.Shared.Domain; using Microsoft.EntityFrameworkCore.Metadata.Builders; namespace CustomCADs.Modules.Identity.Infrastructure.Identity.Configurations.AppRoles; -using static DomainConstants.Roles; - internal static class Utilities { extension(EntityTypeBuilder builder) @@ -13,13 +10,37 @@ internal static class Utilities internal EntityTypeBuilder SetSeeding() { builder.HasData([ - new(Customer) { NormalizedName = Customer.ToUpperInvariant(), Id = new(CustomerId), ConcurrencyStamp = "51da1b9f-803c-4bd3-9a00-da7ac259ce32", }, - new(Contributor) { NormalizedName = Contributor.ToUpperInvariant(), Id = new(ContributorId), ConcurrencyStamp = "a1a170e0-ee84-4afe-afd9-1df57009f291", }, - new(Designer) { NormalizedName = Designer.ToUpperInvariant(), Id = new(DesignerId), ConcurrencyStamp = "1a8ba0a7-4853-42da-980d-3107784e7ab1", }, - new(Admin) { NormalizedName = Admin.ToUpperInvariant(), Id = new(AdminId), ConcurrencyStamp = "42174679-32f1-48b0-9524-0f00791ec760", }, + CreateAppRole( + id: Constants.Roles.CustomerId, + name: Shared.Domain.DomainConstants.Users.CustomerRole, + concurrencyStamp: "51da1b9f-803c-4bd3-9a00-da7ac259ce32" + ), + CreateAppRole( + id: Constants.Roles.ContributorId, + name: Shared.Domain.DomainConstants.Users.ContributorRole, + concurrencyStamp: "a1a170e0-ee84-4afe-afd9-1df57009f291" + ), + CreateAppRole( + id: Constants.Roles.DesignerId, + name: Shared.Domain.DomainConstants.Users.DesignerRole, + concurrencyStamp: "1a8ba0a7-4853-42da-980d-3107784e7ab1" + ), + CreateAppRole( + id: Constants.Roles.AdminId, + name: Shared.Domain.DomainConstants.Users.AdminRole, + concurrencyStamp: "42174679-32f1-48b0-9524-0f00791ec760" + ), ]); return builder; + + static AppRole CreateAppRole(Guid id, string name, string concurrencyStamp) + => new(name) + { + NormalizedName = name.ToUpperInvariant(), + Id = id, + ConcurrencyStamp = concurrencyStamp, + }; } } diff --git a/src/Modules/Identity/Infrastructure/Identity/Configurations/AppUserRoles/Configurations.cs b/src/Modules/Identity/Infrastructure/Identity/Configurations/AppUserRoles/Configurations.cs index 12adaf36b..1748998f1 100644 --- a/src/Modules/Identity/Infrastructure/Identity/Configurations/AppUserRoles/Configurations.cs +++ b/src/Modules/Identity/Infrastructure/Identity/Configurations/AppUserRoles/Configurations.cs @@ -3,8 +3,6 @@ namespace CustomCADs.Modules.Identity.Infrastructure.Identity.Configurations.AppUserRoles; -using AppUserRole = Microsoft.AspNetCore.Identity.IdentityUserRole; - public class Configurations : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) diff --git a/src/Modules/Identity/Infrastructure/Identity/Configurations/AppUserRoles/Utilities.cs b/src/Modules/Identity/Infrastructure/Identity/Configurations/AppUserRoles/Utilities.cs index 630d2aaab..d6cd92ed6 100644 --- a/src/Modules/Identity/Infrastructure/Identity/Configurations/AppUserRoles/Utilities.cs +++ b/src/Modules/Identity/Infrastructure/Identity/Configurations/AppUserRoles/Utilities.cs @@ -1,10 +1,8 @@ -using CustomCADs.Shared.Domain; -using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Microsoft.EntityFrameworkCore.Metadata.Builders; namespace CustomCADs.Modules.Identity.Infrastructure.Identity.Configurations.AppUserRoles; -using static DomainConstants; -using AppUserRole = Microsoft.AspNetCore.Identity.IdentityUserRole; +using static Infrastructure.Constants; internal static class Utilities { @@ -13,10 +11,11 @@ internal static class Utilities internal EntityTypeBuilder SetSeeding() { builder.HasData([ - new() { RoleId = new(Roles.CustomerId), UserId = new(Users.CustomerUserId) }, - new() { RoleId = new(Roles.ContributorId), UserId = new(Users.ContributorUserId) }, - new() { RoleId = new(Roles.DesignerId), UserId = new(Users.DesignerUserId) }, - new() { RoleId = new(Roles.AdminId), UserId = new(Users.AdminUserId) }, + new() { RoleId = Roles.CustomerId, UserId = Users.CustomerId }, + new() { RoleId = Roles.ContributorId, UserId = Users.ContributorId }, + new() { RoleId = Roles.DesignerId, UserId = Users.DesignerId }, + new() { RoleId = Roles.DesignerId, UserId = Users.HeadDesignerId }, + new() { RoleId = Roles.AdminId, UserId = Users.AdminId }, ]); return builder; diff --git a/src/Modules/Identity/Infrastructure/Identity/Configurations/AppUsers/Utilities.cs b/src/Modules/Identity/Infrastructure/Identity/Configurations/AppUsers/Utilities.cs index ac8999962..252c6ff19 100644 --- a/src/Modules/Identity/Infrastructure/Identity/Configurations/AppUsers/Utilities.cs +++ b/src/Modules/Identity/Infrastructure/Identity/Configurations/AppUsers/Utilities.cs @@ -1,13 +1,10 @@ using CustomCADs.Modules.Identity.Infrastructure.Identity.ShadowEntities; -using CustomCADs.Shared.Domain; using CustomCADs.Shared.Domain.TypedIds.Accounts; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; namespace CustomCADs.Modules.Identity.Infrastructure.Identity.Configurations.AppUsers; -using static DomainConstants.Users; - internal static class Utilities { extension(EntityTypeBuilder builder) @@ -53,37 +50,46 @@ internal EntityTypeBuilder SetSeeding() { builder.HasData([ CreateAppUser( - id: new(CustomerUserId), - accountId: new(CustomerAccountId), - username: CustomerUsername, - email: CustomerEmail, + id: Constants.Users.CustomerId, + accountId: Shared.Domain.DomainConstants.Users.CustomerAccountId, + username: Shared.Domain.DomainConstants.Users.CustomerUsername, + email: Shared.Domain.DomainConstants.Users.CustomerEmail, passHash: "AQAAAAIAAYagAAAAEJFCGOTxNAgjhqU5lrA63WEtv924ujxXHt0x1R70qlS8dV9Pzz4II8GOgjVOaRzuDQ==", concStamp: "0c5bbfb2-d132-407b-9b1b-e1e640ccc14e", secStamp: "3A6TFN6VVZNRZEG22J777XJTPQY7342B" ), CreateAppUser( - id: new(ContributorUserId), - accountId: new(ContributorAccountId), - username: ContributorUsername, - email: ContributorEmail, + id: Constants.Users.ContributorId, + accountId: Shared.Domain.DomainConstants.Users.ContributorAccountId, + username: Shared.Domain.DomainConstants.Users.ContributorUsername, + email: Shared.Domain.DomainConstants.Users.ContributorEmail, passHash: "AQAAAAIAAYagAAAAEGjQ1Zes3r2XJgjoHQykiyr11OgUEDw+YDnOKeENyN7Kqi9RWKKRCtwd7ZtEyywdYA==", concStamp: "c77927de-61e7-4d53-be8d-a5390fafc75c", secStamp: "NWGZ3JTQSDNS346DMU7RP4IT4BDLHIQC" ), CreateAppUser( - id: new(DesignerUserId), - accountId: new(DesignerAccountId), - username: DesignerUsername, - email: DesignerEmail, + id: Constants.Users.DesignerId, + accountId: Shared.Domain.DomainConstants.Users.DesignerAccountId, + username: Shared.Domain.DomainConstants.Users.DesignerUsername, + email: Shared.Domain.DomainConstants.Users.DesignerEmail, passHash: "AQAAAAIAAYagAAAAEEUe31maWfuZY6V8MQBzUWKerMKobDukREinVfML3Yl2z+Nr6IIQZKvX4WKqbTUw6w==", concStamp: "c5940d6f-d5c0-4f84-a262-da9b07525c3c", secStamp: "FNNIT3NPOZKZK2E67WFLV5R3RGVBX7LV" ), CreateAppUser( - id: new(AdminUserId), - accountId: new(AdminAccountId), - username: AdminUsername, - email: AdminEmail, + id: Constants.Users.HeadDesignerId, + accountId: Shared.Domain.DomainConstants.Users.HeadDesignerAccountId, + username: Shared.Domain.DomainConstants.Users.HeadDesignerUsername, + email: Shared.Domain.DomainConstants.Users.HeadDesignerEmail, + passHash: "AQAAAAIAAYagAAAAEJGRCbKUsSh9BwxPXoIQRG1AVXYDfmWsY5vEA4aEnqBBQAzdcgLFCHUtpwd86+B4mA==", + concStamp: "c11d44ef-29c5-43ac-89f9-a2e7cd482ec8", + secStamp: "MHG763AUJVOJPKKWUC64FQSF7DIVVBOU" + ), + CreateAppUser( + id: Constants.Users.AdminId, + accountId: Shared.Domain.DomainConstants.Users.AdminAccountId, + username: Shared.Domain.DomainConstants.Users.AdminUsername, + email: Shared.Domain.DomainConstants.Users.AdminEmail, passHash: "AQAAAAIAAYagAAAAEFqtQ33BvarNRyFcmV4z48fPBlIY8zd0de90qq3Cdm1Row+2WRmEjVJk1yPadBkrSA==", concStamp: "5c94b43f-861c-4efa-a670-5627e49d354d", secStamp: "YIA26UZDSN2V2U5PVDEK4F3EJS3P5D3X" @@ -94,8 +100,8 @@ internal EntityTypeBuilder SetSeeding() } } - private static AppUser CreateAppUser(Guid id, string username, string email, string passHash, Guid accountId, string concStamp, string secStamp) - => new(username, email, AccountId.New(accountId)) + private static AppUser CreateAppUser(Guid id, string username, string email, string passHash, AccountId accountId, string concStamp, string secStamp) + => new(username, email, accountId) { Id = id, NormalizedUserName = username.ToUpperInvariant(), diff --git a/src/Modules/Identity/Infrastructure/Identity/Extensions.cs b/src/Modules/Identity/Infrastructure/Identity/Extensions.cs deleted file mode 100644 index d3970f710..000000000 --- a/src/Modules/Identity/Infrastructure/Identity/Extensions.cs +++ /dev/null @@ -1,24 +0,0 @@ -using CustomCADs.Modules.Identity.Domain.Users; -using CustomCADs.Modules.Identity.Infrastructure.Identity.ShadowEntities; -using Microsoft.AspNetCore.Identity; - -namespace CustomCADs.Modules.Identity.Infrastructure.Identity; - -public static class Extensions -{ - extension(AppUser appUser) - { - public async Task ToUserWithRoleAsync(UserManager manager) - => appUser.ToUser(role: await manager.GetRoleAsync(appUser).ConfigureAwait(false)); - } - - extension(UserManager manager) - { - internal async Task GetRoleAsync(AppUser appUser) - { - var roles = await manager.GetRolesAsync(appUser).ConfigureAwait(false); - return roles.Single(); - } - } - -} diff --git a/src/Modules/Identity/Infrastructure/Identity/Mapper.cs b/src/Modules/Identity/Infrastructure/Identity/Mapper.cs index 5982af701..f1eefd785 100644 --- a/src/Modules/Identity/Infrastructure/Identity/Mapper.cs +++ b/src/Modules/Identity/Infrastructure/Identity/Mapper.cs @@ -7,24 +7,6 @@ namespace CustomCADs.Modules.Identity.Infrastructure.Identity; internal static class Mapper { - private static class Shallow - { - internal static RefreshToken ToRefreshToken(AppRefreshToken rt) - => RefreshToken.Create( - id: RefreshTokenId.New(rt.Id), - value: rt.Value, - userId: UserId.New(rt.UserId), - issuedAt: rt.IssuedAt, - expiresAt: rt.ExpiresAt - ); - - internal static AppRefreshToken ToAppRefreshToken(RefreshToken rt) - => new(rt.Value, rt.UserId.Value, rt.IssuedAt, rt.ExpiresAt) - { - Id = rt.Id.Value, - }; - } - extension(AppUser appUser) { internal User ToUser(string role) @@ -34,7 +16,7 @@ internal User ToUser(string role) username: appUser.Username, email: new(appUser.Email ?? string.Empty, appUser.EmailConfirmed), accountId: appUser.AccountId, - refreshTokens: [.. appUser.RefreshTokens.Select(x => Shallow.ToRefreshToken(x))] + refreshTokens: [.. appUser.RefreshTokens.Select(ToRefreshToken)] ); } @@ -50,7 +32,7 @@ internal AppUser ToAppUser() Email = user.Email.Value, EmailConfirmed = user.Email.IsVerified, AccountId = user.AccountId, - }.FillRefreshTokens([.. user.RefreshTokens.Select(x => Shallow.ToAppRefreshToken(x))]); + }.FillRefreshTokens([.. user.RefreshTokens.Select(ToAppRefreshToken)]); internal AppUser ToAppUser(string provider) @@ -63,7 +45,7 @@ internal AppUser ToAppUser(string provider) Email = user.Email.Value, EmailConfirmed = true, AccountId = user.AccountId, - }.FillRefreshTokens([.. user.RefreshTokens.Select(x => Shallow.ToAppRefreshToken(x))]); + }.FillRefreshTokens([.. user.RefreshTokens.Select(ToAppRefreshToken)]); } extension(AppRefreshToken rt) @@ -71,6 +53,7 @@ internal AppUser ToAppUser(string provider) internal RefreshToken ToRefreshToken() => RefreshToken.Create( id: RefreshTokenId.New(rt.Id), + fingerprint: rt.Fingerprint, value: rt.Value, userId: UserId.New(rt.UserId), issuedAt: rt.IssuedAt, @@ -81,7 +64,7 @@ internal RefreshToken ToRefreshToken() extension(RefreshToken rt) { internal AppRefreshToken ToAppRefreshToken() - => new(rt.Value, rt.UserId.Value, rt.IssuedAt, rt.ExpiresAt) + => new(rt.Value, rt.Fingerprint, rt.UserId.Value, rt.IssuedAt, rt.ExpiresAt) { Id = rt.Id.Value, }; diff --git a/src/Modules/Identity/Infrastructure/Migrations/20251215233938_Initial_Migration.Designer.cs b/src/Modules/Identity/Infrastructure/Identity/Migrations/20251215233938_Initial_Migration.Designer.cs similarity index 100% rename from src/Modules/Identity/Infrastructure/Migrations/20251215233938_Initial_Migration.Designer.cs rename to src/Modules/Identity/Infrastructure/Identity/Migrations/20251215233938_Initial_Migration.Designer.cs diff --git a/src/Modules/Identity/Infrastructure/Migrations/20251215233938_Initial_Migration.cs b/src/Modules/Identity/Infrastructure/Identity/Migrations/20251215233938_Initial_Migration.cs similarity index 100% rename from src/Modules/Identity/Infrastructure/Migrations/20251215233938_Initial_Migration.cs rename to src/Modules/Identity/Infrastructure/Identity/Migrations/20251215233938_Initial_Migration.cs diff --git a/src/Modules/Identity/Infrastructure/Identity/Migrations/20260424222719_Added_HeadDesigner_User.Designer.cs b/src/Modules/Identity/Infrastructure/Identity/Migrations/20260424222719_Added_HeadDesigner_User.Designer.cs new file mode 100644 index 000000000..8a48b8e9d --- /dev/null +++ b/src/Modules/Identity/Infrastructure/Identity/Migrations/20260424222719_Added_HeadDesigner_User.Designer.cs @@ -0,0 +1,483 @@ +// +using System; +using CustomCADs.Modules.Identity.Infrastructure.Identity.Context; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace CustomCADs.Modules.Identity.Infrastructure.Migrations +{ + [DbContext(typeof(IdentityContext))] + [Migration("20260424222719_Added_HeadDesigner_User")] + partial class Added_HeadDesigner_User + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("Identity") + .HasAnnotation("ProductVersion", "10.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("CustomCADs.Modules.Identity.Infrastructure.Identity.ShadowEntities.AppRefreshToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("ExpiresAt"); + + b.Property("IssuedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("IssuedAt"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("UserId"); + + b.Property("Value") + .IsRequired() + .HasColumnType("text") + .HasColumnName("Value"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AppRefreshToken", "Identity"); + }); + + modelBuilder.Entity("CustomCADs.Modules.Identity.Infrastructure.Identity.ShadowEntities.AppRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", "Identity"); + + b.HasData( + new + { + Id = new Guid("762ddec2-25c9-4183-9891-72a19d84a839"), + ConcurrencyStamp = "51da1b9f-803c-4bd3-9a00-da7ac259ce32", + Name = "Customer", + NormalizedName = "CUSTOMER" + }, + new + { + Id = new Guid("e1101e2c-32cc-456f-9c82-4f1d1a65d141"), + ConcurrencyStamp = "a1a170e0-ee84-4afe-afd9-1df57009f291", + Name = "Contributor", + NormalizedName = "CONTRIBUTOR" + }, + new + { + Id = new Guid("f3ad41d3-ee90-4988-9195-8b2a8f4f2733"), + ConcurrencyStamp = "1a8ba0a7-4853-42da-980d-3107784e7ab1", + Name = "Designer", + NormalizedName = "DESIGNER" + }, + new + { + Id = new Guid("fad1b19d-5333-4633-bd84-d67c64649f65"), + ConcurrencyStamp = "42174679-32f1-48b0-9524-0f00791ec760", + Name = "Administrator", + NormalizedName = "ADMINISTRATOR" + }); + }); + + modelBuilder.Entity("CustomCADs.Modules.Identity.Infrastructure.Identity.ShadowEntities.AppUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AccessFailedCount") + .HasColumnType("integer"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("AccountId"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("boolean"); + + b.Property("IsSSO") + .HasColumnType("boolean"); + + b.Property("LockoutEnabled") + .HasColumnType("boolean"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PasswordHash") + .HasColumnType("text"); + + b.Property("PhoneNumber") + .HasColumnType("text"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("boolean"); + + b.Property("Provider") + .HasColumnType("text") + .HasColumnName("Provider"); + + b.Property("SecurityStamp") + .HasColumnType("text"); + + b.Property("TwoFactorEnabled") + .HasColumnType("boolean"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", "Identity"); + + b.HasData( + new + { + Id = new Guid("e38c495f-b1f3-4226-d289-08dd11623eb9"), + AccessFailedCount = 0, + AccountId = new Guid("2da61b05-1a27-4af9-9df2-be4f1f4e835f"), + ConcurrencyStamp = "0c5bbfb2-d132-407b-9b1b-e1e640ccc14e", + Email = "ivanzlatinov006@gmail.com", + EmailConfirmed = true, + IsSSO = false, + LockoutEnabled = true, + NormalizedEmail = "IVANZLATINOV006@GMAIL.COM", + NormalizedUserName = "FOR7A7A", + PasswordHash = "AQAAAAIAAYagAAAAEJFCGOTxNAgjhqU5lrA63WEtv924ujxXHt0x1R70qlS8dV9Pzz4II8GOgjVOaRzuDQ==", + PhoneNumberConfirmed = false, + SecurityStamp = "3A6TFN6VVZNRZEG22J777XJTPQY7342B", + TwoFactorEnabled = false, + UserName = "For7a7a" + }, + new + { + Id = new Guid("af840410-f3f2-4a3b-d28a-08dd11623eb9"), + AccessFailedCount = 0, + AccountId = new Guid("6d963818-23dc-4e9a-aaa8-b4c77252bc97"), + ConcurrencyStamp = "c77927de-61e7-4d53-be8d-a5390fafc75c", + Email = "PDMatsaliev20@codingburgas.bg", + EmailConfirmed = true, + IsSSO = false, + LockoutEnabled = true, + NormalizedEmail = "PDMATSALIEV20@CODINGBURGAS.BG", + NormalizedUserName = "PDMATSALIEV20", + PasswordHash = "AQAAAAIAAYagAAAAEGjQ1Zes3r2XJgjoHQykiyr11OgUEDw+YDnOKeENyN7Kqi9RWKKRCtwd7ZtEyywdYA==", + PhoneNumberConfirmed = false, + SecurityStamp = "NWGZ3JTQSDNS346DMU7RP4IT4BDLHIQC", + TwoFactorEnabled = false, + UserName = "PDMatsaliev20" + }, + new + { + Id = new Guid("a8145f5f-a3a4-4f06-9461-9f24b9f23fde"), + AccessFailedCount = 0, + AccountId = new Guid("8d477999-0580-4770-8864-e9ba4bed9cd1"), + ConcurrencyStamp = "c5940d6f-d5c0-4f84-a262-da9b07525c3c", + Email = "john.cad@gmail.com", + EmailConfirmed = true, + IsSSO = false, + LockoutEnabled = true, + NormalizedEmail = "JOHN.CAD@GMAIL.COM", + NormalizedUserName = "JOHN_CAD", + PasswordHash = "AQAAAAIAAYagAAAAEEUe31maWfuZY6V8MQBzUWKerMKobDukREinVfML3Yl2z+Nr6IIQZKvX4WKqbTUw6w==", + PhoneNumberConfirmed = false, + SecurityStamp = "FNNIT3NPOZKZK2E67WFLV5R3RGVBX7LV", + TwoFactorEnabled = false, + UserName = "John_CAD" + }, + new + { + Id = new Guid("4337a774-2c5c-4c27-d28b-08dd11623eb9"), + AccessFailedCount = 0, + AccountId = new Guid("0fb3212f-7d51-4586-8fc2-0f333ec9fbc1"), + ConcurrencyStamp = "c11d44ef-29c5-43ac-89f9-a2e7cd482ec8", + Email = "boriskolev2006@gmail.com", + EmailConfirmed = true, + IsSSO = false, + LockoutEnabled = true, + NormalizedEmail = "BORISKOLEV2006@GMAIL.COM", + NormalizedUserName = "ORACLE3000", + PasswordHash = "AQAAAAIAAYagAAAAEJGRCbKUsSh9BwxPXoIQRG1AVXYDfmWsY5vEA4aEnqBBQAzdcgLFCHUtpwd86+B4mA==", + PhoneNumberConfirmed = false, + SecurityStamp = "MHG763AUJVOJPKKWUC64FQSF7DIVVBOU", + TwoFactorEnabled = false, + UserName = "Oracle3000" + }, + new + { + Id = new Guid("cb7749fb-3fff-4902-d28c-08dd11623eb9"), + AccessFailedCount = 0, + AccountId = new Guid("e995039c-a535-4f20-8288-7aadcb71b252"), + ConcurrencyStamp = "5c94b43f-861c-4efa-a670-5627e49d354d", + Email = "ivanangelov414@gmail.com", + EmailConfirmed = true, + IsSSO = false, + LockoutEnabled = true, + NormalizedEmail = "IVANANGELOV414@GMAIL.COM", + NormalizedUserName = "NINJATABG", + PasswordHash = "AQAAAAIAAYagAAAAEFqtQ33BvarNRyFcmV4z48fPBlIY8zd0de90qq3Cdm1Row+2WRmEjVJk1yPadBkrSA==", + PhoneNumberConfirmed = false, + SecurityStamp = "YIA26UZDSN2V2U5PVDEK4F3EJS3P5D3X", + TwoFactorEnabled = false, + UserName = "NinjataBG" + }); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("RoleId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", "Identity"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", "Identity"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("ProviderKey") + .HasColumnType("text"); + + b.Property("ProviderDisplayName") + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", "Identity"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("RoleId") + .HasColumnType("uuid"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", "Identity"); + + b.HasData( + new + { + UserId = new Guid("e38c495f-b1f3-4226-d289-08dd11623eb9"), + RoleId = new Guid("762ddec2-25c9-4183-9891-72a19d84a839") + }, + new + { + UserId = new Guid("af840410-f3f2-4a3b-d28a-08dd11623eb9"), + RoleId = new Guid("e1101e2c-32cc-456f-9c82-4f1d1a65d141") + }, + new + { + UserId = new Guid("a8145f5f-a3a4-4f06-9461-9f24b9f23fde"), + RoleId = new Guid("f3ad41d3-ee90-4988-9195-8b2a8f4f2733") + }, + new + { + UserId = new Guid("4337a774-2c5c-4c27-d28b-08dd11623eb9"), + RoleId = new Guid("f3ad41d3-ee90-4988-9195-8b2a8f4f2733") + }, + new + { + UserId = new Guid("cb7749fb-3fff-4902-d28c-08dd11623eb9"), + RoleId = new Guid("fad1b19d-5333-4633-bd84-d67c64649f65") + }); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", "Identity"); + }); + + modelBuilder.Entity("CustomCADs.Modules.Identity.Infrastructure.Identity.ShadowEntities.AppRefreshToken", b => + { + b.HasOne("CustomCADs.Modules.Identity.Infrastructure.Identity.ShadowEntities.AppUser", "User") + .WithMany("RefreshTokens") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("CustomCADs.Modules.Identity.Infrastructure.Identity.ShadowEntities.AppRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("CustomCADs.Modules.Identity.Infrastructure.Identity.ShadowEntities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("CustomCADs.Modules.Identity.Infrastructure.Identity.ShadowEntities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("CustomCADs.Modules.Identity.Infrastructure.Identity.ShadowEntities.AppRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("CustomCADs.Modules.Identity.Infrastructure.Identity.ShadowEntities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("CustomCADs.Modules.Identity.Infrastructure.Identity.ShadowEntities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("CustomCADs.Modules.Identity.Infrastructure.Identity.ShadowEntities.AppUser", b => + { + b.Navigation("RefreshTokens"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Modules/Identity/Infrastructure/Identity/Migrations/20260424222719_Added_HeadDesigner_User.cs b/src/Modules/Identity/Infrastructure/Identity/Migrations/20260424222719_Added_HeadDesigner_User.cs new file mode 100644 index 000000000..fc206345b --- /dev/null +++ b/src/Modules/Identity/Infrastructure/Identity/Migrations/20260424222719_Added_HeadDesigner_User.cs @@ -0,0 +1,57 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace CustomCADs.Modules.Identity.Infrastructure.Migrations; + +/// +public partial class Added_HeadDesigner_User : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.UpdateData( + schema: "Identity", + table: "AspNetUsers", + keyColumn: "Id", + keyValue: new Guid("4337a774-2c5c-4c27-d28b-08dd11623eb9"), + columns: new[] { "ConcurrencyStamp", "PasswordHash", "SecurityStamp" }, + values: new object[] { "c11d44ef-29c5-43ac-89f9-a2e7cd482ec8", "AQAAAAIAAYagAAAAEJGRCbKUsSh9BwxPXoIQRG1AVXYDfmWsY5vEA4aEnqBBQAzdcgLFCHUtpwd86+B4mA==", "MHG763AUJVOJPKKWUC64FQSF7DIVVBOU" }); + + migrationBuilder.InsertData( + schema: "Identity", + table: "AspNetUsers", + columns: new[] { "Id", "AccessFailedCount", "AccountId", "ConcurrencyStamp", "Email", "EmailConfirmed", "IsSSO", "LockoutEnabled", "LockoutEnd", "NormalizedEmail", "NormalizedUserName", "PasswordHash", "PhoneNumber", "PhoneNumberConfirmed", "Provider", "SecurityStamp", "TwoFactorEnabled", "UserName" }, + values: new object[] { new Guid("a8145f5f-a3a4-4f06-9461-9f24b9f23fde"), 0, new Guid("8d477999-0580-4770-8864-e9ba4bed9cd1"), "c5940d6f-d5c0-4f84-a262-da9b07525c3c", "john.cad@gmail.com", true, false, true, null, "JOHN.CAD@GMAIL.COM", "JOHN_CAD", "AQAAAAIAAYagAAAAEEUe31maWfuZY6V8MQBzUWKerMKobDukREinVfML3Yl2z+Nr6IIQZKvX4WKqbTUw6w==", null, false, null, "FNNIT3NPOZKZK2E67WFLV5R3RGVBX7LV", false, "John_CAD" }); + + migrationBuilder.InsertData( + schema: "Identity", + table: "AspNetUserRoles", + columns: new[] { "RoleId", "UserId" }, + values: new object[] { new Guid("f3ad41d3-ee90-4988-9195-8b2a8f4f2733"), new Guid("a8145f5f-a3a4-4f06-9461-9f24b9f23fde") }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DeleteData( + schema: "Identity", + table: "AspNetUserRoles", + keyColumns: new[] { "RoleId", "UserId" }, + keyValues: new object[] { new Guid("f3ad41d3-ee90-4988-9195-8b2a8f4f2733"), new Guid("a8145f5f-a3a4-4f06-9461-9f24b9f23fde") }); + + migrationBuilder.DeleteData( + schema: "Identity", + table: "AspNetUsers", + keyColumn: "Id", + keyValue: new Guid("a8145f5f-a3a4-4f06-9461-9f24b9f23fde")); + + migrationBuilder.UpdateData( + schema: "Identity", + table: "AspNetUsers", + keyColumn: "Id", + keyValue: new Guid("4337a774-2c5c-4c27-d28b-08dd11623eb9"), + columns: new[] { "ConcurrencyStamp", "PasswordHash", "SecurityStamp" }, + values: new object[] { "c5940d6f-d5c0-4f84-a262-da9b07525c3c", "AQAAAAIAAYagAAAAEEUe31maWfuZY6V8MQBzUWKerMKobDukREinVfML3Yl2z+Nr6IIQZKvX4WKqbTUw6w==", "FNNIT3NPOZKZK2E67WFLV5R3RGVBX7LV" }); + } +} diff --git a/src/Modules/Identity/Infrastructure/Identity/Migrations/20260506185244_Added_RefreshToken_Fingerprints.Designer.cs b/src/Modules/Identity/Infrastructure/Identity/Migrations/20260506185244_Added_RefreshToken_Fingerprints.Designer.cs new file mode 100644 index 000000000..8a5837f07 --- /dev/null +++ b/src/Modules/Identity/Infrastructure/Identity/Migrations/20260506185244_Added_RefreshToken_Fingerprints.Designer.cs @@ -0,0 +1,498 @@ +// +using System; +using System.Collections.Generic; +using CustomCADs.Modules.Identity.Infrastructure.Identity.Context; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace CustomCADs.Modules.Identity.Infrastructure.Migrations +{ + [DbContext(typeof(IdentityContext))] + [Migration("20260506185244_Added_RefreshToken_Fingerprints")] + partial class Added_RefreshToken_Fingerprints + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("Identity") + .HasAnnotation("ProductVersion", "10.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("CustomCADs.Modules.Identity.Infrastructure.Identity.ShadowEntities.AppRefreshToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("ExpiresAt"); + + b.Property("IssuedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("IssuedAt"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("UserId"); + + b.Property("Value") + .IsRequired() + .HasColumnType("text") + .HasColumnName("Value"); + + b.ComplexProperty(typeof(Dictionary), "Fingerprint", "CustomCADs.Modules.Identity.Infrastructure.Identity.ShadowEntities.AppRefreshToken.Fingerprint#Fingerprint", b1 => + { + b1.IsRequired(); + + b1.Property("Device") + .IsRequired() + .HasColumnType("text") + .HasColumnName("Device"); + + b1.Property("Location") + .HasColumnType("text") + .HasColumnName("Location"); + }); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AppRefreshToken", "Identity"); + }); + + modelBuilder.Entity("CustomCADs.Modules.Identity.Infrastructure.Identity.ShadowEntities.AppRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", "Identity"); + + b.HasData( + new + { + Id = new Guid("762ddec2-25c9-4183-9891-72a19d84a839"), + ConcurrencyStamp = "51da1b9f-803c-4bd3-9a00-da7ac259ce32", + Name = "Customer", + NormalizedName = "CUSTOMER" + }, + new + { + Id = new Guid("e1101e2c-32cc-456f-9c82-4f1d1a65d141"), + ConcurrencyStamp = "a1a170e0-ee84-4afe-afd9-1df57009f291", + Name = "Contributor", + NormalizedName = "CONTRIBUTOR" + }, + new + { + Id = new Guid("f3ad41d3-ee90-4988-9195-8b2a8f4f2733"), + ConcurrencyStamp = "1a8ba0a7-4853-42da-980d-3107784e7ab1", + Name = "Designer", + NormalizedName = "DESIGNER" + }, + new + { + Id = new Guid("fad1b19d-5333-4633-bd84-d67c64649f65"), + ConcurrencyStamp = "42174679-32f1-48b0-9524-0f00791ec760", + Name = "Administrator", + NormalizedName = "ADMINISTRATOR" + }); + }); + + modelBuilder.Entity("CustomCADs.Modules.Identity.Infrastructure.Identity.ShadowEntities.AppUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AccessFailedCount") + .HasColumnType("integer"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("AccountId"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("boolean"); + + b.Property("IsSSO") + .HasColumnType("boolean"); + + b.Property("LockoutEnabled") + .HasColumnType("boolean"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PasswordHash") + .HasColumnType("text"); + + b.Property("PhoneNumber") + .HasColumnType("text"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("boolean"); + + b.Property("Provider") + .HasColumnType("text") + .HasColumnName("Provider"); + + b.Property("SecurityStamp") + .HasColumnType("text"); + + b.Property("TwoFactorEnabled") + .HasColumnType("boolean"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", "Identity"); + + b.HasData( + new + { + Id = new Guid("e38c495f-b1f3-4226-d289-08dd11623eb9"), + AccessFailedCount = 0, + AccountId = new Guid("2da61b05-1a27-4af9-9df2-be4f1f4e835f"), + ConcurrencyStamp = "0c5bbfb2-d132-407b-9b1b-e1e640ccc14e", + Email = "ivanzlatinov006@gmail.com", + EmailConfirmed = true, + IsSSO = false, + LockoutEnabled = true, + NormalizedEmail = "IVANZLATINOV006@GMAIL.COM", + NormalizedUserName = "FOR7A7A", + PasswordHash = "AQAAAAIAAYagAAAAEJFCGOTxNAgjhqU5lrA63WEtv924ujxXHt0x1R70qlS8dV9Pzz4II8GOgjVOaRzuDQ==", + PhoneNumberConfirmed = false, + SecurityStamp = "3A6TFN6VVZNRZEG22J777XJTPQY7342B", + TwoFactorEnabled = false, + UserName = "For7a7a" + }, + new + { + Id = new Guid("af840410-f3f2-4a3b-d28a-08dd11623eb9"), + AccessFailedCount = 0, + AccountId = new Guid("6d963818-23dc-4e9a-aaa8-b4c77252bc97"), + ConcurrencyStamp = "c77927de-61e7-4d53-be8d-a5390fafc75c", + Email = "PDMatsaliev20@codingburgas.bg", + EmailConfirmed = true, + IsSSO = false, + LockoutEnabled = true, + NormalizedEmail = "PDMATSALIEV20@CODINGBURGAS.BG", + NormalizedUserName = "PDMATSALIEV20", + PasswordHash = "AQAAAAIAAYagAAAAEGjQ1Zes3r2XJgjoHQykiyr11OgUEDw+YDnOKeENyN7Kqi9RWKKRCtwd7ZtEyywdYA==", + PhoneNumberConfirmed = false, + SecurityStamp = "NWGZ3JTQSDNS346DMU7RP4IT4BDLHIQC", + TwoFactorEnabled = false, + UserName = "PDMatsaliev20" + }, + new + { + Id = new Guid("a8145f5f-a3a4-4f06-9461-9f24b9f23fde"), + AccessFailedCount = 0, + AccountId = new Guid("8d477999-0580-4770-8864-e9ba4bed9cd1"), + ConcurrencyStamp = "c5940d6f-d5c0-4f84-a262-da9b07525c3c", + Email = "john.cad@gmail.com", + EmailConfirmed = true, + IsSSO = false, + LockoutEnabled = true, + NormalizedEmail = "JOHN.CAD@GMAIL.COM", + NormalizedUserName = "JOHN_CAD", + PasswordHash = "AQAAAAIAAYagAAAAEEUe31maWfuZY6V8MQBzUWKerMKobDukREinVfML3Yl2z+Nr6IIQZKvX4WKqbTUw6w==", + PhoneNumberConfirmed = false, + SecurityStamp = "FNNIT3NPOZKZK2E67WFLV5R3RGVBX7LV", + TwoFactorEnabled = false, + UserName = "John_CAD" + }, + new + { + Id = new Guid("4337a774-2c5c-4c27-d28b-08dd11623eb9"), + AccessFailedCount = 0, + AccountId = new Guid("0fb3212f-7d51-4586-8fc2-0f333ec9fbc1"), + ConcurrencyStamp = "c11d44ef-29c5-43ac-89f9-a2e7cd482ec8", + Email = "boriskolev2006@gmail.com", + EmailConfirmed = true, + IsSSO = false, + LockoutEnabled = true, + NormalizedEmail = "BORISKOLEV2006@GMAIL.COM", + NormalizedUserName = "ORACLE3000", + PasswordHash = "AQAAAAIAAYagAAAAEJGRCbKUsSh9BwxPXoIQRG1AVXYDfmWsY5vEA4aEnqBBQAzdcgLFCHUtpwd86+B4mA==", + PhoneNumberConfirmed = false, + SecurityStamp = "MHG763AUJVOJPKKWUC64FQSF7DIVVBOU", + TwoFactorEnabled = false, + UserName = "Oracle3000" + }, + new + { + Id = new Guid("cb7749fb-3fff-4902-d28c-08dd11623eb9"), + AccessFailedCount = 0, + AccountId = new Guid("e995039c-a535-4f20-8288-7aadcb71b252"), + ConcurrencyStamp = "5c94b43f-861c-4efa-a670-5627e49d354d", + Email = "ivanangelov414@gmail.com", + EmailConfirmed = true, + IsSSO = false, + LockoutEnabled = true, + NormalizedEmail = "IVANANGELOV414@GMAIL.COM", + NormalizedUserName = "NINJATABG", + PasswordHash = "AQAAAAIAAYagAAAAEFqtQ33BvarNRyFcmV4z48fPBlIY8zd0de90qq3Cdm1Row+2WRmEjVJk1yPadBkrSA==", + PhoneNumberConfirmed = false, + SecurityStamp = "YIA26UZDSN2V2U5PVDEK4F3EJS3P5D3X", + TwoFactorEnabled = false, + UserName = "NinjataBG" + }); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("RoleId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", "Identity"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", "Identity"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("ProviderKey") + .HasColumnType("text"); + + b.Property("ProviderDisplayName") + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", "Identity"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("RoleId") + .HasColumnType("uuid"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", "Identity"); + + b.HasData( + new + { + UserId = new Guid("e38c495f-b1f3-4226-d289-08dd11623eb9"), + RoleId = new Guid("762ddec2-25c9-4183-9891-72a19d84a839") + }, + new + { + UserId = new Guid("af840410-f3f2-4a3b-d28a-08dd11623eb9"), + RoleId = new Guid("e1101e2c-32cc-456f-9c82-4f1d1a65d141") + }, + new + { + UserId = new Guid("a8145f5f-a3a4-4f06-9461-9f24b9f23fde"), + RoleId = new Guid("f3ad41d3-ee90-4988-9195-8b2a8f4f2733") + }, + new + { + UserId = new Guid("4337a774-2c5c-4c27-d28b-08dd11623eb9"), + RoleId = new Guid("f3ad41d3-ee90-4988-9195-8b2a8f4f2733") + }, + new + { + UserId = new Guid("cb7749fb-3fff-4902-d28c-08dd11623eb9"), + RoleId = new Guid("fad1b19d-5333-4633-bd84-d67c64649f65") + }); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", "Identity"); + }); + + modelBuilder.Entity("CustomCADs.Modules.Identity.Infrastructure.Identity.ShadowEntities.AppRefreshToken", b => + { + b.HasOne("CustomCADs.Modules.Identity.Infrastructure.Identity.ShadowEntities.AppUser", "User") + .WithMany("RefreshTokens") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("CustomCADs.Modules.Identity.Infrastructure.Identity.ShadowEntities.AppRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("CustomCADs.Modules.Identity.Infrastructure.Identity.ShadowEntities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("CustomCADs.Modules.Identity.Infrastructure.Identity.ShadowEntities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("CustomCADs.Modules.Identity.Infrastructure.Identity.ShadowEntities.AppRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("CustomCADs.Modules.Identity.Infrastructure.Identity.ShadowEntities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("CustomCADs.Modules.Identity.Infrastructure.Identity.ShadowEntities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("CustomCADs.Modules.Identity.Infrastructure.Identity.ShadowEntities.AppUser", b => + { + b.Navigation("RefreshTokens"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Modules/Identity/Infrastructure/Identity/Migrations/20260506185244_Added_RefreshToken_Fingerprints.cs b/src/Modules/Identity/Infrastructure/Identity/Migrations/20260506185244_Added_RefreshToken_Fingerprints.cs new file mode 100644 index 000000000..8597b3ae9 --- /dev/null +++ b/src/Modules/Identity/Infrastructure/Identity/Migrations/20260506185244_Added_RefreshToken_Fingerprints.cs @@ -0,0 +1,42 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace CustomCADs.Modules.Identity.Infrastructure.Migrations; + +/// +public partial class Added_RefreshToken_Fingerprints : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Device", + schema: "Identity", + table: "AppRefreshToken", + type: "text", + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "Location", + schema: "Identity", + table: "AppRefreshToken", + type: "text", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Device", + schema: "Identity", + table: "AppRefreshToken"); + + migrationBuilder.DropColumn( + name: "Location", + schema: "Identity", + table: "AppRefreshToken"); + } +} diff --git a/src/Modules/Identity/Infrastructure/Migrations/IdentityContextModelSnapshot.cs b/src/Modules/Identity/Infrastructure/Identity/Migrations/IdentityContextModelSnapshot.cs similarity index 90% rename from src/Modules/Identity/Infrastructure/Migrations/IdentityContextModelSnapshot.cs rename to src/Modules/Identity/Infrastructure/Identity/Migrations/IdentityContextModelSnapshot.cs index 0f82e2b95..5fd21bc06 100644 --- a/src/Modules/Identity/Infrastructure/Migrations/IdentityContextModelSnapshot.cs +++ b/src/Modules/Identity/Infrastructure/Identity/Migrations/IdentityContextModelSnapshot.cs @@ -1,5 +1,6 @@ // using System; +using System.Collections.Generic; using CustomCADs.Modules.Identity.Infrastructure.Identity.Context; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; @@ -46,6 +47,20 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("text") .HasColumnName("Value"); + b.ComplexProperty(typeof(Dictionary), "Fingerprint", "CustomCADs.Modules.Identity.Infrastructure.Identity.ShadowEntities.AppRefreshToken.Fingerprint#Fingerprint", b1 => + { + b1.IsRequired(); + + b1.Property("Device") + .IsRequired() + .HasColumnType("text") + .HasColumnName("Device"); + + b1.Property("Location") + .HasColumnType("text") + .HasColumnName("Location"); + }); + b.HasKey("Id"); b.HasIndex("UserId"); @@ -223,20 +238,38 @@ protected override void BuildModel(ModelBuilder modelBuilder) UserName = "PDMatsaliev20" }, new + { + Id = new Guid("a8145f5f-a3a4-4f06-9461-9f24b9f23fde"), + AccessFailedCount = 0, + AccountId = new Guid("8d477999-0580-4770-8864-e9ba4bed9cd1"), + ConcurrencyStamp = "c5940d6f-d5c0-4f84-a262-da9b07525c3c", + Email = "john.cad@gmail.com", + EmailConfirmed = true, + IsSSO = false, + LockoutEnabled = true, + NormalizedEmail = "JOHN.CAD@GMAIL.COM", + NormalizedUserName = "JOHN_CAD", + PasswordHash = "AQAAAAIAAYagAAAAEEUe31maWfuZY6V8MQBzUWKerMKobDukREinVfML3Yl2z+Nr6IIQZKvX4WKqbTUw6w==", + PhoneNumberConfirmed = false, + SecurityStamp = "FNNIT3NPOZKZK2E67WFLV5R3RGVBX7LV", + TwoFactorEnabled = false, + UserName = "John_CAD" + }, + new { Id = new Guid("4337a774-2c5c-4c27-d28b-08dd11623eb9"), AccessFailedCount = 0, AccountId = new Guid("0fb3212f-7d51-4586-8fc2-0f333ec9fbc1"), - ConcurrencyStamp = "c5940d6f-d5c0-4f84-a262-da9b07525c3c", + ConcurrencyStamp = "c11d44ef-29c5-43ac-89f9-a2e7cd482ec8", Email = "boriskolev2006@gmail.com", EmailConfirmed = true, IsSSO = false, LockoutEnabled = true, NormalizedEmail = "BORISKOLEV2006@GMAIL.COM", NormalizedUserName = "ORACLE3000", - PasswordHash = "AQAAAAIAAYagAAAAEEUe31maWfuZY6V8MQBzUWKerMKobDukREinVfML3Yl2z+Nr6IIQZKvX4WKqbTUw6w==", + PasswordHash = "AQAAAAIAAYagAAAAEJGRCbKUsSh9BwxPXoIQRG1AVXYDfmWsY5vEA4aEnqBBQAzdcgLFCHUtpwd86+B4mA==", PhoneNumberConfirmed = false, - SecurityStamp = "FNNIT3NPOZKZK2E67WFLV5R3RGVBX7LV", + SecurityStamp = "MHG763AUJVOJPKKWUC64FQSF7DIVVBOU", TwoFactorEnabled = false, UserName = "Oracle3000" }, @@ -355,6 +388,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) RoleId = new Guid("e1101e2c-32cc-456f-9c82-4f1d1a65d141") }, new + { + UserId = new Guid("a8145f5f-a3a4-4f06-9461-9f24b9f23fde"), + RoleId = new Guid("f3ad41d3-ee90-4988-9195-8b2a8f4f2733") + }, + new { UserId = new Guid("4337a774-2c5c-4c27-d28b-08dd11623eb9"), RoleId = new Guid("f3ad41d3-ee90-4988-9195-8b2a8f4f2733") diff --git a/src/Modules/Identity/Infrastructure/Identity/AppRoleService.cs b/src/Modules/Identity/Infrastructure/Identity/Services/AppRoleService.cs similarity index 89% rename from src/Modules/Identity/Infrastructure/Identity/AppRoleService.cs rename to src/Modules/Identity/Infrastructure/Identity/Services/AppRoleService.cs index 76cd75723..d32f6e59d 100644 --- a/src/Modules/Identity/Infrastructure/Identity/AppRoleService.cs +++ b/src/Modules/Identity/Infrastructure/Identity/Services/AppRoleService.cs @@ -2,7 +2,7 @@ using CustomCADs.Modules.Identity.Infrastructure.Identity.ShadowEntities; using Microsoft.AspNetCore.Identity; -namespace CustomCADs.Modules.Identity.Infrastructure.Identity; +namespace CustomCADs.Modules.Identity.Infrastructure.Identity.Services; public class AppRoleService(RoleManager manager) : IRoleService { diff --git a/src/Modules/Identity/Infrastructure/Identity/Services/AppUserService.Reads.cs b/src/Modules/Identity/Infrastructure/Identity/Services/AppUserService.Reads.cs new file mode 100644 index 000000000..da4597d6d --- /dev/null +++ b/src/Modules/Identity/Infrastructure/Identity/Services/AppUserService.Reads.cs @@ -0,0 +1,113 @@ +using CustomCADs.Modules.Identity.Domain.Users; +using CustomCADs.Modules.Identity.Domain.Users.Entities; +using CustomCADs.Modules.Identity.Infrastructure.Identity.ShadowEntities; +using CustomCADs.Shared.Application.Exceptions; +using CustomCADs.Shared.Domain.TypedIds.Accounts; +using CustomCADs.Shared.Domain.TypedIds.Identity; +using Microsoft.EntityFrameworkCore; + +namespace CustomCADs.Modules.Identity.Infrastructure.Identity.Services; + +public partial class AppUserService +{ + private async Task MapToUserAsync(AppUser user) + => user.ToUser( + role: (await manager.GetRolesAsync(user).ConfigureAwait(false)).Single() + ); + + private static IQueryable QueryByUsername(IQueryable query, string username) + => query.Where(x => x.UserName == (x.IsSSO ? x.Provider + '/' + username : username)); + + private async Task<(User User, RefreshToken RefreshToken)> GetByRefreshTokenAsync(RefreshTokenId refreshTokenId) + { + AppUser appUser = await context.Users + .Include(x => x.RefreshTokens) + .FirstOrDefaultAsync(x => x.RefreshTokens.Any(x => x.Id == refreshTokenId.Value)) + .ConfigureAwait(false) + ?? throw CustomNotFoundException.ByProp(nameof(RefreshToken.Id), refreshTokenId); + + return ( + User: await MapToUserAsync(appUser).ConfigureAwait(false), + RefreshToken: appUser.RefreshTokens.First(x => x.Id == refreshTokenId.Value).ToRefreshToken() + ); + } + + #region GetUserByX + public async Task GetByAccountIdAsync(AccountId accountId) + { + AppUser appUser = await context.Users + .Include(x => x.RefreshTokens) + .FirstOrDefaultAsync(x => x.AccountId == accountId) + .ConfigureAwait(false) + ?? throw CustomNotFoundException.ByProp(nameof(User.AccountId), accountId); + + return await MapToUserAsync(appUser).ConfigureAwait(false); + } + + public async Task GetByUsernameAsync(string username) + { + AppUser appUser = await QueryByUsername(context.Users.Include(x => x.RefreshTokens), username) + .FirstOrDefaultAsync() + .ConfigureAwait(false) + ?? throw CustomNotFoundException.ByProp(nameof(username), username); + + return await MapToUserAsync(appUser).ConfigureAwait(false); + } + + public async Task GetByEmailAsync(string email) + { + AppUser appUser = await context.Users + .Include(x => x.RefreshTokens) + .FirstOrDefaultAsync(x => x.Email == email) + .ConfigureAwait(false) + ?? throw CustomNotFoundException.ByProp(nameof(email), email); + + return await MapToUserAsync(appUser).ConfigureAwait(false); + } + + public async Task<(User User, RefreshToken RefreshToken)> GetByRefreshTokenAsync(string token) + { + AppUser appUser = await context.Users + .Include(x => x.RefreshTokens) + .FirstOrDefaultAsync(x => x.RefreshTokens.Any(x => x.Value == token)) + .ConfigureAwait(false) + ?? throw CustomNotFoundException.ByProp(nameof(token), token); + + return ( + User: await MapToUserAsync(appUser).ConfigureAwait(false), + RefreshToken: appUser.RefreshTokens.First(x => x.Value == token).ToRefreshToken() + ); + } + #endregion + + #region GetX + public async Task GetExistsByUsernameAsync(string username) + => await QueryByUsername(context.Users, username).AnyAsync().ConfigureAwait(false); + + public async Task GetExistsByEmailAsync(string email) + => await context.Users + .AnyAsync(x => x.Email == email) + .ConfigureAwait(false); + + public async Task GetIsSSOByEmailAsync(string email) + => await context.Users + .AnyAsync(x => x.Email == email && x.IsSSO) + .ConfigureAwait(false); + + public async Task GetIsLockedOutAsync(string username) + { + AppUser appUser = await QueryByUsername(context.Users, username) + .FirstOrDefaultAsync() + .ConfigureAwait(false) + ?? throw CustomNotFoundException.ByProp(nameof(username), username); + + bool isLockedOut = await manager.IsLockedOutAsync(appUser).ConfigureAwait(false); + if (!isLockedOut) + { + return null; + } + + return appUser.LockoutEnd; + } + #endregion +} diff --git a/src/Modules/Identity/Infrastructure/Identity/Services/AppUserService.Writes.cs b/src/Modules/Identity/Infrastructure/Identity/Services/AppUserService.Writes.cs new file mode 100644 index 000000000..b5b5b79e8 --- /dev/null +++ b/src/Modules/Identity/Infrastructure/Identity/Services/AppUserService.Writes.cs @@ -0,0 +1,153 @@ +using CustomCADs.Modules.Identity.Domain.Users; +using CustomCADs.Modules.Identity.Domain.Users.Entities; +using CustomCADs.Modules.Identity.Infrastructure.Identity.ShadowEntities; +using CustomCADs.Shared.Application.Exceptions; +using CustomCADs.Shared.Domain.TypedIds.Accounts; +using CustomCADs.Shared.Domain.TypedIds.Identity; +using Microsoft.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; + +namespace CustomCADs.Modules.Identity.Infrastructure.Identity.Services; + +public partial class AppUserService +{ + #region Lifecycle + public async Task CreateAsync(User user, string password) + { + AppUser appUser = user.ToAppUser(); + await manager.CreateAsync(appUser, password).ConfigureAwait(false); + await AddToRoleAsync(appUser, user.Role).ConfigureAwait(false); + } + + public async Task CreateSSOAsync(User user, string provider) + { + AppUser appUser = user.ToAppUser(provider); + await manager.CreateAsync(appUser).ConfigureAwait(false); + await AddToRoleAsync(appUser, user.Role).ConfigureAwait(false); + } + + private async Task AddToRoleAsync(AppUser user, string role) + { + IdentityResult result = await manager.AddToRoleAsync(user, role).ConfigureAwait(false); + if (!result.Succeeded) + { + throw new CustomException($"Couldn't create an account for: {user.Username}."); + } + } + + public async Task DeleteAsync(AccountId id) + { + AppUser appUser = await context.Users.FirstOrDefaultAsync(x => x.AccountId == id).ConfigureAwait(false) + ?? throw CustomNotFoundException.ByProp(nameof(User.AccountId), id); + + await manager.DeleteAsync(appUser).ConfigureAwait(false); + } + #endregion + + #region Mutation + public async Task CheckPasswordAsync(string username, string password) + { + AppUser appUser = await QueryByUsername(context.Users, username).FirstOrDefaultAsync().ConfigureAwait(false) + ?? throw CustomNotFoundException.ByProp(nameof(username), username); + + bool success = await manager.CheckPasswordAsync(appUser, password).ConfigureAwait(false); + if (success) + { + await manager.ResetAccessFailedCountAsync(appUser).ConfigureAwait(false); + } + else + { + await manager.AccessFailedAsync(appUser).ConfigureAwait(false); + } + + return success; + } + + public async Task UpdateUsernameAsync(UserId id, string username) + { + AppUser appUser = await context.Users.FirstOrDefaultAsync(x => x.Id == id.Value).ConfigureAwait(false) + ?? throw CustomNotFoundException.ByProp(nameof(id), id); + + if (appUser.Username != username) appUser.Username = username; + await manager.UpdateAsync(appUser).ConfigureAwait(false); + } + + public async Task SaveRefreshTokensAsync(User user) + { + AppUser appUser = await context.Users + .Include(x => x.RefreshTokens) + .FirstOrDefaultAsync(x => x.Id == user.Id.Value) + .ConfigureAwait(false) + ?? throw CustomNotFoundException.ByProp(nameof(user.Id), user.Id); + + appUser.FillRefreshTokens([.. user.RefreshTokens.Select(x => x.ToAppRefreshToken())]); + await context.SaveChangesAsync().ConfigureAwait(false); + } + + public async Task RevokeRefreshTokenAsync(RefreshTokenId id) + { + (User User, RefreshToken RefreshToken) = await GetByRefreshTokenAsync(id).ConfigureAwait(false); + User.RemoveRefreshToken(RefreshToken); + + AppUser appUser = await context.Users.FirstOrDefaultAsync(x => x.Id == User.Id.Value).ConfigureAwait(false) + ?? throw CustomNotFoundException.ByProp(nameof(User.Id), User.Id); + + appUser.FillRefreshTokens([.. User.RefreshTokens.Select(x => x.ToAppRefreshToken())]); + await context.SaveChangesAsync().ConfigureAwait(false); + } + + public async Task RevokeRefreshTokenAsync(string token) + { + (User User, RefreshToken RefreshToken) = await GetByRefreshTokenAsync(token).ConfigureAwait(false); + User.RemoveRefreshToken(RefreshToken); + + AppUser appUser = await context.Users.FirstOrDefaultAsync(x => x.Id == User.Id.Value).ConfigureAwait(false) + ?? throw CustomNotFoundException.ByProp(nameof(User.Id), User.Id); + + appUser.FillRefreshTokens([.. User.RefreshTokens.Select(x => x.ToAppRefreshToken())]); + await context.SaveChangesAsync().ConfigureAwait(false); + } + #endregion + + #region Token Generation + public async Task GenerateEmailConfirmationTokenAsync(string username) + { + AppUser appUser = await QueryByUsername(context.Users, username).FirstOrDefaultAsync().ConfigureAwait(false) + ?? throw CustomNotFoundException.ByProp(nameof(username), username); + + return await manager.GenerateEmailConfirmationTokenAsync(appUser).ConfigureAwait(false); + } + + public async Task ConfirmEmailAsync(string username, string token) + { + AppUser appUser = await QueryByUsername(context.Users, username).FirstOrDefaultAsync().ConfigureAwait(false) + ?? throw CustomNotFoundException.ByProp(nameof(username), username); + + IdentityResult result = await manager.ConfirmEmailAsync(appUser, token).ConfigureAwait(false); + if (!result.Succeeded) + { + throw CustomAuthorizationException.Custom($"Error confirming Account: {username}'s email."); + } + } + + public async Task GeneratePasswordResetTokenAsync(string email) + { + AppUser appUser = await context.Users.FirstOrDefaultAsync(x => x.Email == email).ConfigureAwait(false) + ?? throw CustomNotFoundException.ByProp(nameof(email), email); + + return await manager.GeneratePasswordResetTokenAsync(appUser).ConfigureAwait(false); + } + + public async Task ResetPasswordAsync(string email, string token, string newPassword) + { + AppUser appUser = await context.Users.FirstOrDefaultAsync(x => x.Email == email).ConfigureAwait(false) + ?? throw CustomNotFoundException.ByProp(nameof(email), email); + + IdentityResult result = await manager.ResetPasswordAsync(appUser, token, newPassword).ConfigureAwait(false); + if (!result.Succeeded) + { + throw CustomAuthorizationException.Custom($"Failed to reset Account: {email}'s password."); + } + } + #endregion +} diff --git a/src/Modules/Identity/Infrastructure/Identity/Services/AppUserService.cs b/src/Modules/Identity/Infrastructure/Identity/Services/AppUserService.cs new file mode 100644 index 000000000..cf1dfe74b --- /dev/null +++ b/src/Modules/Identity/Infrastructure/Identity/Services/AppUserService.cs @@ -0,0 +1,10 @@ +using CustomCADs.Modules.Identity.Application.Contracts; +using CustomCADs.Modules.Identity.Infrastructure.Identity.ShadowEntities; +using Microsoft.AspNetCore.Identity; + +namespace CustomCADs.Modules.Identity.Infrastructure.Identity.Services; + +public partial class AppUserService( + UserManager manager, + Context.IdentityContext context +) : IUserService; diff --git a/src/Modules/Identity/Infrastructure/Identity/ShadowEntities/AppRefreshToken.cs b/src/Modules/Identity/Infrastructure/Identity/ShadowEntities/AppRefreshToken.cs index e161e4990..b88b1f31c 100644 --- a/src/Modules/Identity/Infrastructure/Identity/ShadowEntities/AppRefreshToken.cs +++ b/src/Modules/Identity/Infrastructure/Identity/ShadowEntities/AppRefreshToken.cs @@ -1,13 +1,14 @@ -using CustomCADs.Shared.Domain.Bases.Entities; +using CustomCADs.Modules.Identity.Domain.Users.ValueObjects; namespace CustomCADs.Modules.Identity.Infrastructure.Identity.ShadowEntities; -public class AppRefreshToken : BaseEntity +public class AppRefreshToken { public AppRefreshToken() { } - public AppRefreshToken(string value, Guid userId, DateTimeOffset issuedAt, DateTimeOffset expiresAt) + public AppRefreshToken(string value, Fingerprint fingerprint, Guid userId, DateTimeOffset issuedAt, DateTimeOffset expiresAt) { Value = value; + Fingerprint = fingerprint; UserId = userId; IssuedAt = issuedAt; ExpiresAt = expiresAt; @@ -17,6 +18,7 @@ public AppRefreshToken(string value, Guid userId, DateTimeOffset issuedAt, DateT public DateTimeOffset IssuedAt { get; init; } public DateTimeOffset ExpiresAt { get; init; } public string Value { get; private set; } = string.Empty; + public Fingerprint Fingerprint { get; private set; } = new(); public Guid UserId { get; private set; } public AppUser User { get; init; } = null!; } diff --git a/src/Modules/Identity/Infrastructure/Identity/ShadowEntities/AppUser.cs b/src/Modules/Identity/Infrastructure/Identity/ShadowEntities/AppUser.cs index 5a3e3d39a..70002927b 100644 --- a/src/Modules/Identity/Infrastructure/Identity/ShadowEntities/AppUser.cs +++ b/src/Modules/Identity/Infrastructure/Identity/ShadowEntities/AppUser.cs @@ -5,7 +5,7 @@ namespace CustomCADs.Modules.Identity.Infrastructure.Identity.ShadowEntities; public class AppUser : IdentityUser { - private List refreshTokens = []; + private readonly List refreshTokens = []; private string? provider; public AppUser() : base() { } @@ -36,7 +36,16 @@ public string Username internal AppUser FillRefreshTokens(ICollection refreshTokens) { - this.refreshTokens = [.. refreshTokens]; + Dictionary incomingById = refreshTokens.ToDictionary(x => x.Id); + + this.refreshTokens.RemoveAll(rt => !incomingById.ContainsKey(rt.Id)); + this.refreshTokens.RemoveAll(rt => !refreshTokens.Any(nt => nt.Id == rt.Id)); + foreach (var token in refreshTokens) + { + if (!this.refreshTokens.Any(rt => rt.Id == token.Id)) + this.refreshTokens.Add(token); + } + return this; } diff --git a/src/Modules/Identity/Infrastructure/Modules.Identity.Infrastructure.csproj b/src/Modules/Identity/Infrastructure/Modules.Identity.Infrastructure.csproj index fd36a7ab7..049bb52e9 100644 --- a/src/Modules/Identity/Infrastructure/Modules.Identity.Infrastructure.csproj +++ b/src/Modules/Identity/Infrastructure/Modules.Identity.Infrastructure.csproj @@ -1,6 +1,7 @@  + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Modules/Identity/Infrastructure/Tokens/IdentityTokenService.cs b/src/Modules/Identity/Infrastructure/Tokens/JwtTokenService.cs similarity index 93% rename from src/Modules/Identity/Infrastructure/Tokens/IdentityTokenService.cs rename to src/Modules/Identity/Infrastructure/Tokens/JwtTokenService.cs index 966b3fa7a..a4ace05b6 100644 --- a/src/Modules/Identity/Infrastructure/Tokens/IdentityTokenService.cs +++ b/src/Modules/Identity/Infrastructure/Tokens/JwtTokenService.cs @@ -2,7 +2,6 @@ using CustomCADs.Modules.Identity.Application.Users.Dtos; using CustomCADs.Modules.Identity.Domain.Users; using CustomCADs.Modules.Identity.Domain.Users.Entities; -using CustomCADs.Shared.Domain; using CustomCADs.Shared.Domain.TypedIds.Accounts; using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Tokens; @@ -13,9 +12,9 @@ namespace CustomCADs.Modules.Identity.Infrastructure.Tokens; -using static DomainConstants.Tokens; +using static Domain.Constants.Tokens; -public sealed class IdentityTokenService(IOptions jwtOptions) : ITokenService +public sealed class JwtTokenService(IOptions jwtOptions) : ITokenService { private const string Algorithm = SecurityAlgorithms.HmacSha256; private readonly JwtSettings jwtSettings = jwtOptions.Value; diff --git a/src/Modules/Notifications/API/Notifications/Endpoints/NotificationsGroup.cs b/src/Modules/Notifications/API/Notifications/Endpoints/NotificationsGroup.cs index 66e6e2745..3c182583e 100644 --- a/src/Modules/Notifications/API/Notifications/Endpoints/NotificationsGroup.cs +++ b/src/Modules/Notifications/API/Notifications/Endpoints/NotificationsGroup.cs @@ -1,7 +1,7 @@ namespace CustomCADs.Modules.Notifications.API.Notifications.Endpoints; using static APIConstants; -using static DomainConstants.Roles; +using static DomainConstants.Users; public class NotificationsGroup : Group { @@ -9,7 +9,7 @@ public NotificationsGroup() { Configure(Paths.Notifications, x => { - x.Roles(Customer, Contributor, Designer, Admin); + x.Roles(CustomerRole, ContributorRole, DesignerRole, AdminRole); x.Description(x => x.WithTags(Tags[Paths.Notifications])); }); } diff --git a/src/Modules/Printing/API/Materials/Endpoints/MaterialsGroup.cs b/src/Modules/Printing/API/Materials/Endpoints/MaterialsGroup.cs index 275432996..deb286946 100644 --- a/src/Modules/Printing/API/Materials/Endpoints/MaterialsGroup.cs +++ b/src/Modules/Printing/API/Materials/Endpoints/MaterialsGroup.cs @@ -1,7 +1,7 @@ namespace CustomCADs.Modules.Printing.API.Materials.Endpoints; using static APIConstants; -using static DomainConstants.Roles; +using static DomainConstants.Users; public class MaterialsGroup : Group { @@ -9,7 +9,7 @@ public MaterialsGroup() { Configure(Paths.Materials, x => { - x.Roles(Admin); + x.Roles(AdminRole); x.Description(x => x.WithTags(Tags[Paths.Materials])); }); } diff --git a/src/Modules/Printing/Application/Materials/Policies/MaterialTextureReplacePolicy.cs b/src/Modules/Printing/Application/Materials/Policies/MaterialTextureReplacePolicy.cs index 8b1088f0e..97f7b233f 100644 --- a/src/Modules/Printing/Application/Materials/Policies/MaterialTextureReplacePolicy.cs +++ b/src/Modules/Printing/Application/Materials/Policies/MaterialTextureReplacePolicy.cs @@ -16,7 +16,7 @@ public async Task EnsureReplaceGrantedAsync(IFileReplacePolicy.FileCont query: new GetUserRoleByIdQuery(context.CallerId) ).ConfigureAwait(false); - if (role is not DomainConstants.Roles.Admin) + if (role is not DomainConstants.Users.AdminRole) { throw CustomAuthorizationException.ById(context.FileId, "Texture"); } diff --git a/src/Modules/Printing/Application/Materials/Policies/MaterialTextureUploadPolicy.cs b/src/Modules/Printing/Application/Materials/Policies/MaterialTextureUploadPolicy.cs index e9c243e3b..ff6e38e17 100644 --- a/src/Modules/Printing/Application/Materials/Policies/MaterialTextureUploadPolicy.cs +++ b/src/Modules/Printing/Application/Materials/Policies/MaterialTextureUploadPolicy.cs @@ -16,7 +16,7 @@ public async Task EnsureUploadGrantedAsync(IFileUploadPolicy.FileContex query: new GetUserRoleByIdQuery(context.CallerId) ).ConfigureAwait(false); - if (role is not DomainConstants.Roles.Admin) + if (role is not DomainConstants.Users.AdminRole) { throw CustomAuthorizationException.Custom("Only Admins can upload a Material's Texture"); } diff --git a/src/Presentation/Program.cs b/src/Presentation/Program.cs index 99e661500..c9fbc91da 100644 --- a/src/Presentation/Program.cs +++ b/src/Presentation/Program.cs @@ -1,14 +1,14 @@ using CustomCADs.Modules.Identity.API; using CustomCADs.Presentation; using CustomCADs.Shared.API; -using static CustomCADs.Shared.Domain.DomainConstants.Roles; +using CustomCADs.Shared.Domain; var builder = WebApplication.CreateBuilder(args); // Neccessities builder.Services.AddCorsForClient(builder.Configuration); builder.Services.AddAuthN().AddJwt(builder.Configuration).AddSSO(builder.Configuration, APIConstants.SSO.Providers); -builder.Services.AddAuthZ(Customer, Contributor, Designer, Admin); +builder.Services.AddAuthZ(DomainConstants.Users.Roles); // Use Cases builder.Services.AddUseCases(builder.Environment); @@ -19,6 +19,7 @@ // External Services builder.Services.AddEmailService(builder.Configuration); builder.Services.AddTokensService(builder.Configuration); +builder.Services.AddFingerprintsService(); builder.Services.AddPaymentService(builder.Configuration); builder.Services.AddDeliveryService(builder.Configuration); builder.Services.AddStorageService(builder.Configuration); diff --git a/src/Presentation/ProgramExtensions.cs b/src/Presentation/ProgramExtensions.cs index eee712afa..89da98437 100644 --- a/src/Presentation/ProgramExtensions.cs +++ b/src/Presentation/ProgramExtensions.cs @@ -5,6 +5,7 @@ using CustomCADs.Shared.API; using CustomCADs.Shared.API.Extensions; using CustomCADs.Shared.Domain.TypedIds.Accounts; +using CustomCADs.Shared.Application.Abstractions.Requests.Validator; using FastEndpoints; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.JwtBearer; @@ -171,10 +172,13 @@ public IApplicationBuilder UseEndpoints() ep.AuthSchemes(AuthScheme); ep.Description(d => d.RequireRateLimiting(APIConstants.RateLimitPolicy)); }; + cfg.Endpoints.RoutePrefix = "api"; cfg.Versioning.DefaultVersion = 1; cfg.Versioning.PrependToRoute = true; - }); + + cfg.Errors.ResponseBuilder = (failures, _, _) => throw failures.ValidationException; + }).UseEmptyBodySanitization(); public IApplicationBuilder UseDisableBrowserCaching() => app.Use(async (context, next) => @@ -189,6 +193,21 @@ public IApplicationBuilder UseDisableBrowserCaching() await next().ConfigureAwait(false); }); + + private IApplicationBuilder UseEmptyBodySanitization() + { + app.Use(async (context, next) => + { + if (context.Request.IsGetWithEmptyBody) + { + context.Request.ContentType = null; + } + + await next().ConfigureAwait(false); + }); + + return app; + } } extension(IEndpointRouteBuilder router) diff --git a/src/Shared/API/Extensions/ClaimsPrincipalExtensions.cs b/src/Shared/API/Extensions/ClaimsPrincipalExtensions.cs index 15f371933..f6c63ee59 100644 --- a/src/Shared/API/Extensions/ClaimsPrincipalExtensions.cs +++ b/src/Shared/API/Extensions/ClaimsPrincipalExtensions.cs @@ -12,7 +12,7 @@ public static class ClaimsPrincipalExtensions public bool IsAuthenticated => user.Identity?.IsAuthenticated ?? false; public string? Authorization => user.FindFirstValue(ClaimTypes.Role); - public void ExtractUserFromSSO(out string email, out string username) + public void ExtractUserFromSSO(out string email, out string username, out string? firstName, out string? lastName) { email = user.Claims.FirstOrDefault(c => c.Type switch { @@ -29,6 +29,20 @@ public void ExtractUserFromSSO(out string email, out string username) "name" => true, _ => false, })?.Value ?? email.Split('@').First(); + + firstName = user.Claims.FirstOrDefault(c => c.Type switch + { + ClaimTypes.GivenName => true, + "givenname" => true, + _ => false, + })?.Value; + + lastName = user.Claims.FirstOrDefault(c => c.Type switch + { + ClaimTypes.Surname => true, + "surname" => true, + _ => false, + })?.Value; } } } diff --git a/src/Shared/API/Extensions/HttpExtensions.cs b/src/Shared/API/Extensions/HttpExtensions.cs index 017b896d5..2b544db6f 100644 --- a/src/Shared/API/Extensions/HttpExtensions.cs +++ b/src/Shared/API/Extensions/HttpExtensions.cs @@ -8,6 +8,10 @@ public static class HttpExtensions { public bool IsSignalR => request.Path.StartsWithSegments($"/{APIConstants.RequestPrefixForSignalR}"); + public bool IsGetWithEmptyBody => + HttpMethods.IsGet(request.Method) + && request is { ContentLength: null or 0 }; + public bool IsIdempotentBySpec => HttpMethods.IsGet(request.Method) || HttpMethods.IsPut(request.Method) @@ -19,6 +23,12 @@ public static class HttpExtensions || HttpMethods.IsPatch(request.Method) || HttpMethods.IsDelete(request.Method); + public Dictionary HeadersDictionary => + request.Headers.ToDictionary( + x => x.Key, + x => x.Value.FirstOrDefault() + ); + public bool TryGetIdempotencyKey(out Guid idempotencyKey, string idempotencyHeader = "Idempotency-Key") { string? header = request.Headers[idempotencyHeader]; diff --git a/src/Shared/Application/Abstractions/Requests/Validator/ValidationExtensions.cs b/src/Shared/Application/Abstractions/Requests/Validator/ValidationExtensions.cs new file mode 100644 index 000000000..64109f75a --- /dev/null +++ b/src/Shared/Application/Abstractions/Requests/Validator/ValidationExtensions.cs @@ -0,0 +1,31 @@ +namespace CustomCADs.Shared.Application.Abstractions.Requests.Validator; + +using Shared.Domain; + +public static class ValidationExtensions +{ + extension(IEnumerable failures) + { + public FluentValidation.ValidationException ValidationException => new( + message: string.Join( + separator: ".\n", + values: failures + .Select(f => + { + f.PropertyName = f.PropertyName.AsCapitalized(); + f.ErrorMessage = f.ErrorMessage.AsCapitalized(); + return f; + }) + .GroupBy(f => f.PropertyName) + .ToDictionary( + f => f.Key, + f => string.Join( + separator: "; ", + values: f.Select((x) => x.ErrorMessage).Distinct() + ).Trim() + ) + .Select(e => string.IsNullOrWhiteSpace(e.Key) ? e.Value : $"{e.Key}: {e.Value}") + ) + '.' + ); + } +} diff --git a/src/Shared/Application/Events/Account/Accounts/AccountDeletedApplicationEvent.cs b/src/Shared/Application/Events/Account/Accounts/AccountDeletedApplicationEvent.cs index 988e78b68..28efee03d 100644 --- a/src/Shared/Application/Events/Account/Accounts/AccountDeletedApplicationEvent.cs +++ b/src/Shared/Application/Events/Account/Accounts/AccountDeletedApplicationEvent.cs @@ -1,5 +1,5 @@ namespace CustomCADs.Shared.Application.Events.Account.Accounts; public record AccountDeletedApplicationEvent( - string Username + AccountId Id ) : BaseApplicationEvent; diff --git a/src/Shared/Application/Events/Catalog/UserViewedProductApplicationEvent.cs b/src/Shared/Application/Events/Catalog/UserViewedProductApplicationEvent.cs index 0b5712782..02485e742 100644 --- a/src/Shared/Application/Events/Catalog/UserViewedProductApplicationEvent.cs +++ b/src/Shared/Application/Events/Catalog/UserViewedProductApplicationEvent.cs @@ -2,5 +2,6 @@ public record UserViewedProductApplicationEvent( AccountId AccountId, - ProductId Id + ProductId Id, + DateTimeOffset ViewedAt ) : BaseApplicationEvent; diff --git a/src/Shared/Application/Events/Identity/UserEditedApplicationEvent.cs b/src/Shared/Application/Events/Identity/UserEditedApplicationEvent.cs index ef7ef942e..8dad9bd5b 100644 --- a/src/Shared/Application/Events/Identity/UserEditedApplicationEvent.cs +++ b/src/Shared/Application/Events/Identity/UserEditedApplicationEvent.cs @@ -2,6 +2,9 @@ namespace CustomCADs.Shared.Application.Events.Identity; public record UserEditedApplicationEvent( AccountId Id, + NamesDto? Names = null, string? Username = null, bool? TrackViewedProducts = null ) : BaseApplicationEvent; + +public record NamesDto(string? FirstName = null, string? LastName = null); diff --git a/src/Shared/Application/UseCases/Accounts/Commands/DeleteViewedProductCommand.cs b/src/Shared/Application/UseCases/Accounts/Commands/DeleteViewedProductCommand.cs new file mode 100644 index 000000000..604a0c440 --- /dev/null +++ b/src/Shared/Application/UseCases/Accounts/Commands/DeleteViewedProductCommand.cs @@ -0,0 +1,6 @@ +namespace CustomCADs.Shared.Application.UseCases.Accounts.Commands; + +public record DeleteViewedProductCommand( + ProductId ProductId, + AccountId CallerId +) : ICommand; diff --git a/src/Shared/Application/UseCases/Accounts/Queries/GetAccountExistsByUsernameQuery.cs b/src/Shared/Application/UseCases/Accounts/Queries/GetAccountExistsByUsernameQuery.cs new file mode 100644 index 000000000..0d4e8c566 --- /dev/null +++ b/src/Shared/Application/UseCases/Accounts/Queries/GetAccountExistsByUsernameQuery.cs @@ -0,0 +1,3 @@ +namespace CustomCADs.Shared.Application.UseCases.Accounts.Queries; + +public sealed record GetAccountExistsByUsernameQuery(string Username) : IQuery; diff --git a/src/Shared/Application/UseCases/Accounts/Queries/GetAccountInfoByUsernameQuery.cs b/src/Shared/Application/UseCases/Accounts/Queries/GetAccountInfoByUsernameQuery.cs index c7c2aa922..23510bf65 100644 --- a/src/Shared/Application/UseCases/Accounts/Queries/GetAccountInfoByUsernameQuery.cs +++ b/src/Shared/Application/UseCases/Accounts/Queries/GetAccountInfoByUsernameQuery.cs @@ -5,6 +5,7 @@ string Username ) : IQuery; public sealed record AccountInfoDto( + AccountId Id, DateTimeOffset CreatedAt, bool TrackViewedProducts, string? FirstName, diff --git a/src/Shared/Application/UseCases/Accounts/Queries/GetAccountViewedProductsByUsernameQuery.cs b/src/Shared/Application/UseCases/Accounts/Queries/GetAccountViewedProductsByUsernameQuery.cs index 83804c413..5a61180df 100644 --- a/src/Shared/Application/UseCases/Accounts/Queries/GetAccountViewedProductsByUsernameQuery.cs +++ b/src/Shared/Application/UseCases/Accounts/Queries/GetAccountViewedProductsByUsernameQuery.cs @@ -2,4 +2,9 @@ namespace CustomCADs.Shared.Application.UseCases.Accounts.Queries; public sealed record GetAccountViewedProductsByUsernameQuery( string Username -) : IQuery; +) : IQuery; + +public sealed record ViewedProductDto( + ProductId Id, + DateTimeOffset ViewedAt +); diff --git a/src/Shared/Domain/Bases/Entities/ISoftDeletable.cs b/src/Shared/Domain/Bases/Entities/ISoftDeletable.cs new file mode 100644 index 000000000..5aa48c817 --- /dev/null +++ b/src/Shared/Domain/Bases/Entities/ISoftDeletable.cs @@ -0,0 +1,8 @@ +namespace CustomCADs.Shared.Domain.Bases.Entities; + +public interface ISoftDeletable where TEntity : BaseEntity +{ + bool IsDeleted { get; } + DateTimeOffset? DeletedAt { get; } + TEntity Delete(); +} diff --git a/src/Shared/Domain/DomainConstants.cs b/src/Shared/Domain/DomainConstants.cs index fc966fa50..8d69ef0cc 100644 --- a/src/Shared/Domain/DomainConstants.cs +++ b/src/Shared/Domain/DomainConstants.cs @@ -1,4 +1,5 @@ -using CustomCADs.Shared.Domain.TypedIds.Catalog; +using CustomCADs.Shared.Domain.TypedIds.Accounts; +using CustomCADs.Shared.Domain.TypedIds.Catalog; using CustomCADs.Shared.Domain.TypedIds.Files; using System.Text.RegularExpressions; @@ -40,51 +41,30 @@ public static class Textures public static readonly ImageId Wood = ImageId.New(Guid.Parse("3fe2472c-d2c6-434c-a013-ef117319bed3")); } - public static class Roles - { - public const string CustomerId = "762ddec2-25c9-4183-9891-72a19d84a839"; - public const string ContributorId = "e1101e2c-32cc-456f-9c82-4f1d1a65d141"; - public const string DesignerId = "f3ad41d3-ee90-4988-9195-8b2a8f4f2733"; - public const string AdminId = "fad1b19d-5333-4633-bd84-d67c64649f65"; - - public const string Customer = "Customer"; - public const string Contributor = "Contributor"; - public const string Designer = "Designer"; - public const string Admin = "Administrator"; - - public const string CustomerDescription = "Can buy Products from the Gallery as Cart Items; Can request Customs from our Designers and contact them; Can download purchased CADs and track requested Shipments."; - public const string ContributorDescription = "Can upload 3D Models to the Gallery as Products; Can sell CADs to our Designers and contact them; Can apply to become a Designer himself."; - public const string DesignerDescription = "Can accept and work on Customers' Customs; Can validate or report Contributors' Products; Can do everything a Contributor can do."; - public const string AdminDescription = "Can access all non-sensitive info from all resources; Can ban reported resources - Customs, Products, Users, ...; Can modify Categories and Roles."; - } - public static class Users { - public const string CustomerUserId = "e38c495f-b1f3-4226-d289-08dd11623eb9"; - public const string ContributorUserId = "af840410-f3f2-4a3b-d28a-08dd11623eb9"; - public const string DesignerUserId = "4337a774-2c5c-4c27-d28b-08dd11623eb9"; - public const string AdminUserId = "cb7749fb-3fff-4902-d28c-08dd11623eb9"; + public static readonly AccountId CustomerAccountId = AccountId.New("2da61b05-1a27-4af9-9df2-be4f1f4e835f"); + public static readonly AccountId ContributorAccountId = AccountId.New("6d963818-23dc-4e9a-aaa8-b4c77252bc97"); + public static readonly AccountId DesignerAccountId = AccountId.New("8d477999-0580-4770-8864-e9ba4bed9cd1"); + public static readonly AccountId HeadDesignerAccountId = AccountId.New("0fb3212f-7d51-4586-8fc2-0f333ec9fbc1"); + public static readonly AccountId AdminAccountId = AccountId.New("e995039c-a535-4f20-8288-7aadcb71b252"); - public const string CustomerAccountId = "2da61b05-1a27-4af9-9df2-be4f1f4e835f"; - public const string ContributorAccountId = "6d963818-23dc-4e9a-aaa8-b4c77252bc97"; - public const string DesignerAccountId = "0fb3212f-7d51-4586-8fc2-0f333ec9fbc1"; - public const string AdminAccountId = "e995039c-a535-4f20-8288-7aadcb71b252"; + public static readonly string[] Roles = [CustomerRole, ContributorRole, DesignerRole, AdminRole]; + public const string CustomerRole = "Customer"; + public const string ContributorRole = "Contributor"; + public const string DesignerRole = "Designer"; + public const string AdminRole = "Administrator"; public const string CustomerUsername = "For7a7a"; public const string ContributorUsername = "PDMatsaliev20"; - public const string DesignerUsername = "Oracle3000"; + public const string DesignerUsername = "John_CAD"; + public const string HeadDesignerUsername = "Oracle3000"; public const string AdminUsername = "NinjataBG"; public const string CustomerEmail = "ivanzlatinov006@gmail.com"; public const string ContributorEmail = "PDMatsaliev20@codingburgas.bg"; - public const string DesignerEmail = "boriskolev2006@gmail.com"; + public const string DesignerEmail = "john.cad@gmail.com"; + public const string HeadDesignerEmail = "boriskolev2006@gmail.com"; public const string AdminEmail = "ivanangelov414@gmail.com"; } - - public static class Tokens - { - public const int JwtDurationInMinutes = 15; - public const int RtDurationInDays = 7; - public const int LongerRtDurationInDays = 15; - } } diff --git a/src/Shared/Domain/StringExtensions.cs b/src/Shared/Domain/StringExtensions.cs new file mode 100644 index 000000000..7e33f1b09 --- /dev/null +++ b/src/Shared/Domain/StringExtensions.cs @@ -0,0 +1,33 @@ +namespace CustomCADs.Shared.Domain; + +public static class StringExtensions +{ + extension(string value) + { + public string AsCapitalized(bool allUppercase = false, bool dismantlePascal = true) + { + if (value.Length == 0) return value; + + System.Text.StringBuilder result = new(value); + result[0] = char.ToUpper(result[0]); + + if (dismantlePascal) for (int i = 1; i < result.Length; ++i) + { + if (char.IsLower(result[i - 1]) && char.IsUpper(result[i])) + { + result.Insert(i, ' '); + } + } + + if (allUppercase) for (int i = 1; i < result.Length; ++i) + { + if (char.IsWhiteSpace(result[i - 1]) && !char.IsWhiteSpace(result[i])) + { + result[i] = char.ToUpper(result[i]); + } + } + + return result.ToString(); + } + } +} diff --git a/src/Shared/Infrastructure/DependencyInjection.cs b/src/Shared/Infrastructure/DependencyInjection.cs index 77785580b..3beb0500c 100644 --- a/src/Shared/Infrastructure/DependencyInjection.cs +++ b/src/Shared/Infrastructure/DependencyInjection.cs @@ -65,6 +65,7 @@ public void AddMessagingServices(bool codeGen, Assembly entry, params Assembly[] } cfg.UseFluentValidation(); + cfg.Services.AddSingleton(typeof(IFailureAction<>), typeof(CustomFailureAction<>)); }); services.AddScoped(); diff --git a/src/Shared/Infrastructure/Requests/CustomFailureAction.cs b/src/Shared/Infrastructure/Requests/CustomFailureAction.cs new file mode 100644 index 000000000..84518cc43 --- /dev/null +++ b/src/Shared/Infrastructure/Requests/CustomFailureAction.cs @@ -0,0 +1,11 @@ +using CustomCADs.Shared.Application.Abstractions.Requests.Validator; +using FluentValidation.Results; +using Wolverine.FluentValidation; + +namespace CustomCADs.Shared.Infrastructure.Requests; + +public class CustomFailureAction : IFailureAction +{ + public void Throw(T message, IReadOnlyList failures) + => throw failures.ValidationException; +} diff --git a/src/Tools/CodeGen/ProgramExtensions.cs b/src/Tools/CodeGen/ProgramExtensions.cs index 82dd774d1..41fbf7344 100644 --- a/src/Tools/CodeGen/ProgramExtensions.cs +++ b/src/Tools/CodeGen/ProgramExtensions.cs @@ -135,7 +135,7 @@ public IServiceCollection AddIdentity(IConfiguration config) options.Password.RequireUppercase = false; options.Password.RequiredLength = PasswordMinLength; options.User.RequireUniqueEmail = true; - options.User.AllowedUserNameCharacters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+" + ' '; // default + space + options.User.AllowedUserNameCharacters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+" + ' ' + '/'; // default + space + '/' options.Lockout.MaxFailedAccessAttempts = 5; options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(5); }) diff --git a/src/Tools/Identity/ProgramExtensions.cs b/src/Tools/Identity/ProgramExtensions.cs index 84633e9d7..9330cd443 100644 --- a/src/Tools/Identity/ProgramExtensions.cs +++ b/src/Tools/Identity/ProgramExtensions.cs @@ -32,7 +32,7 @@ public IServiceCollection AddIdentity(IConfiguration config) options.Password.RequireUppercase = false; options.Password.RequiredLength = PasswordMinLength; options.User.RequireUniqueEmail = true; - options.User.AllowedUserNameCharacters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+" + ' '; // default + space + options.User.AllowedUserNameCharacters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+" + ' ' + '/'; // default + space + '/' options.Lockout.MaxFailedAccessAttempts = 5; options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(5); }) diff --git a/tests/UnitTests/Accounts/Application/Accounts/Commands/Internal/Create/CreateAccountHandlerUnitTests.cs b/tests/UnitTests/Accounts/Application/Accounts/Commands/Internal/Create/CreateAccountHandlerUnitTests.cs index bde977271..29a13a445 100644 --- a/tests/UnitTests/Accounts/Application/Accounts/Commands/Internal/Create/CreateAccountHandlerUnitTests.cs +++ b/tests/UnitTests/Accounts/Application/Accounts/Commands/Internal/Create/CreateAccountHandlerUnitTests.cs @@ -8,7 +8,6 @@ namespace CustomCADs.UnitTests.Accounts.Application.Accounts.Commands.Internal.Create; using static AccountsData; -using static DomainConstants; public class CreateAccountHandlerUnitTests : AccountsBaseUnitTests { @@ -23,7 +22,7 @@ public CreateAccountHandlerUnitTests() writes.Setup(x => x.AddAsync( It.Is(x => - x.RoleName == Roles.Customer + x.RoleName == ValidRole && x.Username == ValidUsername && x.Email == ValidEmail1 && x.FirstName == ValidFirstName @@ -38,7 +37,7 @@ public async Task Handle_ShouldPersistToDatabase() { // Arrange CreateAccountCommand command = new( - Role: Roles.Customer, + Role: ValidRole, Username: ValidUsername, Email: ValidEmail1, Password: ValidPassword, @@ -52,7 +51,7 @@ public async Task Handle_ShouldPersistToDatabase() // Assert writes.Verify(x => x.AddAsync( It.Is(x => - x.RoleName == Roles.Customer + x.RoleName == ValidRole && x.Username == ValidUsername && x.Email == ValidEmail1 && x.FirstName == ValidFirstName @@ -68,7 +67,7 @@ public async Task Handle_ShouldRaiseEvents() { // Arrange CreateAccountCommand command = new( - Role: Roles.Customer, + Role: ValidRole, Username: ValidUsername, Email: ValidEmail1, Password: ValidPassword, @@ -95,7 +94,7 @@ public async Task Handle_ShouldReturnResult() { // Arrange CreateAccountCommand command = new( - Role: Roles.Customer, + Role: ValidRole, Username: ValidUsername, Email: ValidEmail1, Password: ValidPassword, diff --git a/tests/UnitTests/Accounts/Application/Accounts/Commands/Internal/Delete/DeleteAccountHandlerUnitTests.cs b/tests/UnitTests/Accounts/Application/Accounts/Commands/Internal/Delete/DeleteAccountHandlerUnitTests.cs index 3ae1d6711..3d902cb21 100644 --- a/tests/UnitTests/Accounts/Application/Accounts/Commands/Internal/Delete/DeleteAccountHandlerUnitTests.cs +++ b/tests/UnitTests/Accounts/Application/Accounts/Commands/Internal/Delete/DeleteAccountHandlerUnitTests.cs @@ -64,7 +64,7 @@ public async Task Handle_ShouldRaiseEvents() // Assert raiser.Verify(x => x.RaiseApplicationEventAsync( - It.Is(x => x.Username == ValidUsername) + It.Is(x => x.Id == ValidId) ), Times.Once()); } diff --git a/tests/UnitTests/Accounts/Application/Accounts/Commands/Shared/Create/CreateAccountHandlerUnitTests.cs b/tests/UnitTests/Accounts/Application/Accounts/Commands/Shared/Create/CreateAccountHandlerUnitTests.cs index 932c27f4f..b10fd1526 100644 --- a/tests/UnitTests/Accounts/Application/Accounts/Commands/Shared/Create/CreateAccountHandlerUnitTests.cs +++ b/tests/UnitTests/Accounts/Application/Accounts/Commands/Shared/Create/CreateAccountHandlerUnitTests.cs @@ -4,10 +4,10 @@ using CustomCADs.Shared.Application.UseCases.Accounts.Commands; using CustomCADs.Shared.Domain.TypedIds.Accounts; -namespace CustomCADs.UnitTests.Accounts.Application.Accounts.Commands.Shared.Create; +using static CustomCADs.UnitTests.Accounts.Data.AccountsData; +using static CustomCADs.Shared.Domain.DomainConstants; -using static AccountsData; -using static DomainConstants; +namespace CustomCADs.UnitTests.Accounts.Application.Accounts.Commands.Shared.Create; public class CreateAccountHandlerUnitTests : AccountsBaseUnitTests { @@ -21,7 +21,7 @@ public CreateAccountHandlerUnitTests() writes.Setup(x => x.AddAsync( It.Is(x => - x.RoleName == Roles.Customer + x.RoleName == ValidRole && x.Username == ValidUsername && x.Email == ValidEmail1 && x.FirstName == ValidFirstName @@ -36,7 +36,7 @@ public async Task Handle_ShouldPersistToDatabase() { // Arrange CreateAccountCommand command = new( - Role: Roles.Customer, + Role: ValidRole, Username: ValidUsername, Email: ValidEmail1, FirstName: ValidFirstName, @@ -49,7 +49,7 @@ public async Task Handle_ShouldPersistToDatabase() // Assert writes.Verify(x => x.AddAsync( It.Is(x => - x.RoleName == Roles.Customer + x.RoleName == ValidRole && x.Username == ValidUsername && x.Email == ValidEmail1 && x.FirstName == ValidFirstName @@ -65,7 +65,7 @@ public async Task Handle_ShouldReturnResult() { // Arrange CreateAccountCommand command = new( - Role: Roles.Customer, + Role: ValidRole, Username: ValidUsername, Email: ValidEmail1, FirstName: ValidFirstName, diff --git a/tests/UnitTests/Accounts/Application/Accounts/Events/Application/UserViewedProductHandlerUnitTests.cs b/tests/UnitTests/Accounts/Application/Accounts/Events/Application/UserViewedProductHandlerUnitTests.cs index 37f4c79b5..2745f7543 100644 --- a/tests/UnitTests/Accounts/Application/Accounts/Events/Application/UserViewedProductHandlerUnitTests.cs +++ b/tests/UnitTests/Accounts/Application/Accounts/Events/Application/UserViewedProductHandlerUnitTests.cs @@ -15,6 +15,7 @@ public class UserViewedProductHandlerUnitTests : AccountsBaseUnitTests private static readonly AccountId id = AccountId.New(); private static readonly ProductId productId = ProductId.New(); + private static readonly DateTimeOffset viewedAt = DateTimeOffset.UtcNow; public UserViewedProductHandlerUnitTests() { @@ -25,13 +26,13 @@ public UserViewedProductHandlerUnitTests() public async Task Handle_ShouldPersistToDatabase() { // Arrange - UserViewedProductApplicationEvent ie = new(id, productId); + UserViewedProductApplicationEvent ie = new(id, productId, viewedAt); // Act await handler.HandleAsync(ie); // Assert - writes.Verify(x => x.ViewProductAsync(id, productId, ct), Times.Once()); + writes.Verify(x => x.ViewProductAsync(id, productId, viewedAt, ct), Times.Once()); uow.Verify(x => x.SaveChangesAsync(ct), Times.Once()); } } diff --git a/tests/UnitTests/Accounts/Application/Accounts/Queries/Internal/GetAll/GetAllAccountsHandlerUnitTests.cs b/tests/UnitTests/Accounts/Application/Accounts/Queries/Internal/GetAll/GetAllAccountsHandlerUnitTests.cs index 2f685f6c0..6eb9987f7 100644 --- a/tests/UnitTests/Accounts/Application/Accounts/Queries/Internal/GetAll/GetAllAccountsHandlerUnitTests.cs +++ b/tests/UnitTests/Accounts/Application/Accounts/Queries/Internal/GetAll/GetAllAccountsHandlerUnitTests.cs @@ -5,19 +5,16 @@ namespace CustomCADs.UnitTests.Accounts.Application.Accounts.Queries.Internal.GetAll; -using static DomainConstants; -using static DomainConstants.Users; - public class GetAllAccountsHandlerUnitTests : AccountsBaseUnitTests { private readonly GetAllAccountsHandler handler; private readonly Mock reads = new(); private readonly Account[] accounts = [ - Account.CreateWithId(AccountId.New(), Roles.Customer, CustomerUsername, CustomerEmail, DateTimeOffset.UtcNow), - Account.CreateWithId(AccountId.New(), Roles.Contributor, ContributorUsername, ContributorEmail, DateTimeOffset.UtcNow), - Account.CreateWithId(AccountId.New(), Roles.Designer, DesignerUsername, DesignerEmail, DateTimeOffset.UtcNow), - Account.CreateWithId(AccountId.New(), Roles.Admin, AdminUsername, AdminEmail, DateTimeOffset.UtcNow), + CreateAccountWithId(id: AccountId.New()), + CreateAccountWithId(id: AccountId.New()), + CreateAccountWithId(id: AccountId.New()), + CreateAccountWithId(id: AccountId.New()), ]; private readonly AccountQuery query = new(Pagination: new(1, 1)); diff --git a/tests/UnitTests/Accounts/Application/Accounts/Queries/Shared/GetExists/GetAccountExistsByUsernameHandlerUnitTests.cs b/tests/UnitTests/Accounts/Application/Accounts/Queries/Shared/GetExists/GetAccountExistsByUsernameHandlerUnitTests.cs new file mode 100644 index 000000000..a9f3a32f0 --- /dev/null +++ b/tests/UnitTests/Accounts/Application/Accounts/Queries/Shared/GetExists/GetAccountExistsByUsernameHandlerUnitTests.cs @@ -0,0 +1,47 @@ +using CustomCADs.Modules.Accounts.Application.Accounts.Queries.Shared.Exists; +using CustomCADs.Modules.Accounts.Domain.Repositories.Reads; +using CustomCADs.Shared.Application.UseCases.Accounts.Queries; + +namespace CustomCADs.UnitTests.Accounts.Application.Accounts.Queries.Shared.GetExists; + +using static AccountsData; + +public class GetAccountExistsByUsernameHandlerUnitTests : AccountsBaseUnitTests +{ + private readonly GetAccountExistsByUsernameHandler handler; + private readonly Mock reads = new(); + + public GetAccountExistsByUsernameHandlerUnitTests() + { + handler = new(reads.Object); + } + + [Fact] + public async Task Handle_ShouldQueryDatabase() + { + // Arrange + GetAccountExistsByUsernameQuery query = new(ValidUsername); + + // Act + await handler.Handle(query, ct); + + // Assert + reads.Verify(x => x.ExistsByUsernameAsync(ValidUsername, ct), Times.Once()); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task Handle_ShouldReturnResult(bool exists) + { + // Arrange + reads.Setup(x => x.ExistsByUsernameAsync(ValidUsername, ct)).ReturnsAsync(exists); + GetAccountExistsByUsernameQuery query = new(ValidUsername); + + // Act + bool result = await handler.Handle(query, ct); + + // Assert + Assert.Equal(exists, result); + } +} diff --git a/tests/UnitTests/Accounts/Application/Accounts/Queries/Shared/GetInfo/GetAccountInfoByUsernameHandlerUnitTests.cs b/tests/UnitTests/Accounts/Application/Accounts/Queries/Shared/GetInfo/GetAccountInfoByUsernameHandlerUnitTests.cs index e67832132..8bf252c0f 100644 --- a/tests/UnitTests/Accounts/Application/Accounts/Queries/Shared/GetInfo/GetAccountInfoByUsernameHandlerUnitTests.cs +++ b/tests/UnitTests/Accounts/Application/Accounts/Queries/Shared/GetInfo/GetAccountInfoByUsernameHandlerUnitTests.cs @@ -45,6 +45,7 @@ public async Task Handle_ShouldReturnResult() // Assert Assert.Multiple( + () => Assert.Equal(account.Id, info.Id), () => Assert.Equal(account.CreatedAt, info.CreatedAt), () => Assert.Equal(account.TrackViewedProducts, info.TrackViewedProducts), () => Assert.Equal(account.FirstName, info.FirstName), diff --git a/tests/UnitTests/Accounts/Application/Accounts/Queries/Shared/GetUsernames/GetUsernamesByIdsHandlerUnitTests.cs b/tests/UnitTests/Accounts/Application/Accounts/Queries/Shared/GetUsernames/GetUsernamesByIdsHandlerUnitTests.cs index f6a2a7495..f94f9cb7e 100644 --- a/tests/UnitTests/Accounts/Application/Accounts/Queries/Shared/GetUsernames/GetUsernamesByIdsHandlerUnitTests.cs +++ b/tests/UnitTests/Accounts/Application/Accounts/Queries/Shared/GetUsernames/GetUsernamesByIdsHandlerUnitTests.cs @@ -7,7 +7,6 @@ namespace CustomCADs.UnitTests.Accounts.Application.Accounts.Queries.Shared.GetUsernames; using static AccountsData; -using static DomainConstants.Roles; using static DomainConstants.Users; public class GetUsernamesByIdsHandlerUnitTests : AccountsBaseUnitTests @@ -16,7 +15,7 @@ public class GetUsernamesByIdsHandlerUnitTests : AccountsBaseUnitTests private readonly Mock reads = new(); private static readonly AccountId[] ids = [ValidId, ValidId, ValidId, ValidId]; - private static readonly string[] usernames = [CustomerUsername, ContributorUsername, DesignerUsername, AdminUsername]; + private static readonly string[] usernames = [CustomerUsername, ContributorUsername, DesignerUsername, HeadDesignerUsername, AdminUsername]; private static readonly AccountQuery accountQuery = new(Pagination: new(1, ids.Length), Ids: ids); public GetUsernamesByIdsHandlerUnitTests() @@ -26,10 +25,11 @@ public GetUsernamesByIdsHandlerUnitTests() reads.Setup(x => x.AllAsync(accountQuery, false, ct)).ReturnsAsync(new Result( Count: ids.Length, Items: [ - CreateAccountWithId(AccountId.New(), Customer, CustomerUsername), - CreateAccountWithId(AccountId.New(), Contributor, ContributorUsername), - CreateAccountWithId(AccountId.New(), Designer, DesignerUsername), - CreateAccountWithId(AccountId.New(), Admin, AdminUsername), + CreateAccountWithId(id: AccountId.New(), username: CustomerUsername), + CreateAccountWithId(id: AccountId.New(), username: ContributorUsername), + CreateAccountWithId(id: AccountId.New(), username: DesignerUsername), + CreateAccountWithId(id: AccountId.New(), username: HeadDesignerUsername), + CreateAccountWithId(id: AccountId.New(), username: AdminUsername), ] )); } diff --git a/tests/UnitTests/Accounts/Application/Accounts/Queries/Shared/ViewedProduct/GetAccountViewedProductHandlerUnitTests.cs b/tests/UnitTests/Accounts/Application/Accounts/Queries/Shared/ViewedProducts/GetAccountViewedProductHandlerUnitTests.cs similarity index 96% rename from tests/UnitTests/Accounts/Application/Accounts/Queries/Shared/ViewedProduct/GetAccountViewedProductHandlerUnitTests.cs rename to tests/UnitTests/Accounts/Application/Accounts/Queries/Shared/ViewedProducts/GetAccountViewedProductHandlerUnitTests.cs index 463c07072..762d49449 100644 --- a/tests/UnitTests/Accounts/Application/Accounts/Queries/Shared/ViewedProduct/GetAccountViewedProductHandlerUnitTests.cs +++ b/tests/UnitTests/Accounts/Application/Accounts/Queries/Shared/ViewedProducts/GetAccountViewedProductHandlerUnitTests.cs @@ -1,9 +1,9 @@ -using CustomCADs.Modules.Accounts.Application.Accounts.Queries.Shared.ViewedProduct; +using CustomCADs.Modules.Accounts.Application.Accounts.Queries.Shared.ViewedProducts; using CustomCADs.Modules.Accounts.Domain.Repositories.Reads; using CustomCADs.Shared.Application.UseCases.Accounts.Queries; using CustomCADs.Shared.Domain.TypedIds.Catalog; -namespace CustomCADs.UnitTests.Accounts.Application.Accounts.Queries.Shared.ViewedProduct; +namespace CustomCADs.UnitTests.Accounts.Application.Accounts.Queries.Shared.ViewedProducts; using static AccountsData; diff --git a/tests/UnitTests/Accounts/Application/Accounts/Queries/Shared/ViewedProduct/GetAccountViewedProductsByUsernameHandlerUnitTests.cs b/tests/UnitTests/Accounts/Application/Accounts/Queries/Shared/ViewedProducts/GetAccountViewedProductsByUsernameHandlerUnitTests.cs similarity index 78% rename from tests/UnitTests/Accounts/Application/Accounts/Queries/Shared/ViewedProduct/GetAccountViewedProductsByUsernameHandlerUnitTests.cs rename to tests/UnitTests/Accounts/Application/Accounts/Queries/Shared/ViewedProducts/GetAccountViewedProductsByUsernameHandlerUnitTests.cs index 1211a6270..e1d97783a 100644 --- a/tests/UnitTests/Accounts/Application/Accounts/Queries/Shared/ViewedProduct/GetAccountViewedProductsByUsernameHandlerUnitTests.cs +++ b/tests/UnitTests/Accounts/Application/Accounts/Queries/Shared/ViewedProducts/GetAccountViewedProductsByUsernameHandlerUnitTests.cs @@ -1,9 +1,9 @@ -using CustomCADs.Modules.Accounts.Application.Accounts.Queries.Shared.ViewedProduct; +using CustomCADs.Modules.Accounts.Application.Accounts.Queries.Shared.ViewedProducts; +using CustomCADs.Modules.Accounts.Domain.Accounts.Entities; using CustomCADs.Modules.Accounts.Domain.Repositories.Reads; using CustomCADs.Shared.Application.UseCases.Accounts.Queries; -using CustomCADs.Shared.Domain.TypedIds.Catalog; -namespace CustomCADs.UnitTests.Accounts.Application.Accounts.Queries.Shared.ViewedProduct; +namespace CustomCADs.UnitTests.Accounts.Application.Accounts.Queries.Shared.ViewedProducts; using static AccountsData; @@ -12,7 +12,7 @@ public class GetAccountViewedProductsByUsernameHandlerUnitTests : AccountsBaseUn private readonly GetAccountViewedProductsByUsernameHandler handler; private readonly Mock reads = new(); - private static readonly ProductId[] expected = []; + private static readonly ViewedProduct[] expected = []; public GetAccountViewedProductsByUsernameHandlerUnitTests() { @@ -41,9 +41,9 @@ public async Task Handle_ShouldReturnResult() GetAccountViewedProductsByUsernameQuery query = new(ValidUsername); // Act - ProductId[] ids = await handler.Handle(query, ct); + ViewedProductDto[] products = await handler.Handle(query, ct); // Assert - Assert.Equal(expected, ids); + Assert.Equal(expected.Select(x => x.ProductId), products.Select(x => x.Id)); } } diff --git a/tests/UnitTests/Accounts/Application/Roles/Queries/Internal/GetAll/GetAllRolesHandlerUnitTests.cs b/tests/UnitTests/Accounts/Application/Roles/Queries/Internal/GetAll/GetAllRolesHandlerUnitTests.cs index 5ab563f3b..081c1afe9 100644 --- a/tests/UnitTests/Accounts/Application/Roles/Queries/Internal/GetAll/GetAllRolesHandlerUnitTests.cs +++ b/tests/UnitTests/Accounts/Application/Roles/Queries/Internal/GetAll/GetAllRolesHandlerUnitTests.cs @@ -5,8 +5,6 @@ namespace CustomCADs.UnitTests.Accounts.Application.Roles.Queries.Internal.GetAll; -using static DomainConstants.Roles; - public class GetAllRolesHandlerUnitTests : RolesBaseUnitTests { private readonly GetAllRolesHandler handler; @@ -14,10 +12,10 @@ public class GetAllRolesHandlerUnitTests : RolesBaseUnitTests private readonly Mock> cache = new(); private readonly Role[] roles = [ - Role.CreateWithId(RoleId.New(), Customer, CustomerDescription), - Role.CreateWithId(RoleId.New(), Contributor, ContributorDescription), - Role.CreateWithId(RoleId.New(), Designer, DesignerDescription), - Role.CreateWithId(RoleId.New(), Admin, AdminDescription), + CreateRoleWithId(), + CreateRoleWithId(), + CreateRoleWithId(), + CreateRoleWithId(), ]; public GetAllRolesHandlerUnitTests() diff --git a/tests/UnitTests/Accounts/Application/Roles/RolesBaseUnitTests.cs b/tests/UnitTests/Accounts/Application/Roles/RolesBaseUnitTests.cs index fda56f06e..3c43c5b22 100644 --- a/tests/UnitTests/Accounts/Application/Roles/RolesBaseUnitTests.cs +++ b/tests/UnitTests/Accounts/Application/Roles/RolesBaseUnitTests.cs @@ -8,8 +8,8 @@ public class RolesBaseUnitTests public static readonly CancellationToken ct = CancellationToken.None; protected static Role CreateRole(string? name = null, string? description = null) - => Role.Create(name ?? ValidName, MinValidDescription); + => Role.Create(name ?? ValidName, description ?? MinValidDescription); protected static Role CreateRoleWithId(RoleId? id = null, string? name = null, string? description = null) - => Role.CreateWithId(id ?? ValidId, name ?? ValidName, MinValidDescription); + => Role.CreateWithId(id ?? ValidId, name ?? ValidName, description ?? MinValidDescription); } diff --git a/tests/UnitTests/Accounts/Data/AccountsData.cs b/tests/UnitTests/Accounts/Data/AccountsData.cs index f6bb3e515..19ae29520 100644 --- a/tests/UnitTests/Accounts/Data/AccountsData.cs +++ b/tests/UnitTests/Accounts/Data/AccountsData.cs @@ -19,7 +19,6 @@ public static class AccountsData public const string ValidEmail1 = CustomerEmail; public const string ValidEmail2 = ContributorEmail; public const string ValidEmail3 = DesignerEmail; - public const string ValidEmail4 = AdminEmail; public const string InvalidEmail = ""; public const string InvalidEmailLocal = "@domain.tld"; public const string InvalidEmailDomain = "local@"; @@ -36,6 +35,7 @@ public static class AccountsData public static readonly string MinInvalidLastName = new('a', NameMinLength - 1); public static readonly string MaxInvalidLastName = new('a', NameMaxLength + 1); + public const string ValidRole = "role123"; public const string ValidPassword = "password123"; - public static readonly AccountId ValidId = AccountId.New(CustomerAccountId); + public static readonly AccountId ValidId = AccountId.New(); } diff --git a/tests/UnitTests/Accounts/Data/RolesData.cs b/tests/UnitTests/Accounts/Data/RolesData.cs index 0fbd8c1de..70df47815 100644 --- a/tests/UnitTests/Accounts/Data/RolesData.cs +++ b/tests/UnitTests/Accounts/Data/RolesData.cs @@ -1,15 +1,15 @@ using CustomCADs.Modules.Accounts.Domain.Roles; -using CustomCADs.Shared.Domain; +using CustomCADs.Modules.Accounts.Domain; using CustomCADs.Shared.Domain.TypedIds.Accounts; namespace CustomCADs.UnitTests.Accounts.Data; -using static DomainConstants.Roles; +using static Constants.Roles; using static RoleConstants; public static class RolesData { - public static readonly string ValidName = Customer; + public static readonly string ValidName = Shared.Domain.DomainConstants.Users.CustomerRole; public static readonly string MinValidName = new('a', NameMinLength + 1); public static readonly string MaxValidName = new('a', NameMaxLength - 1); public const string InvalidName = ""; diff --git a/tests/UnitTests/Accounts/Domain/Accounts/Behaviors/Email/Data/AccountEmailValidData.cs b/tests/UnitTests/Accounts/Domain/Accounts/Behaviors/Email/Data/AccountEmailValidData.cs index bafdb65ab..833ee48a3 100644 --- a/tests/UnitTests/Accounts/Domain/Accounts/Behaviors/Email/Data/AccountEmailValidData.cs +++ b/tests/UnitTests/Accounts/Domain/Accounts/Behaviors/Email/Data/AccountEmailValidData.cs @@ -9,6 +9,5 @@ public AccountEmailValidData() Add(ValidEmail1); Add(ValidEmail2); Add(ValidEmail3); - Add(ValidEmail4); } } diff --git a/tests/UnitTests/Catalog/Application/Products/Commands/Internal/Creator/Create/CreateProductHandlerUnitTests.cs b/tests/UnitTests/Catalog/Application/Products/Commands/Internal/Creator/Create/CreateProductHandlerUnitTests.cs index 6b8b1788e..9617f2ff6 100644 --- a/tests/UnitTests/Catalog/Application/Products/Commands/Internal/Creator/Create/CreateProductHandlerUnitTests.cs +++ b/tests/UnitTests/Catalog/Application/Products/Commands/Internal/Creator/Create/CreateProductHandlerUnitTests.cs @@ -47,7 +47,7 @@ public CreateProductHandlerUnitTests() sender.Setup(x => x.SendQueryAsync( It.Is(x => x.Id == ValidCreatorId), ct - )).ReturnsAsync(DomainConstants.Roles.Contributor); + )).ReturnsAsync("role"); sender.Setup(x => x.SendQueryAsync( It.Is(x => x.Id == ValidCadId), ct @@ -186,7 +186,7 @@ public async Task Handle_ShouldValidateStatus_WhenDesignerRole() sender.Setup(x => x.SendQueryAsync( It.Is(x => x.Id == ValidCreatorId), ct - )).ReturnsAsync(DomainConstants.Roles.Designer); + )).ReturnsAsync(DomainConstants.Users.DesignerRole); CreateProductCommand command = new( Name: MinValidName, @@ -212,7 +212,7 @@ public async Task Handle_ShouldTagProfessional_WhenDesignerRole() sender.Setup(x => x.SendQueryAsync( It.Is(x => x.Id == ValidCreatorId), ct - )).ReturnsAsync(DomainConstants.Roles.Designer); + )).ReturnsAsync(DomainConstants.Users.DesignerRole); CreateProductCommand command = new( Name: MinValidName, diff --git a/tests/UnitTests/Catalog/Application/Products/Events/Application/ProductViewedHandlerUnitTests.cs b/tests/UnitTests/Catalog/Application/Products/Events/Application/ProductViewedHandlerUnitTests.cs index 028bae911..66eef43e5 100644 --- a/tests/UnitTests/Catalog/Application/Products/Events/Application/ProductViewedHandlerUnitTests.cs +++ b/tests/UnitTests/Catalog/Application/Products/Events/Application/ProductViewedHandlerUnitTests.cs @@ -22,12 +22,14 @@ public class ProductViewedHandlerUnitTests : ProductsBaseUnitTests private const string Username = Users.CustomerUsername; private readonly AccountInfoDto info = new( + Id: ValidCreatorId, CreatedAt: default, TrackViewedProducts: true, FirstName: null, LastName: null ); private readonly Product product = CreateProduct(); + private static readonly DateTimeOffset viewedAt = DateTimeOffset.UtcNow; public ProductViewedHandlerUnitTests() { @@ -56,7 +58,7 @@ public ProductViewedHandlerUnitTests() public async Task Handle_ShouldQueryDatabase() { // Arrange - ProductViewedApplicationEvent ae = new(ValidId, ValidCreatorId); + ProductViewedApplicationEvent ae = new(ValidId, ValidCreatorId, viewedAt); // Act await handler.HandleAsync(ae); @@ -69,7 +71,7 @@ public async Task Handle_ShouldQueryDatabase() public async Task Handle_ShouldPersistToDatabase() { // Arrange - ProductViewedApplicationEvent ae = new(ValidId, ValidCreatorId); + ProductViewedApplicationEvent ae = new(ValidId, ValidCreatorId, viewedAt); // Act await handler.HandleAsync(ae); @@ -82,7 +84,7 @@ public async Task Handle_ShouldPersistToDatabase() public async Task Handle_ShouldSendRequests() { // Arrange - ProductViewedApplicationEvent ae = new(ValidId, ValidCreatorId); + ProductViewedApplicationEvent ae = new(ValidId, ValidCreatorId, viewedAt); // Act await handler.HandleAsync(ae); @@ -106,7 +108,7 @@ public async Task Handle_ShouldSendRequests() public async Task Handle_ShouldRaiseEvents() { // Arrange - ProductViewedApplicationEvent ae = new(ValidId, ValidCreatorId); + ProductViewedApplicationEvent ae = new(ValidId, ValidCreatorId, viewedAt); // Act await handler.HandleAsync(ae); @@ -121,7 +123,7 @@ public async Task Handle_ShouldRaiseEvents() public async Task Handle_ShouldPopulateProperties() { // Arrange - ProductViewedApplicationEvent ae = new(ValidId, ValidCreatorId); + ProductViewedApplicationEvent ae = new(ValidId, ValidCreatorId, viewedAt); // Act await handler.HandleAsync(ae); @@ -138,16 +140,18 @@ public async Task Handle_ShouldReturnEarly_WhenUserDoesNotTrackViewedProducts() It.Is(x => x.Username == Username), ct )).ReturnsAsync(info with { TrackViewedProducts = false }); - ProductViewedApplicationEvent ae = new(ValidId, ValidCreatorId); + ProductViewedApplicationEvent ae = new(ValidId, ValidCreatorId, viewedAt); // Act await handler.HandleAsync(ae); // Assert - sender.Verify(x => x.SendQueryAsync( - It.Is(x => x.Id == ValidCreatorId && x.ProductId == ValidId), - ct + reads.Verify(x => x.SingleByIdAsync(ValidId, true, ct), Times.Never()); + uow.Verify(x => x.SaveChangesAsync(ct), Times.Never()); + raiser.Verify(x => x.RaiseApplicationEventAsync( + It.Is(x => x.Id == ValidId && x.AccountId == ValidCreatorId) ), Times.Never()); + Assert.Equal(0, product.Counts.Views); } [Fact] @@ -158,12 +162,20 @@ public async Task Handle_ShouldReturnEarly_WhenUserAlreadyViewedProduct() It.Is(x => x.Id == ValidCreatorId && x.ProductId == ValidId), ct )).ReturnsAsync(true); - ProductViewedApplicationEvent ae = new(ValidId, ValidCreatorId); + ProductViewedApplicationEvent ae = new(ValidId, ValidCreatorId, viewedAt); // Act await handler.HandleAsync(ae); // Assert + sender.Verify(x => x.SendQueryAsync( + It.Is(x => x.Username == Username), + ct + ), Times.Never()); + + reads.Verify(x => x.SingleByIdAsync(ValidId, true, ct), Times.Never()); + uow.Verify(x => x.SaveChangesAsync(ct), Times.Never()); + raiser.Verify(x => x.RaiseApplicationEventAsync( It.Is(x => x.Id == ValidId && x.AccountId == ValidCreatorId) ), Times.Never()); @@ -177,7 +189,7 @@ public async Task Handle_ShouldThrowException_WhenProductNotFound() reads.Setup(x => x.SingleByIdAsync(ValidId, true, ct)) .ReturnsAsync(null as Product); - ProductViewedApplicationEvent ae = new(ValidId, ValidCreatorId); + ProductViewedApplicationEvent ae = new(ValidId, ValidCreatorId, viewedAt); // Assert await Assert.ThrowsAsync>( diff --git a/tests/UnitTests/Catalog/Application/Products/Queries/Internal/Gallery/GetById/GalleryGetProductByIdHandlerUnitTests.cs b/tests/UnitTests/Catalog/Application/Products/Queries/Internal/Gallery/GetById/GalleryGetProductByIdHandlerUnitTests.cs index 913ab487b..7f01efda7 100644 --- a/tests/UnitTests/Catalog/Application/Products/Queries/Internal/Gallery/GetById/GalleryGetProductByIdHandlerUnitTests.cs +++ b/tests/UnitTests/Catalog/Application/Products/Queries/Internal/Gallery/GetById/GalleryGetProductByIdHandlerUnitTests.cs @@ -21,6 +21,7 @@ public class GalleryGetProductByIdHandlerUnitTests : ProductsBaseUnitTests private readonly Mock raiser = new(); private readonly Product product = CreateProductWithId(id: ValidId); + private static readonly DateTimeOffset viewedAt = DateTimeOffset.UtcNow; public GalleryGetProductByIdHandlerUnitTests() { @@ -37,7 +38,7 @@ public GalleryGetProductByIdHandlerUnitTests() public async Task Handle_ShouldQueryDatabase() { // Arrange - GalleryGetProductByIdQuery query = new(ValidId, ValidCreatorId); + GalleryGetProductByIdQuery query = new(ValidId, ValidCreatorId, Viewed: false); // Act await handler.Handle(query, ct); @@ -50,7 +51,7 @@ public async Task Handle_ShouldQueryDatabase() public async Task Handle_ShouldSendRequests() { // Arrange - GalleryGetProductByIdQuery query = new(ValidId, ValidCreatorId); + GalleryGetProductByIdQuery query = new(ValidId, ValidCreatorId, Viewed: false); // Act await handler.Handle(query, ct); @@ -66,26 +67,17 @@ public async Task Handle_ShouldSendRequests() ), Times.Once()); } - [Fact] - public async Task Handle_ShouldRaiseEvents_WhenAccountIdEmpty() + [Theory] + [InlineData(false, false)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(true, true)] + public async Task Handle_ShouldRaiseEvents(bool authenticatedUser, bool viewed) { - // Arrange - GalleryGetProductByIdQuery query = new(ValidId, ValidCreatorId); + AccountId creatorId = authenticatedUser ? ValidCreatorId : AccountId.New(Guid.Empty); - // Act - await handler.Handle(query, ct); - - // Assert - raiser.Verify(x => x.RaiseApplicationEventAsync( - It.Is(x => x.Id == product.Id) - ), Times.Once()); - } - - [Fact] - public async Task Handle_ShouldNotRaiseEvents_WhenAccountIdEmpty() - { // Arrange - GalleryGetProductByIdQuery query = new(ValidId, AccountId.New(Guid.Empty)); + GalleryGetProductByIdQuery query = new(ValidId, creatorId, Viewed: viewed); // Act await handler.Handle(query, ct); @@ -93,14 +85,14 @@ public async Task Handle_ShouldNotRaiseEvents_WhenAccountIdEmpty() // Assert raiser.Verify(x => x.RaiseApplicationEventAsync( It.Is(x => x.Id == product.Id) - ), Times.Never()); + ), Times.Exactly(authenticatedUser && viewed ? 1 : 0)); } [Fact] public async Task Handle_ShouldReturnResult() { // Arrange - GalleryGetProductByIdQuery query = new(ValidId, ValidCreatorId); + GalleryGetProductByIdQuery query = new(ValidId, ValidCreatorId, Viewed: false); // Act var result = await handler.Handle(query, ct); @@ -120,7 +112,7 @@ public async Task Handle_ShouldThrowException_WhenStatusIsNotValid() { // Arrange product.Report(ValidDesignerId); - GalleryGetProductByIdQuery query = new(ValidId, ValidCreatorId); + GalleryGetProductByIdQuery query = new(ValidId, ValidCreatorId, Viewed: false); // Assert await Assert.ThrowsAsync>( @@ -135,7 +127,7 @@ public async Task Handle_ShouldThrowException_WhenProductNotFound() // Arrange reads.Setup(x => x.SingleByIdAsync(ValidId, false, ct)) .ReturnsAsync(null as Product); - GalleryGetProductByIdQuery query = new(ValidId, ValidCreatorId); + GalleryGetProductByIdQuery query = new(ValidId, ValidCreatorId, Viewed: false); // Assert await Assert.ThrowsAsync>( diff --git a/tests/UnitTests/Delivery/Application/Shipments/Commands/Internal/Cancel/CancelShipmentHandlerUnitTests.cs b/tests/UnitTests/Delivery/Application/Shipments/Commands/Internal/Cancel/CancelShipmentHandlerUnitTests.cs index b7d5db5d1..5334faf16 100644 --- a/tests/UnitTests/Delivery/Application/Shipments/Commands/Internal/Cancel/CancelShipmentHandlerUnitTests.cs +++ b/tests/UnitTests/Delivery/Application/Shipments/Commands/Internal/Cancel/CancelShipmentHandlerUnitTests.cs @@ -3,6 +3,7 @@ using CustomCADs.Modules.Delivery.Domain.Repositories; using CustomCADs.Modules.Delivery.Domain.Repositories.Reads; using CustomCADs.Shared.Application.Exceptions; +using CustomCADs.Shared.Domain.Exceptions; namespace CustomCADs.UnitTests.Delivery.Application.Shipments.Commands.Internal.Cancel; @@ -81,7 +82,7 @@ public async Task Handle_ShouldThrowException_WhenShipmentStatusInvalid() CancelShipmentCommand command = new(ValidId, Comment); // Assert - await Assert.ThrowsAsync>( + await Assert.ThrowsAsync>( // Act async () => await handler.Handle(command, ct) ); diff --git a/tests/UnitTests/Delivery/Application/Shipments/Queries/Internal/GetWaybill/GetShipmentWaybillHandlerUnitTests.cs b/tests/UnitTests/Delivery/Application/Shipments/Queries/Internal/GetWaybill/GetShipmentWaybillHandlerUnitTests.cs index 27693c724..0f0675ac6 100644 --- a/tests/UnitTests/Delivery/Application/Shipments/Queries/Internal/GetWaybill/GetShipmentWaybillHandlerUnitTests.cs +++ b/tests/UnitTests/Delivery/Application/Shipments/Queries/Internal/GetWaybill/GetShipmentWaybillHandlerUnitTests.cs @@ -3,7 +3,6 @@ using CustomCADs.Modules.Delivery.Domain.Repositories.Reads; using CustomCADs.Shared.Application.Exceptions; using CustomCADs.Shared.Domain; -using CustomCADs.Shared.Domain.TypedIds.Accounts; namespace CustomCADs.UnitTests.Delivery.Application.Shipments.Queries.Internal.GetWaybill; @@ -17,7 +16,6 @@ public class GetShipmentWaybillHandlerUnitTests : ShipmentsBaseUnitTests private readonly Mock delivery = new(); private static readonly byte[] bytes = [1, 2, 3, 4, 5, 6]; - private static readonly AccountId headDesignerId = AccountId.New(DesignerAccountId); public GetShipmentWaybillHandlerUnitTests() { @@ -33,7 +31,7 @@ public GetShipmentWaybillHandlerUnitTests() public async Task Handle_ShouldQueryDatabase() { // Arrange - GetShipmentWaybillQuery query = new(ValidId, headDesignerId); + GetShipmentWaybillQuery query = new(ValidId, HeadDesignerAccountId); // Act await handler.Handle(query, ct); @@ -49,7 +47,7 @@ public async Task Handle_ShouldQueryDatabase() public async Task Handle_ShouldCallDelivery() { // Arrange - GetShipmentWaybillQuery query = new(ValidId, headDesignerId); + GetShipmentWaybillQuery query = new(ValidId, HeadDesignerAccountId); // Act await handler.Handle(query, ct); @@ -62,7 +60,7 @@ public async Task Handle_ShouldCallDelivery() public async Task Handle_ShouldReturnResult() { // Arrange - GetShipmentWaybillQuery query = new(ValidId, headDesignerId); + GetShipmentWaybillQuery query = new(ValidId, HeadDesignerAccountId); // Act byte[] result = await handler.Handle(query, ct); @@ -89,7 +87,7 @@ public async Task Handle_ShouldThrowException_WhenShipmentStatusInvalid() { // Arrange reads.Setup(x => x.SingleByIdAsync(ValidId, false, ct)).ReturnsAsync(CreateShipment()); - GetShipmentWaybillQuery query = new(ValidId, headDesignerId); + GetShipmentWaybillQuery query = new(ValidId, HeadDesignerAccountId); // Assert await Assert.ThrowsAsync>( diff --git a/tests/UnitTests/Delivery/Data/ShipmentsData.cs b/tests/UnitTests/Delivery/Data/ShipmentsData.cs index e788b7778..e383e04c3 100644 --- a/tests/UnitTests/Delivery/Data/ShipmentsData.cs +++ b/tests/UnitTests/Delivery/Data/ShipmentsData.cs @@ -44,5 +44,5 @@ public static class ShipmentsData public const string ValidReferenceId = "some-reference-id"; public static readonly ShipmentId ValidId = ShipmentId.New(); - public static readonly AccountId ValidBuyerId = AccountId.New(CustomerAccountId); + public static readonly AccountId ValidBuyerId = AccountId.New(); } diff --git a/tests/UnitTests/Identity/Application/Users/Commands/Internal/ChangeUsername/ChangeUsernameHandlerUnitTests.cs b/tests/UnitTests/Identity/Application/Users/Commands/Internal/ChangeUsername/ChangeUsernameHandlerUnitTests.cs index cbaa26111..7be6ccee3 100644 --- a/tests/UnitTests/Identity/Application/Users/Commands/Internal/ChangeUsername/ChangeUsernameHandlerUnitTests.cs +++ b/tests/UnitTests/Identity/Application/Users/Commands/Internal/ChangeUsername/ChangeUsernameHandlerUnitTests.cs @@ -13,13 +13,13 @@ public class ChangeUsernameHandlerUnitTests : UsersBaseUnitTests private readonly Mock service = new(); private readonly Mock raiser = new(); - private readonly User user = CreateUser(username: MaxValidUsername); + private readonly User user = CreateUser(); public ChangeUsernameHandlerUnitTests() { handler = new(service.Object, raiser.Object); - service.Setup(x => x.GetByUsernameAsync(user.Username)).ReturnsAsync(user); + service.Setup(x => x.GetByAccountIdAsync(user.AccountId)).ReturnsAsync(user); } [Fact] @@ -27,15 +27,17 @@ public async Task Handle_ShouldCallService() { // Arrange ChangeUsernameCommand command = new( - Username: user.Username, - NewUsername: MinValidUsername + Id: user.AccountId, + Username: MinValidUsername, + FirstName: null, + LastName: null ); // Act await handler.Handle(command, ct); // Assert - service.Verify(x => x.GetByUsernameAsync(MaxValidUsername), Times.Once()); + service.Verify(x => x.GetByAccountIdAsync(user.AccountId), Times.Once()); service.Verify(x => x.UpdateUsernameAsync(user.Id, MinValidUsername), Times.Once()); } @@ -44,8 +46,10 @@ public async Task Handle_ShouldRaiseEvents() { // Arrange ChangeUsernameCommand command = new( - Username: MaxValidUsername, - NewUsername: MinValidUsername + Id: user.AccountId, + Username: MinValidUsername, + FirstName: null, + LastName: null ); // Act diff --git a/tests/UnitTests/Identity/Application/Users/Commands/Internal/Delete/DeleteUserHandlerUnitTests.cs b/tests/UnitTests/Identity/Application/Users/Commands/Internal/Delete/DeleteUserHandlerUnitTests.cs index f3b8f13b8..b9e554f5f 100644 --- a/tests/UnitTests/Identity/Application/Users/Commands/Internal/Delete/DeleteUserHandlerUnitTests.cs +++ b/tests/UnitTests/Identity/Application/Users/Commands/Internal/Delete/DeleteUserHandlerUnitTests.cs @@ -18,29 +18,26 @@ public class DeleteUserHandlerUnitTests : UsersBaseUnitTests public DeleteUserHandlerUnitTests() { handler = new(service.Object, raiser.Object); - - service.Setup(x => x.GetAccountIdAsync(user.Username)).ReturnsAsync(ValidAccountId); } [Fact] public async Task Handle_ShouldCallService() { // Arrange - DeleteUserCommand command = new(user.Username); + DeleteUserCommand command = new(user.AccountId); // Act await handler.Handle(command, ct); // Assert - service.Verify(x => x.GetAccountIdAsync(MaxValidUsername), Times.Once()); - service.Verify(x => x.DeleteAsync(user.Username), Times.Once()); + service.Verify(x => x.DeleteAsync(user.AccountId), Times.Once()); } [Fact] public async Task Handle_ShouldRaiseEvents() { // Arrange - DeleteUserCommand command = new(user.Username); + DeleteUserCommand command = new(user.AccountId); // Act await handler.Handle(command, ct); diff --git a/tests/UnitTests/Identity/Application/Users/Commands/Internal/Login/LoginUserHandlerUnitTests.cs b/tests/UnitTests/Identity/Application/Users/Commands/Internal/Login/LoginUserHandlerUnitTests.cs index 31d85d0c6..1f456ee8a 100644 --- a/tests/UnitTests/Identity/Application/Users/Commands/Internal/Login/LoginUserHandlerUnitTests.cs +++ b/tests/UnitTests/Identity/Application/Users/Commands/Internal/Login/LoginUserHandlerUnitTests.cs @@ -15,7 +15,7 @@ public class LoginUserHandlerUnitTests : UsersBaseUnitTests private readonly Mock tokenService = new(); private readonly User User = CreateUser(username: MaxValidUsername); - private static readonly RefreshToken RefreshToken = RefreshToken.Create("refresh-token", ValidId, false); + private static readonly RefreshToken RefreshToken = RefreshToken.Create("refresh-token", ValidFingerprint, ValidId, false); private static readonly TokensDto Tokens = new( Role: "role", AccessToken: new("access-token", DateTimeOffset.UtcNow), @@ -43,7 +43,8 @@ public async Task Handle_ShouldCallService() LoginUserCommand command = new( Username: User.Username, Password: MinValidPassword, - LongerExpireTime: false + LongerExpireTime: false, + Fingerprint: ValidFingerprint ); // Act @@ -62,7 +63,8 @@ public async Task Handle_ShouldIssueTokens() LoginUserCommand command = new( Username: User.Username, Password: MinValidPassword, - LongerExpireTime: false + LongerExpireTime: false, + Fingerprint: ValidFingerprint ); // Act @@ -82,7 +84,8 @@ public async Task Handle_ShouldReturnResult() LoginUserCommand command = new( Username: User.Username, Password: MinValidPassword, - LongerExpireTime: false + LongerExpireTime: false, + Fingerprint: ValidFingerprint ); // Act @@ -101,7 +104,8 @@ public async Task Handle_ShouldThrowException_WhenPasswordIncorrect() LoginUserCommand command = new( Username: User.Username, Password: MinValidPassword, - LongerExpireTime: false + LongerExpireTime: false, + Fingerprint: ValidFingerprint ); // Assert @@ -120,7 +124,8 @@ public async Task Handle_ShouldThrowException_WhenUserLockedOut() LoginUserCommand command = new( Username: User.Username, Password: MinValidPassword, - LongerExpireTime: false + LongerExpireTime: false, + Fingerprint: ValidFingerprint ); // Assert @@ -140,7 +145,8 @@ public async Task Handle_ShouldThrowException_WhenUserNotVerified() LoginUserCommand command = new( Username: unverifiedUser.Username, Password: MinValidPassword, - LongerExpireTime: false + LongerExpireTime: false, + Fingerprint: ValidFingerprint ); // Assert diff --git a/tests/UnitTests/Identity/Application/Users/Commands/Internal/Logout/LogoutUserHandlerUnitTests.cs b/tests/UnitTests/Identity/Application/Users/Commands/Internal/Logout/LogoutUserHandlerUnitTests.cs index fa1dfe610..a933fabea 100644 --- a/tests/UnitTests/Identity/Application/Users/Commands/Internal/Logout/LogoutUserHandlerUnitTests.cs +++ b/tests/UnitTests/Identity/Application/Users/Commands/Internal/Logout/LogoutUserHandlerUnitTests.cs @@ -12,7 +12,7 @@ public class LogoutUserHandlerUnitTests : UsersBaseUnitTests private readonly LogoutUserHandler handler; private readonly Mock service = new(); - private static readonly RefreshToken token = RefreshToken.Create("refresh-token", ValidId, longerSession: false); + private static readonly RefreshToken token = RefreshToken.Create("refresh-token", ValidFingerprint, ValidId, longerSession: false); private readonly User user = CreateUser(username: MaxValidUsername); public LogoutUserHandlerUnitTests() diff --git a/tests/UnitTests/Identity/Application/Users/Commands/Internal/Refresh/RefreshUserHandlerUnitTests.cs b/tests/UnitTests/Identity/Application/Users/Commands/Internal/Refresh/RefreshUserHandlerUnitTests.cs index 2f9fbe0b2..34baa527f 100644 --- a/tests/UnitTests/Identity/Application/Users/Commands/Internal/Refresh/RefreshUserHandlerUnitTests.cs +++ b/tests/UnitTests/Identity/Application/Users/Commands/Internal/Refresh/RefreshUserHandlerUnitTests.cs @@ -3,11 +3,10 @@ using CustomCADs.Modules.Identity.Application.Users.Dtos; using CustomCADs.Modules.Identity.Domain.Users.Entities; using CustomCADs.Shared.Application.Exceptions; -using CustomCADs.Shared.Domain; +using static CustomCADs.Modules.Identity.Domain.Constants.Tokens; namespace CustomCADs.UnitTests.Identity.Application.Users.Commands.Internal.Refresh; -using static DomainConstants.Tokens; using static UsersData; public class RefreshUserHandlerUnitTests : UsersBaseUnitTests @@ -16,7 +15,7 @@ public class RefreshUserHandlerUnitTests : UsersBaseUnitTests private readonly Mock service = new(); private readonly Mock tokenService = new(); - private static readonly RefreshToken RefreshToken = RefreshToken.Create("refresh-token", ValidId, longerSession: false); + private static readonly RefreshToken RefreshToken = RefreshToken.Create("refresh-token", ValidFingerprint, ValidId, longerSession: false); private readonly User User = CreateUser(username: MaxValidUsername); private static readonly TokensDto Tokens = new( Role: "role", @@ -29,9 +28,6 @@ public RefreshUserHandlerUnitTests() { handler = new(service.Object, tokenService.Object); - tokenService.Setup(x => x.IssueRefreshToken( - It.IsAny>() - )).Returns(RefreshToken); tokenService.Setup(x => x.IssueTokens(User, RefreshToken)).Returns(Tokens); service.Setup(x => x.GetByRefreshTokenAsync(RefreshToken.Value)).ReturnsAsync((User, RefreshToken)); @@ -41,7 +37,7 @@ public RefreshUserHandlerUnitTests() public async Task Handle_ShouldCallService() { // Arrange - RefreshUserCommand command = new(RefreshToken.Value); + RefreshUserCommand command = new(RefreshToken.Value, ValidFingerprint); // Act await handler.Handle(command, ct); @@ -54,15 +50,12 @@ public async Task Handle_ShouldCallService() public async Task Handle_ShouldIssueTokens() { // Arrange - RefreshUserCommand command = new(RefreshToken.Value); + RefreshUserCommand command = new(RefreshToken.Value, ValidFingerprint); // Act await handler.Handle(command, ct); // Assert - tokenService.Verify(x => x.IssueRefreshToken( - It.IsAny>() - ), Times.Once()); tokenService.Verify(x => x.IssueTokens(User, RefreshToken), Times.Once()); } @@ -70,7 +63,7 @@ public async Task Handle_ShouldIssueTokens() public async Task Handle_ShouldReturnResult() { // Arrange - RefreshUserCommand command = new(RefreshToken.Value); + RefreshUserCommand command = new(RefreshToken.Value, ValidFingerprint); // Act TokensDto tokens = await handler.Handle(command, ct); @@ -83,7 +76,7 @@ public async Task Handle_ShouldReturnResult() public async Task Handle_ShouldThrowException_WhenMissingToken() { // Arrange - RefreshUserCommand command = new(Token: null); + RefreshUserCommand command = new(Token: null, ValidFingerprint); // Assert await Assert.ThrowsAsync>( @@ -100,13 +93,14 @@ public async Task Handle_ShouldThrowException_WhenTokenExpired() RefreshToken token = RefreshToken.Create( id: RefreshTokenId.New(), value: "refresh-token", + fingerprint: ValidFingerprint, userId: ValidId, issuedAt: yesterday.AddDays(-RtDurationInDays), expiresAt: yesterday ); service.Setup(x => x.GetByRefreshTokenAsync(token.Value)).ReturnsAsync((User, token)); - RefreshUserCommand command = new(token.Value); + RefreshUserCommand command = new(token.Value, ValidFingerprint); // Assert await Assert.ThrowsAsync>( diff --git a/tests/UnitTests/Identity/Application/Users/Commands/Internal/SSO/SingleSignOnUserUnitTests.cs b/tests/UnitTests/Identity/Application/Users/Commands/Internal/SSO/SingleSignOnUserUnitTests.cs index 0c3ed79d1..b28e83d39 100644 --- a/tests/UnitTests/Identity/Application/Users/Commands/Internal/SSO/SingleSignOnUserUnitTests.cs +++ b/tests/UnitTests/Identity/Application/Users/Commands/Internal/SSO/SingleSignOnUserUnitTests.cs @@ -18,7 +18,7 @@ public class SingleSignOnUserUnitTests : UsersBaseUnitTests private const string Provider = "Google"; private readonly User User = CreateUser(username: MaxValidUsername); - private static readonly RefreshToken RefreshToken = RefreshToken.Create("refresh-token", ValidId, false); + private static readonly RefreshToken RefreshToken = RefreshToken.Create("refresh-token", ValidFingerprint, ValidId, false); private static readonly TokensDto Tokens = new( Role: "role", AccessToken: new("access-token", DateTimeOffset.UtcNow), @@ -50,9 +50,12 @@ public async Task Handle_ShouldCallService() // Arrange SingleSignOnUserCommand command = new( Role: User.Role, + FirstName: null, + LastName: null, Username: User.Username, Email: User.Email.Value, - Provider: Provider + Provider: Provider, + Fingerprint: ValidFingerprint ); // Act @@ -76,9 +79,12 @@ public async Task Handle_ShouldCallGetByEmail_WhenUsernameDoesNotExist() SingleSignOnUserCommand command = new( Role: User.Role, + FirstName: null, + LastName: null, Username: User.Username, Email: User.Email.Value, - Provider: Provider + Provider: Provider, + Fingerprint: ValidFingerprint ); // Act @@ -97,9 +103,12 @@ public async Task Handle_ShouldCallCreateUser_WhenUsernameAndEmailDoNotExist() SingleSignOnUserCommand command = new( Role: User.Role, + FirstName: null, + LastName: null, Username: User.Username, Email: User.Email.Value, - Provider: Provider + Provider: Provider, + Fingerprint: ValidFingerprint ); // Act @@ -130,9 +139,12 @@ public async Task Handle_ShouldIssueTokens() // Arrange SingleSignOnUserCommand command = new( Role: User.Role, + FirstName: null, + LastName: null, Username: User.Username, Email: User.Email.Value, - Provider: Provider + Provider: Provider, + Fingerprint: ValidFingerprint ); // Act @@ -151,9 +163,12 @@ public async Task Handle_ShouldReturnResult() // Arrange SingleSignOnUserCommand command = new( Role: User.Role, + FirstName: null, + LastName: null, Username: User.Username, Email: User.Email.Value, - Provider: Provider + Provider: Provider, + Fingerprint: ValidFingerprint ); // Act diff --git a/tests/UnitTests/Identity/Application/Users/Commands/Internal/ToggleViewedProductsTracking/ToggleViewedProductsTrackingHandlerUnitTests.cs b/tests/UnitTests/Identity/Application/Users/Commands/Internal/ToggleViewedProductsTracking/ToggleViewedProductsTrackingHandlerUnitTests.cs index f14b4f405..41ab7eb44 100644 --- a/tests/UnitTests/Identity/Application/Users/Commands/Internal/ToggleViewedProductsTracking/ToggleViewedProductsTrackingHandlerUnitTests.cs +++ b/tests/UnitTests/Identity/Application/Users/Commands/Internal/ToggleViewedProductsTracking/ToggleViewedProductsTrackingHandlerUnitTests.cs @@ -12,7 +12,6 @@ namespace CustomCADs.UnitTests.Identity.Application.Users.Commands.Internal.Togg public class ToggleViewedProductsTrackingHandlerUnitTests : UsersBaseUnitTests { private readonly ToggleViewedProductsTrackingHandler handler; - private readonly Mock service = new(); private readonly Mock sender = new(); private readonly Mock raiser = new(); @@ -20,33 +19,19 @@ public class ToggleViewedProductsTrackingHandlerUnitTests : UsersBaseUnitTests public ToggleViewedProductsTrackingHandlerUnitTests() { - handler = new(service.Object, sender.Object, raiser.Object); + handler = new(sender.Object, raiser.Object); - service.Setup(x => x.GetAccountIdAsync(MaxValidUsername)).ReturnsAsync(ValidAccountId); sender.Setup(x => x.SendQueryAsync( It.Is(x => x.Username == MaxValidUsername), ct - )).ReturnsAsync(new AccountInfoDto(default, InitialTrackViewedProducts, null, null)); - } - - [Fact] - public async Task Handle_ShouldCallService() - { - // Arrange - ToggleViewedProductsTrackingCommand command = new(MaxValidUsername); - - // Act - await handler.Handle(command, ct); - - // Assert - service.Verify(x => x.GetAccountIdAsync(MaxValidUsername), Times.Once()); + )).ReturnsAsync(new AccountInfoDto(ValidAccountId, default, InitialTrackViewedProducts, null, null)); } [Fact] public async Task Handle_ShouldSendRequests() { // Arrange - ToggleViewedProductsTrackingCommand command = new(MaxValidUsername); + ToggleViewedProductsTrackingCommand command = new(MaxValidUsername, ValidAccountId); // Act await handler.Handle(command, ct); @@ -62,7 +47,7 @@ public async Task Handle_ShouldSendRequests() public async Task Handle_ShouldRaiseEvents() { // Arrange - ToggleViewedProductsTrackingCommand command = new(MaxValidUsername); + ToggleViewedProductsTrackingCommand command = new(MaxValidUsername, ValidAccountId); // Act await handler.Handle(command, ct); diff --git a/tests/UnitTests/Identity/Application/Users/Commands/Internal/VerifyEmail/VerifyUserEmailHandlerUnitTests.cs b/tests/UnitTests/Identity/Application/Users/Commands/Internal/VerifyEmail/VerifyUserEmailHandlerUnitTests.cs index 56b878b8a..e562d6cc4 100644 --- a/tests/UnitTests/Identity/Application/Users/Commands/Internal/VerifyEmail/VerifyUserEmailHandlerUnitTests.cs +++ b/tests/UnitTests/Identity/Application/Users/Commands/Internal/VerifyEmail/VerifyUserEmailHandlerUnitTests.cs @@ -16,7 +16,7 @@ public class VerifyUserEmailHandlerUnitTests : UsersBaseUnitTests private const string Token = "email-token"; private readonly User User = CreateUser(email: new(ValidEmail, IsVerified: false)); - private static readonly RefreshToken RefreshToken = RefreshToken.Create(Token, ValidId, false); + private static readonly RefreshToken RefreshToken = RefreshToken.Create(Token, ValidFingerprint, ValidId, false); private static readonly TokensDto Tokens = new( Role: "role", AccessToken: new("access-token", DateTimeOffset.UtcNow), @@ -40,7 +40,7 @@ public VerifyUserEmailHandlerUnitTests() public async Task Handle_ShouldCallService() { // Arrange - VerifyUserEmailCommand command = new(User.Username, Token); + VerifyUserEmailCommand command = new(User.Username, Token, ValidFingerprint); // Act await handler.Handle(command, ct); @@ -54,7 +54,7 @@ public async Task Handle_ShouldCallService() public async Task Handle_ShouldIssueTokens() { // Arrange - VerifyUserEmailCommand command = new(User.Username, Token); + VerifyUserEmailCommand command = new(User.Username, Token, ValidFingerprint); // Act await handler.Handle(command, ct); @@ -70,7 +70,7 @@ public async Task Handle_ShouldIssueTokens() public async Task Handle_ShouldReturnResult() { // Arrange - VerifyUserEmailCommand command = new(User.Username, Token); + VerifyUserEmailCommand command = new(User.Username, Token, ValidFingerprint); // Act TokensDto tokens = await handler.Handle(command, ct); @@ -86,7 +86,7 @@ public async Task Handle_ShouldThrowException_WhenEmailVerified() User verifiedUser = CreateUser(email: new(ValidEmail, IsVerified: true)); service.Setup(x => x.GetByUsernameAsync(verifiedUser.Username)).ReturnsAsync(verifiedUser); - VerifyUserEmailCommand command = new(verifiedUser.Username, Token); + VerifyUserEmailCommand command = new(verifiedUser.Username, Token, ValidFingerprint); // Assert await Assert.ThrowsAsync>( diff --git a/tests/UnitTests/Identity/Application/Users/Events/Application/Users/UserDeletedHandlerUnitTests.cs b/tests/UnitTests/Identity/Application/Users/Events/Application/Users/UserDeletedHandlerUnitTests.cs index 4701cce79..3b3728fab 100644 --- a/tests/UnitTests/Identity/Application/Users/Events/Application/Users/UserDeletedHandlerUnitTests.cs +++ b/tests/UnitTests/Identity/Application/Users/Events/Application/Users/UserDeletedHandlerUnitTests.cs @@ -20,12 +20,12 @@ public UserDeletedHandlerUnitTests() public async Task Handle_ShouldCallService() { // Arrange - AccountDeletedApplicationEvent ae = new(MaxValidUsername); + AccountDeletedApplicationEvent ae = new(ValidAccountId); // Act await handler.HandleAsync(ae); // Assert - service.Verify(x => x.DeleteAsync(MaxValidUsername), Times.Once()); + service.Verify(x => x.DeleteAsync(ValidAccountId), Times.Once()); } } diff --git a/tests/UnitTests/Identity/Application/Users/Queries/Internal/GetByUsername/GetUserByUsernameHandlerUnitTests.cs b/tests/UnitTests/Identity/Application/Users/Queries/Internal/GetByUsername/GetUserByUsernameHandlerUnitTests.cs index 51bed3b13..b5e5a0288 100644 --- a/tests/UnitTests/Identity/Application/Users/Queries/Internal/GetByUsername/GetUserByUsernameHandlerUnitTests.cs +++ b/tests/UnitTests/Identity/Application/Users/Queries/Internal/GetByUsername/GetUserByUsernameHandlerUnitTests.cs @@ -12,49 +12,52 @@ public class GetUserByUsernameHandlerUnitTests : UsersBaseUnitTests private readonly GetUserByUsernameHandler handler; private readonly Mock service = new(); private readonly Mock sender = new(); + private readonly User user = CreateUserWithId(); public GetUserByUsernameHandlerUnitTests() { + user.AddRefreshToken("refresh-token", ValidFingerprint, false); + handler = new(service.Object, sender.Object); - service.Setup(x => x.GetByUsernameAsync(MaxValidUsername)) - .ReturnsAsync(CreateUserWithId()); + service.Setup(x => x.GetByAccountIdAsync(user.AccountId)) + .ReturnsAsync(user); sender.Setup(x => x.SendQueryAsync( - It.Is(x => x.Username == MaxValidUsername), + It.Is(x => x.Username == user.Username), ct - )).ReturnsAsync(new AccountInfoDto(DateTimeOffset.UtcNow, true, null, null)); + )).ReturnsAsync(new AccountInfoDto(ValidAccountId, DateTimeOffset.UtcNow, true, null, null)); } [Fact] public async Task Handle_ShouldCallService() { // Arrange - GetUserByUsernameQuery query = new(MaxValidUsername); + GetUserByUsernameQuery query = new(user.AccountId, "refresh-token"); // Act await handler.Handle(query, ct); // Assert - service.Verify(x => x.GetByUsernameAsync(MaxValidUsername), Times.Once()); + service.Verify(x => x.GetByAccountIdAsync(user.AccountId), Times.Once()); } [Fact] public async Task Handle_ShouldSendRequests() { // Arrange - GetUserByUsernameQuery query = new(MaxValidUsername); + GetUserByUsernameQuery query = new(user.AccountId, "refresh-token"); // Act await handler.Handle(query, ct); // Assert sender.Verify(x => x.SendQueryAsync( - It.Is(x => x.Username == MaxValidUsername), + It.Is(x => x.Username == user.Username), ct ), Times.Once()); sender.Verify(x => x.SendQueryAsync( - It.Is(x => x.Username == MaxValidUsername), + It.Is(x => x.Username == user.Username), ct ), Times.Once()); } @@ -63,7 +66,7 @@ public async Task Handle_ShouldSendRequests() public async Task Handle_ShouldReturnResult() { // Arrange - GetUserByUsernameQuery query = new(MaxValidUsername); + GetUserByUsernameQuery query = new(user.AccountId, "refresh-token"); // Act var result = await handler.Handle(query, ct); diff --git a/tests/UnitTests/Identity/Data/UsersData.cs b/tests/UnitTests/Identity/Data/UsersData.cs index 0652f4242..000e8ca91 100644 --- a/tests/UnitTests/Identity/Data/UsersData.cs +++ b/tests/UnitTests/Identity/Data/UsersData.cs @@ -1,4 +1,5 @@ using CustomCADs.Modules.Identity.Domain.Users; +using CustomCADs.Modules.Identity.Domain.Users.ValueObjects; using CustomCADs.Shared.Domain; using CustomCADs.Shared.Domain.TypedIds.Accounts; using CustomCADs.Shared.Domain.TypedIds.Identity; @@ -10,7 +11,7 @@ namespace CustomCADs.UnitTests.Identity.Data; public static class UsersData { - public const string ValidRole = Roles.Customer; + public const string ValidRole = Users.CustomerRole; public static readonly string InvalidRole = string.Empty; public static readonly string MinValidUsername = new('a', UsernameMinLength + 1); @@ -26,6 +27,8 @@ public static class UsersData public static readonly string InvalidPassword = string.Empty; public static readonly string MinInvalidPassword = new('a', PasswordMinLength - 1); + public static readonly Fingerprint ValidFingerprint = new(); + public static readonly UserId ValidId = UserId.New(); public static readonly AccountId ValidAccountId = AccountId.New(); } diff --git a/tests/UnitTests/Identity/Domain/Users/Behaviors/AddRefreshToken/UserAddRefreshTokenUnitTests.cs b/tests/UnitTests/Identity/Domain/Users/Behaviors/AddRefreshToken/UserAddRefreshTokenUnitTests.cs index 9e11f18d1..1dff27f7a 100644 --- a/tests/UnitTests/Identity/Domain/Users/Behaviors/AddRefreshToken/UserAddRefreshTokenUnitTests.cs +++ b/tests/UnitTests/Identity/Domain/Users/Behaviors/AddRefreshToken/UserAddRefreshTokenUnitTests.cs @@ -1,10 +1,8 @@ using CustomCADs.Modules.Identity.Domain.Users.Entities; -using CustomCADs.Shared.Domain; +using static CustomCADs.Modules.Identity.Domain.Constants; namespace CustomCADs.UnitTests.Identity.Domain.Users.Behaviors.AddRefreshToken; -using static DomainConstants; - public class UserAddRefreshTokenUnitTests : UsersBaseUnitTests { private const string Value = "refresh-token"; @@ -13,7 +11,7 @@ public class UserAddRefreshTokenUnitTests : UsersBaseUnitTests [Fact] public void AddRefreshToken_ShouldNotThrowException() { - user.AddRefreshToken(Value, longerSession: false); + user.AddRefreshToken(Value, new(), longerSession: false); } [Theory] @@ -26,7 +24,7 @@ public void AddRefreshToken_ShouldReturnResult(bool longerSession) : Tokens.RtDurationInDays; TimeSpan expectedDuration = TimeSpan.FromDays(expectedDurationDays); - RefreshToken rt = user.AddRefreshToken(Value, longerSession); + RefreshToken rt = user.AddRefreshToken(Value, new(), longerSession); TimeSpan actualDuration = rt.ExpiresAt - rt.IssuedAt; Assert.Multiple( @@ -39,7 +37,7 @@ public void AddRefreshToken_ShouldReturnResult(bool longerSession) [Fact] public void AddRefreshToken_PopulatesProperty() { - RefreshToken rt = user.AddRefreshToken(Value, longerSession: false); + RefreshToken rt = user.AddRefreshToken(Value, new(), longerSession: false); Assert.Contains(rt, user.RefreshTokens); } } diff --git a/tests/UnitTests/Identity/Domain/Users/Behaviors/RemoveRefreshToken/UserRemoveRefreshTokenUnitTests.cs b/tests/UnitTests/Identity/Domain/Users/Behaviors/RemoveRefreshToken/UserRemoveRefreshTokenUnitTests.cs index cf5095e3c..62c8bfbe1 100644 --- a/tests/UnitTests/Identity/Domain/Users/Behaviors/RemoveRefreshToken/UserRemoveRefreshTokenUnitTests.cs +++ b/tests/UnitTests/Identity/Domain/Users/Behaviors/RemoveRefreshToken/UserRemoveRefreshTokenUnitTests.cs @@ -9,7 +9,7 @@ public class UserRemoveRefreshTokenUnitTests : UsersBaseUnitTests public UserRemoveRefreshTokenUnitTests() { - rt = user.AddRefreshToken("refresh-token", longerSession: false); + rt = user.AddRefreshToken("refresh-token", new(), longerSession: false); } [Fact]