├── .dockerignore ├── .editorconfig ├── .gitattributes ├── .github ├── CODEOWNERS ├── pull_request_template.md ├── renovate.json └── workflows │ ├── docker.yml │ ├── main.yml │ └── migrations.yml ├── .gitignore ├── .idea └── .idea.PasswordlessServer │ └── .idea │ └── IntelliLang.xml ├── .vscode ├── launch.json └── tasks.json ├── Api.dockerfile ├── CONTRIBUTING.md ├── Directory.Build.props ├── LICENSE.txt ├── PasswordlessServer.sln ├── README.md ├── SECURITY.md ├── codecov.yml ├── global.json ├── run_self-hosting-image-ssl.sh ├── run_self-hosting-image.sh ├── self-host ├── Dockerfile ├── README.md ├── docker-compose.yml ├── entrypoint.sh ├── examples │ └── traefik │ │ └── simple-example │ │ └── docker-compose.yml ├── hbs │ ├── config.yaml │ └── nginx-config.hbs ├── mounted_dir │ └── config.json.example ├── nginx │ ├── logrotate.sh │ ├── mime.types │ ├── nginx.conf │ ├── proxy.conf │ ├── security-headers-ssl.conf │ └── security-headers.conf ├── settings.env └── supervisord │ ├── admin.ini │ ├── api.ini │ ├── nginx.ini │ └── supervisord.conf ├── src ├── AdminConsole │ ├── .gitignore │ ├── AdminConsole.csproj │ ├── Authorization │ │ ├── CustomClaimTypes.cs │ │ ├── CustomPolicy.cs │ │ ├── HasAppHandler.cs │ │ └── HasAppRoleRequirement.cs │ ├── BackgroundServices │ │ ├── ApplicationCleanupBackgroundService.cs │ │ └── OnboardingCleanupBackgroundService.cs │ ├── Billing │ │ ├── BackgroundServices │ │ │ ├── MeteredBillingBackgroundService.cs │ │ │ ├── StripeConfigurationUpdaterBackgroundService.cs │ │ │ └── UserCountUpdaterBackgroundService.cs │ │ ├── BillingBootstrap.cs │ │ ├── Configuration │ │ │ ├── BillingOptions.cs │ │ │ ├── BillingPlanOptions.cs │ │ │ ├── FeaturesOptions.cs │ │ │ ├── StoreOptions.cs │ │ │ └── UiOptions.cs │ │ ├── Constants │ │ │ └── PlanConstants.cs │ │ └── NoOpBillingService.cs │ ├── Components │ │ ├── Account │ │ │ └── IdentityRevalidatingAuthenticationStateProvider.cs │ │ ├── App.razor │ │ ├── Layouts │ │ │ ├── ApplicationNavMenu.razor │ │ │ ├── MainLayout.razor │ │ │ ├── NavMenu.razor │ │ │ └── NavMenuItem.razor │ │ ├── Pages │ │ │ ├── Account │ │ │ │ ├── AccessDenied.razor │ │ │ │ ├── Logout.razor │ │ │ │ ├── Magic.razor │ │ │ │ ├── Magic.razor.cs │ │ │ │ ├── Profile.razor │ │ │ │ ├── Profile.razor.cs │ │ │ │ ├── UserOnboarding.razor │ │ │ │ └── UserOnboarding.razor.cs │ │ │ ├── App │ │ │ │ ├── BaseApplicationPage.cs │ │ │ │ ├── Credentials │ │ │ │ │ ├── List.razor │ │ │ │ │ ├── List.razor.cs │ │ │ │ │ ├── User.razor │ │ │ │ │ └── User.razor.cs │ │ │ │ ├── Log.razor │ │ │ │ ├── Onboarding │ │ │ │ │ ├── GetStarted.razor │ │ │ │ │ └── GetStarted.razor.cs │ │ │ │ ├── Reporting.razor │ │ │ │ ├── ReportingComponents │ │ │ │ │ ├── TotalCredentialsCountChart.razor │ │ │ │ │ └── TotalUsersCountChart.razor │ │ │ │ └── Settings │ │ │ │ │ ├── AuthenticationConfiguration │ │ │ │ │ ├── AuthenticationConfigurationForm.razor │ │ │ │ │ ├── AuthenticationConfigurationFormModel.cs │ │ │ │ │ ├── CredentialHintStringAttribute.cs │ │ │ │ │ ├── EditAuthenticationConfiguration.razor │ │ │ │ │ └── NewAuthenticationConfiguration.razor │ │ │ │ │ ├── Authenticators.razor │ │ │ │ │ ├── CreatePublicKey.razor │ │ │ │ │ ├── CreateSecretKey.razor │ │ │ │ │ ├── ManageAuthenticators.razor │ │ │ │ │ ├── SecretKeyCreated.razor │ │ │ │ │ ├── Settings.razor │ │ │ │ │ ├── Settings.razor.cs │ │ │ │ │ └── SettingsComponents │ │ │ │ │ ├── ApiKeysSection.razor │ │ │ │ │ ├── ApiKeysSection.razor.cs │ │ │ │ │ ├── AttestationSection.razor │ │ │ │ │ ├── AuthenticationConfiguration.razor │ │ │ │ │ ├── CreateApiKeyComponent.razor │ │ │ │ │ ├── DeleteApplicationSection.razor │ │ │ │ │ ├── DeleteApplicationSection.razor.cs │ │ │ │ │ ├── FeatureListItemIcon.razor │ │ │ │ │ ├── MagicLinksSection.razor │ │ │ │ │ ├── MagicLinksSection.razor.cs │ │ │ │ │ ├── ManuallyGeneratedAuthenticationTokensSection.razor │ │ │ │ │ ├── ManuallyGeneratedAuthenticationTokensSection.razor.cs │ │ │ │ │ └── PlanSection.razor │ │ │ ├── Billing │ │ │ │ ├── Cancelled.razor │ │ │ │ ├── Default.razor │ │ │ │ ├── Invoices.razor │ │ │ │ ├── Manage.razor │ │ │ │ ├── Manage.razor.cs │ │ │ │ └── Success.razor │ │ │ ├── Initialize.razor │ │ │ └── Organization │ │ │ │ ├── Admins.razor │ │ │ │ ├── Admins.razor.cs │ │ │ │ ├── Create.razor │ │ │ │ ├── Create.razor.cs │ │ │ │ ├── Create.razor.css │ │ │ │ ├── Log.razor │ │ │ │ ├── Overview.razor │ │ │ │ ├── Overview.razor.cs │ │ │ │ ├── Settings.razor │ │ │ │ ├── Settings.razor.cs │ │ │ │ ├── SettingsComponents │ │ │ │ ├── DeleteOrganizationComponent.razor │ │ │ │ ├── DeleteOrganizationComponent.razor.cs │ │ │ │ ├── SecurityComponent.razor │ │ │ │ └── SecurityComponent.razor.cs │ │ │ │ ├── Verify.razor │ │ │ │ └── Verify.razor.cs │ │ ├── Routes.razor │ │ ├── Shared │ │ │ ├── AddPasskeysComponent.razor │ │ │ ├── Alerts │ │ │ │ ├── Alert.razor │ │ │ │ ├── Alert.razor.cs │ │ │ │ ├── LinkAlert.razor │ │ │ │ └── LinkAlert.razor.css │ │ │ ├── ApexCharts │ │ │ │ ├── ApexChart.razor │ │ │ │ ├── Models │ │ │ │ │ ├── ApexChartOptions.cs │ │ │ │ │ ├── Chart.cs │ │ │ │ │ ├── ChartTypes.cs │ │ │ │ │ ├── Color.cs │ │ │ │ │ ├── Export.cs │ │ │ │ │ ├── LabelsFormattingTypes.cs │ │ │ │ │ ├── Legend.cs │ │ │ │ │ ├── NoData.cs │ │ │ │ │ ├── Series.cs │ │ │ │ │ ├── Title.cs │ │ │ │ │ ├── Toolbar.cs │ │ │ │ │ ├── Xaxis.cs │ │ │ │ │ ├── XaxisTypes.cs │ │ │ │ │ ├── Yaxis.cs │ │ │ │ │ └── YaxisLabels.cs │ │ │ │ ├── Serialization │ │ │ │ │ ├── ApexChartJsonSerializer.cs │ │ │ │ │ └── ColorConverter.cs │ │ │ │ └── Validators │ │ │ │ │ └── ColorValidator.cs │ │ │ ├── Badge.razor │ │ │ ├── BaseRazorComponent.cs │ │ │ ├── BreadCrumb.razor │ │ │ ├── Cards │ │ │ │ ├── Card.razor │ │ │ │ ├── Card.razor.css │ │ │ │ ├── CardDetailsProperty.razor │ │ │ │ ├── CardDetailsProperty.razor.css │ │ │ │ ├── CardSummaryProperty.razor │ │ │ │ ├── CardSummaryProperty.razor.css │ │ │ │ └── LinkCard.razor │ │ │ ├── ContextualStyles.cs │ │ │ ├── Credentials.razor │ │ │ ├── Credentials.razor.cs │ │ │ ├── Credentials.razor.css │ │ │ ├── Forms │ │ │ │ ├── ConfirmButton.razor │ │ │ │ ├── ConfirmButton.razor.cs │ │ │ │ ├── ConfirmEditForm.razor │ │ │ │ ├── ConfirmEditForm.razor.cs │ │ │ │ └── CopyInputField.razor │ │ │ ├── Icons │ │ │ │ ├── Alerts │ │ │ │ │ ├── BaseAlertIcon.cs │ │ │ │ │ ├── DangerAlertIcon.razor │ │ │ │ │ ├── InfoAlertIcon.razor │ │ │ │ │ ├── SuccessAlertIcon.razor │ │ │ │ │ └── WarningAlertIcon.razor │ │ │ │ ├── BeakerIcon.razor │ │ │ │ ├── CodeBracketSquareIcon.razor │ │ │ │ ├── CogIcon.razor │ │ │ │ ├── DeleteIcon.razor │ │ │ │ ├── DeveloperIcons │ │ │ │ │ ├── AndroidIcon.razor │ │ │ │ │ ├── DotNetIcon.razor │ │ │ │ │ ├── JavaIcon.razor │ │ │ │ │ ├── JavascriptIcon.razor │ │ │ │ │ ├── NodeJsIcon.razor │ │ │ │ │ ├── OpenApiIcon.razor │ │ │ │ │ ├── PythonIcon.razor │ │ │ │ │ └── ReactIcon.razor │ │ │ │ ├── DocumentTextIcon.razor │ │ │ │ ├── DollarIcon.razor │ │ │ │ ├── DownloadIcon.razor │ │ │ │ ├── Fido2AuthenticatorIcon.razor │ │ │ │ ├── HamburgerIcon.razor │ │ │ │ ├── LockIcon.razor │ │ │ │ ├── LogoFull.razor │ │ │ │ ├── LogoMobile.razor │ │ │ │ ├── MailIcon.razor │ │ │ │ ├── PaymentMethods │ │ │ │ │ ├── AmexIcon.razor │ │ │ │ │ ├── DinersIcon.razor │ │ │ │ │ ├── DiscoverIcon.razor │ │ │ │ │ ├── JcbIcon.razor │ │ │ │ │ ├── MasterCardIcon.razor │ │ │ │ │ ├── UnionPayIcon.razor │ │ │ │ │ ├── UnknownCardIcon.razor │ │ │ │ │ └── VisaIcon.razor │ │ │ │ ├── PencilSquareIcon.razor │ │ │ │ ├── SignOutIcon.razor │ │ │ │ ├── StackedBarChartIcon.razor │ │ │ │ ├── StartIcon.razor │ │ │ │ ├── UnlockIcon.razor │ │ │ │ └── UsersIcon.razor │ │ │ ├── Links │ │ │ │ └── LinkHome.razor │ │ │ ├── LocalDateTime.razor │ │ │ ├── LocalDateTime.razor.cs │ │ │ ├── Modals │ │ │ │ └── SimpleAlert.razor │ │ │ ├── Page.razor │ │ │ ├── Panel.razor │ │ │ ├── PanelFooter.razor │ │ │ ├── RedirectToLogin.razor │ │ │ ├── SecureImportMapScript.razor │ │ │ ├── SecureImportMapScript.razor.cs │ │ │ ├── SecureScript.razor │ │ │ ├── SecureStylesheet.razor │ │ │ ├── Stats │ │ │ │ ├── BaseCardStats.cs │ │ │ │ ├── SimpleCardsStats.razor │ │ │ │ └── TrendingCardsStats.razor │ │ │ ├── Tables │ │ │ │ ├── PagedTable.cs │ │ │ │ └── Table.razor │ │ │ └── Validation │ │ │ │ └── CustomValidationErrors.razor │ │ └── _Imports.razor │ ├── Db │ │ ├── ConsoleDbContext.cs │ │ ├── DatabaseBootstrap.cs │ │ ├── MsSqlDesignTimeDbContextFactory.cs │ │ ├── MssqlConsoleDbContext.cs │ │ ├── SqliteConsoleDbContext.cs │ │ └── SqliteDesignTimeDbContextFactory.cs │ ├── Endpoints │ │ ├── ApplicationEndpoints.cs │ │ ├── BillingEndpoints.cs │ │ └── ComplimentaryEndpoints.cs │ ├── EventLog │ │ ├── AddEventLoggingRegistration.cs │ │ ├── DTOs │ │ │ ├── AuditLogResponses.cs │ │ │ └── OrganizationEventDto.cs │ │ ├── EventDeletionBackgroundWorker.cs │ │ ├── Loggers │ │ │ ├── EventLoggerEfReadStorage.cs │ │ │ ├── EventLoggerEfUnauthenticatedWriteStorage.cs │ │ │ ├── EventLoggerEfWriteStorage.cs │ │ │ ├── IEventLogger.cs │ │ │ ├── IEventLoggerStorage.cs │ │ │ ├── InternalEventStorage.cs │ │ │ └── NoOpEventLogger.cs │ │ └── Models │ │ │ └── OrganizationEvent.cs │ ├── FeatureManagement │ │ ├── FeatureFlags.cs │ │ └── OrganizationFeatureFilter.cs │ ├── HealthChecks │ │ └── HealthCheckBootstrap.cs │ ├── Helpers │ │ ├── ClaimsExtension.cs │ │ ├── IFileVersionProviderExtensions.cs │ │ ├── IHttpContextAccessorExtensions.cs │ │ ├── IUrlHelperExtensions.cs │ │ ├── LoginEndpointFilter.cs │ │ └── RazorPageHtmlExtensions.cs │ ├── Identity │ │ ├── ConsoleUser.cs │ │ └── Invite.cs │ ├── Middleware │ │ ├── CurrentContext.cs │ │ ├── CurrentContextMiddleware.cs │ │ ├── EventLogStorageCommitMiddleware.cs │ │ ├── ICurrentContext.cs │ │ ├── RouteParameters.cs │ │ └── SecurityHeadersMiddleware.cs │ ├── Migrations │ │ ├── Mssql │ │ │ ├── 20230510074816_Init.Designer.cs │ │ │ ├── 20230510074816_Init.cs │ │ │ ├── 20230809074539_UpdateTableApplications_AddColumnDeleteAt.Designer.cs │ │ │ ├── 20230809074539_UpdateTableApplications_AddColumnDeleteAt.cs │ │ │ ├── 20230818131329_AlterTableOrganizations_AddColumnBecamePaidAt.Designer.cs │ │ │ ├── 20230818131329_AlterTableOrganizations_AddColumnBecamePaidAt.cs │ │ │ ├── 20230922162834_AddOrgEventTable.Designer.cs │ │ │ ├── 20230922162834_AddOrgEventTable.cs │ │ │ ├── 20231009133628_RenameAuditLogToEventLog.Designer.cs │ │ │ ├── 20231009133628_RenameAuditLogToEventLog.cs │ │ │ ├── 20240716084408_AllowDisablingMagicLinksForOrganizations.Designer.cs │ │ │ ├── 20240716084408_AllowDisablingMagicLinksForOrganizations.cs │ │ │ └── MssqlConsoleDbContextModelSnapshot.cs │ │ └── Sqlite │ │ │ ├── 20230510074813_Init.Designer.cs │ │ │ ├── 20230510074813_Init.cs │ │ │ ├── 20230809074533_UpdateTableApplications_AddColumnDeleteAt.Designer.cs │ │ │ ├── 20230809074533_UpdateTableApplications_AddColumnDeleteAt.cs │ │ │ ├── 20230818130514_AlterTableOrganizations_AddColumnBecamePaidAt.Designer.cs │ │ │ ├── 20230818130514_AlterTableOrganizations_AddColumnBecamePaidAt.cs │ │ │ ├── 20230922162714_AddOrgEventTable.Designer.cs │ │ │ ├── 20230922162714_AddOrgEventTable.cs │ │ │ ├── 20231006101217_RenameAuditLogToEventLog.Designer.cs │ │ │ ├── 20231006101217_RenameAuditLogToEventLog.cs │ │ │ ├── 20240716084433_AllowDisablingMagicLinksForOrganizations.Designer.cs │ │ │ ├── 20240716084433_AllowDisablingMagicLinksForOrganizations.cs │ │ │ └── SqliteConsoleDbContextModelSnapshot.cs │ ├── Models │ │ ├── Application.cs │ │ ├── ApplicationFeatureContext.cs │ │ ├── Authenticator.cs │ │ ├── FeaturesContext.cs │ │ ├── Onboarding.cs │ │ ├── Organization.cs │ │ ├── OrganizationFeaturesContext.cs │ │ ├── OrganizationType.cs │ │ └── UseCaseType.cs │ ├── Pages │ │ ├── Account │ │ │ ├── Login.cshtml │ │ │ └── Login.cshtml.cs │ │ ├── App │ │ │ └── Playground │ │ │ │ ├── Client.cshtml │ │ │ │ ├── Client.cshtml.cs │ │ │ │ ├── NewAccount.cshtml │ │ │ │ └── NewAccount.cshtml.cs │ │ ├── BaseExtendedPageModel.cs │ │ ├── Error.cshtml │ │ ├── Error.cshtml.cs │ │ ├── Index.cshtml │ │ ├── Index.cshtml.cs │ │ ├── Organization │ │ │ ├── CreateApplication.cshtml │ │ │ ├── CreateApplication.cshtml.cs │ │ │ ├── Join.cshtml │ │ │ ├── Join.cshtml.cs │ │ │ ├── JoinBusy.cshtml │ │ │ └── JoinBusy.cshtml.cs │ │ ├── Shared │ │ │ ├── _Error.cshtml │ │ │ ├── _Layout.cshtml │ │ │ ├── _Layout.cshtml.css │ │ │ └── _Ok.cshtml │ │ ├── _ViewImports.cshtml │ │ └── _ViewStart.cshtml │ ├── Program.cs │ ├── Properties │ │ └── launchSettings.json │ ├── RateLimiting │ │ ├── AddRateLimitingExtensions.cs │ │ └── AdminPageRateLimit.cs │ ├── RoutingHelpers │ │ ├── ApplicationPageRoutingContext.cs │ │ ├── LinkGenerator.cs │ │ └── RazorOptionsExtensions.cs │ ├── Services │ │ ├── AdminService.cs │ │ ├── ApplicationService.cs │ │ ├── AuditLogService.cs │ │ ├── AuthenticatorData │ │ │ ├── AuthenticatorDataDto.cs │ │ │ ├── AuthenticatorDataItemDto.cs │ │ │ ├── AuthenticatorDataJsonSerializerContext.cs │ │ │ ├── AuthenticatorDataProvider.cs │ │ │ ├── IAuthenticatorDataProvider.cs │ │ │ └── authenticator-data.json │ │ ├── BaseBillingService.cs │ │ ├── CustomUserClaimsPrincipalFactory.cs │ │ ├── DataService.cs │ │ ├── IAdminService.cs │ │ ├── IApplicationService.cs │ │ ├── IDataService.cs │ │ ├── IInvitationService.cs │ │ ├── IOrganizationFeatureService.cs │ │ ├── IPostSignInHandlerService.cs │ │ ├── IScopedPasswordlessClient.cs │ │ ├── ISetupService.cs │ │ ├── ISharedBillingService.cs │ │ ├── IUsageService.cs │ │ ├── InvitationService.cs │ │ ├── InvoiceModel.cs │ │ ├── MagicLinks │ │ │ ├── IMagicLinkBuilder.cs │ │ │ ├── MagicLinkBuilder.cs │ │ │ └── MagicLinkSignInManager.cs │ │ ├── Mail │ │ │ ├── DefaultMailService.cs │ │ │ └── IMailService.cs │ │ ├── OrganizationFeatureService.cs │ │ ├── PasswordlessManagement │ │ │ ├── IPasswordlessManagementClient.cs │ │ │ ├── PasswordlessManagementBootstrap.cs │ │ │ ├── PasswordlessManagementClient.cs │ │ │ └── PasswordlessManagementOptions.cs │ │ ├── PostSignInHandlerService.cs │ │ ├── ProblemDetailsDelegatingHandler.cs │ │ ├── ScopedPasswordlessClient.cs │ │ ├── SetupService.cs │ │ ├── SharedStripeBillingService.cs │ │ └── UsageService.cs │ ├── Styles │ │ └── tailwind.css │ ├── TagHelpers │ │ ├── AlertBoxes │ │ │ ├── BaseAlertBox.cs │ │ │ ├── DangerAlertBox.cs │ │ │ ├── InfoAlertBox.cs │ │ │ ├── SuccessAlertBox.cs │ │ │ └── WarningAlertBox.cs │ │ ├── Card.cs │ │ ├── ColorVariant.cs │ │ ├── Extensions │ │ │ ├── TagHelperAttributesListExtensions.cs │ │ │ ├── TagHelperExtensions.cs │ │ │ └── TagOutputHelperExtensions.cs │ │ ├── Feedback.cs │ │ ├── Html5Validation.cs │ │ ├── Icons │ │ │ ├── BaseAlertIcon.cs │ │ │ ├── DangerAlertIcon.cs │ │ │ ├── FeatureListItemIcon.cs │ │ │ ├── InfoAlertIcon.cs │ │ │ ├── SuccessAlertIcon.cs │ │ │ └── WarningAlertIcon.cs │ │ ├── IfTagHelper.cs │ │ ├── Panel.cs │ │ └── ScriptTag.cs │ ├── appsettings.Development.json │ ├── appsettings.json │ ├── datadog.json │ ├── migrate.sh │ ├── package-lock.json │ ├── package.json │ ├── readme.md │ ├── tailwind.config.js │ └── wwwroot │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-512x512.png │ │ ├── apple-touch-icon.png │ │ ├── css │ │ └── site.css │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── favicon.ico │ │ ├── humans.txt │ │ ├── icon.svg │ │ ├── img │ │ ├── passwordless-dark.svg │ │ ├── passwordless-light.svg │ │ ├── tag1.svg │ │ ├── tag2.svg │ │ ├── tag3.svg │ │ └── tag4.svg │ │ ├── js │ │ ├── composables │ │ │ └── useFocusTrap.mjs │ │ ├── core.js │ │ ├── modal.mjs │ │ └── site.js │ │ ├── security.txt │ │ └── site.webmanifest ├── Api │ ├── .gitignore │ ├── Api.csproj │ ├── Authorization │ │ ├── AuthenticationBuilderExtensions.cs │ │ ├── Constants.cs │ │ ├── DefaultProblemDetailWriter.cs │ │ ├── EndpointConventionBuilderExtensions.cs │ │ ├── HeaderHandler.cs │ │ ├── IProblemDetailWriter.cs │ │ └── ManagementOptions.cs │ ├── Email │ │ └── DispatchedEmailCleanupService.cs │ ├── Endpoints │ │ ├── Alias.cs │ │ ├── Apps.cs │ │ ├── AuthenticationConfiguration.cs │ │ ├── Authenticators.cs │ │ ├── Credentials.cs │ │ ├── Events.cs │ │ ├── Health.cs │ │ ├── Magic.cs │ │ ├── MetaDataService.cs │ │ ├── Register.cs │ │ ├── Reporting.cs │ │ ├── Signin.cs │ │ └── Users.cs │ ├── Extensions │ │ ├── HeaderDictionaryExtensions.cs │ │ ├── HttpRequestExtensions.cs │ │ └── MagicLinkBootstrap.cs │ ├── HealthChecks │ │ └── HealthCheckBootstrap.cs │ ├── Helpers │ │ ├── AddDatabaseExtensionMethod.cs │ │ ├── AutoNumberToStringConverter.cs │ │ ├── DesignTimeFactories.cs │ │ ├── FriendlyExceptionsMiddleware.cs │ │ ├── IRequestContext.cs │ │ ├── LoggerExtensions.cs │ │ ├── LoggingMiddleware.cs │ │ ├── RequestContext.cs │ │ └── SecurityHeaders.cs │ ├── Middleware │ │ ├── EventLogContextMiddleware.cs │ │ ├── EventLogStorageCommitMiddleware.cs │ │ └── PathValidation.cs │ ├── Models │ │ ├── CredentialsDeleteDTO.cs │ │ ├── CredentialsListDTO.cs │ │ ├── ListResponse.cs │ │ └── ResponseBase.cs │ ├── OpenApi │ │ ├── Extensions │ │ │ └── OpenApiParameterListExtensions.cs │ │ ├── ExternalDocsAttribute.cs │ │ ├── Filters │ │ │ ├── AuthorizationOperationFilter.cs │ │ │ ├── ExtendedStatusDescriptionsOperationFilter.cs │ │ │ └── ExternalDocsOperationFilter.cs │ │ ├── OpenApiBootstrap.cs │ │ ├── OpenApiTags.cs │ │ └── swagger.html │ ├── Program.cs │ ├── Properties │ │ └── launchSettings.json │ ├── RateLimiting │ │ └── RateLimitingExtensions.cs │ ├── Reporting │ │ └── Background │ │ │ ├── ActiveUserReportingBackgroundService.cs │ │ │ ├── ReportingBootstrap.cs │ │ │ └── UsersCountReportingBackgroundService.cs │ ├── appsettings.Development.json │ ├── appsettings.json │ ├── datadog.json │ ├── mds.jwt │ ├── readme.md │ └── wwwroot │ │ ├── DMSans-VariableFont_opsz,wght.ttf │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── favicon.ico │ │ ├── humans.txt │ │ ├── openapi.css │ │ ├── passwordless-light.svg │ │ └── security.txt ├── Common │ ├── Background │ │ ├── BaseDelayedPeriodicBackgroundService.cs │ │ ├── BasePeriodicBackgroundService.cs │ │ ├── ExecutionPlan.cs │ │ └── ExecutionPlanUtility.cs │ ├── Common.csproj │ ├── Configuration │ │ ├── ConfigurationExtensions.cs │ │ ├── IConfigurationBuilderExtensions.cs │ │ └── WebApplicationBuilderExtensions.cs │ ├── Constants │ │ ├── PublicKeyScopes.cs │ │ ├── RegularExpressions.cs │ │ └── SecretKeyScopes.cs │ ├── Converters │ │ └── SignInPurposeConverter.cs │ ├── Db │ │ └── Converters │ │ │ └── EnumToStringConverter.cs │ ├── EventLog │ │ ├── Enums │ │ │ ├── EventType.cs │ │ │ └── Severity.cs │ │ └── Models │ │ │ └── Event.cs │ ├── Extensions │ │ ├── AssemblyExtensions.cs │ │ ├── ByteArrayExtensions.cs │ │ ├── DbContextExtensions.cs │ │ ├── EnumExtensions.cs │ │ ├── HttpRequestExtensions.cs │ │ ├── StringExtensions.cs │ │ └── UrlExtensions.cs │ ├── HealthChecks │ │ ├── HealthCheckEndpoints.cs │ │ ├── HealthCheckResponseWriter.cs │ │ ├── MailKitHealthCheck.cs │ │ ├── SimpleHealthCheck.cs │ │ └── VersionHealthCheck.cs │ ├── Helpers │ │ └── GuidHelper.cs │ ├── Logging │ │ └── SerilogBootstrap.cs │ ├── MagicLinks │ │ ├── Models │ │ │ └── SendMagicLinkRequest.cs │ │ └── Validation │ │ │ └── MagicLinkUrlTemplateAttribute.cs │ ├── Middleware │ │ └── SelfHosting │ │ │ └── HttpOverridesMiddleware.cs │ ├── Models │ │ ├── ApplicationPublicKey.cs │ │ ├── ApplicationSecretKey.cs │ │ ├── Apps │ │ │ ├── ApiKeyResponse.cs │ │ │ ├── ApiKeyTypes.cs │ │ │ ├── AppFeatureResponse.cs │ │ │ ├── AuthenticationConfiguration.cs │ │ │ ├── AuthenticationConfigurationDto.cs │ │ │ ├── CancelApplicationDeletionResponse.cs │ │ │ ├── CreateApiKeyResponse.cs │ │ │ ├── CreateAppDto.cs │ │ │ ├── CreateAppResultDto.cs │ │ │ ├── CreateApplicationRequest.cs │ │ │ ├── CreatePublicKeyRequest.cs │ │ │ ├── CreateSecretKeyRequest.cs │ │ │ ├── DeleteAuthenticationConfigurationRequest.cs │ │ │ ├── GetAppIdAvailabilityRequest.cs │ │ │ ├── GetAppIdAvailabilityResponse.cs │ │ │ ├── GetAuthenticationConfigurationsFilter.cs │ │ │ ├── GetAuthenticationConfigurationsResult.cs │ │ │ ├── ManageFeaturesRequest.cs │ │ │ ├── MarkDeleteApplicationRequest.cs │ │ │ ├── MarkDeleteApplicationResponse.cs │ │ │ ├── SetAuthenticationConfigurationRequest.cs │ │ │ ├── SetFeaturesRequest.cs │ │ │ └── SignInPurpose.cs │ │ ├── Authenticators │ │ │ ├── AddAuthenticatorsRequest.cs │ │ │ ├── ConfiguredAuthenticatorRequest.cs │ │ │ ├── ConfiguredAuthenticatorResponse.cs │ │ │ └── RemoveAuthenticatorsRequest.cs │ │ ├── Credentials │ │ │ └── GetCredentialsRequest.cs │ │ ├── MDS │ │ │ ├── EntriesRequest.cs │ │ │ └── EntryResponse.cs │ │ └── Reporting │ │ │ ├── PeriodicActiveUserReportRequest.cs │ │ │ ├── PeriodicActiveUserReportResponse.cs │ │ │ ├── PeriodicCredentialReportRequest.cs │ │ │ └── PeriodicCredentialReportResponse.cs │ ├── Overrides │ │ ├── ApplicationOverrides.cs │ │ └── ApplicationOverridesOptions.cs │ ├── Serialization │ │ └── HtmlSanitizer.cs │ ├── Services │ │ └── Mail │ │ │ ├── AggregateMailProvider.cs │ │ │ ├── Aws │ │ │ ├── AwsChannelOptions.cs │ │ │ ├── AwsEmailChannelStrategy.cs │ │ │ ├── AwsMailProvider.cs │ │ │ └── AwsMailProviderOptions.cs │ │ │ ├── BaseMailProviderOptions.cs │ │ │ ├── File │ │ │ ├── FileEmailChannelStrategy.cs │ │ │ ├── FileMailProvider.cs │ │ │ └── FileMailProviderOptions.cs │ │ │ ├── IMailProvider.cs │ │ │ ├── IMailProviderFactory.cs │ │ │ ├── MailBootstrap.cs │ │ │ ├── MailConfiguration.cs │ │ │ ├── MailMessage.cs │ │ │ ├── MailProviderFactory.cs │ │ │ ├── SendGrid │ │ │ ├── SendGridEmailChannelStrategy.cs │ │ │ ├── SendGridMailProvider.cs │ │ │ └── SendGridMailProviderOptions.cs │ │ │ ├── Smtp │ │ │ ├── SmtpEmailChannelStrategy.cs │ │ │ ├── SmtpMailProvider.cs │ │ │ └── SmtpMailProviderOptions.cs │ │ │ └── Strategies │ │ │ ├── Channel.cs │ │ │ └── ChannelOptions.cs │ ├── Utils │ │ └── ApiKeyUtils.cs │ └── Validation │ │ ├── Base64UrlAttribute.cs │ │ ├── EmailAttribute.cs │ │ ├── EmailValidationError.cs │ │ ├── EmailValidationErrorCode.cs │ │ ├── EmailValidator.cs │ │ ├── MaxLengthCollectionAttribute.cs │ │ ├── NoForbiddenContentAttribute.cs │ │ ├── RegularExpressionCollectionAttribute.cs │ │ └── RequiredCollectionAttribute.cs └── Service │ ├── ApplicationService.cs │ ├── AuthenticationConfigurationService.cs │ ├── EventLog │ ├── AddEventLoggingRegistration.cs │ ├── EventDeletionBackgroundWorker.cs │ ├── Loggers │ │ ├── EventCache.cs │ │ ├── EventLoggerEfReadStorage.cs │ │ ├── EventLoggerEfWriteStorage.cs │ │ ├── IEventLogStorage.cs │ │ ├── IEventLogger.cs │ │ ├── NoOpEventLogger.cs │ │ └── UnauthenticatedEventLoggerEfWriteStorage.cs │ ├── Mappings │ │ └── EventExtensions.cs │ └── Models │ │ ├── ApplicationEvent.cs │ │ ├── EventDto.cs │ │ ├── EventLogContext.cs │ │ └── EventResponse.cs │ ├── Extensions │ ├── ConversionFunctions.cs │ └── Models │ │ ├── AppFeatureExtensions.cs │ │ └── AuthenticationConfigurationExtensions.cs │ ├── Features │ ├── FeatureContextProvider.cs │ ├── FeaturesContext.cs │ ├── IFeatureContextProvider.cs │ ├── IFeaturesContext.cs │ └── NullFeaturesContext.cs │ ├── Fido2Service.cs │ ├── Helpers │ ├── ApiException.cs │ └── UnknownCredentialException.cs │ ├── IApplicationService.cs │ ├── IFido2Service.cs │ ├── IReportingService.cs │ ├── ITenantProvider.cs │ ├── ITokenService.cs │ ├── MDS │ ├── CacheHandler.cs │ ├── IMetaDataService.cs │ ├── MetaDataService.cs │ └── MetaDataServiceBootstrap.cs │ ├── MagicLinks │ ├── Extensions │ │ └── SendMagicLinkRequestExtension.cs │ ├── MagicLinkService.cs │ └── MagicLinksOptions.cs │ ├── Migrations │ ├── Mssql │ │ ├── 20230510071243_Init.Designer.cs │ │ ├── 20230510071243_Init.cs │ │ ├── 20230816081530_AddTableAppFeatures.Designer.cs │ │ ├── 20230816081530_AddTableAppFeatures.cs │ │ ├── 20230911155025_AddAppEvents.Designer.cs │ │ ├── 20230911155025_AddAppEvents.cs │ │ ├── 20231009134026_RenameAuditLogToEventLog.Designer.cs │ │ ├── 20231009134026_RenameAuditLogToEventLog.cs │ │ ├── 20231108124641_AddCredProps.Designer.cs │ │ ├── 20231108124641_AddCredProps.cs │ │ ├── 20231114144435_AppEventFkAccountInfo.Designer.cs │ │ ├── 20231114144435_AppEventFkAccountInfo.cs │ │ ├── 20231204125125_Pas64MaxUsers.Designer.cs │ │ ├── 20231204125125_Pas64MaxUsers.cs │ │ ├── 20231215205517_AddSignInTokenEndpointSetting.Designer.cs │ │ ├── 20231215205517_AddSignInTokenEndpointSetting.cs │ │ ├── 20240108114346_AlterTableAppFeaturesAddColumnAllowAttestation.Designer.cs │ │ ├── 20240108114346_AlterTableAppFeaturesAddColumnAllowAttestation.cs │ │ ├── 20240112151209_AddMagicLinksSetting.Designer.cs │ │ ├── 20240112151209_AddMagicLinksSetting.cs │ │ ├── 20240112162755_AddTablePeriodicCredentialReports.Designer.cs │ │ ├── 20240112162755_AddTablePeriodicCredentialReports.cs │ │ ├── 20240115082612_AlterTableApiKeyDescAddColumnCreatedAt.Designer.cs │ │ ├── 20240115082612_AlterTableApiKeyDescAddColumnCreatedAt.cs │ │ ├── 20240118190159_NormalizeNullability.Designer.cs │ │ ├── 20240118190159_NormalizeNullability.cs │ │ ├── 20240123093041_AddTableAuthenticators.Designer.cs │ │ ├── 20240123093041_AddTableAuthenticators.cs │ │ ├── 20240205152209_AddTablePeriodicActiveUserReports.Designer.cs │ │ ├── 20240205152209_AddTablePeriodicActiveUserReports.cs │ │ ├── 20240213130303_MagicLinkEmailQuota.Designer.cs │ │ ├── 20240213130303_MagicLinkEmailQuota.cs │ │ ├── 20240225074633_RemoveObsoleteColumns.Designer.cs │ │ ├── 20240225074633_RemoveObsoleteColumns.cs │ │ ├── 20240228181428_DateTimeOffsetToDateTime.Designer.cs │ │ ├── 20240228181428_DateTimeOffsetToDateTime.cs │ │ ├── 20240416151550_AddAuthenticationConfiguration.Designer.cs │ │ ├── 20240416151550_AddAuthenticationConfiguration.cs │ │ ├── 20240709134147_AddIndicesForHotCodePaths.Designer.cs │ │ ├── 20240709134147_AddIndicesForHotCodePaths.cs │ │ ├── 20240722200735_CredentialHints.Designer.cs │ │ ├── 20240722200735_CredentialHints.cs │ │ └── MsSqlContextModelSnapshot.cs │ └── Sqlite │ │ ├── 20230510071208_Init.Designer.cs │ │ ├── 20230510071208_Init.cs │ │ ├── 20230816081613_AddTableAppFeatures.Designer.cs │ │ ├── 20230816081613_AddTableAppFeatures.cs │ │ ├── 20230908132105_AddTableAddEvents.Designer.cs │ │ ├── 20230908132105_AddTableAddEvents.cs │ │ ├── 20231009134056_RenameAuditLogToEventLog.Designer.cs │ │ ├── 20231009134056_RenameAuditLogToEventLog.cs │ │ ├── 20231108124617_AddCredProps.Designer.cs │ │ ├── 20231108124617_AddCredProps.cs │ │ ├── 20231113215305_AppEventFkAccountInfo.Designer.cs │ │ ├── 20231113215305_AppEventFkAccountInfo.cs │ │ ├── 20231204125048_Pas64MaxUsers.Designer.cs │ │ ├── 20231204125048_Pas64MaxUsers.cs │ │ ├── 20231215205459_AddSignInTokenEndpointSetting.Designer.cs │ │ ├── 20231215205459_AddSignInTokenEndpointSetting.cs │ │ ├── 20240108114420_AlterTableAppFeaturesAddColumnAllowAttestation.Designer.cs │ │ ├── 20240108114420_AlterTableAppFeaturesAddColumnAllowAttestation.cs │ │ ├── 20240112151023_AddMagicLinksSetting.Designer.cs │ │ ├── 20240112151023_AddMagicLinksSetting.cs │ │ ├── 20240112162858_AddTablePeriodicCredentialReports.Designer.cs │ │ ├── 20240112162858_AddTablePeriodicCredentialReports.cs │ │ ├── 20240115082633_AlterTableApiKeyDescAddColumnCreatedAt.Designer.cs │ │ ├── 20240115082633_AlterTableApiKeyDescAddColumnCreatedAt.cs │ │ ├── 20240118190130_NormalizeNullability.Designer.cs │ │ ├── 20240118190130_NormalizeNullability.cs │ │ ├── 20240123093132_AddTableAuthenticators.Designer.cs │ │ ├── 20240123093132_AddTableAuthenticators.cs │ │ ├── 20240205152232_AddTablePeriodicActiveUserReports.Designer.cs │ │ ├── 20240205152232_AddTablePeriodicActiveUserReports.cs │ │ ├── 20240213130331_MagicLinkEmailQuota.Designer.cs │ │ ├── 20240213130331_MagicLinkEmailQuota.cs │ │ ├── 20240225074704_RemoveObsoleteColumns.Designer.cs │ │ ├── 20240225074704_RemoveObsoleteColumns.cs │ │ ├── 20240228181418_DateTimeOffsetToDateTime.Designer.cs │ │ ├── 20240228181418_DateTimeOffsetToDateTime.cs │ │ ├── 20240416151601_AddAuthenticationConfiguration.Designer.cs │ │ ├── 20240416151601_AddAuthenticationConfiguration.cs │ │ ├── 20240709134121_AddIndicesForHotCodePaths.Designer.cs │ │ ├── 20240709134121_AddIndicesForHotCodePaths.cs │ │ ├── 20240722200755_CredentialHints.Designer.cs │ │ ├── 20240722200755_CredentialHints.cs │ │ └── SqliteContextModelSnapshot.cs │ ├── Models │ ├── AccountMetaInformation.cs │ ├── AliasPayload.cs │ ├── AliasPointer.cs │ ├── ApiKeyDesc.cs │ ├── AppFeature.cs │ ├── AppIdDTO.cs │ ├── AuthenticationConfiguration.cs │ ├── AuthenticationSessionConfiguration.cs │ ├── Authenticator.cs │ ├── DispatchedEmail.cs │ ├── EFStoredCredential.cs │ ├── EmailAboutAccountDeletion.cs │ ├── FidoRegistrationBeginDTO.cs │ ├── FidoSettings.cs │ ├── KeyDescriptorEntity.cs │ ├── MagicLinkTokenRequest.cs │ ├── PerTenant.cs │ ├── PeriodicActiveUserReport.cs │ ├── PeriodicCredentialReport.cs │ ├── RegisterOptions.cs │ ├── RegisterSession.cs │ ├── RegisterToken.cs │ ├── RegistrationCompleteDTO.cs │ ├── RequestBase.cs │ ├── SignInBeginDTO.cs │ ├── SignInCompleteDTO.cs │ ├── SignInVerifyDTO.cs │ ├── SigninTokenRequest.cs │ ├── StoredCredential.cs │ ├── TokenKey.cs │ ├── TokenResponse.cs │ ├── UserSummary.cs │ ├── ValidatePublicKeyDto.cs │ └── ValidateSecretKeyDto.cs │ ├── ReportingService.cs │ ├── Service.csproj │ ├── SharedManagementService.cs │ ├── Storage │ └── Ef │ │ ├── DbGlobalContext.cs │ │ ├── DbGlobalMsSqlContext.cs │ │ ├── DbGlobalSqliteContext.cs │ │ ├── DbTenantContext.cs │ │ ├── DbTenantFactory.cs │ │ ├── DbTenantMsSqlContext.cs │ │ ├── DbTenantSqliteContext.cs │ │ ├── EfGlobalGlobalStorage.cs │ │ ├── EfTenantStorage.cs │ │ ├── IGlobalStorage.cs │ │ ├── ITenantStorage.cs │ │ ├── ManualTenantProvider.cs │ │ └── ValueComparers │ │ ├── EnumerableValueComparer.cs │ │ └── NullableEnumerableValueComparer.cs │ ├── TenantProvider.cs │ ├── TimeProviderSystemClockAdapter.cs │ ├── TokenService.cs │ ├── UserCredentialsService.cs │ └── Validation │ └── TokenValidator.cs └── tests ├── AdminConsole.Tests ├── AdminConsole.Tests.csproj ├── Components │ ├── BadgeTests.cs │ ├── Pages │ │ ├── Account │ │ │ └── MagicTests.cs │ │ ├── App │ │ │ ├── BaseApplicationPageTests.cs │ │ │ ├── Credentials │ │ │ │ └── UserTests.cs │ │ │ ├── LogTests.cs │ │ │ ├── ReportingComponents │ │ │ │ ├── TotalCredentialsCountChartTests.cs │ │ │ │ └── TotalUsersCountChartTests.cs │ │ │ └── Settings │ │ │ │ ├── AuthenticatorsTests.cs │ │ │ │ ├── SettingsComponents │ │ │ │ ├── ApiKeysSectionTests.cs │ │ │ │ ├── CreateApiKeyComponentTests.cs │ │ │ │ └── DeleteApplicationSectionTests.cs │ │ │ │ └── SettingsTests.cs │ │ └── Organization │ │ │ ├── OverviewTests.cs │ │ │ ├── Settings │ │ │ ├── SettingsComponents │ │ │ │ └── DeleteOrganizationComponentTests.cs │ │ │ └── SettingsTests.cs │ │ │ └── VerifyTests.cs │ └── Shared │ │ ├── Alerts │ │ └── LinkAlertTests.cs │ │ ├── ApexCharts │ │ ├── Serialization │ │ │ ├── ApexChartJsonSerializerTests.cs │ │ │ └── ColorConverterTests.cs │ │ └── Validators │ │ │ └── ColorValidatorTests.cs │ │ ├── BreadCrumbTests.cs │ │ ├── Layouts │ │ ├── ApplicationNavMenuTests.cs │ │ └── NavMenuTests.cs │ │ ├── LocalDateTimeTests.cs │ │ ├── Modals │ │ └── SimpleAlertTests.cs │ │ ├── SecureImportMapScriptTests.cs │ │ ├── SecureScriptTests.cs │ │ ├── SecureStylesheetTests.cs │ │ ├── Stats │ │ ├── SimpleCardsStatsTests.cs │ │ └── TrendingCardsStatsTests.cs │ │ ├── Table │ │ ├── PagedTableTests.cs │ │ └── TableTests.cs │ │ └── Validation │ │ └── CustomValidationErrorsTests.cs ├── DataFactory │ ├── FakeMagicLinkSignInManager.cs │ └── FakeUserManager.cs ├── Endpoints │ └── ApplicationEndpointsTests.cs ├── Factory │ ├── ClaimsPrincipalFactory.cs │ └── DbContextFactory.cs ├── FeatureManagement │ └── OrganizationFilterTests.cs ├── Mail │ └── DefaultMailServiceTests.cs └── Services │ └── AdminServiceTests.cs ├── Api.IntegrationTests ├── Api.IntegrationTests.csproj ├── ApiCollectionFixture.cs ├── AuthorizationTests.cs ├── Endpoints │ ├── App │ │ └── AppTests.cs │ ├── AuthenticationConfiguration │ │ └── AuthenticationConfigurationTests.cs │ ├── Authenticators │ │ └── AuthenticatorsTests.cs │ ├── Credentials │ │ └── CredentialsTests.cs │ ├── Events │ │ └── EventsTests.cs │ ├── Magic │ │ └── MagicTests.cs │ ├── Register │ │ ├── RegisterAttestationTests.cs │ │ ├── RegisterTests.cs │ │ └── RegisterTokenTests.cs │ └── SignIn │ │ └── SignInTests.cs ├── Helpers │ ├── App │ │ ├── AppManagementHelpers.cs │ │ ├── CreateAppHelpers.cs │ │ └── RequestHelpers.cs │ ├── AssertHelpers.cs │ ├── BrowserCredentialsHelper.cs │ ├── FakeMailProviderFactory.cs │ ├── HttpClientTestExtensions.cs │ ├── Js │ │ ├── converters.js │ │ ├── createCredential.js │ │ └── getCredential.js │ ├── NoopMailProvider.cs │ ├── User │ │ └── UserHelpers.cs │ └── WebDriverFactory.cs ├── Middleware │ ├── AuthorizationIntegrationTests.cs │ └── RoutingIntegrationTests.cs ├── PasswordlessApi.cs ├── PasswordlessApiFixture.cs ├── PasswordlessApiOptions.cs └── xunit.runner.json ├── Api.Tests ├── Api.Tests.csproj ├── Authorization │ └── HeaderHandlerTests.cs ├── Endpoints │ ├── AppsEndpointsTests.cs │ ├── AuthenticatorsEndpointsTests.cs │ └── ReportingEndpointsTests.cs ├── Extensions │ └── HeaderDictionaryExtensionsTests.cs └── Usings.cs ├── Common.Tests ├── Background │ └── ExecutionPlanUtilityTests.cs ├── Common.Tests.csproj ├── Extensions │ ├── EnumExtensionsTests.cs │ ├── StringExtensionsTests.cs │ └── UrlExtensionsTests.cs ├── Middleware │ └── SelfHosting │ │ └── HttpOverridesMiddlewareTests.cs ├── Serialization │ └── HtmlSanitizerTests.cs ├── Services │ └── Mail │ │ └── AggregateMailProviderTests.cs ├── Usings.cs ├── Utils │ └── ApiKeyUtilsTests.cs └── Validation │ ├── EmailAttributeTests.cs │ ├── EmailData.cs │ ├── EmailValidatorTests.cs │ ├── NoForbiddenContentAttributeTests.cs │ └── RegularExpressionCollectionAttributeTests.cs └── Service.Tests ├── Implementations ├── ApplicationServiceTests.cs ├── AuthenticationConfigurationServiceTests.cs ├── Fido2ServiceTests.cs ├── SharedManagementServiceTests.cs └── TokenServiceTests.cs ├── MDS └── CacheHandlerTests.cs ├── Models └── ApiDescTests.cs ├── Service.Tests.csproj ├── Storage └── Ef │ └── ValueComparers │ ├── EnumerableValueComparerTests.cs │ └── NullableEnumerableValueComparerTests.cs ├── Usings.cs └── Validation └── TokenValidatorTests.cs /.dockerignore: -------------------------------------------------------------------------------- 1 | # directories 2 | **/bin/ 3 | **/obj/ 4 | **/out/ 5 | 6 | # files 7 | Dockerfile* 8 | **/*.md 9 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | *.sh text eol=lf -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Please sort into logical groups with comment headers. Sort groups in order of specificity. 2 | # For example, default owners should always be the first group. 3 | # Sort lines alphabetically within these groups to avoid accidentally adding duplicates. 4 | # 5 | # https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners 6 | 7 | # Default file owners 8 | * @bitwarden/team-passwordless-dev 9 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": ["github>bitwarden/renovate-config"], 4 | "enabledManagers": ["github-actions", "npm", "nuget"], 5 | "packageRules": [ 6 | { 7 | "groupName": "gh minor", 8 | "matchManagers": ["github-actions"], 9 | "matchUpdateTypes": ["minor", "patch"] 10 | }, 11 | { 12 | "groupName": "npm minor", 13 | "matchManagers": ["npm"], 14 | "matchUpdateTypes": ["minor", "patch"] 15 | }, 16 | { 17 | "groupName": "nuget minor", 18 | "matchManagers": ["nuget"], 19 | "matchUpdateTypes": ["minor", "patch"] 20 | } 21 | ], 22 | "ignoreDeps": ["Datadog.Trace", "dotnet-sdk"] 23 | } 24 | -------------------------------------------------------------------------------- /.idea/.idea.PasswordlessServer/.idea/IntelliLang.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SecureScript 6 | 7 | 8 | 9 | 10 | SecureStylesheet 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | net9.0 4 | Passwordless.$(MSBuildProjectName) 5 | Passwordless.$(MSBuildProjectName) 6 | 7 | 8 | 9 | latest 10 | enable 11 | enable 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | patch: false 4 | project: false 5 | -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "version": "9.0.101", 4 | "rollForward": "latestFeature" 5 | } 6 | } -------------------------------------------------------------------------------- /run_self-hosting-image-ssl.sh: -------------------------------------------------------------------------------- 1 | docker stop passwordless 2 | docker rm passwordless 3 | docker build . -f self-host/Dockerfile -t bitwarden/passwordless 4 | 5 | docker run \ 6 | --name passwordless \ 7 | -p 5701:5701 \ 8 | -e BWP_PORT=5701 \ 9 | -e BWP_DOMAIN=yourdomain.local \ 10 | -e BWP_ENABLE_SSL=true \ 11 | -e BWP_ENABLE_SSL_CA=true \ 12 | -e BWP_SSL_CA_CERT=ssl.crt \ 13 | -v /your/path/passwordless-self-hosting:/etc/bitwarden_passwordless \ 14 | bitwarden/passwordless -------------------------------------------------------------------------------- /run_self-hosting-image.sh: -------------------------------------------------------------------------------- 1 | docker stop passwordless 2 | docker rm passwordless 3 | docker build . -f self-host/Dockerfile -t bitwarden/passwordless 4 | 5 | docker run \ 6 | --name passwordless \ 7 | -p 5701:5701 \ 8 | -e BWP_DOMAIN=yourdomain.com \ 9 | -e BWP_ENABLE_SSL=false \ 10 | -v /your/path/passwordless-self-hosting:/etc/bitwarden_passwordless \ 11 | bitwarden/passwordless -------------------------------------------------------------------------------- /self-host/README.md: -------------------------------------------------------------------------------- 1 | # Self-hosting 2 | 3 | Follow these instructions to run Passwordless.dev in a self-hosted environment. 4 | 5 | ## Build the image 6 | 7 | 1. Clone the repository 8 | 2. Set your working directory to match the root of the cloned files, for example: 9 | `~/src/passwordless-server` 10 | 3. In your terminal execute: 11 | ```bash 12 | docker build . -f self-host/Dockerfile -t bitwarden/passwordless 13 | ``` 14 | 15 | ## Run the image 16 | 17 | 1. In your terminal execute: 18 | ```bash 19 | docker run -d --name passwordless -p 5701:5701 bitwarden/passwordless 20 | ``` -------------------------------------------------------------------------------- /self-host/docker-compose.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: "3.8" 3 | 4 | services: 5 | bitwarden-passwordless: 6 | depends_on: 7 | - db 8 | env_file: 9 | - settings.env 10 | image: ${REGISTRY:-bitwarden}/passwordless:${TAG:-latest} 11 | restart: always 12 | ports: 13 | - "5701:5701" 14 | volumes: 15 | - bitwarden_passwordless:/etc/bitwarden_passwordless 16 | - logs:/var/log/bitwarden_passwordless 17 | 18 | # MS SQL Server Example 19 | # Docs: https://learn.microsoft.com/en-us/sql/linux/sql-server-linux-docker-container-deployment 20 | db: 21 | environment: 22 | MSSQL_SA_PASSWORD: "super_strong_password" 23 | ACCEPT_EULA: Y 24 | image: mcr.microsoft.com/mssql/server:2022-latest 25 | restart: always 26 | volumes: 27 | - data:/var/opt/mssql 28 | 29 | volumes: 30 | bitwarden_passwordless: 31 | logs: 32 | data: 33 | -------------------------------------------------------------------------------- /self-host/hbs/config.yaml: -------------------------------------------------------------------------------- 1 | helper_categories: 2 | - String 3 | templates: 4 | - src: /etc/hbs/nginx-config.hbs 5 | dest: /etc/nginx/http.d/bitwarden.conf -------------------------------------------------------------------------------- /self-host/mounted_dir/config.json.example: -------------------------------------------------------------------------------- 1 | { 2 | "ConnectionStrings": { 3 | "sqlite": { 4 | "admin": "Data Source=/app/storage/admin.db", 5 | "api": "Data Source=/app/storage/api.db" 6 | } 7 | }, 8 | "Passwordless": { 9 | "ApiKey": "test:public:6c3bd9b57e931da80d765d615d7798e1", 10 | "ApiSecret": "test:secret:9bd0132c7b7afe18ccef1aeadce12bec" 11 | }, 12 | "PasswordlessManagement": { 13 | "ManagementKey": "17b64cd1ca3fea9b7f163157ca83d0da" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /self-host/nginx/logrotate.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | while true 4 | do 5 | [ "$1" = "loop" ] && sleep $((24 * 3600 - (`date +%_H` * 3600 + `date +%_M` * 60 + `date +%_S`))) 6 | ts=$(date +%Y%m%d_%H%M%S) 7 | mv /var/log/nginx/access.log /var/log/nginx/access.$ts.log 8 | mv /var/log/nginx/error.log /var/log/nginx/error.$ts.log 9 | kill -USR1 `cat /var/run/nginx/nginx.pid` 10 | sleep 1 11 | gzip /var/log/nginx/access.$ts.log 12 | gzip /var/log/nginx/error.$ts.log 13 | find /var/log/nginx/ -name "*.gz" -mtime +32 -delete 14 | [ "$1" != "loop" ] && break 15 | done 16 | -------------------------------------------------------------------------------- /self-host/nginx/proxy.conf: -------------------------------------------------------------------------------- 1 | proxy_redirect off; 2 | 3 | map $http_host $upstream_host { 4 | default "$host"; 5 | ~. "$http_host"; 6 | } 7 | proxy_set_header Host $upstream_host; 8 | 9 | proxy_set_header X-Real-IP $remote_addr; 10 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 11 | 12 | map $http_x_forwarded_proto $upstream_scheme { 13 | default "$scheme"; 14 | ~. "$http_x_forwarded_proto"; 15 | } 16 | proxy_set_header X-Url-Scheme $upstream_scheme; 17 | proxy_set_header X-Forwarded-Proto $upstream_scheme; 18 | 19 | client_max_body_size 505m; 20 | client_body_buffer_size 128k; 21 | proxy_connect_timeout 90; 22 | proxy_send_timeout 90; 23 | proxy_read_timeout 90; 24 | proxy_buffer_size 128k; 25 | proxy_buffers 4 256k; 26 | proxy_busy_buffers_size 256k; 27 | large_client_header_buffers 4 32k; 28 | -------------------------------------------------------------------------------- /self-host/nginx/security-headers-ssl.conf: -------------------------------------------------------------------------------- 1 | # This will enforce HTTP browsing into HTTPS and avoid ssl stripping attack. 6 months age 2 | add_header Strict-Transport-Security max-age=15768000; -------------------------------------------------------------------------------- /self-host/nginx/security-headers.conf: -------------------------------------------------------------------------------- 1 | add_header Referrer-Policy same-origin; 2 | add_header X-Content-Type-Options nosniff; 3 | add_header X-XSS-Protection "1; mode=block"; -------------------------------------------------------------------------------- /self-host/supervisord/admin.ini: -------------------------------------------------------------------------------- 1 | [program:admin] 2 | autostart=true 3 | autorestart=true 4 | command=/usr/bin/dotnet "Passwordless.AdminConsole.dll" 5 | directory=/app/AdminConsole 6 | environment=ASPNETCORE_URLS="http://+:5001",ConnectionStrings__sqlite="%(ENV_ConnectionStrings__sqlite__admin)s" 7 | redirect_stderr=true 8 | startsecs=15 9 | stdout_logfile=/var/log/bitwarden_passwordless/admin.log 10 | -------------------------------------------------------------------------------- /self-host/supervisord/api.ini: -------------------------------------------------------------------------------- 1 | [program:api] 2 | autostart=true 3 | autorestart=true 4 | command=/usr/bin/dotnet "Passwordless.Api.dll" 5 | directory=/app/Api 6 | environment=ASPNETCORE_URLS="http://+:5000",ConnectionStrings__sqlite="%(ENV_ConnectionStrings__sqlite__api)s" 7 | redirect_stderr=true 8 | startsecs=15 9 | stdout_logfile=/var/log/bitwarden_passwordless/api.log 10 | -------------------------------------------------------------------------------- /self-host/supervisord/nginx.ini: -------------------------------------------------------------------------------- 1 | [program:nginx] 2 | autostart=true 3 | autorestart=true 4 | command=nginx 5 | redirect_stderr=true 6 | startsecs=15 7 | stdout_logfile=/var/log/bitwarden/nginx.log 8 | -------------------------------------------------------------------------------- /self-host/supervisord/supervisord.conf: -------------------------------------------------------------------------------- 1 | [unix_http_server] 2 | file=/run/supervisord.sock ; the path to the socket file 3 | 4 | [supervisord] 5 | logfile=/var/log/supervisord.log ; main log file; default $CWD/supervisord.log 6 | nodaemon=true ; start in foreground if true; default false 7 | 8 | [rpcinterface:supervisor] 9 | supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface 10 | 11 | [supervisorctl] 12 | serverurl=unix:///run/supervisord.sock ; use a unix:// URL for a unix socket 13 | 14 | [include] 15 | files = /etc/supervisor.d/*.ini 16 | -------------------------------------------------------------------------------- /src/AdminConsole/.gitignore: -------------------------------------------------------------------------------- 1 | /wwwroot/css/tailwind.css 2 | /wwwroot/lib/ -------------------------------------------------------------------------------- /src/AdminConsole/Authorization/CustomClaimTypes.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.AdminConsole.Authorization; 2 | 3 | public static class CustomClaimTypes 4 | { 5 | public const string OrgId = "OrgId"; 6 | } -------------------------------------------------------------------------------- /src/AdminConsole/Authorization/CustomPolicy.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.AdminConsole.Authorization; 2 | 3 | public static class CustomPolicy 4 | { 5 | public const string HasAppRole = "HasAppRole"; 6 | } -------------------------------------------------------------------------------- /src/AdminConsole/Authorization/HasAppRoleRequirement.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authorization; 2 | 3 | namespace Passwordless.AdminConsole.Authorization; 4 | 5 | public class HasAppRoleRequirement : IAuthorizationRequirement; -------------------------------------------------------------------------------- /src/AdminConsole/Billing/Configuration/BillingOptions.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.AdminConsole.Billing.Configuration; 2 | 3 | public class BillingOptions 4 | { 5 | public string WebhookSecret { get; set; } 6 | 7 | public string ApiKey { get; set; } 8 | 9 | public StoreOptions Store { get; set; } 10 | 11 | public Dictionary Plans { get; set; } 12 | } -------------------------------------------------------------------------------- /src/AdminConsole/Billing/Configuration/BillingPlanOptions.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.AdminConsole.Billing.Configuration; 2 | 3 | public class BillingPlanOptions 4 | { 5 | public ushort Order { get; set; } 6 | public string? PriceId { get; set; } 7 | public UiOptions Ui { get; set; } 8 | 9 | public FeaturesOptions Features { get; set; } 10 | } -------------------------------------------------------------------------------- /src/AdminConsole/Billing/Configuration/FeaturesOptions.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.AdminConsole.Billing.Configuration; 2 | 3 | public class FeaturesOptions 4 | { 5 | public bool EventLoggingIsEnabled { get; set; } 6 | 7 | public int EventLoggingRetentionPeriod { get; set; } 8 | 9 | /// 10 | /// Maximum allowed magic link emails sent for this application. 11 | /// Depending on the age of the application, the actual limit may be lower. 12 | /// 13 | public int MagicLinkEmailMonthlyQuota { get; set; } 14 | 15 | public int MaxAdmins { get; set; } 16 | 17 | public int MaxApplications { get; set; } 18 | 19 | /// 20 | /// Maximum number of individual users allowed to use the application 21 | /// 22 | public long? MaxUsers { get; set; } 23 | 24 | public bool AllowAttestation { get; set; } 25 | } -------------------------------------------------------------------------------- /src/AdminConsole/Billing/Configuration/StoreOptions.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.AdminConsole.Billing.Configuration; 2 | 3 | public class StoreOptions 4 | { 5 | public string Free { get; set; } 6 | public string Pro { get; set; } 7 | public string Enterprise { get; set; } 8 | } -------------------------------------------------------------------------------- /src/AdminConsole/Billing/Configuration/UiOptions.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.AdminConsole.Billing.Configuration; 2 | 3 | public class UiOptions 4 | { 5 | public string Price { get; set; } 6 | 7 | public string? PriceHint { get; set; } 8 | 9 | public ICollection Features { get; set; } 10 | 11 | public string Label { get; set; } 12 | } -------------------------------------------------------------------------------- /src/AdminConsole/Billing/Constants/PlanConstants.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.AdminConsole.Billing.Constants; 2 | 3 | public class PlanConstants 4 | { 5 | public const string Free = "Free"; 6 | public const string Pro = "Pro"; 7 | public const string Enterprise = "Enterprise"; 8 | } -------------------------------------------------------------------------------- /src/AdminConsole/Components/Layouts/NavMenuItem.razor: -------------------------------------------------------------------------------- 1 | 2 | @Label 3 | 4 | 5 | @code { 6 | public IDictionary IconParameters { get; } = new Dictionary 7 | { 8 | { "class", "nav-link-icon" } 9 | }; 10 | 11 | [Parameter] 12 | public required string Href { get; set; } 13 | 14 | [Parameter] 15 | public required Type Icon { get; set; } 16 | 17 | [Parameter] 18 | public required string Label { get; set; } 19 | } -------------------------------------------------------------------------------- /src/AdminConsole/Components/Pages/Account/Magic.razor: -------------------------------------------------------------------------------- 1 | @page "/Account/BadMagic" 2 | @using Passwordless.AdminConsole.Identity 3 | @using Passwordless.AdminConsole.Services.MagicLinks 4 | 5 | @attribute [AllowAnonymous] 6 | 7 | @inject MagicLinkSignInManager SignInManager 8 | @inject NavigationManager NavigationManager 9 | 10 | 11 | 12 | Sorry, something went wrong. The token was not valid. 13 | 14 | -------------------------------------------------------------------------------- /src/AdminConsole/Components/Pages/Account/Magic.razor.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Components; 2 | 3 | namespace Passwordless.AdminConsole.Components.Pages.Account; 4 | 5 | public partial class Magic : ComponentBase 6 | { 7 | protected override async Task OnInitializedAsync() 8 | { 9 | 10 | } 11 | } -------------------------------------------------------------------------------- /src/AdminConsole/Components/Pages/Account/Profile.razor: -------------------------------------------------------------------------------- 1 | @page "/Account/Profile" 2 | 3 | @inject IHttpContextAccessor HttpContextAccessor 4 | @inject IPasswordlessClient PasswordlessClient 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/AdminConsole/Components/Pages/Account/Profile.razor.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Components; 2 | using Passwordless.AdminConsole.Helpers; 3 | 4 | namespace Passwordless.AdminConsole.Components.Pages.Account; 5 | 6 | public partial class Profile : ComponentBase 7 | { 8 | public string UserId => HttpContextAccessor.HttpContext!.User.GetId(); 9 | } -------------------------------------------------------------------------------- /src/AdminConsole/Components/Pages/Account/UserOnboarding.razor.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Components; 2 | using Passwordless.AdminConsole.Helpers; 3 | 4 | namespace Passwordless.AdminConsole.Components.Pages.Account; 5 | 6 | public partial class UserOnboarding : ComponentBase 7 | { 8 | public string UserId => HttpContextAccessor.HttpContext!.User.GetId(); 9 | 10 | } -------------------------------------------------------------------------------- /src/AdminConsole/Components/Pages/App/BaseApplicationPage.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authorization; 2 | using Microsoft.AspNetCore.Components; 3 | using Passwordless.AdminConsole.Authorization; 4 | 5 | namespace Passwordless.AdminConsole.Components.Pages.App; 6 | 7 | [Authorize(CustomPolicy.HasAppRole)] 8 | public abstract class BaseApplicationPage : ComponentBase 9 | { 10 | /// 11 | /// The route parameter for the application id for application scoped pages. 12 | /// 13 | [Parameter] 14 | public required string AppId { get; set; } 15 | } -------------------------------------------------------------------------------- /src/AdminConsole/Components/Pages/App/Credentials/User.razor.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Components; 2 | 3 | namespace Passwordless.AdminConsole.Components.Pages.App.Credentials; 4 | 5 | public partial class User : BaseApplicationPage 6 | { 7 | private const bool HideCredentialDetails = false; 8 | 9 | public IReadOnlyCollection? Aliases { get; set; } 10 | 11 | [Parameter] 12 | public required string UserId { get; set; } 13 | 14 | protected override async Task OnInitializedAsync() 15 | { 16 | Aliases = await ApiClient.ListAliasesAsync(UserId); 17 | } 18 | } -------------------------------------------------------------------------------- /src/AdminConsole/Components/Pages/App/Settings/SettingsComponents/AttestationSection.razor: -------------------------------------------------------------------------------- 1 | @inject ICurrentContext CurrentContext; 2 | 3 | 4 |

