Compare commits
2 Commits
0bca018ca2
...
e9a986058d
| Author | SHA1 | Date | |
|---|---|---|---|
| e9a986058d | |||
| e88a483025 |
@ -6,6 +6,8 @@
|
||||
<entry key="Flawless.Client.Avanonia/Views/MainWindow.axaml" value="Flawless.Client.Avanonia/Flawless.Client.Avanonia.csproj" />
|
||||
<entry key="Flawless.Client/App.axaml" value="Flawless.Client/Flawless.Client.csproj" />
|
||||
<entry key="Flawless.Client/Theme/ToggleSwitch.axaml" value="Flawless.Client/Flawless.Client.csproj" />
|
||||
<entry key="Flawless.Client/Views/HelloSetup/FirsetSetipViewModel.axaml" value="Flawless.Client/Flawless.Client.csproj" />
|
||||
<entry key="Flawless.Client/Views/HelloSetup/FirstSetupView.axaml" value="Flawless.Client/Flawless.Client.csproj" />
|
||||
<entry key="Flawless.Client/Views/HelloSetup/LoginPageView.axaml" value="Flawless.Client/Flawless.Client.csproj" />
|
||||
<entry key="Flawless.Client/Views/HelloSetup/RegisterPageView.axaml" value="Flawless.Client/Flawless.Client.csproj" />
|
||||
<entry key="Flawless.Client/Views/HelloSetup/ServerSetupPageView.axaml" value="Flawless.Client/Flawless.Client.csproj" />
|
||||
@ -24,7 +26,9 @@
|
||||
<entry key="Flawless.Client/Views/ModalBox/EditRepositoryMemberDialogueView.axaml" value="Flawless.Client/Flawless.Client.csproj" />
|
||||
<entry key="Flawless.Client/Views/ModalBox/IssueDetailEditView.axaml" value="Flawless.Client/Flawless.Client.csproj" />
|
||||
<entry key="Flawless.Client/Views/ModalBox/MergeDialogView.axaml" value="Flawless.Client/Flawless.Client.csproj" />
|
||||
<entry key="Flawless.Client/Views/ModalBox/PasswordChangeDialogView.axaml" value="Flawless.Client/Flawless.Client.csproj" />
|
||||
<entry key="Flawless.Client/Views/ModalBox/SimpleMessageDialogView.axaml" value="Flawless.Client/Flawless.Client.csproj" />
|
||||
<entry key="Flawless.Client/Views/ModalBox/UserCreateDialogView.axaml" value="Flawless.Client/Flawless.Client.csproj" />
|
||||
<entry key="Flawless.Client/Views/RegisterPageView.axaml" value="Flawless.Client/Flawless.Client.csproj" />
|
||||
<entry key="Flawless.Client/Views/RegisterView.axaml" value="Flawless.Client/Flawless.Client.csproj" />
|
||||
<entry key="Flawless.Client/Views/RepositoryPage/IssueEditDialogView.axaml" value="Flawless.Client/Flawless.Client.csproj" />
|
||||
|
||||
@ -6,30 +6,44 @@
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AAsyncValueTaskMethodBuilder_00601_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fa6b7f037ba7b44df80b8d3aa7e58eeb2e8e938_003Fa5_003Ff3a8130e_003FAsyncValueTaskMethodBuilder_00601_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AAsyncValueTaskMethodBuilder_00601_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F4c8540adf3cd4f6ab5d99f290234ba1ad19c00_003Fd1_003Ff1626b2e_003FAsyncValueTaskMethodBuilder_00601_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AAuthenticatorTokenProvider_00601_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fd56cb0a089b14dab96ad3ee133819f966d938_003Feb_003Fa2d5eee1_003FAuthenticatorTokenProvider_00601_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AAuthorizeAttribute_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fa76d739202ae43edab35cc7fc392c26f15800_003F0b_003F08d32ef0_003FAuthorizeAttribute_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ACheckBoxColumnOptions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F25ac83f0b30c483ab65ac482108a36294cc00_003F_005Fe6928_003FCheckBoxColumnOptions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ACheckBoxColumn_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F2142c5e8387b5a71988ab7c9ece92b77aa8cea9b3f6f76a861547f959780d5_003FCheckBoxColumn_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AClaimsPrincipal_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fbf5ce3f8ae0647439d514bb1a3c7f96b13600_003F20_003Fabeaf9ae_003FClaimsPrincipal_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AClaimTypes_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fbf5ce3f8ae0647439d514bb1a3c7f96b13600_003Fbd_003F4cde67a5_003FClaimTypes_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AColumnBase_00601_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F7d901ebbf99b7e87c21edb35dc72355e0e287a2559c8ee26452dc86792b2a_003FColumnBase_00601_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AConsoleLoggerExtensions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F0be9eafc3b2b44b09f5bd633da8bbd1922a00_003Fcc_003F534ab9e1_003FConsoleLoggerExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AControllerBase_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fdffdaf205cf54e098aa7d66ba76b38621de920_003F53_003F6f15feba_003FControllerBase_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ADbContext_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fcb0587797ea44bd6915ede69888c6766291038_003Fbc_003F2b4c89d0_003FDbContext_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ADbContext_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fcb0587797ea44bd6915ede69888c6766291038_003F8d_003F1d8bdb2b_003FDbContext_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AEventId_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fd0319a0b2a6743ffb27031fdd18a267a1f000_003F72_003Fcd473540_003FEventId_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AEventPipeEventProvider_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F84661d4944484e5e93fdc174ab4e7d09d19e00_003F46_003F893b69bd_003FEventPipeEventProvider_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AFileInfo_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F4c8540adf3cd4f6ab5d99f290234ba1ad19c00_003Ffe_003F5a5023e2_003FFileInfo_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIdentityDbContext_00601_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F3eeae7a548684642a53a9ceddc825b7a1a930_003Fcf_003F6a374370_003FIdentityDbContext_00601_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIdentityDbContext_00603_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F3eeae7a548684642a53a9ceddc825b7a1a930_003F67_003Ff95019b3_003FIdentityDbContext_00603_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIdentityDbContext_00608_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F3eeae7a548684642a53a9ceddc825b7a1a930_003Fc6_003Fcbfda1f6_003FIdentityDbContext_00608_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIdentityUserToken_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Facfead36ed0138084f13ee724545d2dcb853f354ec658443b72fc26eff58781_003FIdentityUserToken_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIdentityUser_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fc405819100144b0483c14b61d32c5aa215930_003F90_003F4d8e1a86_003FIdentityUser_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIdentityUser_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F7d382df578ec93391918cfaa4ce7f4b8f35c9aed1241d6556dc9be26df13c_003FIdentityUser_002Ecs_002Fz_003A2_002D1/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIdentityUser_00601_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fd991417b721d4ddab50a2b715d0ad696b138_003Fa2_003Fdb3874bc_003FIdentityUser_00601_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIdentityUser_00601_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fd99404009f8a4a8da97cdbe0d6ecd5dd10800_003F1f_003Fe7cb9bcb_003FIdentityUser_00601_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIPAddress_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F8347fcf3395c4c319b76f192255524c434800_003F42_003F1428ea6f_003FIPAddress_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIReactiveObjectExtensions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F404d064a80dc4960b93f90c9bd69770750810_003F5a_003F1516290d_003FIReactiveObjectExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AJwtBearerHandler_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F61447741f88e235f7cd1a276ef5abe648b2dee4b210873893d178b861c9d0_003FJwtBearerHandler_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ALoggerFactory_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F1b4c09e663de4a9ea6963d356da7095e13a00_003Ffa_003F8ad0b278_003FLoggerFactory_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ALogLevel_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fd0319a0b2a6743ffb27031fdd18a267a1f000_003F81_003Fe3a61b51_003FLogLevel_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003APath_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fa6b7f037ba7b44df80b8d3aa7e58eeb2e8e938_003F28_003F6a41ec86_003FPath_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AReactiveObject_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F404d064a80dc4960b93f90c9bd69770750810_003F5c_003F228dd86b_003FReactiveObject_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AReactiveUrsaView_00601_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F01a3316df1f14aff8404556f39e0d9e52200_003F3c_003F7b186404_003FReactiveUrsaView_00601_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AReactiveUrsaView_00601_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F01a3316df1f14aff8404556f39e0d9e52200_003Fde_003F4660a705_003FReactiveUrsaView_00601_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AReactiveUrsaWindow_00601_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F01a3316df1f14aff8404556f39e0d9e52200_003F1f_003F9c292772_003FReactiveUrsaWindow_00601_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ARefitSettings_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fa2b3a0b86cd053ed984c171eb448b551b9a2a0c89c5a68191996cffac5d85d2b_003FRefitSettings_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AScopedLoggerFactory_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fcb0587797ea44bd6915ede69888c6766291038_003Fb3_003Fdcbe23f1_003FScopedLoggerFactory_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AServiceCollectionServiceExtensions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F72233fb198024142af6c60c711ba00711e000_003Fdd_003Fd0945119_003FServiceCollectionServiceExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AServiceProvider_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fce37be1a06b16c6faa02038d2cc477dd3bca5b217ceeb41c5f2ad45c1bf9_003FServiceProvider_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ASignInManager_00601_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F40411c547364428dafc988a7615774e28b910_003F6d_003F1a409232_003FSignInManager_00601_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AStringValue_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fee39d1e9346e41aa9d44f0e1b1c6630f76268_003F49_003Fb92346b2_003FStringValue_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AString_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F84661d4944484e5e93fdc174ab4e7d09d19e00_003Fba_003Ff053af03_003FString_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ATestMethodInfo_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F5ef53d675c5d34a6b85963919015dc0c1b06e5ea9834aac59ae6911f4c6f38_003FTestMethodInfo_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ATextColumnOptions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fb453ad4346847ab5eb73bb5f57e6ef748946b57c6b76525b4d62e36c6b_003FTextColumnOptions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ATextColumn_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Ff28c315cd14c2851fdb3c6fc6050618665d26fab17f3d1d81edf3d638f6a477_003FTextColumn_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
|
||||
@ -8,4 +8,14 @@ public partial class AppSettingModel : ReactiveModel
|
||||
{
|
||||
[Reactive, NonSerialized]
|
||||
private string _repositoryPath = AppDefaultValues.DefaultRepositoryDirectory;
|
||||
|
||||
[Reactive, NonSerialized] private bool
|
||||
_refreshWorkspaceOnOpen = true,
|
||||
_refreshWorkspaceOnFilesystemChanges = true;
|
||||
|
||||
|
||||
[Reactive, NonSerialized] private string _diffTool;
|
||||
|
||||
|
||||
[Reactive, NonSerialized] private string _fileManagerTool;
|
||||
}
|
||||
@ -30,4 +30,6 @@ public partial class UserModel : ReactiveModel
|
||||
[Reactive] private string _phoneNumber;
|
||||
|
||||
[Reactive] private DateTime _joinDate;
|
||||
|
||||
[Reactive] private bool _isAdmin;
|
||||
}
|
||||
@ -32,7 +32,7 @@ public class Api : BaseService<Api>
|
||||
|
||||
public IObservable<string?> ServerUrl => _serverUrl;
|
||||
|
||||
public IObservable<ServerStatusResponse?> Status => _status;
|
||||
public IReactiveProperty<ServerStatusResponse?> Status => _status;
|
||||
|
||||
public IObservable<TokenInfo?> Token => _token;
|
||||
|
||||
|
||||
@ -18,6 +18,17 @@ namespace Flawless.Client.Remote
|
||||
[System.CodeDom.Compiler.GeneratedCode("Refitter", "1.5.3.0")]
|
||||
public partial interface IFlawlessServer
|
||||
{
|
||||
/// <returns>A <see cref="Task"/> that completes when the request is finished.</returns>
|
||||
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
|
||||
[Post("/api/admin/superuser/{username}")]
|
||||
Task SuperuserPost(string username, [Query] bool? toSuper, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <returns>OK</returns>
|
||||
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
|
||||
[Headers("Accept: text/plain, application/json, text/json")]
|
||||
[Get("/api/admin/superuser/{username}")]
|
||||
Task<bool> SuperuserGet(string username, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <returns>A <see cref="Task"/> that completes when the request is finished.</returns>
|
||||
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
|
||||
[Post("/api/admin/user/delete/{username}")]
|
||||
@ -39,12 +50,48 @@ namespace Flawless.Client.Remote
|
||||
[Post("/api/admin/user/reset_password")]
|
||||
Task ResetPassword([Body] ResetPasswordRequest body, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <returns>A <see cref="Task"/> that completes when the request is finished.</returns>
|
||||
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
|
||||
[Headers("Content-Type: application/json")]
|
||||
[Post("/api/admin/access_control/ip_whitelist")]
|
||||
Task IpWhitelistPost([Body] IEnumerable<string> body, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <returns>OK</returns>
|
||||
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
|
||||
[Headers("Accept: text/plain, application/json, text/json")]
|
||||
[Get("/api/admin/access_control/ip_whitelist")]
|
||||
Task<ICollection<string>> IpWhitelistGet(CancellationToken cancellationToken = default);
|
||||
|
||||
/// <returns>A <see cref="Task"/> that completes when the request is finished.</returns>
|
||||
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
|
||||
[Headers("Content-Type: application/json")]
|
||||
[Post("/api/admin/access_control/ip_blacklist")]
|
||||
Task IpBlacklistPost([Body] IEnumerable<string> body, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <returns>OK</returns>
|
||||
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
|
||||
[Headers("Accept: text/plain, application/json, text/json")]
|
||||
[Get("/api/admin/access_control/ip_blacklist")]
|
||||
Task<ICollection<string>> IpBlacklistGet(CancellationToken cancellationToken = default);
|
||||
|
||||
/// <returns>OK</returns>
|
||||
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
|
||||
[Headers("Accept: text/plain, application/json, text/json")]
|
||||
[Get("/api/admin/logs")]
|
||||
Task<ICollection<LogEntryResponse>> Logs([Query] System.DateTimeOffset? startTime, [Query] System.DateTimeOffset? endTime, [Query] LogLevel? level, [Query] int? page, [Query] int? pageSize, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <returns>OK</returns>
|
||||
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
|
||||
[Headers("Accept: text/plain, application/json, text/json")]
|
||||
[Get("/api/auth/status")]
|
||||
Task<ServerStatusResponse> Status(CancellationToken cancellationToken = default);
|
||||
|
||||
/// <returns>A <see cref="Task"/> that completes when the request is finished.</returns>
|
||||
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
|
||||
[Headers("Content-Type: application/json")]
|
||||
[Post("/api/auth/first-setup")]
|
||||
Task FirstSetup([Body] FirstSetupRequest body, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <returns>A <see cref="Task"/> that completes when the request is finished.</returns>
|
||||
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
|
||||
[Headers("Content-Type: application/json")]
|
||||
@ -152,6 +199,28 @@ namespace Flawless.Client.Remote
|
||||
[Get("/api/repo/{userName}/{repositoryName}/get_users")]
|
||||
Task<RepoUserRoleListingResponse> GetUsers(string userName, string repositoryName, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <returns>A <see cref="Task"/> that completes when the request is finished.</returns>
|
||||
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
|
||||
[Headers("Content-Type: application/json")]
|
||||
[Post("/api/repo/{userName}/{repositoryName}/create_webhook")]
|
||||
Task CreateWebhook(string userName, string repositoryName, [Body] WebhookCreateRequest body, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <returns>OK</returns>
|
||||
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
|
||||
[Headers("Accept: text/plain, application/json, text/json")]
|
||||
[Get("/api/repo/{userName}/{repositoryName}")]
|
||||
Task<ICollection<Webhook>> RepoGet(string userName, string repositoryName, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <returns>A <see cref="Task"/> that completes when the request is finished.</returns>
|
||||
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
|
||||
[Post("/api/repo/{userName}/{repositoryName}/{webhookId}/toggle")]
|
||||
Task Toggle(string userName, string repositoryName, int webhookId, [Query] bool? activate, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <returns>A <see cref="Task"/> that completes when the request is finished.</returns>
|
||||
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
|
||||
[Delete("/api/repo/{userName}/{repositoryName}/{webhookId}")]
|
||||
Task RepoDelete(string userName, string repositoryName, int webhookId, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <returns>OK</returns>
|
||||
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
|
||||
[Headers("Accept: text/plain, application/json, text/json")]
|
||||
@ -214,7 +283,7 @@ namespace Flawless.Client.Remote
|
||||
/// <returns>A <see cref="Task"/> that completes when the request is finished.</returns>
|
||||
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
|
||||
[Post("/api/update_user")]
|
||||
Task UpdateUser([Query] string repositoryName, [Query] string modUser, [Query] RepositoryModel.RepositoryRole role);
|
||||
Task UpdateUser([Query] string repositoryName, [Query] string modUser, [Query] RepositoryModel.RepositoryRole? role, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <returns>A <see cref="Task"/> that completes when the request is finished.</returns>
|
||||
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
|
||||
@ -372,7 +441,7 @@ namespace Flawless.Client.Remote
|
||||
public string Description { get; set; }
|
||||
|
||||
[JsonPropertyName("tag")]
|
||||
public string? Tag { get; set; }
|
||||
public string Tag { get; set; }
|
||||
|
||||
}
|
||||
|
||||
@ -388,6 +457,21 @@ namespace Flawless.Client.Remote
|
||||
|
||||
}
|
||||
|
||||
[System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.3.0.0 (NJsonSchema v11.2.0.0 (Newtonsoft.Json v13.0.0.0))")]
|
||||
public partial class FirstSetupRequest
|
||||
{
|
||||
|
||||
[JsonPropertyName("adminEmail")]
|
||||
public string AdminEmail { get; set; }
|
||||
|
||||
[JsonPropertyName("adminUsername")]
|
||||
public string AdminUsername { get; set; }
|
||||
|
||||
[JsonPropertyName("adminPassword")]
|
||||
public string AdminPassword { get; set; }
|
||||
|
||||
}
|
||||
|
||||
[System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.3.0.0 (NJsonSchema v11.2.0.0 (Newtonsoft.Json v13.0.0.0))")]
|
||||
public partial class GuidPeekResponse
|
||||
{
|
||||
@ -484,6 +568,44 @@ namespace Flawless.Client.Remote
|
||||
|
||||
}
|
||||
|
||||
[System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.3.0.0 (NJsonSchema v11.2.0.0 (Newtonsoft.Json v13.0.0.0))")]
|
||||
public partial class LogEntryResponse
|
||||
{
|
||||
|
||||
[JsonPropertyName("timestamp")]
|
||||
public System.DateTimeOffset Timestamp { get; set; }
|
||||
|
||||
[JsonPropertyName("level")]
|
||||
public string Level { get; set; }
|
||||
|
||||
[JsonPropertyName("message")]
|
||||
public string Message { get; set; }
|
||||
|
||||
[JsonPropertyName("exception")]
|
||||
public string Exception { get; set; }
|
||||
|
||||
}
|
||||
|
||||
[System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.3.0.0 (NJsonSchema v11.2.0.0 (Newtonsoft.Json v13.0.0.0))")]
|
||||
public enum LogLevel
|
||||
{
|
||||
|
||||
_0 = 0,
|
||||
|
||||
_1 = 1,
|
||||
|
||||
_2 = 2,
|
||||
|
||||
_3 = 3,
|
||||
|
||||
_4 = 4,
|
||||
|
||||
_5 = 5,
|
||||
|
||||
_6 = 6,
|
||||
|
||||
}
|
||||
|
||||
[System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.3.0.0 (NJsonSchema v11.2.0.0 (Newtonsoft.Json v13.0.0.0))")]
|
||||
public partial class LoginRequest
|
||||
{
|
||||
@ -640,6 +762,12 @@ namespace Flawless.Client.Remote
|
||||
[JsonPropertyName("allowPublicRegister")]
|
||||
public bool AllowPublicRegister { get; set; }
|
||||
|
||||
[JsonPropertyName("allowWebHook")]
|
||||
public bool AllowWebHook { get; set; }
|
||||
|
||||
[JsonPropertyName("requireInitialization")]
|
||||
public bool RequireInitialization { get; set; }
|
||||
|
||||
}
|
||||
|
||||
[System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.3.0.0 (NJsonSchema v11.2.0.0 (Newtonsoft.Json v13.0.0.0))")]
|
||||
@ -660,13 +788,13 @@ namespace Flawless.Client.Remote
|
||||
{
|
||||
|
||||
[JsonPropertyName("title")]
|
||||
public string? Title { get; set; }
|
||||
public string Title { get; set; }
|
||||
|
||||
[JsonPropertyName("description")]
|
||||
public string? Description { get; set; }
|
||||
public string Description { get; set; }
|
||||
|
||||
[JsonPropertyName("tag")]
|
||||
public string? Tag { get; set; }
|
||||
public string Tag { get; set; }
|
||||
|
||||
}
|
||||
|
||||
@ -690,7 +818,7 @@ namespace Flawless.Client.Remote
|
||||
public string NickName { get; set; }
|
||||
|
||||
[JsonPropertyName("gender")]
|
||||
public UserSex Gender { get; set; }
|
||||
public UserSex? Gender { get; set; }
|
||||
|
||||
[JsonPropertyName("bio")]
|
||||
public string Bio { get; set; }
|
||||
@ -756,6 +884,62 @@ namespace Flawless.Client.Remote
|
||||
|
||||
}
|
||||
|
||||
[System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.3.0.0 (NJsonSchema v11.2.0.0 (Newtonsoft.Json v13.0.0.0))")]
|
||||
public partial class Webhook
|
||||
{
|
||||
|
||||
[JsonPropertyName("id")]
|
||||
public int Id { get; set; }
|
||||
|
||||
[JsonPropertyName("repositoryId")]
|
||||
[System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)]
|
||||
public System.Guid RepositoryId { get; set; }
|
||||
|
||||
[JsonPropertyName("targetUrl")]
|
||||
[System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)]
|
||||
public string TargetUrl { get; set; }
|
||||
|
||||
[JsonPropertyName("eventType")]
|
||||
public WebhookEventType EventType { get; set; }
|
||||
|
||||
[JsonPropertyName("secret")]
|
||||
public string Secret { get; set; }
|
||||
|
||||
[JsonPropertyName("isActive")]
|
||||
public bool IsActive { get; set; }
|
||||
|
||||
[JsonPropertyName("createdAt")]
|
||||
public System.DateTimeOffset CreatedAt { get; set; }
|
||||
|
||||
}
|
||||
|
||||
[System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.3.0.0 (NJsonSchema v11.2.0.0 (Newtonsoft.Json v13.0.0.0))")]
|
||||
public partial class WebhookCreateRequest
|
||||
{
|
||||
|
||||
[JsonPropertyName("targetUrl")]
|
||||
public string TargetUrl { get; set; }
|
||||
|
||||
[JsonPropertyName("eventType")]
|
||||
public WebhookEventType EventType { get; set; }
|
||||
|
||||
[JsonPropertyName("secret")]
|
||||
public string Secret { get; set; }
|
||||
|
||||
}
|
||||
|
||||
[System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.3.0.0 (NJsonSchema v11.2.0.0 (Newtonsoft.Json v13.0.0.0))")]
|
||||
public enum WebhookEventType
|
||||
{
|
||||
|
||||
_0 = 0,
|
||||
|
||||
_1 = 1,
|
||||
|
||||
_2 = 2,
|
||||
|
||||
}
|
||||
|
||||
[System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.3.0.0 (NJsonSchema v11.2.0.0 (Newtonsoft.Json v13.0.0.0))")]
|
||||
public partial class WorkspaceFile
|
||||
{
|
||||
|
||||
@ -15,8 +15,19 @@ public class SettingService : BaseService<SettingService>
|
||||
public static string SettingFilePath { get; } =
|
||||
Path.Combine(AppDefaultValues.ProgramDataDirectory, "settings.json");
|
||||
|
||||
public AppSettingModel AppSetting { get; } = new AppSettingModel();
|
||||
public AppSettingModel AppSetting { get; private set; } = new AppSettingModel();
|
||||
|
||||
public ValueTask ResetAsync()
|
||||
{
|
||||
AppSetting.RepositoryPath = AppDefaultValues.DefaultRepositoryDirectory;
|
||||
AppSetting.RefreshWorkspaceOnOpen = true;
|
||||
AppSetting.RefreshWorkspaceOnFilesystemChanges = true;
|
||||
AppSetting.DiffTool = "";
|
||||
AppSetting.FileManagerTool = "";
|
||||
|
||||
return WriteToDiskAsync();
|
||||
}
|
||||
|
||||
public async ValueTask WriteToDiskAsync()
|
||||
{
|
||||
var stream = File.Exists(SettingFilePath) ? File.OpenWrite(SettingFilePath) : File.Create(SettingFilePath);
|
||||
|
||||
@ -86,7 +86,7 @@ public static class UIHelper
|
||||
}
|
||||
}
|
||||
|
||||
public static void NotifyError(string title, string content)
|
||||
public static void NotifyError(string content, string title = "Error")
|
||||
{
|
||||
try
|
||||
{
|
||||
@ -146,4 +146,17 @@ public static class UIHelper
|
||||
var vm = new SimpleMessageDialogViewModel(content);
|
||||
return OverlayDialog.ShowModal<SimpleMessageDialogView, SimpleMessageDialogViewModel>(vm, AppDefaultValues.HostId, opt);
|
||||
}
|
||||
|
||||
public static void NotifySuccess(string content, string title = "Success")
|
||||
{
|
||||
try
|
||||
{
|
||||
var nf = new Notification(title, content, NotificationType.Success);
|
||||
Notify.Show(nf);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine($"Can not notify success to users: {title} - {content}, {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
60
Flawless.Client/ViewModels/FirstSetupViewModel.cs
Normal file
60
Flawless.Client/ViewModels/FirstSetupViewModel.cs
Normal file
@ -0,0 +1,60 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Flawless.Client.Remote;
|
||||
using Flawless.Client.Service;
|
||||
using ReactiveUI;
|
||||
using ReactiveUI.SourceGenerators;
|
||||
using Refit;
|
||||
|
||||
namespace Flawless.Client.ViewModels;
|
||||
|
||||
public partial class FirstSetupViewModel : ViewModelBase, IRoutableViewModel
|
||||
{
|
||||
|
||||
public string? UrlPathSegment { get; } = Guid.NewGuid().ToString();
|
||||
|
||||
public IScreen HostScreen { get; }
|
||||
|
||||
[Reactive] private string _email;
|
||||
|
||||
[Reactive] private string _username;
|
||||
|
||||
[Reactive] private string _password;
|
||||
|
||||
public FirstSetupViewModel(IScreen hostScreen)
|
||||
{
|
||||
HostScreen = hostScreen;
|
||||
}
|
||||
|
||||
|
||||
[ReactiveCommand]
|
||||
private async Task SetupAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
using var l = UIHelper.MakeLoading("Setup...");
|
||||
await Api.C.Gateway.FirstSetup(new FirstSetupRequest()
|
||||
{
|
||||
AdminEmail = Email,
|
||||
AdminUsername = Username,
|
||||
AdminPassword = Password
|
||||
});
|
||||
|
||||
|
||||
await Api.C.LoginAsync(Username, Password);
|
||||
Api.C.Status.Value!.RequireInitialization = false;
|
||||
}
|
||||
catch (ApiException ex)
|
||||
{
|
||||
await Console.Error.WriteLineAsync($"Register as '{Username}' Failed: {ex.Content}");
|
||||
UIHelper.NotifyError(ex);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await Console.Error.WriteLineAsync($"Register as '{Username}' Failed: {ex}");
|
||||
UIHelper.NotifyError(ex);
|
||||
}
|
||||
|
||||
Console.WriteLine($"Register as '{Username}' success!");
|
||||
}
|
||||
}
|
||||
@ -34,7 +34,7 @@ public partial class HomeViewModel : ViewModelBase, IRoutableViewModel
|
||||
HostScreen = hostScreen;
|
||||
Username = Api.C.Username.Value!;
|
||||
Api.C.ServerUrl.SubscribeOn(AvaloniaScheduler.Instance)
|
||||
.Subscribe(v => ServerFriendlyName = v ?? "Unknown Server");
|
||||
.Subscribe(v => ServerFriendlyName = v ?? "Uname Server");
|
||||
|
||||
RefreshRepositoriesCommand.Execute();
|
||||
}
|
||||
|
||||
@ -37,7 +37,7 @@ public partial class IssueDetailViewModel : RoutableViewModelBase
|
||||
if (!await _service.UpdateIssueDetailsFromServerAsync(repo, issueId, false))
|
||||
{
|
||||
if (quitIfFailed) await NavigateBackAsync();
|
||||
UIHelper.NotifyError("Operation failed", "Can not load issue...");
|
||||
UIHelper.NotifyError("Can not load issue...", "Operation failed");
|
||||
return;
|
||||
}
|
||||
|
||||
@ -80,7 +80,7 @@ public partial class IssueDetailViewModel : RoutableViewModelBase
|
||||
using var _ = UIHelper.MakeLoading("Update issue...");
|
||||
if (string.IsNullOrWhiteSpace(NewComment))
|
||||
{
|
||||
UIHelper.NotifyError("No input", "No comment has been written!");
|
||||
UIHelper.NotifyError("No comment has been written!", "No input");
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@ -17,9 +17,9 @@ public partial class LoginPageViewModel : ViewModelBase, IRoutableViewModel
|
||||
|
||||
public IScreen HostScreen { get; }
|
||||
|
||||
[Reactive] private string _username = "cardidi";
|
||||
[Reactive] private string _username;
|
||||
|
||||
[Reactive] private string _password = "4453A2b33";
|
||||
[Reactive] private string _password;
|
||||
|
||||
public IObservable<bool> CanLogin;
|
||||
|
||||
|
||||
@ -0,0 +1,20 @@
|
||||
using System;
|
||||
using ReactiveUI;
|
||||
using ReactiveUI.SourceGenerators;
|
||||
|
||||
namespace Flawless.Client.ViewModels.ModalBox;
|
||||
|
||||
public partial class PasswordChangeDialogViewModel : ReactiveObject
|
||||
{
|
||||
[Reactive] public string OldPassword { get; set; } = string.Empty;
|
||||
[Reactive] public string NewPassword { get; set; } = string.Empty;
|
||||
[Reactive] public string ConfirmPassword { get; set; } = string.Empty;
|
||||
|
||||
|
||||
public bool Validate()
|
||||
{
|
||||
return !string.IsNullOrEmpty(NewPassword)
|
||||
&& NewPassword == ConfirmPassword
|
||||
&& !string.IsNullOrEmpty(OldPassword);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
using System;
|
||||
using ReactiveUI;
|
||||
using ReactiveUI.SourceGenerators;
|
||||
|
||||
namespace Flawless.Client.ViewModels.ModalBox;
|
||||
|
||||
public partial class UserCreateDialogViewModel : ReactiveObject
|
||||
{
|
||||
[Reactive] public string Username { get; set; } = string.Empty;
|
||||
[Reactive] public string Password { get; set; } = string.Empty;
|
||||
[Reactive] public string Email { get; set; } = string.Empty;
|
||||
|
||||
public bool Validate()
|
||||
{
|
||||
return !string.IsNullOrWhiteSpace(Username)
|
||||
&& !string.IsNullOrWhiteSpace(Password)
|
||||
&& !string.IsNullOrWhiteSpace(Email);
|
||||
}
|
||||
}
|
||||
@ -580,7 +580,7 @@ public partial class RepositoryViewModel : RoutableViewModelBase
|
||||
{
|
||||
if (!IsOwnerRole)
|
||||
{
|
||||
UIHelper.NotifyError("Permission issue", "Only repository owner can edit this!");
|
||||
UIHelper.NotifyError("Only repository owner can edit this!", "Permission issue");
|
||||
return;
|
||||
}
|
||||
|
||||
@ -599,7 +599,7 @@ public partial class RepositoryViewModel : RoutableViewModelBase
|
||||
{
|
||||
if (!IsOwnerRole)
|
||||
{
|
||||
UIHelper.NotifyError("Permission issue", "Only repository owner can edit this!");
|
||||
UIHelper.NotifyError("Only repository owner can edit this!", "Permission issue");
|
||||
return;
|
||||
}
|
||||
|
||||
@ -615,13 +615,13 @@ public partial class RepositoryViewModel : RoutableViewModelBase
|
||||
{
|
||||
if (vm.SafeRole == RepositoryModel.RepositoryRole.Owner)
|
||||
{
|
||||
UIHelper.NotifyError("Permission issue", "Invalid role level!");
|
||||
UIHelper.NotifyError("Invalid role level!", "Permission issue");
|
||||
return;
|
||||
}
|
||||
|
||||
if (vm.SafeRole == member.Role)
|
||||
{
|
||||
UIHelper.NotifyError("Modification issue", "No modification yet.");
|
||||
UIHelper.NotifyError("No modification yet.", "Modification issue");
|
||||
return;
|
||||
}
|
||||
|
||||
@ -636,7 +636,7 @@ public partial class RepositoryViewModel : RoutableViewModelBase
|
||||
{
|
||||
if (!IsOwnerRole)
|
||||
{
|
||||
UIHelper.NotifyError("Permission issue", "Only repository owner can edit this!");
|
||||
UIHelper.NotifyError("Only repository owner can edit this!", "Permission issue");
|
||||
return;
|
||||
}
|
||||
|
||||
@ -649,14 +649,14 @@ public partial class RepositoryViewModel : RoutableViewModelBase
|
||||
{
|
||||
if (vm.SafeRole == RepositoryModel.RepositoryRole.Owner)
|
||||
{
|
||||
UIHelper.NotifyError("Permission issue", "Invalid role level!");
|
||||
UIHelper.NotifyError("Invalid role level!", "Permission issue");
|
||||
return;
|
||||
}
|
||||
|
||||
vm.Username = vm.Username.Trim();
|
||||
if (string.IsNullOrEmpty(vm.Username) || vm.Username.Length < 3)
|
||||
{
|
||||
UIHelper.NotifyError("Parameter error", "Not a valid username!");
|
||||
UIHelper.NotifyError("Not a valid username!", "Parameter error");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@ -33,7 +33,10 @@ public partial class ServerSetupPageViewModel : RoutableViewModelBase
|
||||
{
|
||||
using var l = UIHelper.MakeLoading("Contacting to server...");
|
||||
await Api.C.SetGatewayAsync(Host);
|
||||
HostScreen.Router.Navigate.Execute(new LoginPageViewModel(HostScreen));
|
||||
|
||||
if (Api.C.Status.Value.RequireInitialization)
|
||||
HostScreen.Router.Navigate.Execute(new FirstSetupViewModel(HostScreen));
|
||||
else HostScreen.Router.Navigate.Execute(new LoginPageViewModel(HostScreen));
|
||||
}
|
||||
catch (ApiException ex)
|
||||
{
|
||||
|
||||
@ -1,12 +1,372 @@
|
||||
using System;
|
||||
using System.Windows.Input;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia.Threading;
|
||||
using DynamicData;
|
||||
using Flawless.Client.Models;
|
||||
using Flawless.Client.Remote;
|
||||
using Flawless.Client.Service;
|
||||
using Flawless.Client.ViewModels.ModalBox;
|
||||
using Flawless.Client.Views.ModalBox;
|
||||
using ReactiveUI;
|
||||
using ReactiveUI.SourceGenerators;
|
||||
using Refit;
|
||||
using Ursa.Controls;
|
||||
|
||||
namespace Flawless.Client.ViewModels;
|
||||
|
||||
public class SettingViewModel : RoutableViewModelBase
|
||||
public record Log(DateTime Time, Microsoft.Extensions.Logging.LogLevel Level, string Message)
|
||||
{
|
||||
public static Log From(LogEntryResponse response) => new(
|
||||
response.Timestamp.LocalDateTime,
|
||||
Enum.Parse<Microsoft.Extensions.Logging.LogLevel>(response.Level),
|
||||
response.Message);
|
||||
}
|
||||
|
||||
public partial class SettingViewModel : RoutableViewModelBase
|
||||
{
|
||||
public ObservableCollection<UserModel> Users { get; } = new();
|
||||
|
||||
public ObservableCollection<Log> Logs { get; } = new();
|
||||
|
||||
public AppSettingModel SettingModel => SettingService.C.AppSetting;
|
||||
|
||||
[Reactive] private UserModel _loginUser;
|
||||
|
||||
[Reactive] private string _nickname;
|
||||
|
||||
[Reactive] private string _email;
|
||||
|
||||
[Reactive] private string _phoneNumber;
|
||||
|
||||
[Reactive] private UserModel.SexType _gender;
|
||||
|
||||
[Reactive] private string _bio;
|
||||
|
||||
[Reactive] private DateTime?
|
||||
_logSearchFrom = null,
|
||||
_logSearchTo = null;
|
||||
|
||||
[Reactive] private int _page = 1;
|
||||
|
||||
[Reactive] private int _pageSize = 50;
|
||||
|
||||
[Reactive] private Microsoft.Extensions.Logging.LogLevel _loglevel = Microsoft.Extensions.Logging.LogLevel.Information;
|
||||
|
||||
[Reactive] private string _serverBlacklist, _serverWhitelist;
|
||||
|
||||
public SettingViewModel(IScreen hostScreen) : base(hostScreen)
|
||||
{
|
||||
LoadClientSettings();
|
||||
}
|
||||
|
||||
private async Task LoadServerData()
|
||||
{
|
||||
try
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
ServerBlacklist = sb.AppendJoin(",\n", await Api.C.Gateway.IpWhitelistGet()).ToString();
|
||||
ServerBlacklist = sb.Clear().AppendJoin(",\n", await Api.C.Gateway.IpBlacklistGet()).ToString();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
UIHelper.NotifyError(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadClientSettings()
|
||||
{
|
||||
LoginUser = (await UserService.C.GetOrDownloadUserInfoAsync(Api.C.Username.Value!))!;
|
||||
|
||||
Nickname = LoginUser.Nickname;
|
||||
Email = LoginUser.Email;
|
||||
PhoneNumber = LoginUser.PhoneNumber;
|
||||
Gender = LoginUser.Sex;
|
||||
Bio = LoginUser.Bio;
|
||||
|
||||
if (LoginUser.IsAdmin) await LoadServerData();
|
||||
}
|
||||
|
||||
[ReactiveCommand]
|
||||
private async Task SaveAccountChangesAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
await Api.C.Gateway.UpdateInfo(new UserInfoModifyResponse
|
||||
{
|
||||
NickName = this.Nickname == LoginUser.Nickname ? null! : this.Nickname,
|
||||
Gender = this.Gender == LoginUser.Sex ? null : (UserSex) this.Gender,
|
||||
Bio = this.Bio == LoginUser.Bio ? null! : this.Bio
|
||||
});
|
||||
|
||||
if (Email != LoginUser.Email)
|
||||
await Api.C.Gateway.UpdateEmail(new UserContactModifyResponse
|
||||
{ Email = Email, });
|
||||
|
||||
if (PhoneNumber != LoginUser.PhoneNumber)
|
||||
await Api.C.Gateway.UpdatePhone(new UserContactModifyResponse
|
||||
{ Phone = PhoneNumber, });
|
||||
|
||||
UIHelper.NotifySuccess("Saved");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
UIHelper.NotifyError(ex);
|
||||
}
|
||||
}
|
||||
|
||||
[ReactiveCommand]
|
||||
private async Task ChangePasswordAsync()
|
||||
{
|
||||
var result = new PasswordChangeDialogViewModel();
|
||||
var opt = UIHelper.DefaultOverlayDialogOptionsYesNo();
|
||||
while (true)
|
||||
{
|
||||
var r = await OverlayDialog.ShowModal<PasswordChangeDialogView, PasswordChangeDialogViewModel>(result, AppDefaultValues.HostId, opt);
|
||||
if (r == DialogResult.No) return;
|
||||
if (result.Validate()) break;
|
||||
|
||||
UIHelper.NotifyError("Please check your input");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await Api.C.Gateway.ResetPassword(new ResetPasswordRequest()
|
||||
{
|
||||
Identity = LoginUser.Username,
|
||||
NewPassword = result.NewPassword,
|
||||
OldPassword = result.OldPassword
|
||||
});
|
||||
|
||||
UIHelper.NotifySuccess("Password create successfully");
|
||||
}
|
||||
catch (ApiException ex)
|
||||
{
|
||||
UIHelper.NotifyError(ex);
|
||||
}
|
||||
}
|
||||
|
||||
[ReactiveCommand]
|
||||
private async Task DeleteSelfAccountAsync()
|
||||
{
|
||||
if (await UIHelper.SimpleAskAsync("This operation can not undo. Do you sure that?") == DialogResult.Yes)
|
||||
{
|
||||
try
|
||||
{
|
||||
await Api.C.Gateway.Delete(Api.C.Username.Value!);
|
||||
Process.GetCurrentProcess().Kill();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
UIHelper.NotifyError(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[ReactiveCommand]
|
||||
private async Task SaveClientPreferenceAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
await SettingService.C.WriteToDiskAsync();
|
||||
UIHelper.NotifySuccess("Saved Success!");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
UIHelper.NotifyError(e);
|
||||
}
|
||||
}
|
||||
|
||||
[ReactiveCommand]
|
||||
private async Task SaveServerPreferenceAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
await Api.C.Gateway.IpWhitelistPost(ServerWhitelist.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries));
|
||||
await Api.C.Gateway.IpBlacklistPost(ServerBlacklist.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries));
|
||||
UIHelper.NotifySuccess("Saved Success!");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
UIHelper.NotifyError(ex);
|
||||
}
|
||||
}
|
||||
|
||||
[ReactiveCommand]
|
||||
private async Task ResetClientPreferenceAsync()
|
||||
{
|
||||
if (await UIHelper.SimpleAskAsync("Do you sure reset settings to default?") == DialogResult.Yes)
|
||||
{
|
||||
await SettingService.C.ResetAsync();
|
||||
}
|
||||
}
|
||||
|
||||
[ReactiveCommand]
|
||||
private async Task ResetServerPreferenceAsync()
|
||||
{
|
||||
if (await UIHelper.SimpleAskAsync("Do you sure reset settings to default?") == DialogResult.Yes)
|
||||
{
|
||||
try
|
||||
{
|
||||
ServerBlacklist = ServerWhitelist = string.Empty;
|
||||
await Api.C.Gateway.IpBlacklistPost([]);
|
||||
await Api.C.Gateway.IpWhitelistPost([]);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
UIHelper.NotifyError(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[ReactiveCommand]
|
||||
private async Task CreateUserAsync()
|
||||
{
|
||||
var result = new UserCreateDialogViewModel();
|
||||
result.Password = GenerateRandomPassword();
|
||||
var opt = UIHelper.DefaultOverlayDialogOptionsYesNo();
|
||||
while (true)
|
||||
{
|
||||
var r = await OverlayDialog.ShowModal<UserCreateDialogView, UserCreateDialogViewModel>(result, AppDefaultValues.HostId, opt);
|
||||
if (r == DialogResult.No) return;
|
||||
if (result.Validate()) break;
|
||||
|
||||
UIHelper.NotifyError("Please check your input");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await Api.C.Gateway.Register(new RegisterRequest()
|
||||
{
|
||||
Username = result.Username,
|
||||
Password = result.Password,
|
||||
Email = result.Email
|
||||
});
|
||||
|
||||
Users.Add(UserService.C.GetUserInfoAsync(result.Username)!);
|
||||
UIHelper.NotifySuccess($"User {result.Username} create successfully");
|
||||
}
|
||||
catch (ApiException ex)
|
||||
{
|
||||
UIHelper.NotifyError(ex);
|
||||
}
|
||||
}
|
||||
|
||||
[ReactiveCommand]
|
||||
private async Task DeleteUserAsync(string username)
|
||||
{
|
||||
if (await UIHelper.SimpleAskAsync($"Do you sure that wanted to delete user '{username}' permanently?") == DialogResult.Yes)
|
||||
{
|
||||
try
|
||||
{
|
||||
await Api.C.Gateway.Delete(username);
|
||||
Users.Remove(Users.First(u => u.Username == username));
|
||||
UIHelper.NotifySuccess("User deleted");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
UIHelper.NotifyError(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[ReactiveCommand]
|
||||
private async Task ForceUpdateUserPasswordAsync(string username)
|
||||
{
|
||||
var newPassword = GenerateRandomPassword();
|
||||
try
|
||||
{
|
||||
await Api.C.Gateway.RenewPassword(new ResetPasswordRequest
|
||||
{
|
||||
Identity = username,
|
||||
NewPassword = newPassword
|
||||
});
|
||||
await UIHelper.SimpleAlert($"Password has been reset to {newPassword}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
UIHelper.NotifyError(ex);
|
||||
}
|
||||
}
|
||||
|
||||
[ReactiveCommand]
|
||||
private async Task ActivateUserAsync(string username)
|
||||
{
|
||||
await UpdateUserActivateAsync(username, true);
|
||||
}
|
||||
|
||||
[ReactiveCommand]
|
||||
private async Task InactivateUserAsync(string username)
|
||||
{
|
||||
await UpdateUserActivateAsync(username, false);
|
||||
}
|
||||
|
||||
[ReactiveCommand]
|
||||
private async Task ToSuperUserAsync(string username)
|
||||
{
|
||||
await UpdateUserSuperAsync(username, true);
|
||||
}
|
||||
|
||||
[ReactiveCommand]
|
||||
private async Task ToNormalUserAsync(string username)
|
||||
{
|
||||
await UpdateUserSuperAsync(username, false);
|
||||
}
|
||||
|
||||
[ReactiveCommand]
|
||||
private async Task DownloadServerLogAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var logs = await Api.C.Gateway.Logs(
|
||||
LogSearchFrom, LogSearchTo, (LogLevel?)Loglevel, Page, PageSize);
|
||||
|
||||
Logs.AddRange(logs.Select(Log.From));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
UIHelper.NotifyError(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task UpdateUserActivateAsync(string username, bool active)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (active) await Api.C.Gateway.Enable(username);
|
||||
else await Api.C.Gateway.Disable(username);
|
||||
|
||||
UIHelper.NotifySuccess($"{username} has already {(active ? "enabled" : "disabled")}.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
UIHelper.NotifyError(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task UpdateUserSuperAsync(string username, bool active)
|
||||
{
|
||||
try
|
||||
{
|
||||
await Api.C.Gateway.SuperuserPost(username, active);
|
||||
Users.First(x => x.Username == username).IsAdmin = active;
|
||||
UIHelper.NotifySuccess($"{username} has already {(active ? "enabled" : "disabled")}.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
UIHelper.NotifyError(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static string GenerateRandomPassword(int length = 12)
|
||||
{
|
||||
const string validChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890!@#$%^&*";
|
||||
var random = new Random();
|
||||
return new string(Enumerable.Repeat(validChars, length)
|
||||
.Select(s => s[random.Next(s.Length)]).ToArray());
|
||||
}
|
||||
}
|
||||
24
Flawless.Client/Views/HelloSetup/FirstSetupView.axaml
Normal file
24
Flawless.Client/Views/HelloSetup/FirstSetupView.axaml
Normal file
@ -0,0 +1,24 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:vm="clr-namespace:Flawless.Client.ViewModels"
|
||||
mc:Ignorable="d" d:DesignWidth="380" d:DesignHeight="540"
|
||||
x:Class="Flawless.Client.Views.HelloSetup.FirstSetupView"
|
||||
x:DataType="vm:FirstSetupViewModel">
|
||||
|
||||
<Design.DataContext>
|
||||
<!-- This only sets the DataContext for the previewer in an IDE,
|
||||
to set the actual DataContext for runtime, set the DataContext property in code (look at App.axaml.cs) -->
|
||||
<vm:RegisterPageViewModel/>
|
||||
</Design.DataContext>
|
||||
|
||||
<StackPanel HorizontalAlignment="Stretch" VerticalAlignment="Center" Spacing="10">
|
||||
<TextBox Watermark="Email" Text="{Binding Email, Mode=TwoWay}"/>
|
||||
<TextBox Watermark="Username" Text="{Binding Username, Mode=TwoWay}"/>
|
||||
<TextBox Watermark="Password" PasswordChar="*" Text="{Binding Password, Mode=TwoWay}"/>
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="{DynamicResource Semi}">
|
||||
<Button Content="Register" Command="{Binding SetupCommand}"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</UserControl>
|
||||
15
Flawless.Client/Views/HelloSetup/FirstSetupView.axaml.cs
Normal file
15
Flawless.Client/Views/HelloSetup/FirstSetupView.axaml.cs
Normal file
@ -0,0 +1,15 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Flawless.Client.ViewModels;
|
||||
using Ursa.ReactiveUIExtension;
|
||||
|
||||
namespace Flawless.Client.Views.HelloSetup;
|
||||
|
||||
public partial class FirstSetupView : ReactiveUrsaView<FirstSetupViewModel>
|
||||
{
|
||||
public FirstSetupView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,20 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="using:Flawless.Client.ViewModels.ModalBox"
|
||||
x:Class="Flawless.Client.Views.ModalBox.PasswordChangeDialogView"
|
||||
x:DataType="vm:PasswordChangeDialogViewModel">
|
||||
|
||||
<Grid Margin="10" RowDefinitions="Auto,Auto,Auto">
|
||||
<TextBox Watermark="Old Password"
|
||||
PasswordChar="*"
|
||||
Text="{Binding OldPassword}"/>
|
||||
|
||||
<TextBox Grid.Row="1" Watermark="New Password"
|
||||
PasswordChar="*"
|
||||
Text="{Binding NewPassword}"/>
|
||||
|
||||
<TextBox Grid.Row="2" Watermark="Confirm New Password"
|
||||
PasswordChar="*"
|
||||
Text="{Binding ConfirmPassword}"/>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@ -0,0 +1,13 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
|
||||
namespace Flawless.Client.Views.ModalBox;
|
||||
|
||||
public partial class PasswordChangeDialogView : UserControl
|
||||
{
|
||||
public PasswordChangeDialogView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
13
Flawless.Client/Views/ModalBox/UserCreateDialogView.axaml
Normal file
13
Flawless.Client/Views/ModalBox/UserCreateDialogView.axaml
Normal file
@ -0,0 +1,13 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="using:Flawless.Client.ViewModels.ModalBox"
|
||||
x:Class="Flawless.Client.Views.ModalBox.UserCreateDialogView"
|
||||
x:DataType="vm:UserCreateDialogViewModel">
|
||||
|
||||
<Grid Margin="10" RowDefinitions="Auto,Auto,Auto">
|
||||
<TextBox Grid.Row="0" Watermark="Username" Text="{Binding Username}"/>
|
||||
<TextBox Grid.Row="1" Watermark="Password"
|
||||
PasswordChar="*" Text="{Binding Password}"/>
|
||||
<TextBox Grid.Row="2" Watermark="Email" Text="{Binding Email}"/>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
13
Flawless.Client/Views/ModalBox/UserCreateDialogView.axaml.cs
Normal file
13
Flawless.Client/Views/ModalBox/UserCreateDialogView.axaml.cs
Normal file
@ -0,0 +1,13 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
|
||||
namespace Flawless.Client.Views.ModalBox;
|
||||
|
||||
public partial class UserCreateDialogView : UserControl
|
||||
{
|
||||
public UserCreateDialogView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@ -3,7 +3,6 @@
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:u="https://irihi.tech/ursa"
|
||||
xmlns:semi="https://irihi.tech/semi"
|
||||
xmlns:vm="using:Flawless.Client.ViewModels"
|
||||
x:DataType="vm:SettingViewModel"
|
||||
mc:Ignorable="d" d:DesignWidth="1280" d:DesignHeight="768"
|
||||
@ -19,18 +18,20 @@
|
||||
<TabItem Header="Account">
|
||||
<ScrollViewer Width="600" HorizontalAlignment="Left" Margin="6">
|
||||
<u:Form HorizontalAlignment="Stretch" VerticalAlignment="Stretch" LabelPosition="Top">
|
||||
<Label Content="You are a server manager!"/>
|
||||
<TextBox u:FormItem.Label="Username" IsReadOnly="True"/>
|
||||
<TextBox u:FormItem.Label="Nickname" IsReadOnly="True"/>
|
||||
<TextBox u:FormItem.Label="Email" IsReadOnly="True"/>
|
||||
<TextBox u:FormItem.Label="Phone Number" IsReadOnly="True"/>
|
||||
<ComboBox u:FormItem.Label="Gender">
|
||||
<ComboBoxItem Content="Unset"/>
|
||||
<ComboBoxItem Content="Male"/>
|
||||
<ComboBoxItem Content="Female"/>
|
||||
<ComboBoxItem Content="Walmart Plastic Bag"/>
|
||||
</ComboBox>
|
||||
<TextBox u:FormItem.Label="Bio" Classes="TextArea"/>
|
||||
<Label IsVisible="{Binding LoginUser.IsAdmin}" Content="You are a server manager!"/>
|
||||
<TextBox u:FormItem.Label="Username" IsReadOnly="True"
|
||||
Text="{Binding LoginUser.Username, Mode=OneWay}"/>
|
||||
<TextBox u:FormItem.Label="Nickname" Text="{Binding Nickname}"/>
|
||||
<TextBox u:FormItem.Label="Email" Text="{Binding Email}"/>
|
||||
<TextBox u:FormItem.Label="Phone Number" Text="{Binding PhoneNumber}"/>
|
||||
<u:EnumSelector u:FormItem.Label="Gender" SelectedValue="{Binding Gender}"/>
|
||||
<TextBox u:FormItem.Label="Bio" Classes="TextArea" Text="{Binding Bio}"/>
|
||||
|
||||
<StackPanel Orientation="Horizontal" Spacing="4">
|
||||
<u:IconButton Content="Save" Command="{Binding SaveAccountChangesCommand}"/>
|
||||
<u:IconButton Content="Change Password" Command="{Binding ChangePasswordCommand}"/>
|
||||
<u:IconButton Classes="Danger" Content="Delete" Command="{Binding DeleteSelfAccountCommand}"/>
|
||||
</StackPanel>
|
||||
</u:Form>
|
||||
</ScrollViewer>
|
||||
</TabItem>
|
||||
@ -42,40 +43,70 @@
|
||||
</u:FormItem>
|
||||
<u:FormGroup Header="Workspace Refresh">
|
||||
<StackPanel Spacing="4">
|
||||
<CheckBox Theme="{StaticResource CardCheckBox}" Content="Open Workspace"/>
|
||||
<CheckBox Theme="{StaticResource CardCheckBox}" Content="Filesystem Changed"/>
|
||||
<CheckBox Theme="{StaticResource CardCheckBox}" Content="Gain Focus"/>
|
||||
<CheckBox Theme="{StaticResource CardCheckBox}" Content="Open Workspace"
|
||||
IsChecked="{Binding SettingModel.RefreshWorkspaceOnOpen}"/>
|
||||
<CheckBox Theme="{StaticResource CardCheckBox}" Content="Filesystem Changed"
|
||||
IsChecked="{Binding SettingModel.RefreshWorkspaceOnFilesystemChanges}"/>
|
||||
</StackPanel>
|
||||
</u:FormGroup>
|
||||
<u:FormGroup Header="Extern Tools">
|
||||
<u:PathPicker u:FormItem.Label="Open Folder"/>
|
||||
<u:PathPicker u:FormItem.Label="Diff Tool"/>
|
||||
</u:FormGroup>
|
||||
<StackPanel Orientation="Horizontal" Spacing="4">
|
||||
<u:IconButton Content="Save" Command="{Binding SaveClientPreferenceCommand}"/>
|
||||
<u:IconButton Classes="Danger" Content="Reset" Command="{Binding ResetClientPreferenceCommand}"/>
|
||||
</StackPanel>
|
||||
</u:Form>
|
||||
</ScrollViewer>
|
||||
</TabItem>
|
||||
<TabItem Header="Server">
|
||||
<TabItem IsVisible="{Binding LoginUser.IsAdmin}" Header="Server">
|
||||
<ScrollViewer Width="600" HorizontalAlignment="Left" Margin="6">
|
||||
<u:Form HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
|
||||
<TextBox u:FormItem.Label="Server Public Name"/>
|
||||
<ToggleSwitch u:FormItem.Label="Allow Public Register"/>
|
||||
<!-- <TextBox u:FormItem.Label="Server Public Name"/> -->
|
||||
<!-- <ToggleSwitch u:FormItem.Label="Allow Public Register"/> -->
|
||||
<u:FormGroup>
|
||||
<TextBox u:FormItem.Label="IP Whitelist" Classes="TextArea"/>
|
||||
<TextBox u:FormItem.Label="IP Blacklist" Classes="TextArea"/>
|
||||
<TextBox u:FormItem.Label="IP Whitelist" Classes="TextArea" AcceptsReturn="True"
|
||||
Text="{Binding ServerWhitelist}"/>
|
||||
<TextBox u:FormItem.Label="IP Blacklist" Classes="TextArea" AcceptsReturn="True"
|
||||
Text="{Binding ServerBlacklist}"/>
|
||||
</u:FormGroup>
|
||||
<StackPanel Orientation="Horizontal" Spacing="4">
|
||||
<u:IconButton Content="Save" Command="{Binding SaveServerPreferenceCommand}"/>
|
||||
<u:IconButton Classes="Danger" Content="Reset"
|
||||
Command="{Binding ResetServerPreferenceCommand}"/>
|
||||
</StackPanel>
|
||||
</u:Form>
|
||||
</ScrollViewer>
|
||||
</TabItem>
|
||||
<TabItem Header="Users">
|
||||
<TabItem IsVisible="{Binding LoginUser.IsAdmin}" Header="Users">
|
||||
</TabItem>
|
||||
<TabItem Header="Server Logfile">
|
||||
<StackPanel Spacing="8" Orientation="Vertical" HorizontalAlignment="Stretch">
|
||||
<TabItem IsVisible="{Binding LoginUser.IsAdmin}" Header="Logfile">
|
||||
<StackPanel Spacing="8" Orientation="Vertical" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
|
||||
<StackPanel Orientation="Horizontal" Spacing="4">
|
||||
<u:IconButton Icon="{StaticResource}"></u:IconButton>
|
||||
<DatePicker SelectedDate="{Binding LogSearchFrom}"/>
|
||||
<DatePicker SelectedDate="{Binding LogSearchTo}"/>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Spacing="4">
|
||||
<u:EnumSelector SelectedValue="{Binding Loglevel}"/>
|
||||
<NumericUpDown Value="{Binding PageSize}" Minimum="10" Maximum="100"/>
|
||||
<NumericUpDown Value="{Binding Page}" Minimum="1"/>
|
||||
<u:IconButton Icon="{StaticResource SemiIconSearch}"
|
||||
Command="{Binding DownloadServerLogCommand}"/>
|
||||
</StackPanel>
|
||||
<ListBox ItemsSource="{Binding Logs}">
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Grid RowDefinitions="Auto, *" ColumnDefinitions="Auto,*,Auto">
|
||||
<Label Grid.Row="0" Grid.Column="0" FontSize="16" Content="{Binding Level}"/>
|
||||
<Label Grid.Row="0" Grid.Column="2" FontSize="16" Content="{Binding Time}"/>
|
||||
<Label Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="3" Content="{Binding Message}"/>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
</StackPanel>
|
||||
</TabItem>
|
||||
<TabItem Header="Server Statitics"></TabItem>
|
||||
</TabControl>
|
||||
</DockPanel>
|
||||
</UserControl>
|
||||
|
||||
10
Flawless.Communication/Request/FirstSetupRequest.cs
Normal file
10
Flawless.Communication/Request/FirstSetupRequest.cs
Normal file
@ -0,0 +1,10 @@
|
||||
namespace Flawless.Communication.Request;
|
||||
|
||||
public record FirstSetupRequest
|
||||
{
|
||||
public string AdminEmail { get; set; }
|
||||
|
||||
public string AdminUsername { get; set; }
|
||||
|
||||
public string AdminPassword { get; set; }
|
||||
}
|
||||
5
Flawless.Communication/Request/WebHookCreateRequest.cs
Normal file
5
Flawless.Communication/Request/WebHookCreateRequest.cs
Normal file
@ -0,0 +1,5 @@
|
||||
using Flawless.Communication.Shared;
|
||||
|
||||
namespace Flawless.Communication.Request;
|
||||
|
||||
public record WebhookCreateRequest(string TargetUrl, WebhookEventType EventType, string Secret);
|
||||
7
Flawless.Communication/Response/LogEntryResponse.cs
Normal file
7
Flawless.Communication/Response/LogEntryResponse.cs
Normal file
@ -0,0 +1,7 @@
|
||||
namespace Flawless.Communication.Response;
|
||||
|
||||
public record LogEntryResponse(
|
||||
DateTime Timestamp,
|
||||
string Level,
|
||||
string Message,
|
||||
string? Exception);
|
||||
3
Flawless.Communication/Response/PaginatedResponse.cs
Normal file
3
Flawless.Communication/Response/PaginatedResponse.cs
Normal file
@ -0,0 +1,3 @@
|
||||
namespace Flawless.Communication.Response;
|
||||
|
||||
public record PaginatedResponse<T>(List<T> Results, int TotalCount, int Page, int PageSize);
|
||||
@ -5,4 +5,8 @@ public record ServerStatusResponse
|
||||
public required string? FriendlyName { get; init; }
|
||||
|
||||
public required bool AllowPublicRegister { get; set; }
|
||||
|
||||
public required bool AllowWebHook { get; set; }
|
||||
|
||||
public required bool RequireInitialization { get; set; }
|
||||
}
|
||||
@ -21,4 +21,6 @@ public record UserInfoResponse
|
||||
public bool? PublicEmail { get; set; }
|
||||
|
||||
public DateTime? CreatedAt { get; set; }
|
||||
|
||||
public bool? IsAdmin { get; set; }
|
||||
}
|
||||
8
Flawless.Communication/Shared/WebhookEventType.cs
Normal file
8
Flawless.Communication/Shared/WebhookEventType.cs
Normal file
@ -0,0 +1,8 @@
|
||||
namespace Flawless.Communication.Shared;
|
||||
|
||||
public enum WebhookEventType
|
||||
{
|
||||
Push,
|
||||
IssueCreated,
|
||||
IssueUpdate
|
||||
}
|
||||
154
Flawless.Server/Controllers/AdminController.cs
Normal file
154
Flawless.Server/Controllers/AdminController.cs
Normal file
@ -0,0 +1,154 @@
|
||||
using System.Net;
|
||||
using Flawless.Communication.Request;
|
||||
using Flawless.Communication.Response;
|
||||
using Flawless.Server.Models;
|
||||
using Flawless.Server.Services;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Flawless.Server.Controllers;
|
||||
|
||||
[ApiController, Authorize(Roles = "admin"), Route("api/admin")]
|
||||
public class AdminController(
|
||||
UserManager<AppUser> userManager,
|
||||
AccessControlService accessControlService,
|
||||
AppDbContext dbContext) : ControllerBase
|
||||
{
|
||||
[HttpPost("superuser/{username}")]
|
||||
public async Task<IActionResult> SetSuperuserAsync(string username, bool toSuper)
|
||||
{
|
||||
var user = await userManager.FindByNameAsync(username);
|
||||
var optUser = (await userManager.GetUserAsync(HttpContext.User))!;
|
||||
|
||||
if (user == null) return BadRequest(new FailedResponse("User does not exist!"));
|
||||
if (optUser == user) return BadRequest(new FailedResponse("You cannot set/unset yourself to superuser!"));
|
||||
|
||||
user.Admin = toSuper;
|
||||
await userManager.UpdateAsync(user);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
|
||||
[HttpGet("superuser/{username}")]
|
||||
public async Task<ActionResult<bool>> GetSuperuserAsync(string username)
|
||||
{
|
||||
var user = await userManager.FindByNameAsync(username);
|
||||
if (user == null) return BadRequest(new FailedResponse("User does not exist!"));
|
||||
|
||||
return user.Admin;
|
||||
}
|
||||
|
||||
[HttpPost("user/delete/{username}")]
|
||||
public async Task<IActionResult> DeleteUserAsync(string username)
|
||||
{
|
||||
var user = await userManager.FindByNameAsync(username);
|
||||
|
||||
if (user == null) return BadRequest(new FailedResponse("User does not exist!"));
|
||||
var result = await userManager.DeleteAsync(user);
|
||||
|
||||
if (!result.Succeeded) return BadRequest(new FailedResponse(result.Errors));
|
||||
return Ok();
|
||||
}
|
||||
|
||||
[HttpPost("user/enable/{username}")]
|
||||
public async Task<IActionResult> EnableUserAsync(string username)
|
||||
{
|
||||
var user = await userManager.FindByNameAsync(username);
|
||||
|
||||
if (user == null) return BadRequest(new FailedResponse("User does not exist!"));
|
||||
var result = await userManager.SetLockoutEnabledAsync(user, false);
|
||||
|
||||
if (!result.Succeeded) return BadRequest(new FailedResponse(result.Errors));
|
||||
return Ok();
|
||||
}
|
||||
|
||||
[HttpPost("user/disable/{username}")]
|
||||
public async Task<IActionResult> DisableUserAsync(string username)
|
||||
{
|
||||
var user = await userManager.FindByNameAsync(username);
|
||||
|
||||
if (user == null) return BadRequest(new FailedResponse("User does not exist!"));
|
||||
var result = await userManager.SetLockoutEnabledAsync(user, true);
|
||||
|
||||
if (!result.Succeeded) return BadRequest(new FailedResponse(result.Errors));
|
||||
return Ok();
|
||||
}
|
||||
|
||||
[HttpPost("user/reset_password")]
|
||||
public async Task<IActionResult> ResetPasswordAsync(ResetPasswordRequest r)
|
||||
{
|
||||
if (r.Identity == null) return BadRequest(new FailedResponse("Identity (User Id) is not set!"));
|
||||
var user = await userManager.FindByIdAsync(r.Identity);
|
||||
|
||||
if (user == null) return BadRequest(new FailedResponse("Identity (User Id) does not exist!"));
|
||||
var resetToken = await userManager.GeneratePasswordResetTokenAsync(user);
|
||||
var result = await userManager.ResetPasswordAsync(user, resetToken, r.NewPassword);
|
||||
|
||||
if (!result.Succeeded) return BadRequest(new FailedResponse(result.Errors));
|
||||
return Ok();
|
||||
}
|
||||
|
||||
[HttpPost("access_control/ip_whitelist")]
|
||||
public async Task<IActionResult> SetIpWhitelistAsync([FromBody] string[] ips)
|
||||
{
|
||||
await accessControlService.UpdatePolicyAsync(IpPolicyType.Whitelist, ips);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
[HttpGet("access_control/ip_whitelist")]
|
||||
public async Task<ActionResult<IEnumerable<string>>> GetIpWhitelistAsync()
|
||||
{
|
||||
return Ok(await accessControlService.GetIpListAsync(IpPolicyType.Whitelist));
|
||||
}
|
||||
|
||||
[HttpPost("access_control/ip_blacklist")]
|
||||
public async Task<IActionResult> SetIpBlacklistAsync([FromBody] string[] ips)
|
||||
{
|
||||
await accessControlService.UpdatePolicyAsync(IpPolicyType.Blacklist, ips);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
[HttpGet("access_control/ip_blacklist")]
|
||||
public async Task<ActionResult<IEnumerable<string>>> GetIpBlacklistAsync()
|
||||
{
|
||||
return Ok(await accessControlService.GetIpListAsync(IpPolicyType.Blacklist));
|
||||
}
|
||||
|
||||
[HttpGet("logs")]
|
||||
public async Task<ActionResult<IEnumerable<LogEntryResponse>>> GetSystemLogsAsync(
|
||||
[FromQuery] DateTime? startTime = null,
|
||||
[FromQuery] DateTime? endTime = null,
|
||||
[FromQuery] LogLevel? level = null,
|
||||
[FromQuery] int page = 1,
|
||||
[FromQuery] int pageSize = 50)
|
||||
{
|
||||
var query = dbContext.SystemLogs.AsQueryable();
|
||||
|
||||
// 时间过滤
|
||||
if (startTime.HasValue)
|
||||
query = query.Where(l => l.Timestamp >= startTime);
|
||||
if (endTime.HasValue)
|
||||
query = query.Where(l => l.Timestamp <= endTime);
|
||||
|
||||
// 日志级别过滤
|
||||
if (level.HasValue && level.Value != LogLevel.None)
|
||||
query = query.Where(l => l.LogLevel == level.Value);
|
||||
|
||||
// 分页处理
|
||||
var totalCount = await query.CountAsync();
|
||||
var results = await query
|
||||
.OrderByDescending(l => l.Timestamp)
|
||||
.Skip((page - 1) * pageSize)
|
||||
.Take(pageSize)
|
||||
.Select(l => new LogEntryResponse(
|
||||
l.Timestamp,
|
||||
l.LogLevel.ToString(),
|
||||
l.Message,
|
||||
l.Exception))
|
||||
.ToListAsync();
|
||||
|
||||
return Ok(new PaginatedResponse<LogEntryResponse>(results, totalCount, page, pageSize));
|
||||
}
|
||||
}
|
||||
@ -1,64 +0,0 @@
|
||||
using Flawless.Communication.Request;
|
||||
using Flawless.Communication.Response;
|
||||
using Flawless.Server.Models;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Flawless.Server.Controllers;
|
||||
|
||||
[ApiController, Authorize, Route("api/admin")]
|
||||
public class AdminUserController(
|
||||
UserManager<AppUser> userManager) : ControllerBase
|
||||
{
|
||||
[HttpPost("user/delete/{username}")]
|
||||
public async Task<IActionResult> DeleteUserAsync(string username)
|
||||
{
|
||||
var user = await userManager.FindByNameAsync(username);
|
||||
|
||||
if (user == null) return BadRequest(new FailedResponse("User does not exist!"));
|
||||
var result = await userManager.DeleteAsync(user);
|
||||
|
||||
if (!result.Succeeded) return BadRequest(new FailedResponse(result.Errors));
|
||||
return Ok();
|
||||
}
|
||||
|
||||
[HttpPost("user/enable/{username}")]
|
||||
public async Task<IActionResult> EnableUserAsync(string username)
|
||||
{
|
||||
var user = await userManager.FindByNameAsync(username);
|
||||
|
||||
if (user == null) return BadRequest(new FailedResponse("User does not exist!"));
|
||||
var result = await userManager.SetLockoutEnabledAsync(user, false);
|
||||
|
||||
if (!result.Succeeded) return BadRequest(new FailedResponse(result.Errors));
|
||||
return Ok();
|
||||
}
|
||||
|
||||
[HttpPost("user/disable/{username}")]
|
||||
public async Task<IActionResult> DisableUserAsync(string username)
|
||||
{
|
||||
var user = await userManager.FindByNameAsync(username);
|
||||
|
||||
if (user == null) return BadRequest(new FailedResponse("User does not exist!"));
|
||||
var result = await userManager.SetLockoutEnabledAsync(user, true);
|
||||
|
||||
if (!result.Succeeded) return BadRequest(new FailedResponse(result.Errors));
|
||||
return Ok();
|
||||
}
|
||||
|
||||
[HttpPost("user/reset_password")]
|
||||
public async Task<IActionResult> ResetPasswordAsync(ResetPasswordRequest r)
|
||||
{
|
||||
if (r.Identity == null) return BadRequest(new FailedResponse("Identity (User Id) is not set!"));
|
||||
var user = await userManager.FindByIdAsync(r.Identity);
|
||||
|
||||
if (user == null) return BadRequest(new FailedResponse("Identity (User Id) does not exist!"));
|
||||
var resetToken = await userManager.GeneratePasswordResetTokenAsync(user);
|
||||
var result = await userManager.ResetPasswordAsync(user, resetToken, r.NewPassword);
|
||||
|
||||
if (!result.Succeeded) return BadRequest(new FailedResponse(result.Errors));
|
||||
return Ok();
|
||||
}
|
||||
|
||||
}
|
||||
@ -17,7 +17,7 @@ public class AuthenticationController(
|
||||
SignInManager<AppUser> signInManager,
|
||||
TokenGenerationService tokenService,
|
||||
AppDbContext dbContext,
|
||||
IConfiguration config,
|
||||
SettingFacade setting,
|
||||
ILogger<AuthenticationController> logger)
|
||||
: ControllerBase
|
||||
{
|
||||
@ -27,15 +27,44 @@ public class AuthenticationController(
|
||||
{
|
||||
return Task.FromResult<ActionResult<ServerStatusResponse>>(Ok(new ServerStatusResponse()
|
||||
{
|
||||
AllowPublicRegister = true,
|
||||
FriendlyName = "Ca2didi Server"
|
||||
AllowPublicRegister = setting.AllowPublicRegistration,
|
||||
AllowWebHook = setting.UseWebHook,
|
||||
FriendlyName = setting.ServerName,
|
||||
RequireInitialization = !dbContext.Users.Any(x => x.Admin),
|
||||
}));
|
||||
}
|
||||
|
||||
[HttpPost("first-setup")]
|
||||
public async Task<ActionResult> FirstSetupAsync(FirstSetupRequest r)
|
||||
{
|
||||
if (dbContext.Users.Any(x => x.Admin))
|
||||
{
|
||||
return BadRequest(new FailedResponse("Server has been initialized."));
|
||||
}
|
||||
|
||||
var user = new AppUser
|
||||
{
|
||||
UserName = r.AdminUsername,
|
||||
Email = r.AdminEmail,
|
||||
EmailConfirmed = true,
|
||||
CreatedOn = DateTime.UtcNow,
|
||||
};
|
||||
|
||||
user.RenewSecurityStamp();
|
||||
var result = await userManager.CreateAsync(user, r.AdminPassword);
|
||||
if (result.Succeeded)
|
||||
{
|
||||
logger.LogInformation("User '{0}' created (PUBLIC REGISTER)", user.UserName);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
return BadRequest(new FailedResponse(result.Errors));
|
||||
}
|
||||
|
||||
[HttpPost("register")]
|
||||
public async Task<ActionResult> PublicRegisterAsync(RegisterRequest request)
|
||||
{
|
||||
if (!config.GetValue("User:PublicRegister", true))
|
||||
if (!setting.AllowPublicRegistration)
|
||||
return BadRequest(new FailedResponse("Not opened for public register."));
|
||||
|
||||
|
||||
@ -45,9 +74,9 @@ public class AuthenticationController(
|
||||
Email = request.Email,
|
||||
EmailConfirmed = true,
|
||||
CreatedOn = DateTime.UtcNow,
|
||||
SecurityStamp = Guid.NewGuid().ToString()
|
||||
};
|
||||
|
||||
user.RenewSecurityStamp();
|
||||
var result = await userManager.CreateAsync(user, request.Password);
|
||||
if (result.Succeeded)
|
||||
{
|
||||
|
||||
@ -17,6 +17,7 @@ namespace Flawless.Server.Controllers;
|
||||
public class RepositoryInnieController(
|
||||
UserManager<AppUser> userManager,
|
||||
AppDbContext dbContext,
|
||||
WebhookService webhookService,
|
||||
PathTransformer transformer) : ControllerBase
|
||||
{
|
||||
|
||||
@ -161,6 +162,52 @@ public class RepositoryInnieController(
|
||||
|
||||
return rp;
|
||||
}
|
||||
|
||||
[HttpPost("create_webhook")]
|
||||
public async Task<IActionResult> CreateWebhook(
|
||||
string userName,
|
||||
string repositoryName,
|
||||
[FromBody] WebhookCreateRequest request)
|
||||
{
|
||||
var user = (await userManager.GetUserAsync(HttpContext.User))!;
|
||||
var grantIssue = await ValidateRepositoryAsync(userName, repositoryName, user, RepositoryRole.Owner);
|
||||
if (grantIssue is not Repository rp) return (IActionResult) grantIssue;
|
||||
|
||||
await webhookService.AddWebhookAsync(rp.Id, request.TargetUrl, request.EventType, request.Secret);
|
||||
return Created();
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<IEnumerable<Webhook>>> GetWebhooks(string userName, string repositoryName)
|
||||
{
|
||||
var user = (await userManager.GetUserAsync(HttpContext.User))!;
|
||||
var grantIssue = await ValidateRepositoryAsync(userName, repositoryName, user, RepositoryRole.Developer);
|
||||
if (grantIssue is not Repository rp) return (ActionResult) grantIssue;
|
||||
|
||||
return Ok(await webhookService.GetWebhooksAsync(rp.Id));
|
||||
}
|
||||
|
||||
[HttpPost("{webhookId}/toggle")]
|
||||
public async Task<IActionResult> ToggleWebhook(string userName, string repositoryName, int webhookId, bool activate)
|
||||
{
|
||||
var user = (await userManager.GetUserAsync(HttpContext.User))!;
|
||||
var grantIssue = await ValidateRepositoryAsync(userName, repositoryName, user, RepositoryRole.Owner);
|
||||
if (grantIssue is not Repository rp) return (ActionResult) grantIssue;
|
||||
|
||||
await webhookService.ToggleWebhookAsync(rp.Id, webhookId, activate);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
[HttpDelete("{webhookId}")]
|
||||
public async Task<IActionResult> DeleteWebhook(string userName, string repositoryName, int webhookId)
|
||||
{
|
||||
var user = (await userManager.GetUserAsync(HttpContext.User))!;
|
||||
var grantIssue = await ValidateRepositoryAsync(userName, repositoryName, user, RepositoryRole.Owner);
|
||||
if (grantIssue is not Repository rp) return (ActionResult) grantIssue;
|
||||
|
||||
await webhookService.DeleteWebhookAsync(rp.Id, webhookId);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
|
||||
[HttpPost("fetch_manifest")]
|
||||
|
||||
@ -128,6 +128,7 @@ public class UserController(
|
||||
PublicEmail = authorized ? queryUser.PublicEmail : null,
|
||||
Email = queryUser.PublicEmail || authorized ? queryUser.Email : null,
|
||||
Phone = authorized ? queryUser.PhoneNumber : null,
|
||||
IsAdmin = queryUser.Admin
|
||||
};
|
||||
}
|
||||
}
|
||||
0
Flawless.Server/Data/AppDbContext.cs
Normal file
0
Flawless.Server/Data/AppDbContext.cs
Normal file
@ -7,6 +7,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="MailKit" Version="4.11.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.3" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="9.0.3" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.2"/>
|
||||
@ -37,9 +38,4 @@
|
||||
<Compile Remove="Migrations\20250322194407_InitialCreate.Designer.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="data\development\ff9864da-eac3-41f5-9d0f-99d51e608743\Depots\" />
|
||||
<Folder Include="data\development\ff9864da-eac3-41f5-9d0f-99d51e608743\Manifests\" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
36
Flawless.Server/Middlewares/IpFilterMiddleware.cs
Normal file
36
Flawless.Server/Middlewares/IpFilterMiddleware.cs
Normal file
@ -0,0 +1,36 @@
|
||||
using Flawless.Server.Services;
|
||||
|
||||
namespace Flawless.Server.Middlewares;
|
||||
|
||||
public class IpFilterMiddleware : IDisposable
|
||||
{
|
||||
private readonly RequestDelegate _next;
|
||||
private readonly IServiceScope _subScope;
|
||||
private readonly AccessControlService _accessControl;
|
||||
|
||||
public IpFilterMiddleware(RequestDelegate next, IServiceProvider provider)
|
||||
{
|
||||
_next = next;
|
||||
_subScope = provider.CreateScope();
|
||||
_accessControl = _subScope.ServiceProvider.GetRequiredService<AccessControlService>();
|
||||
}
|
||||
|
||||
public async Task InvokeAsync(HttpContext context)
|
||||
{
|
||||
var ip = context.Connection.RemoteIpAddress?.ToString();
|
||||
|
||||
if (string.IsNullOrEmpty(ip) || !await _accessControl.IsIpAllowedAsync(ip))
|
||||
{
|
||||
context.Response.StatusCode = StatusCodes.Status403Forbidden;
|
||||
await context.Response.WriteAsync($"IP access is denied. Your IP: {ip}");
|
||||
return;
|
||||
}
|
||||
|
||||
await _next(context);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_subScope.Dispose();
|
||||
}
|
||||
}
|
||||
@ -21,6 +21,8 @@ public class AppUser : IdentityUser<Guid>
|
||||
[Required]
|
||||
public bool PublicEmail { get; set; } = true;
|
||||
|
||||
public bool Admin { get; set; } = false;
|
||||
|
||||
|
||||
public void RenewSecurityStamp()
|
||||
{
|
||||
|
||||
21
Flawless.Server/Models/IpPolicy.cs
Normal file
21
Flawless.Server/Models/IpPolicy.cs
Normal file
@ -0,0 +1,21 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Flawless.Server.Models;
|
||||
|
||||
public enum IpPolicyType
|
||||
{
|
||||
Whitelist,
|
||||
Blacklist
|
||||
}
|
||||
|
||||
public class IpPolicy
|
||||
{
|
||||
[Key]
|
||||
public int Id { get; set; }
|
||||
|
||||
public required string IpAddress { get; set; }
|
||||
|
||||
public required IpPolicyType PolicyType { get; set; }
|
||||
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
16
Flawless.Server/Models/SystemLog.cs
Normal file
16
Flawless.Server/Models/SystemLog.cs
Normal file
@ -0,0 +1,16 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Flawless.Server.Models;
|
||||
|
||||
[Index(nameof(Timestamp))]
|
||||
public class SystemLog
|
||||
{
|
||||
[Key]
|
||||
public int Id { get; set; }
|
||||
public DateTime Timestamp { get; set; } = DateTime.UtcNow;
|
||||
public LogLevel LogLevel { get; set; }
|
||||
public string Message { get; set; } = string.Empty;
|
||||
public string? Exception { get; set; }
|
||||
public string? Source { get; set; }
|
||||
}
|
||||
22
Flawless.Server/Models/Webhook.cs
Normal file
22
Flawless.Server/Models/Webhook.cs
Normal file
@ -0,0 +1,22 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Flawless.Communication.Shared;
|
||||
|
||||
namespace Flawless.Server.Models;
|
||||
|
||||
public class Webhook
|
||||
{
|
||||
[Key]
|
||||
public int Id { get; set; }
|
||||
|
||||
public required Guid RepositoryId { get; set; }
|
||||
|
||||
public required string TargetUrl { get; set; }
|
||||
|
||||
public required WebhookEventType EventType { get; set; }
|
||||
|
||||
public required string? Secret { get; set; }
|
||||
|
||||
public bool IsActive { get; set; } = true;
|
||||
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
@ -1,7 +1,6 @@
|
||||
using System.Security;
|
||||
using System.Security.Claims;
|
||||
using System.Text;
|
||||
using System.Text.Json.Serialization;
|
||||
using Flawless.Communication.Response;
|
||||
using Flawless.Server.Middlewares;
|
||||
using Flawless.Server.Models;
|
||||
using Flawless.Server.Services;
|
||||
@ -29,7 +28,7 @@ public static class Program
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
SetupWebApplication(app);
|
||||
SetupMiddleware(app);
|
||||
app.Run();
|
||||
}
|
||||
|
||||
@ -49,8 +48,15 @@ public static class Program
|
||||
opt.MultipartHeadersLengthLimit = int.MaxValue;
|
||||
});
|
||||
|
||||
// Api related
|
||||
// Logic services
|
||||
builder.Services.AddHttpClient();
|
||||
builder.Services.AddSingleton<PathTransformer>();
|
||||
builder.Services.AddSingleton<SettingFacade>();
|
||||
builder.Services.AddScoped<AccessControlService>();
|
||||
builder.Services.AddScoped<EmailService>();
|
||||
builder.Services.AddScoped<WebhookService>();
|
||||
|
||||
// Api related
|
||||
builder.Services.AddOpenApi();
|
||||
builder.Services.AddControllers();
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
@ -79,6 +85,7 @@ public static class Program
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
private static void ConfigDbContext(WebApplicationBuilder builder)
|
||||
@ -88,6 +95,11 @@ public static class Program
|
||||
{
|
||||
opt.UseNpgsql(builder.Configuration.GetConnectionString("CoreDb"));
|
||||
});
|
||||
|
||||
builder.Logging.ClearProviders()
|
||||
.AddConsole()
|
||||
.AddDebug()
|
||||
.Services.AddSingleton<ILoggerProvider, DatabaseLoggerProvider>();
|
||||
|
||||
builder.Services.AddIdentityCore<AppUser>(opt =>
|
||||
{
|
||||
@ -141,18 +153,12 @@ public static class Program
|
||||
});
|
||||
}
|
||||
|
||||
private static void SetupWebApplication(WebApplication app)
|
||||
private static void SetupMiddleware(WebApplication app)
|
||||
{
|
||||
app.UseMiddleware<IpFilterMiddleware>();
|
||||
app.UseMiddleware<ExceptionTransformMiddleware>();
|
||||
app.UseRouting();
|
||||
|
||||
// Config WebSocket support.
|
||||
app.UseWebSockets(new WebSocketOptions
|
||||
{
|
||||
KeepAliveInterval = TimeSpan.FromSeconds(60),
|
||||
KeepAliveTimeout = TimeSpan.FromSeconds(300),
|
||||
});
|
||||
|
||||
// Configure identity control
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
@ -178,11 +184,12 @@ public static class Program
|
||||
{
|
||||
if (context.Principal != null)
|
||||
{
|
||||
var p = context.Principal!;
|
||||
var auth = context.HttpContext.GetEndpoint()?.Metadata.GetOrderedMetadata<AuthorizeAttribute>();
|
||||
var p = context.Principal!;
|
||||
|
||||
if (auth?.Any() ?? false)
|
||||
{
|
||||
var adminOnly = auth.Any(a => a.Policy?.ToLower() == "admin");
|
||||
var id = p.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
||||
if (id == null) throw new SecurityTokenExpiredException("User is not defined in the token!");
|
||||
|
||||
@ -194,6 +201,8 @@ public static class Program
|
||||
|
||||
var u = await db.FindByIdAsync(id!);
|
||||
if (u == null) throw new SecurityTokenExpiredException("User is not existed.");
|
||||
if (adminOnly && u.Admin == false)
|
||||
throw new SecurityException("This api is Admin called only!");
|
||||
|
||||
if (u.SecurityStamp != stamp) throw new SecurityTokenExpiredException("SecurityStamp is mismatched.");
|
||||
// if (u.LockoutEnabled) throw new SecurityTokenExpiredException("User has been locked."); //todo Fix lockout prob
|
||||
|
||||
65
Flawless.Server/Services/AccessControlService.cs
Normal file
65
Flawless.Server/Services/AccessControlService.cs
Normal file
@ -0,0 +1,65 @@
|
||||
using System.Net;
|
||||
using Flawless.Server.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Flawless.Server.Services;
|
||||
|
||||
public class AccessControlService(AppDbContext dbContext)
|
||||
{
|
||||
public async Task<IEnumerable<string>> GetIpListAsync(IpPolicyType policyType)
|
||||
{
|
||||
return await dbContext.IpPolicies
|
||||
.Where(x => x.PolicyType == policyType)
|
||||
.Select(x => x.IpAddress)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task UpdatePolicyAsync(IpPolicyType policyType, IEnumerable<string> ips)
|
||||
{
|
||||
var validIps = ips.Where(IsValidIp).Distinct().ToList();
|
||||
|
||||
// 删除旧策略
|
||||
var existing = await dbContext.IpPolicies
|
||||
.Where(x => x.PolicyType == policyType)
|
||||
.ToListAsync();
|
||||
dbContext.IpPolicies.RemoveRange(existing);
|
||||
|
||||
// 添加新策略
|
||||
var newPolicies = validIps.Select(ip => new IpPolicy
|
||||
{
|
||||
IpAddress = ip,
|
||||
PolicyType = policyType,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
});
|
||||
|
||||
await dbContext.IpPolicies.AddRangeAsync(newPolicies);
|
||||
|
||||
await dbContext.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task<bool> IsIpAllowedAsync(string ip)
|
||||
{
|
||||
if (!IsValidIp(ip)) return false;
|
||||
|
||||
var policies = await dbContext.IpPolicies
|
||||
.Where(x => x.IpAddress == ip)
|
||||
.ToListAsync();
|
||||
|
||||
var isWhitelisted = policies.Any(x => x.PolicyType == IpPolicyType.Whitelist);
|
||||
var isBlacklisted = policies.Any(x => x.PolicyType == IpPolicyType.Blacklist);
|
||||
|
||||
// 如果有白名单记录则优先判断
|
||||
var hasAnyWhitelist = await dbContext.IpPolicies
|
||||
.AnyAsync(x => x.PolicyType == IpPolicyType.Whitelist);
|
||||
|
||||
return hasAnyWhitelist ?
|
||||
isWhitelisted && !isBlacklisted :
|
||||
!isBlacklisted;
|
||||
}
|
||||
|
||||
private static bool IsValidIp(string ip)
|
||||
{
|
||||
return IPAddress.TryParse(ip, out _);
|
||||
}
|
||||
|
||||
}
|
||||
@ -14,6 +14,12 @@ public class AppDbContext(DbContextOptions<AppDbContext> options)
|
||||
public DbSet<Repository> Repositories { get; set; }
|
||||
|
||||
public DbSet<RepositoryIssue> RepositoryIssues { get; set; }
|
||||
|
||||
public DbSet<IpPolicy> IpPolicies { get; set; }
|
||||
|
||||
public DbSet<SystemLog> SystemLogs { get; set; }
|
||||
|
||||
public DbSet<Webhook> Webhooks { get; set; }
|
||||
|
||||
public async ValueTask<(bool existed, bool authorized)> CheckRepositoryExistedAuthorizedAsync(
|
||||
AppUser owner, string name, AppUser user, RepositoryRole role)
|
||||
|
||||
47
Flawless.Server/Services/DatabaseLoggerProvider.cs
Normal file
47
Flawless.Server/Services/DatabaseLoggerProvider.cs
Normal file
@ -0,0 +1,47 @@
|
||||
using Flawless.Server.Models;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
|
||||
namespace Flawless.Server.Services;
|
||||
|
||||
public class DatabaseLoggerProvider(IServiceProvider serviceProvider) : ILoggerProvider
|
||||
{
|
||||
public ILogger CreateLogger(string categoryName)
|
||||
{
|
||||
// 过滤非业务错误日志
|
||||
if (!categoryName.StartsWith("Flawless.Server")) return NullLogger.Instance;
|
||||
return new DatabaseLogger(serviceProvider.CreateScope());
|
||||
}
|
||||
|
||||
public void Dispose() { }
|
||||
}
|
||||
|
||||
public class DatabaseLogger(IServiceScope scope) : ILogger
|
||||
{
|
||||
private readonly AppDbContext _dbContext = scope.ServiceProvider.GetService<AppDbContext>()!;
|
||||
|
||||
public IDisposable? BeginScope<TState>(TState state) => scope;
|
||||
|
||||
public bool IsEnabled(LogLevel logLevel) => true;
|
||||
|
||||
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception,
|
||||
Func<TState, Exception?, string> formatter)
|
||||
{
|
||||
try
|
||||
{
|
||||
_dbContext.SystemLogs.Add(new SystemLog
|
||||
{
|
||||
LogLevel = logLevel,
|
||||
Message = formatter(state, exception),
|
||||
Exception = exception?.ToString(),
|
||||
Timestamp = DateTime.UtcNow,
|
||||
Source = eventId.Name
|
||||
});
|
||||
|
||||
_dbContext.SaveChanges();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.Error.WriteLine("Cannot save logfile into database: " + e);
|
||||
}
|
||||
}
|
||||
}
|
||||
37
Flawless.Server/Services/EmailService.cs
Normal file
37
Flawless.Server/Services/EmailService.cs
Normal file
@ -0,0 +1,37 @@
|
||||
using MailKit.Net.Smtp;
|
||||
using MimeKit;
|
||||
namespace Flawless.Server.Services;
|
||||
|
||||
public class EmailService(SettingFacade setting, ILogger<EmailService> logger)
|
||||
{
|
||||
public async Task SendEmailAsync(string toEmail, string subject, string body)
|
||||
{
|
||||
if (!setting.UseSmtp)
|
||||
{
|
||||
logger.LogWarning("SMTP is deactivated, skip sending email.");
|
||||
return;
|
||||
}
|
||||
|
||||
var message = new MimeMessage();
|
||||
message.From.Add(new MailboxAddress(setting.ServerName, setting.SmtpUsername));
|
||||
message.To.Add(new MailboxAddress("用户", toEmail));
|
||||
message.Subject = subject;
|
||||
|
||||
message.Body = new TextPart("html") { Text = body };
|
||||
|
||||
using var client = new SmtpClient();
|
||||
await client.ConnectAsync(
|
||||
setting.SmtpHost,
|
||||
setting.SmtpPort,
|
||||
setting.SmtpUseSsl);
|
||||
|
||||
await client.AuthenticateAsync(
|
||||
setting.SmtpUsername,
|
||||
setting.SmtpPassword);
|
||||
|
||||
await client.SendAsync(message);
|
||||
await client.DisconnectAsync(true);
|
||||
|
||||
logger.LogInformation($"Mail has sent to {toEmail}");
|
||||
}
|
||||
}
|
||||
6
Flawless.Server/Services/RepositoryEventSender.cs
Normal file
6
Flawless.Server/Services/RepositoryEventSender.cs
Normal file
@ -0,0 +1,6 @@
|
||||
namespace Flawless.Server.Services;
|
||||
|
||||
public class RepositoryEventSender(WebhookService webhook, EmailService mail)
|
||||
{
|
||||
|
||||
}
|
||||
27
Flawless.Server/Services/SettingFacade.cs
Normal file
27
Flawless.Server/Services/SettingFacade.cs
Normal file
@ -0,0 +1,27 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace Flawless.Server.Services;
|
||||
|
||||
public class SettingFacade
|
||||
{
|
||||
private readonly IConfiguration _config;
|
||||
|
||||
public SettingFacade(IConfiguration config) => _config = config;
|
||||
|
||||
// 系统基础配置
|
||||
public string? ServerName => _config[SettingKey.ServerName] ?? null;
|
||||
public bool AllowPublicRegistration => _config.GetValue(SettingKey.AllowPublicRegistration, false);
|
||||
|
||||
// Webhook配置
|
||||
public bool UseWebHook => _config.GetValue(SettingKey.UseWebHook, false);
|
||||
public int WebhookTimeout => _config.GetValue(SettingKey.WebhookTimeout, 5000);
|
||||
public int WebhookMaxRetries => _config.GetValue(SettingKey.WebhookMaxRetries, 3);
|
||||
|
||||
// SMTP配置
|
||||
public bool UseSmtp => _config.GetValue(SettingKey.UseSmtp, false);
|
||||
public string SmtpHost => _config[SettingKey.SmtpHost]?.Trim() ?? "localhost";
|
||||
public int SmtpPort => _config.GetValue(SettingKey.SmtpPort, 587);
|
||||
public bool SmtpUseSsl => _config.GetValue(SettingKey.SmtpUseSsl, false);
|
||||
public string SmtpUsername => _config[SettingKey.SmtpUsername]?.Trim() ?? String.Empty;
|
||||
public string SmtpPassword => _config[SettingKey.SmtpPassword]?.Trim()?? String.Empty;
|
||||
}
|
||||
114
Flawless.Server/Services/WebhookService.cs
Normal file
114
Flawless.Server/Services/WebhookService.cs
Normal file
@ -0,0 +1,114 @@
|
||||
using System.Net;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using Flawless.Communication.Shared;
|
||||
using Flawless.Server.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Flawless.Server.Services;
|
||||
|
||||
public class WebhookService(
|
||||
SettingFacade settings,
|
||||
AppDbContext context,
|
||||
IHttpClientFactory httpFactory,
|
||||
ILogger<WebhookService> logger)
|
||||
{
|
||||
public async Task AddWebhookAsync(Guid repoId, string targetUrl, WebhookEventType eventType, string? secret)
|
||||
{
|
||||
// 新增参数校验
|
||||
if (string.IsNullOrWhiteSpace(targetUrl) || !Uri.TryCreate(targetUrl, UriKind.Absolute, out _))
|
||||
throw new ArgumentException("No valid target URL provided");
|
||||
|
||||
var webhook = new Webhook {
|
||||
RepositoryId = repoId,
|
||||
TargetUrl = targetUrl,
|
||||
EventType = eventType,
|
||||
Secret = secret,
|
||||
IsActive = true
|
||||
};
|
||||
|
||||
await context.Webhooks.AddAsync(webhook);
|
||||
await context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task ToggleWebhookAsync(Guid repoId, int webhookId, bool activated)
|
||||
{
|
||||
var hook = await context.Webhooks.FindAsync(webhookId);
|
||||
if (hook == null || hook.RepositoryId != repoId) return;
|
||||
|
||||
if (hook.IsActive == activated) return;
|
||||
|
||||
hook.IsActive = activated;
|
||||
context.Webhooks.Update(hook);
|
||||
await context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task DeleteWebhookAsync(Guid repoId, int webhookId)
|
||||
{
|
||||
var hook = await context.Webhooks.FindAsync(webhookId);
|
||||
if (hook == null || hook.RepositoryId != repoId) return;
|
||||
|
||||
context.Webhooks.Remove(hook);
|
||||
await context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<Webhook>> GetWebhooksAsync(Guid repoId)
|
||||
{
|
||||
return await context.Webhooks
|
||||
.Where(w => w.RepositoryId == repoId)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task TriggerWebhooksAsync(Guid repoId, WebhookEventType eventType, object payload)
|
||||
{
|
||||
if (!settings.UseWebHook) return;
|
||||
|
||||
var hooks = await context.Webhooks
|
||||
.Where(w => w.RepositoryId == repoId && w.EventType == eventType && w.IsActive)
|
||||
.ToListAsync();
|
||||
|
||||
using var client = httpFactory.CreateClient();
|
||||
client.Timeout = TimeSpan.FromSeconds(settings.WebhookTimeout);
|
||||
|
||||
foreach (var hook in hooks)
|
||||
{
|
||||
for (var retry = 0; retry < settings.WebhookMaxRetries; retry++)
|
||||
{
|
||||
try
|
||||
{
|
||||
var content = new StringContent(JsonSerializer.Serialize(payload),
|
||||
Encoding.UTF8, "application/json");
|
||||
|
||||
if (hook.Secret != null)
|
||||
{
|
||||
var signature = HMACSHA256.HashData(
|
||||
Encoding.UTF8.GetBytes(hook.Secret),
|
||||
await content.ReadAsByteArrayAsync());
|
||||
|
||||
content.Headers.Add("X-Signature", $"sha256={Convert.ToHexString(signature)}");
|
||||
}
|
||||
|
||||
var response = await client.PostAsync(hook.TargetUrl, content);
|
||||
if (response.IsSuccessStatusCode) break;
|
||||
|
||||
logger.LogWarning($"Webhook {hook.Id} Failed:{response.StatusCode}");
|
||||
await Task.Delay(1000 * (int)Math.Pow(2, retry)); // 指数退避
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, $"Webhook {hook.Id} Failed for {retry + 1} times.");
|
||||
if (retry == settings.WebhookMaxRetries - 1)
|
||||
await DeactivateFailedWebhook(hook);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DeactivateFailedWebhook(Webhook hook)
|
||||
{
|
||||
hook.IsActive = false;
|
||||
context.Webhooks.Update(hook);
|
||||
await context.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
27
Flawless.Server/SettingKey.cs
Normal file
27
Flawless.Server/SettingKey.cs
Normal file
@ -0,0 +1,27 @@
|
||||
namespace Flawless.Server;
|
||||
|
||||
public static class SettingKey
|
||||
{
|
||||
public const string ServerName = nameof(ServerName);
|
||||
|
||||
public const string AllowPublicRegistration = nameof(AllowPublicRegistration);
|
||||
|
||||
public const string UseWebHook = nameof(UseWebHook);
|
||||
|
||||
public const string WebhookTimeout = nameof(WebhookTimeout);
|
||||
|
||||
public const string WebhookMaxRetries = nameof(WebhookMaxRetries);
|
||||
|
||||
public const string UseSmtp = nameof(UseSmtp);
|
||||
|
||||
public const string SmtpHost = nameof(SmtpHost);
|
||||
|
||||
public const string SmtpPort = nameof(SmtpPort);
|
||||
|
||||
public const string SmtpUseSsl = nameof(SmtpUseSsl);
|
||||
|
||||
public const string SmtpUsername = nameof(SmtpUsername);
|
||||
|
||||
public const string SmtpPassword = nameof(SmtpPassword);
|
||||
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user