diff --git a/.gitignore b/.gitignore index e10897e..f349058 100644 --- a/.gitignore +++ b/.gitignore @@ -54,4 +54,8 @@ TestResult.xml nunit-*.xml -appsettings.Development.json \ No newline at end of file +appsettings.Development.json +appsettings.json +*appsettings.json + +.env \ No newline at end of file diff --git a/.vs/Fin-Backend/CopilotIndices/17.14.734.62261/CodeChunks.db b/.vs/Fin-Backend/CopilotIndices/17.14.734.62261/CodeChunks.db new file mode 100644 index 0000000..82e55a1 Binary files /dev/null and b/.vs/Fin-Backend/CopilotIndices/17.14.734.62261/CodeChunks.db differ diff --git a/.vs/Fin-Backend/CopilotIndices/17.14.734.62261/SemanticSymbols.db b/.vs/Fin-Backend/CopilotIndices/17.14.734.62261/SemanticSymbols.db new file mode 100644 index 0000000..a2a78ed Binary files /dev/null and b/.vs/Fin-Backend/CopilotIndices/17.14.734.62261/SemanticSymbols.db differ diff --git a/.vs/Fin-Backend/DesignTimeBuild/.dtbcache.v2 b/.vs/Fin-Backend/DesignTimeBuild/.dtbcache.v2 new file mode 100644 index 0000000..275c4e5 Binary files /dev/null and b/.vs/Fin-Backend/DesignTimeBuild/.dtbcache.v2 differ diff --git a/.vs/Fin-Backend/FileContentIndex/6f825f2f-6b18-4bcc-9a27-756adc3ec514.vsidx b/.vs/Fin-Backend/FileContentIndex/6f825f2f-6b18-4bcc-9a27-756adc3ec514.vsidx new file mode 100644 index 0000000..ebc3c49 Binary files /dev/null and b/.vs/Fin-Backend/FileContentIndex/6f825f2f-6b18-4bcc-9a27-756adc3ec514.vsidx differ diff --git a/.vs/Fin-Backend/FileContentIndex/8be34b6f-428b-401a-b31e-b52358fdea5c.vsidx b/.vs/Fin-Backend/FileContentIndex/8be34b6f-428b-401a-b31e-b52358fdea5c.vsidx new file mode 100644 index 0000000..89767a3 Binary files /dev/null and b/.vs/Fin-Backend/FileContentIndex/8be34b6f-428b-401a-b31e-b52358fdea5c.vsidx differ diff --git a/.vs/Fin-Backend/v17/.futdcache.v2 b/.vs/Fin-Backend/v17/.futdcache.v2 new file mode 100644 index 0000000..f46f1a6 Binary files /dev/null and b/.vs/Fin-Backend/v17/.futdcache.v2 differ diff --git a/.vs/Fin-Backend/v17/.suo b/.vs/Fin-Backend/v17/.suo new file mode 100644 index 0000000..679eb1d Binary files /dev/null and b/.vs/Fin-Backend/v17/.suo differ diff --git a/.vs/Fin-Backend/v17/DocumentLayout.json b/.vs/Fin-Backend/v17/DocumentLayout.json new file mode 100644 index 0000000..6d93739 --- /dev/null +++ b/.vs/Fin-Backend/v17/DocumentLayout.json @@ -0,0 +1,394 @@ +{ + "Version": 1, + "WorkspaceRootPath": "C:\\Users\\Gustavo Passos\\Desktop\\Develop\\Fin\\fin-backend\\", + "Documents": [ + { + "AbsoluteMoniker": "D:0:0:{8A2A53B4-B6F1-47DC-A681-E1819AE39A0B}|Fin.Infrastructure\\Fin.Infrastructure.csproj|c:\\users\\gustavo passos\\desktop\\develop\\fin\\fin-backend\\fin.infrastructure\\authentications\\activatedusermiddleware.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", + "RelativeMoniker": "D:0:0:{8A2A53B4-B6F1-47DC-A681-E1819AE39A0B}|Fin.Infrastructure\\Fin.Infrastructure.csproj|solutionrelative:fin.infrastructure\\authentications\\activatedusermiddleware.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" + }, + { + "AbsoluteMoniker": "D:0:0:{E698F897-BB2D-42EF-879A-03B0DA36F212}|Fin.Domain\\Fin.Domain.csproj|c:\\users\\gustavo passos\\desktop\\develop\\fin\\fin-backend\\fin.domain\\tenants\\entities\\tenantuser.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", + "RelativeMoniker": "D:0:0:{E698F897-BB2D-42EF-879A-03B0DA36F212}|Fin.Domain\\Fin.Domain.csproj|solutionrelative:fin.domain\\tenants\\entities\\tenantuser.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" + }, + { + "AbsoluteMoniker": "D:0:0:{E698F897-BB2D-42EF-879A-03B0DA36F212}|Fin.Domain\\Fin.Domain.csproj|c:\\users\\gustavo passos\\desktop\\develop\\fin\\fin-backend\\fin.domain\\tenants\\entities\\tenant.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", + "RelativeMoniker": "D:0:0:{E698F897-BB2D-42EF-879A-03B0DA36F212}|Fin.Domain\\Fin.Domain.csproj|solutionrelative:fin.domain\\tenants\\entities\\tenant.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" + }, + { + "AbsoluteMoniker": "D:0:0:{1F2A1D01-85F1-46DC-A5BD-03B7FCF47B82}|Fin.Application\\Fin.Application.csproj|c:\\users\\gustavo passos\\desktop\\develop\\fin\\fin-backend\\fin.application\\globals\\services\\randomgenerator.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", + "RelativeMoniker": "D:0:0:{1F2A1D01-85F1-46DC-A5BD-03B7FCF47B82}|Fin.Application\\Fin.Application.csproj|solutionrelative:fin.application\\globals\\services\\randomgenerator.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" + }, + { + "AbsoluteMoniker": "D:0:0:{1F2A1D01-85F1-46DC-A5BD-03B7FCF47B82}|Fin.Application\\Fin.Application.csproj|c:\\users\\gustavo passos\\desktop\\develop\\fin\\fin-backend\\fin.application\\authentications\\services\\authenticationservice.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", + "RelativeMoniker": "D:0:0:{1F2A1D01-85F1-46DC-A5BD-03B7FCF47B82}|Fin.Application\\Fin.Application.csproj|solutionrelative:fin.application\\authentications\\services\\authenticationservice.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" + }, + { + "AbsoluteMoniker": "D:0:0:{E698F897-BB2D-42EF-879A-03B0DA36F212}|Fin.Domain\\Fin.Domain.csproj|c:\\users\\gustavo passos\\desktop\\develop\\fin\\fin-backend\\fin.domain\\users\\dtos\\userdto.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", + "RelativeMoniker": "D:0:0:{E698F897-BB2D-42EF-879A-03B0DA36F212}|Fin.Domain\\Fin.Domain.csproj|solutionrelative:fin.domain\\users\\dtos\\userdto.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" + }, + { + "AbsoluteMoniker": "D:0:0:{E698F897-BB2D-42EF-879A-03B0DA36F212}|Fin.Domain\\Fin.Domain.csproj|c:\\users\\gustavo passos\\desktop\\develop\\fin\\fin-backend\\fin.domain\\users\\dtos\\userdeleterequestdto.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", + "RelativeMoniker": "D:0:0:{E698F897-BB2D-42EF-879A-03B0DA36F212}|Fin.Domain\\Fin.Domain.csproj|solutionrelative:fin.domain\\users\\dtos\\userdeleterequestdto.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" + }, + { + "AbsoluteMoniker": "D:0:0:{E698F897-BB2D-42EF-879A-03B0DA36F212}|Fin.Domain\\Fin.Domain.csproj|c:\\users\\gustavo passos\\desktop\\develop\\fin\\fin-backend\\fin.domain\\notifications\\entities\\notificationuserdelivery.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", + "RelativeMoniker": "D:0:0:{E698F897-BB2D-42EF-879A-03B0DA36F212}|Fin.Domain\\Fin.Domain.csproj|solutionrelative:fin.domain\\notifications\\entities\\notificationuserdelivery.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" + }, + { + "AbsoluteMoniker": "D:0:0:{E698F897-BB2D-42EF-879A-03B0DA36F212}|Fin.Domain\\Fin.Domain.csproj|c:\\users\\gustavo passos\\desktop\\develop\\fin\\fin-backend\\fin.domain\\notifications\\enums\\notificationway.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", + "RelativeMoniker": "D:0:0:{E698F897-BB2D-42EF-879A-03B0DA36F212}|Fin.Domain\\Fin.Domain.csproj|solutionrelative:fin.domain\\notifications\\enums\\notificationway.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" + }, + { + "AbsoluteMoniker": "D:0:0:{8A2A53B4-B6F1-47DC-A681-E1819AE39A0B}|Fin.Infrastructure\\Fin.Infrastructure.csproj|c:\\users\\gustavo passos\\desktop\\develop\\fin\\fin-backend\\fin.infrastructure\\unitofworks\\unitofwork.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", + "RelativeMoniker": "D:0:0:{8A2A53B4-B6F1-47DC-A681-E1819AE39A0B}|Fin.Infrastructure\\Fin.Infrastructure.csproj|solutionrelative:fin.infrastructure\\unitofworks\\unitofwork.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" + }, + { + "AbsoluteMoniker": "D:0:0:{8A2A53B4-B6F1-47DC-A681-E1819AE39A0B}|Fin.Infrastructure\\Fin.Infrastructure.csproj|c:\\users\\gustavo passos\\desktop\\develop\\fin\\fin-backend\\fin.infrastructure\\authentications\\enums\\loginerrorcode.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", + "RelativeMoniker": "D:0:0:{8A2A53B4-B6F1-47DC-A681-E1819AE39A0B}|Fin.Infrastructure\\Fin.Infrastructure.csproj|solutionrelative:fin.infrastructure\\authentications\\enums\\loginerrorcode.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" + }, + { + "AbsoluteMoniker": "D:0:0:{8A2A53B4-B6F1-47DC-A681-E1819AE39A0B}|Fin.Infrastructure\\Fin.Infrastructure.csproj|c:\\users\\gustavo passos\\desktop\\develop\\fin\\fin-backend\\fin.infrastructure\\authentications\\dtos\\logininput.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", + "RelativeMoniker": "D:0:0:{8A2A53B4-B6F1-47DC-A681-E1819AE39A0B}|Fin.Infrastructure\\Fin.Infrastructure.csproj|solutionrelative:fin.infrastructure\\authentications\\dtos\\logininput.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" + }, + { + "AbsoluteMoniker": "D:0:0:{8A2A53B4-B6F1-47DC-A681-E1819AE39A0B}|Fin.Infrastructure\\Fin.Infrastructure.csproj|c:\\users\\gustavo passos\\desktop\\develop\\fin\\fin-backend\\fin.infrastructure\\authentications\\dtos\\loginoutput.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", + "RelativeMoniker": "D:0:0:{8A2A53B4-B6F1-47DC-A681-E1819AE39A0B}|Fin.Infrastructure\\Fin.Infrastructure.csproj|solutionrelative:fin.infrastructure\\authentications\\dtos\\loginoutput.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" + }, + { + "AbsoluteMoniker": "D:0:0:{8A2A53B4-B6F1-47DC-A681-E1819AE39A0B}|Fin.Infrastructure\\Fin.Infrastructure.csproj|c:\\users\\gustavo passos\\desktop\\develop\\fin\\fin-backend\\fin.infrastructure\\firebases\\firebaseclient.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", + "RelativeMoniker": "D:0:0:{8A2A53B4-B6F1-47DC-A681-E1819AE39A0B}|Fin.Infrastructure\\Fin.Infrastructure.csproj|solutionrelative:fin.infrastructure\\firebases\\firebaseclient.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" + }, + { + "AbsoluteMoniker": "D:0:0:{8A2A53B4-B6F1-47DC-A681-E1819AE39A0B}|Fin.Infrastructure\\Fin.Infrastructure.csproj|c:\\users\\gustavo passos\\desktop\\develop\\fin\\fin-backend\\fin.infrastructure\\firebases\\firebasenotificationservice.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", + "RelativeMoniker": "D:0:0:{8A2A53B4-B6F1-47DC-A681-E1819AE39A0B}|Fin.Infrastructure\\Fin.Infrastructure.csproj|solutionrelative:fin.infrastructure\\firebases\\firebasenotificationservice.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" + }, + { + "AbsoluteMoniker": "D:0:0:{8A2A53B4-B6F1-47DC-A681-E1819AE39A0B}|Fin.Infrastructure\\Fin.Infrastructure.csproj|c:\\users\\gustavo passos\\desktop\\develop\\fin\\fin-backend\\fin.infrastructure\\firebases\\addfirebaseextension.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", + "RelativeMoniker": "D:0:0:{8A2A53B4-B6F1-47DC-A681-E1819AE39A0B}|Fin.Infrastructure\\Fin.Infrastructure.csproj|solutionrelative:fin.infrastructure\\firebases\\addfirebaseextension.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" + }, + { + "AbsoluteMoniker": "D:0:0:{8A2A53B4-B6F1-47DC-A681-E1819AE39A0B}|Fin.Infrastructure\\Fin.Infrastructure.csproj|c:\\users\\gustavo passos\\desktop\\develop\\fin\\fin-backend\\fin.infrastructure\\redis\\addredisextension.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", + "RelativeMoniker": "D:0:0:{8A2A53B4-B6F1-47DC-A681-E1819AE39A0B}|Fin.Infrastructure\\Fin.Infrastructure.csproj|solutionrelative:fin.infrastructure\\redis\\addredisextension.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" + }, + { + "AbsoluteMoniker": "D:0:0:{8A2A53B4-B6F1-47DC-A681-E1819AE39A0B}|Fin.Infrastructure\\Fin.Infrastructure.csproj|c:\\users\\gustavo passos\\desktop\\develop\\fin\\fin-backend\\fin.infrastructure\\notifications\\hubs\\notificationhub.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", + "RelativeMoniker": "D:0:0:{8A2A53B4-B6F1-47DC-A681-E1819AE39A0B}|Fin.Infrastructure\\Fin.Infrastructure.csproj|solutionrelative:fin.infrastructure\\notifications\\hubs\\notificationhub.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" + }, + { + "AbsoluteMoniker": "D:0:0:{8A2A53B4-B6F1-47DC-A681-E1819AE39A0B}|Fin.Infrastructure\\Fin.Infrastructure.csproj|c:\\users\\gustavo passos\\desktop\\develop\\fin\\fin-backend\\fin.infrastructure\\notifications\\hubs\\customuseridprovider.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", + "RelativeMoniker": "D:0:0:{8A2A53B4-B6F1-47DC-A681-E1819AE39A0B}|Fin.Infrastructure\\Fin.Infrastructure.csproj|solutionrelative:fin.infrastructure\\notifications\\hubs\\customuseridprovider.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" + }, + { + "AbsoluteMoniker": "D:0:0:{8A2A53B4-B6F1-47DC-A681-E1819AE39A0B}|Fin.Infrastructure\\Fin.Infrastructure.csproj|c:\\users\\gustavo passos\\desktop\\develop\\fin\\fin-backend\\fin.infrastructure\\notifications\\hubs\\addnotificationbackgroundjob.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", + "RelativeMoniker": "D:0:0:{8A2A53B4-B6F1-47DC-A681-E1819AE39A0B}|Fin.Infrastructure\\Fin.Infrastructure.csproj|solutionrelative:fin.infrastructure\\notifications\\hubs\\addnotificationbackgroundjob.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" + }, + { + "AbsoluteMoniker": "D:0:0:{8A2A53B4-B6F1-47DC-A681-E1819AE39A0B}|Fin.Infrastructure\\Fin.Infrastructure.csproj|c:\\users\\gustavo passos\\desktop\\develop\\fin\\fin-backend\\fin.infrastructure\\consts\\appconsts.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", + "RelativeMoniker": "D:0:0:{8A2A53B4-B6F1-47DC-A681-E1819AE39A0B}|Fin.Infrastructure\\Fin.Infrastructure.csproj|solutionrelative:fin.infrastructure\\consts\\appconsts.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" + }, + { + "AbsoluteMoniker": "D:0:0:{8A2A53B4-B6F1-47DC-A681-E1819AE39A0B}|Fin.Infrastructure\\Fin.Infrastructure.csproj|c:\\users\\gustavo passos\\desktop\\develop\\fin\\fin-backend\\fin.infrastructure\\firebases\\firebasesendresult.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", + "RelativeMoniker": "D:0:0:{8A2A53B4-B6F1-47DC-A681-E1819AE39A0B}|Fin.Infrastructure\\Fin.Infrastructure.csproj|solutionrelative:fin.infrastructure\\firebases\\firebasesendresult.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" + } + ], + "DocumentGroupContainers": [ + { + "Orientation": 0, + "VerticalTabListWidth": 256, + "DocumentGroups": [ + { + "DockedWidth": 200, + "SelectedChildIndex": 8, + "Children": [ + { + "$type": "Document", + "DocumentIndex": 3, + "Title": "RandomGenerator.cs", + "DocumentMoniker": "C:\\Users\\Gustavo Passos\\Desktop\\Develop\\Fin\\fin-backend\\Fin.Application\\Globals\\Services\\RandomGenerator.cs", + "RelativeDocumentMoniker": "Fin.Application\\Globals\\Services\\RandomGenerator.cs", + "ToolTip": "C:\\Users\\Gustavo Passos\\Desktop\\Develop\\Fin\\fin-backend\\Fin.Application\\Globals\\Services\\RandomGenerator.cs", + "RelativeToolTip": "Fin.Application\\Globals\\Services\\RandomGenerator.cs", + "ViewState": "AgIAAAAAAAAAAAAAAAAAAAsAAAAbAAAAAAAAAA==", + "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", + "WhenOpened": "2025-08-15T14:49:22.412Z", + "EditorCaption": "" + }, + { + "$type": "Document", + "DocumentIndex": 4, + "Title": "AuthenticationService.cs", + "DocumentMoniker": "C:\\Users\\Gustavo Passos\\Desktop\\Develop\\Fin\\fin-backend\\Fin.Application\\Authentications\\Services\\AuthenticationService.cs", + "RelativeDocumentMoniker": "Fin.Application\\Authentications\\Services\\AuthenticationService.cs", + "ToolTip": "C:\\Users\\Gustavo Passos\\Desktop\\Develop\\Fin\\fin-backend\\Fin.Application\\Authentications\\Services\\AuthenticationService.cs", + "RelativeToolTip": "Fin.Application\\Authentications\\Services\\AuthenticationService.cs", + "ViewState": "AgIAABMAAAAAAAAAAIAwwCAAAAAAAAAAAAAAAA==", + "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", + "WhenOpened": "2025-08-15T14:18:54.877Z", + "EditorCaption": "" + }, + { + "$type": "Document", + "DocumentIndex": 5, + "Title": "UserDto.cs", + "DocumentMoniker": "C:\\Users\\Gustavo Passos\\Desktop\\Develop\\Fin\\fin-backend\\Fin.Domain\\Users\\Dtos\\UserDto.cs", + "RelativeDocumentMoniker": "Fin.Domain\\Users\\Dtos\\UserDto.cs", + "ToolTip": "C:\\Users\\Gustavo Passos\\Desktop\\Develop\\Fin\\fin-backend\\Fin.Domain\\Users\\Dtos\\UserDto.cs*", + "RelativeToolTip": "Fin.Domain\\Users\\Dtos\\UserDto.cs*", + "ViewState": "AgIAAAMAAAAAAAAAAAAAABEAAAAWAAAAAAAAAA==", + "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", + "WhenOpened": "2025-08-15T14:05:36.16Z", + "EditorCaption": "" + }, + { + "$type": "Document", + "DocumentIndex": 6, + "Title": "UserDeleteRequestDto.cs", + "DocumentMoniker": "C:\\Users\\Gustavo Passos\\Desktop\\Develop\\Fin\\fin-backend\\Fin.Domain\\Users\\Dtos\\UserDeleteRequestDto.cs", + "RelativeDocumentMoniker": "Fin.Domain\\Users\\Dtos\\UserDeleteRequestDto.cs", + "ToolTip": "C:\\Users\\Gustavo Passos\\Desktop\\Develop\\Fin\\fin-backend\\Fin.Domain\\Users\\Dtos\\UserDeleteRequestDto.cs", + "RelativeToolTip": "Fin.Domain\\Users\\Dtos\\UserDeleteRequestDto.cs", + "ViewState": "AgIAAAcAAAAAAAAAAAAQwB4AAAA1AAAAAAAAAA==", + "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", + "WhenOpened": "2025-08-15T14:05:29.028Z", + "EditorCaption": "" + }, + { + "$type": "Document", + "DocumentIndex": 7, + "Title": "NotificationUserDelivery.cs", + "DocumentMoniker": "C:\\Users\\Gustavo Passos\\Desktop\\Develop\\Fin\\fin-backend\\Fin.Domain\\Notifications\\Entities\\NotificationUserDelivery.cs", + "RelativeDocumentMoniker": "Fin.Domain\\Notifications\\Entities\\NotificationUserDelivery.cs", + "ToolTip": "C:\\Users\\Gustavo Passos\\Desktop\\Develop\\Fin\\fin-backend\\Fin.Domain\\Notifications\\Entities\\NotificationUserDelivery.cs", + "RelativeToolTip": "Fin.Domain\\Notifications\\Entities\\NotificationUserDelivery.cs", + "ViewState": "AgIAAAAAAAAAAAAAAAAAABEAAAAFAAAAAAAAAA==", + "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", + "WhenOpened": "2025-08-15T13:11:44.769Z", + "EditorCaption": "" + }, + { + "$type": "Document", + "DocumentIndex": 8, + "Title": "NotificationWay.cs", + "DocumentMoniker": "C:\\Users\\Gustavo Passos\\Desktop\\Develop\\Fin\\fin-backend\\Fin.Domain\\Notifications\\Enums\\NotificationWay.cs", + "RelativeDocumentMoniker": "Fin.Domain\\Notifications\\Enums\\NotificationWay.cs", + "ToolTip": "C:\\Users\\Gustavo Passos\\Desktop\\Develop\\Fin\\fin-backend\\Fin.Domain\\Notifications\\Enums\\NotificationWay.cs", + "RelativeToolTip": "Fin.Domain\\Notifications\\Enums\\NotificationWay.cs", + "ViewState": "AgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", + "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", + "WhenOpened": "2025-08-15T12:39:58.617Z", + "EditorCaption": "" + }, + { + "$type": "Document", + "DocumentIndex": 1, + "Title": "TenantUser.cs", + "DocumentMoniker": "C:\\Users\\Gustavo Passos\\Desktop\\Develop\\Fin\\fin-backend\\Fin.Domain\\Tenants\\Entities\\TenantUser.cs", + "RelativeDocumentMoniker": "Fin.Domain\\Tenants\\Entities\\TenantUser.cs", + "ToolTip": "C:\\Users\\Gustavo Passos\\Desktop\\Develop\\Fin\\fin-backend\\Fin.Domain\\Tenants\\Entities\\TenantUser.cs", + "RelativeToolTip": "Fin.Domain\\Tenants\\Entities\\TenantUser.cs", + "ViewState": "AgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", + "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", + "WhenOpened": "2025-08-15T12:39:48.55Z", + "EditorCaption": "" + }, + { + "$type": "Document", + "DocumentIndex": 2, + "Title": "Tenant.cs", + "DocumentMoniker": "C:\\Users\\Gustavo Passos\\Desktop\\Develop\\Fin\\fin-backend\\Fin.Domain\\Tenants\\Entities\\Tenant.cs", + "RelativeDocumentMoniker": "Fin.Domain\\Tenants\\Entities\\Tenant.cs", + "ToolTip": "C:\\Users\\Gustavo Passos\\Desktop\\Develop\\Fin\\fin-backend\\Fin.Domain\\Tenants\\Entities\\Tenant.cs", + "RelativeToolTip": "Fin.Domain\\Tenants\\Entities\\Tenant.cs", + "ViewState": "AgIAAAUAAAAAAAAAAAAxwBwAAAABAAAAAAAAAA==", + "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", + "WhenOpened": "2025-08-15T12:39:38.11Z", + "EditorCaption": "" + }, + { + "$type": "Document", + "DocumentIndex": 0, + "Title": "ActivatedUserMiddleware.cs", + "DocumentMoniker": "C:\\Users\\Gustavo Passos\\Desktop\\Develop\\Fin\\fin-backend\\Fin.Infrastructure\\Authentications\\ActivatedUserMiddleware.cs", + "RelativeDocumentMoniker": "Fin.Infrastructure\\Authentications\\ActivatedUserMiddleware.cs", + "ToolTip": "C:\\Users\\Gustavo Passos\\Desktop\\Develop\\Fin\\fin-backend\\Fin.Infrastructure\\Authentications\\ActivatedUserMiddleware.cs", + "RelativeToolTip": "Fin.Infrastructure\\Authentications\\ActivatedUserMiddleware.cs", + "ViewState": "AgIAAAYAAAAAAAAAAAAAABcAAAAJAAAAAAAAAA==", + "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", + "WhenOpened": "2025-08-15T12:38:34.582Z", + "EditorCaption": "" + }, + { + "$type": "Document", + "DocumentIndex": 9, + "Title": "UnitOfWork.cs", + "DocumentMoniker": "C:\\Users\\Gustavo Passos\\Desktop\\Develop\\Fin\\fin-backend\\Fin.Infrastructure\\UnitOfWorks\\UnitOfWork.cs", + "RelativeDocumentMoniker": "Fin.Infrastructure\\UnitOfWorks\\UnitOfWork.cs", + "ToolTip": "C:\\Users\\Gustavo Passos\\Desktop\\Develop\\Fin\\fin-backend\\Fin.Infrastructure\\UnitOfWorks\\UnitOfWork.cs", + "RelativeToolTip": "Fin.Infrastructure\\UnitOfWorks\\UnitOfWork.cs", + "ViewState": "AgIAADIAAAAAAAAAAAAUwAAAAAAAAAAAAAAAAA==", + "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", + "WhenOpened": "2025-08-15T12:39:08.167Z", + "EditorCaption": "" + }, + { + "$type": "Document", + "DocumentIndex": 10, + "Title": "LoginErrorCode.cs", + "DocumentMoniker": "C:\\Users\\Gustavo Passos\\Desktop\\Develop\\Fin\\fin-backend\\Fin.Infrastructure\\Authentications\\Enums\\LoginErrorCode.cs", + "RelativeDocumentMoniker": "Fin.Infrastructure\\Authentications\\Enums\\LoginErrorCode.cs", + "ToolTip": "C:\\Users\\Gustavo Passos\\Desktop\\Develop\\Fin\\fin-backend\\Fin.Infrastructure\\Authentications\\Enums\\LoginErrorCode.cs", + "RelativeToolTip": "Fin.Infrastructure\\Authentications\\Enums\\LoginErrorCode.cs", + "ViewState": "AgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", + "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", + "WhenOpened": "2025-08-15T12:38:20.554Z", + "EditorCaption": "" + }, + { + "$type": "Document", + "DocumentIndex": 12, + "Title": "LoginOutput.cs", + "DocumentMoniker": "C:\\Users\\Gustavo Passos\\Desktop\\Develop\\Fin\\fin-backend\\Fin.Infrastructure\\Authentications\\Dtos\\LoginOutput.cs", + "RelativeDocumentMoniker": "Fin.Infrastructure\\Authentications\\Dtos\\LoginOutput.cs", + "ToolTip": "C:\\Users\\Gustavo Passos\\Desktop\\Develop\\Fin\\fin-backend\\Fin.Infrastructure\\Authentications\\Dtos\\LoginOutput.cs", + "RelativeToolTip": "Fin.Infrastructure\\Authentications\\Dtos\\LoginOutput.cs", + "ViewState": "AgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", + "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", + "WhenOpened": "2025-08-15T12:38:03.449Z", + "EditorCaption": "" + }, + { + "$type": "Document", + "DocumentIndex": 11, + "Title": "LoginInput.cs", + "DocumentMoniker": "C:\\Users\\Gustavo Passos\\Desktop\\Develop\\Fin\\fin-backend\\Fin.Infrastructure\\Authentications\\Dtos\\LoginInput.cs", + "RelativeDocumentMoniker": "Fin.Infrastructure\\Authentications\\Dtos\\LoginInput.cs", + "ToolTip": "C:\\Users\\Gustavo Passos\\Desktop\\Develop\\Fin\\fin-backend\\Fin.Infrastructure\\Authentications\\Dtos\\LoginInput.cs", + "RelativeToolTip": "Fin.Infrastructure\\Authentications\\Dtos\\LoginInput.cs", + "ViewState": "AgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", + "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", + "WhenOpened": "2025-08-15T12:38:02.088Z", + "EditorCaption": "" + }, + { + "$type": "Document", + "DocumentIndex": 13, + "Title": "FirebaseClient.cs", + "DocumentMoniker": "C:\\Users\\Gustavo Passos\\Desktop\\Develop\\Fin\\fin-backend\\Fin.Infrastructure\\Firebases\\FirebaseClient.cs", + "RelativeDocumentMoniker": "Fin.Infrastructure\\Firebases\\FirebaseClient.cs", + "ToolTip": "C:\\Users\\Gustavo Passos\\Desktop\\Develop\\Fin\\fin-backend\\Fin.Infrastructure\\Firebases\\FirebaseClient.cs", + "RelativeToolTip": "Fin.Infrastructure\\Firebases\\FirebaseClient.cs", + "ViewState": "AgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", + "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", + "WhenOpened": "2025-08-15T12:37:26.15Z", + "EditorCaption": "" + }, + { + "$type": "Document", + "DocumentIndex": 14, + "Title": "FirebaseNotificationService.cs", + "DocumentMoniker": "C:\\Users\\Gustavo Passos\\Desktop\\Develop\\Fin\\fin-backend\\Fin.Infrastructure\\Firebases\\FirebaseNotificationService.cs", + "RelativeDocumentMoniker": "Fin.Infrastructure\\Firebases\\FirebaseNotificationService.cs", + "ToolTip": "C:\\Users\\Gustavo Passos\\Desktop\\Develop\\Fin\\fin-backend\\Fin.Infrastructure\\Firebases\\FirebaseNotificationService.cs", + "RelativeToolTip": "Fin.Infrastructure\\Firebases\\FirebaseNotificationService.cs", + "ViewState": "AgIAAAEAAAAAAAAAAAAgwAwAAAAkAAAAAAAAAA==", + "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", + "WhenOpened": "2025-08-15T12:36:52.361Z", + "EditorCaption": "" + }, + { + "$type": "Document", + "DocumentIndex": 15, + "Title": "AddFirebaseExtension.cs", + "DocumentMoniker": "C:\\Users\\Gustavo Passos\\Desktop\\Develop\\Fin\\fin-backend\\Fin.Infrastructure\\Firebases\\AddFirebaseExtension.cs", + "RelativeDocumentMoniker": "Fin.Infrastructure\\Firebases\\AddFirebaseExtension.cs", + "ToolTip": "C:\\Users\\Gustavo Passos\\Desktop\\Develop\\Fin\\fin-backend\\Fin.Infrastructure\\Firebases\\AddFirebaseExtension.cs", + "RelativeToolTip": "Fin.Infrastructure\\Firebases\\AddFirebaseExtension.cs", + "ViewState": "AgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", + "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", + "WhenOpened": "2025-08-15T12:36:48.056Z", + "EditorCaption": "" + }, + { + "$type": "Document", + "DocumentIndex": 16, + "Title": "AddRedisExtension.cs", + "DocumentMoniker": "C:\\Users\\Gustavo Passos\\Desktop\\Develop\\Fin\\fin-backend\\Fin.Infrastructure\\Redis\\AddRedisExtension.cs", + "RelativeDocumentMoniker": "Fin.Infrastructure\\Redis\\AddRedisExtension.cs", + "ToolTip": "C:\\Users\\Gustavo Passos\\Desktop\\Develop\\Fin\\fin-backend\\Fin.Infrastructure\\Redis\\AddRedisExtension.cs", + "RelativeToolTip": "Fin.Infrastructure\\Redis\\AddRedisExtension.cs", + "ViewState": "AgIAAAAAAAAAAAAAAAAAABcAAAABAAAAAAAAAA==", + "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", + "WhenOpened": "2025-08-15T12:36:15.37Z", + "EditorCaption": "" + }, + { + "$type": "Document", + "DocumentIndex": 17, + "Title": "NotificationHub.cs", + "DocumentMoniker": "C:\\Users\\Gustavo Passos\\Desktop\\Develop\\Fin\\fin-backend\\Fin.Infrastructure\\Notifications\\Hubs\\NotificationHub.cs", + "RelativeDocumentMoniker": "Fin.Infrastructure\\Notifications\\Hubs\\NotificationHub.cs", + "ToolTip": "C:\\Users\\Gustavo Passos\\Desktop\\Develop\\Fin\\fin-backend\\Fin.Infrastructure\\Notifications\\Hubs\\NotificationHub.cs", + "RelativeToolTip": "Fin.Infrastructure\\Notifications\\Hubs\\NotificationHub.cs", + "ViewState": "AgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", + "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", + "WhenOpened": "2025-08-15T12:36:12.834Z", + "EditorCaption": "" + }, + { + "$type": "Document", + "DocumentIndex": 18, + "Title": "CustomUserIdProvider.cs", + "DocumentMoniker": "C:\\Users\\Gustavo Passos\\Desktop\\Develop\\Fin\\fin-backend\\Fin.Infrastructure\\Notifications\\Hubs\\CustomUserIdProvider.cs", + "RelativeDocumentMoniker": "Fin.Infrastructure\\Notifications\\Hubs\\CustomUserIdProvider.cs", + "ToolTip": "C:\\Users\\Gustavo Passos\\Desktop\\Develop\\Fin\\fin-backend\\Fin.Infrastructure\\Notifications\\Hubs\\CustomUserIdProvider.cs", + "RelativeToolTip": "Fin.Infrastructure\\Notifications\\Hubs\\CustomUserIdProvider.cs", + "ViewState": "AgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", + "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", + "WhenOpened": "2025-08-15T12:36:09.812Z", + "EditorCaption": "" + }, + { + "$type": "Document", + "DocumentIndex": 19, + "Title": "AddNotificationBackgroundJob.cs", + "DocumentMoniker": "C:\\Users\\Gustavo Passos\\Desktop\\Develop\\Fin\\fin-backend\\Fin.Infrastructure\\Notifications\\Hubs\\AddNotificationBackgroundJob.cs", + "RelativeDocumentMoniker": "Fin.Infrastructure\\Notifications\\Hubs\\AddNotificationBackgroundJob.cs", + "ToolTip": "C:\\Users\\Gustavo Passos\\Desktop\\Develop\\Fin\\fin-backend\\Fin.Infrastructure\\Notifications\\Hubs\\AddNotificationBackgroundJob.cs", + "RelativeToolTip": "Fin.Infrastructure\\Notifications\\Hubs\\AddNotificationBackgroundJob.cs", + "ViewState": "AgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", + "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", + "WhenOpened": "2025-08-15T12:36:07.883Z", + "EditorCaption": "" + }, + { + "$type": "Document", + "DocumentIndex": 20, + "Title": "AppConsts.cs", + "DocumentMoniker": "C:\\Users\\Gustavo Passos\\Desktop\\Develop\\Fin\\fin-backend\\Fin.Infrastructure\\Consts\\AppConsts.cs", + "RelativeDocumentMoniker": "Fin.Infrastructure\\Consts\\AppConsts.cs", + "ToolTip": "C:\\Users\\Gustavo Passos\\Desktop\\Develop\\Fin\\fin-backend\\Fin.Infrastructure\\Consts\\AppConsts.cs", + "RelativeToolTip": "Fin.Infrastructure\\Consts\\AppConsts.cs", + "ViewState": "AgIAAAAAAAAAAAAAAAAAAAUAAAABAAAAAAAAAA==", + "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", + "WhenOpened": "2025-08-15T12:35:46.004Z", + "EditorCaption": "" + }, + { + "$type": "Document", + "DocumentIndex": 21, + "Title": "FirebaseSendResult.cs", + "DocumentMoniker": "C:\\Users\\Gustavo Passos\\Desktop\\Develop\\Fin\\fin-backend\\Fin.Infrastructure\\Firebases\\FirebaseSendResult.cs", + "RelativeDocumentMoniker": "Fin.Infrastructure\\Firebases\\FirebaseSendResult.cs", + "ToolTip": "C:\\Users\\Gustavo Passos\\Desktop\\Develop\\Fin\\fin-backend\\Fin.Infrastructure\\Firebases\\FirebaseSendResult.cs", + "RelativeToolTip": "Fin.Infrastructure\\Firebases\\FirebaseSendResult.cs", + "ViewState": "AgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", + "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", + "WhenOpened": "2025-08-15T12:32:54.541Z", + "EditorCaption": "" + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/.vs/ProjectEvaluation/fin-backend.metadata.v9.bin b/.vs/ProjectEvaluation/fin-backend.metadata.v9.bin new file mode 100644 index 0000000..f3a089d Binary files /dev/null and b/.vs/ProjectEvaluation/fin-backend.metadata.v9.bin differ diff --git a/.vs/ProjectEvaluation/fin-backend.projects.v9.bin b/.vs/ProjectEvaluation/fin-backend.projects.v9.bin new file mode 100644 index 0000000..8d5050b Binary files /dev/null and b/.vs/ProjectEvaluation/fin-backend.projects.v9.bin differ diff --git a/.vs/ProjectEvaluation/fin-backend.strings.v9.bin b/.vs/ProjectEvaluation/fin-backend.strings.v9.bin new file mode 100644 index 0000000..c462925 Binary files /dev/null and b/.vs/ProjectEvaluation/fin-backend.strings.v9.bin differ diff --git a/Fin.Api/Fin.Api.csproj.user b/Fin.Api/Fin.Api.csproj.user new file mode 100644 index 0000000..9ff5820 --- /dev/null +++ b/Fin.Api/Fin.Api.csproj.user @@ -0,0 +1,6 @@ + + + + https + + \ No newline at end of file diff --git a/Fin.Api/Program.cs b/Fin.Api/Program.cs index 4e6f02a..99e99bf 100644 --- a/Fin.Api/Program.cs +++ b/Fin.Api/Program.cs @@ -1,7 +1,7 @@ using Fin.Application.Notifications.Extensions; using Fin.Infrastructure.Constants; using Fin.Infrastructure.Extensions; -using Fin.Infrastructure.Notifications.Hubs; +using Fin.Infrastructure.Seeders.Extensions; using Hangfire; var builder = WebApplication.CreateBuilder(args); @@ -10,7 +10,6 @@ builder.Services .AddInfrastructure(builder.Configuration) - .AddNotifications() .AddOpenApiDocument() .AddCors(options => { @@ -19,7 +18,8 @@ { policy.WithOrigins(frontEndUrl) .AllowAnyHeader() - .AllowAnyMethod(); + .AllowAnyMethod() + .AllowCredentials(); }); }) .AddControllers(); @@ -36,9 +36,9 @@ app.UseCors("AllowAngularLocalhost"); } - app.UseNotifications(); app.UseFinMiddlewares(); +await app.UseSeeders(); app.UseAuthentication(); app.UseAuthorization(); diff --git a/Fin.Application/Notifications/Extensions/UseNotificationExtension.cs b/Fin.Application/Notifications/Extensions/UseNotificationExtension.cs index 0f51081..6fcc2bb 100644 --- a/Fin.Application/Notifications/Extensions/UseNotificationExtension.cs +++ b/Fin.Application/Notifications/Extensions/UseNotificationExtension.cs @@ -7,6 +7,7 @@ public static class UseNotificationExtension { public static void UseNotifications(this WebApplication app) { - app.MapHub("/notifications"); + app.MapHub("/notifications-hub"); } + } \ No newline at end of file diff --git a/Fin.Application/Notifications/Services/DeliveryServices/NotificationDeliveryService.cs b/Fin.Application/Notifications/Services/DeliveryServices/NotificationDeliveryService.cs index 54b1f9e..b16006b 100644 --- a/Fin.Application/Notifications/Services/DeliveryServices/NotificationDeliveryService.cs +++ b/Fin.Application/Notifications/Services/DeliveryServices/NotificationDeliveryService.cs @@ -15,6 +15,7 @@ using Microsoft.AspNetCore.SignalR; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; using Notification = FirebaseAdmin.Messaging.Notification; namespace Fin.Application.Notifications.Services.DeliveryServices; @@ -32,19 +33,19 @@ public class NotificationDeliveryService( IRepository userSettingsRepository, IConfiguration configuration, IAmbientData ambientData, - IDateTimeProvider _dateTimeProvider, - + IDateTimeProvider dateTimeProvider, IHubContext hubContext, IEmailSenderService emailSenderService, - IFirebaseNotificationService firebaseNotification - ) + IFirebaseNotificationService firebaseNotification, + ILogger logger) : INotificationDeliveryService, IAutoTransient { private readonly string SEND_NOTIFICATION_ACTION = "ReceiveNotification"; + private readonly CryptoHelper _cryptoHelper = new( configuration.GetSection(AuthenticationConstants.EncryptKeyConfigKey).Value ?? "", configuration.GetSection(AuthenticationConstants.EncryptIvConfigKey).Value ?? "" - ); + ); public async Task SendNotification(NotifyUserDto notifyUser, bool autoSave = true) @@ -52,31 +53,46 @@ public async Task SendNotification(NotifyUserDto notifyUser, bool autoSave = tru var notificationDelivery = await deliveryRepository.Query() .FirstOrDefaultAsync(n => n.NotificationId == notifyUser.NotificationId && n.UserId == notifyUser.UserId); if (notificationDelivery == null) - throw new Exception($"Notification not found to send. NotificationId {notifyUser.NotificationId}, UserId {notifyUser.UserId}"); + throw new Exception( + $"Notification not found to send. NotificationId {notifyUser.NotificationId}, UserId {notifyUser.UserId}"); var userSettings = await userSettingsRepository.Query() .FirstOrDefaultAsync(u => u.UserId == notifyUser.UserId); if (userSettings == null) - throw new Exception($"User Notification Settings not found to send. NotificationId {notifyUser.NotificationId}, UserId {notifyUser.UserId}"); + throw new Exception( + $"User Notification Settings not found to send. NotificationId {notifyUser.NotificationId}, UserId {notifyUser.UserId}"); - foreach (var way in notifyUser.Ways) - { - if (way is NotificationWay.Snack or NotificationWay.Message) - { - await hubContext.Clients.User(notifyUser.UserId.ToString()).SendAsync(SEND_NOTIFICATION_ACTION, notifyUser); - continue; - } + var allowedWaysToSend = userSettings.AllowedWays.Intersect(notifyUser.Ways).ToList(); - if (!userSettings.Enabled || !userSettings.AllowedWays.Contains(way)) continue; + if (!userSettings.Enabled || !allowedWaysToSend.Any()) + { + logger.LogWarning( + "User {userId}, don't get notification {notificationId} on any way because notifications is disabled or any way is allowed.", + notifyUser.UserId, notifyUser.NotificationId); + return; + } - if (way is NotificationWay.Push) - await SendPush(notifyUser, userSettings, false); - if (way is NotificationWay.Email) - await SendEmail(notifyUser); + try + { + if ( + allowedWaysToSend.Contains(NotificationWay.Snack) + | allowedWaysToSend.Contains(NotificationWay.Message) + | allowedWaysToSend.Contains(NotificationWay.Push) + ) await SendWebSocket(notifyUser); + if (allowedWaysToSend.Contains(NotificationWay.Push)) + await SendFirebase(notifyUser, userSettings, false); + if (allowedWaysToSend.Contains(NotificationWay.Email)) + await SendEmail(notifyUser); + notificationDelivery.MarkAsDelivered(); + } + catch (Exception e) + { + logger.LogError( + "Error on send notification with id {notificationId} to user id: {userId}.\nError: {err}", + notifyUser.NotificationId, notifyUser.UserId, e.ToString()); } - notificationDelivery.MarkAsDelivered(); - await deliveryRepository.UpdateAsync(notificationDelivery, false); + await deliveryRepository.UpdateAsync(notificationDelivery); if (autoSave) await deliveryRepository.SaveChangesAsync(); @@ -87,7 +103,7 @@ public async Task MarkAsVisualized(Guid notificationId, bool autoSave = tr if (!ambientData.IsLogged) throw new UnauthorizedAccessException("User not logged"); - var userId = ambientData.UserId; + var userId = ambientData.UserId.Value; var notification = await deliveryRepository.Query() .FirstOrDefaultAsync(n => n.NotificationId == notificationId && n.UserId == userId); if (notification == null) @@ -100,37 +116,48 @@ public async Task MarkAsVisualized(Guid notificationId, bool autoSave = tr public async Task> GetUnvisualizedNotifications(bool autoSave = true) { - var userId = ambientData.UserId; - var now = _dateTimeProvider.UtcNow(); + if (!ambientData.IsLogged) + throw new UnauthorizedAccessException("User not logged"); + + var userId = ambientData.UserId.Value; + var now = dateTimeProvider.UtcNow(); - var notifications = await deliveryRepository.Query(tracking: false) + var userNotification = await deliveryRepository.Query(tracking: false) .Include(u => u.Notification) .Where(n => !n.Visualized && n.UserId == userId) - .Where(n => n.Notification.StartToDelivery <= now) + .Where(n => n.Notification.StartToDelivery <= now.AddMinutes(1)) .Where(n => !n.Notification.StopToDelivery.HasValue || n.Notification.StopToDelivery.Value >= now) - .ToListAsync(); - var userNotification = notifications - .Where(n => n.Notification.Ways.Contains(NotificationWay.Push) || - n.Notification.Ways.Contains(NotificationWay.Message) || - n.Notification.Ways.Contains(NotificationWay.Snack)) .Select(n => new NotifyUserDto(n.Notification, n)) + .ToListAsync(); + + userNotification = userNotification + .Where(n => n.Ways.Any(n => n != NotificationWay.Email)) + .ToList(); + + var notificationToMarkAsDelivery = userNotification + .Select(u => u.NotificationId) .ToList(); await deliveryRepository.Query() - .Where(n => userNotification.Select(u => u.NotificationId).Contains(n.NotificationId)) + .Where(n => notificationToMarkAsDelivery.Contains(n.NotificationId)) .ExecuteUpdateAsync(x => x - .SetProperty(a => a.Visualized, true)); + .SetProperty(a => a.Delivery, true)); if (autoSave) await deliveryRepository.SaveChangesAsync(); return userNotification; } - private async Task SendPush(NotifyUserDto notify, UserNotificationSettings userSettings, bool autoSave) + private async Task SendWebSocket(NotifyUserDto notifyUser) { - await hubContext.Clients.User(userSettings.UserId.ToString()).SendAsync(SEND_NOTIFICATION_ACTION, notify); + await hubContext.Clients.User(notifyUser.UserId.ToString()).SendAsync(SEND_NOTIFICATION_ACTION, notifyUser); + } + private async Task SendFirebase(NotifyUserDto notify, UserNotificationSettings userSettings, bool autoSave) + { + if (userSettings.FirebaseTokens is not { Count: > 0 }) return; + var messages = userSettings.FirebaseTokens .Select(t => new Message { @@ -140,6 +167,8 @@ private async Task SendPush(NotifyUserDto notify, UserNotificationSettings userS { "htmlBody", notify.HtmlBody }, { "textBody", notify.TextBody }, { "notificationId", notify.NotificationId.ToString() }, + { "severity", notify.Severity.ToString() }, + { "link", notify.Link }, }, Notification = new Notification { @@ -165,7 +194,7 @@ private async Task SendEmail(NotifyUserDto notification) var userCredencial = await credencialRepository.Query(false) .FirstOrDefaultAsync(n => n.UserId == notification.UserId); if (userCredencial == null) - throw new Exception($"User not found to send email notification. UserId {notification.UserId}, NotificationId {notification.NotificationId}"); + throw new Exception("User not found to send email notification."); var email = _cryptoHelper.Decrypt(userCredencial.EncryptedEmail); await emailSenderService.SendEmailAsync(email, notification.Title, notification.HtmlBody); diff --git a/Fin.Application/Users/Services/UserCreateService.cs b/Fin.Application/Users/Services/UserCreateService.cs index 4a5731f..77c5c15 100644 --- a/Fin.Application/Users/Services/UserCreateService.cs +++ b/Fin.Application/Users/Services/UserCreateService.cs @@ -8,6 +8,7 @@ using Fin.Domain.Tenants.Entities; using Fin.Domain.Users.Dtos; using Fin.Domain.Users.Entities; +using Fin.Domain.Users.Factories; using Fin.Infrastructure.Authentications.Constants; using Fin.Infrastructure.AutoServices.Interfaces; using Fin.Infrastructure.Constants; @@ -197,7 +198,7 @@ public async Task> CreateUser(string creationToken, var now = _dateTimeProvider.UtcNow(); var user = new User(input, now); - var credential = new UserCredential(user.Id, process.EncryptedEmail, process.EncryptedPassword); + var credential = UserCredentialFactory.Create(user.Id, process.EncryptedEmail, process.EncryptedPassword, UserCredentialFactoryType.Password); var tenant = new Tenant(now); user.Tenants.Add(tenant); @@ -205,15 +206,15 @@ public async Task> CreateUser(string creationToken, var notificationSetting = new UserNotificationSettings(user.Id, tenant.Id); var rememberUseSetting = new UserRememberUseSetting(user.Id, tenant.Id); - await _unitOfWork.BeginTransactionAsync(); - - await _tenantRepository.AddAsync(tenant); - await _userRepository.AddAsync(user); - await _credentialRepository.AddAsync(credential); - await _userRememberUseSettingRepository.AddAsync(rememberUseSetting); - await _notificationSettingsRepository.AddAsync(notificationSetting); - await _unitOfWork.CommitAsync(); - + await using (await _unitOfWork.BeginTransactionAsync()) + { + await _tenantRepository.AddAsync(tenant); + await _userRepository.AddAsync(user); + await _credentialRepository.AddAsync(credential); + await _userRememberUseSettingRepository.AddAsync(rememberUseSetting); + await _notificationSettingsRepository.AddAsync(notificationSetting); + await _unitOfWork.CommitAsync(); + } await _cache.RemoveAsync(GenerateProcessCacheKey(creationToken)); @@ -241,7 +242,7 @@ public async Task> CreateUser(string googleId, stri var now = _dateTimeProvider.UtcNow(); var user = new User(input, now); - var credential = UserCredential.CreateWithGoogle(user.Id, encryptedEmail, googleId); + var credential = UserCredentialFactory.Create(user.Id, encryptedEmail, googleId, UserCredentialFactoryType.Google); var tenant = new Tenant(now); user.Tenants.Add(tenant); @@ -249,14 +250,15 @@ public async Task> CreateUser(string googleId, stri var notificationSetting = new UserNotificationSettings(user.Id, tenant.Id); var rememberUseSetting = new UserRememberUseSetting(user.Id, tenant.Id); - await _unitOfWork.BeginTransactionAsync(); - await _tenantRepository.AddAsync(tenant); - await _userRepository.AddAsync(user); - await _credentialRepository.AddAsync(credential); - await _userRememberUseSettingRepository.AddAsync(rememberUseSetting); - await _notificationSettingsRepository.AddAsync(notificationSetting); - await _unitOfWork.CommitAsync(); - + await using (await _unitOfWork.BeginTransactionAsync()) + { + await _tenantRepository.AddAsync(tenant); + await _userRepository.AddAsync(user); + await _credentialRepository.AddAsync(credential); + await _userRememberUseSettingRepository.AddAsync(rememberUseSetting); + await _notificationSettingsRepository.AddAsync(notificationSetting); + await _unitOfWork.CommitAsync(); + } user.Tenants.First().Users = null; user.Credential.User = null; diff --git a/Fin.Application/Users/Services/UserDeleteService.cs b/Fin.Application/Users/Services/UserDeleteService.cs index abca735..033f3bd 100644 --- a/Fin.Application/Users/Services/UserDeleteService.cs +++ b/Fin.Application/Users/Services/UserDeleteService.cs @@ -153,31 +153,34 @@ private async Task DeleteUser(Guid userId, CancellationToken cancellationToken = var notifications = notificationDeliveries.Select(n => n.Notification) .Where(n => n.UserDeliveries.Count == 1); - await unitOfWork.BeginTransactionAsync(cancellationToken); + await using (await unitOfWork.BeginTransactionAsync(cancellationToken)) + { - foreach (var notification in notifications) - await notificationRepo.DeleteAsync(notification, cancellationToken); - foreach (var delivery in notificationDeliveries) - await notificationDeliveryRepo.DeleteAsync(delivery, cancellationToken); + foreach (var notification in notifications) + await notificationRepo.DeleteAsync(notification, cancellationToken); + foreach (var delivery in notificationDeliveries) + await notificationDeliveryRepo.DeleteAsync(delivery, cancellationToken); - await rememberRepo.DeleteAsync(rememberSetting, cancellationToken); - await notificationSettingsRepo.DeleteAsync(notificationSetting, cancellationToken); + await rememberRepo.DeleteAsync(rememberSetting, cancellationToken); + await notificationSettingsRepo.DeleteAsync(notificationSetting, cancellationToken); - foreach (var otherDeleteRequest in otherDeleteRequests) - await userDeleteRequestRepo.DeleteAsync(otherDeleteRequest, cancellationToken); + foreach (var otherDeleteRequest in otherDeleteRequests) + await userDeleteRequestRepo.DeleteAsync(otherDeleteRequest, cancellationToken); - foreach (var tenantUser in tenantsUser) - await tenantUserRepo.DeleteAsync(tenantUser, cancellationToken); + foreach (var tenantUser in tenantsUser) + await tenantUserRepo.DeleteAsync(tenantUser, cancellationToken); - foreach (var tenant in tenants) - await tenantRepo.DeleteAsync(tenant, cancellationToken); + foreach (var tenant in tenants) + await tenantRepo.DeleteAsync(tenant, cancellationToken); - await credentialRepo.DeleteAsync(credential, cancellationToken); - await userDeleteRequestRepo.DeleteAsync(deleteRequest, cancellationToken); - await userRepo.DeleteAsync(user, cancellationToken); + await credentialRepo.DeleteAsync(credential, cancellationToken); + await userDeleteRequestRepo.DeleteAsync(deleteRequest, cancellationToken); + await userRepo.DeleteAsync(user, cancellationToken); - await emailSender.SendEmailAsync(userEmail, "Conta deletada", "Sua conta no FinApp foi deletada. Agora você não poderá mais acessar seus dados e eles foram removidos da plataforma."); + await emailSender.SendEmailAsync(userEmail, "Conta deletada", + "Sua conta no FinApp foi deletada. Agora você não poderá mais acessar seus dados e eles foram removidos da plataforma."); - await unitOfWork.CommitAsync(cancellationToken); + await unitOfWork.CommitAsync(cancellationToken); + } } } \ No newline at end of file diff --git a/Fin.Domain/Global/Interfaces/ITenantEntity.cs b/Fin.Domain/Global/Interfaces/ITenantEntity.cs index df5590f..a26541f 100644 --- a/Fin.Domain/Global/Interfaces/ITenantEntity.cs +++ b/Fin.Domain/Global/Interfaces/ITenantEntity.cs @@ -1,6 +1,4 @@ -using System.ComponentModel.DataAnnotations; - -namespace Fin.Domain.Global.Interfaces; +namespace Fin.Domain.Global.Interfaces; public interface ITenantEntity: IEntity { diff --git a/Fin.Domain/Notifications/Dtos/NotificationInput.cs b/Fin.Domain/Notifications/Dtos/NotificationInput.cs index e5c4389..4dbe864 100644 --- a/Fin.Domain/Notifications/Dtos/NotificationInput.cs +++ b/Fin.Domain/Notifications/Dtos/NotificationInput.cs @@ -12,4 +12,6 @@ public class NotificationInput public DateTime StartToDelivery { get; set; } public DateTime? StopToDelivery { get; set; } public List UserIds { get; set; } = new(); + public string Link { get; set; } + public NotificationSeverity Severity { get; set; } } \ No newline at end of file diff --git a/Fin.Domain/Notifications/Dtos/NotificationOutput.cs b/Fin.Domain/Notifications/Dtos/NotificationOutput.cs index 72e54dc..0cdeab2 100644 --- a/Fin.Domain/Notifications/Dtos/NotificationOutput.cs +++ b/Fin.Domain/Notifications/Dtos/NotificationOutput.cs @@ -13,6 +13,8 @@ public class NotificationOutput(Notification input) public bool Continuous { get; set; } = input.Continuous; public DateTime StartToDelivery { get; set; } = input.StartToDelivery; public DateTime? StopToDelivery { get; set; } = input.StopToDelivery; + public string Link { get; set; } = input.Link; + public NotificationSeverity Severity { get; set; } = input.Severity; public NotificationOutput() : this(new Notification()) { diff --git a/Fin.Domain/Notifications/Dtos/NotificationUserDeliveryOutput.cs b/Fin.Domain/Notifications/Dtos/NotificationUserDeliveryOutput.cs deleted file mode 100644 index 981c015..0000000 --- a/Fin.Domain/Notifications/Dtos/NotificationUserDeliveryOutput.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Fin.Domain.Notifications.Entities; - -namespace Fin.Domain.Notifications.Dtos; - -public class NotificationUserDeliveryOutput(NotificationUserDelivery input) -{ - public Guid NotificationId { get; set; } = input.NotificationId; - public Guid UserId { get; set; } = input.UserId; - public bool Delivery { get; set; } = input.Delivery; - public bool Visualized { get; set; } = input.Visualized; -} \ No newline at end of file diff --git a/Fin.Domain/Notifications/Dtos/NotifyUserDto.cs b/Fin.Domain/Notifications/Dtos/NotifyUserDto.cs index 2efc723..49183b0 100644 --- a/Fin.Domain/Notifications/Dtos/NotifyUserDto.cs +++ b/Fin.Domain/Notifications/Dtos/NotifyUserDto.cs @@ -9,6 +9,10 @@ public class NotifyUserDto(Notification notification, NotificationUserDelivery n public string TextBody { get; set; } = notification.TextBody; public string HtmlBody { get; set; } = notification.HtmlBody; public string Title { get; set; } = notification.Title; + public string Link { get; set; } = notification.Link; + public bool Continuous { get; set; } = notification.Continuous; + public NotificationSeverity Severity { get; set; } = notification.Severity; + public Guid NotificationId { get; set; } = notification.Id; public Guid UserId { get; set; } = notificationUserDelivery.UserId; diff --git a/Fin.Domain/Notifications/Entities/Notification.cs b/Fin.Domain/Notifications/Entities/Notification.cs index b99da6d..1d5b396 100644 --- a/Fin.Domain/Notifications/Entities/Notification.cs +++ b/Fin.Domain/Notifications/Entities/Notification.cs @@ -17,6 +17,8 @@ public class Notification: IAuditedEntity public string NormalizedTitle { get; set; } public DateTime StartToDelivery { get; set; } public DateTime? StopToDelivery { get; set; } + public string Link { get; set; } + public NotificationSeverity Severity { get; set; } public Guid Id { get; set; } public Guid CreatedBy { get; set; } @@ -41,6 +43,8 @@ public Notification(NotificationInput input) Continuous = input.Continuous; Title = input.Title; + Link = input.Link; + Severity = input.Severity; NormalizedTitle = Title.NormalizeForComparison(); StartToDelivery = input.StartToDelivery; StopToDelivery = input.StopToDelivery; @@ -59,6 +63,8 @@ public List UpdateAndReturnToRemoveDeliveries(Notifica StartToDelivery = input.StartToDelivery; StopToDelivery = input.StopToDelivery; Continuous = input.Continuous; + Link = input.Link; + Severity = input.Severity; var updatedDeliveries = input.UserIds.Select(userId => new NotificationUserDelivery(userId, Id)).ToList(); diff --git a/Fin.Domain/Notifications/Enums/NotificationSeverity.cs b/Fin.Domain/Notifications/Enums/NotificationSeverity.cs new file mode 100644 index 0000000..6964b79 --- /dev/null +++ b/Fin.Domain/Notifications/Enums/NotificationSeverity.cs @@ -0,0 +1,10 @@ +namespace Fin.Domain.Notifications.Enums; + +public enum NotificationSeverity +{ + Default = 0, + Success = 1, + Error = 2, + Warning = 3, + Info = 4, +} \ No newline at end of file diff --git a/Fin.Domain/Users/Entities/UserCredential.cs b/Fin.Domain/Users/Entities/UserCredential.cs index d9f7d75..10a1d50 100644 --- a/Fin.Domain/Users/Entities/UserCredential.cs +++ b/Fin.Domain/Users/Entities/UserCredential.cs @@ -7,44 +7,21 @@ public class UserCredential : IEntity { public Guid Id { get; set; } - public string EncryptedEmail { get; private set; } - public string EncryptedPassword { get; private set; } + public string EncryptedEmail { get; set; } + public string EncryptedPassword { get; set; } public string GoogleId { get; set; } public string ResetToken { get; set; } = ""; - public int FailLoginAttempts { get; private set; } + public int FailLoginAttempts { get; set; } - public Guid UserId { get; private set; } + public Guid UserId { get; set; } public virtual User User { get; set; } public bool HasGoogle => !string.IsNullOrEmpty(GoogleId); public bool HasPassword => !string.IsNullOrEmpty(EncryptedPassword); public bool ExceededAttempts => FailLoginAttempts >= 5; - public UserCredential() - { - } - - public UserCredential(Guid userId, string encryptedEmail, string encryptedPassword) - { - Id = Guid.NewGuid(); - UserId = userId; - EncryptedEmail = encryptedEmail; - EncryptedPassword = encryptedPassword; - } - - public static UserCredential CreateWithGoogle(Guid userId, string encryptedEmail, string googleId) - { - return new UserCredential - { - Id = Guid.NewGuid(), - UserId = userId, - EncryptedEmail = encryptedEmail, - GoogleId = googleId, - }; - } - public bool ResetPassword(string newPasswordEncrypted, string resetToken) { if (resetToken != ResetToken) return false; diff --git a/Fin.Domain/Users/Factories/UserCredentialFactory.cs b/Fin.Domain/Users/Factories/UserCredentialFactory.cs new file mode 100644 index 0000000..225b0ff --- /dev/null +++ b/Fin.Domain/Users/Factories/UserCredentialFactory.cs @@ -0,0 +1,36 @@ +using Fin.Domain.Users.Entities; + +namespace Fin.Domain.Users.Factories; + +public enum UserCredentialFactoryType +{ + Password = 0, + Google = 1 +} + +public static class UserCredentialFactory +{ + public static UserCredential Create(Guid userId, string encryptedEmail, string value, UserCredentialFactoryType type) + { + var credential = new UserCredential + { + Id = Guid.NewGuid(), + UserId = userId, + EncryptedEmail = encryptedEmail + }; + + switch (type) + { + case UserCredentialFactoryType.Password: + credential.EncryptedPassword = value; + break; + case UserCredentialFactoryType.Google: + credential.GoogleId = value; + break; + default: + throw new ArgumentOutOfRangeException(nameof(type), type, null); + } + + return credential; + } +} \ No newline at end of file diff --git a/Fin.Infrastructure/Authentications/AddAuthenticationExtension.cs b/Fin.Infrastructure/Authentications/AddAuthenticationExtension.cs index 49532f4..0f20173 100644 --- a/Fin.Infrastructure/Authentications/AddAuthenticationExtension.cs +++ b/Fin.Infrastructure/Authentications/AddAuthenticationExtension.cs @@ -42,6 +42,22 @@ public static IServiceCollection AddFinAuthentication(this IServiceCollection se ValidAudience = configuration.GetSection(AuthenticationConstants.TokenJwtAudienceConfigKey).Value ?? "", IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration.GetSection(AuthenticationConstants.TokenJwtKeyConfigKey).Value ?? "")) }; + + options.Events = new JwtBearerEvents + { + OnMessageReceived = context => + { + var accessToken = context.Request.Query["access_token"]; + var path = context.HttpContext.Request.Path; + + if (!string.IsNullOrEmpty(accessToken) && path.StartsWithSegments("/notifications-hub")) + { + context.Token = accessToken; + } + + return Task.CompletedTask; + } + }; }); services.AddAuthorization(); diff --git a/Fin.Infrastructure/AutoServices/Extensions/AddAutoServicesExtension.cs b/Fin.Infrastructure/AutoServices/Extensions/AddAutoServicesExtension.cs index e7b0d54..31a38f7 100644 --- a/Fin.Infrastructure/AutoServices/Extensions/AddAutoServicesExtension.cs +++ b/Fin.Infrastructure/AutoServices/Extensions/AddAutoServicesExtension.cs @@ -35,7 +35,7 @@ public static IServiceCollection AddAutoSingletonServices(this IServiceCollectio return services; } - private static void RegisterDependencyByType(IServiceCollection serviceCollection, Type dependencyType, ServiceLifetime lifeStyle) + public static void RegisterDependencyByType(IServiceCollection serviceCollection, Type dependencyType, ServiceLifetime lifeStyle) { var loadedAssemblies = AppDomain.CurrentDomain.GetAssemblies().ToList(); var referencedPaths = Directory.GetFiles(AppDomain.CurrentDomain.BaseDirectory, "*.dll"); diff --git a/Fin.Infrastructure/Extensions/AddInfrastructureExtension.cs b/Fin.Infrastructure/Extensions/AddInfrastructureExtension.cs index 36d36ab..c3f4670 100644 --- a/Fin.Infrastructure/Extensions/AddInfrastructureExtension.cs +++ b/Fin.Infrastructure/Extensions/AddInfrastructureExtension.cs @@ -4,7 +4,9 @@ using Fin.Infrastructure.BackgroundJobs; using Fin.Infrastructure.Database.Extensions; using Fin.Infrastructure.Firebases; +using Fin.Infrastructure.Notifications.Hubs; using Fin.Infrastructure.Redis; +using Fin.Infrastructure.Seeders.Extensions; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -20,10 +22,12 @@ public static IServiceCollection AddInfrastructure(this IServiceCollection servi .AddAutoServices() .AddBackgroundJobs(configuration) .AddScoped() - .AddScoped() .AddScoped() + .AddScoped() .AddDatabase(configuration) - .AddFirebase(configuration); + .AddFirebase(configuration) + .AddSeeders() + .AddNotifications(); return services; } diff --git a/Fin.Infrastructure/Migrations/20250922223535_adding_severity_link_on_notification.Designer.cs b/Fin.Infrastructure/Migrations/20250922223535_adding_severity_link_on_notification.Designer.cs new file mode 100644 index 0000000..5dc579b --- /dev/null +++ b/Fin.Infrastructure/Migrations/20250922223535_adding_severity_link_on_notification.Designer.cs @@ -0,0 +1,513 @@ +// +using System; +using Fin.Infrastructure.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Fin.Infrastructure.Migrations +{ + [DbContext(typeof(FinDbContext))] + [Migration("20250922223535_adding_severity_link_on_notification")] + partial class adding_severity_link_on_notification + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("public") + .HasAnnotation("ProductVersion", "9.0.4") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Fin.Domain.Menus.Entities.Menu", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Color") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("FrontRoute") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Icon") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("KeyWords") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("OnlyForAdmin") + .HasColumnType("boolean"); + + b.Property("Position") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.ToTable("Menus", "public"); + }); + + modelBuilder.Entity("Fin.Domain.Notifications.Entities.Notification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Continuous") + .HasColumnType("boolean"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("HtmlBody") + .HasColumnType("text"); + + b.Property("Link") + .HasColumnType("text"); + + b.Property("NormalizedTextBody") + .HasColumnType("text"); + + b.Property("NormalizedTitle") + .HasColumnType("text"); + + b.Property("Severity") + .HasColumnType("integer"); + + b.Property("StartToDelivery") + .HasColumnType("timestamp with time zone"); + + b.Property("StopToDelivery") + .HasColumnType("timestamp with time zone"); + + b.Property("TextBody") + .HasColumnType("text"); + + b.Property("Title") + .HasMaxLength(250) + .HasColumnType("character varying(250)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.Property("Ways") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Notifications", "public"); + }); + + modelBuilder.Entity("Fin.Domain.Notifications.Entities.NotificationUserDelivery", b => + { + b.Property("NotificationId") + .HasColumnType("uuid"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("BackgroundJobId") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Delivery") + .HasColumnType("boolean"); + + b.Property("Visualized") + .HasColumnType("boolean"); + + b.HasKey("NotificationId", "UserId"); + + b.HasIndex("UserId"); + + b.ToTable("NotificationUserDeliveries", "public"); + }); + + modelBuilder.Entity("Fin.Domain.Notifications.Entities.UserNotificationSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AllowedWays") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("FirebaseTokens") + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UserNotificationSettings", "public"); + }); + + modelBuilder.Entity("Fin.Domain.Notifications.Entities.UserRememberUseSetting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("NotifyOn") + .HasColumnType("interval"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("Ways") + .HasColumnType("text"); + + b.Property("WeekDays") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UserRememberUseSettings", "public"); + }); + + modelBuilder.Entity("Fin.Domain.Tenants.Entities.Tenant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Locale") + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("Timezone") + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Tenants", "public"); + }); + + modelBuilder.Entity("Fin.Domain.Tenants.Entities.TenantUser", b => + { + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("TenantId", "UserId"); + + b.HasIndex("UserId"); + + b.ToTable("TenantUsers", "public"); + }); + + modelBuilder.Entity("Fin.Domain.Users.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BirthDate") + .HasColumnType("date"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DisplayName") + .HasMaxLength(150) + .HasColumnType("character varying(150)"); + + b.Property("FirstName") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Gender") + .HasColumnType("integer"); + + b.Property("ImagePublicUrl") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("IsActivity") + .HasColumnType("boolean"); + + b.Property("IsAdmin") + .HasColumnType("boolean"); + + b.Property("LastName") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Users", "public"); + }); + + modelBuilder.Entity("Fin.Domain.Users.Entities.UserCredential", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("EncryptedEmail") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("EncryptedPassword") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("FailLoginAttempts") + .HasColumnType("integer"); + + b.Property("GoogleId") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ResetToken") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("EncryptedEmail") + .IsUnique(); + + b.HasIndex("GoogleId") + .IsUnique(); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("Credentials", "public"); + }); + + modelBuilder.Entity("Fin.Domain.Users.Entities.UserDeleteRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Aborted") + .HasColumnType("boolean"); + + b.Property("AbortedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DeleteEffectivatedAt") + .HasColumnType("date"); + + b.Property("DeleteRequestedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.Property("UserAbortedId") + .HasColumnType("uuid"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserAbortedId"); + + b.HasIndex("UserId"); + + b.ToTable("UserDeleteRequests", "public"); + }); + + modelBuilder.Entity("Fin.Domain.Notifications.Entities.NotificationUserDelivery", b => + { + b.HasOne("Fin.Domain.Notifications.Entities.Notification", "Notification") + .WithMany("UserDeliveries") + .HasForeignKey("NotificationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Fin.Domain.Users.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Notification"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Fin.Domain.Notifications.Entities.UserNotificationSettings", b => + { + b.HasOne("Fin.Domain.Users.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Fin.Domain.Notifications.Entities.UserRememberUseSetting", b => + { + b.HasOne("Fin.Domain.Users.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Fin.Domain.Tenants.Entities.TenantUser", b => + { + b.HasOne("Fin.Domain.Tenants.Entities.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Fin.Domain.Users.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Fin.Domain.Users.Entities.UserCredential", b => + { + b.HasOne("Fin.Domain.Users.Entities.User", "User") + .WithOne("Credential") + .HasForeignKey("Fin.Domain.Users.Entities.UserCredential", "UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Fin.Domain.Users.Entities.UserDeleteRequest", b => + { + b.HasOne("Fin.Domain.Users.Entities.User", "UserAborted") + .WithMany() + .HasForeignKey("UserAbortedId"); + + b.HasOne("Fin.Domain.Users.Entities.User", "User") + .WithMany("DeleteRequests") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + + b.Navigation("UserAborted"); + }); + + modelBuilder.Entity("Fin.Domain.Notifications.Entities.Notification", b => + { + b.Navigation("UserDeliveries"); + }); + + modelBuilder.Entity("Fin.Domain.Users.Entities.User", b => + { + b.Navigation("Credential"); + + b.Navigation("DeleteRequests"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Fin.Infrastructure/Migrations/20250922223535_adding_severity_link_on_notification.cs b/Fin.Infrastructure/Migrations/20250922223535_adding_severity_link_on_notification.cs new file mode 100644 index 0000000..9917395 --- /dev/null +++ b/Fin.Infrastructure/Migrations/20250922223535_adding_severity_link_on_notification.cs @@ -0,0 +1,43 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Fin.Infrastructure.Migrations +{ + /// + public partial class adding_severity_link_on_notification : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Link", + schema: "public", + table: "Notifications", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "Severity", + schema: "public", + table: "Notifications", + type: "integer", + nullable: false, + defaultValue: 0); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Link", + schema: "public", + table: "Notifications"); + + migrationBuilder.DropColumn( + name: "Severity", + schema: "public", + table: "Notifications"); + } + } +} diff --git a/Fin.Infrastructure/Migrations/FinDbContextModelSnapshot.cs b/Fin.Infrastructure/Migrations/FinDbContextModelSnapshot.cs index 07fecbe..1e384e8 100644 --- a/Fin.Infrastructure/Migrations/FinDbContextModelSnapshot.cs +++ b/Fin.Infrastructure/Migrations/FinDbContextModelSnapshot.cs @@ -93,12 +93,18 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("HtmlBody") .HasColumnType("text"); + b.Property("Link") + .HasColumnType("text"); + b.Property("NormalizedTextBody") .HasColumnType("text"); b.Property("NormalizedTitle") .HasColumnType("text"); + b.Property("Severity") + .HasColumnType("integer"); + b.Property("StartToDelivery") .HasColumnType("timestamp with time zone"); diff --git a/Fin.Infrastructure/Notifications/Hubs/CustomUserIdProvider.cs b/Fin.Infrastructure/Notifications/Hubs/CustomUserIdProvider.cs index de78544..5307784 100644 --- a/Fin.Infrastructure/Notifications/Hubs/CustomUserIdProvider.cs +++ b/Fin.Infrastructure/Notifications/Hubs/CustomUserIdProvider.cs @@ -6,6 +6,6 @@ public class CustomUserIdProvider : IUserIdProvider { public string GetUserId(HubConnectionContext connection) { - return connection.User?.FindFirst("user_id")?.Value; + return connection.User.FindFirst("userId")?.Value; } } \ No newline at end of file diff --git a/Fin.Infrastructure/Notifications/Hubs/NotificationHub.cs b/Fin.Infrastructure/Notifications/Hubs/NotificationHub.cs index 397c1a3..1f930a0 100644 --- a/Fin.Infrastructure/Notifications/Hubs/NotificationHub.cs +++ b/Fin.Infrastructure/Notifications/Hubs/NotificationHub.cs @@ -1,7 +1,9 @@ -using Microsoft.AspNetCore.SignalR; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.SignalR; namespace Fin.Infrastructure.Notifications.Hubs; +[Authorize] public class NotificationHub: Hub { } \ No newline at end of file diff --git a/Fin.Infrastructure/Seeders/Extensions/SeedersExtensions.cs b/Fin.Infrastructure/Seeders/Extensions/SeedersExtensions.cs new file mode 100644 index 0000000..df665c7 --- /dev/null +++ b/Fin.Infrastructure/Seeders/Extensions/SeedersExtensions.cs @@ -0,0 +1,26 @@ +using Fin.Infrastructure.AutoServices.Extensions; +using Fin.Infrastructure.Seeders.interfaces; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; + +namespace Fin.Infrastructure.Seeders.Extensions; + +public static class SeedersExtensions +{ + public static IServiceCollection AddSeeders(this IServiceCollection services) + { + AddAutoServicesExtension.RegisterDependencyByType(services, typeof(ISeeder), ServiceLifetime.Transient); + return services; + } + + public static async Task UseSeeders(this WebApplication app) + { + using var scope = app.Services.CreateScope(); + var seeders = scope.ServiceProvider.GetServices(); + foreach (var seeder in seeders) + { + await seeder.SeedAsync(); + } + } + +} \ No newline at end of file diff --git a/Fin.Infrastructure/Seeders/Seeders/DefaultMenusSeeder.cs b/Fin.Infrastructure/Seeders/Seeders/DefaultMenusSeeder.cs new file mode 100644 index 0000000..ed7bf31 --- /dev/null +++ b/Fin.Infrastructure/Seeders/Seeders/DefaultMenusSeeder.cs @@ -0,0 +1,46 @@ +using Fin.Domain.Menus.Entities; +using Fin.Domain.Menus.Enums; +using Fin.Infrastructure.Database.Repositories; +using Fin.Infrastructure.Seeders.interfaces; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; + +namespace Fin.Infrastructure.Seeders.Seeders; + +public class DefaultMenusSeeder( + IRepository menusRepository, + ILogger logger +) : ISeeder +{ + public async Task SeedAsync() + { + logger.LogInformation("Seeding default menus"); + + var defaultMenus = new List + { + new() + { + Id = Guid.Parse("01994133-6669-7fcd-b6db-19a9b0c06f20"), + FrontRoute = "/admin/menus", + Name = "finCore.features.menus.title", + Color = "#fdc570", + Icon = "list", + OnlyForAdmin = true, + Position = MenuPosition.LeftTop, + KeyWords = "Menu" + } + }; + var defaultMenusIds = defaultMenus.Select(x => x.Id).ToList(); + var menusIdsAlreadyCreated = await menusRepository.Query(false) + .Where(x => defaultMenusIds.Contains(x.Id)) + .Select(x => x.Id).ToListAsync(); + var menusToCreate = defaultMenus.Where(x => !menusIdsAlreadyCreated.Contains(x.Id)).ToList(); + + if (menusToCreate.Any()) + { + await menusRepository.AddRangeAsync(menusToCreate, true); + } + + logger.LogInformation("Default menus created"); + } +} \ No newline at end of file diff --git a/Fin.Infrastructure/Seeders/interfaces/ISeeder.cs b/Fin.Infrastructure/Seeders/interfaces/ISeeder.cs new file mode 100644 index 0000000..fbde280 --- /dev/null +++ b/Fin.Infrastructure/Seeders/interfaces/ISeeder.cs @@ -0,0 +1,6 @@ +namespace Fin.Infrastructure.Seeders.interfaces; + +public interface ISeeder +{ + public Task SeedAsync(); +} \ No newline at end of file diff --git a/Fin.Infrastructure/UnitOfWorks/UnitOfWork.cs b/Fin.Infrastructure/UnitOfWorks/UnitOfWork.cs index 85f97cd..4a9727a 100644 --- a/Fin.Infrastructure/UnitOfWorks/UnitOfWork.cs +++ b/Fin.Infrastructure/UnitOfWorks/UnitOfWork.cs @@ -6,10 +6,11 @@ namespace Fin.Infrastructure.UnitOfWorks; public interface IUnitOfWork : IDisposable, IAsyncDisposable { - Task BeginTransactionAsync(CancellationToken cancellationToken = default); + Task BeginTransactionAsync(CancellationToken cancellationToken = default); Task CommitAsync(CancellationToken cancellationToken = default); Task RollbackAsync(CancellationToken cancellationToken = default); Task SaveChangesAsync(CancellationToken cancellationToken = default); + bool IsInTransaction(); } public class UnitOfWork(FinDbContext context): IUnitOfWork, IAutoScoped @@ -17,35 +18,35 @@ public class UnitOfWork(FinDbContext context): IUnitOfWork, IAutoScoped private IDbContextTransaction _transaction; private bool _disposed; - public async Task BeginTransactionAsync(CancellationToken cancellationToken = default) + public async Task BeginTransactionAsync(CancellationToken cancellationToken = default) { _transaction ??= await context.Database.BeginTransactionAsync(cancellationToken); + return _transaction; } public async Task CommitAsync(CancellationToken cancellationToken = default) { - try - { - var result = await context.SaveChangesAsync(cancellationToken); + var result = await context.SaveChangesAsync(cancellationToken); + + if (IsInTransaction()) await _transaction.CommitAsync(cancellationToken); - return result; - } - catch - { - await RollbackAsync(cancellationToken); - throw; - } + return result; } public async Task RollbackAsync(CancellationToken cancellationToken = default) { - if (_transaction != null) + if (IsInTransaction()) { await _transaction?.RollbackAsync(cancellationToken)!; } } public async Task SaveChangesAsync(CancellationToken cancellationToken = default) => await context.SaveChangesAsync(cancellationToken); + + public bool IsInTransaction() + { + return _transaction != null; + } public void Dispose() { diff --git a/Fin.Test/Authentications/AuthenticationServiceTest.cs b/Fin.Test/Authentications/AuthenticationServiceTest.cs index bba5f15..6c60f67 100644 --- a/Fin.Test/Authentications/AuthenticationServiceTest.cs +++ b/Fin.Test/Authentications/AuthenticationServiceTest.cs @@ -6,6 +6,7 @@ using Fin.Domain.Global; using Fin.Domain.Users.Dtos; using Fin.Domain.Users.Entities; +using Fin.Domain.Users.Factories; using Fin.Infrastructure.Authentications; using Fin.Infrastructure.Authentications.Constants; using Fin.Infrastructure.Authentications.Dtos; @@ -42,10 +43,9 @@ public async Task SendResetPasswordEmail_ExistEmail() }; var user = new User(); - var credential = new UserCredential(user.Id, encryptedEmail, null) - { - User = user - }; + var credential = UserCredentialFactory.Create(user.Id, encryptedEmail, null, UserCredentialFactoryType.Password); + credential.User = user; + user.Credential = credential; user.ToggleActivity(); await resources.CredentialRepository.AddAsync(credential, true); @@ -80,10 +80,9 @@ public async Task SendResetPasswordEmail_InactivatedUser() }; var user = new User(); - var credential = new UserCredential(user.Id, encryptedEmail, null) - { - User = user - }; + var credential = UserCredentialFactory.Create(user.Id, encryptedEmail, null, UserCredentialFactoryType.Password); + credential.User = user; + await resources.CredentialRepository.AddAsync(credential, true); // Act @@ -225,11 +224,9 @@ public async Task ResetPassword_TokenExpired() var user = new User { Id = TestUtils.Guids[0], - Credential = new UserCredential(TestUtils.Guids[0], "123", "123") - { - ResetToken = input.ResetToken - } + Credential = UserCredentialFactory.Create(TestUtils.Guids[0], "123", "123", UserCredentialFactoryType.Password) }; + user.Credential.ResetToken = input.ResetToken; user.Credential.User = user; await resources.CredentialRepository.AddAsync(user.Credential, true); @@ -263,11 +260,9 @@ public async Task ResetPassword_InvalidToken() var user = new User { Id = TestUtils.Guids[0], - Credential = new UserCredential(TestUtils.Guids[0], "123", "123") - { - ResetToken = input.ResetToken - } + Credential = UserCredentialFactory.Create(TestUtils.Guids[0], "123", "123", UserCredentialFactoryType.Password) }; + user.Credential.ResetToken = input.ResetToken; user.Credential.User = user; await resources.CredentialRepository.AddAsync(user.Credential, true); @@ -305,11 +300,9 @@ public async Task ResetPassword_Success() var user = new User { Id = TestUtils.Guids[0], - Credential = new UserCredential(TestUtils.Guids[0], "123", "123") - { - ResetToken = input.ResetToken - } + Credential = UserCredentialFactory.Create(TestUtils.Guids[0], "123", "123", UserCredentialFactoryType.Password) }; + user.Credential.ResetToken = input.ResetToken; user.Credential.User = user; await resources.CredentialRepository.AddAsync(user.Credential, true); @@ -390,10 +383,9 @@ public async Task LoginOrSingInWithGoogle_InactivatedUser() }; var user = new User(); - var credential = new UserCredential(user.Id, encryptedEmail, null) - { - User = user - }; + var credential = UserCredentialFactory.Create(user.Id, encryptedEmail, null, UserCredentialFactoryType.Password); + credential.User = user; + await resources.CredentialRepository.AddAsync(credential, true); // Act @@ -428,7 +420,7 @@ public async Task LoginOrSingInWithGoogle_DifferentGoogleId() }; var user = new User(); - var credential = UserCredential.CreateWithGoogle(user.Id, encryptedEmail, TestUtils.Strings[1]); + var credential = UserCredentialFactory.Create(user.Id, encryptedEmail, TestUtils.Strings[1], UserCredentialFactoryType.Google); credential.User = user; user.Credential = credential; user.ToggleActivity(); @@ -560,10 +552,9 @@ public async Task LoginOrSingInWithGoogle_Success_Not_CreatingUser_NeverGoogle() { Id = TestUtils.Guids[0] }; - var credential = new UserCredential(user.Id, encryptedEmail, null) - { - User = user - }; + var credential = UserCredentialFactory.Create(user.Id, encryptedEmail, null, UserCredentialFactoryType.Password); + credential.User = user; + user.Credential = credential; user.ToggleActivity(); await resources.CredentialRepository.AddAsync(credential, true); @@ -618,11 +609,10 @@ public async Task LoginOrSingInWithGoogle_Success_Not_CreatingUser_AlreadyGoogle { Id = TestUtils.Guids[0], }; - var credential = new UserCredential(user.Id, encryptedEmail, null) - { - User = user, - GoogleId = input.GoogleId, - }; + var credential = UserCredentialFactory.Create(user.Id, encryptedEmail, null, UserCredentialFactoryType.Google); + credential.User = user; + credential.GoogleId = input.GoogleId; + user.Credential = credential; user.ToggleActivity(); await resources.CredentialRepository.AddAsync(credential, true); diff --git a/Fin.Test/Authentications/AuthenticationTokenServiceTest.cs b/Fin.Test/Authentications/AuthenticationTokenServiceTest.cs index ce539a2..d093302 100644 --- a/Fin.Test/Authentications/AuthenticationTokenServiceTest.cs +++ b/Fin.Test/Authentications/AuthenticationTokenServiceTest.cs @@ -2,6 +2,7 @@ using Fin.Domain.Tenants.Entities; using Fin.Domain.Users.Dtos; using Fin.Domain.Users.Entities; +using Fin.Domain.Users.Factories; using Fin.Infrastructure.Authentications; using Fin.Infrastructure.Authentications.Constants; using Fin.Infrastructure.Authentications.Dtos; @@ -59,7 +60,8 @@ public async Task Login_Success() Id = TestUtils.Guids[0], Tenants = [new Tenant(now)] }; - var credential = new UserCredential(user.Id, encryptedEmail, encryptedPass); + var credential = + UserCredentialFactory.Create(user.Id, encryptedEmail, encryptedPass, UserCredentialFactoryType.Password); credential.Id = TestUtils.Guids[1]; await resources.UseRepository.AddAsync(user); @@ -110,7 +112,7 @@ public async Task Login_Fail(LoginErrorCode code, string credentialEmail, string Id = TestUtils.Guids[0], Tenants = [new Tenant(now)] }; - var credential = new UserCredential(user.Id, encryptedEmail, encryptedPass); + var credential = UserCredentialFactory.Create(user.Id, encryptedEmail, encryptedPass, UserCredentialFactoryType.Password); credential.Id = TestUtils.Guids[1]; credential.User = user; diff --git a/Fin.Test/Notifications/Services/NotificationDeliveryServiceTest.cs b/Fin.Test/Notifications/Services/NotificationDeliveryServiceTest.cs index 6afc8a0..59c4218 100644 --- a/Fin.Test/Notifications/Services/NotificationDeliveryServiceTest.cs +++ b/Fin.Test/Notifications/Services/NotificationDeliveryServiceTest.cs @@ -4,6 +4,7 @@ using Fin.Domain.Notifications.Entities; using Fin.Domain.Notifications.Enums; using Fin.Domain.Users.Entities; +using Fin.Domain.Users.Factories; using Fin.Infrastructure.Authentications.Constants; using Fin.Infrastructure.Database.Repositories; using Fin.Infrastructure.EmailSenders; @@ -146,7 +147,7 @@ public async Task SendNotification_ShouldSendEmail_WhenAllowed() Notification = new Notification { Id = notifyDto.NotificationId } }; var settings = new UserNotificationSettings() { UserId = userId, Enabled = true, AllowedWays = [NotificationWay.Email]}; - var credential = new UserCredential(userId, encryptedEmail, null); + var credential = UserCredentialFactory.Create(userId, encryptedEmail, null, UserCredentialFactoryType.Password); await resources.DeliveryRepository.AddAsync(delivery, true); await resources.UserSettingsRepository.AddAsync(settings, true); diff --git a/Fin.Test/Users/UserCreateServiceTest.cs b/Fin.Test/Users/UserCreateServiceTest.cs index 4dfdf38..eb8167d 100644 --- a/Fin.Test/Users/UserCreateServiceTest.cs +++ b/Fin.Test/Users/UserCreateServiceTest.cs @@ -7,6 +7,7 @@ using Fin.Domain.Tenants.Entities; using Fin.Domain.Users.Dtos; using Fin.Domain.Users.Entities; +using Fin.Domain.Users.Factories; using Fin.Infrastructure.Authentications.Constants; using Fin.Infrastructure.Constants; using Fin.Infrastructure.Database.Repositories; @@ -107,10 +108,10 @@ public async Task StartCreateUser_EmailInUse() var encryptedEmail = resources.CryptoHelper.Encrypt(input.Email); var user = new User(); - var credential = new UserCredential(user.Id, encryptedEmail, null) - { - User = user - }; + var credential = + UserCredentialFactory.Create(user.Id, encryptedEmail, null, UserCredentialFactoryType.Password); + credential.User = user; + await resources.CredentialRepository.AddAsync(credential, true); // Act @@ -699,7 +700,7 @@ public async Task CreateUser_WithGoogle_EmailAlreadyInUse() var user = new User() { Id = userId, - Credential = UserCredential.CreateWithGoogle(userId, encryptedEmail, googleId) + Credential = UserCredentialFactory.Create(userId, encryptedEmail, googleId, UserCredentialFactoryType.Google) }; await resources.UserRepository.AddAsync(user, true); diff --git a/Fin.Test/Users/UserDeleteServiceTest.cs b/Fin.Test/Users/UserDeleteServiceTest.cs index 01f3dac..c0c22f3 100644 --- a/Fin.Test/Users/UserDeleteServiceTest.cs +++ b/Fin.Test/Users/UserDeleteServiceTest.cs @@ -5,6 +5,7 @@ using Fin.Domain.Notifications.Entities; using Fin.Domain.Tenants.Entities; using Fin.Domain.Users.Entities; +using Fin.Domain.Users.Factories; using Fin.Infrastructure.Authentications.Constants; using Fin.Infrastructure.Database.Repositories; using Fin.Infrastructure.EmailSenders; @@ -74,7 +75,7 @@ public async Task RequestDeleteUser_Success() var user = await resources.UserRepository.Query(false).FirstOrDefaultAsync(); var email = TestUtils.Strings[2]; var encryptedEmail = resources.CryptoHelper.Encrypt(email); - user.Credential = new UserCredential(user.Id, encryptedEmail, null); + user.Credential = UserCredentialFactory.Create(user.Id, encryptedEmail, null, UserCredentialFactoryType.Password); await resources.CredentialRepository.AddAsync(user.Credential, true); // Act @@ -129,7 +130,7 @@ public async Task EffectiveDeleteUsers_WithUsersToDelete_Success() var email = TestUtils.Strings[2]; var encryptedEmail = resources.CryptoHelper.Encrypt(email); - user.Credential = new UserCredential(user.Id, encryptedEmail, null); + user.Credential = UserCredentialFactory.Create(user.Id, encryptedEmail, null, UserCredentialFactoryType.Google); await resources.CredentialRepository.AddAsync(user.Credential, true); var deleteRequest = new UserDeleteRequest(user.Id, TestUtils.UtcDateTimes[0]); @@ -239,7 +240,7 @@ public async Task AbortDeleteUser_AsAdmin_Success() var encryptedEmail = resources.CryptoHelper.Encrypt(email); var targetUser = new User { Id = TestUtils.Guids[3] }; - var targetCredential = new UserCredential(targetUser.Id, encryptedEmail, null); + var targetCredential = UserCredentialFactory.Create(targetUser.Id, encryptedEmail, null, UserCredentialFactoryType.Google); targetUser.Credential = targetCredential; await resources.UserRepository.AddAsync(targetUser, true); @@ -281,7 +282,7 @@ public async Task AbortDeleteUser_AsOwner_Success() var email = TestUtils.Strings[2]; var encryptedEmail = resources.CryptoHelper.Encrypt(email); - user.Credential = new UserCredential(user.Id, encryptedEmail, null); + user.Credential = UserCredentialFactory.Create(user.Id, encryptedEmail, null, UserCredentialFactoryType.Password); await resources.CredentialRepository.AddAsync(user.Credential, true); var deleteRequest = new UserDeleteRequest(user.Id, TestUtils.UtcDateTimes[0]); diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..7f54fc6 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,38 @@ +version: '3.8' + +services: + postgres: + image: postgres:16 + container_name: fin_app + restart: always + environment: + POSTGRES_DB: fin_app + POSTGRES_USER: fin_app + POSTGRES_PASSWORD: fin_app + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U fin_app"] + interval: 10s + timeout: 5s + retries: 5 + + redis: + image: redis:7-alpine + container_name: fin_redis + restart: always + ports: + - "6379:6379" + volumes: + - redis_data:/data + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 3s + retries: 5 + +volumes: + postgres_data: + redis_data: diff --git a/docker.md b/docker.md new file mode 100644 index 0000000..bb8a2e3 --- /dev/null +++ b/docker.md @@ -0,0 +1,122 @@ +# Docker Compose - Fin Backend Infrastructure + +## Como rodar o banco de dados PostgreSQL e Redis + +1. **Suba os serviços (PostgreSQL + Redis):** + ``` + docker compose up -d + ``` + Isso irá criar os seguintes containers: + + **PostgreSQL:** + - Container: `fin_app` + - Banco: `fin_app` + - Usuário: `fin_app` + - Senha: `fin_app` + - Porta: `5432` + + **Redis:** + - Container: `fin_redis` + - Porta: `6379` + - Versão: Redis 7 (Alpine) + + **Verificar se os containers estão rodando:** + ``` + docker ps -a + ``` + +2. **Configure as connection strings no seu `appsettings.json`:** + **Para rodar a aplicação localmente:** + ```json + "ConnectionStrings": { + "DefaultConnection": "Host=localhost;Port=5432;Database=fin_app;Username=fin_app;Password=fin_app" + }, + "ApiSettings": { + "Redis": "localhost:6379" + } + ``` + +3. **Rode as migrations para criar as tabelas:** + ``` + dotnet ef database update --project .\Fin.Infrastructure\ + ``` + > Se necessário, instale o pacote de ferramentas: + > ``` + > dotnet tool install --global dotnet-ef + > ``` + +4. **Verifique se tudo está ok:** + ``` + dotnet build + ``` + +5. **Inicie a aplicação:** + ``` + dotnet run --project .\Fin.Api\ + ``` +--- +## Observações + +### PostgreSQL +- O container do PostgreSQL pode ser acessado via qualquer cliente PostgreSQL na porta `5432`. +- As migrations do Entity Framework Core criam todas as tabelas necessárias para todos os domínios do projeto. + +### Redis +- O Redis é usado para cache e sessões no projeto. +- Porta padrão: `6379` +- Os dados do Redis são persistidos no volume `redis_data` +- Health check configurado para verificar se o serviço está ativo + +### Desenvolvimento +- O comando `dotnet build` verifica se o projeto está compilando corretamente. +- O comando `dotnet run` inicia a API. +- Certifique-se de que tanto PostgreSQL quanto Redis estejam rodando antes de iniciar a aplicação. + +**Se já possui o banco e Redis configurados, utilize suas variáveis de ambiente em um `.env`:** + ```json + "ConnectionStrings": { + "DefaultConnection": "Host=${POSTGRES_HOST};Port=${POSTGRES_PORT};Database=${POSTGRES_DB};Username=${POSTGRES_USER};Password=${POSTGRES_PASSWORD}" + }, + "ApiSettings": { + "Redis": "${REDIS_HOST}:${REDIS_PORT}" + } + ``` + + **Exemplo de arquivo `.env`:** + ```env + POSTGRES_HOST=localhost + POSTGRES_PORT=5432 + POSTGRES_DB=fin_app + POSTGRES_USER=fin_app + POSTGRES_PASSWORD=fin_app + REDIS_HOST=localhost + REDIS_PORT=6379 + ``` + +--- +## Comandos úteis + +**Para parar os serviços:** +``` +docker compose down +``` + +**Para parar e remover volumes (CUIDADO: apaga os dados):** +``` +docker compose down -v +``` + +**Para ver os logs dos containers:** +``` +docker compose logs -f +``` + +**Para conectar diretamente ao PostgreSQL:** +``` +docker exec -it fin_app psql -U fin_app -d fin_app +``` + +**Para conectar diretamente ao Redis:** +``` +docker exec -it fin_redis redis-cli +``` \ No newline at end of file diff --git a/notifications.md b/notifications.md new file mode 100644 index 0000000..e1f9e87 --- /dev/null +++ b/notifications.md @@ -0,0 +1,45 @@ +# Notification System Rules - Technical Specification + +## Email & Snack Notifications + +### Delivery Rules +- **Single delivery**: Sent once and immediately marked as sent +- **Scheduled delivery**: Must have a specific time to be sent +- **No end date**: Cannot have an end date configured +- **Non-continuous**: Cannot be set as continuous notifications + +### Trigger Behavior +- **On start**: Send email immediately when notification starts +- **User logged in**: Send push notification if user is logged in when notification starts +- **Firebase integration**: Send to Firebase but do NOT mark as read (frontend must handle marking as read) + +### Visibility Rules +- **Email**: Not available in `GetUnvisualizedNotifications` endpoint (called by frontend on startup or notification loading) +- **Push**: Automatically marked as visualized in `GetUnvisualizedNotifications` + +## Push & Message Notifications + +### Delivery Rules +- **End date support**: Can have an end date for delivery +- **Continuous support**: Messages can be set as continuous notifications +- **Trigger condition**: Send when start date is reached, if user is logged in OR send to Firebase + +### Visibility Rules +- **Availability**: Available in `GetUnvisualizedNotifications` but NOT automatically marked as visualized +- **Push notifications**: Must wait for user to click or clear to mark as visualized +- **Messages (non-continuous)**: Marked as visualized when user clicks "OK" +- **Messages (continuous)**: Marked as visualized only when user clicks "Don't show again" +- **Message Clicked**: open the message on screen +- **Messages**: message can be removed, than mark as read + +### UI Behavior +- **Side menu**: Both message and push notifications remain in side menu until marked as visualized +- **Screen display**: Messages appear on screen when received via notification hub + +## API Endpoint Specifications + +### GetUnvisualizedNotifications +- **Includes**: Push notifications, Message notifications, Snack notifications +- **Excludes**: Email notifications +- **Auto-marking**: Only Push notifications are automatically marked as visualized +- **Manual marking**: Message asn push notifications require user interaction to be marked as visualized \ No newline at end of file