When attestation is enabled, you can choose to limit the authenticators used with your application.

5 | 6 | 9 |
-------------------------------------------------------------------------------- /src/AdminConsole/Components/Pages/App/Settings/SettingsComponents/FeatureListItemIcon.razor: -------------------------------------------------------------------------------- 1 | 4 | 5 | @code { 6 | private const string BaseClass = "text-blue-400"; 7 | 8 | private string Class => AdditionalAttributes.ContainsKey("class") 9 | ? $"{BaseClass} {AdditionalAttributes["class"]}" 10 | : BaseClass; 11 | 12 | [Parameter(CaptureUnmatchedValues = true)] 13 | public IDictionary AdditionalAttributes { get; set; } = new Dictionary(0); 14 | } -------------------------------------------------------------------------------- /src/AdminConsole/Components/Pages/Billing/Cancelled.razor: -------------------------------------------------------------------------------- 1 | @page "/billing/cancelled" 2 | 3 | 4 | 5 | Checkout was cancelled. 6 |

To try again, please visit our Billing page.

7 |
8 |
9 | -------------------------------------------------------------------------------- /src/AdminConsole/Components/Pages/Billing/Default.razor: -------------------------------------------------------------------------------- 1 | @page "/billing/default" 2 | 3 | @inject NavigationManager NavigationManager 4 | 5 | @code { 6 | protected override void OnInitialized() 7 | { 8 | NavigationManager.NavigateTo("/billing/manage"); 9 | } 10 | } -------------------------------------------------------------------------------- /src/AdminConsole/Components/Pages/Billing/Success.razor: -------------------------------------------------------------------------------- 1 | @page "/billing/success" 2 | 3 | 4 | 5 | Successfully upgraded your organization. 6 |

