Adds Blazor Web App standard login

This commit is contained in:
Penry committed 2025-09-24 21:28:15 +02:00
1 parent 1152bc4f7e
commit 8ec615496e
141 files changed
+62726 -1354

No files matched your search

-13
View File
@@ -1,13 +0,0 @@
# Default ignored files
/shelf/
/workspace.xml
# Rider ignored files
/modules.xml
/contentModel.xml
/.idea.WatchLog.iml
/projectSettingsUpdater.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
-15
View File
@@ -1,15 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
<data-source source="LOCAL" name="WatchLog.db" uuid="abe4604a-cdad-4978-ad56-2fc7ddae1f90">
<driver-ref>sqlite.xerial</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
<jdbc-url>jdbc:sqlite:$PROJECT_DIR$/Data/WatchLog.db</jdbc-url>
<jdbc-additional-properties>
<property name="com.intellij.clouds.kubernetes.db.enabled" value="false" />
</jdbc-additional-properties>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
</component>
</project>
-4
View File
@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding" addBOMForNewFiles="with BOM under Windows, with no BOM otherwise" />
</project>
-8
View File
@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="UserContentModel">
<attachedFolders />
<explicitIncludes />
<explicitExcludes />
</component>
</project>
-6
View File
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
</component>
</project>
-997
View File
@@ -1,997 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
IIS configuration sections.
For schema documentation, see
%IIS_BIN%\config\schema\IIS_schema.xml.
Please make a backup of this file before making any changes to it.
NOTE: The following environment variables are available to be used
within this file and are understood by the IIS Express.
%IIS_USER_HOME% - The IIS Express home directory for the user
%IIS_SITES_HOME% - The default home directory for sites
%IIS_BIN% - The location of the IIS Express binaries
%SYSTEMDRIVE% - The drive letter of %IIS_BIN%
-->
<configuration>
<!--
The <configSections> section controls the registration of sections.
Section is the basic unit of deployment, locking, searching and
containment for configuration settings.
Every section belongs to one section group.
A section group is a container of logically-related sections.
Sections cannot be nested.
Section groups may be nested.
<section
name="" [Required, Collection Key] [XML name of the section]
allowDefinition="Everywhere" [MachineOnly|MachineToApplication|AppHostOnly|Everywhere] [Level where it can be set]
overrideModeDefault="Allow" [Allow|Deny] [Default delegation mode]
allowLocation="true" [true|false] [Allowed in location tags]
/>
The recommended way to unlock sections is by using a location tag:
<location path="Default Web Site" overrideMode="Allow">
<system.webServer>
<asp />
</system.webServer>
</location>
-->
<configSections>
<sectionGroup name="system.applicationHost">
<section name="applicationPools" allowDefinition="AppHostOnly" overrideModeDefault="Deny" />
<section name="configHistory" allowDefinition="AppHostOnly" overrideModeDefault="Deny" />
<section name="customMetadata" allowDefinition="AppHostOnly" overrideModeDefault="Deny" />
<section name="listenerAdapters" allowDefinition="AppHostOnly" overrideModeDefault="Deny" />
<section name="log" allowDefinition="AppHostOnly" overrideModeDefault="Deny" />
<section name="serviceAutoStartProviders" allowDefinition="AppHostOnly" overrideModeDefault="Deny" />
<section name="sites" allowDefinition="AppHostOnly" overrideModeDefault="Deny" />
<section name="webLimits" allowDefinition="AppHostOnly" overrideModeDefault="Deny" />
</sectionGroup>
<sectionGroup name="system.webServer">
<section name="asp" overrideModeDefault="Deny" />
<section name="caching" overrideModeDefault="Allow" />
<section name="cgi" overrideModeDefault="Deny" />
<section name="defaultDocument" overrideModeDefault="Allow" />
<section name="directoryBrowse" overrideModeDefault="Allow" />
<section name="fastCgi" allowDefinition="AppHostOnly" overrideModeDefault="Deny" />
<section name="globalModules" allowDefinition="AppHostOnly" overrideModeDefault="Deny" />
<section name="handlers" overrideModeDefault="Deny" />
<section name="httpCompression" overrideModeDefault="Allow" allowDefinition="Everywhere" />
<section name="httpErrors" overrideModeDefault="Allow" />
<section name="httpLogging" overrideModeDefault="Deny" />
<section name="httpProtocol" overrideModeDefault="Allow" />
<section name="httpRedirect" overrideModeDefault="Allow" />
<section name="httpTracing" overrideModeDefault="Deny" />
<section name="isapiFilters" allowDefinition="MachineToApplication" overrideModeDefault="Deny" />
<section name="modules" allowDefinition="MachineToApplication" overrideModeDefault="Deny" />
<section name="applicationInitialization" allowDefinition="MachineToApplication" overrideModeDefault="Allow" />
<section name="odbcLogging" overrideModeDefault="Deny" />
<sectionGroup name="security">
<section name="access" overrideModeDefault="Deny" />
<section name="applicationDependencies" overrideModeDefault="Deny" />
<sectionGroup name="authentication">
<section name="anonymousAuthentication" overrideModeDefault="Allow" />
<section name="basicAuthentication" overrideModeDefault="Deny" />
<section name="clientCertificateMappingAuthentication" overrideModeDefault="Deny" />
<section name="digestAuthentication" overrideModeDefault="Deny" />
<section name="iisClientCertificateMappingAuthentication" overrideModeDefault="Deny" />
<section name="windowsAuthentication" overrideModeDefault="Allow" />
</sectionGroup>
<section name="authorization" overrideModeDefault="Allow" />
<section name="ipSecurity" overrideModeDefault="Deny" />
<section name="dynamicIpSecurity" overrideModeDefault="Deny" />
<section name="isapiCgiRestriction" allowDefinition="AppHostOnly" overrideModeDefault="Deny" />
<section name="requestFiltering" overrideModeDefault="Allow" />
</sectionGroup>
<section name="serverRuntime" overrideModeDefault="Deny" />
<section name="serverSideInclude" overrideModeDefault="Deny" />
<section name="staticContent" overrideModeDefault="Allow" />
<sectionGroup name="tracing">
<section name="traceFailedRequests" overrideModeDefault="Allow" />
<section name="traceProviderDefinitions" overrideModeDefault="Deny" />
</sectionGroup>
<section name="urlCompression" overrideModeDefault="Allow" />
<section name="validation" overrideModeDefault="Allow" />
<sectionGroup name="webdav">
<section name="globalSettings" overrideModeDefault="Deny" />
<section name="authoring" overrideModeDefault="Deny" />
<section name="authoringRules" overrideModeDefault="Deny" />
</sectionGroup>
<sectionGroup name="rewrite">
<section name="allowedServerVariables" overrideModeDefault="Deny" />
<section name="rules" overrideModeDefault="Allow" />
<section name="outboundRules" overrideModeDefault="Allow" />
<section name="globalRules" overrideModeDefault="Deny" allowDefinition="AppHostOnly" />
<section name="providers" overrideModeDefault="Allow" />
<section name="rewriteMaps" overrideModeDefault="Allow" />
</sectionGroup>
<section name="webSocket" overrideModeDefault="Deny" />
<section name="aspNetCore" overrideModeDefault="Allow" />
</sectionGroup>
</configSections>
<configProtectedData>
<providers>
<add name="IISWASOnlyRsaProvider" type="" description="Uses RsaCryptoServiceProvider to encrypt and decrypt" keyContainerName="iisWasKey" cspProviderName="" useMachineContainer="true" useOAEP="false" />
<add name="AesProvider" type="Microsoft.ApplicationHost.AesProtectedConfigurationProvider" description="Uses an AES session key to encrypt and decrypt" keyContainerName="iisConfigurationKey" cspProviderName="" useOAEP="false" useMachineContainer="true" sessionKey="AQIAAA5mAAAApAAA/HKxkz6alrlAPez0IUgujj/6k3WxCDriHp6jvpv3yEZmo7h6SMzGLxo4mTrIQVHSkB7tmElHKfUFTzE2BWF7nFWHY6Z6qmGBauFzwJMwESjril7Gjz69RBFH259HQ6aRDq9Xfx7U7H4HtdmnKNqGjgl/hwPQBGeIlWiDh+sYv3vKB0QU971tjX6H2B+9armlnC8UOuA6JYMDMI/VLLL16sng0fWAy5JYe0YVABVjiAWDW264RZW9Tr1Oax4qHZKg+SdjULxeOc2YmpX+d0yeITo1HkPF1hN1gHpIPIUDo05ilHUNfR3OkjVCIQK4cFKCq1s8NH+y+13MxUC4Fn1AlQ==" />
<add name="IISWASOnlyAesProvider" type="Microsoft.ApplicationHost.AesProtectedConfigurationProvider" description="Uses an AES session key to encrypt and decrypt" keyContainerName="iisWasKey" cspProviderName="" useOAEP="false" useMachineContainer="true" sessionKey="AQIAAA5mAAAApAAALmU8lTC+v2qtfQiiiquvvLpUQqKLEXs+jSKoWCM/uPhyB++k4dwug19mGidNK5FYiWK2KYE1yhjVJcbp12E98Q0R2nT7eBiCMY2JairxQ591rqABK7keGaIjwH7PwGzSpILl3RJ4YFvJ/7ZXEJxeDZIjW8ZxWVXx+/VyHs9U3WguLEkgMUX3jrxJi8LouxaIVPJAv/YQ1ZCWs8zImitxX/C/7o7yaIxznfsN5nGQzQfpUDPeby99aw2zPVTtZI2LaWIBON8guABvZ6JtJVDWmfdK6sodbnwdZkr6/Z2rfvamT1dC1SpQrGG7ulR/f9/GXvCaW10ZVKxekBF/CYlNMg==" />
</providers>
</configProtectedData>
<system.applicationHost>
<applicationPools>
<add name="Clr4IntegratedAppPool" managedRuntimeVersion="v4.0" managedPipelineMode="Integrated" CLRConfigFile="%IIS_USER_HOME%\config\aspnet.config" autoStart="true" />
<add name="Clr4ClassicAppPool" managedRuntimeVersion="v4.0" managedPipelineMode="Classic" CLRConfigFile="%IIS_USER_HOME%\config\aspnet.config" autoStart="true" />
<add name="Clr2IntegratedAppPool" managedRuntimeVersion="v2.0" managedPipelineMode="Integrated" CLRConfigFile="%IIS_USER_HOME%\config\aspnet.config" autoStart="true" />
<add name="Clr2ClassicAppPool" managedRuntimeVersion="v2.0" managedPipelineMode="Classic" CLRConfigFile="%IIS_USER_HOME%\config\aspnet.config" autoStart="true" />
<add name="UnmanagedClassicAppPool" managedRuntimeVersion="" managedPipelineMode="Classic" autoStart="true" />
<applicationPoolDefaults managedRuntimeVersion="v4.0">
<processModel loadUserProfile="true" setProfileEnvironment="false" />
</applicationPoolDefaults>
<add name="WatchLog AppPool" managedRuntimeVersion="" />
</applicationPools>
<!--
The <listenerAdapters> section defines the protocols with which the
Windows Process Activation Service (WAS) binds.
-->
<listenerAdapters>
<add name="http" />
</listenerAdapters>
<sites>
<siteDefaults>
<!-- To enable logging, please change the below attribute "enabled" to "true" -->
<logFile logFormat="W3C" directory="%AppData%\Microsoft\IISExpressLogs" enabled="false" />
<traceFailedRequestsLogging directory="%AppData%\Microsoft" enabled="false" maxLogFileSizeKB="1024" />
</siteDefaults>
<applicationDefaults applicationPool="Clr4IntegratedAppPool" />
<virtualDirectoryDefaults allowSubDirConfig="true" />
<site name="WatchLog" id="1">
<application path="/" applicationPool="WatchLog AppPool">
<virtualDirectory path="/" physicalPath="D:\wc\Watchlog\WatchLog" />
</application>
<bindings>
<binding protocol="http" bindingInformation="*:34600:localhost" />
<binding protocol="https" bindingInformation="*:44365:localhost" />
</bindings>
</site>
</sites>
<webLimits />
</system.applicationHost>
<system.webServer>
<serverRuntime />
<asp scriptErrorSentToBrowser="true">
<cache diskTemplateCacheDirectory="%TEMP%\iisexpress\ASP Compiled Templates" />
<limits />
</asp>
<caching enabled="true" enableKernelCache="true"></caching>
<cgi />
<defaultDocument enabled="true">
<files>
<add value="Default.htm" />
<add value="Default.asp" />
<add value="index.htm" />
<add value="index.html" />
<add value="iisstart.htm" />
<add value="default.aspx" />
</files>
</defaultDocument>
<directoryBrowse enabled="false" />
<fastCgi />
<!--
The <globalModules> section defines all native-code modules.
To enable a module, specify it in the <modules> section.
-->
<globalModules>
<add name="HttpLoggingModule" image="%IIS_BIN%\loghttp.dll" />
<add name="UriCacheModule" image="%IIS_BIN%\cachuri.dll" />
<add name="TokenCacheModule" image="%IIS_BIN%\cachtokn.dll" />
<add name="DynamicCompressionModule" image="%IIS_BIN%\compdyn.dll" />
<add name="StaticCompressionModule" image="%IIS_BIN%\compstat.dll" />
<add name="DefaultDocumentModule" image="%IIS_BIN%\defdoc.dll" />
<add name="DirectoryListingModule" image="%IIS_BIN%\dirlist.dll" />
<add name="ProtocolSupportModule" image="%IIS_BIN%\protsup.dll" />
<add name="HttpRedirectionModule" image="%IIS_BIN%\redirect.dll" />
<add name="ServerSideIncludeModule" image="%IIS_BIN%\iis_ssi.dll" />
<add name="StaticFileModule" image="%IIS_BIN%\static.dll" />
<add name="AnonymousAuthenticationModule" image="%IIS_BIN%\authanon.dll" />
<add name="CertificateMappingAuthenticationModule" image="%IIS_BIN%\authcert.dll" />
<add name="UrlAuthorizationModule" image="%IIS_BIN%\urlauthz.dll" />
<add name="BasicAuthenticationModule" image="%IIS_BIN%\authbas.dll" />
<add name="WindowsAuthenticationModule" image="%IIS_BIN%\authsspi.dll" />
<add name="IISCertificateMappingAuthenticationModule" image="%IIS_BIN%\authmap.dll" />
<add name="IpRestrictionModule" image="%IIS_BIN%\iprestr.dll" />
<add name="DynamicIpRestrictionModule" image="%IIS_BIN%\diprestr.dll" />
<add name="RequestFilteringModule" image="%IIS_BIN%\modrqflt.dll" />
<add name="CustomLoggingModule" image="%IIS_BIN%\logcust.dll" />
<add name="CustomErrorModule" image="%IIS_BIN%\custerr.dll" />
<add name="FailedRequestsTracingModule" image="%IIS_BIN%\iisfreb.dll" />
<add name="RequestMonitorModule" image="%IIS_BIN%\iisreqs.dll" />
<add name="IsapiModule" image="%IIS_BIN%\isapi.dll" />
<add name="IsapiFilterModule" image="%IIS_BIN%\filter.dll" />
<add name="CgiModule" image="%IIS_BIN%\cgi.dll" />
<add name="FastCgiModule" image="%IIS_BIN%\iisfcgi.dll" />
<!-- <add name="WebDAVModule" image="%IIS_BIN%\webdav.dll" /> -->
<add name="RewriteModule" image="%IIS_BIN%\rewrite.dll" />
<add name="ConfigurationValidationModule" image="%IIS_BIN%\validcfg.dll" />
<add name="WebSocketModule" image="%IIS_BIN%\iiswsock.dll" />
<add name="WebMatrixSupportModule" image="%IIS_BIN%\webmatrixsup.dll" />
<add name="ManagedEngine" image="%windir%\Microsoft.NET\Framework\v2.0.50727\webengine.dll" preCondition="integratedMode,runtimeVersionv2.0,bitness32" />
<add name="ManagedEngine64" image="%windir%\Microsoft.NET\Framework64\v2.0.50727\webengine.dll" preCondition="integratedMode,runtimeVersionv2.0,bitness64" />
<add name="ManagedEngineV4.0_32bit" image="%windir%\Microsoft.NET\Framework\v4.0.30319\webengine4.dll" preCondition="integratedMode,runtimeVersionv4.0,bitness32" />
<add name="ManagedEngineV4.0_64bit" image="%windir%\Microsoft.NET\Framework64\v4.0.30319\webengine4.dll" preCondition="integratedMode,runtimeVersionv4.0,bitness64" />
<add name="ApplicationInitializationModule" image="%IIS_BIN%\warmup.dll" />
<add name="AspNetCoreModule" image="%IIS_BIN%\aspnetcore.dll" />
<add name="AspNetCoreModuleV2" image="%IIS_BIN%\Asp.Net Core Module\V2\aspnetcorev2.dll" />
</globalModules>
<httpCompression directory="%TEMP%">
<scheme name="gzip" dll="%IIS_BIN%\gzip.dll" />
<dynamicTypes>
<add mimeType="text/*" enabled="true" />
<add mimeType="message/*" enabled="true" />
<add mimeType="application/x-javascript" enabled="true" />
<add mimeType="application/javascript" enabled="true" />
<add mimeType="*/*" enabled="false" />
<add mimeType="text/event-stream" enabled="false" />
</dynamicTypes>
<staticTypes>
<add mimeType="text/*" enabled="true" />
<add mimeType="message/*" enabled="true" />
<add mimeType="application/javascript" enabled="true" />
<add mimeType="application/atom+xml" enabled="true" />
<add mimeType="application/xaml+xml" enabled="true" />
<add mimeType="image/svg+xml" enabled="true" />
<add mimeType="*/*" enabled="false" />
</staticTypes>
</httpCompression>
<httpErrors lockAttributes="allowAbsolutePathsWhenDelegated,defaultPath">
<error statusCode="401" prefixLanguageFilePath="%IIS_BIN%\custerr" path="401.htm" />
<error statusCode="403" prefixLanguageFilePath="%IIS_BIN%\custerr" path="403.htm" />
<error statusCode="404" prefixLanguageFilePath="%IIS_BIN%\custerr" path="404.htm" />
<error statusCode="405" prefixLanguageFilePath="%IIS_BIN%\custerr" path="405.htm" />
<error statusCode="406" prefixLanguageFilePath="%IIS_BIN%\custerr" path="406.htm" />
<error statusCode="412" prefixLanguageFilePath="%IIS_BIN%\custerr" path="412.htm" />
<error statusCode="500" prefixLanguageFilePath="%IIS_BIN%\custerr" path="500.htm" />
<error statusCode="501" prefixLanguageFilePath="%IIS_BIN%\custerr" path="501.htm" />
<error statusCode="502" prefixLanguageFilePath="%IIS_BIN%\custerr" path="502.htm" />
</httpErrors>
<httpLogging dontLog="false" />
<httpProtocol>
<customHeaders>
<clear />
<add name="X-Powered-By" value="ASP.NET" />
</customHeaders>
<redirectHeaders>
<clear />
</redirectHeaders>
</httpProtocol>
<httpRedirect enabled="false" />
<httpTracing />
<isapiFilters>
<filter name="ASP.Net_2.0.50727-64" path="%windir%\Microsoft.NET\Framework64\v2.0.50727\aspnet_filter.dll" enableCache="true" preCondition="bitness64,runtimeVersionv2.0" />
<filter name="ASP.Net_2.0.50727.0" path="%windir%\Microsoft.NET\Framework\v2.0.50727\aspnet_filter.dll" enableCache="true" preCondition="bitness32,runtimeVersionv2.0" />
<filter name="ASP.Net_2.0_for_v1.1" path="%windir%\Microsoft.NET\Framework\v2.0.50727\aspnet_filter.dll" enableCache="true" preCondition="runtimeVersionv1.1" />
<filter name="ASP.Net_4.0_32bit" path="%windir%\Microsoft.NET\Framework\v4.0.30319\aspnet_filter.dll" enableCache="true" preCondition="bitness32,runtimeVersionv4.0" />
<filter name="ASP.Net_4.0_64bit" path="%windir%\Microsoft.NET\Framework64\v4.0.30319\aspnet_filter.dll" enableCache="true" preCondition="bitness64,runtimeVersionv4.0" />
</isapiFilters>
<odbcLogging />
<security>
<access sslFlags="None" />
<applicationDependencies>
<application name="Active Server Pages" groupId="ASP" />
</applicationDependencies>
<authentication>
<anonymousAuthentication enabled="true" userName="" />
<basicAuthentication enabled="false" />
<clientCertificateMappingAuthentication enabled="false" />
<digestAuthentication enabled="false" />
<iisClientCertificateMappingAuthentication enabled="false"></iisClientCertificateMappingAuthentication>
<windowsAuthentication enabled="false">
<providers>
<add value="Negotiate" />
<add value="NTLM" />
</providers>
</windowsAuthentication>
</authentication>
<authorization>
<add accessType="Allow" users="*" />
</authorization>
<ipSecurity allowUnlisted="true" />
<isapiCgiRestriction notListedIsapisAllowed="true" notListedCgisAllowed="true">
<add path="%windir%\Microsoft.NET\Framework64\v4.0.30319\webengine4.dll" allowed="true" groupId="ASP.NET_v4.0" description="ASP.NET_v4.0" />
<add path="%windir%\Microsoft.NET\Framework\v4.0.30319\webengine4.dll" allowed="true" groupId="ASP.NET_v4.0" description="ASP.NET_v4.0" />
<add path="%windir%\Microsoft.NET\Framework64\v2.0.50727\aspnet_isapi.dll" allowed="true" groupId="ASP.NET v2.0.50727" description="ASP.NET v2.0.50727" />
<add path="%windir%\Microsoft.NET\Framework\v2.0.50727\aspnet_isapi.dll" allowed="true" groupId="ASP.NET v2.0.50727" description="ASP.NET v2.0.50727" />
</isapiCgiRestriction>
<requestFiltering>
<fileExtensions allowUnlisted="true" applyToWebDAV="true">
<add fileExtension=".asa" allowed="false" />
<add fileExtension=".asax" allowed="false" />
<add fileExtension=".ascx" allowed="false" />
<add fileExtension=".master" allowed="false" />
<add fileExtension=".skin" allowed="false" />
<add fileExtension=".browser" allowed="false" />
<add fileExtension=".sitemap" allowed="false" />
<add fileExtension=".config" allowed="false" />
<add fileExtension=".cs" allowed="false" />
<add fileExtension=".csproj" allowed="false" />
<add fileExtension=".vb" allowed="false" />
<add fileExtension=".vbproj" allowed="false" />
<add fileExtension=".webinfo" allowed="false" />
<add fileExtension=".licx" allowed="false" />
<add fileExtension=".resx" allowed="false" />
<add fileExtension=".resources" allowed="false" />
<add fileExtension=".mdb" allowed="false" />
<add fileExtension=".vjsproj" allowed="false" />
<add fileExtension=".java" allowed="false" />
<add fileExtension=".jsl" allowed="false" />
<add fileExtension=".ldb" allowed="false" />
<add fileExtension=".dsdgm" allowed="false" />
<add fileExtension=".ssdgm" allowed="false" />
<add fileExtension=".lsad" allowed="false" />
<add fileExtension=".ssmap" allowed="false" />
<add fileExtension=".cd" allowed="false" />
<add fileExtension=".dsprototype" allowed="false" />
<add fileExtension=".lsaprototype" allowed="false" />
<add fileExtension=".sdm" allowed="false" />
<add fileExtension=".sdmDocument" allowed="false" />
<add fileExtension=".mdf" allowed="false" />
<add fileExtension=".ldf" allowed="false" />
<add fileExtension=".ad" allowed="false" />
<add fileExtension=".dd" allowed="false" />
<add fileExtension=".ldd" allowed="false" />
<add fileExtension=".sd" allowed="false" />
<add fileExtension=".adprototype" allowed="false" />
<add fileExtension=".lddprototype" allowed="false" />
<add fileExtension=".exclude" allowed="false" />
<add fileExtension=".refresh" allowed="false" />
<add fileExtension=".compiled" allowed="false" />
<add fileExtension=".msgx" allowed="false" />
<add fileExtension=".vsdisco" allowed="false" />
<add fileExtension=".rules" allowed="false" />
</fileExtensions>
<verbs allowUnlisted="true" applyToWebDAV="true" />
<hiddenSegments applyToWebDAV="true">
<add segment="web.config" />
<add segment="bin" />
<add segment="App_code" />
<add segment="App_GlobalResources" />
<add segment="App_LocalResources" />
<add segment="App_WebReferences" />
<add segment="App_Data" />
<add segment="App_Browsers" />
</hiddenSegments>
</requestFiltering>
</security>
<serverSideInclude ssiExecDisable="false" />
<staticContent lockAttributes="isDocFooterFileName">
<mimeMap fileExtension=".323" mimeType="text/h323" />
<mimeMap fileExtension=".3g2" mimeType="video/3gpp2" />
<mimeMap fileExtension=".3gp2" mimeType="video/3gpp2" />
<mimeMap fileExtension=".3gp" mimeType="video/3gpp" />
<mimeMap fileExtension=".3gpp" mimeType="video/3gpp" />
<mimeMap fileExtension=".aac" mimeType="audio/aac" />
<mimeMap fileExtension=".aaf" mimeType="application/octet-stream" />
<mimeMap fileExtension=".aca" mimeType="application/octet-stream" />
<mimeMap fileExtension=".accdb" mimeType="application/msaccess" />
<mimeMap fileExtension=".accde" mimeType="application/msaccess" />
<mimeMap fileExtension=".accdt" mimeType="application/msaccess" />
<mimeMap fileExtension=".acx" mimeType="application/internet-property-stream" />
<mimeMap fileExtension=".adt" mimeType="audio/vnd.dlna.adts" />
<mimeMap fileExtension=".adts" mimeType="audio/vnd.dlna.adts" />
<mimeMap fileExtension=".afm" mimeType="application/octet-stream" />
<mimeMap fileExtension=".ai" mimeType="application/postscript" />
<mimeMap fileExtension=".aif" mimeType="audio/x-aiff" />
<mimeMap fileExtension=".aifc" mimeType="audio/aiff" />
<mimeMap fileExtension=".aiff" mimeType="audio/aiff" />
<mimeMap fileExtension=".appcache" mimeType="text/cache-manifest" />
<mimeMap fileExtension=".application" mimeType="application/x-ms-application" />
<mimeMap fileExtension=".art" mimeType="image/x-jg" />
<mimeMap fileExtension=".asd" mimeType="application/octet-stream" />
<mimeMap fileExtension=".asf" mimeType="video/x-ms-asf" />
<mimeMap fileExtension=".asi" mimeType="application/octet-stream" />
<mimeMap fileExtension=".asm" mimeType="text/plain" />
<mimeMap fileExtension=".asr" mimeType="video/x-ms-asf" />
<mimeMap fileExtension=".asx" mimeType="video/x-ms-asf" />
<mimeMap fileExtension=".atom" mimeType="application/atom+xml" />
<mimeMap fileExtension=".au" mimeType="audio/basic" />
<mimeMap fileExtension=".avi" mimeType="video/avi" />
<mimeMap fileExtension=".axs" mimeType="application/olescript" />
<mimeMap fileExtension=".bas" mimeType="text/plain" />
<mimeMap fileExtension=".bcpio" mimeType="application/x-bcpio" />
<mimeMap fileExtension=".bin" mimeType="application/octet-stream" />
<mimeMap fileExtension=".bmp" mimeType="image/bmp" />
<mimeMap fileExtension=".c" mimeType="text/plain" />
<mimeMap fileExtension=".cab" mimeType="application/vnd.ms-cab-compressed" />
<mimeMap fileExtension=".calx" mimeType="application/vnd.ms-office.calx" />
<mimeMap fileExtension=".cat" mimeType="application/vnd.ms-pki.seccat" />
<mimeMap fileExtension=".cdf" mimeType="application/x-cdf" />
<mimeMap fileExtension=".chm" mimeType="application/octet-stream" />
<mimeMap fileExtension=".class" mimeType="application/x-java-applet" />
<mimeMap fileExtension=".clp" mimeType="application/x-msclip" />
<mimeMap fileExtension=".cmx" mimeType="image/x-cmx" />
<mimeMap fileExtension=".cnf" mimeType="text/plain" />
<mimeMap fileExtension=".cod" mimeType="image/cis-cod" />
<mimeMap fileExtension=".cpio" mimeType="application/x-cpio" />
<mimeMap fileExtension=".cpp" mimeType="text/plain" />
<mimeMap fileExtension=".crd" mimeType="application/x-mscardfile" />
<mimeMap fileExtension=".crl" mimeType="application/pkix-crl" />
<mimeMap fileExtension=".crt" mimeType="application/x-x509-ca-cert" />
<mimeMap fileExtension=".csh" mimeType="application/x-csh" />
<mimeMap fileExtension=".css" mimeType="text/css" />
<mimeMap fileExtension=".csv" mimeType="application/octet-stream" />
<mimeMap fileExtension=".cur" mimeType="application/octet-stream" />
<mimeMap fileExtension=".dcr" mimeType="application/x-director" />
<mimeMap fileExtension=".deploy" mimeType="application/octet-stream" />
<mimeMap fileExtension=".der" mimeType="application/x-x509-ca-cert" />
<mimeMap fileExtension=".dib" mimeType="image/bmp" />
<mimeMap fileExtension=".dir" mimeType="application/x-director" />
<mimeMap fileExtension=".disco" mimeType="text/xml" />
<mimeMap fileExtension=".dll" mimeType="application/x-msdownload" />
<mimeMap fileExtension=".dll.config" mimeType="text/xml" />
<mimeMap fileExtension=".dlm" mimeType="text/dlm" />
<mimeMap fileExtension=".doc" mimeType="application/msword" />
<mimeMap fileExtension=".docm" mimeType="application/vnd.ms-word.document.macroEnabled.12" />
<mimeMap fileExtension=".docx" mimeType="application/vnd.openxmlformats-officedocument.wordprocessingml.document" />
<mimeMap fileExtension=".dot" mimeType="application/msword" />
<mimeMap fileExtension=".dotm" mimeType="application/vnd.ms-word.template.macroEnabled.12" />
<mimeMap fileExtension=".dotx" mimeType="application/vnd.openxmlformats-officedocument.wordprocessingml.template" />
<mimeMap fileExtension=".dsp" mimeType="application/octet-stream" />
<mimeMap fileExtension=".dtd" mimeType="text/xml" />
<mimeMap fileExtension=".dvi" mimeType="application/x-dvi" />
<mimeMap fileExtension=".dvr-ms" mimeType="video/x-ms-dvr" />
<mimeMap fileExtension=".dwf" mimeType="drawing/x-dwf" />
<mimeMap fileExtension=".dwp" mimeType="application/octet-stream" />
<mimeMap fileExtension=".dxr" mimeType="application/x-director" />
<mimeMap fileExtension=".eml" mimeType="message/rfc822" />
<mimeMap fileExtension=".emz" mimeType="application/octet-stream" />
<mimeMap fileExtension=".eot" mimeType="application/vnd.ms-fontobject" />
<mimeMap fileExtension=".eps" mimeType="application/postscript" />
<mimeMap fileExtension=".esd" mimeType="application/vnd.ms-cab-compressed" />
<mimeMap fileExtension=".etx" mimeType="text/x-setext" />
<mimeMap fileExtension=".evy" mimeType="application/envoy" />
<mimeMap fileExtension=".exe" mimeType="application/octet-stream" />
<mimeMap fileExtension=".exe.config" mimeType="text/xml" />
<mimeMap fileExtension=".fdf" mimeType="application/vnd.fdf" />
<mimeMap fileExtension=".fif" mimeType="application/fractals" />
<mimeMap fileExtension=".fla" mimeType="application/octet-stream" />
<mimeMap fileExtension=".flr" mimeType="x-world/x-vrml" />
<mimeMap fileExtension=".flv" mimeType="video/x-flv" />
<mimeMap fileExtension=".gif" mimeType="image/gif" />
<mimeMap fileExtension=".glb" mimeType="model/gltf-binary" />
<mimeMap fileExtension=".gtar" mimeType="application/x-gtar" />
<mimeMap fileExtension=".gz" mimeType="application/x-gzip" />
<mimeMap fileExtension=".h" mimeType="text/plain" />
<mimeMap fileExtension=".hdf" mimeType="application/x-hdf" />
<mimeMap fileExtension=".hdml" mimeType="text/x-hdml" />
<mimeMap fileExtension=".hhc" mimeType="application/x-oleobject" />
<mimeMap fileExtension=".hhk" mimeType="application/octet-stream" />
<mimeMap fileExtension=".hhp" mimeType="application/octet-stream" />
<mimeMap fileExtension=".hlp" mimeType="application/winhlp" />
<mimeMap fileExtension=".hqx" mimeType="application/mac-binhex40" />
<mimeMap fileExtension=".hta" mimeType="application/hta" />
<mimeMap fileExtension=".htc" mimeType="text/x-component" />
<mimeMap fileExtension=".htm" mimeType="text/html" />
<mimeMap fileExtension=".html" mimeType="text/html" />
<mimeMap fileExtension=".htt" mimeType="text/webviewhtml" />
<mimeMap fileExtension=".hxt" mimeType="text/html" />
<mimeMap fileExtension=".ico" mimeType="image/x-icon" />
<mimeMap fileExtension=".ics" mimeType="text/calendar" />
<mimeMap fileExtension=".ief" mimeType="image/ief" />
<mimeMap fileExtension=".iii" mimeType="application/x-iphone" />
<mimeMap fileExtension=".inf" mimeType="application/octet-stream" />
<mimeMap fileExtension=".ins" mimeType="application/x-internet-signup" />
<mimeMap fileExtension=".isp" mimeType="application/x-internet-signup" />
<mimeMap fileExtension=".IVF" mimeType="video/x-ivf" />
<mimeMap fileExtension=".jar" mimeType="application/java-archive" />
<mimeMap fileExtension=".java" mimeType="application/octet-stream" />
<mimeMap fileExtension=".jck" mimeType="application/liquidmotion" />
<mimeMap fileExtension=".jcz" mimeType="application/liquidmotion" />
<mimeMap fileExtension=".jfif" mimeType="image/pjpeg" />
<mimeMap fileExtension=".jpb" mimeType="application/octet-stream" />
<mimeMap fileExtension=".jpe" mimeType="image/jpeg" />
<mimeMap fileExtension=".jpeg" mimeType="image/jpeg" />
<mimeMap fileExtension=".jpg" mimeType="image/jpeg" />
<mimeMap fileExtension=".js" mimeType="application/javascript" />
<mimeMap fileExtension=".json" mimeType="application/json" />
<mimeMap fileExtension=".jsonld" mimeType="application/ld+json" />
<mimeMap fileExtension=".jsx" mimeType="text/jscript" />
<mimeMap fileExtension=".latex" mimeType="application/x-latex" />
<mimeMap fileExtension=".less" mimeType="text/css" />
<mimeMap fileExtension=".lit" mimeType="application/x-ms-reader" />
<mimeMap fileExtension=".lpk" mimeType="application/octet-stream" />
<mimeMap fileExtension=".lsf" mimeType="video/x-la-asf" />
<mimeMap fileExtension=".lsx" mimeType="video/x-la-asf" />
<mimeMap fileExtension=".lzh" mimeType="application/octet-stream" />
<mimeMap fileExtension=".m13" mimeType="application/x-msmediaview" />
<mimeMap fileExtension=".m14" mimeType="application/x-msmediaview" />
<mimeMap fileExtension=".m1v" mimeType="video/mpeg" />
<mimeMap fileExtension=".m2ts" mimeType="video/vnd.dlna.mpeg-tts" />
<mimeMap fileExtension=".m3u" mimeType="audio/x-mpegurl" />
<mimeMap fileExtension=".m4a" mimeType="audio/mp4" />
<mimeMap fileExtension=".m4v" mimeType="video/mp4" />
<mimeMap fileExtension=".man" mimeType="application/x-troff-man" />
<mimeMap fileExtension=".manifest" mimeType="application/x-ms-manifest" />
<mimeMap fileExtension=".map" mimeType="text/plain" />
<mimeMap fileExtension=".mdb" mimeType="application/x-msaccess" />
<mimeMap fileExtension=".mdp" mimeType="application/octet-stream" />
<mimeMap fileExtension=".me" mimeType="application/x-troff-me" />
<mimeMap fileExtension=".mht" mimeType="message/rfc822" />
<mimeMap fileExtension=".mhtml" mimeType="message/rfc822" />
<mimeMap fileExtension=".mid" mimeType="audio/mid" />
<mimeMap fileExtension=".midi" mimeType="audio/mid" />
<mimeMap fileExtension=".mix" mimeType="application/octet-stream" />
<mimeMap fileExtension=".mmf" mimeType="application/x-smaf" />
<mimeMap fileExtension=".mno" mimeType="text/xml" />
<mimeMap fileExtension=".mny" mimeType="application/x-msmoney" />
<mimeMap fileExtension=".mov" mimeType="video/quicktime" />
<mimeMap fileExtension=".movie" mimeType="video/x-sgi-movie" />
<mimeMap fileExtension=".mp2" mimeType="video/mpeg" />
<mimeMap fileExtension=".mp3" mimeType="audio/mpeg" />
<mimeMap fileExtension=".mp4" mimeType="video/mp4" />
<mimeMap fileExtension=".mp4v" mimeType="video/mp4" />
<mimeMap fileExtension=".mpa" mimeType="video/mpeg" />
<mimeMap fileExtension=".mpe" mimeType="video/mpeg" />
<mimeMap fileExtension=".mpeg" mimeType="video/mpeg" />
<mimeMap fileExtension=".mpg" mimeType="video/mpeg" />
<mimeMap fileExtension=".mpp" mimeType="application/vnd.ms-project" />
<mimeMap fileExtension=".mpv2" mimeType="video/mpeg" />
<mimeMap fileExtension=".ms" mimeType="application/x-troff-ms" />
<mimeMap fileExtension=".msi" mimeType="application/octet-stream" />
<mimeMap fileExtension=".mso" mimeType="application/octet-stream" />
<mimeMap fileExtension=".mvb" mimeType="application/x-msmediaview" />
<mimeMap fileExtension=".mvc" mimeType="application/x-miva-compiled" />
<mimeMap fileExtension=".nc" mimeType="application/x-netcdf" />
<mimeMap fileExtension=".nsc" mimeType="video/x-ms-asf" />
<mimeMap fileExtension=".nws" mimeType="message/rfc822" />
<mimeMap fileExtension=".ocx" mimeType="application/octet-stream" />
<mimeMap fileExtension=".oda" mimeType="application/oda" />
<mimeMap fileExtension=".odc" mimeType="text/x-ms-odc" />
<mimeMap fileExtension=".ods" mimeType="application/oleobject" />
<mimeMap fileExtension=".oga" mimeType="audio/ogg" />
<mimeMap fileExtension=".ogg" mimeType="video/ogg" />
<mimeMap fileExtension=".ogv" mimeType="video/ogg" />
<mimeMap fileExtension=".one" mimeType="application/onenote" />
<mimeMap fileExtension=".onea" mimeType="application/onenote" />
<mimeMap fileExtension=".onetoc" mimeType="application/onenote" />
<mimeMap fileExtension=".onetoc2" mimeType="application/onenote" />
<mimeMap fileExtension=".onetmp" mimeType="application/onenote" />
<mimeMap fileExtension=".onepkg" mimeType="application/onenote" />
<mimeMap fileExtension=".osdx" mimeType="application/opensearchdescription+xml" />
<mimeMap fileExtension=".otf" mimeType="font/otf" />
<mimeMap fileExtension=".p10" mimeType="application/pkcs10" />
<mimeMap fileExtension=".p12" mimeType="application/x-pkcs12" />
<mimeMap fileExtension=".p7b" mimeType="application/x-pkcs7-certificates" />
<mimeMap fileExtension=".p7c" mimeType="application/pkcs7-mime" />
<mimeMap fileExtension=".p7m" mimeType="application/pkcs7-mime" />
<mimeMap fileExtension=".p7r" mimeType="application/x-pkcs7-certreqresp" />
<mimeMap fileExtension=".p7s" mimeType="application/pkcs7-signature" />
<mimeMap fileExtension=".pbm" mimeType="image/x-portable-bitmap" />
<mimeMap fileExtension=".pcx" mimeType="application/octet-stream" />
<mimeMap fileExtension=".pcz" mimeType="application/octet-stream" />
<mimeMap fileExtension=".pdf" mimeType="application/pdf" />
<mimeMap fileExtension=".pfb" mimeType="application/octet-stream" />
<mimeMap fileExtension=".pfm" mimeType="application/octet-stream" />
<mimeMap fileExtension=".pfx" mimeType="application/x-pkcs12" />
<mimeMap fileExtension=".pgm" mimeType="image/x-portable-graymap" />
<mimeMap fileExtension=".pko" mimeType="application/vnd.ms-pki.pko" />
<mimeMap fileExtension=".pma" mimeType="application/x-perfmon" />
<mimeMap fileExtension=".pmc" mimeType="application/x-perfmon" />
<mimeMap fileExtension=".pml" mimeType="application/x-perfmon" />
<mimeMap fileExtension=".pmr" mimeType="application/x-perfmon" />
<mimeMap fileExtension=".pmw" mimeType="application/x-perfmon" />
<mimeMap fileExtension=".png" mimeType="image/png" />
<mimeMap fileExtension=".pnm" mimeType="image/x-portable-anymap" />
<mimeMap fileExtension=".pnz" mimeType="image/png" />
<mimeMap fileExtension=".pot" mimeType="application/vnd.ms-powerpoint" />
<mimeMap fileExtension=".potm" mimeType="application/vnd.ms-powerpoint.template.macroEnabled.12" />
<mimeMap fileExtension=".potx" mimeType="application/vnd.openxmlformats-officedocument.presentationml.template" />
<mimeMap fileExtension=".ppam" mimeType="application/vnd.ms-powerpoint.addin.macroEnabled.12" />
<mimeMap fileExtension=".ppm" mimeType="image/x-portable-pixmap" />
<mimeMap fileExtension=".pps" mimeType="application/vnd.ms-powerpoint" />
<mimeMap fileExtension=".ppsm" mimeType="application/vnd.ms-powerpoint.slideshow.macroEnabled.12" />
<mimeMap fileExtension=".ppsx" mimeType="application/vnd.openxmlformats-officedocument.presentationml.slideshow" />
<mimeMap fileExtension=".ppt" mimeType="application/vnd.ms-powerpoint" />
<mimeMap fileExtension=".pptm" mimeType="application/vnd.ms-powerpoint.presentation.macroEnabled.12" />
<mimeMap fileExtension=".pptx" mimeType="application/vnd.openxmlformats-officedocument.presentationml.presentation" />
<mimeMap fileExtension=".prf" mimeType="application/pics-rules" />
<mimeMap fileExtension=".prm" mimeType="application/octet-stream" />
<mimeMap fileExtension=".prx" mimeType="application/octet-stream" />
<mimeMap fileExtension=".ps" mimeType="application/postscript" />
<mimeMap fileExtension=".psd" mimeType="application/octet-stream" />
<mimeMap fileExtension=".psm" mimeType="application/octet-stream" />
<mimeMap fileExtension=".psp" mimeType="application/octet-stream" />
<mimeMap fileExtension=".pub" mimeType="application/x-mspublisher" />
<mimeMap fileExtension=".qt" mimeType="video/quicktime" />
<mimeMap fileExtension=".qtl" mimeType="application/x-quicktimeplayer" />
<mimeMap fileExtension=".qxd" mimeType="application/octet-stream" />
<mimeMap fileExtension=".ra" mimeType="audio/x-pn-realaudio" />
<mimeMap fileExtension=".ram" mimeType="audio/x-pn-realaudio" />
<mimeMap fileExtension=".rar" mimeType="application/octet-stream" />
<mimeMap fileExtension=".ras" mimeType="image/x-cmu-raster" />
<mimeMap fileExtension=".rf" mimeType="image/vnd.rn-realflash" />
<mimeMap fileExtension=".rgb" mimeType="image/x-rgb" />
<mimeMap fileExtension=".rm" mimeType="application/vnd.rn-realmedia" />
<mimeMap fileExtension=".rmi" mimeType="audio/mid" />
<mimeMap fileExtension=".roff" mimeType="application/x-troff" />
<mimeMap fileExtension=".rpm" mimeType="audio/x-pn-realaudio-plugin" />
<mimeMap fileExtension=".rtf" mimeType="application/rtf" />
<mimeMap fileExtension=".rtx" mimeType="text/richtext" />
<mimeMap fileExtension=".scd" mimeType="application/x-msschedule" />
<mimeMap fileExtension=".sct" mimeType="text/scriptlet" />
<mimeMap fileExtension=".sea" mimeType="application/octet-stream" />
<mimeMap fileExtension=".setpay" mimeType="application/set-payment-initiation" />
<mimeMap fileExtension=".setreg" mimeType="application/set-registration-initiation" />
<mimeMap fileExtension=".sgml" mimeType="text/sgml" />
<mimeMap fileExtension=".sh" mimeType="application/x-sh" />
<mimeMap fileExtension=".shar" mimeType="application/x-shar" />
<mimeMap fileExtension=".sit" mimeType="application/x-stuffit" />
<mimeMap fileExtension=".sldm" mimeType="application/vnd.ms-powerpoint.slide.macroEnabled.12" />
<mimeMap fileExtension=".sldx" mimeType="application/vnd.openxmlformats-officedocument.presentationml.slide" />
<mimeMap fileExtension=".smd" mimeType="audio/x-smd" />
<mimeMap fileExtension=".smi" mimeType="application/octet-stream" />
<mimeMap fileExtension=".smx" mimeType="audio/x-smd" />
<mimeMap fileExtension=".smz" mimeType="audio/x-smd" />
<mimeMap fileExtension=".snd" mimeType="audio/basic" />
<mimeMap fileExtension=".snp" mimeType="application/octet-stream" />
<mimeMap fileExtension=".spc" mimeType="application/x-pkcs7-certificates" />
<mimeMap fileExtension=".spl" mimeType="application/futuresplash" />
<mimeMap fileExtension=".spx" mimeType="audio/ogg" />
<mimeMap fileExtension=".src" mimeType="application/x-wais-source" />
<mimeMap fileExtension=".ssm" mimeType="application/streamingmedia" />
<mimeMap fileExtension=".sst" mimeType="application/vnd.ms-pki.certstore" />
<mimeMap fileExtension=".stl" mimeType="application/vnd.ms-pki.stl" />
<mimeMap fileExtension=".sv4cpio" mimeType="application/x-sv4cpio" />
<mimeMap fileExtension=".sv4crc" mimeType="application/x-sv4crc" />
<mimeMap fileExtension=".svg" mimeType="image/svg+xml" />
<mimeMap fileExtension=".svgz" mimeType="image/svg+xml" />
<mimeMap fileExtension=".swf" mimeType="application/x-shockwave-flash" />
<mimeMap fileExtension=".t" mimeType="application/x-troff" />
<mimeMap fileExtension=".tar" mimeType="application/x-tar" />
<mimeMap fileExtension=".tcl" mimeType="application/x-tcl" />
<mimeMap fileExtension=".tex" mimeType="application/x-tex" />
<mimeMap fileExtension=".texi" mimeType="application/x-texinfo" />
<mimeMap fileExtension=".texinfo" mimeType="application/x-texinfo" />
<mimeMap fileExtension=".tgz" mimeType="application/x-compressed" />
<mimeMap fileExtension=".thmx" mimeType="application/vnd.ms-officetheme" />
<mimeMap fileExtension=".thn" mimeType="application/octet-stream" />
<mimeMap fileExtension=".tif" mimeType="image/tiff" />
<mimeMap fileExtension=".tiff" mimeType="image/tiff" />
<mimeMap fileExtension=".toc" mimeType="application/octet-stream" />
<mimeMap fileExtension=".tr" mimeType="application/x-troff" />
<mimeMap fileExtension=".trm" mimeType="application/x-msterminal" />
<mimeMap fileExtension=".ts" mimeType="video/vnd.dlna.mpeg-tts" />
<mimeMap fileExtension=".tsv" mimeType="text/tab-separated-values" />
<mimeMap fileExtension=".ttf" mimeType="application/octet-stream" />
<mimeMap fileExtension=".tts" mimeType="video/vnd.dlna.mpeg-tts" />
<mimeMap fileExtension=".txt" mimeType="text/plain" />
<mimeMap fileExtension=".u32" mimeType="application/octet-stream" />
<mimeMap fileExtension=".uls" mimeType="text/iuls" />
<mimeMap fileExtension=".ustar" mimeType="application/x-ustar" />
<mimeMap fileExtension=".vbs" mimeType="text/vbscript" />
<mimeMap fileExtension=".vcf" mimeType="text/x-vcard" />
<mimeMap fileExtension=".vcs" mimeType="text/plain" />
<mimeMap fileExtension=".vdx" mimeType="application/vnd.ms-visio.viewer" />
<mimeMap fileExtension=".vml" mimeType="text/xml" />
<mimeMap fileExtension=".vsd" mimeType="application/vnd.visio" />
<mimeMap fileExtension=".vss" mimeType="application/vnd.visio" />
<mimeMap fileExtension=".vst" mimeType="application/vnd.visio" />
<mimeMap fileExtension=".vsto" mimeType="application/x-ms-vsto" />
<mimeMap fileExtension=".vsw" mimeType="application/vnd.visio" />
<mimeMap fileExtension=".vsx" mimeType="application/vnd.visio" />
<mimeMap fileExtension=".vtx" mimeType="application/vnd.visio" />
<mimeMap fileExtension=".wasm" mimeType="application/wasm" />
<mimeMap fileExtension=".wav" mimeType="audio/wav" />
<mimeMap fileExtension=".wax" mimeType="audio/x-ms-wax" />
<mimeMap fileExtension=".wbmp" mimeType="image/vnd.wap.wbmp" />
<mimeMap fileExtension=".wcm" mimeType="application/vnd.ms-works" />
<mimeMap fileExtension=".wdb" mimeType="application/vnd.ms-works" />
<mimeMap fileExtension=".webm" mimeType="video/webm" />
<mimeMap fileExtension=".wks" mimeType="application/vnd.ms-works" />
<mimeMap fileExtension=".wm" mimeType="video/x-ms-wm" />
<mimeMap fileExtension=".wma" mimeType="audio/x-ms-wma" />
<mimeMap fileExtension=".wmd" mimeType="application/x-ms-wmd" />
<mimeMap fileExtension=".wmf" mimeType="application/x-msmetafile" />
<mimeMap fileExtension=".wml" mimeType="text/vnd.wap.wml" />
<mimeMap fileExtension=".wmlc" mimeType="application/vnd.wap.wmlc" />
<mimeMap fileExtension=".wmls" mimeType="text/vnd.wap.wmlscript" />
<mimeMap fileExtension=".wmlsc" mimeType="application/vnd.wap.wmlscriptc" />
<mimeMap fileExtension=".wmp" mimeType="video/x-ms-wmp" />
<mimeMap fileExtension=".wmv" mimeType="video/x-ms-wmv" />
<mimeMap fileExtension=".wmx" mimeType="video/x-ms-wmx" />
<mimeMap fileExtension=".wmz" mimeType="application/x-ms-wmz" />
<mimeMap fileExtension=".woff" mimeType="font/x-woff" />
<mimeMap fileExtension=".woff2" mimeType="application/font-woff2" />
<mimeMap fileExtension=".wps" mimeType="application/vnd.ms-works" />
<mimeMap fileExtension=".wri" mimeType="application/x-mswrite" />
<mimeMap fileExtension=".wrl" mimeType="x-world/x-vrml" />
<mimeMap fileExtension=".wrz" mimeType="x-world/x-vrml" />
<mimeMap fileExtension=".wsdl" mimeType="text/xml" />
<mimeMap fileExtension=".wtv" mimeType="video/x-ms-wtv" />
<mimeMap fileExtension=".wvx" mimeType="video/x-ms-wvx" />
<mimeMap fileExtension=".x" mimeType="application/directx" />
<mimeMap fileExtension=".xaf" mimeType="x-world/x-vrml" />
<mimeMap fileExtension=".xaml" mimeType="application/xaml+xml" />
<mimeMap fileExtension=".xap" mimeType="application/x-silverlight-app" />
<mimeMap fileExtension=".xbap" mimeType="application/x-ms-xbap" />
<mimeMap fileExtension=".xbm" mimeType="image/x-xbitmap" />
<mimeMap fileExtension=".xdr" mimeType="text/plain" />
<mimeMap fileExtension=".xht" mimeType="application/xhtml+xml" />
<mimeMap fileExtension=".xhtml" mimeType="application/xhtml+xml" />
<mimeMap fileExtension=".xla" mimeType="application/vnd.ms-excel" />
<mimeMap fileExtension=".xlam" mimeType="application/vnd.ms-excel.addin.macroEnabled.12" />
<mimeMap fileExtension=".xlc" mimeType="application/vnd.ms-excel" />
<mimeMap fileExtension=".xlm" mimeType="application/vnd.ms-excel" />
<mimeMap fileExtension=".xls" mimeType="application/vnd.ms-excel" />
<mimeMap fileExtension=".xlsb" mimeType="application/vnd.ms-excel.sheet.binary.macroEnabled.12" />
<mimeMap fileExtension=".xlsm" mimeType="application/vnd.ms-excel.sheet.macroEnabled.12" />
<mimeMap fileExtension=".xlsx" mimeType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" />
<mimeMap fileExtension=".xlt" mimeType="application/vnd.ms-excel" />
<mimeMap fileExtension=".xltm" mimeType="application/vnd.ms-excel.template.macroEnabled.12" />
<mimeMap fileExtension=".xltx" mimeType="application/vnd.openxmlformats-officedocument.spreadsheetml.template" />
<mimeMap fileExtension=".xlw" mimeType="application/vnd.ms-excel" />
<mimeMap fileExtension=".xml" mimeType="text/xml" />
<mimeMap fileExtension=".xof" mimeType="x-world/x-vrml" />
<mimeMap fileExtension=".xpm" mimeType="image/x-xpixmap" />
<mimeMap fileExtension=".xps" mimeType="application/vnd.ms-xpsdocument" />
<mimeMap fileExtension=".xsd" mimeType="text/xml" />
<mimeMap fileExtension=".xsf" mimeType="text/xml" />
<mimeMap fileExtension=".xsl" mimeType="text/xml" />
<mimeMap fileExtension=".xslt" mimeType="text/xml" />
<mimeMap fileExtension=".xsn" mimeType="application/octet-stream" />
<mimeMap fileExtension=".xtp" mimeType="application/octet-stream" />
<mimeMap fileExtension=".xwd" mimeType="image/x-xwindowdump" />
<mimeMap fileExtension=".z" mimeType="application/x-compress" />
<mimeMap fileExtension=".zip" mimeType="application/x-zip-compressed" />
</staticContent>
<tracing>
<traceFailedRequests>
<add path="*">
<traceAreas>
<add provider="ASP" verbosity="Verbose" />
<add provider="ASPNET" areas="Infrastructure,Module,Page,AppServices" verbosity="Verbose" />
<add provider="ISAPI Extension" verbosity="Verbose" />
<add provider="WWW Server" areas="Authentication,Security,Filter,StaticFile,CGI,Compression,Cache,RequestNotifications,Module,Rewrite,WebSocket" verbosity="Verbose" />
</traceAreas>
<failureDefinitions statusCodes="200-999" />
</add>
</traceFailedRequests>
<traceProviderDefinitions>
<add name="WWW Server" guid="{3a2a4e84-4c21-4981-ae10-3fda0d9b0f83}">
<areas>
<clear />
<add name="Authentication" value="2" />
<add name="Security" value="4" />
<add name="Filter" value="8" />
<add name="StaticFile" value="16" />
<add name="CGI" value="32" />
<add name="Compression" value="64" />
<add name="Cache" value="128" />
<add name="RequestNotifications" value="256" />
<add name="Module" value="512" />
<add name="Rewrite" value="1024" />
<add name="FastCGI" value="4096" />
<add name="WebSocket" value="16384" />
<add name="ANCM" value="65536" />
</areas>
</add>
<add name="ASP" guid="{06b94d9a-b15e-456e-a4ef-37c984a2cb4b}">
<areas>
<clear />
</areas>
</add>
<add name="ISAPI Extension" guid="{a1c2040e-8840-4c31-ba11-9871031a19ea}">
<areas>
<clear />
</areas>
</add>
<add name="ASPNET" guid="{AFF081FE-0247-4275-9C4E-021F3DC1DA35}">
<areas>
<add name="Infrastructure" value="1" />
<add name="Module" value="2" />
<add name="Page" value="4" />
<add name="AppServices" value="8" />
</areas>
</add>
</traceProviderDefinitions>
</tracing>
<urlCompression />
<validation />
<webdav>
<globalSettings>
<propertyStores>
<add name="webdav_simple_prop" image="%IIS_BIN%\webdav_simple_prop.dll" image32="%IIS_BIN%\webdav_simple_prop.dll" />
</propertyStores>
<lockStores>
<add name="webdav_simple_lock" image="%IIS_BIN%\webdav_simple_lock.dll" image32="%IIS_BIN%\webdav_simple_lock.dll" />
</lockStores>
</globalSettings>
<authoring>
<locks enabled="true" lockStore="webdav_simple_lock" />
</authoring>
<authoringRules />
</webdav>
<webSocket />
<applicationInitialization />
</system.webServer>
<location path="" overrideMode="Allow">
<system.webServer>
<modules>
<add name="IsapiFilterModule" lockItem="true" />
<add name="BasicAuthenticationModule" lockItem="true" />
<add name="IsapiModule" lockItem="true" />
<add name="HttpLoggingModule" lockItem="true" />
<add name="DynamicCompressionModule" lockItem="true" />
<add name="StaticCompressionModule" lockItem="true" />
<add name="DefaultDocumentModule" lockItem="true" />
<add name="DirectoryListingModule" lockItem="true" />
<add name="ProtocolSupportModule" lockItem="true" />
<add name="HttpRedirectionModule" lockItem="true" />
<add name="ServerSideIncludeModule" lockItem="true" />
<add name="StaticFileModule" lockItem="true" />
<add name="AnonymousAuthenticationModule" lockItem="false" />
<add name="CertificateMappingAuthenticationModule" lockItem="true" />
<add name="UrlAuthorizationModule" lockItem="true" />
<add name="WindowsAuthenticationModule" lockItem="false" />
<add name="IISCertificateMappingAuthenticationModule" lockItem="true" />
<add name="WebMatrixSupportModule" lockItem="true" />
<add name="IpRestrictionModule" lockItem="true" />
<add name="DynamicIpRestrictionModule" lockItem="true" />
<add name="RequestFilteringModule" lockItem="true" />
<add name="CustomLoggingModule" lockItem="true" />
<add name="CustomErrorModule" lockItem="true" />
<add name="FailedRequestsTracingModule" lockItem="true" />
<add name="CgiModule" lockItem="true" />
<add name="FastCgiModule" lockItem="true" />
<!-- <add name="WebDAVModule" /> -->
<add name="RewriteModule" />
<add name="OutputCache" type="System.Web.Caching.OutputCacheModule" preCondition="managedHandler" />
<add name="Session" type="System.Web.SessionState.SessionStateModule" preCondition="managedHandler" />
<add name="WindowsAuthentication" type="System.Web.Security.WindowsAuthenticationModule" preCondition="managedHandler" />
<add name="FormsAuthentication" type="System.Web.Security.FormsAuthenticationModule" preCondition="managedHandler" />
<add name="DefaultAuthentication" type="System.Web.Security.DefaultAuthenticationModule" preCondition="managedHandler" />
<add name="RoleManager" type="System.Web.Security.RoleManagerModule" preCondition="managedHandler" />
<add name="UrlAuthorization" type="System.Web.Security.UrlAuthorizationModule" preCondition="managedHandler" />
<add name="FileAuthorization" type="System.Web.Security.FileAuthorizationModule" preCondition="managedHandler" />
<add name="AnonymousIdentification" type="System.Web.Security.AnonymousIdentificationModule" preCondition="managedHandler" />
<add name="Profile" type="System.Web.Profile.ProfileModule" preCondition="managedHandler" />
<add name="UrlMappingsModule" type="System.Web.UrlMappingsModule" preCondition="managedHandler" />
<add name="ApplicationInitializationModule" lockItem="true" />
<add name="WebSocketModule" lockItem="true" />
<add name="ServiceModel-4.0" type="System.ServiceModel.Activation.ServiceHttpModule,System.ServiceModel.Activation,Version=4.0.0.0,Culture=neutral,PublicKeyToken=31bf3856ad364e35" preCondition="managedHandler,runtimeVersionv4.0" />
<add name="ConfigurationValidationModule" lockItem="true" />
<add name="UrlRoutingModule-4.0" type="System.Web.Routing.UrlRoutingModule" preCondition="managedHandler,runtimeVersionv4.0" />
<add name="ScriptModule-4.0" type="System.Web.Handlers.ScriptModule, System.Web.Extensions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" preCondition="managedHandler,runtimeVersionv4.0" />
<add name="ServiceModel" type="System.ServiceModel.Activation.HttpModule, System.ServiceModel, Version=3.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" preCondition="managedHandler,runtimeVersionv2.0" />
<add name="AspNetCoreModule" lockItem="true" />
<add name="AspNetCoreModuleV2" lockItem="true" />
</modules>
<handlers accessPolicy="Read, Script">
<!-- <add name="WebDAV" path="*" verb="PROPFIND,PROPPATCH,MKCOL,PUT,COPY,DELETE,MOVE,LOCK,UNLOCK" modules="WebDAVModule" resourceType="Unspecified" requireAccess="None" /> -->
<add name="AXD-ISAPI-4.0_64bit" path="*.axd" verb="GET,HEAD,POST,DEBUG" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework64\v4.0.30319\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv4.0,bitness64" responseBufferLimit="0" />
<add name="PageHandlerFactory-ISAPI-4.0_64bit" path="*.aspx" verb="GET,HEAD,POST,DEBUG" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework64\v4.0.30319\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv4.0,bitness64" responseBufferLimit="0" />
<add name="SimpleHandlerFactory-ISAPI-4.0_64bit" path="*.ashx" verb="GET,HEAD,POST,DEBUG" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework64\v4.0.30319\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv4.0,bitness64" responseBufferLimit="0" />
<add name="WebServiceHandlerFactory-ISAPI-4.0_64bit" path="*.asmx" verb="GET,HEAD,POST,DEBUG" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework64\v4.0.30319\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv4.0,bitness64" responseBufferLimit="0" />
<add name="HttpRemotingHandlerFactory-rem-ISAPI-4.0_64bit" path="*.rem" verb="GET,HEAD,POST,DEBUG" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework64\v4.0.30319\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv4.0,bitness64" responseBufferLimit="0" />
<add name="HttpRemotingHandlerFactory-soap-ISAPI-4.0_64bit" path="*.soap" verb="GET,HEAD,POST,DEBUG" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework64\v4.0.30319\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv4.0,bitness64" responseBufferLimit="0" />
<add name="svc-ISAPI-4.0_64bit" path="*.svc" verb="*" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework64\v4.0.30319\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv4.0,bitness64" />
<add name="rules-ISAPI-4.0_64bit" path="*.rules" verb="*" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework64\v4.0.30319\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv4.0,bitness64" />
<add name="xoml-ISAPI-4.0_64bit" path="*.xoml" verb="*" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework64\v4.0.30319\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv4.0,bitness64" />
<add name="xamlx-ISAPI-4.0_64bit" path="*.xamlx" verb="GET,HEAD,POST,DEBUG" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework64\v4.0.30319\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv4.0,bitness64" />
<add name="aspq-ISAPI-4.0_64bit" path="*.aspq" verb="*" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework64\v4.0.30319\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv4.0,bitness64" responseBufferLimit="0" />
<add name="cshtm-ISAPI-4.0_64bit" path="*.cshtm" verb="GET,HEAD,POST,DEBUG" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework64\v4.0.30319\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv4.0,bitness64" responseBufferLimit="0" />
<add name="cshtml-ISAPI-4.0_64bit" path="*.cshtml" verb="GET,HEAD,POST,DEBUG" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework64\v4.0.30319\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv4.0,bitness64" responseBufferLimit="0" />
<add name="vbhtm-ISAPI-4.0_64bit" path="*.vbhtm" verb="GET,HEAD,POST,DEBUG" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework64\v4.0.30319\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv4.0,bitness64" responseBufferLimit="0" />
<add name="vbhtml-ISAPI-4.0_64bit" path="*.vbhtml" verb="GET,HEAD,POST,DEBUG" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework64\v4.0.30319\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv4.0,bitness64" responseBufferLimit="0" />
<add name="svc-Integrated" path="*.svc" verb="*" type="System.ServiceModel.Activation.HttpHandler, System.ServiceModel, Version=3.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" preCondition="integratedMode,runtimeVersionv2.0" />
<add name="svc-ISAPI-2.0" path="*.svc" verb="*" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework\v2.0.50727\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv2.0,bitness32" />
<add name="xoml-Integrated" path="*.xoml" verb="*" type="System.ServiceModel.Activation.HttpHandler, System.ServiceModel, Version=3.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" preCondition="integratedMode,runtimeVersionv2.0" />
<add name="xoml-ISAPI-2.0" path="*.xoml" verb="*" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework\v2.0.50727\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv2.0,bitness32" />
<add name="rules-Integrated" path="*.rules" verb="*" type="System.ServiceModel.Activation.HttpHandler, System.ServiceModel, Version=3.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" preCondition="integratedMode,runtimeVersionv2.0" />
<add name="rules-ISAPI-2.0" path="*.rules" verb="*" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework\v2.0.50727\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv2.0,bitness32" />
<add name="AXD-ISAPI-4.0_32bit" path="*.axd" verb="GET,HEAD,POST,DEBUG" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework\v4.0.30319\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv4.0,bitness32" responseBufferLimit="0" />
<add name="PageHandlerFactory-ISAPI-4.0_32bit" path="*.aspx" verb="GET,HEAD,POST,DEBUG" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework\v4.0.30319\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv4.0,bitness32" responseBufferLimit="0" />
<add name="SimpleHandlerFactory-ISAPI-4.0_32bit" path="*.ashx" verb="GET,HEAD,POST,DEBUG" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework\v4.0.30319\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv4.0,bitness32" responseBufferLimit="0" />
<add name="WebServiceHandlerFactory-ISAPI-4.0_32bit" path="*.asmx" verb="GET,HEAD,POST,DEBUG" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework\v4.0.30319\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv4.0,bitness32" responseBufferLimit="0" />
<add name="HttpRemotingHandlerFactory-rem-ISAPI-4.0_32bit" path="*.rem" verb="GET,HEAD,POST,DEBUG" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework\v4.0.30319\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv4.0,bitness32" responseBufferLimit="0" />
<add name="HttpRemotingHandlerFactory-soap-ISAPI-4.0_32bit" path="*.soap" verb="GET,HEAD,POST,DEBUG" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework\v4.0.30319\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv4.0,bitness32" responseBufferLimit="0" />
<add name="svc-ISAPI-4.0_32bit" path="*.svc" verb="*" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework\v4.0.30319\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv4.0,bitness32" />
<add name="rules-ISAPI-4.0_32bit" path="*.rules" verb="*" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework\v4.0.30319\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv4.0,bitness32" />
<add name="xoml-ISAPI-4.0_32bit" path="*.xoml" verb="*" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework\v4.0.30319\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv4.0,bitness32" />
<add name="xamlx-ISAPI-4.0_32bit" path="*.xamlx" verb="GET,HEAD,POST,DEBUG" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework\v4.0.30319\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv4.0,bitness32" />
<add name="aspq-ISAPI-4.0_32bit" path="*.aspq" verb="*" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework\v4.0.30319\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv4.0,bitness32" responseBufferLimit="0" />
<add name="cshtm-ISAPI-4.0_32bit" path="*.cshtm" verb="GET,HEAD,POST,DEBUG" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework\v4.0.30319\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv4.0,bitness32" responseBufferLimit="0" />
<add name="cshtml-ISAPI-4.0_32bit" path="*.cshtml" verb="GET,HEAD,POST,DEBUG" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework\v4.0.30319\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv4.0,bitness32" responseBufferLimit="0" />
<add name="vbhtm-ISAPI-4.0_32bit" path="*.vbhtm" verb="GET,HEAD,POST,DEBUG" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework\v4.0.30319\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv4.0,bitness32" responseBufferLimit="0" />
<add name="vbhtml-ISAPI-4.0_32bit" path="*.vbhtml" verb="GET,HEAD,POST,DEBUG" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework\v4.0.30319\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv4.0,bitness32" responseBufferLimit="0" />
<add name="TraceHandler-Integrated-4.0" path="trace.axd" verb="GET,HEAD,POST,DEBUG" type="System.Web.Handlers.TraceHandler" preCondition="integratedMode,runtimeVersionv4.0" />
<add name="WebAdminHandler-Integrated-4.0" path="WebAdmin.axd" verb="GET,DEBUG" type="System.Web.Handlers.WebAdminHandler" preCondition="integratedMode,runtimeVersionv4.0" />
<add name="AssemblyResourceLoader-Integrated-4.0" path="WebResource.axd" verb="GET,DEBUG" type="System.Web.Handlers.AssemblyResourceLoader" preCondition="integratedMode,runtimeVersionv4.0" />
<add name="PageHandlerFactory-Integrated-4.0" path="*.aspx" verb="GET,HEAD,POST,DEBUG" type="System.Web.UI.PageHandlerFactory" preCondition="integratedMode,runtimeVersionv4.0" />
<add name="SimpleHandlerFactory-Integrated-4.0" path="*.ashx" verb="GET,HEAD,POST,DEBUG" type="System.Web.UI.SimpleHandlerFactory" preCondition="integratedMode,runtimeVersionv4.0" />
<add name="WebServiceHandlerFactory-Integrated-4.0" path="*.asmx" verb="GET,HEAD,POST,DEBUG" type="System.Web.Script.Services.ScriptHandlerFactory, System.Web.Extensions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" preCondition="integratedMode,runtimeVersionv4.0" />
<add name="HttpRemotingHandlerFactory-rem-Integrated-4.0" path="*.rem" verb="GET,HEAD,POST,DEBUG" type="System.Runtime.Remoting.Channels.Http.HttpRemotingHandlerFactory, System.Runtime.Remoting, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" preCondition="integratedMode,runtimeVersionv4.0" />
<add name="HttpRemotingHandlerFactory-soap-Integrated-4.0" path="*.soap" verb="GET,HEAD,POST,DEBUG" type="System.Runtime.Remoting.Channels.Http.HttpRemotingHandlerFactory, System.Runtime.Remoting, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" preCondition="integratedMode,runtimeVersionv4.0" />
<add name="svc-Integrated-4.0" path="*.svc" verb="*" type="System.ServiceModel.Activation.ServiceHttpHandlerFactory, System.ServiceModel.Activation, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" preCondition="integratedMode,runtimeVersionv4.0" />
<add name="rules-Integrated-4.0" path="*.rules" verb="*" type="System.ServiceModel.Activation.ServiceHttpHandlerFactory, System.ServiceModel.Activation, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" preCondition="integratedMode,runtimeVersionv4.0" />
<add name="xoml-Integrated-4.0" path="*.xoml" verb="*" type="System.ServiceModel.Activation.ServiceHttpHandlerFactory, System.ServiceModel.Activation, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" preCondition="integratedMode,runtimeVersionv4.0" />
<add name="xamlx-Integrated-4.0" path="*.xamlx" verb="GET,HEAD,POST,DEBUG" type="System.Xaml.Hosting.XamlHttpHandlerFactory, System.Xaml.Hosting, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" preCondition="integratedMode,runtimeVersionv4.0" />
<add name="aspq-Integrated-4.0" path="*.aspq" verb="GET,HEAD,POST,DEBUG" type="System.Web.HttpForbiddenHandler" preCondition="integratedMode,runtimeVersionv4.0" />
<add name="cshtm-Integrated-4.0" path="*.cshtm" verb="GET,HEAD,POST,DEBUG" type="System.Web.HttpForbiddenHandler" preCondition="integratedMode,runtimeVersionv4.0" />
<add name="cshtml-Integrated-4.0" path="*.cshtml" verb="GET,HEAD,POST,DEBUG" type="System.Web.HttpForbiddenHandler" preCondition="integratedMode,runtimeVersionv4.0" />
<add name="vbhtm-Integrated-4.0" path="*.vbhtm" verb="GET,HEAD,POST,DEBUG" type="System.Web.HttpForbiddenHandler" preCondition="integratedMode,runtimeVersionv4.0" />
<add name="vbhtml-Integrated-4.0" path="*.vbhtml" verb="GET,HEAD,POST,DEBUG" type="System.Web.HttpForbiddenHandler" preCondition="integratedMode,runtimeVersionv4.0" />
<add name="ScriptHandlerFactoryAppServices-Integrated-4.0" path="*_AppService.axd" verb="*" type="System.Web.Script.Services.ScriptHandlerFactory, System.Web.Extensions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" preCondition="integratedMode,runtimeVersionv4.0" />
<add name="ScriptResourceIntegrated-4.0" path="*ScriptResource.axd" verb="GET,HEAD" type="System.Web.Handlers.ScriptResourceHandler, System.Web.Extensions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" preCondition="integratedMode,runtimeVersionv4.0" />
<add name="ASPClassic" path="*.asp" verb="GET,HEAD,POST" modules="IsapiModule" scriptProcessor="%IIS_BIN%\asp.dll" resourceType="File" />
<add name="SecurityCertificate" path="*.cer" verb="GET,HEAD,POST" modules="IsapiModule" scriptProcessor="%IIS_BIN%\asp.dll" resourceType="File" />
<add name="ISAPI-dll" path="*.dll" verb="*" modules="IsapiModule" resourceType="File" requireAccess="Execute" allowPathInfo="true" />
<add name="TraceHandler-Integrated" path="trace.axd" verb="GET,HEAD,POST,DEBUG" type="System.Web.Handlers.TraceHandler" preCondition="integratedMode,runtimeVersionv2.0" />
<add name="WebAdminHandler-Integrated" path="WebAdmin.axd" verb="GET,DEBUG" type="System.Web.Handlers.WebAdminHandler" preCondition="integratedMode,runtimeVersionv2.0" />
<add name="AssemblyResourceLoader-Integrated" path="WebResource.axd" verb="GET,DEBUG" type="System.Web.Handlers.AssemblyResourceLoader" preCondition="integratedMode,runtimeVersionv2.0" />
<add name="PageHandlerFactory-Integrated" path="*.aspx" verb="GET,HEAD,POST,DEBUG" type="System.Web.UI.PageHandlerFactory" preCondition="integratedMode,runtimeVersionv2.0" />
<add name="SimpleHandlerFactory-Integrated" path="*.ashx" verb="GET,HEAD,POST,DEBUG" type="System.Web.UI.SimpleHandlerFactory" preCondition="integratedMode,runtimeVersionv2.0" />
<add name="WebServiceHandlerFactory-Integrated" path="*.asmx" verb="GET,HEAD,POST,DEBUG" type="System.Web.Services.Protocols.WebServiceHandlerFactory,System.Web.Services,Version=2.0.0.0,Culture=neutral,PublicKeyToken=b03f5f7f11d50a3a" preCondition="integratedMode,runtimeVersionv2.0" />
<add name="HttpRemotingHandlerFactory-rem-Integrated" path="*.rem" verb="GET,HEAD,POST,DEBUG" type="System.Runtime.Remoting.Channels.Http.HttpRemotingHandlerFactory,System.Runtime.Remoting,Version=2.0.0.0,Culture=neutral,PublicKeyToken=b77a5c561934e089" preCondition="integratedMode,runtimeVersionv2.0" />
<add name="HttpRemotingHandlerFactory-soap-Integrated" path="*.soap" verb="GET,HEAD,POST,DEBUG" type="System.Runtime.Remoting.Channels.Http.HttpRemotingHandlerFactory,System.Runtime.Remoting,Version=2.0.0.0,Culture=neutral,PublicKeyToken=b77a5c561934e089" preCondition="integratedMode,runtimeVersionv2.0" />
<add name="AXD-ISAPI-2.0" path="*.axd" verb="GET,HEAD,POST,DEBUG" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework\v2.0.50727\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv2.0,bitness32" responseBufferLimit="0" />
<add name="PageHandlerFactory-ISAPI-2.0" path="*.aspx" verb="GET,HEAD,POST,DEBUG" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework\v2.0.50727\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv2.0,bitness32" responseBufferLimit="0" />
<add name="SimpleHandlerFactory-ISAPI-2.0" path="*.ashx" verb="GET,HEAD,POST,DEBUG" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework\v2.0.50727\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv2.0,bitness32" responseBufferLimit="0" />
<add name="WebServiceHandlerFactory-ISAPI-2.0" path="*.asmx" verb="GET,HEAD,POST,DEBUG" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework\v2.0.50727\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv2.0,bitness32" responseBufferLimit="0" />
<add name="HttpRemotingHandlerFactory-rem-ISAPI-2.0" path="*.rem" verb="GET,HEAD,POST,DEBUG" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework\v2.0.50727\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv2.0,bitness32" responseBufferLimit="0" />
<add name="HttpRemotingHandlerFactory-soap-ISAPI-2.0" path="*.soap" verb="GET,HEAD,POST,DEBUG" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework\v2.0.50727\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv2.0,bitness32" responseBufferLimit="0" />
<add name="svc-ISAPI-2.0-64" path="*.svc" verb="*" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework64\v2.0.50727\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv2.0,bitness64" />
<add name="AXD-ISAPI-2.0-64" path="*.axd" verb="GET,HEAD,POST,DEBUG" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework64\v2.0.50727\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv2.0,bitness64" responseBufferLimit="0" />
<add name="PageHandlerFactory-ISAPI-2.0-64" path="*.aspx" verb="GET,HEAD,POST,DEBUG" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework64\v2.0.50727\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv2.0,bitness64" responseBufferLimit="0" />
<add name="SimpleHandlerFactory-ISAPI-2.0-64" path="*.ashx" verb="GET,HEAD,POST,DEBUG" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework64\v2.0.50727\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv2.0,bitness64" responseBufferLimit="0" />
<add name="WebServiceHandlerFactory-ISAPI-2.0-64" path="*.asmx" verb="GET,HEAD,POST,DEBUG" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework64\v2.0.50727\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv2.0,bitness64" responseBufferLimit="0" />
<add name="HttpRemotingHandlerFactory-rem-ISAPI-2.0-64" path="*.rem" verb="GET,HEAD,POST,DEBUG" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework64\v2.0.50727\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv2.0,bitness64" responseBufferLimit="0" />
<add name="HttpRemotingHandlerFactory-soap-ISAPI-2.0-64" path="*.soap" verb="GET,HEAD,POST,DEBUG" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework64\v2.0.50727\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv2.0,bitness64" responseBufferLimit="0" />
<add name="rules-64-ISAPI-2.0" path="*.rules" verb="*" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework64\v2.0.50727\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv2.0,bitness64" />
<add name="xoml-64-ISAPI-2.0" path="*.xoml" verb="*" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework64\v2.0.50727\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv2.0,bitness64" />
<add name="CGI-exe" path="*.exe" verb="*" modules="CgiModule" resourceType="File" requireAccess="Execute" allowPathInfo="true" />
<add name="SSINC-stm" path="*.stm" verb="GET,HEAD,POST" modules="ServerSideIncludeModule" resourceType="File" />
<add name="SSINC-shtm" path="*.shtm" verb="GET,HEAD,POST" modules="ServerSideIncludeModule" resourceType="File" />
<add name="SSINC-shtml" path="*.shtml" verb="GET,HEAD,POST" modules="ServerSideIncludeModule" resourceType="File" />
<add name="TRACEVerbHandler" path="*" verb="TRACE" modules="ProtocolSupportModule" requireAccess="None" />
<add name="OPTIONSVerbHandler" path="*" verb="OPTIONS" modules="ProtocolSupportModule" requireAccess="None" />
<add name="ExtensionlessUrlHandler-ISAPI-4.0_32bit" path="*." verb="GET,HEAD,POST,DEBUG" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework\v4.0.30319\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv4.0,bitness32" responseBufferLimit="0" />
<add name="ExtensionlessUrlHandler-ISAPI-4.0_64bit" path="*." verb="GET,HEAD,POST,DEBUG" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework64\v4.0.30319\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv4.0,bitness64" responseBufferLimit="0" />
<add name="ExtensionlessUrlHandler-Integrated-4.0" path="*." verb="GET,HEAD,POST,DEBUG" type="System.Web.Handlers.TransferRequestHandler" preCondition="integratedMode,runtimeVersionv4.0" responseBufferLimit="0" />
<add name="StaticFile" path="*" verb="*" modules="StaticFileModule,DefaultDocumentModule,DirectoryListingModule" resourceType="Either" requireAccess="Read" />
</handlers>
</system.webServer>
</location>
<location path="WatchLog" inheritInChildApplications="false">
<system.webServer>
<security>
<authentication>
<anonymousAuthentication enabled="true" />
<windowsAuthentication enabled="false" />
</authentication>
</security>
<handlers>
<add name="aspNetCore" path="*" verb="*" resourceType="Unspecified" modules="AspNetCoreModuleV2" />
</handlers>
<modules>
<remove name="WebMatrixSupportModule" />
</modules>
<aspNetCore stdoutLogEnabled="false" startupTimeLimit="3600" requestTimeout="23:00:00" processPath="%ANCM_LAUNCHER_PATH%" hostingModel="InProcess" arguments="%ANCM_LAUNCHER_ARGS%" />
<httpCompression>
<dynamicTypes>
<add mimeType="text/event-stream" enabled="false" />
</dynamicTypes>
</httpCompression>
</system.webServer>
</location>
</configuration>
@@ -0,0 +1,113 @@
using System.Security.Claims;
using System.Text.Json;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Primitives;
using WatchLog.Components.Account.Pages;
using WatchLog.Components.Account.Pages.Manage;
using WatchLog.Data;
namespace Microsoft.AspNetCore.Routing
{
internal static class IdentityComponentsEndpointRouteBuilderExtensions
{
// These endpoints are required by the Identity Razor components defined in the /Components/Account/Pages directory of this project.
public static IEndpointConventionBuilder MapAdditionalIdentityEndpoints(this IEndpointRouteBuilder endpoints)
{
ArgumentNullException.ThrowIfNull(endpoints);
var accountGroup = endpoints.MapGroup("/Account");
accountGroup.MapPost("/PerformExternalLogin", (
HttpContext context,
[FromServices] SignInManager<ApplicationUser> signInManager,
[FromForm] string provider,
[FromForm] string returnUrl) =>
{
IEnumerable<KeyValuePair<string, StringValues>> query = [
new("ReturnUrl", returnUrl),
new("Action", ExternalLogin.LoginCallbackAction)];
var redirectUrl = UriHelper.BuildRelative(
context.Request.PathBase,
"/Account/ExternalLogin",
QueryString.Create(query));
var properties = signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl);
return TypedResults.Challenge(properties, [provider]);
});
accountGroup.MapPost("/Logout", async (
ClaimsPrincipal user,
[FromServices] SignInManager<ApplicationUser> signInManager,
[FromForm] string returnUrl) =>
{
await signInManager.SignOutAsync();
return TypedResults.LocalRedirect($"~/{returnUrl}");
});
var manageGroup = accountGroup.MapGroup("/Manage").RequireAuthorization();
manageGroup.MapPost("/LinkExternalLogin", async (
HttpContext context,
[FromServices] SignInManager<ApplicationUser> signInManager,
[FromForm] string provider) =>
{
// Clear the existing external cookie to ensure a clean login process
await context.SignOutAsync(IdentityConstants.ExternalScheme);
var redirectUrl = UriHelper.BuildRelative(
context.Request.PathBase,
"/Account/Manage/ExternalLogins",
QueryString.Create("Action", ExternalLogins.LinkLoginCallbackAction));
var properties = signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl, signInManager.UserManager.GetUserId(context.User));
return TypedResults.Challenge(properties, [provider]);
});
var loggerFactory = endpoints.ServiceProvider.GetRequiredService<ILoggerFactory>();
var downloadLogger = loggerFactory.CreateLogger("DownloadPersonalData");
manageGroup.MapPost("/DownloadPersonalData", async (
HttpContext context,
[FromServices] UserManager<ApplicationUser> userManager,
[FromServices] AuthenticationStateProvider authenticationStateProvider) =>
{
var user = await userManager.GetUserAsync(context.User);
if (user is null)
{
return Results.NotFound($"Unable to load user with ID '{userManager.GetUserId(context.User)}'.");
}
var userId = await userManager.GetUserIdAsync(user);
downloadLogger.LogInformation("User with ID '{UserId}' asked for their personal data.", userId);
// Only include personal data for download
var personalData = new Dictionary<string, string>();
var personalDataProps = typeof(ApplicationUser).GetProperties().Where(
prop => Attribute.IsDefined(prop, typeof(PersonalDataAttribute)));
foreach (var p in personalDataProps)
{
personalData.Add(p.Name, p.GetValue(user)?.ToString() ?? "null");
}
var logins = await userManager.GetLoginsAsync(user);
foreach (var l in logins)
{
personalData.Add($"{l.LoginProvider} external login provider key", l.ProviderKey);
}
personalData.Add("Authenticator Key", (await userManager.GetAuthenticatorKeyAsync(user))!);
var fileBytes = JsonSerializer.SerializeToUtf8Bytes(personalData);
context.Response.Headers.TryAdd("Content-Disposition", "attachment; filename=PersonalData.json");
return TypedResults.File(fileBytes, contentType: "application/json", fileDownloadName: "PersonalData.json");
});
return accountGroup;
}
}
}
@@ -0,0 +1,21 @@
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.UI.Services;
using WatchLog.Data;
namespace WatchLog.Components.Account
{
// Remove the "else if (EmailSender is IdentityNoOpEmailSender)" block from RegisterConfirmation.razor after updating with a real implementation.
internal sealed class IdentityNoOpEmailSender : IEmailSender<ApplicationUser>
{
private readonly IEmailSender emailSender = new NoOpEmailSender();
public Task SendConfirmationLinkAsync(ApplicationUser user, string email, string confirmationLink) =>
emailSender.SendEmailAsync(email, "Confirm your email", $"Please confirm your account by <a href='{confirmationLink}'>clicking here</a>.");
public Task SendPasswordResetLinkAsync(ApplicationUser user, string email, string resetLink) =>
emailSender.SendEmailAsync(email, "Reset your password", $"Please reset your password by <a href='{resetLink}'>clicking here</a>.");
public Task SendPasswordResetCodeAsync(ApplicationUser user, string email, string resetCode) =>
emailSender.SendEmailAsync(email, "Reset your password", $"Please reset your password using the following code: {resetCode}");
}
}
@@ -0,0 +1,59 @@
using System.Diagnostics.CodeAnalysis;
using Microsoft.AspNetCore.Components;
namespace WatchLog.Components.Account
{
internal sealed class IdentityRedirectManager(NavigationManager navigationManager)
{
public const string StatusCookieName = "Identity.StatusMessage";
private static readonly CookieBuilder StatusCookieBuilder = new()
{
SameSite = SameSiteMode.Strict,
HttpOnly = true,
IsEssential = true,
MaxAge = TimeSpan.FromSeconds(5),
};
[DoesNotReturn]
public void RedirectTo(string? uri)
{
uri ??= "";
// Prevent open redirects.
if (!Uri.IsWellFormedUriString(uri, UriKind.Relative))
{
uri = navigationManager.ToBaseRelativePath(uri);
}
// During static rendering, NavigateTo throws a NavigationException which is handled by the framework as a redirect.
// So as long as this is called from a statically rendered Identity component, the InvalidOperationException is never thrown.
navigationManager.NavigateTo(uri);
throw new InvalidOperationException($"{nameof(IdentityRedirectManager)} can only be used during static rendering.");
}
[DoesNotReturn]
public void RedirectTo(string uri, Dictionary<string, object?> queryParameters)
{
var uriWithoutQuery = navigationManager.ToAbsoluteUri(uri).GetLeftPart(UriPartial.Path);
var newUri = navigationManager.GetUriWithQueryParameters(uriWithoutQuery, queryParameters);
RedirectTo(newUri);
}
[DoesNotReturn]
public void RedirectToWithStatus(string uri, string message, HttpContext context)
{
context.Response.Cookies.Append(StatusCookieName, message, StatusCookieBuilder.Build(context));
RedirectTo(uri);
}
private string CurrentPath => navigationManager.ToAbsoluteUri(navigationManager.Uri).GetLeftPart(UriPartial.Path);
[DoesNotReturn]
public void RedirectToCurrentPage() => RedirectTo(CurrentPath);
[DoesNotReturn]
public void RedirectToCurrentPageWithStatus(string message, HttpContext context)
=> RedirectToWithStatus(CurrentPath, message, context);
}
}
@@ -0,0 +1,48 @@
using System.Security.Claims;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Components.Server;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options;
using WatchLog.Data;
namespace WatchLog.Components.Account
{
// This is a server-side AuthenticationStateProvider that revalidates the security stamp for the connected user
// every 30 minutes an interactive circuit is connected.
internal sealed class IdentityRevalidatingAuthenticationStateProvider(
ILoggerFactory loggerFactory,
IServiceScopeFactory scopeFactory,
IOptions<IdentityOptions> options)
: RevalidatingServerAuthenticationStateProvider(loggerFactory)
{
protected override TimeSpan RevalidationInterval => TimeSpan.FromMinutes(30);
protected override async Task<bool> ValidateAuthenticationStateAsync(
AuthenticationState authenticationState, CancellationToken cancellationToken)
{
// Get the user manager from a new scope to ensure it fetches fresh data
await using var scope = scopeFactory.CreateAsyncScope();
var userManager = scope.ServiceProvider.GetRequiredService<UserManager<ApplicationUser>>();
return await ValidateSecurityStampAsync(userManager, authenticationState.User);
}
private async Task<bool> ValidateSecurityStampAsync(UserManager<ApplicationUser> userManager, ClaimsPrincipal principal)
{
var user = await userManager.GetUserAsync(principal);
if (user is null)
{
return false;
}
else if (!userManager.SupportsUserSecurityStamp)
{
return true;
}
else
{
var principalStamp = principal.FindFirstValue(options.Value.ClaimsIdentity.SecurityStampClaimType);
var userStamp = await userManager.GetSecurityStampAsync(user);
return principalStamp == userStamp;
}
}
}
}
@@ -0,0 +1,20 @@
using Microsoft.AspNetCore.Identity;
using WatchLog.Data;
namespace WatchLog.Components.Account
{
internal sealed class IdentityUserAccessor(UserManager<ApplicationUser> userManager, IdentityRedirectManager redirectManager)
{
public async Task<ApplicationUser> GetRequiredUserAsync(HttpContext context)
{
var user = await userManager.GetUserAsync(context.User);
if (user is null)
{
redirectManager.RedirectToWithStatus("Account/InvalidUser", $"Error: Unable to load user with ID '{userManager.GetUserId(context.User)}'.", context);
}
return user;
}
}
}
@@ -0,0 +1,8 @@
@page "/Account/AccessDenied"
<PageTitle>Access denied</PageTitle>
<header>
<h1 class="text-danger">Access denied</h1>
<p class="text-danger">You do not have access to this resource.</p>
</header>
@@ -0,0 +1,48 @@
@page "/Account/ConfirmEmail"
@using System.Text
@using Microsoft.AspNetCore.Identity
@using Microsoft.AspNetCore.WebUtilities
@using WatchLog.Data
@inject UserManager<ApplicationUser> UserManager
@inject IdentityRedirectManager RedirectManager
<PageTitle>Confirm email</PageTitle>
<h1>Confirm email</h1>
<StatusMessage Message="@statusMessage" />
@code {
private string? statusMessage;
[CascadingParameter]
private HttpContext HttpContext { get; set; } = default!;
[SupplyParameterFromQuery]
private string? UserId { get; set; }
[SupplyParameterFromQuery]
private string? Code { get; set; }
protected override async Task OnInitializedAsync()
{
if (UserId is null || Code is null)
{
RedirectManager.RedirectTo("");
}
var user = await UserManager.FindByIdAsync(UserId);
if (user is null)
{
HttpContext.Response.StatusCode = StatusCodes.Status404NotFound;
statusMessage = $"Error loading user with ID {UserId}";
}
else
{
var code = Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(Code));
var result = await UserManager.ConfirmEmailAsync(user, code);
statusMessage = result.Succeeded ? "Thank you for confirming your email." : "Error confirming your email.";
}
}
}
@@ -0,0 +1,68 @@
@page "/Account/ConfirmEmailChange"
@using System.Text
@using Microsoft.AspNetCore.Identity
@using Microsoft.AspNetCore.WebUtilities
@using WatchLog.Data
@inject UserManager<ApplicationUser> UserManager
@inject SignInManager<ApplicationUser> SignInManager
@inject IdentityRedirectManager RedirectManager
<PageTitle>Confirm email change</PageTitle>
<h1>Confirm email change</h1>
<StatusMessage Message="@message" />
@code {
private string? message;
[CascadingParameter]
private HttpContext HttpContext { get; set; } = default!;
[SupplyParameterFromQuery]
private string? UserId { get; set; }
[SupplyParameterFromQuery]
private string? Email { get; set; }
[SupplyParameterFromQuery]
private string? Code { get; set; }
protected override async Task OnInitializedAsync()
{
if (UserId is null || Email is null || Code is null)
{
RedirectManager.RedirectToWithStatus(
"Account/Login", "Error: Invalid email change confirmation link.", HttpContext);
}
var user = await UserManager.FindByIdAsync(UserId);
if (user is null)
{
message = "Unable to find user with Id '{userId}'";
return;
}
var code = Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(Code));
var result = await UserManager.ChangeEmailAsync(user, Email, code);
if (!result.Succeeded)
{
message = "Error changing email.";
return;
}
// In our UI email and user name are one and the same, so when we update the email
// we need to update the user name.
var setUserNameResult = await UserManager.SetUserNameAsync(user, Email);
if (!setUserNameResult.Succeeded)
{
message = "Error changing user name.";
return;
}
await SignInManager.RefreshSignInAsync(user);
message = "Thank you for confirming your email change.";
}
}
@@ -0,0 +1,205 @@
@page "/Account/ExternalLogin"
@using System.ComponentModel.DataAnnotations
@using System.Security.Claims
@using System.Text
@using System.Text.Encodings.Web
@using Microsoft.AspNetCore.Identity
@using Microsoft.AspNetCore.WebUtilities
@using WatchLog.Data
@inject SignInManager<ApplicationUser> SignInManager
@inject UserManager<ApplicationUser> UserManager
@inject IUserStore<ApplicationUser> UserStore
@inject IEmailSender<ApplicationUser> EmailSender
@inject NavigationManager NavigationManager
@inject IdentityRedirectManager RedirectManager
@inject ILogger<ExternalLogin> Logger
<PageTitle>Register</PageTitle>
<StatusMessage Message="@message" />
<h1>Register</h1>
<h2>Associate your @ProviderDisplayName account.</h2>
<hr />
<div class="alert alert-info">
You've successfully authenticated with <strong>@ProviderDisplayName</strong>.
Please enter an email address for this site below and click the Register button to finish
logging in.
</div>
<div class="row">
<div class="col-md-4">
<EditForm Model="Input" OnValidSubmit="OnValidSubmitAsync" FormName="confirmation" method="post">
<DataAnnotationsValidator />
<ValidationSummary class="text-danger" role="alert" />
<div class="form-floating mb-3">
<InputText @bind-Value="Input.Email" id="Input.Email" class="form-control" autocomplete="email" placeholder="Please enter your email." />
<label for="Input.Email" class="form-label">Email</label>
<ValidationMessage For="() => Input.Email" />
</div>
<button type="submit" class="w-100 btn btn-lg btn-primary">Register</button>
</EditForm>
</div>
</div>
@code {
public const string LoginCallbackAction = "LoginCallback";
private string? message;
private ExternalLoginInfo? externalLoginInfo;
[CascadingParameter]
private HttpContext HttpContext { get; set; } = default!;
[SupplyParameterFromForm]
private InputModel Input { get; set; } = new();
[SupplyParameterFromQuery]
private string? RemoteError { get; set; }
[SupplyParameterFromQuery]
private string? ReturnUrl { get; set; }
[SupplyParameterFromQuery]
private string? Action { get; set; }
private string? ProviderDisplayName => externalLoginInfo?.ProviderDisplayName;
protected override async Task OnInitializedAsync()
{
if (RemoteError is not null)
{
RedirectManager.RedirectToWithStatus("Account/Login", $"Error from external provider: {RemoteError}", HttpContext);
}
var info = await SignInManager.GetExternalLoginInfoAsync();
if (info is null)
{
RedirectManager.RedirectToWithStatus("Account/Login", "Error loading external login information.", HttpContext);
}
externalLoginInfo = info;
if (HttpMethods.IsGet(HttpContext.Request.Method))
{
if (Action == LoginCallbackAction)
{
await OnLoginCallbackAsync();
return;
}
// We should only reach this page via the login callback, so redirect back to
// the login page if we get here some other way.
RedirectManager.RedirectTo("Account/Login");
}
}
private async Task OnLoginCallbackAsync()
{
if (externalLoginInfo is null)
{
RedirectManager.RedirectToWithStatus("Account/Login", "Error loading external login information.", HttpContext);
}
// Sign in the user with this external login provider if the user already has a login.
var result = await SignInManager.ExternalLoginSignInAsync(
externalLoginInfo.LoginProvider,
externalLoginInfo.ProviderKey,
isPersistent: false,
bypassTwoFactor: true);
if (result.Succeeded)
{
Logger.LogInformation(
"{Name} logged in with {LoginProvider} provider.",
externalLoginInfo.Principal.Identity?.Name,
externalLoginInfo.LoginProvider);
RedirectManager.RedirectTo(ReturnUrl);
}
else if (result.IsLockedOut)
{
RedirectManager.RedirectTo("Account/Lockout");
}
// If the user does not have an account, then ask the user to create an account.
if (externalLoginInfo.Principal.HasClaim(c => c.Type == ClaimTypes.Email))
{
Input.Email = externalLoginInfo.Principal.FindFirstValue(ClaimTypes.Email) ?? "";
}
}
private async Task OnValidSubmitAsync()
{
if (externalLoginInfo is null)
{
RedirectManager.RedirectToWithStatus("Account/Login", "Error loading external login information during confirmation.", HttpContext);
}
var emailStore = GetEmailStore();
var user = CreateUser();
await UserStore.SetUserNameAsync(user, Input.Email, CancellationToken.None);
await emailStore.SetEmailAsync(user, Input.Email, CancellationToken.None);
var result = await UserManager.CreateAsync(user);
if (result.Succeeded)
{
result = await UserManager.AddLoginAsync(user, externalLoginInfo);
if (result.Succeeded)
{
Logger.LogInformation("User created an account using {Name} provider.", externalLoginInfo.LoginProvider);
var userId = await UserManager.GetUserIdAsync(user);
var code = await UserManager.GenerateEmailConfirmationTokenAsync(user);
code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
var callbackUrl = NavigationManager.GetUriWithQueryParameters(
NavigationManager.ToAbsoluteUri("Account/ConfirmEmail").AbsoluteUri,
new Dictionary<string, object?> { ["userId"] = userId, ["code"] = code });
await EmailSender.SendConfirmationLinkAsync(user, Input.Email, HtmlEncoder.Default.Encode(callbackUrl));
// If account confirmation is required, we need to show the link if we don't have a real email sender
if (UserManager.Options.SignIn.RequireConfirmedAccount)
{
RedirectManager.RedirectTo("Account/RegisterConfirmation", new() { ["email"] = Input.Email });
}
await SignInManager.SignInAsync(user, isPersistent: false, externalLoginInfo.LoginProvider);
RedirectManager.RedirectTo(ReturnUrl);
}
}
message = $"Error: {string.Join(",", result.Errors.Select(error => error.Description))}";
}
private ApplicationUser CreateUser()
{
try
{
return Activator.CreateInstance<ApplicationUser>();
}
catch
{
throw new InvalidOperationException($"Can't create an instance of '{nameof(ApplicationUser)}'. " +
$"Ensure that '{nameof(ApplicationUser)}' is not an abstract class and has a parameterless constructor");
}
}
private IUserEmailStore<ApplicationUser> GetEmailStore()
{
if (!UserManager.SupportsUserEmail)
{
throw new NotSupportedException("The default UI requires a user store with email support.");
}
return (IUserEmailStore<ApplicationUser>)UserStore;
}
private sealed class InputModel
{
[Required]
[EmailAddress]
public string Email { get; set; } = "";
}
}
@@ -0,0 +1,68 @@
@page "/Account/ForgotPassword"
@using System.ComponentModel.DataAnnotations
@using System.Text
@using System.Text.Encodings.Web
@using Microsoft.AspNetCore.Identity
@using Microsoft.AspNetCore.WebUtilities
@using WatchLog.Data
@inject UserManager<ApplicationUser> UserManager
@inject IEmailSender<ApplicationUser> EmailSender
@inject NavigationManager NavigationManager
@inject IdentityRedirectManager RedirectManager
<PageTitle>Forgot your password?</PageTitle>
<h1>Forgot your password?</h1>
<h2>Enter your email.</h2>
<hr />
<div class="row">
<div class="col-md-4">
<EditForm Model="Input" FormName="forgot-password" OnValidSubmit="OnValidSubmitAsync" method="post">
<DataAnnotationsValidator />
<ValidationSummary class="text-danger" role="alert" />
<div class="form-floating mb-3">
<InputText @bind-Value="Input.Email" id="Input.Email" class="form-control" autocomplete="username" aria-required="true" placeholder="name@example.com" />
<label for="Input.Email" class="form-label">Email</label>
<ValidationMessage For="() => Input.Email" class="text-danger" />
</div>
<button type="submit" class="w-100 btn btn-lg btn-primary">Reset password</button>
</EditForm>
</div>
</div>
@code {
[SupplyParameterFromForm]
private InputModel Input { get; set; } = new();
private async Task OnValidSubmitAsync()
{
var user = await UserManager.FindByEmailAsync(Input.Email);
if (user is null || !(await UserManager.IsEmailConfirmedAsync(user)))
{
// Don't reveal that the user does not exist or is not confirmed
RedirectManager.RedirectTo("Account/ForgotPasswordConfirmation");
}
// For more information on how to enable account confirmation and password reset please
// visit https://go.microsoft.com/fwlink/?LinkID=532713
var code = await UserManager.GeneratePasswordResetTokenAsync(user);
code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
var callbackUrl = NavigationManager.GetUriWithQueryParameters(
NavigationManager.ToAbsoluteUri("Account/ResetPassword").AbsoluteUri,
new Dictionary<string, object?> { ["code"] = code });
await EmailSender.SendPasswordResetLinkAsync(user, Input.Email, HtmlEncoder.Default.Encode(callbackUrl));
RedirectManager.RedirectTo("Account/ForgotPasswordConfirmation");
}
private sealed class InputModel
{
[Required]
[EmailAddress]
public string Email { get; set; } = "";
}
}
@@ -0,0 +1,8 @@
@page "/Account/ForgotPasswordConfirmation"
<PageTitle>Forgot password confirmation</PageTitle>
<h1>Forgot password confirmation</h1>
<p role="alert">
Please check your email to reset your password.
</p>
@@ -0,0 +1,8 @@
@page "/Account/InvalidPasswordReset"
<PageTitle>Invalid password reset</PageTitle>
<h1>Invalid password reset</h1>
<p role="alert">
The password reset link is invalid.
</p>
@@ -0,0 +1,7 @@
@page "/Account/InvalidUser"
<PageTitle>Invalid user</PageTitle>
<h3>Invalid user</h3>
<StatusMessage />
@@ -0,0 +1,8 @@
@page "/Account/Lockout"
<PageTitle>Locked out</PageTitle>
<header>
<h1 class="text-danger">Locked out</h1>
<p class="text-danger" role="alert">This account has been locked out, please try again later.</p>
</header>
@@ -0,0 +1,128 @@
@page "/Account/Login"
@using System.ComponentModel.DataAnnotations
@using Microsoft.AspNetCore.Authentication
@using Microsoft.AspNetCore.Identity
@using WatchLog.Data
@inject SignInManager<ApplicationUser> SignInManager
@inject ILogger<Login> Logger
@inject NavigationManager NavigationManager
@inject IdentityRedirectManager RedirectManager
<PageTitle>Log in</PageTitle>
<h1>Log in</h1>
<div class="row">
<div class="col-lg-6">
<section>
<StatusMessage Message="@errorMessage" />
<EditForm Model="Input" method="post" OnValidSubmit="LoginUser" FormName="login">
<DataAnnotationsValidator />
<h2>Use a local account to log in.</h2>
<hr />
<ValidationSummary class="text-danger" role="alert" />
<div class="form-floating mb-3">
<InputText @bind-Value="Input.Email" id="Input.Email" class="form-control" autocomplete="username" aria-required="true" placeholder="name@example.com" />
<label for="Input.Email" class="form-label">Email</label>
<ValidationMessage For="() => Input.Email" class="text-danger" />
</div>
<div class="form-floating mb-3">
<InputText type="password" @bind-Value="Input.Password" id="Input.Password" class="form-control" autocomplete="current-password" aria-required="true" placeholder="password" />
<label for="Input.Password" class="form-label">Password</label>
<ValidationMessage For="() => Input.Password" class="text-danger" />
</div>
<div class="checkbox mb-3">
<label class="form-label">
<InputCheckbox @bind-Value="Input.RememberMe" class="darker-border-checkbox form-check-input" />
Remember me
</label>
</div>
<div>
<button type="submit" class="w-100 btn btn-lg btn-primary">Log in</button>
</div>
<div>
<p>
<a href="Account/ForgotPassword">Forgot your password?</a>
</p>
<p>
<a href="@(NavigationManager.GetUriWithQueryParameters("Account/Register", new Dictionary<string, object?> { ["ReturnUrl"] = ReturnUrl }))">Register as a new user</a>
</p>
<p>
<a href="Account/ResendEmailConfirmation">Resend email confirmation</a>
</p>
</div>
</EditForm>
</section>
</div>
<div class="col-lg-4 col-lg-offset-2">
<section>
<h3>Use another service to log in.</h3>
<hr />
<ExternalLoginPicker />
</section>
</div>
</div>
@code {
private string? errorMessage;
[CascadingParameter]
private HttpContext HttpContext { get; set; } = default!;
[SupplyParameterFromForm]
private InputModel Input { get; set; } = new();
[SupplyParameterFromQuery]
private string? ReturnUrl { get; set; }
protected override async Task OnInitializedAsync()
{
if (HttpMethods.IsGet(HttpContext.Request.Method))
{
// Clear the existing external cookie to ensure a clean login process
await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme);
}
}
public async Task LoginUser()
{
// This doesn't count login failures towards account lockout
// To enable password failures to trigger account lockout, set lockoutOnFailure: true
var result = await SignInManager.PasswordSignInAsync(Input.Email, Input.Password, Input.RememberMe, lockoutOnFailure: false);
if (result.Succeeded)
{
Logger.LogInformation("User logged in.");
RedirectManager.RedirectTo(ReturnUrl);
}
else if (result.RequiresTwoFactor)
{
RedirectManager.RedirectTo(
"Account/LoginWith2fa",
new() { ["returnUrl"] = ReturnUrl, ["rememberMe"] = Input.RememberMe });
}
else if (result.IsLockedOut)
{
Logger.LogWarning("User account locked out.");
RedirectManager.RedirectTo("Account/Lockout");
}
else
{
errorMessage = "Error: Invalid login attempt.";
}
}
private sealed class InputModel
{
[Required]
[EmailAddress]
public string Email { get; set; } = "";
[Required]
[DataType(DataType.Password)]
public string Password { get; set; } = "";
[Display(Name = "Remember me?")]
public bool RememberMe { get; set; }
}
}
@@ -0,0 +1,101 @@
@page "/Account/LoginWith2fa"
@using System.ComponentModel.DataAnnotations
@using Microsoft.AspNetCore.Identity
@using WatchLog.Data
@inject SignInManager<ApplicationUser> SignInManager
@inject UserManager<ApplicationUser> UserManager
@inject IdentityRedirectManager RedirectManager
@inject ILogger<LoginWith2fa> Logger
<PageTitle>Two-factor authentication</PageTitle>
<h1>Two-factor authentication</h1>
<hr />
<StatusMessage Message="@message" />
<p>Your login is protected with an authenticator app. Enter your authenticator code below.</p>
<div class="row">
<div class="col-md-4">
<EditForm Model="Input" FormName="login-with-2fa" OnValidSubmit="OnValidSubmitAsync" method="post">
<input type="hidden" name="ReturnUrl" value="@ReturnUrl" />
<input type="hidden" name="RememberMe" value="@RememberMe" />
<DataAnnotationsValidator />
<ValidationSummary class="text-danger" role="alert" />
<div class="form-floating mb-3">
<InputText @bind-Value="Input.TwoFactorCode" id="Input.TwoFactorCode" class="form-control" autocomplete="off" />
<label for="Input.TwoFactorCode" class="form-label">Authenticator code</label>
<ValidationMessage For="() => Input.TwoFactorCode" class="text-danger" />
</div>
<div class="checkbox mb-3">
<label for="remember-machine" class="form-label">
<InputCheckbox @bind-Value="Input.RememberMachine" />
Remember this machine
</label>
</div>
<div>
<button type="submit" class="w-100 btn btn-lg btn-primary">Log in</button>
</div>
</EditForm>
</div>
</div>
<p>
Don't have access to your authenticator device? You can
<a href="Account/LoginWithRecoveryCode?ReturnUrl=@ReturnUrl">log in with a recovery code</a>.
</p>
@code {
private string? message;
private ApplicationUser user = default!;
[SupplyParameterFromForm]
private InputModel Input { get; set; } = new();
[SupplyParameterFromQuery]
private string? ReturnUrl { get; set; }
[SupplyParameterFromQuery]
private bool RememberMe { get; set; }
protected override async Task OnInitializedAsync()
{
// Ensure the user has gone through the username & password screen first
user = await SignInManager.GetTwoFactorAuthenticationUserAsync() ??
throw new InvalidOperationException("Unable to load two-factor authentication user.");
}
private async Task OnValidSubmitAsync()
{
var authenticatorCode = Input.TwoFactorCode!.Replace(" ", string.Empty).Replace("-", string.Empty);
var result = await SignInManager.TwoFactorAuthenticatorSignInAsync(authenticatorCode, RememberMe, Input.RememberMachine);
var userId = await UserManager.GetUserIdAsync(user);
if (result.Succeeded)
{
Logger.LogInformation("User with ID '{UserId}' logged in with 2fa.", userId);
RedirectManager.RedirectTo(ReturnUrl);
}
else if (result.IsLockedOut)
{
Logger.LogWarning("User with ID '{UserId}' account locked out.", userId);
RedirectManager.RedirectTo("Account/Lockout");
}
else
{
Logger.LogWarning("Invalid authenticator code entered for user with ID '{UserId}'.", userId);
message = "Error: Invalid authenticator code.";
}
}
private sealed class InputModel
{
[Required]
[StringLength(7, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
[DataType(DataType.Text)]
[Display(Name = "Authenticator code")]
public string? TwoFactorCode { get; set; }
[Display(Name = "Remember this machine")]
public bool RememberMachine { get; set; }
}
}
@@ -0,0 +1,85 @@
@page "/Account/LoginWithRecoveryCode"
@using System.ComponentModel.DataAnnotations
@using Microsoft.AspNetCore.Identity
@using WatchLog.Data
@inject SignInManager<ApplicationUser> SignInManager
@inject UserManager<ApplicationUser> UserManager
@inject IdentityRedirectManager RedirectManager
@inject ILogger<LoginWithRecoveryCode> Logger
<PageTitle>Recovery code verification</PageTitle>
<h1>Recovery code verification</h1>
<hr />
<StatusMessage Message="@message" />
<p>
You have requested to log in with a recovery code. This login will not be remembered until you provide
an authenticator app code at log in or disable 2FA and log in again.
</p>
<div class="row">
<div class="col-md-4">
<EditForm Model="Input" FormName="login-with-recovery-code" OnValidSubmit="OnValidSubmitAsync" method="post">
<DataAnnotationsValidator />
<ValidationSummary class="text-danger" role="alert" />
<div class="form-floating mb-3">
<InputText @bind-Value="Input.RecoveryCode" id="Input.RecoveryCode" class="form-control" autocomplete="off" placeholder="RecoveryCode" />
<label for="Input.RecoveryCode" class="form-label">Recovery Code</label>
<ValidationMessage For="() => Input.RecoveryCode" class="text-danger" />
</div>
<button type="submit" class="w-100 btn btn-lg btn-primary">Log in</button>
</EditForm>
</div>
</div>
@code {
private string? message;
private ApplicationUser user = default!;
[SupplyParameterFromForm]
private InputModel Input { get; set; } = new();
[SupplyParameterFromQuery]
private string? ReturnUrl { get; set; }
protected override async Task OnInitializedAsync()
{
// Ensure the user has gone through the username & password screen first
user = await SignInManager.GetTwoFactorAuthenticationUserAsync() ??
throw new InvalidOperationException("Unable to load two-factor authentication user.");
}
private async Task OnValidSubmitAsync()
{
var recoveryCode = Input.RecoveryCode.Replace(" ", string.Empty);
var result = await SignInManager.TwoFactorRecoveryCodeSignInAsync(recoveryCode);
var userId = await UserManager.GetUserIdAsync(user);
if (result.Succeeded)
{
Logger.LogInformation("User with ID '{UserId}' logged in with a recovery code.", userId);
RedirectManager.RedirectTo(ReturnUrl);
}
else if (result.IsLockedOut)
{
Logger.LogWarning("User account locked out.");
RedirectManager.RedirectTo("Account/Lockout");
}
else
{
Logger.LogWarning("Invalid recovery code entered for user with ID '{UserId}' ", userId);
message = "Error: Invalid recovery code entered.";
}
}
private sealed class InputModel
{
[Required]
[DataType(DataType.Text)]
[Display(Name = "Recovery Code")]
public string RecoveryCode { get; set; } = "";
}
}
@@ -0,0 +1,96 @@
@page "/Account/Manage/ChangePassword"
@using System.ComponentModel.DataAnnotations
@using Microsoft.AspNetCore.Identity
@using WatchLog.Data
@inject UserManager<ApplicationUser> UserManager
@inject SignInManager<ApplicationUser> SignInManager
@inject IdentityUserAccessor UserAccessor
@inject IdentityRedirectManager RedirectManager
@inject ILogger<ChangePassword> Logger
<PageTitle>Change password</PageTitle>
<h3>Change password</h3>
<StatusMessage Message="@message" />
<div class="row">
<div class="col-xl-6">
<EditForm Model="Input" FormName="change-password" OnValidSubmit="OnValidSubmitAsync" method="post">
<DataAnnotationsValidator />
<ValidationSummary class="text-danger" role="alert" />
<div class="form-floating mb-3">
<InputText type="password" @bind-Value="Input.OldPassword" id="Input.OldPassword" class="form-control" autocomplete="current-password" aria-required="true" placeholder="Enter the old password" />
<label for="Input.OldPassword" class="form-label">Old password</label>
<ValidationMessage For="() => Input.OldPassword" class="text-danger" />
</div>
<div class="form-floating mb-3">
<InputText type="password" @bind-Value="Input.NewPassword" id="Input.NewPassword" class="form-control" autocomplete="new-password" aria-required="true" placeholder="Enter the new password" />
<label for="Input.NewPassword" class="form-label">New password</label>
<ValidationMessage For="() => Input.NewPassword" class="text-danger" />
</div>
<div class="form-floating mb-3">
<InputText type="password" @bind-Value="Input.ConfirmPassword" id="Input.ConfirmPassword" class="form-control" autocomplete="new-password" aria-required="true" placeholder="Enter the new password" />
<label for="Input.ConfirmPassword" class="form-label">Confirm password</label>
<ValidationMessage For="() => Input.ConfirmPassword" class="text-danger" />
</div>
<button type="submit" class="w-100 btn btn-lg btn-primary">Update password</button>
</EditForm>
</div>
</div>
@code {
private string? message;
private ApplicationUser user = default!;
private bool hasPassword;
[CascadingParameter]
private HttpContext HttpContext { get; set; } = default!;
[SupplyParameterFromForm]
private InputModel Input { get; set; } = new();
protected override async Task OnInitializedAsync()
{
user = await UserAccessor.GetRequiredUserAsync(HttpContext);
hasPassword = await UserManager.HasPasswordAsync(user);
if (!hasPassword)
{
RedirectManager.RedirectTo("Account/Manage/SetPassword");
}
}
private async Task OnValidSubmitAsync()
{
var changePasswordResult = await UserManager.ChangePasswordAsync(user, Input.OldPassword, Input.NewPassword);
if (!changePasswordResult.Succeeded)
{
message = $"Error: {string.Join(",", changePasswordResult.Errors.Select(error => error.Description))}";
return;
}
await SignInManager.RefreshSignInAsync(user);
Logger.LogInformation("User changed their password successfully.");
RedirectManager.RedirectToCurrentPageWithStatus("Your password has been changed", HttpContext);
}
private sealed class InputModel
{
[Required]
[DataType(DataType.Password)]
[Display(Name = "Current password")]
public string OldPassword { get; set; } = "";
[Required]
[StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
[DataType(DataType.Password)]
[Display(Name = "New password")]
public string NewPassword { get; set; } = "";
[DataType(DataType.Password)]
[Display(Name = "Confirm new password")]
[Compare("NewPassword", ErrorMessage = "The new password and confirmation password do not match.")]
public string ConfirmPassword { get; set; } = "";
}
}
@@ -0,0 +1,86 @@
@page "/Account/Manage/DeletePersonalData"
@using System.ComponentModel.DataAnnotations
@using Microsoft.AspNetCore.Identity
@using WatchLog.Data
@inject UserManager<ApplicationUser> UserManager
@inject SignInManager<ApplicationUser> SignInManager
@inject IdentityUserAccessor UserAccessor
@inject IdentityRedirectManager RedirectManager
@inject ILogger<DeletePersonalData> Logger
<PageTitle>Delete Personal Data</PageTitle>
<StatusMessage Message="@message" />
<h3>Delete Personal Data</h3>
<div class="alert alert-warning" role="alert">
<p>
<strong>Deleting this data will permanently remove your account, and this cannot be recovered.</strong>
</p>
</div>
<div>
<EditForm Model="Input" FormName="delete-user" OnValidSubmit="OnValidSubmitAsync" method="post">
<DataAnnotationsValidator />
<ValidationSummary class="text-danger" role="alert" />
@if (requirePassword)
{
<div class="form-floating mb-3">
<InputText type="password" @bind-Value="Input.Password" id="Input.Password" class="form-control" autocomplete="current-password" aria-required="true" placeholder="Please enter your password." />
<label for="Input.Password" class="form-label">Password</label>
<ValidationMessage For="() => Input.Password" class="text-danger" />
</div>
}
<button class="w-100 btn btn-lg btn-danger" type="submit">Delete data and close my account</button>
</EditForm>
</div>
@code {
private string? message;
private ApplicationUser user = default!;
private bool requirePassword;
[CascadingParameter]
private HttpContext HttpContext { get; set; } = default!;
[SupplyParameterFromForm]
private InputModel Input { get; set; } = new();
protected override async Task OnInitializedAsync()
{
Input ??= new();
user = await UserAccessor.GetRequiredUserAsync(HttpContext);
requirePassword = await UserManager.HasPasswordAsync(user);
}
private async Task OnValidSubmitAsync()
{
if (requirePassword && !await UserManager.CheckPasswordAsync(user, Input.Password))
{
message = "Error: Incorrect password.";
return;
}
var result = await UserManager.DeleteAsync(user);
if (!result.Succeeded)
{
throw new InvalidOperationException("Unexpected error occurred deleting user.");
}
await SignInManager.SignOutAsync();
var userId = await UserManager.GetUserIdAsync(user);
Logger.LogInformation("User with ID '{UserId}' deleted themselves.", userId);
RedirectManager.RedirectToCurrentPage();
}
private sealed class InputModel
{
[DataType(DataType.Password)]
public string Password { get; set; } = "";
}
}
@@ -0,0 +1,64 @@
@page "/Account/Manage/Disable2fa"
@using Microsoft.AspNetCore.Identity
@using WatchLog.Data
@inject UserManager<ApplicationUser> UserManager
@inject IdentityUserAccessor UserAccessor
@inject IdentityRedirectManager RedirectManager
@inject ILogger<Disable2fa> Logger
<PageTitle>Disable two-factor authentication (2FA)</PageTitle>
<StatusMessage />
<h3>Disable two-factor authentication (2FA)</h3>
<div class="alert alert-warning" role="alert">
<p>
<strong>This action only disables 2FA.</strong>
</p>
<p>
Disabling 2FA does not change the keys used in authenticator apps. If you wish to change the key
used in an authenticator app you should <a href="Account/Manage/ResetAuthenticator">reset your authenticator keys.</a>
</p>
</div>
<div>
<form @formname="disable-2fa" @onsubmit="OnSubmitAsync" method="post">
<AntiforgeryToken />
<button class="btn btn-danger" type="submit">Disable 2FA</button>
</form>
</div>
@code {
private ApplicationUser user = default!;
[CascadingParameter]
private HttpContext HttpContext { get; set; } = default!;
protected override async Task OnInitializedAsync()
{
user = await UserAccessor.GetRequiredUserAsync(HttpContext);
if (HttpMethods.IsGet(HttpContext.Request.Method) && !await UserManager.GetTwoFactorEnabledAsync(user))
{
throw new InvalidOperationException("Cannot disable 2FA for user as it's not currently enabled.");
}
}
private async Task OnSubmitAsync()
{
var disable2faResult = await UserManager.SetTwoFactorEnabledAsync(user, false);
if (!disable2faResult.Succeeded)
{
throw new InvalidOperationException("Unexpected error occurred disabling 2FA.");
}
var userId = await UserManager.GetUserIdAsync(user);
Logger.LogInformation("User with ID '{UserId}' has disabled 2fa.", userId);
RedirectManager.RedirectToWithStatus(
"Account/Manage/TwoFactorAuthentication",
"2fa has been disabled. You can reenable 2fa when you setup an authenticator app",
HttpContext);
}
}
@@ -0,0 +1,123 @@
@page "/Account/Manage/Email"
@using System.ComponentModel.DataAnnotations
@using System.Text
@using System.Text.Encodings.Web
@using Microsoft.AspNetCore.Identity
@using Microsoft.AspNetCore.WebUtilities
@using WatchLog.Data
@inject UserManager<ApplicationUser> UserManager
@inject IEmailSender<ApplicationUser> EmailSender
@inject IdentityUserAccessor UserAccessor
@inject NavigationManager NavigationManager
<PageTitle>Manage email</PageTitle>
<h3>Manage email</h3>
<StatusMessage Message="@message"/>
<div class="row">
<div class="col-xl-6">
<form @onsubmit="OnSendEmailVerificationAsync" @formname="send-verification" id="send-verification-form" method="post">
<AntiforgeryToken />
</form>
<EditForm Model="Input" FormName="change-email" OnValidSubmit="OnValidSubmitAsync" method="post">
<DataAnnotationsValidator />
<ValidationSummary class="text-danger" role="alert" />
@if (isEmailConfirmed)
{
<div class="form-floating mb-3 input-group">
<input type="text" value="@email" id="email" class="form-control" placeholder="Enter your email" disabled />
<div class="input-group-append">
<span class="h-100 input-group-text text-success font-weight-bold">✓</span>
</div>
<label for="email" class="form-label">Email</label>
</div>
}
else
{
<div class="form-floating mb-3">
<input type="text" value="@email" id="email" class="form-control" placeholder="Enter your email" disabled />
<label for="email" class="form-label">Email</label>
<button type="submit" class="btn btn-link" form="send-verification-form">Send verification email</button>
</div>
}
<div class="form-floating mb-3">
<InputText @bind-Value="Input.NewEmail" id="Input.NewEmail" class="form-control" autocomplete="email" aria-required="true" placeholder="Enter a new email" />
<label for="Input.NewEmail" class="form-label">New email</label>
<ValidationMessage For="() => Input.NewEmail" class="text-danger" />
</div>
<button type="submit" class="w-100 btn btn-lg btn-primary">Change email</button>
</EditForm>
</div>
</div>
@code {
private string? message;
private ApplicationUser user = default!;
private string? email;
private bool isEmailConfirmed;
[CascadingParameter]
private HttpContext HttpContext { get; set; } = default!;
[SupplyParameterFromForm(FormName = "change-email")]
private InputModel Input { get; set; } = new();
protected override async Task OnInitializedAsync()
{
user = await UserAccessor.GetRequiredUserAsync(HttpContext);
email = await UserManager.GetEmailAsync(user);
isEmailConfirmed = await UserManager.IsEmailConfirmedAsync(user);
Input.NewEmail ??= email;
}
private async Task OnValidSubmitAsync()
{
if (Input.NewEmail is null || Input.NewEmail == email)
{
message = "Your email is unchanged.";
return;
}
var userId = await UserManager.GetUserIdAsync(user);
var code = await UserManager.GenerateChangeEmailTokenAsync(user, Input.NewEmail);
code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
var callbackUrl = NavigationManager.GetUriWithQueryParameters(
NavigationManager.ToAbsoluteUri("Account/ConfirmEmailChange").AbsoluteUri,
new Dictionary<string, object?> { ["userId"] = userId, ["email"] = Input.NewEmail, ["code"] = code });
await EmailSender.SendConfirmationLinkAsync(user, Input.NewEmail, HtmlEncoder.Default.Encode(callbackUrl));
message = "Confirmation link to change email sent. Please check your email.";
}
private async Task OnSendEmailVerificationAsync()
{
if (email is null)
{
return;
}
var userId = await UserManager.GetUserIdAsync(user);
var code = await UserManager.GenerateEmailConfirmationTokenAsync(user);
code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
var callbackUrl = NavigationManager.GetUriWithQueryParameters(
NavigationManager.ToAbsoluteUri("Account/ConfirmEmail").AbsoluteUri,
new Dictionary<string, object?> { ["userId"] = userId, ["code"] = code });
await EmailSender.SendConfirmationLinkAsync(user, email, HtmlEncoder.Default.Encode(callbackUrl));
message = "Verification email sent. Please check your email.";
}
private sealed class InputModel
{
[Required]
[EmailAddress]
[Display(Name = "New email")]
public string? NewEmail { get; set; }
}
}
@@ -0,0 +1,172 @@
@page "/Account/Manage/EnableAuthenticator"
@using System.ComponentModel.DataAnnotations
@using System.Globalization
@using System.Text
@using System.Text.Encodings.Web
@using Microsoft.AspNetCore.Identity
@using WatchLog.Data
@inject UserManager<ApplicationUser> UserManager
@inject IdentityUserAccessor UserAccessor
@inject UrlEncoder UrlEncoder
@inject IdentityRedirectManager RedirectManager
@inject ILogger<EnableAuthenticator> Logger
<PageTitle>Configure authenticator app</PageTitle>
@if (recoveryCodes is not null)
{
<ShowRecoveryCodes RecoveryCodes="recoveryCodes.ToArray()" StatusMessage="@message" />
}
else
{
<StatusMessage Message="@message" />
<h3>Configure authenticator app</h3>
<div>
<p>To use an authenticator app go through the following steps:</p>
<ol class="list">
<li>
<p>
Download a two-factor authenticator app like Microsoft Authenticator for
<a href="https://go.microsoft.com/fwlink/?Linkid=825072">Android</a> and
<a href="https://go.microsoft.com/fwlink/?Linkid=825073">iOS</a> or
Google Authenticator for
<a href="https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2&amp;hl=en">Android</a> and
<a href="https://itunes.apple.com/us/app/google-authenticator/id388497605?mt=8">iOS</a>.
</p>
</li>
<li>
<p>Scan the QR Code or enter this key <kbd>@sharedKey</kbd> into your two factor authenticator app. Spaces and casing do not matter.</p>
<div class="alert alert-info">Learn how to <a href="https://go.microsoft.com/fwlink/?Linkid=852423">enable QR code generation</a>.</div>
<div></div>
<div data-url="@authenticatorUri"></div>
</li>
<li>
<p>
Once you have scanned the QR code or input the key above, your two factor authentication app will provide you
with a unique code. Enter the code in the confirmation box below.
</p>
<div class="row">
<div class="col-xl-6">
<EditForm Model="Input" FormName="send-code" OnValidSubmit="OnValidSubmitAsync" method="post">
<DataAnnotationsValidator />
<div class="form-floating mb-3">
<InputText @bind-Value="Input.Code" id="Input.Code" class="form-control" autocomplete="off" placeholder="Enter the code" />
<label for="Input.Code" class="control-label form-label">Verification Code</label>
<ValidationMessage For="() => Input.Code" class="text-danger" />
</div>
<button type="submit" class="w-100 btn btn-lg btn-primary">Verify</button>
<ValidationSummary class="text-danger" role="alert" />
</EditForm>
</div>
</div>
</li>
</ol>
</div>
}
@code {
private const string AuthenticatorUriFormat = "otpauth://totp/{0}:{1}?secret={2}&issuer={0}&digits=6";
private string? message;
private ApplicationUser user = default!;
private string? sharedKey;
private string? authenticatorUri;
private IEnumerable<string>? recoveryCodes;
[CascadingParameter]
private HttpContext HttpContext { get; set; } = default!;
[SupplyParameterFromForm]
private InputModel Input { get; set; } = new();
protected override async Task OnInitializedAsync()
{
user = await UserAccessor.GetRequiredUserAsync(HttpContext);
await LoadSharedKeyAndQrCodeUriAsync(user);
}
private async Task OnValidSubmitAsync()
{
// Strip spaces and hyphens
var verificationCode = Input.Code.Replace(" ", string.Empty).Replace("-", string.Empty);
var is2faTokenValid = await UserManager.VerifyTwoFactorTokenAsync(
user, UserManager.Options.Tokens.AuthenticatorTokenProvider, verificationCode);
if (!is2faTokenValid)
{
message = "Error: Verification code is invalid.";
return;
}
await UserManager.SetTwoFactorEnabledAsync(user, true);
var userId = await UserManager.GetUserIdAsync(user);
Logger.LogInformation("User with ID '{UserId}' has enabled 2FA with an authenticator app.", userId);
message = "Your authenticator app has been verified.";
if (await UserManager.CountRecoveryCodesAsync(user) == 0)
{
recoveryCodes = await UserManager.GenerateNewTwoFactorRecoveryCodesAsync(user, 10);
}
else
{
RedirectManager.RedirectToWithStatus("Account/Manage/TwoFactorAuthentication", message, HttpContext);
}
}
private async ValueTask LoadSharedKeyAndQrCodeUriAsync(ApplicationUser user)
{
// Load the authenticator key & QR code URI to display on the form
var unformattedKey = await UserManager.GetAuthenticatorKeyAsync(user);
if (string.IsNullOrEmpty(unformattedKey))
{
await UserManager.ResetAuthenticatorKeyAsync(user);
unformattedKey = await UserManager.GetAuthenticatorKeyAsync(user);
}
sharedKey = FormatKey(unformattedKey!);
var email = await UserManager.GetEmailAsync(user);
authenticatorUri = GenerateQrCodeUri(email!, unformattedKey!);
}
private string FormatKey(string unformattedKey)
{
var result = new StringBuilder();
int currentPosition = 0;
while (currentPosition + 4 < unformattedKey.Length)
{
result.Append(unformattedKey.AsSpan(currentPosition, 4)).Append(' ');
currentPosition += 4;
}
if (currentPosition < unformattedKey.Length)
{
result.Append(unformattedKey.AsSpan(currentPosition));
}
return result.ToString().ToLowerInvariant();
}
private string GenerateQrCodeUri(string email, string unformattedKey)
{
return string.Format(
CultureInfo.InvariantCulture,
AuthenticatorUriFormat,
UrlEncoder.Encode("Microsoft.AspNetCore.Identity.UI"),
UrlEncoder.Encode(email),
unformattedKey);
}
private sealed class InputModel
{
[Required]
[StringLength(7, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
[DataType(DataType.Text)]
[Display(Name = "Verification Code")]
public string Code { get; set; } = "";
}
}
@@ -0,0 +1,140 @@
@page "/Account/Manage/ExternalLogins"
@using Microsoft.AspNetCore.Authentication
@using Microsoft.AspNetCore.Identity
@using WatchLog.Data
@inject UserManager<ApplicationUser> UserManager
@inject SignInManager<ApplicationUser> SignInManager
@inject IdentityUserAccessor UserAccessor
@inject IUserStore<ApplicationUser> UserStore
@inject IdentityRedirectManager RedirectManager
<PageTitle>Manage your external logins</PageTitle>
<StatusMessage />
@if (currentLogins?.Count > 0)
{
<h3>Registered Logins</h3>
<table class="table">
<tbody>
@foreach (var login in currentLogins)
{
<tr>
<td>@login.ProviderDisplayName</td>
<td>
@if (showRemoveButton)
{
<form @formname="@($"remove-login-{login.LoginProvider}")" @onsubmit="OnSubmitAsync" method="post">
<AntiforgeryToken />
<div>
<input type="hidden" name="@nameof(LoginProvider)" value="@login.LoginProvider" />
<input type="hidden" name="@nameof(ProviderKey)" value="@login.ProviderKey" />
<button type="submit" class="btn btn-primary" title="Remove this @login.ProviderDisplayName login from your account">Remove</button>
</div>
</form>
}
else
{
@: &nbsp;
}
</td>
</tr>
}
</tbody>
</table>
}
@if (otherLogins?.Count > 0)
{
<h4>Add another service to log in.</h4>
<hr />
<form class="form-horizontal" action="Account/Manage/LinkExternalLogin" method="post">
<AntiforgeryToken />
<div>
<p>
@foreach (var provider in otherLogins)
{
<button type="submit" class="btn btn-primary" name="Provider" value="@provider.Name" title="Log in using your @provider.DisplayName account">
@provider.DisplayName
</button>
}
</p>
</div>
</form>
}
@code {
public const string LinkLoginCallbackAction = "LinkLoginCallback";
private ApplicationUser user = default!;
private IList<UserLoginInfo>? currentLogins;
private IList<AuthenticationScheme>? otherLogins;
private bool showRemoveButton;
[CascadingParameter]
private HttpContext HttpContext { get; set; } = default!;
[SupplyParameterFromForm]
private string? LoginProvider { get; set; }
[SupplyParameterFromForm]
private string? ProviderKey { get; set; }
[SupplyParameterFromQuery]
private string? Action { get; set; }
protected override async Task OnInitializedAsync()
{
user = await UserAccessor.GetRequiredUserAsync(HttpContext);
currentLogins = await UserManager.GetLoginsAsync(user);
otherLogins = (await SignInManager.GetExternalAuthenticationSchemesAsync())
.Where(auth => currentLogins.All(ul => auth.Name != ul.LoginProvider))
.ToList();
string? passwordHash = null;
if (UserStore is IUserPasswordStore<ApplicationUser> userPasswordStore)
{
passwordHash = await userPasswordStore.GetPasswordHashAsync(user, HttpContext.RequestAborted);
}
showRemoveButton = passwordHash is not null || currentLogins.Count > 1;
if (HttpMethods.IsGet(HttpContext.Request.Method) && Action == LinkLoginCallbackAction)
{
await OnGetLinkLoginCallbackAsync();
}
}
private async Task OnSubmitAsync()
{
var result = await UserManager.RemoveLoginAsync(user, LoginProvider!, ProviderKey!);
if (!result.Succeeded)
{
RedirectManager.RedirectToCurrentPageWithStatus("Error: The external login was not removed.", HttpContext);
}
await SignInManager.RefreshSignInAsync(user);
RedirectManager.RedirectToCurrentPageWithStatus("The external login was removed.", HttpContext);
}
private async Task OnGetLinkLoginCallbackAsync()
{
var userId = await UserManager.GetUserIdAsync(user);
var info = await SignInManager.GetExternalLoginInfoAsync(userId);
if (info is null)
{
RedirectManager.RedirectToCurrentPageWithStatus("Error: Could not load external login info.", HttpContext);
}
var result = await UserManager.AddLoginAsync(user, info);
if (!result.Succeeded)
{
RedirectManager.RedirectToCurrentPageWithStatus("Error: The external login was not added. External logins can only be associated with one account.", HttpContext);
}
// Clear the existing external cookie to ensure a clean login process
await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme);
RedirectManager.RedirectToCurrentPageWithStatus("The external login was added.", HttpContext);
}
}
@@ -0,0 +1,68 @@
@page "/Account/Manage/GenerateRecoveryCodes"
@using Microsoft.AspNetCore.Identity
@using WatchLog.Data
@inject UserManager<ApplicationUser> UserManager
@inject IdentityUserAccessor UserAccessor
@inject IdentityRedirectManager RedirectManager
@inject ILogger<GenerateRecoveryCodes> Logger
<PageTitle>Generate two-factor authentication (2FA) recovery codes</PageTitle>
@if (recoveryCodes is not null)
{
<ShowRecoveryCodes RecoveryCodes="recoveryCodes.ToArray()" StatusMessage="@message" />
}
else
{
<h3>Generate two-factor authentication (2FA) recovery codes</h3>
<div class="alert alert-warning" role="alert">
<p>
<span class="glyphicon glyphicon-warning-sign"></span>
<strong>Put these codes in a safe place.</strong>
</p>
<p>
If you lose your device and don't have the recovery codes you will lose access to your account.
</p>
<p>
Generating new recovery codes does not change the keys used in authenticator apps. If you wish to change the key
used in an authenticator app you should <a href="Account/Manage/ResetAuthenticator">reset your authenticator keys.</a>
</p>
</div>
<div>
<form @formname="generate-recovery-codes" @onsubmit="OnSubmitAsync" method="post">
<AntiforgeryToken />
<button class="btn btn-danger" type="submit">Generate Recovery Codes</button>
</form>
</div>
}
@code {
private string? message;
private ApplicationUser user = default!;
private IEnumerable<string>? recoveryCodes;
[CascadingParameter]
private HttpContext HttpContext { get; set; } = default!;
protected override async Task OnInitializedAsync()
{
user = await UserAccessor.GetRequiredUserAsync(HttpContext);
var isTwoFactorEnabled = await UserManager.GetTwoFactorEnabledAsync(user);
if (!isTwoFactorEnabled)
{
throw new InvalidOperationException("Cannot generate recovery codes for user because they do not have 2FA enabled.");
}
}
private async Task OnSubmitAsync()
{
var userId = await UserManager.GetUserIdAsync(user);
recoveryCodes = await UserManager.GenerateNewTwoFactorRecoveryCodesAsync(user, 10);
message = "You have generated new recovery codes.";
Logger.LogInformation("User with ID '{UserId}' has generated new 2FA recovery codes.", userId);
}
}
@@ -0,0 +1,77 @@
@page "/Account/Manage"
@using System.ComponentModel.DataAnnotations
@using Microsoft.AspNetCore.Identity
@using WatchLog.Data
@inject UserManager<ApplicationUser> UserManager
@inject SignInManager<ApplicationUser> SignInManager
@inject IdentityUserAccessor UserAccessor
@inject IdentityRedirectManager RedirectManager
<PageTitle>Profile</PageTitle>
<h3>Profile</h3>
<StatusMessage />
<div class="row">
<div class="col-xl-6">
<EditForm Model="Input" FormName="profile" OnValidSubmit="OnValidSubmitAsync" method="post">
<DataAnnotationsValidator />
<ValidationSummary class="text-danger" role="alert" />
<div class="form-floating mb-3">
<input type="text" value="@username" id="username" class="form-control" placeholder="Choose your username." disabled />
<label for="username" class="form-label">Username</label>
</div>
<div class="form-floating mb-3">
<InputText @bind-Value="Input.PhoneNumber" id="Input.PhoneNumber" class="form-control" placeholder="Enter your phone number" />
<label for="Input.PhoneNumber" class="form-label">Phone number</label>
<ValidationMessage For="() => Input.PhoneNumber" class="text-danger" />
</div>
<button type="submit" class="w-100 btn btn-lg btn-primary">Save</button>
</EditForm>
</div>
</div>
@code {
private ApplicationUser user = default!;
private string? username;
private string? phoneNumber;
[CascadingParameter]
private HttpContext HttpContext { get; set; } = default!;
[SupplyParameterFromForm]
private InputModel Input { get; set; } = new();
protected override async Task OnInitializedAsync()
{
user = await UserAccessor.GetRequiredUserAsync(HttpContext);
username = await UserManager.GetUserNameAsync(user);
phoneNumber = await UserManager.GetPhoneNumberAsync(user);
Input.PhoneNumber ??= phoneNumber;
}
private async Task OnValidSubmitAsync()
{
if (Input.PhoneNumber != phoneNumber)
{
var setPhoneResult = await UserManager.SetPhoneNumberAsync(user, Input.PhoneNumber);
if (!setPhoneResult.Succeeded)
{
RedirectManager.RedirectToCurrentPageWithStatus("Error: Failed to set phone number.", HttpContext);
}
}
await SignInManager.RefreshSignInAsync(user);
RedirectManager.RedirectToCurrentPageWithStatus("Your profile has been updated", HttpContext);
}
private sealed class InputModel
{
[Phone]
[Display(Name = "Phone number")]
public string? PhoneNumber { get; set; }
}
}
@@ -0,0 +1,34 @@
@page "/Account/Manage/PersonalData"
@inject IdentityUserAccessor UserAccessor
<PageTitle>Personal Data</PageTitle>
<StatusMessage />
<h3>Personal Data</h3>
<div class="row">
<div class="col-md-6">
<p>Your account contains personal data that you have given us. This page allows you to download or delete that data.</p>
<p>
<strong>Deleting this data will permanently remove your account, and this cannot be recovered.</strong>
</p>
<form action="Account/Manage/DownloadPersonalData" method="post">
<AntiforgeryToken />
<button class="btn btn-primary" type="submit">Download</button>
</form>
<p>
<a href="Account/Manage/DeletePersonalData" class="btn btn-danger">Delete</a>
</p>
</div>
</div>
@code {
[CascadingParameter]
private HttpContext HttpContext { get; set; } = default!;
protected override async Task OnInitializedAsync()
{
_ = await UserAccessor.GetRequiredUserAsync(HttpContext);
}
}
@@ -0,0 +1,52 @@
@page "/Account/Manage/ResetAuthenticator"
@using Microsoft.AspNetCore.Identity
@using WatchLog.Data
@inject UserManager<ApplicationUser> UserManager
@inject SignInManager<ApplicationUser> SignInManager
@inject IdentityUserAccessor UserAccessor
@inject IdentityRedirectManager RedirectManager
@inject ILogger<ResetAuthenticator> Logger
<PageTitle>Reset authenticator key</PageTitle>
<StatusMessage />
<h3>Reset authenticator key</h3>
<div class="alert alert-warning" role="alert">
<p>
<span class="glyphicon glyphicon-warning-sign"></span>
<strong>If you reset your authenticator key your authenticator app will not work until you reconfigure it.</strong>
</p>
<p>
This process disables 2FA until you verify your authenticator app.
If you do not complete your authenticator app configuration you may lose access to your account.
</p>
</div>
<div>
<form @formname="reset-authenticator" @onsubmit="OnSubmitAsync" method="post">
<AntiforgeryToken />
<button class="btn btn-danger" type="submit">Reset authenticator key</button>
</form>
</div>
@code {
[CascadingParameter]
private HttpContext HttpContext { get; set; } = default!;
private async Task OnSubmitAsync()
{
var user = await UserAccessor.GetRequiredUserAsync(HttpContext);
await UserManager.SetTwoFactorEnabledAsync(user, false);
await UserManager.ResetAuthenticatorKeyAsync(user);
var userId = await UserManager.GetUserIdAsync(user);
Logger.LogInformation("User with ID '{UserId}' has reset their authentication app key.", userId);
await SignInManager.RefreshSignInAsync(user);
RedirectManager.RedirectToWithStatus(
"Account/Manage/EnableAuthenticator",
"Your authenticator app key has been reset, you will need to configure your authenticator app using the new key.",
HttpContext);
}
}
@@ -0,0 +1,87 @@
@page "/Account/Manage/SetPassword"
@using System.ComponentModel.DataAnnotations
@using Microsoft.AspNetCore.Identity
@using WatchLog.Data
@inject UserManager<ApplicationUser> UserManager
@inject SignInManager<ApplicationUser> SignInManager
@inject IdentityUserAccessor UserAccessor
@inject IdentityRedirectManager RedirectManager
<PageTitle>Set password</PageTitle>
<h3>Set your password</h3>
<StatusMessage Message="@message" />
<p class="text-info">
You do not have a local username/password for this site. Add a local
account so you can log in without an external login.
</p>
<div class="row">
<div class="col-xl-6">
<EditForm Model="Input" FormName="set-password" OnValidSubmit="OnValidSubmitAsync" method="post">
<DataAnnotationsValidator />
<ValidationSummary class="text-danger" role="alert" />
<div class="form-floating mb-3">
<InputText type="password" @bind-Value="Input.NewPassword" id="Input.NewPassword" class="form-control" autocomplete="new-password" placeholder="Enter the new password" />
<label for="Input.NewPassword" class="form-label">New password</label>
<ValidationMessage For="() => Input.NewPassword" class="text-danger" />
</div>
<div class="form-floating mb-3">
<InputText type="password" @bind-Value="Input.ConfirmPassword" id="Input.ConfirmPassword" class="form-control" autocomplete="new-password" placeholder="Enter the new password" />
<label for="Input.ConfirmPassword" class="form-label">Confirm password</label>
<ValidationMessage For="() => Input.ConfirmPassword" class="text-danger" />
</div>
<button type="submit" class="w-100 btn btn-lg btn-primary">Set password</button>
</EditForm>
</div>
</div>
@code {
private string? message;
private ApplicationUser user = default!;
[CascadingParameter]
private HttpContext HttpContext { get; set; } = default!;
[SupplyParameterFromForm]
private InputModel Input { get; set; } = new();
protected override async Task OnInitializedAsync()
{
user = await UserAccessor.GetRequiredUserAsync(HttpContext);
var hasPassword = await UserManager.HasPasswordAsync(user);
if (hasPassword)
{
RedirectManager.RedirectTo("Account/Manage/ChangePassword");
}
}
private async Task OnValidSubmitAsync()
{
var addPasswordResult = await UserManager.AddPasswordAsync(user, Input.NewPassword!);
if (!addPasswordResult.Succeeded)
{
message = $"Error: {string.Join(",", addPasswordResult.Errors.Select(error => error.Description))}";
return;
}
await SignInManager.RefreshSignInAsync(user);
RedirectManager.RedirectToCurrentPageWithStatus("Your password has been set.", HttpContext);
}
private sealed class InputModel
{
[Required]
[StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
[DataType(DataType.Password)]
[Display(Name = "New password")]
public string? NewPassword { get; set; }
[DataType(DataType.Password)]
[Display(Name = "Confirm new password")]
[Compare("NewPassword", ErrorMessage = "The new password and confirmation password do not match.")]
public string? ConfirmPassword { get; set; }
}
}
@@ -0,0 +1,101 @@
@page "/Account/Manage/TwoFactorAuthentication"
@using Microsoft.AspNetCore.Http.Features
@using Microsoft.AspNetCore.Identity
@using WatchLog.Data
@inject UserManager<ApplicationUser> UserManager
@inject SignInManager<ApplicationUser> SignInManager
@inject IdentityUserAccessor UserAccessor
@inject IdentityRedirectManager RedirectManager
<PageTitle>Two-factor authentication (2FA)</PageTitle>
<StatusMessage />
<h3>Two-factor authentication (2FA)</h3>
@if (canTrack)
{
if (is2faEnabled)
{
if (recoveryCodesLeft == 0)
{
<div class="alert alert-danger">
<strong>You have no recovery codes left.</strong>
<p>You must <a href="Account/Manage/GenerateRecoveryCodes">generate a new set of recovery codes</a> before you can log in with a recovery code.</p>
</div>
}
else if (recoveryCodesLeft == 1)
{
<div class="alert alert-danger">
<strong>You have 1 recovery code left.</strong>
<p>You can <a href="Account/Manage/GenerateRecoveryCodes">generate a new set of recovery codes</a>.</p>
</div>
}
else if (recoveryCodesLeft <= 3)
{
<div class="alert alert-warning">
<strong>You have @recoveryCodesLeft recovery codes left.</strong>
<p>You should <a href="Account/Manage/GenerateRecoveryCodes">generate a new set of recovery codes</a>.</p>
</div>
}
if (isMachineRemembered)
{
<form style="display: inline-block" @formname="forget-browser" @onsubmit="OnSubmitForgetBrowserAsync" method="post">
<AntiforgeryToken />
<button type="submit" class="btn btn-primary">Forget this browser</button>
</form>
}
<a href="Account/Manage/Disable2fa" class="btn btn-primary">Disable 2FA</a>
<a href="Account/Manage/GenerateRecoveryCodes" class="btn btn-primary">Reset recovery codes</a>
}
<h4>Authenticator app</h4>
@if (!hasAuthenticator)
{
<a href="Account/Manage/EnableAuthenticator" class="btn btn-primary">Add authenticator app</a>
}
else
{
<a href="Account/Manage/EnableAuthenticator" class="btn btn-primary">Set up authenticator app</a>
<a href="Account/Manage/ResetAuthenticator" class="btn btn-primary">Reset authenticator app</a>
}
}
else
{
<div class="alert alert-danger">
<strong>Privacy and cookie policy have not been accepted.</strong>
<p>You must accept the policy before you can enable two factor authentication.</p>
</div>
}
@code {
private bool canTrack;
private bool hasAuthenticator;
private int recoveryCodesLeft;
private bool is2faEnabled;
private bool isMachineRemembered;
[CascadingParameter]
private HttpContext HttpContext { get; set; } = default!;
protected override async Task OnInitializedAsync()
{
var user = await UserAccessor.GetRequiredUserAsync(HttpContext);
canTrack = HttpContext.Features.Get<ITrackingConsentFeature>()?.CanTrack ?? true;
hasAuthenticator = await UserManager.GetAuthenticatorKeyAsync(user) is not null;
is2faEnabled = await UserManager.GetTwoFactorEnabledAsync(user);
isMachineRemembered = await SignInManager.IsTwoFactorClientRememberedAsync(user);
recoveryCodesLeft = await UserManager.CountRecoveryCodesAsync(user);
}
private async Task OnSubmitForgetBrowserAsync()
{
await SignInManager.ForgetTwoFactorClientAsync();
RedirectManager.RedirectToCurrentPageWithStatus(
"The current browser has been forgotten. When you login again from this browser you will be prompted for your 2fa code.",
HttpContext);
}
}
@@ -0,0 +1,2 @@
@layout ManageLayout
@attribute [Microsoft.AspNetCore.Authorization.Authorize]
@@ -0,0 +1,145 @@
@page "/Account/Register"
@using System.ComponentModel.DataAnnotations
@using System.Text
@using System.Text.Encodings.Web
@using Microsoft.AspNetCore.Identity
@using Microsoft.AspNetCore.WebUtilities
@using WatchLog.Data
@inject UserManager<ApplicationUser> UserManager
@inject IUserStore<ApplicationUser> UserStore
@inject SignInManager<ApplicationUser> SignInManager
@inject IEmailSender<ApplicationUser> EmailSender
@inject ILogger<Register> Logger
@inject NavigationManager NavigationManager
@inject IdentityRedirectManager RedirectManager
<PageTitle>Register</PageTitle>
<h1>Register</h1>
<div class="row">
<div class="col-lg-6">
<StatusMessage Message="@Message" />
<EditForm Model="Input" asp-route-returnUrl="@ReturnUrl" method="post" OnValidSubmit="RegisterUser" FormName="register">
<DataAnnotationsValidator />
<h2>Create a new account.</h2>
<hr />
<ValidationSummary class="text-danger" role="alert" />
<div class="form-floating mb-3">
<InputText @bind-Value="Input.Email" id="Input.Email" class="form-control" autocomplete="username" aria-required="true" placeholder="name@example.com" />
<label for="Input.Email">Email</label>
<ValidationMessage For="() => Input.Email" class="text-danger" />
</div>
<div class="form-floating mb-3">
<InputText type="password" @bind-Value="Input.Password" id="Input.Password" class="form-control" autocomplete="new-password" aria-required="true" placeholder="password" />
<label for="Input.Password">Password</label>
<ValidationMessage For="() => Input.Password" class="text-danger" />
</div>
<div class="form-floating mb-3">
<InputText type="password" @bind-Value="Input.ConfirmPassword" id="Input.ConfirmPassword" class="form-control" autocomplete="new-password" aria-required="true" placeholder="password" />
<label for="Input.ConfirmPassword">Confirm Password</label>
<ValidationMessage For="() => Input.ConfirmPassword" class="text-danger" />
</div>
<button type="submit" class="w-100 btn btn-lg btn-primary">Register</button>
</EditForm>
</div>
<div class="col-lg-4 col-lg-offset-2">
<section>
<h3>Use another service to register.</h3>
<hr />
<ExternalLoginPicker />
</section>
</div>
</div>
@code {
private IEnumerable<IdentityError>? identityErrors;
[SupplyParameterFromForm]
private InputModel Input { get; set; } = new();
[SupplyParameterFromQuery]
private string? ReturnUrl { get; set; }
private string? Message => identityErrors is null ? null : $"Error: {string.Join(", ", identityErrors.Select(error => error.Description))}";
public async Task RegisterUser(EditContext editContext)
{
var user = CreateUser();
await UserStore.SetUserNameAsync(user, Input.Email, CancellationToken.None);
var emailStore = GetEmailStore();
await emailStore.SetEmailAsync(user, Input.Email, CancellationToken.None);
var result = await UserManager.CreateAsync(user, Input.Password);
if (!result.Succeeded)
{
identityErrors = result.Errors;
return;
}
Logger.LogInformation("User created a new account with password.");
var userId = await UserManager.GetUserIdAsync(user);
var code = await UserManager.GenerateEmailConfirmationTokenAsync(user);
code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
var callbackUrl = NavigationManager.GetUriWithQueryParameters(
NavigationManager.ToAbsoluteUri("Account/ConfirmEmail").AbsoluteUri,
new Dictionary<string, object?> { ["userId"] = userId, ["code"] = code, ["returnUrl"] = ReturnUrl });
await EmailSender.SendConfirmationLinkAsync(user, Input.Email, HtmlEncoder.Default.Encode(callbackUrl));
if (UserManager.Options.SignIn.RequireConfirmedAccount)
{
RedirectManager.RedirectTo(
"Account/RegisterConfirmation",
new() { ["email"] = Input.Email, ["returnUrl"] = ReturnUrl });
}
await SignInManager.SignInAsync(user, isPersistent: false);
RedirectManager.RedirectTo(ReturnUrl);
}
private ApplicationUser CreateUser()
{
try
{
return Activator.CreateInstance<ApplicationUser>();
}
catch
{
throw new InvalidOperationException($"Can't create an instance of '{nameof(ApplicationUser)}'. " +
$"Ensure that '{nameof(ApplicationUser)}' is not an abstract class and has a parameterless constructor.");
}
}
private IUserEmailStore<ApplicationUser> GetEmailStore()
{
if (!UserManager.SupportsUserEmail)
{
throw new NotSupportedException("The default UI requires a user store with email support.");
}
return (IUserEmailStore<ApplicationUser>)UserStore;
}
private sealed class InputModel
{
[Required]
[EmailAddress]
[Display(Name = "Email")]
public string Email { get; set; } = "";
[Required]
[StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
[DataType(DataType.Password)]
[Display(Name = "Password")]
public string Password { get; set; } = "";
[DataType(DataType.Password)]
[Display(Name = "Confirm password")]
[Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]
public string ConfirmPassword { get; set; } = "";
}
}
@@ -0,0 +1,68 @@
@page "/Account/RegisterConfirmation"
@using System.Text
@using Microsoft.AspNetCore.Identity
@using Microsoft.AspNetCore.WebUtilities
@using WatchLog.Data
@inject UserManager<ApplicationUser> UserManager
@inject IEmailSender<ApplicationUser> EmailSender
@inject NavigationManager NavigationManager
@inject IdentityRedirectManager RedirectManager
<PageTitle>Register confirmation</PageTitle>
<h1>Register confirmation</h1>
<StatusMessage Message="@statusMessage" />
@if (emailConfirmationLink is not null)
{
<p>
This app does not currently have a real email sender registered, see <a href="https://aka.ms/aspaccountconf">these docs</a> for how to configure a real email sender.
Normally this would be emailed: <a href="@emailConfirmationLink">Click here to confirm your account</a>
</p>
}
else
{
<p role="alert">Please check your email to confirm your account.</p>
}
@code {
private string? emailConfirmationLink;
private string? statusMessage;
[CascadingParameter]
private HttpContext HttpContext { get; set; } = default!;
[SupplyParameterFromQuery]
private string? Email { get; set; }
[SupplyParameterFromQuery]
private string? ReturnUrl { get; set; }
protected override async Task OnInitializedAsync()
{
if (Email is null)
{
RedirectManager.RedirectTo("");
}
var user = await UserManager.FindByEmailAsync(Email);
if (user is null)
{
HttpContext.Response.StatusCode = StatusCodes.Status404NotFound;
statusMessage = "Error finding user for unspecified email";
}
else if (EmailSender is IdentityNoOpEmailSender)
{
// Once you add a real email sender, you should remove this code that lets you confirm the account
var userId = await UserManager.GetUserIdAsync(user);
var code = await UserManager.GenerateEmailConfirmationTokenAsync(user);
code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
emailConfirmationLink = NavigationManager.GetUriWithQueryParameters(
NavigationManager.ToAbsoluteUri("Account/ConfirmEmail").AbsoluteUri,
new Dictionary<string, object?> { ["userId"] = userId, ["code"] = code, ["returnUrl"] = ReturnUrl });
}
}
}
@@ -0,0 +1,68 @@
@page "/Account/ResendEmailConfirmation"
@using System.ComponentModel.DataAnnotations
@using System.Text
@using System.Text.Encodings.Web
@using Microsoft.AspNetCore.Identity
@using Microsoft.AspNetCore.WebUtilities
@using WatchLog.Data
@inject UserManager<ApplicationUser> UserManager
@inject IEmailSender<ApplicationUser> EmailSender
@inject NavigationManager NavigationManager
@inject IdentityRedirectManager RedirectManager
<PageTitle>Resend email confirmation</PageTitle>
<h1>Resend email confirmation</h1>
<h2>Enter your email.</h2>
<hr />
<StatusMessage Message="@message" />
<div class="row">
<div class="col-md-4">
<EditForm Model="Input" FormName="resend-email-confirmation" OnValidSubmit="OnValidSubmitAsync" method="post">
<DataAnnotationsValidator />
<ValidationSummary class="text-danger" role="alert" />
<div class="form-floating mb-3">
<InputText @bind-Value="Input.Email" id="Input.Email" class="form-control" aria-required="true" placeholder="name@example.com" />
<label for="Input.Email" class="form-label">Email</label>
<ValidationMessage For="() => Input.Email" class="text-danger" />
</div>
<button type="submit" class="w-100 btn btn-lg btn-primary">Resend</button>
</EditForm>
</div>
</div>
@code {
private string? message;
[SupplyParameterFromForm]
private InputModel Input { get; set; } = new();
private async Task OnValidSubmitAsync()
{
var user = await UserManager.FindByEmailAsync(Input.Email!);
if (user is null)
{
message = "Verification email sent. Please check your email.";
return;
}
var userId = await UserManager.GetUserIdAsync(user);
var code = await UserManager.GenerateEmailConfirmationTokenAsync(user);
code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
var callbackUrl = NavigationManager.GetUriWithQueryParameters(
NavigationManager.ToAbsoluteUri("Account/ConfirmEmail").AbsoluteUri,
new Dictionary<string, object?> { ["userId"] = userId, ["code"] = code });
await EmailSender.SendConfirmationLinkAsync(user, Input.Email, HtmlEncoder.Default.Encode(callbackUrl));
message = "Verification email sent. Please check your email.";
}
private sealed class InputModel
{
[Required]
[EmailAddress]
public string Email { get; set; } = "";
}
}
@@ -0,0 +1,103 @@
@page "/Account/ResetPassword"
@using System.ComponentModel.DataAnnotations
@using System.Text
@using Microsoft.AspNetCore.Identity
@using Microsoft.AspNetCore.WebUtilities
@using WatchLog.Data
@inject IdentityRedirectManager RedirectManager
@inject UserManager<ApplicationUser> UserManager
<PageTitle>Reset password</PageTitle>
<h1>Reset password</h1>
<h2>Reset your password.</h2>
<hr />
<div class="row">
<div class="col-md-4">
<StatusMessage Message="@Message" />
<EditForm Model="Input" FormName="reset-password" OnValidSubmit="OnValidSubmitAsync" method="post">
<DataAnnotationsValidator />
<ValidationSummary class="text-danger" role="alert" />
<input type="hidden" name="Input.Code" value="@Input.Code" />
<div class="form-floating mb-3">
<InputText @bind-Value="Input.Email" id="Input.Email" class="form-control" autocomplete="username" aria-required="true" placeholder="name@example.com" />
<label for="Input.Email" class="form-label">Email</label>
<ValidationMessage For="() => Input.Email" class="text-danger" />
</div>
<div class="form-floating mb-3">
<InputText type="password" @bind-Value="Input.Password" id="Input.Password" class="form-control" autocomplete="new-password" aria-required="true" placeholder="Please enter your password." />
<label for="Input.Password" class="form-label">Password</label>
<ValidationMessage For="() => Input.Password" class="text-danger" />
</div>
<div class="form-floating mb-3">
<InputText type="password" @bind-Value="Input.ConfirmPassword" id="Input.ConfirmPassword" class="form-control" autocomplete="new-password" aria-required="true" placeholder="Please confirm your password." />
<label for="Input.ConfirmPassword" class="form-label">Confirm password</label>
<ValidationMessage For="() => Input.ConfirmPassword" class="text-danger" />
</div>
<button type="submit" class="w-100 btn btn-lg btn-primary">Reset</button>
</EditForm>
</div>
</div>
@code {
private IEnumerable<IdentityError>? identityErrors;
[SupplyParameterFromForm]
private InputModel Input { get; set; } = new();
[SupplyParameterFromQuery]
private string? Code { get; set; }
private string? Message => identityErrors is null ? null : $"Error: {string.Join(", ", identityErrors.Select(error => error.Description))}";
protected override void OnInitialized()
{
if (Code is null)
{
RedirectManager.RedirectTo("Account/InvalidPasswordReset");
}
Input.Code = Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(Code));
}
private async Task OnValidSubmitAsync()
{
var user = await UserManager.FindByEmailAsync(Input.Email);
if (user is null)
{
// Don't reveal that the user does not exist
RedirectManager.RedirectTo("Account/ResetPasswordConfirmation");
}
var result = await UserManager.ResetPasswordAsync(user, Input.Code, Input.Password);
if (result.Succeeded)
{
RedirectManager.RedirectTo("Account/ResetPasswordConfirmation");
}
identityErrors = result.Errors;
}
private sealed class InputModel
{
[Required]
[EmailAddress]
public string Email { get; set; } = "";
[Required]
[StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
[DataType(DataType.Password)]
public string Password { get; set; } = "";
[DataType(DataType.Password)]
[Display(Name = "Confirm password")]
[Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]
public string ConfirmPassword { get; set; } = "";
[Required]
public string Code { get; set; } = "";
}
}
@@ -0,0 +1,7 @@
@page "/Account/ResetPasswordConfirmation"
<PageTitle>Reset password confirmation</PageTitle>
<h1>Reset password confirmation</h1>
<p role="alert">
Your password has been reset. Please <a href="Account/Login">click here to log in</a>.
</p>
@@ -0,0 +1,2 @@
@using WatchLog.Components.Account.Shared
@attribute [ExcludeFromInteractiveRouting]
@@ -0,0 +1,43 @@
@using Microsoft.AspNetCore.Authentication
@using Microsoft.AspNetCore.Identity
@using WatchLog.Data
@inject SignInManager<ApplicationUser> SignInManager
@inject IdentityRedirectManager RedirectManager
@if (externalLogins.Length == 0)
{
<div>
<p>
There are no external authentication services configured. See this <a href="https://go.microsoft.com/fwlink/?LinkID=532715">article
about setting up this ASP.NET application to support logging in via external services</a>.
</p>
</div>
}
else
{
<form class="form-horizontal" action="Account/PerformExternalLogin" method="post">
<div>
<AntiforgeryToken />
<input type="hidden" name="ReturnUrl" value="@ReturnUrl" />
<p>
@foreach (var provider in externalLogins)
{
<button type="submit" class="btn btn-primary" name="provider" value="@provider.Name" title="Log in using your @provider.DisplayName account">@provider.DisplayName</button>
}
</p>
</div>
</form>
}
@code {
private AuthenticationScheme[] externalLogins = [];
[SupplyParameterFromQuery]
private string? ReturnUrl { get; set; }
protected override async Task OnInitializedAsync()
{
externalLogins = (await SignInManager.GetExternalAuthenticationSchemesAsync()).ToArray();
}
}
@@ -0,0 +1,17 @@
@inherits LayoutComponentBase
@layout WatchLog.Components.Layout.MainLayout
<h1>Manage your account</h1>
<div>
<h2>Change your account settings</h2>
<hr />
<div class="row">
<div class="col-lg-3">
<ManageNavMenu />
</div>
<div class="col-lg-9">
@Body
</div>
</div>
</div>
@@ -0,0 +1,37 @@
@using Microsoft.AspNetCore.Identity
@using WatchLog.Data
@inject SignInManager<ApplicationUser> SignInManager
<ul class="nav nav-pills flex-column">
<li class="nav-item">
<NavLink class="nav-link" href="Account/Manage" Match="NavLinkMatch.All">Profile</NavLink>
</li>
<li class="nav-item">
<NavLink class="nav-link" href="Account/Manage/Email">Email</NavLink>
</li>
<li class="nav-item">
<NavLink class="nav-link" href="Account/Manage/ChangePassword">Password</NavLink>
</li>
@if (hasExternalLogins)
{
<li class="nav-item">
<NavLink class="nav-link" href="Account/Manage/ExternalLogins">External logins</NavLink>
</li>
}
<li class="nav-item">
<NavLink class="nav-link" href="Account/Manage/TwoFactorAuthentication">Two-factor authentication</NavLink>
</li>
<li class="nav-item">
<NavLink class="nav-link" href="Account/Manage/PersonalData">Personal data</NavLink>
</li>
</ul>
@code {
private bool hasExternalLogins;
protected override async Task OnInitializedAsync()
{
hasExternalLogins = (await SignInManager.GetExternalAuthenticationSchemesAsync()).Any();
}
}
@@ -0,0 +1,8 @@
@inject NavigationManager NavigationManager
@code {
protected override void OnInitialized()
{
NavigationManager.NavigateTo($"Account/Login?returnUrl={Uri.EscapeDataString(NavigationManager.Uri)}", forceLoad: true);
}
}
@@ -0,0 +1,28 @@
<StatusMessage Message="@StatusMessage" />
<h3>Recovery codes</h3>
<div class="alert alert-warning" role="alert">
<p>
<strong>Put these codes in a safe place.</strong>
</p>
<p>
If you lose your device and don't have the recovery codes you will lose access to your account.
</p>
</div>
<div class="row">
<div class="col-md-12">
@foreach (var recoveryCode in RecoveryCodes)
{
<div>
<code class="recovery-code">@recoveryCode</code>
</div>
}
</div>
</div>
@code {
[Parameter]
public string[] RecoveryCodes { get; set; } = [];
[Parameter]
public string? StatusMessage { get; set; }
}
@@ -0,0 +1,29 @@
@if (!string.IsNullOrEmpty(DisplayMessage))
{
var statusMessageClass = DisplayMessage.StartsWith("Error") ? "danger" : "success";
<div class="alert alert-@statusMessageClass" role="alert">
@DisplayMessage
</div>
}
@code {
private string? messageFromCookie;
[Parameter]
public string? Message { get; set; }
[CascadingParameter]
private HttpContext HttpContext { get; set; } = default!;
private string? DisplayMessage => Message ?? messageFromCookie;
protected override void OnInitialized()
{
messageFromCookie = HttpContext.Request.Cookies[IdentityRedirectManager.StatusCookieName];
if (messageFromCookie is not null)
{
HttpContext.Response.Cookies.Delete(IdentityRedirectManager.StatusCookieName);
}
}
}
+4 -3
View File
@@ -5,9 +5,10 @@
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<base href="/" />
<link rel="stylesheet" href="bootstrap/bootstrap.min.css" />
<link rel="stylesheet" href="app.css" />
<link rel="stylesheet" href="WatchLog.styles.css" />
<link rel="stylesheet" href="@Assets["lib/bootstrap/dist/css/bootstrap.min.css"]" />
<link rel="stylesheet" href="@Assets["app.css"]" />
<link rel="stylesheet" href="@Assets["WatchLog.styles.css"]" />
<ImportMap />
<link rel="icon" type="image/png" href="favicon.png" />
<HeadOutlet />
</head>
+3 -3
View File
@@ -16,8 +16,8 @@
</main>
</div>
<div id="blazor-error-ui">
<div id="blazor-error-ui" data-nosnippet>
An unhandled error has occurred.
<a href="" class="reload">Reload</a>
<a class="dismiss">🗙</a>
<a href="." class="reload">Reload</a>
<span class="dismiss">🗙</span>
</div>
@@ -77,9 +77,11 @@ main {
}
#blazor-error-ui {
color-scheme: light only;
background: lightyellow;
bottom: 0;
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
box-sizing: border-box;
display: none;
left: 0;
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
+68 -6
View File
@@ -1,4 +1,8 @@
<div class="top-row ps-3 navbar navbar-dark">
@implements IDisposable
@inject NavigationManager NavigationManager
<div class="top-row ps-3 navbar navbar-dark">
<div class="container-fluid">
<a class="navbar-brand" href="">WatchLog</a>
</div>
@@ -7,7 +11,7 @@
<input type="checkbox" title="Navigation menu" class="navbar-toggler" />
<div class="nav-scrollable" onclick="document.querySelector('.navbar-toggler').click()">
<nav class="flex-column">
<nav class="nav flex-column">
<div class="nav-item px-3">
<NavLink class="nav-link" href="" Match="NavLinkMatch.All">
<span class="bi bi-house-door-fill-nav-menu" aria-hidden="true"></span> Home
@@ -15,16 +19,74 @@
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="">
<span class="bi bi-plus-square-fill-nav-menu" aria-hidden="true"></span>
<NavLink class="nav-link" href="counter">
<span class="bi bi-plus-square-fill-nav-menu" aria-hidden="true"></span> Counter
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="">
<span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span>
<NavLink class="nav-link" href="weather">
<span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> Weather
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="auth">
<span class="bi bi-lock-nav-menu" aria-hidden="true"></span> Auth Required
</NavLink>
</div>
<AuthorizeView>
<Authorized>
<div class="nav-item px-3">
<NavLink class="nav-link" href="Account/Manage">
<span class="bi bi-person-fill-nav-menu" aria-hidden="true"></span> @context.User.Identity?.Name
</NavLink>
</div>
<div class="nav-item px-3">
<form action="Account/Logout" method="post">
<AntiforgeryToken />
<input type="hidden" name="ReturnUrl" value="@currentUrl" />
<button type="submit" class="nav-link">
<span class="bi bi-arrow-bar-left-nav-menu" aria-hidden="true"></span> Logout
</button>
</form>
</div>
</Authorized>
<NotAuthorized>
<div class="nav-item px-3">
<NavLink class="nav-link" href="Account/Register">
<span class="bi bi-person-nav-menu" aria-hidden="true"></span> Register
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="Account/Login">
<span class="bi bi-person-badge-nav-menu" aria-hidden="true"></span> Login
</NavLink>
</div>
</NotAuthorized>
</AuthorizeView>
</nav>
</div>
@code {
private string? currentUrl;
protected override void OnInitialized()
{
currentUrl = NavigationManager.ToBaseRelativePath(NavigationManager.Uri);
NavigationManager.LocationChanged += OnLocationChanged;
}
private void OnLocationChanged(object? sender, LocationChangedEventArgs e)
{
currentUrl = NavigationManager.ToBaseRelativePath(e.Location);
StateHasChanged();
}
public void Dispose()
{
NavigationManager.LocationChanged -= OnLocationChanged;
}
}
+21 -1
View File
@@ -16,7 +16,7 @@
}
.top-row {
height: 3.5rem;
min-height: 3.5rem;
background-color: rgba(0,0,0,0.4);
}
@@ -46,6 +46,26 @@
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E");
}
.bi-lock-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath d='M8 1a2 2 0 0 1 2 2v4H6V3a2 2 0 0 1 2-2zm3 6V3a3 3 0 0 0-6 0v4a2 2 0 0 0-2 2v5a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2zM5 8h6a1 1 0 0 1 1 1v5a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V9a1 1 0 0 1 1-1z'/%3E%3C/svg%3E");
}
.bi-person-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-person' viewBox='0 0 16 16'%3E%3Cpath d='M8 8a3 3 0 1 0 0-6 3 3 0 0 0 0 6Zm2-3a2 2 0 1 1-4 0 2 2 0 0 1 4 0Zm4 8c0 1-1 1-1 1H3s-1 0-1-1 1-4 6-4 6 3 6 4Zm-1-.004c-.001-.246-.154-.986-.832-1.664C11.516 10.68 10.289 10 8 10c-2.29 0-3.516.68-4.168 1.332-.678.678-.83 1.418-.832 1.664h10Z'/%3E%3C/svg%3E");
}
.bi-person-badge-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-person-badge' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 2a.5.5 0 0 0 0 1h3a.5.5 0 0 0 0-1h-3zM11 8a3 3 0 1 1-6 0 3 3 0 0 1 6 0z'/%3E%3Cpath d='M4.5 0A2.5 2.5 0 0 0 2 2.5V14a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V2.5A2.5 2.5 0 0 0 11.5 0h-7zM3 2.5A1.5 1.5 0 0 1 4.5 1h7A1.5 1.5 0 0 1 13 2.5v10.795a4.2 4.2 0 0 0-.776-.492C11.392 12.387 10.063 12 8 12s-3.392.387-4.224.803a4.2 4.2 0 0 0-.776.492V2.5z'/%3E%3C/svg%3E");
}
.bi-person-fill-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-person-fill' viewBox='0 0 16 16'%3E%3Cpath d='M3 14s-1 0-1-1 1-4 6-4 6 3 6 4-1 1-1 1H3Zm5-6a3 3 0 1 0 0-6 3 3 0 0 0 0 6Z'/%3E%3C/svg%3E");
}
.bi-arrow-bar-left-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-arrow-bar-left' viewBox='0 0 16 16'%3E%3Cpath d='M12.5 15a.5.5 0 0 1-.5-.5v-13a.5.5 0 0 1 1 0v13a.5.5 0 0 1-.5.5ZM10 8a.5.5 0 0 1-.5.5H3.707l2.147 2.146a.5.5 0 0 1-.708.708l-3-3a.5.5 0 0 1 0-.708l3-3a.5.5 0 1 1 .708.708L3.707 7.5H9.5a.5.5 0 0 1 .5.5Z'/%3E%3C/svg%3E");
}
.nav-item {
font-size: 0.9rem;
padding-bottom: 0.5rem;
+13
View File
@@ -0,0 +1,13 @@
@page "/auth"
@using Microsoft.AspNetCore.Authorization
@attribute [Authorize]
<PageTitle>Auth</PageTitle>
<h1>You are authenticated</h1>
<AuthorizeView>
Hello @context.User.Identity?.Name!
</AuthorizeView>
Whitespace-only changes.
+23
View File
@@ -0,0 +1,23 @@
@page "/counter"
@rendermode InteractiveServer
@using Microsoft.AspNetCore.Authorization
@attribute [Authorize]
<PageTitle>Counter</PageTitle>
<h1>Counter</h1>
<p role="status">Current count: @currentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
@code {
private int currentCount = 0;
private void IncrementCount()
{
currentCount++;
}
}
@@ -1,38 +0,0 @@
@rendermode InteractiveServer
@page "/GlobalEntities"
@using Microsoft.EntityFrameworkCore
@using WatchLog.Data
@inject IDbContextFactory<WatchLogDataContext> WatchLogDataContextFactory;
<PageTitle>GlobalEntities</PageTitle>
@if (ShowCreate)
{
<h3>Add a New GlobalEntity</h3>
<div class="row">
<label for="Title" class="col-4 col-form-label">Name</label>
<div class="col-8">
<input id="Title" name="Title" type="text" class="form-control" @bind="@NewGlobalEntity.Title" />
</div>
</div>
<div class="row">
<label for="Password" class="col-4 col-form-label">Password</label>
<div class="col-8">
<input id="Password" name="Password" type="text" class="form-control"/>
</div>
</div>
<div class="form-group row">
<div class="offset-4 col-8">
<button name="submit" type="submit" class="btn btn-primary" @onclick="CreateNewGlobalEntity">Submit</button>
</div>
</div>
}
else
{
<div class="form-group row">
<div class="offset-4 col-8">
<button name="submit" type="submit" class="btn btn-primary" @onclick="ShowCreateForm">Add a new Global Entity</button>
</div>
</div>
<p>SHOW THE LIST</p>
}
@@ -1,45 +0,0 @@
using Microsoft.AspNetCore.Components;
using WatchLog.Data;
namespace WatchLog.Components.Pages
{
public partial class GlobalEntities
{
public bool ShowCreate { get; set; }
private WatchLogDataContext? _context;
public required GlobalEntity NewGlobalEntity { get; set; }
protected override Task OnInitializedAsync()
{
ShowCreate = false;
return Task.CompletedTask;
}
public void ShowCreateForm()
{
ShowCreate = true;
NewGlobalEntity = new GlobalEntity
{
Title = "",
CreationTime = DateTime.Now,
CreatorId = 1,
TypeId = 1,
};
}
public async Task CreateNewGlobalEntity()
{
_context ??= await WatchLogDataContextFactory.CreateDbContextAsync();
if (NewGlobalEntity is not null)
{
_context?.GlobalEntities.Add(NewGlobalEntity);
_context?.SaveChangesAsync();
}
ShowCreate = false;
}
}
}
+4
View File
@@ -1,5 +1,9 @@
@page "/"
@using Microsoft.AspNetCore.Authorization
@attribute [Authorize]
<PageTitle>Home</PageTitle>
<h1>Hello, world!</h1>
+68
View File
@@ -0,0 +1,68 @@
@page "/weather"
@attribute [StreamRendering]
@using Microsoft.AspNetCore.Authorization
@attribute [Authorize(Roles = "Admin")]
<PageTitle>Weather</PageTitle>
<h1>Weather</h1>
<p>This component demonstrates showing data.</p>
@if (forecasts == null)
{
<p><em>Loading...</em></p>
}
else
{
<table class="table">
<thead>
<tr>
<th>Date</th>
<th aria-label="Temperature in Celsius">Temp. (C)</th>
<th aria-label="Temperature in Farenheit">Temp. (F)</th>
<th>Summary</th>
</tr>
</thead>
<tbody>
@foreach (var forecast in forecasts)
{
<tr>
<td>@forecast.Date.ToShortDateString()</td>
<td>@forecast.TemperatureC</td>
<td>@forecast.TemperatureF</td>
<td>@forecast.Summary</td>
</tr>
}
</tbody>
</table>
}
@code {
private WeatherForecast[]? forecasts;
protected override async Task OnInitializedAsync()
{
// Simulate asynchronous loading to demonstrate streaming rendering
await Task.Delay(500);
var startDate = DateOnly.FromDateTime(DateTime.Now);
var summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" };
forecasts = Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = startDate.AddDays(index),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = summaries[Random.Shared.Next(summaries.Length)]
}).ToArray();
}
private class WeatherForecast
{
public DateOnly Date { get; set; }
public int TemperatureC { get; set; }
public string? Summary { get; set; }
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}
}
+7 -2
View File
@@ -1,6 +1,11 @@
<Router AppAssembly="typeof(Program).Assembly">
@using WatchLog.Components.Account.Shared
<Router AppAssembly="typeof(Program).Assembly">
<Found Context="routeData">
<RouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)" />
<AuthorizeRouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)">
<NotAuthorized>
<RedirectToLogin />
</NotAuthorized>
</AuthorizeRouteView>
<FocusOnNavigate RouteData="routeData" Selector="h1" />
</Found>
</Router>
+1
View File
@@ -1,5 +1,6 @@
@using System.Net.Http
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
+27
View File
@@ -0,0 +1,27 @@
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
namespace WatchLog.Data
{
public class ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : IdentityDbContext<ApplicationUser, IdentityRole, string>(options)
{
// Global
public DbSet<Genre> Genres { get; set; }
public DbSet<GlobalEntity> GlobalEntities { get; set; }
public DbSet<StreamingPlatform> StreamingPlatforms { get; set; }
public DbSet<MediaType> MediaType { get; set; } // 'Watchlog.Data.Type' if namecolsion with System.Type
//Private
public DbSet<Label> Labels { get; set; }
public DbSet<PrivateEntity> PrivateEntities { get; set; }
public DbSet<UserWatchStatus> UserWatchStatuses { get; set; }
//Shared
public DbSet<SharedList> SharedLists { get; set; }
public DbSet<SharedListEntity> SharedListEntities { get; set; }
public DbSet<SharedListLabel> SharedListLabels { get; set; }
public DbSet<SharedWatchStatus> SharedWatchStatuses { get; set; }
}
}
+20
View File
@@ -0,0 +1,20 @@
using Microsoft.AspNetCore.Identity;
namespace WatchLog.Data
{
// Add profile data for application users by adding properties to the ApplicationUser class
public class ApplicationUser : IdentityUser
{
// --- Navigation Properties ---
public virtual ICollection<PrivateEntity> PrivateEntities { get; set; } = new List<PrivateEntity>();
public virtual ICollection<GlobalEntity> CreatedGlobalEntities { get; set; } = new List<GlobalEntity>();
public virtual ICollection<Label> CreatedLabels { get; set; } = new List<Label>();
public virtual ICollection<UserWatchStatus> UserWatchStatuses { get; set; } = new List<UserWatchStatus>();
public virtual ICollection<LinkTableSharedUser> LinkTableSharedUsers { get; set; } = new List<LinkTableSharedUser>();
}
}
@@ -26,15 +26,15 @@ namespace WatchLog.Data
public required int TypeId { get; set; }
[Required]
public required int CreatorId { get; set; }
public required string CreatorId { get; set; }
// --- Navigation Properties ---
[ForeignKey(nameof(TypeId))]
public virtual Type Type { get; set; } = null!;
public virtual MediaType MediaType { get; set; } = null!;
[ForeignKey(nameof(CreatorId))]
public virtual AppUser User { get; set; } = null!;
public virtual ApplicationUser User { get; set; } = null!;
public virtual ICollection<LinkTableGlobalGenre> LinkTableGlobalGenres { get; set; } = new List<LinkTableGlobalGenre>();
@@ -2,7 +2,7 @@
namespace WatchLog.Data
{
public class Type
public class MediaType
{
[Key]
public int Id { get; set; }
@@ -20,12 +20,12 @@ namespace WatchLog.Data
// --- Foreign Key ---
[Required]
public int CreatorId { get; set; }
public string? CreatorId { get; set; }
// --- Navigation Properties ---
[ForeignKey(nameof(CreatorId))]
public virtual AppUser User { get; set; } = null!;
public virtual ApplicationUser User { get; set; } = null!;
public virtual ICollection<LinkTablePrivateLabel> LinkTablePrivateLabels { get; set; } = new List<LinkTablePrivateLabel>();
}
@@ -27,7 +27,7 @@ namespace WatchLog.Data
// --- Foreign Keys ---
[Required]
public int UserId { get; set; }
public string? UserId { get; set; }
[Required]
public int GlobalEntityId { get; set; }
@@ -37,7 +37,7 @@ namespace WatchLog.Data
// --- Navigation Properties ---
[ForeignKey(nameof(UserId))]
public virtual AppUser User { get; set; } = null!;
public virtual ApplicationUser User { get; set; } = null!;
[ForeignKey(nameof(GlobalEntityId))]
public virtual GlobalEntity GlobalEntity { get; set; } = null!;
@@ -26,12 +26,12 @@ namespace WatchLog.Data
// --- Foreign Key ---
[Required]
public int UserId { get; set; }
public string? UserId { get; set; }
// --- Navigation Properties ---
[ForeignKey(nameof(UserId))]
public virtual AppUser User { get; set; } = null!;
public virtual ApplicationUser User { get; set; } = null!;
public virtual ICollection<PrivateEntity> PrivateEntities { get; set; } = new List<PrivateEntity>();
}
@@ -8,7 +8,7 @@ namespace WatchLog.Data
{
// --- Foreign Keys ---
public int SharedListId { get; set; }
public int UserId { get; set; }
public string? UserId { get; set; }
// --- Navigation Properties ---
@@ -16,6 +16,6 @@ namespace WatchLog.Data
public virtual SharedList SharedList { get; set; } = null!;
[ForeignKey(nameof(UserId))]
public virtual AppUser User { get; set; } = null!;
public virtual ApplicationUser User { get; set; } = null!;
}
}
Binary file not shown.
Binary file not shown.
-45
View File
@@ -1,45 +0,0 @@
using Microsoft.EntityFrameworkCore;
namespace WatchLog.Data
{
public class WatchLogDataContext : DbContext
{
protected readonly IConfiguration Configuration;
public WatchLogDataContext(IConfiguration configuration)
{
Configuration = configuration;
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlite(Configuration.GetConnectionString("WatchLogDB"));
}
//Note: Link Tables a commented out because Entity Framework creates the tables by itself
// Global
public DbSet<Genre> Genres { get; set; }
public DbSet<GlobalEntity> GlobalEntities { get; set; }
//public DbSet<LinkTableGlobalGenre> LinkTableGlobalGenres { get; set; }
public DbSet<StreamingPlatform> StreamingPlatforms { get; set; }
public DbSet<Type> Types { get; set; } // 'Watchlog.Data.Type' if namecolsion with System.Type
public DbSet<AppUser> AppUsers { get; set; }
//Private
public DbSet<Label> Labels { get; set; }
//public DbSet<LinkTablePrivateLabel> LinkTablePrivateLabels { get; set; }
//public DbSet<LinkTablePrivateStreamingPlatform> LinkTablePrivateStreamingPlatform { get; set; }
public DbSet<PrivateEntity> PrivateEntities { get; set; }
public DbSet<UserWatchStatus> UserWatchStatuses { get; set; }
//Shared
//public DbSet<LinkTableSharedLabel> LinkTableSharedLabels { get; set; }
//public DbSet<LinkTableSharedStreamingPlatform> LinkTableSharedStreamingPlatforms { get; set; }
//public DbSet<LinkTableSharedUser> LinkTableSharedUsers { get; set; }
public DbSet<SharedList> SharedLists { get; set; }
public DbSet<SharedListEntity> SharedListEntities { get; set; }
public DbSet<SharedListLabel> SharedListLabels { get; set; }
public DbSet<SharedWatchStatus> SharedWatchStatuses { get; set; }
}
}
+130 -80
View File
@@ -1,87 +1,137 @@
using WatchLog.Components;
using WatchLog.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using WatchLog.Components;
using WatchLog.Components.Account;
using WatchLog.Data;
namespace WatchLog
{
public class Program
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents();
builder.Services.AddCascadingAuthenticationState();
builder.Services.AddScoped<IdentityUserAccessor>();
builder.Services.AddScoped<IdentityRedirectManager>();
builder.Services.AddScoped<AuthenticationStateProvider, IdentityRevalidatingAuthenticationStateProvider>();
builder.Services.AddAuthentication(options =>
{
public static void Main(string[] args)
options.DefaultScheme = IdentityConstants.ApplicationScheme;
options.DefaultSignInScheme = IdentityConstants.ExternalScheme;
})
.AddIdentityCookies();
var connectionString = builder.Configuration.GetConnectionString("WatchLogDB") ?? throw new InvalidOperationException("Connection string 'DefaultConnection' not found.");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlite(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddIdentityCore<ApplicationUser>(options => options.SignIn.RequireConfirmedAccount = true)
.AddRoles<IdentityRole>() // <-- Das ist der wichtige Zusatz
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddSignInManager()
.AddDefaultTokenProviders();
builder.Services.AddSingleton<IEmailSender<ApplicationUser>, IdentityNoOpEmailSender>();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseMigrationsEndPoint();
}
else
{
app.UseExceptionHandler("/Error", createScopeForErrors: true);
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseAntiforgery();
app.MapStaticAssets();
app.MapRazorComponents<App>()
.AddInteractiveServerRenderMode();
// Add additional endpoints required by the Identity /Account Razor components.
app.MapAdditionalIdentityEndpoints();
using (var scope = app.Services.CreateScope())
{
var roleManager = scope.ServiceProvider.GetRequiredService<RoleManager<IdentityRole>>();
var userManager = scope.ServiceProvider.GetRequiredService<UserManager<ApplicationUser>>(); // UserManager hinzufügen
string[] roleNames = { "Admin", "User" };
IdentityResult roleResult;
foreach (var roleName in roleNames)
{
var roleExist = await roleManager.RoleExistsAsync(roleName);
if (!roleExist)
{
var builder = WebApplication.CreateBuilder(args);
var connectionString = builder.Configuration.GetConnectionString("WatchLogDB");
// Add services to the container.
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents();
builder.Services.AddDbContextFactory<WatchLogDataContext>(options => options.UseSqlite(connectionString));
builder.Services.AddHttpContextAccessor();
builder.Services.AddIdentityCore<AppUser>(options =>
{
// Hier könntest du Passwortregeln festlegen, z.B.
options.Password.RequireDigit = false;
options.Password.RequireNonAlphanumeric = false;
options.Password.RequiredLength = 4; // Nur für Entwicklung!
})
.AddSignInManager() // Fügt den SignInManager hinzu, der den Login-Prozess steuert.
.AddDefaultTokenProviders(); // Nötig für Features wie Passwort-Reset.
// 2. Jetzt sagen wir Identity, welche Klassen es für seine Aufgaben verwenden soll.
// Dies ist der wichtigste Teil!
builder.Services.AddScoped<IUserStore<AppUser>, MyUserStore>();
builder.Services.AddScoped<IPasswordHasher<AppUser>, PasswordHasher<AppUser>>();
// 3. Da wir IdentityCore verwenden, müssen wir die Cookie-Authentifizierung selbst hinzufügen.
// Die Konfiguration ist fast identisch zu deiner alten, aber sie ist jetzt
// an das Identity-System gekoppelt.
builder.Services.AddAuthentication(IdentityConstants.ApplicationScheme)
.AddCookie(IdentityConstants.ApplicationScheme, options =>
{
options.Cookie.Name = "WatchLogAuthCookie";
options.LoginPath = "/login";
options.LogoutPath = "/logout";
options.AccessDeniedPath = "/access-denied";
options.ExpireTimeSpan = TimeSpan.FromDays(1);
options.SlidingExpiration = true;
});
// 4. Die Autorisierungs-Policy ist perfekt und bleibt genau so!
// Sie sorgt dafür, dass alle Seiten standardmäßig einen Login erfordern.
builder.Services.AddAuthorization(options =>
{
options.FallbackPolicy = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build();
});
var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseAntiforgery();
app.UseAuthentication();
app.UseAuthorization();
app.MapRazorComponents<App>()
.AddInteractiveServerRenderMode();
app.Run();
roleResult = await roleManager.CreateAsync(new IdentityRole(roleName));
}
}
// --- HIER BEGINNT DER NEUE TEIL ---
// Erstellt den Admin-Benutzer und weist ihm die Admin-Rolle zu.
// WICHTIG: Ändere hier die E-Mail-Adresse und das Passwort!
var adminEmail = "admin@deine-app.de";
var adminPassword = "EinSehrSicheresPasswort123!"; // Nur für lokale Entwicklung, besser aus Konfiguration laden
var normalUserEmail = "user@deine-app.de";
// Sucht nach dem Benutzer anhand der E-Mail.
var adminUser = await userManager.FindByEmailAsync(adminEmail);
var normalUser = await userManager.FindByEmailAsync(normalUserEmail);
// Wenn der Admin-Benutzer NICHT existiert, erstellen wir ihn.
if (adminUser == null)
{
adminUser = new ApplicationUser
{
UserName = adminEmail,
Email = adminEmail,
EmailConfirmed = true // Wichtig, damit er sich direkt einloggen kann
};
// Erstellt den Benutzer mit dem definierten Passwort.
var createResult = await userManager.CreateAsync(adminUser, adminPassword);
// Wenn die Erstellung erfolgreich war, weisen wir die Admin-Rolle zu.
if (createResult.Succeeded)
{
await userManager.AddToRoleAsync(adminUser, "Admin");
}
}
else if (normalUser == null)
{
normalUser = new ApplicationUser
{
UserName = normalUserEmail,
Email = normalUserEmail,
EmailConfirmed = true
};
var createResult = await userManager.CreateAsync(normalUser, adminPassword);
if (createResult.Succeeded)
{
await userManager.AddToRoleAsync(adminUser, "User");
}
}
else if (!await userManager.IsInRoleAsync(normalUser, "User"))
{
await userManager.AddToRoleAsync(normalUser, "User");
}
// Optional: Wenn der Benutzer bereits existiert, aber kein Admin ist.
else if (!await userManager.IsInRoleAsync(adminUser, "Admin"))
{
await userManager.AddToRoleAsync(adminUser, "Admin");
}
}
app.Run();
+18 -43
View File
@@ -1,48 +1,23 @@
{
"profiles": {
"http": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "http://localhost:5075",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"dotnetRunMessages": true,
"applicationUrl": "http://localhost:5142"
},
"https": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"dotnetRunMessages": true,
"applicationUrl": "https://localhost:7170;http://localhost:5142"
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "https://localhost:7045;http://localhost:5075",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
},
"WSL": {
"commandName": "WSL2",
"launchBrowser": true,
"launchUrl": "https://localhost:7170",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"ASPNETCORE_URLS": "https://localhost:7170;http://localhost:5142"
},
"distributionName": ""
}
},
"$schema": "http://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:34600",
"sslPort": 44365
}
}
}
@@ -0,0 +1,8 @@
{
"dependencies": {
"mssql1": {
"type": "mssql",
"connectionId": "ConnectionStrings:DefaultConnection"
}
}
}
@@ -0,0 +1,8 @@
{
"dependencies": {
"mssql1": {
"type": "mssql.local",
"connectionId": "ConnectionStrings:DefaultConnection"
}
}
}
@@ -0,0 +1,9 @@
{
"dependencies": {
"mssql1": {
"restored": true,
"restoreTime": "2025-09-23T16:40:52.2029789Z"
}
},
"parameters": {}
}
+13 -12
View File
@@ -1,23 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UserSecretsId>e9a7853f-a38d-4eaf-ab60-c21c566189c7</UserSecretsId>
<UserSecretsId>aspnet-WatchLog-900f4f9e-6fed-4572-b3fa-769f2806a076</UserSecretsId>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.15" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.4">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.4" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.4">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="9.0.9" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="9.0.9" />
<PackageReference Include="Microsoft.AspNetCore.Identity.UI" Version="9.0.9" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.9" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.9" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.9" />
</ItemGroup>
<ItemGroup>
<Folder Include="Data\DatabaseModels\Shared\" />
<Folder Include="Data\DatabaseModels\Private\" />
</ItemGroup>
</Project>
-3
View File
@@ -3,7 +3,4 @@
<PropertyGroup>
<ActiveDebugProfile>https</ActiveDebugProfile>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<DebuggerFlavor>ProjectDebugger</DebuggerFlavor>
</PropertyGroup>
</Project>
+7 -7
View File
@@ -1,9 +1,9 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.13.35931.197 d17.13
VisualStudioVersion = 17.14.36511.14 d17.14
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WatchLog", "WatchLog.csproj", "{A75AFD13-6450-41D3-ABF2-F0C3D59D7ED3}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WatchLog", "WatchLog.csproj", "{4FBF15C3-5FC5-4605-9EBC-3F7DA1ADC47A}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -11,15 +11,15 @@ Global
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{A75AFD13-6450-41D3-ABF2-F0C3D59D7ED3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A75AFD13-6450-41D3-ABF2-F0C3D59D7ED3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A75AFD13-6450-41D3-ABF2-F0C3D59D7ED3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A75AFD13-6450-41D3-ABF2-F0C3D59D7ED3}.Release|Any CPU.Build.0 = Release|Any CPU
{4FBF15C3-5FC5-4605-9EBC-3F7DA1ADC47A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4FBF15C3-5FC5-4605-9EBC-3F7DA1ADC47A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4FBF15C3-5FC5-4605-9EBC-3F7DA1ADC47A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4FBF15C3-5FC5-4605-9EBC-3F7DA1ADC47A}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {169FC3D2-35C2-488F-9A99-46E927EFC891}
SolutionGuid = {F608BFC0-F933-48FA-8F4D-ADA53B025DE3}
EndGlobalSection
EndGlobal
+3 -3
View File
@@ -1,12 +1,12 @@
{
"ConnectionStrings": {
"WatchLogDB": "Data Source=Data\\WatchLog.db"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"ConnectionStrings": {
"WatchLogDB": "Data Source=Data\\WatchLog.db"
},
"AllowedHosts": "*"
}
View File
Whitespace-only changes.
+9
View File
@@ -49,3 +49,12 @@ h1:focus {
.darker-border-checkbox.form-check-input {
border-color: #929292;
}
.form-floating > .form-control-plaintext::placeholder, .form-floating > .form-control::placeholder {
color: var(--bs-secondary-color);
text-align: end;
}
.form-floating > .form-control-plaintext:focus::placeholder, .form-floating > .form-control:focus::placeholder {
text-align: start;
}
-7
View File
@@ -1,7 +0,0 @@
@charset "UTF-8";/*!
* Bootstrap v5.1.0 (https://getbootstrap.com/)
* Copyright 2011-2021 The Bootstrap Authors
* Copyright 2011-2021 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
*/:root{--bs-blue:#0d6efd;--bs-indigo:#6610f2;--bs-purple:#6f42c1;--bs-pink:#d63384;--bs-red:#dc3545;--bs-orange:#fd7e14;--bs-yellow:#ffc107;--bs-green:#198754;--bs-teal:#20c997;--bs-cyan:#0dcaf0;--bs-white:#fff;--bs-gray:#6c757d;--bs-gray-dark:#343a40;--bs-gray-100:#f8f9fa;--bs-gray-200:#e9ecef;--bs-gray-300:#dee2e6;--bs-gray-400:#ced4da;--bs-gray-500:#adb5bd;--bs-gray-600:#6c757d;--bs-gray-700:#495057;--bs-gray-800:#343a40;--bs-gray-900:#212529;--bs-primary:#0d6efd;--bs-secondary:#6c757d;--bs-success:#198754;--bs-info:#0dcaf0;--bs-warning:#ffc107;--bs-danger:#dc3545;--bs-light:#f8f9fa;--bs-dark:#212529;--bs-primary-rgb:13,110,253;--bs-secondary-rgb:108,117,125;--bs-success-rgb:25,135,84;--bs-info-rgb:13,202,240;--bs-warning-rgb:255,193,7;--bs-danger-rgb:220,53,69;--bs-light-rgb:248,249,250;--bs-dark-rgb:33,37,41;--bs-white-rgb:255,255,255;--bs-black-rgb:0,0,0;--bs-body-rgb:33,37,41;--bs-font-sans-serif:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--bs-font-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--bs-gradient:linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));--bs-body-font-family:var(--bs-font-sans-serif);--bs-body-font-size:1rem;--bs-body-font-weight:400;--bs-body-line-height:1.5;--bs-body-color:#212529;--bs-body-bg:#fff}*,::after,::before{box-sizing:border-box}@media (prefers-reduced-motion:no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:var(--bs-body-font-family);font-size:var(--bs-body-font-size);font-weight:var(--bs-body-font-weight);line-height:var(--bs-body-line-height);color:var(--bs-body-color);text-align:var(--bs-body-text-align);background-color:var(--bs-body-bg);-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}hr{margin:1rem 0;color:inherit;background-color:currentColor;border:0;opacity:.25}hr:not([size]){height:1px}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem;font-weight:500;line-height:1.2}.h1,h1{font-size:calc(1.375rem + 1.5vw)}@media (min-width:1200px){.h1,h1{font-size:2.5rem}}.h2,h2{font-size:calc(1.325rem + .9vw)}@media (min-width:1200px){.h2,h2{font-size:2rem}}.h3,h3{font-size:calc(1.3rem + .6vw)}@media (min-width:1200px){.h3,h3{font-size:1.75rem}}.h4,h4{font-size:calc(1.275rem + .3vw)}@media (min-width:1200px){.h4,h4{font-size:1.5rem}}.h5,h5{font-size:1.25rem}.h6,h6{font-size:1rem}p{margin-top:0;margin-bottom:1rem}abbr[data-bs-original-title],abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-left:2rem}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}.small,small{font-size:.875em}.mark,mark{padding:.2em;background-color:#fcf8e3}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#0d6efd;text-decoration:underline}a:hover{color:#0a58ca}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:var(--bs-font-monospace);font-size:1em;direction:ltr;unicode-bidi:bidi-override}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:.875em}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:.875em;color:#d63384;word-wrap:break-word}a>code{color:inherit}kbd{padding:.2rem .4rem;font-size:.875em;color:#fff;background-color:#212529;border-radius:.2rem}kbd kbd{padding:0;font-size:1em;font-weight:700}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.5rem;padding-bottom:.5rem;color:#6c757d;text-align:left}th{text-align:inherit;text-align:-webkit-match-parent}tbody,td,tfoot,th,thead,tr{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]::-webkit-calendar-picker-indicator{display:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:left;width:100%;padding:0;margin-bottom:.5rem;font-size:calc(1.275rem + .3vw);line-height:inherit}@media (min-width:1200px){leLine truncated
/*# sourceMappingURL=bootstrap.min.css.map */
@@ -1 +0,0 @@
{"version":3,"sources":["../../scss/bootstrap.scss","../../scss/_root.scss","../../scss/_reboot.scss","dist/css/bootstrap.css","../../scss/vendor/_rfs.scss","../../scss/mixins/_border-radius.scss","../../scss/_type.scss","../../scss/mixins/_lists.scss","../../scss/_images.scss","../../scss/mixins/_image.scss","../../scss/_containers.scss","../../scss/mixins/_container.scss","../../scss/mixins/_breakpoints.scss","../../scss/_grid.scss","../../scss/mixins/_grid.scss","../../scss/_tables.scss","../../scss/mixins/_table-variants.scss","../../scss/forms/_labels.scss","../../scss/forms/_form-text.scss","../../scss/forms/_form-control.scss","../../scss/mixins/_transition.scss","../../scss/mixins/_gradients.scss","../../scss/forms/_form-select.scss","../../scss/forms/_form-check.scss","../../scss/forms/_form-range.scss","../../scss/forms/_floating-labels.scss","../../scss/forms/_input-group.scss","../../scss/mixins/_forms.scss","../../scss/_buttons.scss","../../scss/mixins/_buttons.scss","../../scss/_transitions.scss","../../scss/_dropdown.scss","../../scss/mixins/_caret.scss","../../scss/_button-group.scss","../../scss/_nav.scss","../../scss/_navbar.scss","../../scss/_card.scss","../../scss/_accordion.scss","../../scss/_breadcrumb.scss","../../scss/_pagination.scss","../../scss/mixins/_pagination.scss","../../scss/_badge.scss","../../scss/_alert.scss","../../scss/mixins/_alert.scss","../../scss/_progress.scss","../../scss/_list-group.scss","../../scss/mixins/_list-group.scss","../../scss/_close.scss","../../scss/_toasts.scss","../../scss/_modal.scss","../../scss/mixins/_backdrop.scss","../../scss/_tooltip.scss","../../scss/mixins/_reset-text.scss","../../scss/_popover.scss","../../scss/_carousel.scss","../../scss/mixins/_clearfix.scss","../../scss/_spinners.scss","../../scss/_offcanvas.scss","../../scss/_placeholders.scss","../../scss/helpers/_colored-links.scss","../../scss/helpers/_ratio.scss","../../scss/helpers/_position.scss","../../scss/helpers/_stacks.scss","../../scss/helpers/_visually-hidden.scss","../../scss/mixins/_visually-hidden.scss","../../scss/helpers/_stretched-link.scss","../../scss/helpers/_text-truncation.scss","../../scss/mixins/_text-truncate.scss","../../scss/helpers/_vr.scss","../../scss/mixins/_utilities.scss","../../scss/utilities/_api.scss"],"names":[],"mappings":"iBAAA;;;;;ACAA,MAQI,UAAA,QAAA,YAAA,QAAA,YAAA,QAAA,UAAA,QAAA,SAAA,QAAA,YAAA,QAAA,YAAA,QAAA,WAAA,QAAA,UAAA,QAAA,UAAA,QAAA,WAAA,KAAA,UAAA,QAAA,eAAA,QAIA,cAAA,QAAA,cAAA,QAAA,cAAA,QAAA,cAAA,QAAA,cAAA,QAAA,cAAA,QAAA,cAAA,QAAA,cAAA,QAAA,cAAA,QAIA,aAAA,QAAA,eAAA,QAAA,aAAA,QAAA,UAAA,QAAA,aAAA,QAAA,YAAA,QAAA,WAAA,QAAA,UAAA,QAIA,iBAAA,EAAA,CAAA,GAAA,CAAA,IAAA,mBAAA,GAAA,CAAA,GAAA,CAAA,IAAA,iBAAA,EAAA,CAAA,GAAA,CAAA,GAAA,cAAA,EAAA,CAAA,GAAA,CAAA,IAAA,iBAAA,GAAA,CAAA,GAAA,CAAA,EAAA,gBAAA,GAAA,CAAA,EAAA,CAAA,GAAA,eAAA,GAAA,CAAA,GAAA,CAAA,IAAA,cAAA,EAAA,CAAA,EAAA,CAAA,GAGF,eAAA,GAAA,CAAA,GAAA,CAAA,IACA,eAAA,CAAA,CAAA,CAAA,CAAA,EACA,cAAA,EAAA,CAAA,EAAA,CAAA,GAMA,qBAAA,SAAA,CAAA,aAAA,CAAA,UAAA,CAAA,MAAA,CAAA,gBAAA,CAAA,KAAA,CAAA,WAAA,CAAA,iBAAA,CAAA,UAAA,CAAA,mBAAA,CAAA,gBAAA,CAAA,iBAAA,CAAA,mBACA,oBAAA,cAAA,CAAA,KAAA,CAAA,MAAA,CAAA,QAAA,CAAA,iBAAA,CAAA,aAAA,CAAA,UACA,cAAA,2EAQA,sBAAA,0BACA,oBAAA,KACA,sBAAA,IACA,sBAAA,IACA,gBAAA,QAIA,aAAA,KClCF,EC+CA,QADA,SD3CE,WAAA,WAeE,8CANJ,MAOM,gBAAA,QAcN,KACE,OAAA,EACA,YAAA,2BEmPI,UAAA,yBFjPJ,YAAA,2BACA,YAAA,2BACA,MAAA,qBACA,WAAA,0BACA,iBAAA,kBACA,yBAAA,KACA,4BAAA,YAUF,GACE,OAAA,KAAA,EACA,MAAA,QACA,iBAAA,aACA,OAAA,EACA,QAAA,IAGF,eACE,OAAA,IAUF,IAAA,IAAA,IAAA,IAAA,IAAA,IAAA,GAAA,GAAA,GAAA,GAAA,GAAA,GACE,WAAA,EACA,cAAA,MAGA,YAAA,IACA,YAAA,IAIF,IAAA,GEwMQ,UAAA,uBAlKJ,0BFtCJ,IAAA,GE+MQ,UAAA,QF1MR,IAAA,GEmMQ,UAAA,sBAlKJ,0BFjCJ,IAAA,GE0MQ,UAAA,MFrMR,IAAA,GE8LQ,UAAA,oBAlKJ,0BF5BJ,IAAA,GEqMQ,UAAA,SFhMR,IAAA,GEyLQ,UAAA,sBAlKJ,0BFvBJ,IAAA,GEgMQ,UAAA,QF3LR,IAAA,GEgLM,UAAA,QF3KN,IAAA,GE2KM,UAAA,KFhKN,EACE,WAAA,EACA,cAAA,KCmBF,6BDRA,YAEE,wBAAA,UAAA,OAAA,gBAAA,UAAA,OACA,OAAA,KACA,iCAAA,KAAA,yBAAA,KAMF,QACE,cAAA,KACA,WAAA,OACA,YAAA,QAMF,GCIA,GDFE,aAAA,KCQF,GDLA,GCIA,GDDE,WAAA,EACA,cAAA,KAGF,MCKA,MACA,MAFA,MDAE,cAAA,EAGF,GACE,YAAA,IAKF,GACE,cAAA,MACA,YAAA,EAMF,WACE,OAAA,EAAA,EAAA,KAQF,ECNA,ODQE,YAAA,OAQF,OAAA,ME4EM,UAAA,OFrEN,MAAA,KACE,QAAA,KACA,iBAAA,QASF,ICpBA,IDsBE,SAAA,SEwDI,UAAA,MFtDJ,YAAA,EACA,eAAA,SAGF,IAAM,OAAA,OACN,IAAM,IAAA,MAKN,EACE,MAAA,QACA,gBAAA,UAEA,QACE,MAAA,QAWF,2BAAA,iCAEE,MAAA,QACA,gBAAA,KCxBJ,KACA,ID8BA,IC7BA,KDiCE,YAAA,yBEcI,UAAA,IFZJ,UAAA,IACA,aAAA,cAOF,IACE,QAAA,MACA,WAAA,EACA,cAAA,KACA,SAAA,KEAI,UAAA,OFKJ,SELI,UAAA,QFOF,MAAA,QACA,WAAA,OAIJ,KEZM,UAAA,OFcJ,MAAA,QACA,UAAA,WAGA,OACE,MAAA,QAIJ,IACE,QAAA,MAAA,MExBI,UAAA,OF0BJ,MAAA,KACA,iBAAA,QG7SE,cAAA,MHgTF,QACE,QAAA,EE/BE,UAAA,IFiCF,YAAA,IASJ,OACE,OAAA,EAAA,EAAA,KAMF,ICjDA,IDmDE,eAAA,OAQF,MACE,aAAA,OACA,gBAAA,SAGF,QACE,YAAA,MACA,eAAA,MACA,MAAA,QACA,WAAA,KAOF,GAEE,WAAA,QACA,WAAA,qBCxDF,MAGA,GAFA,MAGA,GDuDA,MCzDA,GD+DE,aAAA,QACA,aAAA,MACA,aAAA,EAQF,MACE,QAAA,aAMF,OAEE,cAAA,EAQF,iCACE,QAAA,ECtEF,OD2EA,MCzEA,SADA,OAEA,SD6EE,OAAA,EACA,YAAA,QE9HI,UAAA,QFgIJLine truncated
File diff suppressed because it is too large. Load diff
@@ -0,0 +1 @@
{"version":3,"sources":["../../scss/mixins/_banner.scss","../../scss/_containers.scss","../../scss/mixins/_container.scss","bootstrap-grid.css","../../scss/mixins/_breakpoints.scss","../../scss/_variables.scss","../../scss/_grid.scss","../../scss/mixins/_grid.scss","../../scss/mixins/_utilities.scss","../../scss/utilities/_api.scss"],"names":[],"mappings":"AACE;;;;EAAA;ACKA;;;;;;;ECHA,qBAAA;EACA,gBAAA;EACA,WAAA;EACA,6CAAA;EACA,4CAAA;EACA,kBAAA;EACA,iBAAA;ACUF;;AC4CI;EH5CE;IACE,gBIkee;EF9drB;AACF;ACsCI;EH5CE;IACE,gBIkee;EFzdrB;AACF;ACiCI;EH5CE;IACE,gBIkee;EFpdrB;AACF;AC4BI;EH5CE;IACE,iBIkee;EF/crB;AACF;ACuBI;EH5CE;IACE,iBIkee;EF1crB;AACF;AGzCA;EAEI,qBAAA;EAAA,yBAAA;EAAA,yBAAA;EAAA,yBAAA;EAAA,0BAAA;EAAA,2BAAA;AH+CJ;;AG1CE;ECNA,qBAAA;EACA,gBAAA;EACA,aAAA;EACA,eAAA;EAEA,yCAAA;EACA,6CAAA;EACA,4CAAA;AJmDF;AGjDI;ECGF,sBAAA;EAIA,cAAA;EACA,WAAA;EACA,eAAA;EACA,6CAAA;EACA,4CAAA;EACA,8BAAA;AJ8CF;;AICM;EACE,YAAA;AJER;;AICM;EApCJ,cAAA;EACA,WAAA;AJuCF;;AIzBE;EACE,cAAA;EACA,WAAA;AJ4BJ;;AI9BE;EACE,cAAA;EACA,UAAA;AJiCJ;;AInCE;EACE,cAAA;EACA,mBAAA;AJsCJ;;AIxCE;EACE,cAAA;EACA,UAAA;AJ2CJ;;AI7CE;EACE,cAAA;EACA,UAAA;AJgDJ;;AIlDE;EACE,cAAA;EACA,mBAAA;AJqDJ;;AItBM;EAhDJ,cAAA;EACA,WAAA;AJ0EF;;AIrBU;EAhEN,cAAA;EACA,kBAAA;AJyFJ;;AI1BU;EAhEN,cAAA;EACA,mBAAA;AJ8FJ;;AI/BU;EAhEN,cAAA;EACA,UAAA;AJmGJ;;AIpCU;EAhEN,cAAA;EACA,mBAAA;AJwGJ;;AIzCU;EAhEN,cAAA;EACA,mBAAA;AJ6GJ;;AI9CU;EAhEN,cAAA;EACA,UAAA;AJkHJ;;AInDU;EAhEN,cAAA;EACA,mBAAA;AJuHJ;;AIxDU;EAhEN,cAAA;EACA,mBAAA;AJ4HJ;;AI7DU;EAhEN,cAAA;EACA,UAAA;AJiIJ;;AIlEU;EAhEN,cAAA;EACA,mBAAA;AJsIJ;;AIvEU;EAhEN,cAAA;EACA,mBAAA;AJ2IJ;;AI5EU;EAhEN,cAAA;EACA,WAAA;AJgJJ;;AIzEY;EAxDV,wBAAA;AJqIF;;AI7EY;EAxDV,yBAAA;AJyIF;;AIjFY;EAxDV,gBAAA;AJ6IF;;AIrFY;EAxDV,yBAAA;AJiJF;;AIzFY;EAxDV,yBAAA;AJqJF;;AI7FY;EAxDV,gBAAA;AJyJF;;AIjGY;EAxDV,yBAAA;AJ6JF;;AIrGY;EAxDV,yBAAA;AJiKF;;AIzGY;EAxDV,gBAAA;AJqKF;;AI7GY;EAxDV,yBAAA;AJyKF;;AIjHY;EAxDV,yBAAA;AJ6KF;;AI1GQ;;EAEE,gBAAA;AJ6GV;;AI1GQ;;EAEE,gBAAA;AJ6GV;;AIpHQ;;EAEE,sBAAA;AJuHV;;AIpHQ;;EAEE,sBAAA;AJuHV;;AI9HQ;;EAEE,qBAAA;AJiIV;;AI9HQ;;EAEE,qBAAA;AJiIV;;AIxIQ;;EAEE,mBAAA;AJ2IV;;AIxIQ;;EAEE,mBAAA;AJ2IV;;AIlJQ;;EAEE,qBAAA;AJqJV;;AIlJQ;;EAEE,qBAAA;AJqJV;;AI5JQ;;EAEE,mBAAA;AJ+JV;;AI5JQ;;EAEE,mBAAA;AJ+JV;;ACzNI;EGUE;IACE,YAAA;EJmNN;EIhNI;IApCJ,cAAA;IACA,WAAA;EJuPA;EIzOA;IACE,cAAA;IACA,WAAA;EJ2OF;EI7OA;IACE,cAAA;IACA,UAAA;EJ+OF;EIjPA;IACE,cAAA;IACA,mBAAA;EJmPF;EIrPA;IACE,cAAA;IACA,UAAA;EJuPF;EIzPA;IACE,cAAA;IACA,UAAA;EJ2PF;EI7PA;IACE,cAAA;IACA,mBAAA;EJ+PF;EIhOI;IAhDJ,cAAA;IACA,WAAA;EJmRA;EI9NQ;IAhEN,cAAA;IACA,kBAAA;EJiSF;EIlOQ;IAhEN,cAAA;IACA,mBAAA;EJqSF;EItOQ;IAhEN,cAAA;IACA,UAAA;EJySF;EI1OQ;IAhEN,cAAA;IACA,mBAAA;EJ6SF;EI9OQ;IAhEN,cAAA;IACA,mBAAA;EJiTF;EIlPQ;IAhEN,cAAA;IACA,UAAA;EJqTF;EItPQ;IAhEN,cAAA;IACA,mBAAA;EJyTF;EI1PQ;IAhEN,cAAA;IACA,mBAAA;EJ6TF;EI9PQ;IAhEN,cAAA;IACA,UAAA;EJiUF;EIlQQ;IAhEN,cAAA;IACA,mBAAA;EJqUF;EItQQ;IAhEN,cAAA;IACA,mBAAA;EJyUF;EI1QQ;IAhEN,cAAA;IACA,WAAA;EJ6UF;EItQU;IAxDV,cAAA;EJiUA;EIzQU;IAxDV,wBAAA;EJoUA;EI5QU;IAxDV,yBAAA;EJuUA;EI/QU;IAxDV,gBAAA;EJ0UA;EIlRU;IAxDV,yBAAA;EJ6UA;EIrRU;IAxDV,yBAAA;EJgVA;EIxRU;IAxDV,gBAAA;EJmVA;EI3RU;IAxDV,yBAAA;EJsVA;EI9RU;IAxDV,yBAAA;EJyVA;EIjSU;IAxDV,gBAAA;EJ4VA;EIpSU;IAxDV,yBAAA;EJ+VA;EIvSU;IAxDV,yBAAA;EJkWA;EI/RM;;IAEE,gBAAA;EJiSR;EI9RM;;IAEE,gBAAA;EJgSR;EIvSM;;IAEE,sBAAA;EJySR;EItSM;;IAEE,sBAAA;EJwSR;EI/SM;;IAEE,qBAAA;EJiTR;EI9SM;;IAEE,qBAAA;EJgTR;EIvTM;;IAEE,mBAAA;EJyTR;EItTM;;IAEE,mBAAA;EJwTR;EI/TM;;IAEE,qBAAA;EJiUR;EI9TM;;IAEE,qBAAA;EJgUR;EIvUM;;IAEE,mBAAA;EJyUR;EItUM;;IAEE,mBAAA;EJwUR;AACF;ACnYI;EGUE;IACE,YAAA;EJ4XN;EIzXI;IApCJ,cAAA;IACA,WAAA;EJgaA;EIlZA;IACE,cAAA;IACA,WAAA;EJoZF;EItZA;IACE,cAAA;IACA,UAAA;EJwZF;EI1ZA;IACE,cAAA;IACA,mBAAA;EJ4ZF;EI9ZA;IACE,cAAA;IACA,UAAA;EJgaF;EIlaA;IACE,cAAA;IACA,UAAA;EJoaF;EItaA;IACE,cAAA;IACA,mBAAA;EJwaF;EIzYI;IAhDJ,cAAA;IACA,WAAA;EJ4bA;EIvYQ;IAhEN,cAAA;IACA,kBAAA;EJ0cF;EI3YQ;IAhEN,cAAA;IACA,mBAAA;EJ8cF;EI/YQ;IAhEN,cAAA;IACA,UAAA;EJkdF;EInZQ;IAhEN,cAAA;IACA,mBAAA;EJsdF;EIvZQ;IAhEN,cAAA;IACA,mBAAA;EJ0dF;EI3ZQ;IAhEN,cAAA;IACA,UAAA;EJ8dF;EI/ZQ;IAhEN,cAAA;IACA,mBAAA;EJkeF;EInaQ;IAhEN,cAAA;IACA,mBAAA;EJseF;EIvaQ;IAhEN,cAAA;IACA,UAAA;EJ0eF;EI3aQ;IAhEN,cAAA;IACA,mBAAA;EJ8eF;EI/aQ;IAhEN,cAAA;IACA,mBAAA;EJkfF;EInbQ;IAhEN,cAAA;IACA,WAAA;EJsfF;EI/aU;IAxDV,cAAA;EJ0eA;EIlbU;IAxDV,wBAAA;EJ6eA;EIrbU;IAxDV,yBAAA;EJgfA;EIxbU;IAxDV,gBAAA;EJmfA;EI3bU;IAxDV,yBAAA;EJsfA;EI9bU;IAxDV,yBAAA;EJyfA;EIjcU;IAxDV,gBAAA;EJ4fA;EIpcU;IAxDV,yBAAA;EJ+fA;EIvcU;IAxDV,yBAAA;EJkgBA;EI1cU;IAxDV,gBAAA;EJqgBA;EI7cU;IAxDV,yBAAA;EJwgBA;EIhdU;IAxDV,yBAAA;EJ2gBA;EIxcM;;IAEE,gBAAA;EJ0cR;EIvcM;;IAEE,gBAAA;EJycR;EIhdM;;IAEE,sBAAA;EJkdR;EI/cM;;IAEE,sBAAA;EJidR;EIxdM;;IAEE,qBAAA;EJ0dR;EIvdM;;IAEE,qBAAA;EJydR;EIheM;;IAEE,mBAAA;EJkeR;EI/dM;;IAEE,mBAAA;EJieR;EIxeM;;IAEE,qBAAA;EJ0eR;EIveM;;IAEE,qBAAA;EJyeR;EIhfM;;IAEE,mBAAA;EJkfR;EI/eM;;IAEE,mBAAA;EJifR;AACF;AC5iBI;EGUE;IACE,YAAA;EJqiBN;EIliBI;IApCJ,cAAA;IACA,WAAA;EJykBA;EI3jBA;IACE,cAAA;IACA,WAAA;EJ6jBF;EI/jBA;IACE,cAAA;IACA,UAAA;EJikBF;EInkBA;IACE,cAAA;IACA,mBAAA;EJqkBF;EIvkBA;IACE,cAAA;IACA,UAAA;EJykBF;EI3kBA;IACE,cAAA;IACA,UAAA;EJ6kBF;EI/kBA;IACE,cAAA;IACA,mBAAA;EJilBF;EIljBILine truncated
@@ -0,0 +1,6 @@
/*!
* Bootstrap Grid v5.3.3 (https://getbootstrap.com/)
* Copyright 2011-2024 The Bootstrap Authors
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
*/.container,.container-fluid,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{--bs-gutter-x:1.5rem;--bs-gutter-y:0;width:100%;padding-right:calc(var(--bs-gutter-x) * .5);padding-left:calc(var(--bs-gutter-x) * .5);margin-right:auto;margin-left:auto}@media (min-width:576px){.container,.container-sm{max-width:540px}}@media (min-width:768px){.container,.container-md,.container-sm{max-width:720px}}@media (min-width:992px){.container,.container-lg,.container-md,.container-sm{max-width:960px}}@media (min-width:1200px){.container,.container-lg,.container-md,.container-sm,.container-xl{max-width:1140px}}@media (min-width:1400px){.container,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{max-width:1320px}}:root{--bs-breakpoint-xs:0;--bs-breakpoint-sm:576px;--bs-breakpoint-md:768px;--bs-breakpoint-lg:992px;--bs-breakpoint-xl:1200px;--bs-breakpoint-xxl:1400px}.row{--bs-gutter-x:1.5rem;--bs-gutter-y:0;display:flex;flex-wrap:wrap;margin-top:calc(-1 * var(--bs-gutter-y));margin-right:calc(-.5 * var(--bs-gutter-x));margin-left:calc(-.5 * var(--bs-gutter-x))}.row>*{box-sizing:border-box;flex-shrink:0;width:100%;max-width:100%;padding-right:calc(var(--bs-gutter-x) * .5);padding-left:calc(var(--bs-gutter-x) * .5);margin-top:var(--bs-gutter-y)}.col{flex:1 0 0%}.row-cols-auto>*{flex:0 0 auto;width:auto}.row-cols-1>*{flex:0 0 auto;width:100%}.row-cols-2>*{flex:0 0 auto;width:50%}.row-cols-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-4>*{flex:0 0 auto;width:25%}.row-cols-5>*{flex:0 0 auto;width:20%}.row-cols-6>*{flex:0 0 auto;width:16.66666667%}.col-auto{flex:0 0 auto;width:auto}.col-1{flex:0 0 auto;width:8.33333333%}.col-2{flex:0 0 auto;width:16.66666667%}.col-3{flex:0 0 auto;width:25%}.col-4{flex:0 0 auto;width:33.33333333%}.col-5{flex:0 0 auto;width:41.66666667%}.col-6{flex:0 0 auto;width:50%}.col-7{flex:0 0 auto;width:58.33333333%}.col-8{flex:0 0 auto;width:66.66666667%}.col-9{flex:0 0 auto;width:75%}.col-10{flex:0 0 auto;width:83.33333333%}.col-11{flex:0 0 auto;width:91.66666667%}.col-12{flex:0 0 auto;width:100%}.offset-1{margin-left:8.33333333%}.offset-2{margin-left:16.66666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.33333333%}.offset-5{margin-left:41.66666667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.33333333%}.offset-8{margin-left:66.66666667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.33333333%}.offset-11{margin-left:91.66666667%}.g-0,.gx-0{--bs-gutter-x:0}.g-0,.gy-0{--bs-gutter-y:0}.g-1,.gx-1{--bs-gutter-x:0.25rem}.g-1,.gy-1{--bs-gutter-y:0.25rem}.g-2,.gx-2{--bs-gutter-x:0.5rem}.g-2,.gy-2{--bs-gutter-y:0.5rem}.g-3,.gx-3{--bs-gutter-x:1rem}.g-3,.gy-3{--bs-gutter-y:1rem}.g-4,.gx-4{--bs-gutter-x:1.5rem}.g-4,.gy-4{--bs-gutter-y:1.5rem}.g-5,.gx-5{--bs-gutter-x:3rem}.g-5,.gy-5{--bs-gutter-y:3rem}@media (min-width:576px){.col-sm{flex:1 0 0%}.row-cols-sm-auto>*{flex:0 0 auto;width:auto}.row-cols-sm-1>*{flex:0 0 auto;width:100%}.row-cols-sm-2>*{flex:0 0 auto;width:50%}.row-cols-sm-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-sm-4>*{flex:0 0 auto;width:25%}.row-cols-sm-5>*{flex:0 0 auto;width:20%}.row-cols-sm-6>*{flex:0 0 auto;width:16.66666667%}.col-sm-auto{flex:0 0 auto;width:auto}.col-sm-1{flex:0 0 auto;width:8.33333333%}.col-sm-2{flex:0 0 auto;width:16.66666667%}.col-sm-3{flex:0 0 auto;width:25%}.col-sm-4{flex:0 0 auto;width:33.33333333%}.col-sm-5{flex:0 0 auto;width:41.66666667%}.col-sm-6{flex:0 0 auto;width:50%}.col-sm-7{flex:0 0 auto;width:58.33333333%}.col-sm-8{flex:0 0 auto;width:66.66666667%}.col-sm-9{flex:0 0 auto;width:75%}.col-sm-10{flex:0 0 auto;width:83.33333333%}.col-sm-11{flex:0 0 auto;width:91.66666667%}.col-sm-12{flex:0 0 auto;width:100%}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.33333333%}.offset-sm-2{margin-left:16.66666667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.33333333%}.offset-sm-5{margin-left:41.66666667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.33333333%}.offset-sm-8{margin-left:66.66666667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.33333333%}.offset-sm-11{margin-left:91.66666667%}.g-sm-0,.gx-sm-0{--bs-gutter-x:0}.g-sm-0,.gy-sm-0{--bs-gutter-y:0}.g-sm-1,.gx-sm-1{--bs-gutter-x:0.25rem}.g-sm-1,.gy-sm-1{--bs-gutter-y:0.25rem}.g-sm-2,.gx-sm-2{--bs-gutter-x:0.5rem}.g-sm-2,.gy-sm-2{--bs-gutter-y:0.5rem}.g-sm-3,.gx-sm-3{--bs-gutter-x:1rem}.g-sm-3,.gy-sm-3{--bs-gutter-y:1rem}.g-sm-4,.gx-sm-4{--bs-gutter-x:1.5rem}.g-sm-4,.gy-sm-4{--bs-gutter-y:1.5rem}.g-sm-5,.gx-sm-5{--bs-gutter-x:3rem}.g-sm-5,.gy-sm-5{--bs-gutter-y:3rem}}@media (min-width:768px){.col-md{flex:1 0 0%}.row-cols-md-auto>*{flex:0 0 auto;width:auto}.row-cols-md-1>*{flex:0 0 auto;width:100%}.row-cols-md-2>*{flex:0 0 auto;width:50%}.row-cols-md-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-md-4>*{flex:0 0 auto;width:25%}.row-cols-md-5>*{flex:0 0 auto;width:20%}.row-cols-md-6>*{flex:0 0 auto;width:16.66666667%}.col-md-auto{flex:0 0 auto;width:auto}.col-md-1{flex:0 0 auto;width:8.33333333%}.col-md-2{flex:0 0 autLine truncated
/*# sourceMappingURL=bootstrap-grid.min.css.map */
Loaded 100 of 141 files, more files were not shown because too many files have changed in this diff. Show more