To make any changes your payment or retrieve your invoices, please visit our Billing page.

7 |
8 |
9 | -------------------------------------------------------------------------------- /src/AdminConsole/Components/Pages/Organization/Create.razor.css: -------------------------------------------------------------------------------- 1 | .passkey-info { 2 | opacity: 0; 3 | position: absolute; 4 | top: 0; 5 | left: 0; 6 | height: 0; 7 | width: 0; 8 | z-index: -1; 9 | } -------------------------------------------------------------------------------- /src/AdminConsole/Components/Pages/Organization/Settings.razor: -------------------------------------------------------------------------------- 1 | @page "/Organization/Settings" 2 | @using Microsoft.FeatureManagement 3 | @using Passwordless.AdminConsole.Components.Pages.Organization.SettingsComponents 4 | 5 | @inject IDataService DataService 6 | @inject IFeatureManager FeatureManager 7 | 8 | 9 | @if (_organization != null) 10 | { 11 | @if (_allowDisablingMagicLinks) 12 | { 13 | 14 | } 15 | 16 | 19 | } 20 | -------------------------------------------------------------------------------- /src/AdminConsole/Components/Pages/Organization/Settings.razor.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Components; 2 | using Passwordless.AdminConsole.FeatureManagement; 3 | 4 | namespace Passwordless.AdminConsole.Components.Pages.Organization; 5 | 6 | public partial class Settings : ComponentBase 7 | { 8 | private Models.Organization? _organization; 9 | private bool _allowDisablingMagicLinks; 10 | 11 | protected override async Task OnInitializedAsync() 12 | { 13 | _allowDisablingMagicLinks = await FeatureManager.IsEnabledAsync(FeatureFlags.Organization.AllowDisablingMagicLinks); 14 | _organization = await DataService.GetOrganizationWithDataAsync(); 15 | } 16 | } -------------------------------------------------------------------------------- /src/AdminConsole/Components/Shared/ApexCharts/Models/ApexChartOptions.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.AdminConsole.Components.Shared.ApexCharts.Models; 2 | 3 | public class ApexChartOptions 4 | { 5 | public Title? Title { get; set; } 6 | 7 | public Chart? Chart { get; set; } 8 | 9 | public IEnumerable? Colors { get; set; } = new[] { Color.Default, Color.AmberOrange, Color.MelonRed, Color.MediumPurple }; 10 | 11 | public IEnumerable>? Series { get; set; } 12 | 13 | public Xaxis? Xaxis { get; set; } 14 | 15 | public Yaxis Yaxis { get; } = new(); 16 | 17 | public NoData NoData { get; set; } = new(); 18 | 19 | public Legend Legend { get; set; } = new(); 20 | } -------------------------------------------------------------------------------- /src/AdminConsole/Components/Shared/ApexCharts/Models/Chart.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.AdminConsole.Components.Shared.ApexCharts.Models; 2 | 3 | public class Chart 4 | { 5 | public ChartTypes Type { get; set; } 6 | 7 | public string? Height { get; set; } 8 | 9 | public Toolbar? Toolbar { get; set; } 10 | } -------------------------------------------------------------------------------- /src/AdminConsole/Components/Shared/ApexCharts/Models/ChartTypes.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.AdminConsole.Components.Shared.ApexCharts.Models; 2 | 3 | public enum ChartTypes 4 | { 5 | Line 6 | } -------------------------------------------------------------------------------- /src/AdminConsole/Components/Shared/ApexCharts/Models/Export.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.AdminConsole.Components.Shared.ApexCharts.Models; 2 | 3 | public class Export 4 | { 5 | public ExportCsv Csv { get; } = new(); 6 | 7 | public ExportSvg Svg { get; } = new(); 8 | 9 | public ExportPng Png { get; } = new(); 10 | 11 | 12 | /// 13 | /// Sets the file name for all export types. 14 | /// 15 | /// File name without file extension. 16 | public void SetFilename(string filename) 17 | { 18 | Csv.Filename = filename; 19 | Svg.Filename = filename; 20 | Png.Filename = filename; 21 | } 22 | } 23 | 24 | public abstract class BaseExport 25 | { 26 | public string? Filename { get; set; } 27 | } 28 | 29 | public class ExportCsv : BaseExport; 30 | 31 | public class ExportSvg : BaseExport; 32 | 33 | public class ExportPng : BaseExport; -------------------------------------------------------------------------------- /src/AdminConsole/Components/Shared/ApexCharts/Models/LabelsFormattingTypes.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.AdminConsole.Components.Shared.ApexCharts.Models; 2 | 3 | public enum LabelsFormattingTypes 4 | { 5 | Integer, 6 | Double, 7 | DateTime 8 | } -------------------------------------------------------------------------------- /src/AdminConsole/Components/Shared/ApexCharts/Models/Legend.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.AdminConsole.Components.Shared.ApexCharts.Models; 2 | 3 | public class Legend 4 | { 5 | public bool Show { get; set; } = true; 6 | 7 | public bool ShowForSingleSeries { get; set; } = true; 8 | 9 | public bool ShowForNullSeries { get; set; } = true; 10 | 11 | public bool ShowForZeroSeries { get; set; } = true; 12 | } -------------------------------------------------------------------------------- /src/AdminConsole/Components/Shared/ApexCharts/Models/NoData.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.AdminConsole.Components.Shared.ApexCharts.Models; 2 | 3 | public class NoData 4 | { 5 | public string Text { get; set; } = "No data available yet."; 6 | } -------------------------------------------------------------------------------- /src/AdminConsole/Components/Shared/ApexCharts/Models/Series.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.AdminConsole.Components.Shared.ApexCharts.Models; 2 | 3 | public class Series 4 | { 5 | public string? Name { get; set; } 6 | 7 | public IEnumerable? Data { get; set; } 8 | } -------------------------------------------------------------------------------- /src/AdminConsole/Components/Shared/ApexCharts/Models/Title.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.AdminConsole.Components.Shared.ApexCharts.Models; 2 | 3 | public class Title 4 | { 5 | public string? Text { get; set; } 6 | } -------------------------------------------------------------------------------- /src/AdminConsole/Components/Shared/ApexCharts/Models/Toolbar.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.AdminConsole.Components.Shared.ApexCharts.Models; 2 | 3 | public class Toolbar 4 | { 5 | public Export? Export { get; set; } 6 | } -------------------------------------------------------------------------------- /src/AdminConsole/Components/Shared/ApexCharts/Models/Xaxis.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.AdminConsole.Components.Shared.ApexCharts.Models; 2 | 3 | public class Xaxis 4 | { 5 | public XAxisTypes? Type { get; set; } 6 | 7 | public IEnumerable? Categories { get; set; } 8 | } -------------------------------------------------------------------------------- /src/AdminConsole/Components/Shared/ApexCharts/Models/XaxisTypes.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.AdminConsole.Components.Shared.ApexCharts.Models; 2 | 3 | public enum XAxisTypes 4 | { 5 | Category, 6 | Datetime, 7 | Numeric 8 | } -------------------------------------------------------------------------------- /src/AdminConsole/Components/Shared/ApexCharts/Models/Yaxis.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.AdminConsole.Components.Shared.ApexCharts.Models; 2 | 3 | public class Yaxis 4 | { 5 | public YaxisLabels Labels { get; } = new(); 6 | } -------------------------------------------------------------------------------- /src/AdminConsole/Components/Shared/ApexCharts/Models/YaxisLabels.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.AdminConsole.Components.Shared.ApexCharts.Models; 2 | 3 | public class YaxisLabels 4 | { 5 | } -------------------------------------------------------------------------------- /src/AdminConsole/Components/Shared/ApexCharts/Serialization/ApexChartJsonSerializer.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace Passwordless.AdminConsole.Components.Shared.ApexCharts.Serialization; 5 | 6 | public static class ApexChartJsonSerializer 7 | { 8 | public static readonly JsonSerializerOptions Options = new() 9 | { 10 | Converters = 11 | { 12 | new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) 13 | }, 14 | DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, 15 | PropertyNamingPolicy = JsonNamingPolicy.CamelCase, 16 | WriteIndented = false 17 | }; 18 | } -------------------------------------------------------------------------------- /src/AdminConsole/Components/Shared/ApexCharts/Validators/ColorValidator.cs: -------------------------------------------------------------------------------- 1 | using System.Text.RegularExpressions; 2 | 3 | namespace Passwordless.AdminConsole.Components.Shared.ApexCharts.Validators; 4 | 5 | public static class ColorValidator 6 | { 7 | private static readonly Regex HexRegex = new(@"^#(?:[0-9a-fA-F]{3,4}){1,2}$", RegexOptions.Compiled); 8 | 9 | public static bool IsValid(string hex) 10 | { 11 | return HexRegex.IsMatch(hex); 12 | } 13 | } -------------------------------------------------------------------------------- /src/AdminConsole/Components/Shared/BaseRazorComponent.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Components; 2 | 3 | namespace Passwordless.AdminConsole.Components.Shared; 4 | 5 | public abstract class BaseRazorComponent : ComponentBase 6 | { 7 | [Parameter] 8 | public string? Class { get; set; } 9 | } -------------------------------------------------------------------------------- /src/AdminConsole/Components/Shared/Cards/Card.razor: -------------------------------------------------------------------------------- 1 |
2 |
3 |

@Title

4 | @if (!string.IsNullOrWhiteSpace(SubTitle)) 5 | { 6 |

@SubTitle

7 | } 8 | @if (ChildContent != null) 9 | { 10 | 13 | } 14 |
15 |
16 | 17 | @code { 18 | [Parameter] public required string Title { get; set; } 19 | 20 | [Parameter] public string? SubTitle { get; set; } 21 | 22 | [Parameter] public RenderFragment? ChildContent { get; set; } 23 | } -------------------------------------------------------------------------------- /src/AdminConsole/Components/Shared/Cards/Card.razor.css: -------------------------------------------------------------------------------- 1 | .card { 2 | @apply overflow-hidden bg-white shadow sm:rounded-lg; 3 | } 4 | 5 | .card h3 { 6 | @apply line-clamp-1 7 | } 8 | 9 | .card .subtitle { 10 | @apply mt-1 max-w-2xl text-sm text-gray-500 line-clamp-1 11 | } -------------------------------------------------------------------------------- /src/AdminConsole/Components/Shared/Cards/CardDetailsProperty.razor.css: -------------------------------------------------------------------------------- 1 | .details-property { 2 | @apply flex flex-col space-y-1; 3 | } 4 | 5 | .property-label { 6 | @apply text-sm font-medium text-gray-500; 7 | } 8 | 9 | .property-value { 10 | @apply text-sm text-gray-900; 11 | } -------------------------------------------------------------------------------- /src/AdminConsole/Components/Shared/Cards/CardSummaryProperty.razor: -------------------------------------------------------------------------------- 1 |
2 |
@Label
3 |
4 | @if (Value is DateTime) 5 | { 6 | 7 | } 8 | else 9 | { 10 | @Value 11 | } 12 |
13 |
14 | 15 | @code { 16 | [Parameter] public required string Label { get; set; } 17 | 18 | [Parameter] public required object Value { get; set; } 19 | } -------------------------------------------------------------------------------- /src/AdminConsole/Components/Shared/Cards/CardSummaryProperty.razor.css: -------------------------------------------------------------------------------- 1 | .summary-property { 2 | @apply flex items-center space-x-2; 3 | } 4 | 5 | .property-label { 6 | @apply text-sm font-medium text-gray-500; 7 | } 8 | 9 | .property-value { 10 | @apply text-sm text-gray-900; 11 | } -------------------------------------------------------------------------------- /src/AdminConsole/Components/Shared/ContextualStyles.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.AdminConsole.Components.Shared; 2 | 3 | public enum ContextualStyles : byte 4 | { 5 | Primary, 6 | Secondary, 7 | Success, 8 | Danger, 9 | Warning, 10 | Info 11 | } -------------------------------------------------------------------------------- /src/AdminConsole/Components/Shared/Credentials.razor.css: -------------------------------------------------------------------------------- 1 | .credential-card-new { 2 | @apply overflow-hidden shadow sm:rounded-lg max-w-fit border-2 border-green-600; 3 | } 4 | 5 | .credential-card { 6 | @apply overflow-hidden border-2 border-blue-600 shadow sm:rounded-lg max-w-fit; 7 | } 8 | 9 | .credential-card-summary-footer { 10 | @apply flex justify-between items-center mt-2; 11 | } -------------------------------------------------------------------------------- /src/AdminConsole/Components/Shared/Forms/ConfirmButton.razor: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/AdminConsole/Components/Shared/Forms/ConfirmEditForm.razor.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Components; 2 | using Microsoft.AspNetCore.Components.Forms; 3 | 4 | namespace Passwordless.AdminConsole.Components.Shared.Forms; 5 | 6 | /// 7 | /// This form component will intercept any submitted forms and display a confirmation dialog before submitting the form to the server. 8 | /// 9 | public partial class ConfirmEditForm : ComponentBase 10 | { 11 | [Parameter(CaptureUnmatchedValues = true)] 12 | public Dictionary? AdditionalAttributes { get; set; } 13 | 14 | [Parameter] 15 | public RenderFragment? ChildContent { get; set; } 16 | 17 | [Parameter] 18 | public string? FormName { get; set; } 19 | 20 | [Parameter] 21 | public object? Model { get; set; } 22 | 23 | [Parameter] 24 | public EventCallback OnSubmit { get; set; } 25 | } -------------------------------------------------------------------------------- /src/AdminConsole/Components/Shared/Icons/Alerts/DangerAlertIcon.razor: -------------------------------------------------------------------------------- 1 | @inherits BaseAlertIcon 2 | 3 | -------------------------------------------------------------------------------- /src/AdminConsole/Components/Shared/Icons/Alerts/InfoAlertIcon.razor: -------------------------------------------------------------------------------- 1 | @inherits BaseAlertIcon 2 | 3 | -------------------------------------------------------------------------------- /src/AdminConsole/Components/Shared/Icons/Alerts/SuccessAlertIcon.razor: -------------------------------------------------------------------------------- 1 | @inherits BaseAlertIcon 2 | 3 | -------------------------------------------------------------------------------- /src/AdminConsole/Components/Shared/Icons/Alerts/WarningAlertIcon.razor: -------------------------------------------------------------------------------- 1 | @inherits BaseAlertIcon 2 | 3 | -------------------------------------------------------------------------------- /src/AdminConsole/Components/Shared/Icons/BeakerIcon.razor: -------------------------------------------------------------------------------- 1 | @inherits BaseRazorComponent 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/AdminConsole/Components/Shared/Icons/CodeBracketSquareIcon.razor: -------------------------------------------------------------------------------- 1 | @inherits BaseRazorComponent 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/AdminConsole/Components/Shared/Icons/DeleteIcon.razor: -------------------------------------------------------------------------------- 1 | @inherits BaseRazorComponent 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/AdminConsole/Components/Shared/Icons/DocumentTextIcon.razor: -------------------------------------------------------------------------------- 1 | @inherits BaseRazorComponent 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/AdminConsole/Components/Shared/Icons/DollarIcon.razor: -------------------------------------------------------------------------------- 1 | @inherits BaseRazorComponent 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/AdminConsole/Components/Shared/Icons/DownloadIcon.razor: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | @code { 6 | [Parameter(CaptureUnmatchedValues = true)] 7 | public Dictionary? AdditionalAttributes { get; set; } 8 | } -------------------------------------------------------------------------------- /src/AdminConsole/Components/Shared/Icons/HamburgerIcon.razor: -------------------------------------------------------------------------------- 1 | @inherits BaseRazorComponent 2 | 3 | -------------------------------------------------------------------------------- /src/AdminConsole/Components/Shared/Icons/LockIcon.razor: -------------------------------------------------------------------------------- 1 | @inherits BaseRazorComponent 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/AdminConsole/Components/Shared/Icons/MailIcon.razor: -------------------------------------------------------------------------------- 1 | @inherits BaseRazorComponent 2 | 3 | 6 | -------------------------------------------------------------------------------- /src/AdminConsole/Components/Shared/Icons/PaymentMethods/DinersIcon.razor: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/AdminConsole/Components/Shared/Icons/PencilSquareIcon.razor: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | @code { 6 | [Parameter(CaptureUnmatchedValues = true)] 7 | public Dictionary? AdditionalAttributes { get; set; } 8 | } -------------------------------------------------------------------------------- /src/AdminConsole/Components/Shared/Icons/SignOutIcon.razor: -------------------------------------------------------------------------------- 1 | @inherits BaseRazorComponent 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/AdminConsole/Components/Shared/Icons/StackedBarChartIcon.razor: -------------------------------------------------------------------------------- 1 | @inherits BaseRazorComponent 2 | 3 | -------------------------------------------------------------------------------- /src/AdminConsole/Components/Shared/Icons/StartIcon.razor: -------------------------------------------------------------------------------- 1 | @inherits BaseRazorComponent 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/AdminConsole/Components/Shared/Icons/UnlockIcon.razor: -------------------------------------------------------------------------------- 1 | @inherits BaseRazorComponent 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/AdminConsole/Components/Shared/Icons/UsersIcon.razor: -------------------------------------------------------------------------------- 1 | @inherits BaseRazorComponent 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/AdminConsole/Components/Shared/LocalDateTime.razor: -------------------------------------------------------------------------------- 1 | 2 | 3 | document.getElementById(@((MarkupString)JsonSerializer.Serialize(Id))).innerText = new Date(@((MarkupString)JsonSerializer.Serialize(_isoValue!))).toLocaleString(); 4 | -------------------------------------------------------------------------------- /src/AdminConsole/Components/Shared/LocalDateTime.razor.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | using Microsoft.AspNetCore.Components; 3 | 4 | namespace Passwordless.AdminConsole.Components.Shared; 5 | 6 | /// 7 | /// Renders a in the user's local time zone. 8 | /// 9 | public partial class LocalDateTime : ComponentBase 10 | { 11 | public string Id { get; } = Guid.NewGuid().ToString(); 12 | 13 | [Parameter] 14 | public required DateTime Value { get; set; } 15 | 16 | [Parameter] 17 | public string? DateFormat { get; set; } = "yyyy-MM-ddTHH:mm:ss.fffZ"; 18 | 19 | private string? _isoValue; 20 | 21 | protected override void OnInitialized() 22 | { 23 | _isoValue = Value.ToString(DateFormat, CultureInfo.InvariantCulture); 24 | } 25 | } -------------------------------------------------------------------------------- /src/AdminConsole/Components/Shared/Page.razor: -------------------------------------------------------------------------------- 1 | @Title 2 | 3 |
4 | 5 |

@Title

6 |
7 |
8 | @ChildContent 9 |
10 | 11 | @code { 12 | [Parameter] 13 | public required string Title { get; set; } 14 | 15 | [Parameter] public bool HideTitle { get; set; } = false; 16 | 17 | [Parameter] 18 | public required RenderFragment ChildContent { get; set; } 19 | 20 | [Parameter] 21 | public IReadOnlyCollection? BreadCrumbItems { get; set; } 22 | 23 | private string? TitleClass => HideTitle ? "sr-only" : null; 24 | } -------------------------------------------------------------------------------- /src/AdminConsole/Components/Shared/Panel.razor: -------------------------------------------------------------------------------- 1 |
2 | @if (!string.IsNullOrEmpty(Header)) 3 | { 4 |

@Header

5 | } 6 |
7 | @ChildContent 8 |
9 |
10 | 11 | @code { 12 | [Parameter] public string? Header { get; set; } 13 | 14 | [Parameter] public required RenderFragment ChildContent { get; set; } 15 | } -------------------------------------------------------------------------------- /src/AdminConsole/Components/Shared/PanelFooter.razor: -------------------------------------------------------------------------------- 1 | 4 | 5 | @code { 6 | [Parameter] 7 | public required RenderFragment ChildContent { get; set; } 8 | } -------------------------------------------------------------------------------- /src/AdminConsole/Components/Shared/RedirectToLogin.razor: -------------------------------------------------------------------------------- 1 | @inject NavigationManager NavigationManager 2 | 3 | @code { 4 | protected override void OnInitialized() 5 | { 6 | NavigationManager.NavigateTo($"Account/Login", forceLoad: true); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/AdminConsole/Components/Shared/SecureImportMapScript.razor: -------------------------------------------------------------------------------- 1 | @inject IFileVersionProvider Version 2 | @inject IHttpContextAccessor HttpContextAccessor 3 | 4 | @if (Value.Imports.Count > 0) 5 | { 6 | 9 | } -------------------------------------------------------------------------------- /src/AdminConsole/Components/Shared/Stats/BaseCardStats.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Components; 2 | 3 | namespace Passwordless.AdminConsole.Components.Shared.Stats; 4 | 5 | public abstract class BaseCardStats : ComponentBase 6 | { 7 | [Parameter(CaptureUnmatchedValues = true)] 8 | public IDictionary AdditionalAttributes { get; set; } = new Dictionary(); 9 | 10 | protected override void OnInitialized() 11 | { 12 | if (AdditionalAttributes.TryGetValue("class", out var classAttribute)) 13 | { 14 | Class = $"{Class} {classAttribute}"; 15 | } 16 | } 17 | 18 | public string Class { get; protected set; } = "mt-5 grid grid-cols-1 gap-5 sm:grid-cols-3"; 19 | } -------------------------------------------------------------------------------- /src/AdminConsole/Components/Shared/Tables/PagedTable.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.AdminConsole.Components.Shared.Tables; 2 | 3 | public class PagedTable 4 | { 5 | public PagedTable(int items, int page, int pageSize) 6 | { 7 | TotalPages = items / pageSize + (items % pageSize == 0 ? 0 : 1); 8 | CurrentPage = page; 9 | 10 | Pages = Enumerable.Range(1, TotalPages); 11 | } 12 | 13 | public int CurrentPage { get; } 14 | public int TotalPages { get; } 15 | 16 | public IEnumerable Pages { get; } 17 | 18 | public bool HasPreviousPage => CurrentPage > 1; 19 | public bool HasNextPage => CurrentPage < TotalPages; 20 | } -------------------------------------------------------------------------------- /src/AdminConsole/Components/Shared/Validation/CustomValidationErrors.razor: -------------------------------------------------------------------------------- 1 | @if (EditContext.GetValidationMessages().Any()) 2 | { 3 |
    4 | 5 | @foreach (var error in EditContext.GetValidationMessages()) 6 | { 7 |
  • @error
  • 8 | } 9 |
10 | } 11 | 12 | @code { 13 | [Parameter] 14 | public required EditContext EditContext { get; set; } 15 | } -------------------------------------------------------------------------------- /src/AdminConsole/Db/MsSqlDesignTimeDbContextFactory.cs: -------------------------------------------------------------------------------- 1 | // ReSharper disable UnusedType.Global 2 | 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.EntityFrameworkCore.Design; 5 | 6 | namespace Passwordless.AdminConsole.Db; 7 | 8 | /// 9 | /// Do not delete this file. It is used by EF Core to create migrations. 10 | /// 11 | public class MsSqlDesignTimeDbContextFactory : IDesignTimeDbContextFactory 12 | { 13 | public MssqlConsoleDbContext CreateDbContext(string[] args) 14 | { 15 | var options = new DbContextOptionsBuilder(); 16 | options.UseSqlServer(args.Length == 1 ? args[0] : null); 17 | return new MssqlConsoleDbContext(options.Options); 18 | } 19 | } -------------------------------------------------------------------------------- /src/AdminConsole/Db/MssqlConsoleDbContext.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | 3 | namespace Passwordless.AdminConsole.Db; 4 | 5 | public class MssqlConsoleDbContext : ConsoleDbContext 6 | { 7 | public MssqlConsoleDbContext(DbContextOptions options) : base(options) 8 | { 9 | } 10 | } -------------------------------------------------------------------------------- /src/AdminConsole/Db/SqliteConsoleDbContext.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | 3 | namespace Passwordless.AdminConsole.Db; 4 | 5 | public class SqliteConsoleDbContext : ConsoleDbContext 6 | { 7 | public SqliteConsoleDbContext(DbContextOptions options) : base(options) 8 | { 9 | } 10 | } -------------------------------------------------------------------------------- /src/AdminConsole/Db/SqliteDesignTimeDbContextFactory.cs: -------------------------------------------------------------------------------- 1 | // ReSharper disable UnusedType.Global 2 | 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.EntityFrameworkCore.Design; 5 | 6 | namespace Passwordless.AdminConsole.Db; 7 | 8 | /// 9 | /// Do not delete this file. It is used by EF Core to create migrations. 10 | /// 11 | public class SqliteDesignTimeDbContextFactory : IDesignTimeDbContextFactory 12 | { 13 | public SqliteConsoleDbContext CreateDbContext(string[] args) 14 | { 15 | var options = new DbContextOptionsBuilder(); 16 | const string devDefault = "Data Source=adminconsole_dev.db"; 17 | options.UseSqlite(args.Length == 1 ? args[0] : devDefault); 18 | return new SqliteConsoleDbContext(options.Options); 19 | } 20 | } -------------------------------------------------------------------------------- /src/AdminConsole/EventLog/DTOs/AuditLogResponses.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.AdminConsole.EventLog.DTOs; 2 | 3 | public record OrganizationEventLogResponse(IEnumerable Events); 4 | 5 | public record ApplicationEventLogResponse(string TenantId, IEnumerable Events, int TotalEventCount); 6 | 7 | public record EventLogEvent(DateTime PerformedAt, string EventType, string Message, string Severity, string PerformedBy, string Subject, string ApiKeyId); -------------------------------------------------------------------------------- /src/AdminConsole/EventLog/Loggers/EventLoggerEfWriteStorage.cs: -------------------------------------------------------------------------------- 1 | using Passwordless.AdminConsole.Db; 2 | using Passwordless.AdminConsole.EventLog.DTOs; 3 | using Passwordless.AdminConsole.EventLog.Models; 4 | 5 | namespace Passwordless.AdminConsole.EventLog.Loggers; 6 | 7 | public class EventLoggerEfWriteStorage : IEventLogger 8 | { 9 | private readonly List _events = new(); 10 | private readonly ConsoleDbContext _db; 11 | 12 | public EventLoggerEfWriteStorage(ConsoleDbContext db) 13 | { 14 | _db = db; 15 | } 16 | 17 | public virtual void LogEvent(OrganizationEventDto @event) 18 | { 19 | _events.Add(@event.ToNewEvent()); 20 | } 21 | 22 | public async Task FlushAsync() 23 | { 24 | if (_events.Any()) 25 | { 26 | _db.OrganizationEvents.AddRange(_events); 27 | await _db.SaveChangesAsync(); 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /src/AdminConsole/EventLog/Loggers/IEventLoggerStorage.cs: -------------------------------------------------------------------------------- 1 | using Passwordless.AdminConsole.EventLog.DTOs; 2 | 3 | namespace Passwordless.AdminConsole.EventLog.Loggers; 4 | 5 | public interface IEventLoggerStorage 6 | { 7 | Task> GetOrganizationEvents(int organizationId, int pageNumber, int resultsPerPage); 8 | Task GetOrganizationEventCount(int organizationId); 9 | } -------------------------------------------------------------------------------- /src/AdminConsole/EventLog/Loggers/NoOpEventLogger.cs: -------------------------------------------------------------------------------- 1 | using Passwordless.AdminConsole.EventLog.DTOs; 2 | 3 | namespace Passwordless.AdminConsole.EventLog.Loggers; 4 | 5 | public class NoOpEventLogger : IEventLogger 6 | { 7 | public void LogEvent(OrganizationEventDto @event) 8 | { 9 | } 10 | 11 | public Task FlushAsync() => Task.CompletedTask; 12 | 13 | public static NoOpEventLogger Instance { get; } = new(); 14 | } -------------------------------------------------------------------------------- /src/AdminConsole/EventLog/Models/OrganizationEvent.cs: -------------------------------------------------------------------------------- 1 | using Passwordless.AdminConsole.EventLog.DTOs; 2 | using Passwordless.Common.EventLog.Models; 3 | 4 | namespace Passwordless.AdminConsole.EventLog.Models; 5 | 6 | public class OrganizationEvent : Event 7 | { 8 | public int OrganizationId { get; init; } 9 | 10 | public OrganizationEventDto ToDto() => 11 | new(PerformedBy, 12 | EventType, 13 | Message, 14 | Severity, 15 | Subject, 16 | OrganizationId, 17 | PerformedAt); 18 | } -------------------------------------------------------------------------------- /src/AdminConsole/FeatureManagement/FeatureFlags.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.AdminConsole.FeatureManagement; 2 | 3 | public static class FeatureFlags 4 | { 5 | public static class Organization 6 | { 7 | private const string Prefix = "Organization"; 8 | public const string AllowDisablingMagicLinks = $"{Prefix}_AllowDisablingMagicLinks"; 9 | } 10 | } -------------------------------------------------------------------------------- /src/AdminConsole/Helpers/IFileVersionProviderExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.AspNetCore.Mvc.ViewFeatures; 2 | 3 | public static class FileVersionProviderExtensions 4 | { 5 | public static string Get(this IFileVersionProvider provider, string path) => 6 | provider.AddFileVersionToPath("", path); 7 | 8 | public static string Get(this IFileVersionProvider provider, IHttpContextAccessor accessor, string path) => 9 | provider.AddFileVersionToPath(accessor.HttpContext!.Request.PathBase, path); 10 | } -------------------------------------------------------------------------------- /src/AdminConsole/Helpers/IHttpContextAccessorExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure; 2 | 3 | namespace Passwordless.AdminConsole.Helpers; 4 | 5 | public static class IHttpContextAccessorExtensions 6 | { 7 | /// 8 | /// Detect whether the rendered page is a Razor Page or something else (Blazor). 9 | /// 10 | /// 11 | /// 12 | public static bool IsRazorPages(this IHttpContextAccessor httpContextAccessor) 13 | { 14 | var metadata = httpContextAccessor.HttpContext?.GetEndpoint()?.Metadata.GetMetadata(); 15 | return metadata != null; 16 | } 17 | } -------------------------------------------------------------------------------- /src/AdminConsole/Helpers/IUrlHelperExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | 3 | namespace Passwordless.AdminConsole.Helpers; 4 | 5 | public static class IUrlHelperExtensions 6 | { 7 | public static string AppPage(this IUrlHelper url, string page, string app) 8 | { 9 | return url.Page(page, new { app = app }); 10 | } 11 | } -------------------------------------------------------------------------------- /src/AdminConsole/Identity/ConsoleUser.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using Microsoft.AspNetCore.Identity; 3 | using Passwordless.AdminConsole.Models; 4 | 5 | namespace Passwordless.AdminConsole.Identity; 6 | 7 | public class ConsoleAdmin : IdentityUser 8 | { 9 | public ConsoleAdmin() : base() 10 | { 11 | this.Id = Guid.NewGuid().ToString(); 12 | } 13 | 14 | [MaxLength(50)] 15 | public string Name { get; set; } 16 | public int OrganizationId { get; set; } 17 | public Organization Organization { get; set; } 18 | } -------------------------------------------------------------------------------- /src/AdminConsole/Identity/Invite.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace Passwordless.AdminConsole.Identity; 4 | 5 | public class Invite 6 | { 7 | [MaxLength(50)] 8 | public string HashedCode { get; set; } 9 | public DateTime CreatedAt { get; set; } 10 | public DateTime ExpireAt { get; set; } 11 | [MaxLength(50)] 12 | public string FromName { get; set; } 13 | [MaxLength(50)] 14 | public string FromEmail { get; set; } 15 | public int TargetOrgId { get; set; } 16 | [MaxLength(50)] 17 | public string TargetOrgName { get; set; } 18 | [MaxLength(50)] 19 | public string ToEmail { get; set; } 20 | } -------------------------------------------------------------------------------- /src/AdminConsole/Middleware/EventLogStorageCommitMiddleware.cs: -------------------------------------------------------------------------------- 1 | using Passwordless.AdminConsole.EventLog.Loggers; 2 | 3 | namespace Passwordless.AdminConsole.Middleware; 4 | 5 | public partial class EventLogStorageCommitMiddleware 6 | { 7 | private readonly RequestDelegate _next; 8 | 9 | public EventLogStorageCommitMiddleware(RequestDelegate next) => _next = next; 10 | 11 | public async Task InvokeAsync(HttpContext context, IEventLogger eventLogger) 12 | { 13 | // If the request is not in our routing tables, skip this middleware. 14 | if (context.GetEndpoint() == null) 15 | { 16 | await _next(context); 17 | return; 18 | } 19 | 20 | try 21 | { 22 | await _next(context); 23 | } 24 | finally 25 | { 26 | await eventLogger.FlushAsync(); 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /src/AdminConsole/Middleware/RouteParameters.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.AdminConsole.Middleware; 2 | 3 | public class RouteParameters 4 | { 5 | public const string AppId = "appId"; 6 | } -------------------------------------------------------------------------------- /src/AdminConsole/Migrations/Mssql/20230809074539_UpdateTableApplications_AddColumnDeleteAt.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | 3 | #nullable disable 4 | 5 | namespace Passwordless.AdminConsole.Migrations.Mssql; 6 | 7 | /// 8 | public partial class UpdateTableApplications_AddColumnDeleteAt : Migration 9 | { 10 | /// 11 | protected override void Up(MigrationBuilder migrationBuilder) 12 | { 13 | migrationBuilder.AddColumn( 14 | name: "DeleteAt", 15 | table: "Applications", 16 | type: "datetime2", 17 | nullable: true); 18 | } 19 | 20 | /// 21 | protected override void Down(MigrationBuilder migrationBuilder) 22 | { 23 | migrationBuilder.DropColumn( 24 | name: "DeleteAt", 25 | table: "Applications"); 26 | } 27 | } -------------------------------------------------------------------------------- /src/AdminConsole/Migrations/Mssql/20230818131329_AlterTableOrganizations_AddColumnBecamePaidAt.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | 3 | #nullable disable 4 | 5 | namespace Passwordless.AdminConsole.Migrations.Mssql; 6 | 7 | /// 8 | public partial class AlterTableOrganizations_AddColumnBecamePaidAt : Migration 9 | { 10 | /// 11 | protected override void Up(MigrationBuilder migrationBuilder) 12 | { 13 | migrationBuilder.AddColumn( 14 | name: "BecamePaidAt", 15 | table: "Organizations", 16 | type: "datetime2", 17 | nullable: true); 18 | } 19 | 20 | /// 21 | protected override void Down(MigrationBuilder migrationBuilder) 22 | { 23 | migrationBuilder.DropColumn( 24 | name: "BecamePaidAt", 25 | table: "Organizations"); 26 | } 27 | } -------------------------------------------------------------------------------- /src/AdminConsole/Migrations/Mssql/20231009133628_RenameAuditLogToEventLog.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | 3 | #nullable disable 4 | 5 | namespace Passwordless.AdminConsole.Migrations.Mssql; 6 | 7 | /// 8 | public partial class RenameAuditLogToEventLog : Migration 9 | { 10 | /// 11 | protected override void Up(MigrationBuilder migrationBuilder) 12 | { 13 | 14 | } 15 | 16 | /// 17 | protected override void Down(MigrationBuilder migrationBuilder) 18 | { 19 | 20 | } 21 | } -------------------------------------------------------------------------------- /src/AdminConsole/Migrations/Mssql/20240716084408_AllowDisablingMagicLinksForOrganizations.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | 3 | #nullable disable 4 | 5 | namespace Passwordless.AdminConsole.Migrations.Mssql; 6 | 7 | /// 8 | public partial class AllowDisablingMagicLinksForOrganizations : Migration 9 | { 10 | /// 11 | protected override void Up(MigrationBuilder migrationBuilder) 12 | { 13 | migrationBuilder.AddColumn( 14 | name: "IsMagicLinksEnabled", 15 | table: "Organizations", 16 | type: "bit", 17 | nullable: false, 18 | defaultValue: true); 19 | } 20 | 21 | /// 22 | protected override void Down(MigrationBuilder migrationBuilder) 23 | { 24 | migrationBuilder.DropColumn( 25 | name: "IsMagicLinksEnabled", 26 | table: "Organizations"); 27 | } 28 | } -------------------------------------------------------------------------------- /src/AdminConsole/Migrations/Sqlite/20230809074533_UpdateTableApplications_AddColumnDeleteAt.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | 3 | #nullable disable 4 | 5 | namespace Passwordless.AdminConsole.Migrations.Sqlite; 6 | 7 | /// 8 | public partial class UpdateTableApplications_AddColumnDeleteAt : Migration 9 | { 10 | /// 11 | protected override void Up(MigrationBuilder migrationBuilder) 12 | { 13 | migrationBuilder.AddColumn( 14 | name: "DeleteAt", 15 | table: "Applications", 16 | type: "TEXT", 17 | nullable: true); 18 | } 19 | 20 | /// 21 | protected override void Down(MigrationBuilder migrationBuilder) 22 | { 23 | migrationBuilder.DropColumn( 24 | name: "DeleteAt", 25 | table: "Applications"); 26 | } 27 | } -------------------------------------------------------------------------------- /src/AdminConsole/Migrations/Sqlite/20230818130514_AlterTableOrganizations_AddColumnBecamePaidAt.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | 3 | #nullable disable 4 | 5 | namespace Passwordless.AdminConsole.Migrations.Sqlite; 6 | 7 | /// 8 | public partial class AlterTableOrganizations_AddColumnBecamePaidAt : Migration 9 | { 10 | /// 11 | protected override void Up(MigrationBuilder migrationBuilder) 12 | { 13 | migrationBuilder.AddColumn( 14 | name: "BecamePaidAt", 15 | table: "Organizations", 16 | type: "TEXT", 17 | nullable: true); 18 | } 19 | 20 | /// 21 | protected override void Down(MigrationBuilder migrationBuilder) 22 | { 23 | migrationBuilder.DropColumn( 24 | name: "BecamePaidAt", 25 | table: "Organizations"); 26 | } 27 | } -------------------------------------------------------------------------------- /src/AdminConsole/Migrations/Sqlite/20231006101217_RenameAuditLogToEventLog.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | 3 | #nullable disable 4 | 5 | namespace Passwordless.AdminConsole.Migrations.Sqlite; 6 | 7 | /// 8 | public partial class RenameAuditLogToEventLog : Migration 9 | { 10 | /// 11 | protected override void Up(MigrationBuilder migrationBuilder) 12 | { 13 | 14 | } 15 | 16 | /// 17 | protected override void Down(MigrationBuilder migrationBuilder) 18 | { 19 | 20 | } 21 | } -------------------------------------------------------------------------------- /src/AdminConsole/Migrations/Sqlite/20240716084433_AllowDisablingMagicLinksForOrganizations.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | 3 | #nullable disable 4 | 5 | namespace Passwordless.AdminConsole.Migrations.Sqlite; 6 | 7 | /// 8 | public partial class AllowDisablingMagicLinksForOrganizations : Migration 9 | { 10 | /// 11 | protected override void Up(MigrationBuilder migrationBuilder) 12 | { 13 | migrationBuilder.AddColumn( 14 | name: "IsMagicLinksEnabled", 15 | table: "Organizations", 16 | type: "INTEGER", 17 | nullable: false, 18 | defaultValue: true); 19 | } 20 | 21 | /// 22 | protected override void Down(MigrationBuilder migrationBuilder) 23 | { 24 | migrationBuilder.DropColumn( 25 | name: "IsMagicLinksEnabled", 26 | table: "Organizations"); 27 | } 28 | } -------------------------------------------------------------------------------- /src/AdminConsole/Models/Application.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.AdminConsole.Models; 2 | 3 | public class Application 4 | { 5 | public string Id { get; set; } 6 | public string Name { get; set; } 7 | public string Description { get; set; } 8 | public DateTime CreatedAt { get; set; } 9 | 10 | public int OrganizationId { get; set; } 11 | public Organization Organization { get; set; } 12 | 13 | public string ApiKey { get; set; } 14 | public string ApiSecret { get; set; } 15 | public string ApiUrl { get; set; } 16 | 17 | public virtual Onboarding? Onboarding { get; set; } 18 | public string BillingPlan { get; set; } 19 | public string? BillingSubscriptionItemId { get; set; } 20 | public string? BillingPriceId { get; set; } 21 | 22 | public int CurrentUserCount { get; set; } 23 | 24 | public DateTime? DeleteAt { get; set; } 25 | } -------------------------------------------------------------------------------- /src/AdminConsole/Models/ApplicationFeatureContext.cs: -------------------------------------------------------------------------------- 1 | using Passwordless.Common.Models.Apps; 2 | 3 | namespace Passwordless.AdminConsole.Models; 4 | 5 | public record ApplicationFeatureContext( 6 | bool EventLoggingIsEnabled, 7 | int EventLoggingRetentionPeriod, 8 | DateTime? DeveloperLoggingEndsAt, 9 | long? MaxUsers, 10 | bool AllowAttestation, 11 | bool IsGenerateSignInTokenEndpointEnabled, 12 | bool IsMagicLinksEnabled) 13 | { 14 | public static ApplicationFeatureContext FromDto(AppFeatureResponse dto) => 15 | new( 16 | dto.EventLoggingIsEnabled, 17 | dto.EventLoggingRetentionPeriod, 18 | dto.DeveloperLoggingEndsAt, 19 | dto.MaxUsers, 20 | dto.AllowAttestation, 21 | dto.IsGenerateSignInTokenEndpointEnabled, 22 | dto.IsMagicLinksEnabled); 23 | } -------------------------------------------------------------------------------- /src/AdminConsole/Models/Authenticator.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.AdminConsole.Models; 2 | 3 | public class Authenticator 4 | { 5 | public Guid AaGuid { get; set; } 6 | 7 | public string Name { get; set; } 8 | } -------------------------------------------------------------------------------- /src/AdminConsole/Models/FeaturesContext.cs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitwarden/passwordless-server/6bbabde3f4c9ae09367e275d76f3045c9918bca9/src/AdminConsole/Models/FeaturesContext.cs -------------------------------------------------------------------------------- /src/AdminConsole/Models/Onboarding.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.AdminConsole.Models; 2 | 3 | public class Onboarding 4 | { 5 | public string ApplicationId { get; set; } 6 | public string ApiKey { get; set; } 7 | public string ApiSecret { get; set; } 8 | public DateTime SensitiveInfoExpireAt { get; set; } 9 | } -------------------------------------------------------------------------------- /src/AdminConsole/Models/Organization.cs: -------------------------------------------------------------------------------- 1 | using Passwordless.AdminConsole.Identity; 2 | 3 | namespace Passwordless.AdminConsole.Models; 4 | 5 | public class Organization 6 | { 7 | public int Id { get; set; } 8 | public string Name { get; set; } 9 | public DateTime CreatedAt { get; set; } 10 | public OrganizationType InfoOrgType { get; set; } 11 | public UseCaseType InfoUseCase { get; set; } 12 | 13 | public virtual IEnumerable Admins { get; set; } 14 | 15 | public List Applications { get; set; } 16 | public string? BillingCustomerId { get; set; } 17 | public string? BillingSubscriptionId { get; set; } 18 | public DateTime? BecamePaidAt { get; set; } 19 | 20 | public bool HasSubscription => !string.IsNullOrEmpty(BillingSubscriptionId); 21 | public int MaxApplications { get; set; } = 1; 22 | public int MaxAdmins { get; set; } = 1; 23 | public bool IsMagicLinksEnabled { get; set; } = true; 24 | } -------------------------------------------------------------------------------- /src/AdminConsole/Models/OrganizationFeaturesContext.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.AdminConsole.Models; 2 | 3 | public record OrganizationFeaturesContext( 4 | bool EventLoggingIsEnabled, 5 | int EventLoggingRetentionPeriod); -------------------------------------------------------------------------------- /src/AdminConsole/Models/OrganizationType.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.Serialization; 2 | 3 | namespace Passwordless.AdminConsole.Models; 4 | 5 | public enum OrganizationType 6 | { 7 | [EnumMember(Value = "personal")] 8 | Personal = 1, 9 | 10 | [EnumMember(Value = "company")] 11 | Company = 2, 12 | } -------------------------------------------------------------------------------- /src/AdminConsole/Models/UseCaseType.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.Serialization; 2 | 3 | namespace Passwordless.AdminConsole.Models; 4 | 5 | public enum UseCaseType 6 | { 7 | [EnumMember(Value = "customers")] 8 | Customers, 9 | 10 | [EnumMember(Value = "employees")] 11 | Employees, 12 | 13 | [EnumMember(Value = "both")] 14 | Both 15 | } -------------------------------------------------------------------------------- /src/AdminConsole/Pages/BaseExtendedPageModel.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using Microsoft.AspNetCore.Mvc.RazorPages; 3 | using Passwordless.AdminConsole.RoutingHelpers; 4 | 5 | namespace Passwordless.AdminConsole.Pages; 6 | 7 | public abstract class BaseExtendedPageModel : PageModel 8 | { 9 | public RedirectToPageResult RedirectToApplicationPage(string? pageName, ApplicationPageRoutingContext routeValues) 10 | => RedirectToPage(pageName, pageHandler: null, routeValues: routeValues, fragment: null); 11 | } -------------------------------------------------------------------------------- /src/AdminConsole/Pages/Index.cshtml.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using Microsoft.AspNetCore.Mvc.RazorPages; 3 | using Passwordless.AdminConsole.Services; 4 | 5 | namespace Passwordless.AdminConsole.Pages; 6 | 7 | public class IndexModel(ILogger logger, ISetupService setupService) : PageModel 8 | { 9 | public async Task OnGetAsync() 10 | { 11 | if (await setupService.HasSetupCompletedAsync()) 12 | { 13 | if (HttpContext.User.Identity!.IsAuthenticated) 14 | { 15 | return RedirectToPage("/account/login"); 16 | } 17 | 18 | return Redirect("/organization/overview"); 19 | } 20 | 21 | logger.LogInformation("Database has not been initialized. Starting initialization process."); 22 | 23 | return Redirect("/Initialize"); 24 | } 25 | } -------------------------------------------------------------------------------- /src/AdminConsole/Pages/Organization/JoinBusy.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @using Passwordless.AdminConsole.Components.Shared.Forms 3 | @model JoinBusy 4 | 5 | @{ 6 | ViewBag.Title = "It seems like you're already part of an organization"; 7 | } 8 |
9 |

Hi there! We noticed that you're already logged in and part of an organization.

10 |

If you want to join a new one, please open your invite in a different browser or sign out

11 | 12 | 13 |
14 | -------------------------------------------------------------------------------- /src/AdminConsole/Pages/Organization/JoinBusy.cshtml.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using Microsoft.AspNetCore.Mvc.RazorPages; 3 | 4 | namespace Passwordless.AdminConsole.Pages.Organization; 5 | 6 | public class JoinBusy : PageModel 7 | { 8 | public string InviteLink { get; set; } = string.Empty; 9 | 10 | public void OnGet(string code) 11 | { 12 | InviteLink = Url.PageLink("Join", null, new { code = code }) ?? string.Empty; 13 | } 14 | } -------------------------------------------------------------------------------- /src/AdminConsole/Pages/Shared/_Error.cshtml: -------------------------------------------------------------------------------- 1 | @model Passwordless.AdminConsole.TagHelpers.FeedbackModel 2 | 3 |
4 |
5 |
6 | 7 |
8 |
9 |

@Model.Message

10 |

11 | 12 | @Model.LinkText 13 | 14 | 15 |

16 |
17 |
18 |
-------------------------------------------------------------------------------- /src/AdminConsole/Pages/Shared/_Ok.cshtml: -------------------------------------------------------------------------------- 1 | @model Passwordless.AdminConsole.TagHelpers.FeedbackModel 2 | 3 |
4 |
5 |
6 | 7 |
8 |
9 |

@Model.Message

10 |
11 |
12 |
-------------------------------------------------------------------------------- /src/AdminConsole/Pages/_ViewImports.cshtml: -------------------------------------------------------------------------------- 1 | @namespace Passwordless.AdminConsole.Pages 2 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers 3 | @addTagHelper *, Passwordless.AdminConsole 4 | @addTagHelper *, Passwordless.AdminConsole.Components 5 | 6 | // Razor Components 7 | @using Passwordless.AdminConsole.Components 8 | @using Passwordless.AdminConsole.Components.Shared 9 | @using Passwordless.AdminConsole.Components.Shared.Icons 10 | @using Passwordless.AdminConsole.Components.Shared.Links 11 | 12 | // Razor Pages 13 | @using Passwordless.AdminConsole.TagHelpers 14 | 15 | @using Microsoft.AspNetCore.Components -------------------------------------------------------------------------------- /src/AdminConsole/Pages/_ViewStart.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | Layout = "_Layout"; 3 | } 4 | 5 | -------------------------------------------------------------------------------- /src/AdminConsole/RoutingHelpers/ApplicationPageRoutingContext.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.AdminConsole.RoutingHelpers; 2 | 3 | public record ApplicationPageRoutingContext(string AppId); -------------------------------------------------------------------------------- /src/AdminConsole/Services/AuthenticatorData/AuthenticatorDataDto.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.AdminConsole.Services.AuthenticatorData; 2 | 3 | public class AuthenticatorDataDto : Dictionary 4 | { 5 | } -------------------------------------------------------------------------------- /src/AdminConsole/Services/AuthenticatorData/AuthenticatorDataItemDto.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace Passwordless.AdminConsole.Services.AuthenticatorData; 4 | 5 | public class AuthenticatorDataItemDto 6 | { 7 | [JsonPropertyName("name")] 8 | public string Name { get; set; } 9 | } -------------------------------------------------------------------------------- /src/AdminConsole/Services/AuthenticatorData/AuthenticatorDataJsonSerializerContext.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace Passwordless.AdminConsole.Services.AuthenticatorData; 4 | 5 | [JsonSerializable(typeof(AuthenticatorDataDto))] 6 | [JsonSerializable(typeof(AuthenticatorDataItemDto))] 7 | internal partial class AuthenticatorDataJsonSerializerContext : JsonSerializerContext 8 | { 9 | } -------------------------------------------------------------------------------- /src/AdminConsole/Services/AuthenticatorData/IAuthenticatorDataProvider.cs: -------------------------------------------------------------------------------- 1 | using Passwordless.AdminConsole.Models; 2 | 3 | namespace Passwordless.AdminConsole.Services.AuthenticatorData; 4 | 5 | public interface IAuthenticatorDataProvider 6 | { 7 | IReadOnlyCollection Authenticators { get; } 8 | 9 | string? GetName(Guid aaGuid); 10 | } -------------------------------------------------------------------------------- /src/AdminConsole/Services/IAdminService.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.AdminConsole.Services; 2 | 3 | public interface IAdminService 4 | { 5 | Task CanDisableMagicLinksAsync(); 6 | } -------------------------------------------------------------------------------- /src/AdminConsole/Services/IApplicationService.cs: -------------------------------------------------------------------------------- 1 | using Passwordless.AdminConsole.Models; 2 | using Passwordless.Common.Models.Apps; 3 | 4 | namespace Passwordless.AdminConsole.Services; 5 | 6 | public interface IApplicationService 7 | { 8 | Task CanDeleteApplicationImmediatelyAsync(string applicationId); 9 | Task MarkDeleteApplicationAsync(string applicationId, string userName); 10 | Task CancelDeletionForApplicationAsync(string applicationId); 11 | Task GetOnboardingAsync(string applicationId); 12 | Task DeleteAsync(string applicationId); 13 | Task CreateApplicationAsync(Application application); 14 | } -------------------------------------------------------------------------------- /src/AdminConsole/Services/IDataService.cs: -------------------------------------------------------------------------------- 1 | using Passwordless.AdminConsole.Identity; 2 | using Passwordless.AdminConsole.Models; 3 | 4 | namespace Passwordless.AdminConsole.Services; 5 | 6 | public interface IDataService 7 | { 8 | Task> GetApplicationsAsync(); 9 | Task GetOrganizationAsync(); 10 | Task GetOrganizationAsync(int id); 11 | Task UpdateOrganizationSecurityAsync(bool isMagicLinksEnabled); 12 | Task AllowedToCreateApplicationAsync(); 13 | Task CanInviteAdminAsync(); 14 | Task GetOrganizationWithDataAsync(); 15 | Task> GetConsoleAdminsAsync(); 16 | Task GetUserAsync(); 17 | Task DeleteOrganizationAsync(); 18 | Task GetApplicationAsync(string applicationId); 19 | Task CanConnectAsync(); 20 | Task CleanUpOnboardingAsync(); 21 | Task CreateOrganizationAsync(Organization organization); 22 | } -------------------------------------------------------------------------------- /src/AdminConsole/Services/IInvitationService.cs: -------------------------------------------------------------------------------- 1 | using Passwordless.AdminConsole.Identity; 2 | 3 | namespace Passwordless.AdminConsole.Services; 4 | 5 | public interface IInvitationService 6 | { 7 | Task SendInviteAsync(string toEmail, int targetOrgId, string targetOrgName, string fromEmail, string fromName); 8 | Task> GetInvitesAsync(int orgId); 9 | Task CancelInviteAsync(Invite inviteToCancel); 10 | Task GetInviteFromRawCodeAsync(string code); 11 | Task ConsumeInviteAsync(Invite inv); 12 | } -------------------------------------------------------------------------------- /src/AdminConsole/Services/IOrganizationFeatureService.cs: -------------------------------------------------------------------------------- 1 | using Passwordless.AdminConsole.Models; 2 | 3 | namespace Passwordless.AdminConsole.Services; 4 | 5 | public interface IOrganizationFeatureService 6 | { 7 | OrganizationFeaturesContext GetOrganizationFeatures(int orgId); 8 | } -------------------------------------------------------------------------------- /src/AdminConsole/Services/IPostSignInHandlerService.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.AdminConsole.Services; 2 | 3 | public interface IPostSignInHandlerService 4 | { 5 | Task HandleAsync(int organizationId); 6 | } -------------------------------------------------------------------------------- /src/AdminConsole/Services/ISetupService.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.AdminConsole.Services; 2 | 3 | public interface ISetupService 4 | { 5 | Task HasSetupCompletedAsync(); 6 | } -------------------------------------------------------------------------------- /src/AdminConsole/Services/IUsageService.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.AdminConsole.Services; 2 | 3 | public interface IUsageService 4 | { 5 | Task UpdateUsersCountAsync(); 6 | } -------------------------------------------------------------------------------- /src/AdminConsole/Services/InvoiceModel.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.AdminConsole.Services; 2 | 3 | public class InvoiceModel 4 | { 5 | /// 6 | /// The invoice number 7 | /// 8 | public string Number { get; set; } 9 | 10 | /// 11 | /// The invoice date 12 | /// 13 | public DateTime Date { get; set; } 14 | 15 | /// 16 | /// The download link for the invoice 17 | /// 18 | public string Pdf { get; set; } 19 | 20 | public string Amount { get; set; } 21 | 22 | public bool Paid { get; set; } 23 | } -------------------------------------------------------------------------------- /src/AdminConsole/Services/MagicLinks/IMagicLinkBuilder.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.AdminConsole.Services.MagicLinks; 2 | 3 | public interface IMagicLinkBuilder 4 | { 5 | Task GetLinkAsync(string email, string? returnUrl = null); 6 | string GetUrlTemplate(string? returnUrl = null); 7 | } -------------------------------------------------------------------------------- /src/AdminConsole/Services/Mail/IMailService.cs: -------------------------------------------------------------------------------- 1 | using Passwordless.AdminConsole.Identity; 2 | using Passwordless.AdminConsole.Models; 3 | 4 | namespace Passwordless.AdminConsole.Services.Mail; 5 | 6 | /// 7 | /// Used to generate a mail message from input and will call the mail provider to send the message 8 | /// 9 | public interface IMailService 10 | { 11 | Task SendInviteAsync(Invite inv, string link); 12 | Task SendEmailIsAlreadyInUseAsync(string email); 13 | Task SendMagicLinksDisabledAsync(string organizationName, string email); 14 | Task SendOrganizationDeletedAsync(string organizationName, IEnumerable emails, string deletedBy, DateTime deletedAt); 15 | Task SendApplicationDeletedAsync(Application application, DateTime deletedAt, string deletedBy, ICollection emails); 16 | Task SendApplicationToBeDeletedAsync(Application application, string deletedBy, string cancellationLink, ICollection emails); 17 | } -------------------------------------------------------------------------------- /src/AdminConsole/Services/PasswordlessManagement/PasswordlessManagementOptions.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace Passwordless.AdminConsole.Services.PasswordlessManagement; 4 | 5 | public class PasswordlessManagementOptions 6 | { 7 | [Required] 8 | public string ManagementKey { get; set; } 9 | 10 | /// 11 | /// The client is not aware of any networking inside our container when using self-hosting. So we need to provide the external URL. 12 | /// 13 | [Required] 14 | public string ApiUrl { get; set; } 15 | } -------------------------------------------------------------------------------- /src/AdminConsole/TagHelpers/AlertBoxes/DangerAlertBox.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Razor.TagHelpers; 2 | using Passwordless.AdminConsole.TagHelpers.Icons; 3 | 4 | namespace Passwordless.AdminConsole.TagHelpers.AlertBoxes; 5 | 6 | [HtmlTargetElement("danger-alert-box")] 7 | public sealed class DangerAlertBox : BaseAlertBox 8 | { 9 | public DangerAlertBox() 10 | { 11 | Variant = ColorVariant.Danger; 12 | IconTag = DangerAlertIcon.HtmlTag; 13 | } 14 | } -------------------------------------------------------------------------------- /src/AdminConsole/TagHelpers/AlertBoxes/InfoAlertBox.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Razor.TagHelpers; 2 | using Passwordless.AdminConsole.TagHelpers.Icons; 3 | 4 | namespace Passwordless.AdminConsole.TagHelpers.AlertBoxes; 5 | 6 | [HtmlTargetElement("info-alert-box")] 7 | public sealed class InfoAlertBox : BaseAlertBox 8 | { 9 | public InfoAlertBox() 10 | { 11 | Variant = ColorVariant.Info; 12 | IconTag = InfoAlertIcon.HtmlTag; 13 | } 14 | } -------------------------------------------------------------------------------- /src/AdminConsole/TagHelpers/AlertBoxes/SuccessAlertBox.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Razor.TagHelpers; 2 | using Passwordless.AdminConsole.TagHelpers.Icons; 3 | 4 | namespace Passwordless.AdminConsole.TagHelpers.AlertBoxes; 5 | 6 | [HtmlTargetElement("success-alert-box")] 7 | public sealed class SuccessAlertBox : BaseAlertBox 8 | { 9 | public SuccessAlertBox() 10 | { 11 | Variant = ColorVariant.Success; 12 | IconTag = SuccessAlertIcon.HtmlTag; 13 | } 14 | } -------------------------------------------------------------------------------- /src/AdminConsole/TagHelpers/AlertBoxes/WarningAlertBox.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Razor.TagHelpers; 2 | using Passwordless.AdminConsole.TagHelpers.Icons; 3 | 4 | namespace Passwordless.AdminConsole.TagHelpers.AlertBoxes; 5 | 6 | [HtmlTargetElement("warning-alert-box")] 7 | public sealed class WarningAlertBox : BaseAlertBox 8 | { 9 | public WarningAlertBox() 10 | { 11 | Variant = ColorVariant.Warning; 12 | IconTag = WarningAlertIcon.HtmlTag; 13 | } 14 | } -------------------------------------------------------------------------------- /src/AdminConsole/TagHelpers/Card.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Razor.TagHelpers; 2 | 3 | namespace Passwordless.AdminConsole.TagHelpers; 4 | 5 | [HtmlTargetElement("card")] 6 | public class Card : TagHelper 7 | { 8 | public string Title { get; set; } 9 | 10 | public string Description { get; set; } 11 | 12 | public override void Process(TagHelperContext context, TagHelperOutput output) 13 | { 14 | output.TagName = "div"; 15 | output.Attributes.Add("class", 16 | "relative flex rounded-lg border bg-white p-4 shadow-sm focus:outline-none flex flex-col space-y-1"); 17 | 18 | var content = 19 | $""" 20 | {Title} 21 | {Description} 22 | """; 23 | 24 | output.Content.SetHtmlContent(content); 25 | } 26 | } -------------------------------------------------------------------------------- /src/AdminConsole/TagHelpers/ColorVariant.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.AdminConsole.TagHelpers; 2 | 3 | public enum ColorVariant 4 | { 5 | Primary, 6 | Success, 7 | Danger, 8 | Info, 9 | Warning 10 | } -------------------------------------------------------------------------------- /src/AdminConsole/TagHelpers/Extensions/TagHelperExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Razor.TagHelpers; 2 | 3 | namespace Passwordless.AdminConsole.TagHelpers.Extensions; 4 | 5 | public static class TagHelperExtensions 6 | { 7 | /** 8 | * Renders the specified tag helper as HTML. 9 | */ 10 | public static string RenderHtml(this TagHelper tagHelper, TagHelperContext context) 11 | { 12 | if (context == null) throw new ArgumentNullException(nameof(context)); 13 | 14 | var output = new TagHelperOutput( 15 | string.Empty, 16 | new TagHelperAttributeList(), 17 | (useCachedResult, encoder) => 18 | Task.Factory.StartNew( 19 | () => new DefaultTagHelperContent() 20 | )); 21 | 22 | tagHelper.Process(context, output); 23 | return output.RenderHtml(); 24 | } 25 | } -------------------------------------------------------------------------------- /src/AdminConsole/TagHelpers/Extensions/TagOutputHelperExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Razor.TagHelpers; 2 | 3 | namespace Passwordless.AdminConsole.TagHelpers.Extensions; 4 | 5 | public static class TagHelperOutputExtensions 6 | { 7 | /** 8 | * Converts the processed TagHelperOutput to a valid HTML tag. 9 | */ 10 | public static string RenderHtml(this TagHelperOutput output) 11 | { 12 | if (output == null) 13 | { 14 | throw new ArgumentNullException(nameof(output)); 15 | } 16 | 17 | var content = output.Content.GetContent(); 18 | return $"<{output.TagName} {output.Attributes.ToHtmlOutput()}>{content}"; 19 | } 20 | } -------------------------------------------------------------------------------- /src/AdminConsole/TagHelpers/Icons/DangerAlertIcon.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Razor.TagHelpers; 2 | 3 | namespace Passwordless.AdminConsole.TagHelpers.Icons; 4 | 5 | [HtmlTargetElement(HtmlTag)] 6 | public sealed class DangerAlertIcon : BaseAlertIcon 7 | { 8 | public const string HtmlTag = "danger-alert-icon"; 9 | 10 | public DangerAlertIcon() 11 | { 12 | Variant = ColorVariant.Danger; 13 | } 14 | 15 | public override void Process(TagHelperContext context, TagHelperOutput output) 16 | { 17 | base.Process(context, output); 18 | output.Content.AppendHtml(@""); 19 | } 20 | } -------------------------------------------------------------------------------- /src/AdminConsole/TagHelpers/Icons/InfoAlertIcon.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Razor.TagHelpers; 2 | 3 | namespace Passwordless.AdminConsole.TagHelpers.Icons; 4 | 5 | [HtmlTargetElement(HtmlTag)] 6 | public sealed class InfoAlertIcon : BaseAlertIcon 7 | { 8 | public const string HtmlTag = "info-alert-icon"; 9 | 10 | public InfoAlertIcon() 11 | { 12 | Variant = ColorVariant.Info; 13 | } 14 | 15 | public override void Process(TagHelperContext context, TagHelperOutput output) 16 | { 17 | base.Process(context, output); 18 | output.Content.AppendHtml(@""); 19 | } 20 | } -------------------------------------------------------------------------------- /src/AdminConsole/TagHelpers/Icons/SuccessAlertIcon.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Razor.TagHelpers; 2 | 3 | namespace Passwordless.AdminConsole.TagHelpers.Icons; 4 | 5 | [HtmlTargetElement(HtmlTag)] 6 | public sealed class SuccessAlertIcon : BaseAlertIcon 7 | { 8 | public const string HtmlTag = "success-alert-icon"; 9 | 10 | public SuccessAlertIcon() 11 | { 12 | Variant = ColorVariant.Success; 13 | } 14 | 15 | public override void Process(TagHelperContext context, TagHelperOutput output) 16 | { 17 | base.Process(context, output); 18 | output.Content.AppendHtml(@""); 19 | } 20 | } -------------------------------------------------------------------------------- /src/AdminConsole/TagHelpers/Icons/WarningAlertIcon.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Razor.TagHelpers; 2 | 3 | namespace Passwordless.AdminConsole.TagHelpers.Icons; 4 | 5 | [HtmlTargetElement(HtmlTag)] 6 | public sealed class WarningAlertIcon : BaseAlertIcon 7 | { 8 | public const string HtmlTag = "warning-alert-icon"; 9 | 10 | public WarningAlertIcon() 11 | { 12 | Variant = ColorVariant.Warning; 13 | } 14 | 15 | public override void Process(TagHelperContext context, TagHelperOutput output) 16 | { 17 | base.Process(context, output); 18 | output.Content.AppendHtml(@""); 19 | } 20 | } -------------------------------------------------------------------------------- /src/AdminConsole/TagHelpers/Panel.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Razor.TagHelpers; 2 | 3 | namespace Passwordless.AdminConsole.TagHelpers; 4 | 5 | [HtmlTargetElement("panel")] 6 | public class Panel : TagHelper 7 | { 8 | [HtmlAttributeName("header")] 9 | public string Header { get; set; } 10 | 11 | public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output) 12 | { 13 | output.TagName = "div"; 14 | output.Attributes.SetAttribute("class", "panel"); 15 | output.Content.AppendHtml($"

{Header}

"); 16 | 17 | var content = await output.GetChildContentAsync(); 18 | output.Content.AppendHtml($"
{content.GetContent()}
"); 19 | } 20 | } -------------------------------------------------------------------------------- /src/AdminConsole/TagHelpers/ScriptTag.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Razor.TagHelpers; 2 | 3 | namespace Passwordless.AdminConsole.TagHelpers; 4 | 5 | [HtmlTargetElement("script", Attributes = "")] 6 | public class ScriptTagNonce : TagHelper 7 | { 8 | private readonly IHttpContextAccessor _accessor; 9 | 10 | public ScriptTagNonce(IHttpContextAccessor accessor) 11 | { 12 | _accessor = accessor; 13 | } 14 | public override void Process(TagHelperContext context, TagHelperOutput output) 15 | { 16 | if (_accessor.HttpContext?.Items["csp-nonce"] is string nonce) 17 | { 18 | output.Attributes.Add("nonce", nonce); 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /src/AdminConsole/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "DetailedErrors": true, 3 | "Logging": { 4 | "LogLevel": { 5 | "Default": "Information", 6 | "Microsoft.AspNetCore": "Warning" 7 | } 8 | }, 9 | "ConnectionStrings": { 10 | "sqlite:admin": "Data Source=adminconsole_dev.db" 11 | }, 12 | "Mail": { 13 | "From": "hello@maila.passwordless.dev", 14 | "FromName": "Bitwarden Passwordless.dev", 15 | "Providers": [ 16 | { 17 | "Name": "file", 18 | "Path": "../mail.md" 19 | } 20 | ] 21 | }, 22 | "Passwordless": { 23 | "ApiUrl": "http://localhost:7001", 24 | "ApiSecret": "test:secret:a679563b331846c79c20b114a4f56d02", 25 | "ApiKey": "test:public:2e728aa5986f4ba8b073a5b28a939795" 26 | }, 27 | "PasswordlessManagement": { 28 | "ApiUrl": "http://localhost:7001", 29 | "ManagementKey": "shared_dev_key" 30 | } 31 | } -------------------------------------------------------------------------------- /src/AdminConsole/datadog.json: -------------------------------------------------------------------------------- 1 | { 2 | "DD_SITE": "datadoghq.eu", 3 | "DD_ENV": "pass-any", 4 | "DD_VERSION": "0.0.0", 5 | "DD_LOGS_INJECTION": true, 6 | "DD_TRACE_HEADER_TAGS": "Client-Version" 7 | } -------------------------------------------------------------------------------- /src/AdminConsole/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "playground", 3 | "version": "0.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "tw:watch": "tailwindcss -i ./Styles/tailwind.css -o ./wwwroot/css/tailwind.css --watch", 8 | "build": "npx tailwindcss -i ./Styles/tailwind.css -o ./wwwroot/css/tailwind.css" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "@passwordlessdev/passwordless-client": "1.2.2", 15 | "apexcharts": "4.4.0", 16 | "es-module-shims": "2.0.9", 17 | "vue": "3.5.13" 18 | }, 19 | "devDependencies": { 20 | "@tailwindcss/aspect-ratio": "0.4.2", 21 | "@tailwindcss/forms": "0.5.10", 22 | "@tailwindcss/typography": "0.5.16", 23 | "autoprefixer": "10.4.20", 24 | "postcss": "8.5.1", 25 | "postcss-cli": "11.0.0", 26 | "tailwindcss": "3.4.17" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/AdminConsole/wwwroot/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitwarden/passwordless-server/6bbabde3f4c9ae09367e275d76f3045c9918bca9/src/AdminConsole/wwwroot/android-chrome-192x192.png -------------------------------------------------------------------------------- /src/AdminConsole/wwwroot/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitwarden/passwordless-server/6bbabde3f4c9ae09367e275d76f3045c9918bca9/src/AdminConsole/wwwroot/android-chrome-512x512.png -------------------------------------------------------------------------------- /src/AdminConsole/wwwroot/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitwarden/passwordless-server/6bbabde3f4c9ae09367e275d76f3045c9918bca9/src/AdminConsole/wwwroot/apple-touch-icon.png -------------------------------------------------------------------------------- /src/AdminConsole/wwwroot/css/site.css: -------------------------------------------------------------------------------- 1 | [v-cloak] { 2 | display: none; 3 | } 4 | -------------------------------------------------------------------------------- /src/AdminConsole/wwwroot/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitwarden/passwordless-server/6bbabde3f4c9ae09367e275d76f3045c9918bca9/src/AdminConsole/wwwroot/favicon-16x16.png -------------------------------------------------------------------------------- /src/AdminConsole/wwwroot/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitwarden/passwordless-server/6bbabde3f4c9ae09367e275d76f3045c9918bca9/src/AdminConsole/wwwroot/favicon-32x32.png -------------------------------------------------------------------------------- /src/AdminConsole/wwwroot/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitwarden/passwordless-server/6bbabde3f4c9ae09367e275d76f3045c9918bca9/src/AdminConsole/wwwroot/favicon.ico -------------------------------------------------------------------------------- /src/AdminConsole/wwwroot/humans.txt: -------------------------------------------------------------------------------- 1 | Hey Human! We hope you have a great day. 2 | Passwordless.dev was founded by Anders Åberg and is maintained by the Passwordless team at Bitwarden. 3 | Hiring: https://bitwarden.com/careers/#open-positions 4 | -------------------------------------------------------------------------------- /src/AdminConsole/wwwroot/js/site.js: -------------------------------------------------------------------------------- 1 | // Please see documentation at https://docs.microsoft.com/aspnet/core/client-side/bundling-and-minification 2 | // for details on configuring this project to bundle and minify static web assets. 3 | 4 | // Write your JavaScript code. 5 | 6 | -------------------------------------------------------------------------------- /src/AdminConsole/wwwroot/security.txt: -------------------------------------------------------------------------------- 1 | # Hey security researchers. Information related to reporting security vulnerabilities of this site. 2 | Contact: https://bitwarden.com/contact/ 3 | Preferred-Languages: en 4 | Hiring: https://bitwarden.com/careers/#open-positions 5 | -------------------------------------------------------------------------------- /src/AdminConsole/wwwroot/site.webmanifest: -------------------------------------------------------------------------------- 1 | {"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} -------------------------------------------------------------------------------- /src/Api/.gitignore: -------------------------------------------------------------------------------- 1 | /.mds-cache/**.* -------------------------------------------------------------------------------- /src/Api/Authorization/Constants.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.Api.Authorization; 2 | 3 | public static class Constants 4 | { 5 | public const string PublicKeyAuthenticationScheme = "ApiKey"; 6 | public const string SecretKeyAuthenticationScheme = "ApiSecret"; 7 | public const string ManagementKeyAuthenticationScheme = "ManagementKey"; 8 | 9 | public const string PublicKeyType = "public"; 10 | public const string SecretKeyType = "secret"; 11 | public const string ManagementKeyType = "management"; 12 | 13 | public const string PublicKeyHeaderName = "ApiKey"; 14 | public const string SecretKeyHeaderName = "ApiSecret"; 15 | public const string ManagementKeyHeaderName = "ManagementKey"; 16 | } 17 | 18 | public static class CustomClaimTypes 19 | { 20 | public const string AccountName = "accountName"; 21 | public const string KeyType = "keyType"; 22 | public const string Scopes = "scopes"; 23 | } -------------------------------------------------------------------------------- /src/Api/Authorization/IProblemDetailWriter.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.Api.Authorization; 2 | 3 | public interface IProblemDetailWriter 4 | { 5 | IEnumerable GetDetails(HttpContext context, string headerName); 6 | } -------------------------------------------------------------------------------- /src/Api/Authorization/ManagementOptions.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.Api.Authorization; 2 | 3 | public class ManagementOptions 4 | { 5 | public string ManagementKey { get; set; } 6 | } -------------------------------------------------------------------------------- /src/Api/Email/DispatchedEmailCleanupService.cs: -------------------------------------------------------------------------------- 1 | using Passwordless.Common.Background; 2 | using Passwordless.Service.Storage.Ef; 3 | 4 | namespace Passwordless.Api.Email; 5 | 6 | public class DispatchedEmailCleanupService( 7 | TimeProvider timeProvider, 8 | ILogger logger, 9 | IServiceProvider serviceProvider) 10 | : BasePeriodicBackgroundService( 11 | new TimeOnly(00, 00, 00), 12 | TimeSpan.FromDays(1), 13 | timeProvider, logger) 14 | { 15 | protected override async Task DoWorkAsync(CancellationToken cancellationToken) 16 | { 17 | using var scope = serviceProvider.CreateScope(); 18 | var storage = scope.ServiceProvider.GetRequiredService(); 19 | 20 | // We only need 30 days worth of emails, but let's keep a small extra buffer just in case 21 | await storage.DeleteOldDispatchedEmailsAsync(TimeSpan.FromDays(50)); 22 | } 23 | } -------------------------------------------------------------------------------- /src/Api/Endpoints/Health.cs: -------------------------------------------------------------------------------- 1 | using Passwordless.Service.Helpers; 2 | 3 | namespace Passwordless.Api.Endpoints; 4 | 5 | public static class HealthEndpoints 6 | { 7 | public static void MapHealthEndpoints(this WebApplication app) 8 | { 9 | app.MapGet("health/throw/api", (ctx) => throw new ApiException("test_error", "Testing error response", 400)); 10 | app.MapGet("health/throw/exception", (ctx) => throw new Exception("Testing error response", new Exception("Inner exception"))); 11 | } 12 | } -------------------------------------------------------------------------------- /src/Api/Extensions/HeaderDictionaryExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.Api.Extensions; 2 | 3 | public static class HeaderDictionaryExtensions 4 | { 5 | public static string? GetApiSecret(this IHeaderDictionary headerDictionary) => headerDictionary.GetHeaderValue("ApiSecret"); 6 | 7 | public static string? GetPublicApiKey(this IHeaderDictionary headerDictionary) => headerDictionary.GetHeaderValue("ApiKey"); 8 | 9 | private static string? GetHeaderValue(this IHeaderDictionary headerDictionary, string key) 10 | { 11 | headerDictionary.TryGetValue(key, out var value); 12 | return value.SingleOrDefault(); 13 | } 14 | } -------------------------------------------------------------------------------- /src/Api/Extensions/MagicLinkBootstrap.cs: -------------------------------------------------------------------------------- 1 | using Passwordless.Service.MagicLinks; 2 | 3 | namespace Passwordless.Api.Extensions; 4 | 5 | public static class MagicLinkBootstrap 6 | { 7 | public static void AddMagicLinks(this WebApplicationBuilder builder) 8 | { 9 | builder.Services.AddOptions().BindConfiguration("MagicLinks"); 10 | builder.Services.AddScoped(); 11 | } 12 | } -------------------------------------------------------------------------------- /src/Api/Helpers/IRequestContext.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.Api.Helpers; 2 | 3 | public interface IRequestContext 4 | { 5 | string GetBaseUrl(); 6 | } -------------------------------------------------------------------------------- /src/Api/Helpers/LoggerExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.Api.Helpers; 2 | 3 | public static partial class LoggerExtensions 4 | { 5 | [LoggerMessage(10000, LogLevel.Error, "The API has encountered an error")] 6 | public static partial void UncaughtException(this ILogger logger, Exception exception); 7 | 8 | [LoggerMessage(10001, LogLevel.Warning, "Uncaught API Exception")] 9 | public static partial void UncaughtApiException(this ILogger logger, Exception exception); 10 | } -------------------------------------------------------------------------------- /src/Api/Helpers/RequestContext.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | 3 | namespace Passwordless.Api.Helpers; 4 | 5 | public class RequestContext : IRequestContext 6 | { 7 | private readonly IHttpContextAccessor _httpContextAccessor; 8 | 9 | public RequestContext(IHttpContextAccessor httpContextAccessor) 10 | { 11 | _httpContextAccessor = httpContextAccessor; 12 | } 13 | 14 | public string GetBaseUrl() 15 | { 16 | if (_httpContextAccessor.HttpContext == null) 17 | { 18 | throw new InvalidOperationException(); 19 | } 20 | var request = _httpContextAccessor.HttpContext.Request; 21 | var uriBuilder = new StringBuilder(); 22 | uriBuilder.Append(request.Scheme); 23 | uriBuilder.Append("://"); 24 | uriBuilder.Append(request.Host.Value); 25 | return uriBuilder.ToString(); 26 | } 27 | } -------------------------------------------------------------------------------- /src/Api/Helpers/SecurityHeaders.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.Api.Helpers; 2 | 3 | public static class SecurityHeaders 4 | { 5 | public static IApplicationBuilder UseSecurityHeaders(this IApplicationBuilder app) 6 | { 7 | app.Use((context, next) => 8 | { 9 | const string csp = "default-src 'self';"; 10 | context.Response.Headers.Append("Content-Security-Policy", new[] { csp }); 11 | context.Response.Headers.Append("X-Content-Type-Options", new[] { "nosniff" }); 12 | context.Response.Headers.Append("Referrer-Policy", new[] { "no-referrer" }); 13 | return next(); 14 | }); 15 | 16 | return app; 17 | } 18 | } -------------------------------------------------------------------------------- /src/Api/Models/CredentialsDeleteDTO.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | using Fido2NetLib; 3 | 4 | namespace Passwordless.Api.Models; 5 | 6 | public class CredentialsDeleteDTO 7 | { 8 | // TODO: Add Base64Url converter 9 | [JsonConverter(typeof(Base64UrlConverter))] 10 | public byte[] CredentialId { get; set; } = Array.Empty(); 11 | } -------------------------------------------------------------------------------- /src/Api/Models/CredentialsListDTO.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.Api.Models; 2 | 3 | public class CredentialsListDTO 4 | { 5 | public string UserId { get; set; } = ""; 6 | } -------------------------------------------------------------------------------- /src/Api/Models/ListResponse.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.Api.Models; 2 | 3 | public static class ListResponse 4 | { 5 | public static ListResponse Create(IEnumerable value) 6 | { 7 | return new ListResponse 8 | { 9 | Values = value 10 | }; 11 | } 12 | } 13 | 14 | public class ListResponse : ResponseBase 15 | { 16 | public IEnumerable Values { get; set; } 17 | } -------------------------------------------------------------------------------- /src/Api/Models/ResponseBase.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.Api.Models; 2 | 3 | public class ResponseBase 4 | { 5 | } -------------------------------------------------------------------------------- /src/Api/OpenApi/Extensions/OpenApiParameterListExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.OpenApi.Models; 2 | 3 | namespace Passwordless.Api.OpenApi.Extensions; 4 | 5 | public static class OpenApiParameterListExtensions 6 | { 7 | public static OpenApiParameter Get(this IList parameters, string name) 8 | { 9 | return parameters.Single(x => x.Name == name); 10 | } 11 | } -------------------------------------------------------------------------------- /src/Api/OpenApi/ExternalDocsAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.Api.OpenApi; 2 | 3 | /// 4 | /// Specifies the URL for the external documentation for this endpoint. 5 | /// 6 | [AttributeUsage(AttributeTargets.Method)] 7 | public class ExternalDocsAttribute(string url) : Attribute 8 | { 9 | /// 10 | /// External documentation URL. 11 | /// 12 | public string Url { get; } = url; 13 | 14 | /// 15 | /// Link description. 16 | /// 17 | public string? Description { get; init; } 18 | } -------------------------------------------------------------------------------- /src/Api/OpenApi/Filters/ExternalDocsOperationFilter.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using Microsoft.OpenApi.Models; 3 | using Swashbuckle.AspNetCore.SwaggerGen; 4 | 5 | namespace Passwordless.Api.OpenApi.Filters; 6 | 7 | /// 8 | /// Links an operation to external documentation. 9 | /// 10 | public class ExternalDocsOperationFilter : IOperationFilter 11 | { 12 | /// 13 | public void Apply(OpenApiOperation operation, OperationFilterContext context) 14 | { 15 | var externalDocs = context.MethodInfo.GetCustomAttribute(); 16 | if (externalDocs is not null) 17 | { 18 | operation.ExternalDocs = new OpenApiExternalDocs 19 | { 20 | Url = new Uri(externalDocs.Url), 21 | Description = externalDocs.Description ?? "External Documentation" 22 | }; 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /src/Api/OpenApi/OpenApiTags.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.Api.OpenApi; 2 | 3 | public static class OpenApiTags 4 | { 5 | public const string Aliases = "Aliases"; 6 | public const string Applications = "Applications"; 7 | public const string Authenticators = "Authenticators"; 8 | public const string AuthConfigs = "Authenication Configurations"; 9 | public const string Credentials = "Credentials"; 10 | public const string EventLogging = "Event Logging"; 11 | public const string MagicLinks = "Magic Links"; 12 | public const string Registration = "Registration"; 13 | public const string Reporting = "Reporting"; 14 | public const string SignIn = "Sign In"; 15 | public const string Users = "Users"; 16 | } -------------------------------------------------------------------------------- /src/Api/RateLimiting/RateLimitingExtensions.cs: -------------------------------------------------------------------------------- 1 | using Passwordless.Api.Endpoints; 2 | 3 | namespace Passwordless.Api.RateLimiting; 4 | 5 | public static class RateLimitingExtensions 6 | { 7 | public static IServiceCollection AddRateLimiting(this IServiceCollection services) => 8 | services.AddRateLimiter(options => 9 | { 10 | options.RejectionStatusCode = StatusCodes.Status429TooManyRequests; 11 | options.AddMagicRateLimiterPolicy(); 12 | }); 13 | } -------------------------------------------------------------------------------- /src/Api/Reporting/Background/ReportingBootstrap.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.Api.Reporting.Background; 2 | 3 | public static class ReportingBootstrap 4 | { 5 | public static void AddReportingBackgroundServices(this IServiceCollection services) 6 | { 7 | services.AddHostedService(); 8 | services.AddHostedService(); 9 | } 10 | } -------------------------------------------------------------------------------- /src/Api/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "ConnectionStrings": { 3 | "sqlite:api": "Data Source=passwordless_dev.db" 4 | }, 5 | "PasswordlessManagement": { 6 | "ManagementKey": "shared_dev_key" 7 | }, 8 | "ApplicationOverrides": { 9 | "test": { 10 | "IsRateLimitBypassEnabled": true, 11 | "IsMagicLinkQuotaBypassEnabled": true 12 | } 13 | }, 14 | "SALT_TOKEN": "VGhpcyBpcyBhIHN1cGVyIHNlY3JldCBkZXYgc2VjcmV0IQ==" 15 | } 16 | -------------------------------------------------------------------------------- /src/Api/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Serilog": { 3 | "MinimumLevel": { 4 | "Default": "Information", 5 | "Override": { 6 | "Microsoft": "Warning", 7 | "Microsoft.Hosting.Lifetime": "Information", 8 | "System": "Warning" 9 | } 10 | }, 11 | "Filter": [ 12 | { 13 | "Name": "ByExcluding", 14 | "Args": { 15 | "expression": "Contains(RequestPath, '/health/') and StatusCode=200" 16 | } 17 | } 18 | ] 19 | }, 20 | "AllowedHosts": "*", 21 | "SALT_TOKEN": "", 22 | "Datadog": { 23 | "url": "https://http-intake.logs.datadoghq.eu" 24 | }, 25 | "Fido2": { 26 | "MDS": { 27 | "Mode": "Online" 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Api/datadog.json: -------------------------------------------------------------------------------- 1 | { 2 | "DD_SITE": "datadoghq.eu", 3 | "DD_ENV": "pass-any", 4 | "DD_VERSION": "0.0.0", 5 | "DD_LOGS_INJECTION": true, 6 | "DD_TRACE_HEADER_TAGS": "Client-Version" 7 | } -------------------------------------------------------------------------------- /src/Api/wwwroot/DMSans-VariableFont_opsz,wght.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitwarden/passwordless-server/6bbabde3f4c9ae09367e275d76f3045c9918bca9/src/Api/wwwroot/DMSans-VariableFont_opsz,wght.ttf -------------------------------------------------------------------------------- /src/Api/wwwroot/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitwarden/passwordless-server/6bbabde3f4c9ae09367e275d76f3045c9918bca9/src/Api/wwwroot/favicon-16x16.png -------------------------------------------------------------------------------- /src/Api/wwwroot/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitwarden/passwordless-server/6bbabde3f4c9ae09367e275d76f3045c9918bca9/src/Api/wwwroot/favicon-32x32.png -------------------------------------------------------------------------------- /src/Api/wwwroot/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitwarden/passwordless-server/6bbabde3f4c9ae09367e275d76f3045c9918bca9/src/Api/wwwroot/favicon.ico -------------------------------------------------------------------------------- /src/Api/wwwroot/humans.txt: -------------------------------------------------------------------------------- 1 | Hey Human! We hope you have a great day. 2 | Passwordless.dev was founded by Anders Åberg and is maintained by the Passwordless team at Bitwarden. 3 | Hiring: https://bitwarden.com/careers/#open-positions -------------------------------------------------------------------------------- /src/Api/wwwroot/security.txt: -------------------------------------------------------------------------------- 1 | # Hey security researchers. Information related to reporting security vulnerabilities of this site. 2 | Contact: https://bitwarden.com/contact/ 3 | Preferred-Languages: en 4 | Hiring: https://bitwarden.com/careers/#open-positions 5 | -------------------------------------------------------------------------------- /src/Common/Background/ExecutionPlan.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.Common.Background; 2 | 3 | /// 4 | /// 5 | /// 6 | /// Time to wait before the first execution. 7 | public record ExecutionPlan(TimeSpan InitialDelay); -------------------------------------------------------------------------------- /src/Common/Configuration/ConfigurationExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.Common.Configuration; 2 | 3 | public static class ConfigurationExtensions 4 | { 5 | public static bool IsSelfHosted(this IConfiguration configuration) 6 | { 7 | return configuration.GetValue("SelfHosted", false); 8 | } 9 | } -------------------------------------------------------------------------------- /src/Common/Configuration/WebApplicationBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.Common.Configuration; 2 | 3 | public static class WebApplicationBuilderExtensions 4 | { 5 | public static void AddSelfHostingConfiguration(this WebApplicationBuilder builder) 6 | { 7 | if (builder.Environment.IsDevelopment()) 8 | { 9 | builder.Configuration.AddJsonFile("appsettings.SelfHosting.json", true, true); 10 | } 11 | else 12 | { 13 | builder.Configuration.AddJsonFile("/etc/bitwarden_passwordless", "config.json", true, true); 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /src/Common/Constants/PublicKeyScopes.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | 3 | namespace Passwordless.Common.Constants; 4 | 5 | public enum PublicKeyScopes 6 | { 7 | [Description("register")] 8 | Register, 9 | 10 | [Description("login")] 11 | Login 12 | } -------------------------------------------------------------------------------- /src/Common/Constants/RegularExpressions.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.Common.Constants; 2 | 3 | public static class RegularExpressions 4 | { 5 | public const string Base64Url = "^[a-zA-Z0-9_-]+$"; 6 | } -------------------------------------------------------------------------------- /src/Common/Constants/SecretKeyScopes.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | 3 | namespace Passwordless.Common.Constants; 4 | 5 | public enum SecretKeyScopes 6 | { 7 | [Description("token_register")] 8 | TokenRegister, 9 | 10 | [Description("token_verify")] 11 | TokenVerify 12 | } -------------------------------------------------------------------------------- /src/Common/Converters/SignInPurposeConverter.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using System.Text.Json.Serialization; 3 | using Passwordless.Common.Models.Apps; 4 | 5 | namespace Passwordless.Common.Converters; 6 | 7 | public class SignInPurposeConverter : JsonConverter 8 | { 9 | public override SignInPurpose? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 10 | { 11 | var signInPurpose = reader.GetString(); 12 | 13 | return string.IsNullOrWhiteSpace(signInPurpose) 14 | ? null 15 | : new SignInPurpose(signInPurpose); 16 | } 17 | 18 | public override void Write(Utf8JsonWriter writer, SignInPurpose value, JsonSerializerOptions options) 19 | { 20 | writer.WriteStringValue(value.Value); 21 | } 22 | } -------------------------------------------------------------------------------- /src/Common/EventLog/Enums/Severity.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.Common.EventLog.Enums; 2 | 3 | public enum Severity 4 | { 5 | Unknown = 0, 6 | Alert = 1, // token modification / things that would be security breach attempt 7 | Warning = 2, // failed login / expired token / old magic link 8 | Informational = 3, // most things here 9 | Error = 5 // internal , misconfiguration , etc. 10 | } -------------------------------------------------------------------------------- /src/Common/EventLog/Models/Event.cs: -------------------------------------------------------------------------------- 1 | using Passwordless.Common.EventLog.Enums; 2 | 3 | namespace Passwordless.Common.EventLog.Models; 4 | 5 | public class Event 6 | { 7 | public Guid Id { get; init; } 8 | public DateTime PerformedAt { get; init; } 9 | public EventType EventType { get; init; } 10 | public string Message { get; init; } 11 | public Severity Severity { get; init; } 12 | public string PerformedBy { get; init; } 13 | public string Subject { get; init; } 14 | } -------------------------------------------------------------------------------- /src/Common/Extensions/AssemblyExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | 3 | namespace Passwordless.Common.Extensions; 4 | 5 | public static class AssemblyExtensions 6 | { 7 | public static string? GetInformationalVersion(this Assembly assembly) => 8 | assembly.GetCustomAttribute()?.InformationalVersion; 9 | } -------------------------------------------------------------------------------- /src/Common/Extensions/DbContextExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | 3 | namespace Passwordless.Common.Extensions; 4 | 5 | public static class DbContextExtensions 6 | { 7 | public static bool HasAppliedMigrations(this DbContext context) => 8 | context.Database.GetAppliedMigrations().Any(); 9 | 10 | public static async Task HasAppliedMigrationsAsync(this DbContext context) => 11 | (await context.Database.GetAppliedMigrationsAsync()).Any(); 12 | } -------------------------------------------------------------------------------- /src/Common/Extensions/EnumExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | 3 | namespace Passwordless.Common.Extensions; 4 | 5 | public static class EnumExtensions 6 | { 7 | public static string GetDescription(this Enum enumValue) 8 | { 9 | var attributes = (DescriptionAttribute[])enumValue 10 | .GetType() 11 | .GetField(enumValue.ToString())! 12 | .GetCustomAttributes(typeof(DescriptionAttribute), false); 13 | return attributes.Length > 0 ? attributes[0].Description : string.Empty; 14 | } 15 | } -------------------------------------------------------------------------------- /src/Common/Extensions/HttpRequestExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | 3 | namespace Passwordless.Common.Extensions; 4 | 5 | public static class HttpRequestExtensions 6 | { 7 | public static string GetBaseUrl(this HttpRequest request) 8 | { 9 | var uriBuilder = new StringBuilder(); 10 | uriBuilder.Append(request.Scheme); 11 | uriBuilder.Append("://"); 12 | uriBuilder.Append(request.Host.Value); 13 | return uriBuilder.ToString(); 14 | } 15 | } -------------------------------------------------------------------------------- /src/Common/Extensions/StringExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.Common.Extensions; 2 | 3 | public static class StringExtensions 4 | { 5 | public static string GetLast(this string input, int charactersToReturn) => 6 | input == null || input.Length <= charactersToReturn 7 | ? input 8 | : input[^charactersToReturn..]; 9 | } -------------------------------------------------------------------------------- /src/Common/HealthChecks/HealthCheckEndpoints.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.Common.HealthChecks; 2 | 3 | public static class HealthCheckEndpoints 4 | { 5 | public const string Path = "/health"; 6 | } -------------------------------------------------------------------------------- /src/Common/HealthChecks/SimpleHealthCheck.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Diagnostics.HealthChecks; 2 | 3 | namespace Passwordless.Common.HealthChecks; 4 | 5 | public class SimpleHealthCheck : IHealthCheck 6 | { 7 | public Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) 8 | { 9 | return Task.FromResult(HealthCheckResult.Healthy()); 10 | } 11 | } -------------------------------------------------------------------------------- /src/Common/MagicLinks/Models/SendMagicLinkRequest.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using Passwordless.Common.MagicLinks.Validation; 3 | 4 | namespace Passwordless.Common.MagicLinks.Models; 5 | 6 | public class SendMagicLinkRequest 7 | { 8 | public const string TokenTemplate = "$TOKEN"; 9 | 10 | [Required] 11 | [EmailAddress] 12 | public string EmailAddress { get; init; } 13 | 14 | [Required] 15 | [MagicLinkTemplateUrl] 16 | public string UrlTemplate { get; init; } 17 | 18 | [Required(AllowEmptyStrings = false)] 19 | public string UserId { get; init; } 20 | 21 | /// 22 | /// Represents the time to live (TTL) of a magic link in seconds. 23 | /// 24 | /// 25 | /// The TTL is the lifespan of a magic link, i.e., the duration for which the link is valid. 26 | /// 27 | [Range(1, 604800)] 28 | public int? TimeToLive { get; init; } 29 | } -------------------------------------------------------------------------------- /src/Common/Models/ApplicationPublicKey.cs: -------------------------------------------------------------------------------- 1 | using Passwordless.Common.Extensions; 2 | 3 | namespace Passwordless.Common.Models; 4 | 5 | public struct ApplicationPublicKey 6 | { 7 | public const string KeyIdentifier = ":public:"; 8 | public string Value { get; } 9 | public string AbbreviatedValue => Value.GetLast(4); 10 | public string MaskedValue => string.Join("***", AbbreviatedValue); 11 | 12 | public ApplicationPublicKey(string key) 13 | { 14 | if (!key.Contains(KeyIdentifier)) throw new ArgumentException("Api public keys contain 'public'."); 15 | 16 | Value = key; 17 | } 18 | } -------------------------------------------------------------------------------- /src/Common/Models/ApplicationSecretKey.cs: -------------------------------------------------------------------------------- 1 | using Passwordless.Common.Extensions; 2 | 3 | namespace Passwordless.Common.Models; 4 | 5 | public struct ApplicationSecretKey 6 | { 7 | public const string KeyIdentifier = ":secret:"; 8 | public string Value { get; } 9 | public string AbbreviatedValue => Value.GetLast(4); 10 | 11 | public string MaskedValue => string.Join("***", AbbreviatedValue); 12 | 13 | public ApplicationSecretKey(string key) 14 | { 15 | if (!key.Contains(KeyIdentifier)) throw new ArgumentException("Api secret keys contain 'secret'."); 16 | 17 | Value = key; 18 | } 19 | } -------------------------------------------------------------------------------- /src/Common/Models/Apps/ApiKeyResponse.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.Common.Models.Apps; 2 | 3 | public record ApiKeyResponse( 4 | string Id, 5 | DateTime CreatedAt, 6 | string ApiKey, 7 | ApiKeyTypes Type, 8 | HashSet Scopes, 9 | bool IsLocked, 10 | DateTime? LastLockedAt); -------------------------------------------------------------------------------- /src/Common/Models/Apps/ApiKeyTypes.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.Common.Models.Apps; 2 | 3 | public enum ApiKeyTypes 4 | { 5 | Public = 1, 6 | Secret = 2 7 | } -------------------------------------------------------------------------------- /src/Common/Models/Apps/AppFeatureResponse.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.Common.Models.Apps; 2 | 3 | public record AppFeatureResponse( 4 | bool EventLoggingIsEnabled, 5 | int EventLoggingRetentionPeriod, 6 | DateTime? DeveloperLoggingEndsAt, 7 | long? MaxUsers, 8 | bool AllowAttestation, 9 | bool IsGenerateSignInTokenEndpointEnabled, 10 | bool IsMagicLinksEnabled); -------------------------------------------------------------------------------- /src/Common/Models/Apps/AuthenticationConfiguration.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.Common.Models.Apps; 2 | 3 | public record AuthenticationConfiguration( 4 | string Purpose, 5 | string UserVerificationRequirement, 6 | int TimeToLive, 7 | string Hints, 8 | string CreatedBy, 9 | DateTimeOffset? CreatedOn, 10 | string? EditedBy, 11 | DateTimeOffset? EditedOn, 12 | DateTimeOffset? LastUsedOn); -------------------------------------------------------------------------------- /src/Common/Models/Apps/CancelApplicationDeletionResponse.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.Common.Models.Apps; 2 | 3 | public record CancelApplicationDeletionResponse(string Message); -------------------------------------------------------------------------------- /src/Common/Models/Apps/CreateApiKeyResponse.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.Common.Models.Apps; 2 | 3 | public record CreateApiKeyResponse(string ApiKey); -------------------------------------------------------------------------------- /src/Common/Models/Apps/CreateAppDto.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.Common.Models.Apps; 2 | 3 | public record CreateAppDto 4 | { 5 | public string AdminEmail { get; set; } = ""; 6 | 7 | public bool EventLoggingIsEnabled { get; set; } = false; 8 | 9 | public int EventLoggingRetentionPeriod { get; set; } = 365; 10 | 11 | public int MagicLinkEmailMonthlyQuota { get; set; } 12 | 13 | /// 14 | /// Maximum number of users allowed for this application. 15 | /// 16 | public long? MaxUsers { get; set; } 17 | 18 | public bool AllowAttestation { get; set; } = false; 19 | } -------------------------------------------------------------------------------- /src/Common/Models/Apps/CreateAppResultDto.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.Common.Models.Apps; 2 | 3 | public class CreateAppResultDto 4 | { 5 | public string Message { get; set; } 6 | public string ApiKey1 { get; set; } 7 | public string ApiKey2 { get; set; } 8 | public string ApiSecret1 { get; set; } 9 | public string ApiSecret2 { get; set; } 10 | } -------------------------------------------------------------------------------- /src/Common/Models/Apps/CreateApplicationRequest.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using Microsoft.AspNetCore.Mvc; 3 | 4 | namespace Passwordless.Common.Models.Apps; 5 | 6 | public class CreateApplicationRequest 7 | { 8 | [FromRoute, MinLength(3)] 9 | [RegularExpression("^[a-z]{1}[a-z0-9]{2,61}$", ErrorMessage = "'AppId' must be between 3 and 62 characters, contain only lowercase letters and numbers, and start with a lowercase letter.")] 10 | public required string AppId { get; set; } 11 | 12 | [FromBody] 13 | public required CreateAppDto Payload { get; set; } 14 | } -------------------------------------------------------------------------------- /src/Common/Models/Apps/CreatePublicKeyRequest.cs: -------------------------------------------------------------------------------- 1 | using Passwordless.Common.Constants; 2 | 3 | namespace Passwordless.Common.Models.Apps; 4 | 5 | public record CreatePublicKeyRequest(HashSet Scopes); -------------------------------------------------------------------------------- /src/Common/Models/Apps/CreateSecretKeyRequest.cs: -------------------------------------------------------------------------------- 1 | using Passwordless.Common.Constants; 2 | 3 | namespace Passwordless.Common.Models.Apps; 4 | 5 | public record CreateSecretKeyRequest(HashSet Scopes); -------------------------------------------------------------------------------- /src/Common/Models/Apps/DeleteAuthenticationConfigurationRequest.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.Common.Models.Apps; 2 | 3 | public class DeleteAuthenticationConfigurationRequest 4 | { 5 | public required string Purpose { get; set; } 6 | public required string PerformedBy { get; set; } 7 | } -------------------------------------------------------------------------------- /src/Common/Models/Apps/GetAppIdAvailabilityRequest.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace Passwordless.Common.Models.Apps; 4 | 5 | public record GetAppIdAvailabilityRequest([MinLength(3), RegularExpression("^[a-z]{1}[a-z0-9]{2,49}$")] string AppId); -------------------------------------------------------------------------------- /src/Common/Models/Apps/GetAppIdAvailabilityResponse.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.Common.Models.Apps; 2 | 3 | public record GetAppIdAvailabilityResponse(bool Available); -------------------------------------------------------------------------------- /src/Common/Models/Apps/GetAuthenticationConfigurationsFilter.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.Common.Models.Apps; 2 | 3 | public class GetAuthenticationConfigurationsFilter 4 | { 5 | public string? Purpose { get; set; } = string.Empty; 6 | } -------------------------------------------------------------------------------- /src/Common/Models/Apps/GetAuthenticationConfigurationsResult.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.Common.Models.Apps; 2 | 3 | public class GetAuthenticationConfigurationsResult 4 | { 5 | public IEnumerable Configurations { get; set; } = new List(); 6 | } -------------------------------------------------------------------------------- /src/Common/Models/Apps/ManageFeaturesRequest.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace Passwordless.Common.Models.Apps; 4 | 5 | public sealed class ManageFeaturesRequest 6 | { 7 | public bool EventLoggingIsEnabled { get; init; } 8 | 9 | [Range(0, 90)] 10 | public int EventLoggingRetentionPeriod { get; init; } 11 | 12 | /// 13 | /// Maximum allowed magic link emails sent for this application. 14 | /// Depending on the age of the application, the actual limit may be lower. 15 | /// 16 | public int MagicLinkEmailMonthlyQuota { get; init; } 17 | 18 | /// 19 | /// Maximum number of individual users allowed to use the application 20 | /// 21 | public long? MaxUsers { get; init; } 22 | 23 | public bool AllowAttestation { get; init; } 24 | } -------------------------------------------------------------------------------- /src/Common/Models/Apps/MarkDeleteApplicationRequest.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.Common.Models.Apps; 2 | 3 | public record MarkDeleteApplicationRequest(string AppId, string DeletedBy); -------------------------------------------------------------------------------- /src/Common/Models/Apps/MarkDeleteApplicationResponse.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.Common.Models.Apps; 2 | 3 | public record MarkDeleteApplicationResponse(bool IsDeleted, DateTime DeleteAt, ICollection AdminEmails); -------------------------------------------------------------------------------- /src/Common/Models/Apps/SetAuthenticationConfigurationRequest.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using System.Configuration; 3 | using Fido2NetLib.Objects; 4 | 5 | namespace Passwordless.Common.Models.Apps; 6 | 7 | public class SetAuthenticationConfigurationRequest 8 | { 9 | [Required(AllowEmptyStrings = false)] 10 | [RegularExpression(@"^[\w\-]*$", ErrorMessage = "Characters are limited to A-z, 0-9, -, or _.")] 11 | [MaxLength(255)] 12 | public string Purpose { get; set; } = string.Empty; 13 | 14 | public UserVerificationRequirement UserVerificationRequirement { get; set; } = UserVerificationRequirement.Preferred; 15 | 16 | [PositiveTimeSpanValidator] 17 | public TimeSpan TimeToLive { get; set; } 18 | 19 | public IReadOnlyList Hints { get; set; } = []; 20 | 21 | [Required(AllowEmptyStrings = false)] 22 | public string PerformedBy { get; set; } = string.Empty; 23 | } -------------------------------------------------------------------------------- /src/Common/Models/Apps/SignInPurpose.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.Common.Models.Apps; 2 | 3 | public record SignInPurpose(string Value) 4 | { 5 | public const string SignInName = "sign-in"; 6 | public static SignInPurpose SignIn => new(SignInName); 7 | 8 | public const string StepUpName = "step-up"; 9 | public static readonly SignInPurpose StepUp = new(StepUpName); 10 | }; -------------------------------------------------------------------------------- /src/Common/Models/Authenticators/AddAuthenticatorsRequest.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace Passwordless.Common.Models.Authenticators; 4 | 5 | public record AddAuthenticatorsRequest( 6 | [Required, MinLength(1)] 7 | IReadOnlyCollection AaGuids, 8 | 9 | bool IsAllowed); -------------------------------------------------------------------------------- /src/Common/Models/Authenticators/ConfiguredAuthenticatorRequest.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.Common.Models.Authenticators; 2 | 3 | /// 4 | /// 5 | /// 6 | /// When 'true', all authenticators on the allowlist are returned. When 'false', all authenticators on the blocklist are returned. 7 | public record ConfiguredAuthenticatorRequest(bool IsAllowed); -------------------------------------------------------------------------------- /src/Common/Models/Authenticators/ConfiguredAuthenticatorResponse.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.Common.Models.Authenticators; 2 | 3 | public record ConfiguredAuthenticatorResponse(Guid AaGuid, DateTime CreatedAt); -------------------------------------------------------------------------------- /src/Common/Models/Authenticators/RemoveAuthenticatorsRequest.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace Passwordless.Common.Models.Authenticators; 4 | 5 | public record RemoveAuthenticatorsRequest( 6 | [Required, MinLength(1)] 7 | IEnumerable AaGuids); -------------------------------------------------------------------------------- /src/Common/Models/Credentials/GetCredentialsRequest.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace Passwordless.Common.Models.Credentials; 4 | 5 | public record GetCredentialsRequest([MinLength(1), Required] string UserId); -------------------------------------------------------------------------------- /src/Common/Models/MDS/EntriesRequest.cs: -------------------------------------------------------------------------------- 1 | using Passwordless.Common.Validation; 2 | 3 | namespace Passwordless.Common.Models.MDS; 4 | 5 | public class EntriesRequest 6 | { 7 | /// 8 | /// Filters the list of authenticators by the specified attestation types. When null or empty, all authenticators are returned. 9 | /// 10 | [RegularExpressionCollection("^[a-zA-Z0-9_]*$")] 11 | public string[]? AttestationTypes { get; set; } 12 | 13 | /// 14 | /// Filters the list of authenticators by the specified certification statuses. When null or empty, all authenticators are returned. 15 | /// 16 | [RegularExpressionCollection("^[a-zA-Z0-9_]*$")] 17 | public string[]? CertificationStatuses { get; set; } 18 | } -------------------------------------------------------------------------------- /src/Common/Models/MDS/EntryResponse.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.Common.Models.MDS; 2 | 3 | /// Authenticator Attestation GUID 4 | /// Authenticator name 5 | /// Certification statuses 6 | /// Supported attestation types 7 | /// Icon 8 | public record EntryResponse( 9 | Guid AaGuid, 10 | string Name, 11 | IEnumerable CertificationStatuses, 12 | IEnumerable AttestationTypes, 13 | string? Icon); -------------------------------------------------------------------------------- /src/Common/Models/Reporting/PeriodicActiveUserReportRequest.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.Common.Models.Reporting; 2 | 3 | /// From when to obtain reports (inclusive), null means from the beginning of time. 4 | /// To when to obtain reports (inclusive), null means to the end of time. 5 | public record PeriodicActiveUserReportRequest(DateOnly? From, DateOnly? To) 6 | { 7 | public static PeriodicActiveUserReportRequest Create(DateTime? from, DateTime? to) 8 | { 9 | DateOnly? fromValue = from.HasValue ? DateOnly.FromDateTime(from.Value) : null; 10 | DateOnly? toValue = to.HasValue ? DateOnly.FromDateTime(to.Value) : null; 11 | 12 | return new PeriodicActiveUserReportRequest(fromValue, toValue); 13 | } 14 | } -------------------------------------------------------------------------------- /src/Common/Models/Reporting/PeriodicActiveUserReportResponse.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.Common.Models.Reporting; 2 | 3 | /// Day the report was collected. 4 | /// Daily active users by definition of having used at least one of their credentials once. 5 | /// Weekly active users by definition of having used at least one of their credentials once. 6 | /// Total amount of users. 7 | public record PeriodicActiveUserReportResponse(DateOnly CreatedAt, int DailyActiveUsers, int WeeklyActiveUsers, int TotalUsers); -------------------------------------------------------------------------------- /src/Common/Models/Reporting/PeriodicCredentialReportRequest.cs: -------------------------------------------------------------------------------- 1 | 2 | namespace Passwordless.Common.Models.Reporting; 3 | 4 | /// From when to obtain reports (inclusive), null means from the beginning of time. 5 | /// To when to obtain reports (inclusive), null means to the end of time. 6 | public record PeriodicCredentialReportRequest(DateOnly? From, DateOnly? To) 7 | { 8 | public static PeriodicCredentialReportRequest Create(DateTime? from, DateTime? to) 9 | { 10 | DateOnly? fromValue = from.HasValue ? DateOnly.FromDateTime(from.Value) : null; 11 | DateOnly? toValue = to.HasValue ? DateOnly.FromDateTime(to.Value) : null; 12 | 13 | return new PeriodicCredentialReportRequest(fromValue, toValue); 14 | } 15 | } -------------------------------------------------------------------------------- /src/Common/Models/Reporting/PeriodicCredentialReportResponse.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.Common.Models.Reporting; 2 | 3 | /// 4 | /// 5 | /// 6 | /// When the report was created 7 | /// Number of unique users with credentials 8 | /// Number of credentials 9 | public record PeriodicCredentialReportResponse(DateOnly CreatedAt, int Users, int Credentials); -------------------------------------------------------------------------------- /src/Common/Overrides/ApplicationOverrides.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.Common.Overrides; 2 | 3 | /// 4 | /// Configures behavior overrides for an application. 5 | /// 6 | public class ApplicationOverrides 7 | { 8 | /// 9 | /// Whether actions on behalf of this application bypass rate limiting. 10 | /// 11 | public bool IsRateLimitBypassEnabled { get; init; } 12 | 13 | /// 14 | /// Whether actions on behalf of this application bypass magic link quotas and restrictions. 15 | /// 16 | public bool IsMagicLinkQuotaBypassEnabled { get; init; } 17 | } -------------------------------------------------------------------------------- /src/Common/Overrides/ApplicationOverridesOptions.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.Common.Overrides; 2 | 3 | public class ApplicationOverridesOptions : Dictionary 4 | { 5 | /// 6 | /// Gets the application overrides for the specified tenant. If the application is not found, an instance with 7 | /// default values is returned. 8 | /// 9 | /// 10 | /// 11 | public ApplicationOverrides GetApplication(string tenant) 12 | { 13 | return this.TryGetValue(tenant, out var overrides) ? overrides : new ApplicationOverrides(); 14 | } 15 | } -------------------------------------------------------------------------------- /src/Common/Serialization/HtmlSanitizer.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | using Ganss.Xss; 3 | 4 | namespace Passwordless.Common.Serialization; 5 | 6 | public static class HtmlSanitizer 7 | { 8 | private static readonly Ganss.Xss.HtmlSanitizer Instance = new(new HtmlSanitizerOptions 9 | { 10 | AllowedTags = new HashSet(0) 11 | }) 12 | { 13 | KeepChildNodes = true 14 | }; 15 | 16 | public static string Sanitize(string input) 17 | { 18 | var temp = new StringBuilder(Instance.Sanitize(input)); 19 | temp.Replace("https://", string.Empty).Replace("http://", string.Empty); 20 | return temp.ToString(); 21 | } 22 | } -------------------------------------------------------------------------------- /src/Common/Services/Mail/Aws/AwsChannelOptions.cs: -------------------------------------------------------------------------------- 1 | using Passwordless.Common.Services.Mail.Strategies; 2 | 3 | namespace Passwordless.Common.Services.Mail.Aws; 4 | 5 | public class AwsChannelOptions : ChannelOptions 6 | { 7 | public string? ConfigurationSet { get; set; } 8 | } -------------------------------------------------------------------------------- /src/Common/Services/Mail/Aws/AwsMailProviderOptions.cs: -------------------------------------------------------------------------------- 1 | using Passwordless.Common.Services.Mail.Strategies; 2 | 3 | namespace Passwordless.Common.Services.Mail.Aws; 4 | 5 | public class AwsMailProviderOptions : BaseMailProviderOptions 6 | { 7 | public const string Provider = "aws"; 8 | 9 | public AwsMailProviderOptions() 10 | { 11 | Name = Provider; 12 | } 13 | 14 | public string AccessKey { get; set; } 15 | 16 | public string SecretKey { get; set; } 17 | 18 | public string Region { get; set; } 19 | 20 | public Dictionary Channels { get; set; } = new(); 21 | } -------------------------------------------------------------------------------- /src/Common/Services/Mail/BaseMailProviderOptions.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.Common.Services.Mail; 2 | 3 | public abstract class BaseMailProviderOptions 4 | { 5 | /// 6 | /// The name of the provider. 7 | /// 8 | public string Name { get; set; } 9 | 10 | } -------------------------------------------------------------------------------- /src/Common/Services/Mail/File/FileMailProviderOptions.cs: -------------------------------------------------------------------------------- 1 | using Passwordless.Common.Services.Mail.Strategies; 2 | 3 | namespace Passwordless.Common.Services.Mail.File; 4 | 5 | public class FileMailProviderOptions : BaseMailProviderOptions 6 | { 7 | public const string Provider = "file"; 8 | public const string DefaultPath = "../mail.md"; 9 | 10 | public FileMailProviderOptions() 11 | { 12 | Name = Provider; 13 | } 14 | 15 | public string Path { get; set; } = DefaultPath; 16 | 17 | public Dictionary Channels { get; set; } = new(); 18 | 19 | } -------------------------------------------------------------------------------- /src/Common/Services/Mail/IMailProvider.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.Common.Services.Mail; 2 | 3 | /// 4 | /// Used to send a mail message 5 | /// 6 | public interface IMailProvider 7 | { 8 | /// 9 | /// Sends a mail message 10 | /// 11 | /// 12 | Task SendAsync(MailMessage message); 13 | } -------------------------------------------------------------------------------- /src/Common/Services/Mail/IMailProviderFactory.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.Common.Services.Mail; 2 | 3 | /// 4 | /// Responsible for creating instances of . 5 | /// 6 | public interface IMailProviderFactory 7 | { 8 | /// 9 | /// Creates a new instance of . 10 | /// 11 | /// The name of the provider (case insensitive). 12 | /// The options to use when creating the provider. 13 | /// 14 | IMailProvider Create(string name, BaseMailProviderOptions options); 15 | } -------------------------------------------------------------------------------- /src/Common/Services/Mail/MailConfiguration.cs: -------------------------------------------------------------------------------- 1 | using Passwordless.Common.Services.Mail.Strategies; 2 | 3 | namespace Passwordless.Common.Services.Mail; 4 | 5 | public class MailConfiguration 6 | { 7 | public string? From { get; set; } 8 | 9 | public string? FromName { get; set; } 10 | 11 | /// 12 | /// The ordered list of mail providers to use. 13 | /// 14 | public IReadOnlyCollection Providers { get; set; } = new List(); 15 | } -------------------------------------------------------------------------------- /src/Common/Services/Mail/MailMessage.cs: -------------------------------------------------------------------------------- 1 | using Passwordless.Common.Services.Mail.Strategies; 2 | 3 | namespace Passwordless.Common.Services.Mail; 4 | 5 | public class MailMessage 6 | { 7 | public IEnumerable To { get; init; } 8 | 9 | public string? From { get; set; } 10 | 11 | public string? FromDisplayName { get; set; } 12 | 13 | public string Subject { get; init; } 14 | 15 | public required string TextBody { get; init; } 16 | 17 | public required string HtmlBody { get; init; } 18 | 19 | public string Tag { get; init; } 20 | 21 | public Channel Channel { get; init; } = Channel.Default; 22 | 23 | public ICollection Bcc { get; init; } = new List(); 24 | } -------------------------------------------------------------------------------- /src/Common/Services/Mail/SendGrid/SendGridMailProviderOptions.cs: -------------------------------------------------------------------------------- 1 | using Passwordless.Common.Services.Mail.Strategies; 2 | 3 | namespace Passwordless.Common.Services.Mail.SendGrid; 4 | 5 | public class SendGridMailProviderOptions : BaseMailProviderOptions 6 | { 7 | public const string Provider = "sendgrid"; 8 | 9 | public SendGridMailProviderOptions() 10 | { 11 | Name = Provider; 12 | } 13 | 14 | public string ApiKey { get; set; } 15 | 16 | public Dictionary Channels { get; set; } = new(); 17 | } -------------------------------------------------------------------------------- /src/Common/Services/Mail/Smtp/SmtpMailProviderOptions.cs: -------------------------------------------------------------------------------- 1 | using Passwordless.Common.Services.Mail.Strategies; 2 | 3 | namespace Passwordless.Common.Services.Mail.Smtp; 4 | 5 | public class SmtpMailProviderOptions : BaseMailProviderOptions 6 | { 7 | public const string Provider = "smtp"; 8 | 9 | public SmtpMailProviderOptions() 10 | { 11 | Name = Provider; 12 | } 13 | 14 | public string? Host { get; set; } 15 | 16 | public int Port { get; set; } 17 | 18 | public string? Username { get; set; } 19 | 20 | public string? Password { get; set; } 21 | 22 | public bool StartTls { get; set; } 23 | 24 | public bool Ssl { get; set; } 25 | 26 | public bool SslOverride { get; set; } 27 | 28 | public bool TrustServer { get; set; } 29 | 30 | public Dictionary Channels { get; set; } = new(); 31 | } -------------------------------------------------------------------------------- /src/Common/Services/Mail/Strategies/Channel.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.Common.Services.Mail.Strategies; 2 | 3 | public enum Channel 4 | { 5 | Default, 6 | MagicLinks 7 | } -------------------------------------------------------------------------------- /src/Common/Services/Mail/Strategies/ChannelOptions.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.Common.Services.Mail.Strategies; 2 | 3 | public class ChannelOptions 4 | { 5 | /// 6 | /// The default email address to use as the sender. 7 | /// 8 | public string? From { get; set; } 9 | 10 | /// 11 | /// The default name to use as the sender. 12 | /// 13 | public string? FromName { get; set; } 14 | } -------------------------------------------------------------------------------- /src/Common/Validation/Base64UrlAttribute.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using Passwordless.Common.Constants; 3 | 4 | namespace Passwordless.Common.Validation; 5 | 6 | [AttributeUsage(AttributeTargets.Property)] 7 | public sealed class Base64UrlAttribute() : RegularExpressionAttribute(RegularExpressions.Base64Url); -------------------------------------------------------------------------------- /src/Common/Validation/MaxLengthCollectionAttribute.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace Passwordless.Common.Validation; 4 | 5 | public class MaxLengthCollectionAttribute : MaxLengthAttribute 6 | { 7 | public MaxLengthCollectionAttribute(int length) : base(length) 8 | { 9 | ErrorMessage = "The field {0} must be a collection that contains strings with a maximum length of '{1}'."; 10 | } 11 | 12 | public override bool IsValid(object? value) 13 | { 14 | if (value == null) 15 | { 16 | return true; 17 | } 18 | 19 | if (value is not IEnumerable collection) 20 | { 21 | return false; 22 | } 23 | 24 | return collection.All(item => base.IsValid(item)); 25 | } 26 | } -------------------------------------------------------------------------------- /src/Common/Validation/NoForbiddenContentAttribute.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using Passwordless.Common.Serialization; 3 | 4 | namespace Passwordless.Common.Validation; 5 | 6 | [AttributeUsage(AttributeTargets.Property)] 7 | public class NoForbiddenContentAttribute() 8 | : ValidationAttribute("The value contains forbidden content such as HTML tags.") 9 | { 10 | public override bool IsValid(object? value) 11 | { 12 | if (value == null) 13 | { 14 | return true; 15 | } 16 | 17 | if (value is not string valueString) 18 | { 19 | throw new ArgumentException("This attribute can only be applied to strings."); 20 | } 21 | 22 | var sanitizedValue = HtmlSanitizer.Sanitize(valueString); 23 | return sanitizedValue == valueString; 24 | } 25 | } -------------------------------------------------------------------------------- /src/Common/Validation/RequiredCollectionAttribute.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace Passwordless.Common.Validation; 4 | 5 | public class RequiredCollectionAttribute : RequiredAttribute 6 | { 7 | public RequiredCollectionAttribute() 8 | { 9 | ErrorMessage = "The field {0} must be a collection where every item is required."; 10 | } 11 | 12 | public override bool IsValid(object? value) 13 | { 14 | if (value == null) 15 | { 16 | return true; 17 | } 18 | 19 | if (value is not IEnumerable collection) 20 | { 21 | return false; 22 | } 23 | 24 | return collection.All(item => base.IsValid(item)); 25 | } 26 | } -------------------------------------------------------------------------------- /src/Service/EventLog/Loggers/EventCache.cs: -------------------------------------------------------------------------------- 1 | using Passwordless.Service.EventLog.Models; 2 | 3 | namespace Passwordless.Service.EventLog.Loggers; 4 | 5 | public class EventCache 6 | { 7 | private readonly List _events = new(); 8 | public IEnumerable GetEvents() => _events; 9 | public bool IsEmpty() => !_events.Any(); 10 | public void Add(ApplicationEvent applicationEvent) => _events.Add(applicationEvent); 11 | public void Clear() => _events.Clear(); 12 | } -------------------------------------------------------------------------------- /src/Service/EventLog/Loggers/IEventLogStorage.cs: -------------------------------------------------------------------------------- 1 | using Passwordless.Service.EventLog.Models; 2 | 3 | namespace Passwordless.Service.EventLog.Loggers; 4 | 5 | public interface IEventLogStorage 6 | { 7 | Task> GetEventLogAsync(int pageNumber, int resultsPerPage, CancellationToken cancellationToken); 8 | Task GetEventLogCountAsync(CancellationToken cancellationToken); 9 | } -------------------------------------------------------------------------------- /src/Service/EventLog/Loggers/NoOpEventLogger.cs: -------------------------------------------------------------------------------- 1 | using Passwordless.Service.EventLog.Models; 2 | 3 | namespace Passwordless.Service.EventLog.Loggers; 4 | 5 | public class NoOpEventLogger : IEventLogger 6 | { 7 | public void LogEvent(EventDto @event) { } 8 | 9 | public void LogEvent(Func eventFunc) { } 10 | 11 | public Task FlushAsync() => Task.CompletedTask; 12 | 13 | public static NoOpEventLogger Instance { get; } = new(); 14 | } -------------------------------------------------------------------------------- /src/Service/EventLog/Models/ApplicationEvent.cs: -------------------------------------------------------------------------------- 1 | using Passwordless.Common.EventLog.Models; 2 | using Passwordless.Service.Models; 3 | 4 | namespace Passwordless.Service.EventLog.Models; 5 | 6 | public class ApplicationEvent : Event 7 | { 8 | public required string TenantId { get; set; } 9 | public string? ApiKeyId { get; set; } 10 | 11 | public AccountMetaInformation? Application { get; set; } 12 | } -------------------------------------------------------------------------------- /src/Service/EventLog/Models/EventDto.cs: -------------------------------------------------------------------------------- 1 | using Passwordless.Common.EventLog.Enums; 2 | 3 | namespace Passwordless.Service.EventLog.Models; 4 | 5 | public class EventDto 6 | { 7 | public DateTime PerformedAt { get; init; } = DateTime.UtcNow; 8 | public string Message { get; init; } = string.Empty; 9 | public string PerformedBy { get; init; } = string.Empty; 10 | public string TenantId { get; init; } = string.Empty; 11 | public EventType EventType { get; init; } 12 | public Severity Severity { get; init; } 13 | public string Subject { get; init; } = string.Empty; 14 | public string ApiKeyId { get; init; } = string.Empty; 15 | } -------------------------------------------------------------------------------- /src/Service/Extensions/ConversionFunctions.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.Service.Extensions; 2 | 3 | public static class ConversionFunctions 4 | { 5 | public static TimeSpan ToTimeSpanFromSeconds(this int seconds) => TimeSpan.FromSeconds(seconds); 6 | } -------------------------------------------------------------------------------- /src/Service/Extensions/Models/AppFeatureExtensions.cs: -------------------------------------------------------------------------------- 1 | using Passwordless.Common.Models.Apps; 2 | using Passwordless.Service.Models; 3 | 4 | namespace Passwordless.Service.Extensions.Models; 5 | 6 | public static class AppFeatureExtensions 7 | { 8 | public static AppFeatureResponse ToDto(this AppFeature entity) 9 | { 10 | if (entity == null) return null; 11 | 12 | return new AppFeatureResponse( 13 | entity.EventLoggingIsEnabled, 14 | entity.EventLoggingRetentionPeriod, 15 | entity.DeveloperLoggingEndsAt, 16 | entity.MaxUsers, 17 | entity.AllowAttestation, 18 | entity.IsGenerateSignInTokenEndpointEnabled, 19 | entity.IsMagicLinksEnabled); 20 | } 21 | } -------------------------------------------------------------------------------- /src/Service/Features/FeaturesContext.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.Service.Features; 2 | 3 | public sealed record FeaturesContext( 4 | bool EventLoggingIsEnabled, 5 | int EventLoggingRetentionPeriod, 6 | DateTime? DeveloperLoggingEndsAt, 7 | long? MaxUsers, 8 | bool AllowAttestation, 9 | bool IsGenerateSignInTokenEndpointEnabled, 10 | bool IsMagicLinksEnabled) 11 | : IFeaturesContext 12 | { 13 | public bool IsInFeaturesContext => true; 14 | } -------------------------------------------------------------------------------- /src/Service/Features/IFeatureContextProvider.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.Service.Features; 2 | 3 | public interface IFeatureContextProvider 4 | { 5 | Task UseContext(); 6 | } -------------------------------------------------------------------------------- /src/Service/Features/IFeaturesContext.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.Service.Features; 2 | 3 | public interface IFeaturesContext 4 | { 5 | public bool EventLoggingIsEnabled { get; init; } 6 | public int EventLoggingRetentionPeriod { get; init; } 7 | public DateTime? DeveloperLoggingEndsAt { get; init; } 8 | public bool IsInFeaturesContext { get; } 9 | public bool IsGenerateSignInTokenEndpointEnabled { get; init; } 10 | public bool IsMagicLinksEnabled { get; init; } 11 | 12 | /// 13 | /// Maximum number of individual users allowed to use the application 14 | /// 15 | long? MaxUsers { get; init; } 16 | 17 | /// 18 | /// Determines or now whether attestation is allowed for this application. 19 | /// 20 | public bool AllowAttestation { get; init; } 21 | } -------------------------------------------------------------------------------- /src/Service/Features/NullFeaturesContext.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.Service.Features; 2 | 3 | public sealed class NullFeaturesContext : IFeaturesContext 4 | { 5 | public bool EventLoggingIsEnabled { get; init; } 6 | public int EventLoggingRetentionPeriod { get; init; } 7 | public DateTime? DeveloperLoggingEndsAt { get; init; } 8 | public long? MaxUsers { get; init; } 9 | public bool AllowAttestation { get; init; } 10 | public bool IsGenerateSignInTokenEndpointEnabled { get; init; } = true; 11 | public bool IsMagicLinksEnabled { get; init; } = true; 12 | public bool IsInFeaturesContext => false; 13 | } -------------------------------------------------------------------------------- /src/Service/Helpers/ApiException.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.Service.Helpers; 2 | 3 | public class ApiException( 4 | string? errorCode, 5 | string message, 6 | int statusCode = 500, 7 | Dictionary? extras = null) 8 | : Exception(message) 9 | { 10 | public ApiException(string message, int statusCode = 500, Dictionary? extras = null) 11 | : this(null, message, statusCode, extras) 12 | { 13 | } 14 | 15 | public string? ErrorCode { get; } = errorCode; 16 | public int StatusCode { get; } = statusCode; 17 | public Dictionary? Extras { get; } = extras; 18 | } -------------------------------------------------------------------------------- /src/Service/Helpers/UnknownCredentialException.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.Service.Helpers; 2 | 3 | public class UnknownCredentialException : ApiException 4 | { 5 | public UnknownCredentialException(string credentialId) 6 | : base("unknown_credential", "We don't recognize the passkey you sent us.", 400, new() { { "credentialId", credentialId } }) 7 | { 8 | } 9 | } -------------------------------------------------------------------------------- /src/Service/IApplicationService.cs: -------------------------------------------------------------------------------- 1 | using Passwordless.Common.Models.Apps; 2 | using Passwordless.Common.Models.Authenticators; 3 | 4 | namespace Passwordless.Service; 5 | 6 | public interface IApplicationService 7 | { 8 | Task SetFeaturesAsync(SetFeaturesRequest features); 9 | Task> ListConfiguredAuthenticatorsAsync(ConfiguredAuthenticatorRequest request); 10 | Task AddAuthenticatorsAsync(AddAuthenticatorsRequest request); 11 | Task RemoveAuthenticatorsAsync(RemoveAuthenticatorsRequest request); 12 | } -------------------------------------------------------------------------------- /src/Service/IReportingService.cs: -------------------------------------------------------------------------------- 1 | using Passwordless.Common.Models.Reporting; 2 | 3 | namespace Passwordless.Service; 4 | 5 | public interface IReportingService 6 | { 7 | Task UpdatePeriodicCredentialReportsAsync(); 8 | Task> GetPeriodicCredentialReportsAsync(PeriodicCredentialReportRequest parameters); 9 | Task UpdatePeriodicActiveUserReportsAsync(); 10 | Task> GetPeriodicActiveUserReportsAsync(PeriodicActiveUserReportRequest request); 11 | } -------------------------------------------------------------------------------- /src/Service/ITenantProvider.cs: -------------------------------------------------------------------------------- 1 | #nullable enable 2 | 3 | 4 | namespace Passwordless.Service; 5 | 6 | public interface ITenantProvider 7 | { 8 | public string Tenant { get; } 9 | } -------------------------------------------------------------------------------- /src/Service/ITokenService.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.Service; 2 | 3 | public interface ITokenService 4 | { 5 | // Ideally should inherit from Token, but for now we also encode/decode arbitrary objects too 6 | Task DecodeTokenAsync(string token, string prefix, bool contractless = false); 7 | Task EncodeTokenAsync(T token, string prefix, bool contractless = false); 8 | } -------------------------------------------------------------------------------- /src/Service/MDS/IMetaDataService.cs: -------------------------------------------------------------------------------- 1 | using Passwordless.Common.Models.MDS; 2 | 3 | namespace Passwordless.Service.MDS; 4 | 5 | public interface IMetaDataService 6 | { 7 | Task> GetAttestationTypesAsync(); 8 | Task> GetEntriesAsync(EntriesRequest request); 9 | Task> GetCertificationStatusesAsync(); 10 | 11 | /// 12 | /// Checks whether all the given authenticators exist in the FIDO2 MDS. 13 | /// 14 | /// 15 | /// Returns `true` if all the authenticators exist in the FIDO2 MDS; otherwise, `false`. 16 | Task ExistsAsync(IReadOnlyCollection aaGuids); 17 | } -------------------------------------------------------------------------------- /src/Service/MDS/MetaDataServiceBootstrap.cs: -------------------------------------------------------------------------------- 1 | using Fido2NetLib; 2 | using Microsoft.AspNetCore.Builder; 3 | using Microsoft.Extensions.DependencyInjection; 4 | 5 | namespace Passwordless.Service.MDS; 6 | 7 | public static class MetaDataServiceBootstrap 8 | { 9 | public static void AddMetaDataService(this WebApplicationBuilder builder) 10 | { 11 | var httpClientBuilder = builder.Services.AddHttpClient(nameof(Fido2MetadataServiceRepository), client => 12 | { 13 | client.BaseAddress = new Uri("https://mds3.fidoalliance.org/"); 14 | }); 15 | 16 | builder.Services.AddTransient(); 17 | httpClientBuilder.AddHttpMessageHandler(); 18 | 19 | builder.Services.AddSingleton(); 20 | builder.Services.AddSingleton(); 21 | } 22 | } -------------------------------------------------------------------------------- /src/Service/MagicLinks/Extensions/SendMagicLinkRequestExtension.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Mail; 2 | using Passwordless.Common.MagicLinks.Models; 3 | using Passwordless.Service.Models; 4 | 5 | namespace Passwordless.Service.MagicLinks.Extensions; 6 | 7 | public static class SendMagicLinkRequestExtension 8 | { 9 | public static MagicLinkTokenRequest ToDto(this SendMagicLinkRequest request) => new(request.UserId, new MailAddress(request.EmailAddress), request.UrlTemplate, request.TimeToLive); 10 | } -------------------------------------------------------------------------------- /src/Service/MagicLinks/MagicLinksOptions.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.Service.MagicLinks; 2 | 3 | public class MagicLinksOptions 4 | { 5 | public TimeSpan NewAccountTimeout { get; set; } = TimeSpan.FromHours(24); 6 | } -------------------------------------------------------------------------------- /src/Service/Migrations/Mssql/20231204125125_Pas64MaxUsers.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | 3 | #nullable disable 4 | 5 | namespace Passwordless.Service.Migrations.Mssql; 6 | 7 | /// 8 | public partial class Pas64MaxUsers : Migration 9 | { 10 | /// 11 | protected override void Up(MigrationBuilder migrationBuilder) 12 | { 13 | migrationBuilder.AddColumn( 14 | name: "MaxUsers", 15 | table: "AppFeatures", 16 | type: "bigint", 17 | nullable: true); 18 | } 19 | 20 | /// 21 | protected override void Down(MigrationBuilder migrationBuilder) 22 | { 23 | migrationBuilder.DropColumn( 24 | name: "MaxUsers", 25 | table: "AppFeatures"); 26 | } 27 | } -------------------------------------------------------------------------------- /src/Service/Migrations/Mssql/20231215205517_AddSignInTokenEndpointSetting.cs: -------------------------------------------------------------------------------- 1 | #nullable disable 2 | 3 | using Microsoft.EntityFrameworkCore.Migrations; 4 | 5 | namespace Passwordless.Service.Migrations.Mssql; 6 | 7 | /// 8 | public partial class AddSignInTokenEndpointSetting : Migration 9 | { 10 | /// 11 | protected override void Up(MigrationBuilder migrationBuilder) 12 | { 13 | migrationBuilder.AddColumn( 14 | name: "IsGenerateSignInTokenEndpointEnabled", 15 | table: "AppFeatures", 16 | type: "bit", 17 | nullable: false, 18 | defaultValue: true); 19 | } 20 | 21 | /// 22 | protected override void Down(MigrationBuilder migrationBuilder) 23 | { 24 | migrationBuilder.DropColumn( 25 | name: "IsGenerateSignInTokenEndpointEnabled", 26 | table: "AppFeatures"); 27 | } 28 | } -------------------------------------------------------------------------------- /src/Service/Migrations/Mssql/20240108114346_AlterTableAppFeaturesAddColumnAllowAttestation.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | 3 | #nullable disable 4 | 5 | namespace Passwordless.Service.Migrations.Mssql; 6 | 7 | /// 8 | public partial class AlterTableAppFeaturesAddColumnAllowAttestation : Migration 9 | { 10 | /// 11 | protected override void Up(MigrationBuilder migrationBuilder) 12 | { 13 | migrationBuilder.AddColumn( 14 | name: "AllowAttestation", 15 | table: "AppFeatures", 16 | type: "bit", 17 | nullable: false, 18 | defaultValue: false); 19 | } 20 | 21 | /// 22 | protected override void Down(MigrationBuilder migrationBuilder) 23 | { 24 | migrationBuilder.DropColumn( 25 | name: "AllowAttestation", 26 | table: "AppFeatures"); 27 | } 28 | } -------------------------------------------------------------------------------- /src/Service/Migrations/Mssql/20240112151209_AddMagicLinksSetting.cs: -------------------------------------------------------------------------------- 1 | #nullable disable 2 | 3 | using Microsoft.EntityFrameworkCore.Migrations; 4 | 5 | namespace Passwordless.Service.Migrations.Mssql; 6 | 7 | /// 8 | public partial class AddMagicLinksSetting : Migration 9 | { 10 | /// 11 | protected override void Up(MigrationBuilder migrationBuilder) 12 | { 13 | migrationBuilder.AddColumn( 14 | name: "IsMagicLinksEnabled", 15 | table: "AppFeatures", 16 | type: "bit", 17 | nullable: false, 18 | defaultValue: true); 19 | } 20 | 21 | /// 22 | protected override void Down(MigrationBuilder migrationBuilder) 23 | { 24 | migrationBuilder.DropColumn( 25 | name: "IsMagicLinksEnabled", 26 | table: "AppFeatures"); 27 | } 28 | } -------------------------------------------------------------------------------- /src/Service/Migrations/Mssql/20240722200735_CredentialHints.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | 3 | #nullable disable 4 | 5 | namespace Passwordless.Service.Migrations.Mssql; 6 | 7 | /// 8 | public partial class CredentialHints : Migration 9 | { 10 | /// 11 | protected override void Up(MigrationBuilder migrationBuilder) 12 | { 13 | migrationBuilder.AddColumn( 14 | name: "Hints", 15 | table: "AuthenticationConfigurations", 16 | type: "nvarchar(max)", 17 | nullable: false, 18 | defaultValue: ""); 19 | } 20 | 21 | /// 22 | protected override void Down(MigrationBuilder migrationBuilder) 23 | { 24 | migrationBuilder.DropColumn( 25 | name: "Hints", 26 | table: "AuthenticationConfigurations"); 27 | } 28 | } -------------------------------------------------------------------------------- /src/Service/Migrations/Sqlite/20231204125048_Pas64MaxUsers.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | 3 | #nullable disable 4 | 5 | namespace Passwordless.Service.Migrations.Sqlite; 6 | 7 | /// 8 | public partial class Pas64MaxUsers : Migration 9 | { 10 | /// 11 | protected override void Up(MigrationBuilder migrationBuilder) 12 | { 13 | migrationBuilder.AddColumn( 14 | name: "MaxUsers", 15 | table: "AppFeatures", 16 | type: "INTEGER", 17 | nullable: true); 18 | } 19 | 20 | /// 21 | protected override void Down(MigrationBuilder migrationBuilder) 22 | { 23 | migrationBuilder.DropColumn( 24 | name: "MaxUsers", 25 | table: "AppFeatures"); 26 | } 27 | } -------------------------------------------------------------------------------- /src/Service/Migrations/Sqlite/20231215205459_AddSignInTokenEndpointSetting.cs: -------------------------------------------------------------------------------- 1 | #nullable disable 2 | 3 | using Microsoft.EntityFrameworkCore.Migrations; 4 | 5 | namespace Passwordless.Service.Migrations.Sqlite; 6 | 7 | /// 8 | public partial class AddSignInTokenEndpointSetting : Migration 9 | { 10 | /// 11 | protected override void Up(MigrationBuilder migrationBuilder) 12 | { 13 | migrationBuilder.AddColumn( 14 | name: "IsGenerateSignInTokenEndpointEnabled", 15 | table: "AppFeatures", 16 | type: "INTEGER", 17 | nullable: false, 18 | defaultValue: true); 19 | } 20 | 21 | /// 22 | protected override void Down(MigrationBuilder migrationBuilder) 23 | { 24 | migrationBuilder.DropColumn( 25 | name: "IsGenerateSignInTokenEndpointEnabled", 26 | table: "AppFeatures"); 27 | } 28 | } -------------------------------------------------------------------------------- /src/Service/Migrations/Sqlite/20240108114420_AlterTableAppFeaturesAddColumnAllowAttestation.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | 3 | #nullable disable 4 | 5 | namespace Passwordless.Service.Migrations.Sqlite; 6 | 7 | /// 8 | public partial class AlterTableAppFeaturesAddColumnAllowAttestation : Migration 9 | { 10 | /// 11 | protected override void Up(MigrationBuilder migrationBuilder) 12 | { 13 | migrationBuilder.AddColumn( 14 | name: "AllowAttestation", 15 | table: "AppFeatures", 16 | type: "INTEGER", 17 | nullable: false, 18 | defaultValue: false); 19 | } 20 | 21 | /// 22 | protected override void Down(MigrationBuilder migrationBuilder) 23 | { 24 | migrationBuilder.DropColumn( 25 | name: "AllowAttestation", 26 | table: "AppFeatures"); 27 | } 28 | } -------------------------------------------------------------------------------- /src/Service/Migrations/Sqlite/20240112151023_AddMagicLinksSetting.cs: -------------------------------------------------------------------------------- 1 | #nullable disable 2 | 3 | using Microsoft.EntityFrameworkCore.Migrations; 4 | 5 | namespace Passwordless.Service.Migrations.Sqlite; 6 | 7 | /// 8 | public partial class AddMagicLinksSetting : Migration 9 | { 10 | /// 11 | protected override void Up(MigrationBuilder migrationBuilder) 12 | { 13 | migrationBuilder.AddColumn( 14 | name: "IsMagicLinksEnabled", 15 | table: "AppFeatures", 16 | type: "INTEGER", 17 | nullable: false, 18 | defaultValue: true); 19 | } 20 | 21 | /// 22 | protected override void Down(MigrationBuilder migrationBuilder) 23 | { 24 | migrationBuilder.DropColumn( 25 | name: "IsMagicLinksEnabled", 26 | table: "AppFeatures"); 27 | } 28 | } -------------------------------------------------------------------------------- /src/Service/Migrations/Sqlite/20240228181418_DateTimeOffsetToDateTime.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | 3 | #nullable disable 4 | 5 | namespace Passwordless.Service.Migrations.Sqlite; 6 | 7 | /// 8 | public partial class DateTimeOffsetToDateTime : Migration 9 | { 10 | /// 11 | protected override void Up(MigrationBuilder migrationBuilder) 12 | { 13 | 14 | } 15 | 16 | /// 17 | protected override void Down(MigrationBuilder migrationBuilder) 18 | { 19 | 20 | } 21 | } -------------------------------------------------------------------------------- /src/Service/Migrations/Sqlite/20240709134121_AddIndicesForHotCodePaths.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | 3 | #nullable disable 4 | 5 | namespace Passwordless.Service.Migrations.Sqlite; 6 | 7 | /// 8 | public partial class AddIndicesForHotCodePaths : Migration 9 | { 10 | /// 11 | protected override void Up(MigrationBuilder migrationBuilder) 12 | { 13 | migrationBuilder.CreateIndex( 14 | name: "IX_EFStoredCredential_Tenant_UserId", 15 | table: "Credentials", 16 | columns: new[] { "Tenant", "UserId" }); 17 | } 18 | 19 | /// 20 | protected override void Down(MigrationBuilder migrationBuilder) 21 | { 22 | migrationBuilder.DropIndex( 23 | name: "IX_EFStoredCredential_Tenant_UserId", 24 | table: "Credentials"); 25 | } 26 | } -------------------------------------------------------------------------------- /src/Service/Migrations/Sqlite/20240722200755_CredentialHints.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | 3 | #nullable disable 4 | 5 | namespace Passwordless.Service.Migrations.Sqlite; 6 | 7 | /// 8 | public partial class CredentialHints : Migration 9 | { 10 | /// 11 | protected override void Up(MigrationBuilder migrationBuilder) 12 | { 13 | migrationBuilder.AddColumn( 14 | name: "Hints", 15 | table: "AuthenticationConfigurations", 16 | type: "TEXT", 17 | nullable: false, 18 | defaultValue: ""); 19 | } 20 | 21 | /// 22 | protected override void Down(MigrationBuilder migrationBuilder) 23 | { 24 | migrationBuilder.DropColumn( 25 | name: "Hints", 26 | table: "AuthenticationConfigurations"); 27 | } 28 | } -------------------------------------------------------------------------------- /src/Service/Models/AliasPayload.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using Passwordless.Common.Validation; 3 | 4 | namespace Passwordless.Service.Models; 5 | 6 | public record AliasPayload 7 | ( 8 | [Required(AllowEmptyStrings = false)] 9 | string UserId, 10 | 11 | [MaxLength(10), MaxLengthCollection(250), RequiredCollection(AllowEmptyStrings = false)] 12 | HashSet Aliases, 13 | 14 | bool Hashing = true 15 | ); -------------------------------------------------------------------------------- /src/Service/Models/AliasPointer.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.Service.Models; 2 | 3 | public class AliasPointer : PerTenant 4 | { 5 | public required string UserId { get; set; } 6 | public required string Alias { get; set; } 7 | public string? Plaintext { get; set; } 8 | } -------------------------------------------------------------------------------- /src/Service/Models/ApiKeyDesc.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.Service.Models; 2 | 3 | public class ApiKeyDesc : PerTenant 4 | { 5 | public required string Id { get; set; } 6 | 7 | public required string ApiKey { get; set; } 8 | public required string[] Scopes { get; set; } 9 | 10 | public bool IsLocked { get; set; } 11 | 12 | public DateTime? LastLockedAt { get; set; } 13 | public DateTime? LastUnlockedAt { get; set; } 14 | 15 | public DateTime CreatedAt { get; set; } = DateTime.UtcNow; 16 | 17 | public string MaskedApiKey => ApiKey.Contains("public") 18 | ? $"{Tenant}:public:{Id.PadLeft(32, '*')}" 19 | : $"{Tenant}:secret:{Id.PadLeft(32, '*')}"; 20 | } -------------------------------------------------------------------------------- /src/Service/Models/AppIdDTO.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.Service.Models; 2 | 3 | public record AppIdDTO 4 | { 5 | public string AppId { get; set; } 6 | } -------------------------------------------------------------------------------- /src/Service/Models/AuthenticationConfiguration.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using Fido2NetLib.Objects; 3 | 4 | namespace Passwordless.Service.Models; 5 | 6 | public class AuthenticationConfiguration : PerTenant 7 | { 8 | [MaxLength(255)] 9 | public required string Purpose { get; set; } 10 | public UserVerificationRequirement UserVerificationRequirement { get; set; } 11 | public TimeSpan TimeToLive { get; set; } 12 | public IReadOnlyList Hints { get; set; } = []; 13 | 14 | [MaxLength(255)] 15 | public string CreatedBy { get; set; } = string.Empty; 16 | public DateTime? CreatedOn { get; set; } 17 | 18 | [MaxLength(255)] 19 | public string? EditedBy { get; set; } 20 | public DateTime? EditedOn { get; set; } 21 | 22 | public DateTime? LastUsedOn { get; set; } 23 | } -------------------------------------------------------------------------------- /src/Service/Models/AuthenticationSessionConfiguration.cs: -------------------------------------------------------------------------------- 1 | using Fido2NetLib; 2 | using Passwordless.Common.Models.Apps; 3 | 4 | namespace Passwordless.Service.Models; 5 | 6 | public class AuthenticationSessionConfiguration 7 | { 8 | public required AssertionOptions Options { get; set; } 9 | public required SignInPurpose Purpose { get; set; } = SignInPurpose.SignIn; 10 | } -------------------------------------------------------------------------------- /src/Service/Models/Authenticator.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.Service.Models; 2 | 3 | public class Authenticator : PerTenant 4 | { 5 | /// 6 | /// The authenticator attestation GUID. 7 | /// 8 | public Guid AaGuid { get; set; } 9 | 10 | public DateTime CreatedAt { get; set; } 11 | 12 | /// 13 | /// When true, only whitelisted authenticators are allowed to be used during registration. 14 | /// 15 | public bool IsAllowed { get; set; } 16 | 17 | public AppFeature? AppFeature { get; set; } 18 | } -------------------------------------------------------------------------------- /src/Service/Models/DispatchedEmail.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.Service.Models; 2 | 3 | public class DispatchedEmail : PerTenant 4 | { 5 | public required Guid Id { get; set; } 6 | 7 | public required DateTime CreatedAt { get; set; } 8 | 9 | public required string UserId { get; set; } 10 | 11 | public required string EmailAddress { get; set; } 12 | 13 | public required string LinkTemplate { get; set; } 14 | 15 | public virtual AccountMetaInformation? Application { get; set; } 16 | } -------------------------------------------------------------------------------- /src/Service/Models/EmailAboutAccountDeletion.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.Service.Models; 2 | 3 | public class EmailAboutAccountDeletion 4 | { 5 | public string CancelLink { get; set; } = ""; 6 | public string[] Emails { get; set; } = Array.Empty(); 7 | public string AccountName { get; set; } = ""; 8 | public string Message { get; set; } = ""; 9 | } -------------------------------------------------------------------------------- /src/Service/Models/FidoRegistrationBeginDTO.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.Service.Models; 2 | 3 | public class FidoRegistrationBeginDTO : RequestBase 4 | { 5 | public string Token { get; set; } 6 | } -------------------------------------------------------------------------------- /src/Service/Models/FidoSettings.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.Service.Models; 2 | 3 | public class FidoSettings 4 | { 5 | public string RPID { get; set; } 6 | public string Origin { get; set; } 7 | public string Servername { get; set; } 8 | // add more 9 | } -------------------------------------------------------------------------------- /src/Service/Models/KeyDescriptorEntity.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Azure.Cosmos.Table; 2 | 3 | namespace Passwordless.Service.Models; 4 | 5 | public class KeyDescriptorEntity : TableEntity 6 | { 7 | public string Descriptor { get; set; } 8 | } -------------------------------------------------------------------------------- /src/Service/Models/PerTenant.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.Service.Models; 2 | 3 | public abstract class PerTenant 4 | { 5 | public required string Tenant { get; set; } 6 | } -------------------------------------------------------------------------------- /src/Service/Models/PeriodicActiveUserReport.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.Service.Models; 2 | 3 | public class PeriodicActiveUserReport : PerTenant 4 | { 5 | public DateOnly CreatedAt { get; set; } 6 | 7 | public int DailyActiveUsersCount { get; set; } 8 | 9 | public int WeeklyActiveUsersCount { get; set; } 10 | 11 | public int TotalUsersCount { get; set; } 12 | 13 | public virtual AccountMetaInformation Application { get; init; } 14 | } -------------------------------------------------------------------------------- /src/Service/Models/PeriodicCredentialReport.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.Service.Models; 2 | 3 | public class PeriodicCredentialReport : PerTenant 4 | { 5 | public DateOnly CreatedAt { get; set; } 6 | 7 | /// 8 | /// The number of credentials in the tenant. 9 | /// 10 | public int CredentialsCount { get; set; } 11 | 12 | /// 13 | /// The number of users in the tenant. 14 | /// 15 | public int UsersCount { get; set; } 16 | 17 | public virtual AccountMetaInformation? Application { get; set; } 18 | } -------------------------------------------------------------------------------- /src/Service/Models/RegisterOptions.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.Service.Models; 2 | 3 | public class SessionResponse 4 | { 5 | public T Data { get; set; } 6 | public string Session { get; set; } 7 | 8 | public SessionResponse() 9 | { 10 | 11 | } 12 | public SessionResponse(T data) 13 | { 14 | Data = data; 15 | } 16 | } -------------------------------------------------------------------------------- /src/Service/Models/RegisterSession.cs: -------------------------------------------------------------------------------- 1 | using Fido2NetLib; 2 | 3 | namespace Passwordless.Service.Models; 4 | 5 | public class RegisterSession 6 | { 7 | public CredentialCreateOptions Options { get; set; } 8 | public HashSet Aliases { get; set; } 9 | public bool AliasHashing { get; set; } = true; 10 | } -------------------------------------------------------------------------------- /src/Service/Models/RegistrationCompleteDTO.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using Fido2NetLib; 3 | 4 | namespace Passwordless.Service.Models; 5 | 6 | public class RegistrationCompleteDTO : RequestBase 7 | { 8 | public string Session { get; set; } 9 | 10 | public AuthenticatorAttestationRawResponse Response { get; set; } 11 | 12 | [MaxLength(64, ErrorMessage = "Nickname cannot be longer than 64 characters.")] 13 | public string Nickname { get; set; } 14 | } -------------------------------------------------------------------------------- /src/Service/Models/RequestBase.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.Service.Models; 2 | 3 | public class RequestBase 4 | { 5 | public string Origin { get; set; } 6 | public string RPID { get; set; } 7 | } -------------------------------------------------------------------------------- /src/Service/Models/SignInBeginDTO.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | using Passwordless.Common.Converters; 3 | using Passwordless.Common.Models.Apps; 4 | 5 | namespace Passwordless.Service.Models; 6 | 7 | public class SignInBeginDTO : RequestBase 8 | { 9 | public string Alias { get; init; } 10 | public string UserId { get; init; } 11 | [JsonConverter(typeof(SignInPurposeConverter))] 12 | public SignInPurpose Purpose { get; set; } = SignInPurpose.SignIn; 13 | } -------------------------------------------------------------------------------- /src/Service/Models/SignInCompleteDTO.cs: -------------------------------------------------------------------------------- 1 | using Fido2NetLib; 2 | 3 | namespace Passwordless.Service.Models; 4 | 5 | public class SignInCompleteDTO : RequestBase 6 | { 7 | public AuthenticatorAssertionRawResponse Response { get; set; } 8 | public string Session { get; set; } 9 | } -------------------------------------------------------------------------------- /src/Service/Models/SignInVerifyDTO.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | using Passwordless.Common.Converters; 3 | using Passwordless.Common.Models.Apps; 4 | 5 | namespace Passwordless.Service.Models; 6 | 7 | public class SignInVerifyDTO : RequestBase 8 | { 9 | public string Token { get; set; } 10 | } -------------------------------------------------------------------------------- /src/Service/Models/SigninTokenRequest.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using System.Text.Json.Serialization; 3 | using Passwordless.Common.Models.Apps; 4 | using Passwordless.Service.Extensions; 5 | 6 | namespace Passwordless.Service.Models; 7 | 8 | public class SigninTokenRequest : RequestBase 9 | { 10 | private static readonly TimeSpan DefaultTimeToLive = TimeSpan.FromSeconds(120); 11 | 12 | [Required(AllowEmptyStrings = false)] 13 | public required string UserId { get; init; } 14 | 15 | /// 16 | /// Time to live is the number of seconds the token has before it expires. 17 | /// 18 | [JsonPropertyName("timeToLive")] 19 | [Range(1, 604800)] 20 | public int? TimeToLiveSeconds { get; init; } 21 | 22 | [JsonIgnore] 23 | public TimeSpan TimeToLive => TimeToLiveSeconds?.ToTimeSpanFromSeconds() ?? DefaultTimeToLive; 24 | 25 | public string Purpose { get; set; } = SignInPurpose.SignInName; 26 | }; -------------------------------------------------------------------------------- /src/Service/Models/TokenKey.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.Service.Models; 2 | 3 | public class TokenKey : PerTenant 4 | { 5 | public required string KeyMaterial { get; set; } 6 | public required int KeyId { get; set; } 7 | public required DateTime CreatedAt { get; set; } 8 | } -------------------------------------------------------------------------------- /src/Service/Models/TokenResponse.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.Service.Models; 2 | 3 | public record TokenResponse(string Token); -------------------------------------------------------------------------------- /src/Service/Models/UserSummary.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.Service.Models; 2 | 3 | public class UserSummary 4 | { 5 | public string UserId { get; set; } 6 | public int AliasCount { get; set; } 7 | public List Aliases { get; set; } = []; 8 | public int CredentialsCount { get; set; } 9 | public DateTime? LastUsedAt { get; set; } 10 | } -------------------------------------------------------------------------------- /src/Service/Models/ValidatePublicKeyDto.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.Service.Models; 2 | 3 | public record ValidatePublicKeyDto(string ApplicationId, IReadOnlyCollection Scopes); -------------------------------------------------------------------------------- /src/Service/Models/ValidateSecretKeyDto.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.Service.Models; 2 | 3 | public record ValidateSecretKeyDto(string ApplicationId, IReadOnlyCollection Scopes); -------------------------------------------------------------------------------- /src/Service/Storage/Ef/DbGlobalMsSqlContext.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | 3 | namespace Passwordless.Service.Storage.Ef; 4 | 5 | public class DbGlobalMsSqlContext : DbGlobalContext 6 | { 7 | public DbGlobalMsSqlContext(DbContextOptions options) 8 | : base(options) 9 | { 10 | } 11 | } -------------------------------------------------------------------------------- /src/Service/Storage/Ef/DbGlobalSqliteContext.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | 3 | namespace Passwordless.Service.Storage.Ef; 4 | 5 | public class DbGlobalSqliteContext : DbGlobalContext 6 | { 7 | public DbGlobalSqliteContext(DbContextOptions options) 8 | : base(options) 9 | { 10 | } 11 | } -------------------------------------------------------------------------------- /src/Service/Storage/Ef/DbTenantMsSqlContext.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | 3 | namespace Passwordless.Service.Storage.Ef; 4 | 5 | public class DbTenantMsSqlContext : DbTenantContext 6 | { 7 | public DbTenantMsSqlContext(DbContextOptions options, ITenantProvider tenantProvider) 8 | : base(options, tenantProvider) 9 | { 10 | } 11 | } -------------------------------------------------------------------------------- /src/Service/Storage/Ef/DbTenantSqliteContext.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | 3 | namespace Passwordless.Service.Storage.Ef; 4 | 5 | public class DbTenantSqliteContext : DbTenantContext 6 | { 7 | public DbTenantSqliteContext(DbContextOptions options, ITenantProvider tenantProvider) 8 | : base(options, tenantProvider) 9 | { 10 | } 11 | } -------------------------------------------------------------------------------- /src/Service/Storage/Ef/IGlobalStorage.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.Service.Storage.Ef; 2 | public interface IGlobalStorage 3 | { 4 | Task> GetApplicationsPendingDeletionAsync(); 5 | Task UpdatePeriodicCredentialReportsAsync(); 6 | Task UpdatePeriodicActiveUserReportsAsync(); 7 | Task DeleteOldDispatchedEmailsAsync(TimeSpan age); 8 | } -------------------------------------------------------------------------------- /src/Service/Storage/Ef/ManualTenantProvider.cs: -------------------------------------------------------------------------------- 1 | namespace Passwordless.Service.Storage.Ef; 2 | 3 | public record ManualTenantProvider(string Tenant) : ITenantProvider; -------------------------------------------------------------------------------- /src/Service/Storage/Ef/ValueComparers/EnumerableValueComparer.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.ChangeTracking; 2 | 3 | namespace Passwordless.Service.Storage.Ef.ValueComparers; 4 | 5 | public sealed class EnumerableValueComparer() : ValueComparer>( 6 | (c1, c2) => c1!.SequenceEqual(c2!), 7 | c => c.Aggregate(0, (a, v) => HashCode.Combine(a, v.GetHashCode())), 8 | c => c.ToArray()); -------------------------------------------------------------------------------- /src/Service/Storage/Ef/ValueComparers/NullableEnumerableValueComparer.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.ChangeTracking; 2 | 3 | namespace Passwordless.Service.Storage.Ef.ValueComparers; 4 | 5 | public sealed class NullableEnumerableValueComparer() : ValueComparer?>( 6 | (c1, c2) => (c1 == null && c2 == null) || c1!.SequenceEqual(c2!), 7 | c => c != null ? c.Aggregate(0, (a, v) => HashCode.Combine(a, v.GetHashCode())) : 0, 8 | c => c != null ? c.ToArray() : null); -------------------------------------------------------------------------------- /src/Service/TenantProvider.cs: -------------------------------------------------------------------------------- 1 | using System.Security.Claims; 2 | using Microsoft.AspNetCore.Http; 3 | 4 | namespace Passwordless.Service; 5 | 6 | public class TenantProvider : ITenantProvider 7 | { 8 | private readonly IHttpContextAccessor _httpContextAccessor; 9 | 10 | public TenantProvider(IHttpContextAccessor httpContextAccessor) 11 | { 12 | _httpContextAccessor = httpContextAccessor; 13 | } 14 | 15 | public string Tenant 16 | { 17 | get 18 | { 19 | var httpContext = _httpContextAccessor.HttpContext; 20 | var accountName = httpContext?.User.FindFirstValue("accountName"); 21 | return accountName ?? throw new InvalidOperationException("Tenant not found"); 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /src/Service/TimeProviderSystemClockAdapter.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Internal; 2 | 3 | namespace Passwordless.Service; 4 | 5 | /// 6 | /// Wraps to implement . 7 | /// 8 | public class TimeProviderSystemClockAdapter(TimeProvider timeProvider) : 9 | ISystemClock, 10 | #pragma warning disable CS0618 // Type or member is obsolete 11 | Microsoft.AspNetCore.Authentication.ISystemClock 12 | #pragma warning restore CS0618 // Type or member is obsolete 13 | { 14 | public DateTimeOffset UtcNow => timeProvider.GetUtcNow(); 15 | } -------------------------------------------------------------------------------- /tests/AdminConsole.Tests/Factory/ClaimsPrincipalFactory.cs: -------------------------------------------------------------------------------- 1 | using System.Security.Claims; 2 | 3 | namespace Passwordless.AdminConsole.Tests.Factory; 4 | 5 | public static class ClaimsPrincipalFactory 6 | { 7 | public static ClaimsPrincipal CreateJohnDoe() 8 | { 9 | var claims = new List 10 | { 11 | new (ClaimTypes.Name, "John Doe"), 12 | new (ClaimTypes.Email, "johndoe@example.com"), 13 | }; 14 | 15 | var identity = new ClaimsIdentity(claims, "Test"); 16 | return new ClaimsPrincipal(identity); 17 | } 18 | } -------------------------------------------------------------------------------- /tests/AdminConsole.Tests/Factory/DbContextFactory.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Passwordless.AdminConsole.Db; 3 | 4 | namespace Passwordless.AdminConsole.Tests.Factory; 5 | 6 | public static class DbContextFactory 7 | { 8 | public static ConsoleDbContext Create() 9 | { 10 | var options = new DbContextOptionsBuilder() 11 | .UseInMemoryDatabase(databaseName: $"admin-{Guid.NewGuid():N}") 12 | .Options; 13 | return Create(options); 14 | } 15 | 16 | public static ConsoleDbContext Create(DbContextOptions options) 17 | { 18 | return new ConsoleDbContext(options); 19 | } 20 | } -------------------------------------------------------------------------------- /tests/Api.IntegrationTests/ApiCollectionFixture.cs: -------------------------------------------------------------------------------- 1 | using Xunit; 2 | 3 | namespace Passwordless.Api.IntegrationTests; 4 | 5 | [CollectionDefinition(Fixture)] 6 | public class ApiCollectionFixture : ICollectionFixture 7 | { 8 | internal const string Fixture = "Api"; 9 | 10 | } -------------------------------------------------------------------------------- /tests/Api.IntegrationTests/Helpers/FakeMailProviderFactory.cs: -------------------------------------------------------------------------------- 1 | using Passwordless.Common.Services.Mail; 2 | 3 | namespace Passwordless.Api.IntegrationTests.Helpers; 4 | 5 | public class FakeMailProviderFactory : IMailProviderFactory 6 | { 7 | public IMailProvider Create(string name, BaseMailProviderOptions options) 8 | { 9 | return new NoopMailProvider(); 10 | } 11 | } -------------------------------------------------------------------------------- /tests/Api.IntegrationTests/Helpers/NoopMailProvider.cs: -------------------------------------------------------------------------------- 1 | using Passwordless.Common.Services.Mail; 2 | 3 | namespace Passwordless.Api.IntegrationTests.Helpers; 4 | 5 | // If needed by tests in the future, this can be an InMemoryMailProvider, 6 | // where you can read the sent emails. 7 | public class NoopMailProvider : IMailProvider 8 | { 9 | public Task SendAsync(MailMessage message) => Task.CompletedTask; 10 | } -------------------------------------------------------------------------------- /tests/Api.IntegrationTests/PasswordlessApiOptions.cs: -------------------------------------------------------------------------------- 1 | using Xunit.Abstractions; 2 | 3 | namespace Passwordless.Api.IntegrationTests; 4 | 5 | public class PasswordlessApiOptions 6 | { 7 | public IReadOnlyDictionary Settings { get; init; } = new Dictionary(); 8 | 9 | public ITestOutputHelper? TestOutput { get; init; } 10 | } -------------------------------------------------------------------------------- /tests/Api.IntegrationTests/xunit.runner.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://xunit.net/schema/current/xunit.runner.schema.json", 3 | "methodDisplayOptions": "all", 4 | "methodDisplay": "method" 5 | } -------------------------------------------------------------------------------- /tests/Api.Tests/Usings.cs: -------------------------------------------------------------------------------- 1 | global using Moq; 2 | global using Xunit; -------------------------------------------------------------------------------- /tests/Common.Tests/Extensions/EnumExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | using Passwordless.Common.Constants; 2 | using Passwordless.Common.Extensions; 3 | 4 | namespace Passwordless.Common.Tests.Extensions; 5 | 6 | public class EnumExtensionsTests 7 | { 8 | [Fact] 9 | public void GetDescription_GivenEnumValue_ReturnsExpectedDescription() 10 | { 11 | // Arrange 12 | var scope = PublicKeyScopes.Register; 13 | 14 | // Act 15 | var result = scope.GetDescription(); 16 | 17 | // Assert 18 | Assert.Equal("register", result); 19 | } 20 | } -------------------------------------------------------------------------------- /tests/Common.Tests/Usings.cs: -------------------------------------------------------------------------------- 1 | global using Xunit; -------------------------------------------------------------------------------- /tests/Service.Tests/Implementations/TokenServiceTests.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Configuration; 2 | using Microsoft.Extensions.Logging.Abstractions; 3 | using Moq; 4 | using Passwordless.Service.Storage.Ef; 5 | 6 | namespace Passwordless.Service.Tests.Implementations; 7 | 8 | public class TokenServiceTests 9 | { 10 | private readonly Mock _mockConfiguration; 11 | private readonly Mock _mockTenantStorage; 12 | 13 | private readonly TokenService _sut; 14 | 15 | public TokenServiceTests() 16 | { 17 | _mockConfiguration = new Mock(); 18 | _mockTenantStorage = new Mock(); 19 | 20 | _sut = new TokenService(NullLogger.Instance, 21 | _mockConfiguration.Object, 22 | _mockTenantStorage.Object); 23 | } 24 | 25 | [Fact] 26 | public void Test() 27 | { 28 | Assert.NotNull(_sut); 29 | } 30 | } -------------------------------------------------------------------------------- /tests/Service.Tests/Usings.cs: -------------------------------------------------------------------------------- 1 | global using Xunit; --------------------------------------------------------------------------------