@@ -0,0 +1,25 @@ | |||
**/.classpath | |||
**/.dockerignore | |||
**/.env | |||
**/.git | |||
**/.gitignore | |||
**/.project | |||
**/.settings | |||
**/.toolstarget | |||
**/.vs | |||
**/.vscode | |||
**/*.*proj.user | |||
**/*.dbmdl | |||
**/*.jfm | |||
**/azds.yaml | |||
**/bin | |||
**/charts | |||
**/docker-compose* | |||
**/Dockerfile* | |||
**/node_modules | |||
**/npm-debug.log | |||
**/obj | |||
**/secrets.dev.yaml | |||
**/values.dev.yaml | |||
LICENSE | |||
README.md |
@@ -0,0 +1,63 @@ | |||
############################################################################### | |||
# Set default behavior to automatically normalize line endings. | |||
############################################################################### | |||
* text=auto | |||
############################################################################### | |||
# Set default behavior for command prompt diff. | |||
# | |||
# This is need for earlier builds of msysgit that does not have it on by | |||
# default for csharp files. | |||
# Note: This is only used by command line | |||
############################################################################### | |||
#*.cs diff=csharp | |||
############################################################################### | |||
# Set the merge driver for project and solution files | |||
# | |||
# Merging from the command prompt will add diff markers to the files if there | |||
# are conflicts (Merging from VS is not affected by the settings below, in VS | |||
# the diff markers are never inserted). Diff markers may cause the following | |||
# file extensions to fail to load in VS. An alternative would be to treat | |||
# these files as binary and thus will always conflict and require user | |||
# intervention with every merge. To do so, just uncomment the entries below | |||
############################################################################### | |||
#*.sln merge=binary | |||
#*.csproj merge=binary | |||
#*.vbproj merge=binary | |||
#*.vcxproj merge=binary | |||
#*.vcproj merge=binary | |||
#*.dbproj merge=binary | |||
#*.fsproj merge=binary | |||
#*.lsproj merge=binary | |||
#*.wixproj merge=binary | |||
#*.modelproj merge=binary | |||
#*.sqlproj merge=binary | |||
#*.wwaproj merge=binary | |||
############################################################################### | |||
# behavior for image files | |||
# | |||
# image files are treated as binary by default. | |||
############################################################################### | |||
#*.jpg binary | |||
#*.png binary | |||
#*.gif binary | |||
############################################################################### | |||
# diff behavior for common document formats | |||
# | |||
# Convert binary document formats to text before diffing them. This feature | |||
# is only available from the command line. Turn it on by uncommenting the | |||
# entries below. | |||
############################################################################### | |||
#*.doc diff=astextplain | |||
#*.DOC diff=astextplain | |||
#*.docx diff=astextplain | |||
#*.DOCX diff=astextplain | |||
#*.dot diff=astextplain | |||
#*.DOT diff=astextplain | |||
#*.pdf diff=astextplain | |||
#*.PDF diff=astextplain | |||
#*.rtf diff=astextplain | |||
#*.RTF diff=astextplain |
@@ -0,0 +1,364 @@ | |||
## Ignore Visual Studio temporary files, build results, and | |||
## files generated by popular Visual Studio add-ons. | |||
## | |||
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore | |||
# User-specific files | |||
*.rsuser | |||
*.suo | |||
*.user | |||
*.userosscache | |||
*.sln.docstates | |||
*.crt | |||
# User-specific files (MonoDevelop/Xamarin Studio) | |||
*.userprefs | |||
# Mono auto generated files | |||
mono_crash.* | |||
# Build results | |||
[Dd]ebug/ | |||
[Dd]ebugPublic/ | |||
[Rr]elease/ | |||
[Rr]eleases/ | |||
x64/ | |||
x86/ | |||
[Ww][Ii][Nn]32/ | |||
[Aa][Rr][Mm]/ | |||
[Aa][Rr][Mm]64/ | |||
bld/ | |||
[Bb]in/ | |||
[Oo]bj/ | |||
[Oo]ut/ | |||
[Ll]og/ | |||
[Ll]ogs/ | |||
# Visual Studio 2015/2017 cache/options directory | |||
.vs/ | |||
# Uncomment if you have tasks that create the project's static files in wwwroot | |||
#wwwroot/ | |||
# Visual Studio 2017 auto generated files | |||
Generated\ Files/ | |||
# MSTest test Results | |||
[Tt]est[Rr]esult*/ | |||
[Bb]uild[Ll]og.* | |||
# NUnit | |||
*.VisualState.xml | |||
TestResult.xml | |||
nunit-*.xml | |||
# Build Results of an ATL Project | |||
[Dd]ebugPS/ | |||
[Rr]eleasePS/ | |||
dlldata.c | |||
# Benchmark Results | |||
BenchmarkDotNet.Artifacts/ | |||
# .NET Core | |||
project.lock.json | |||
project.fragment.lock.json | |||
artifacts/ | |||
# ASP.NET Scaffolding | |||
ScaffoldingReadMe.txt | |||
# StyleCop | |||
StyleCopReport.xml | |||
# Files built by Visual Studio | |||
*_i.c | |||
*_p.c | |||
*_h.h | |||
*.ilk | |||
*.meta | |||
*.obj | |||
*.iobj | |||
*.pch | |||
*.pdb | |||
*.ipdb | |||
*.pgc | |||
*.pgd | |||
*.rsp | |||
*.sbr | |||
*.tlb | |||
*.tli | |||
*.tlh | |||
*.tmp | |||
*.tmp_proj | |||
*_wpftmp.csproj | |||
*.log | |||
*.vspscc | |||
*.vssscc | |||
.builds | |||
*.pidb | |||
*.svclog | |||
*.scc | |||
# Chutzpah Test files | |||
_Chutzpah* | |||
# Visual C++ cache files | |||
ipch/ | |||
*.aps | |||
*.ncb | |||
*.opendb | |||
*.opensdf | |||
*.sdf | |||
*.cachefile | |||
*.VC.db | |||
*.VC.VC.opendb | |||
# Visual Studio profiler | |||
*.psess | |||
*.vsp | |||
*.vspx | |||
*.sap | |||
# Visual Studio Trace Files | |||
*.e2e | |||
# TFS 2012 Local Workspace | |||
$tf/ | |||
# Guidance Automation Toolkit | |||
*.gpState | |||
# ReSharper is a .NET coding add-in | |||
_ReSharper*/ | |||
*.[Rr]e[Ss]harper | |||
*.DotSettings.user | |||
# TeamCity is a build add-in | |||
_TeamCity* | |||
# DotCover is a Code Coverage Tool | |||
*.dotCover | |||
# AxoCover is a Code Coverage Tool | |||
.axoCover/* | |||
!.axoCover/settings.json | |||
# Coverlet is a free, cross platform Code Coverage Tool | |||
coverage*.json | |||
coverage*.xml | |||
coverage*.info | |||
# Visual Studio code coverage results | |||
*.coverage | |||
*.coveragexml | |||
# NCrunch | |||
_NCrunch_* | |||
.*crunch*.local.xml | |||
nCrunchTemp_* | |||
# MightyMoose | |||
*.mm.* | |||
AutoTest.Net/ | |||
# Web workbench (sass) | |||
.sass-cache/ | |||
# Installshield output folder | |||
[Ee]xpress/ | |||
# DocProject is a documentation generator add-in | |||
DocProject/buildhelp/ | |||
DocProject/Help/*.HxT | |||
DocProject/Help/*.HxC | |||
DocProject/Help/*.hhc | |||
DocProject/Help/*.hhk | |||
DocProject/Help/*.hhp | |||
DocProject/Help/Html2 | |||
DocProject/Help/html | |||
# Click-Once directory | |||
publish/ | |||
# Publish Web Output | |||
*.[Pp]ublish.xml | |||
*.azurePubxml | |||
# Note: Comment the next line if you want to checkin your web deploy settings, | |||
# but database connection strings (with potential passwords) will be unencrypted | |||
*.pubxml | |||
*.publishproj | |||
# Microsoft Azure Web App publish settings. Comment the next line if you want to | |||
# checkin your Azure Web App publish settings, but sensitive information contained | |||
# in these scripts will be unencrypted | |||
PublishScripts/ | |||
# NuGet Packages | |||
*.nupkg | |||
# NuGet Symbol Packages | |||
*.snupkg | |||
# The packages folder can be ignored because of Package Restore | |||
**/[Pp]ackages/* | |||
# except build/, which is used as an MSBuild target. | |||
!**/[Pp]ackages/build/ | |||
# Uncomment if necessary however generally it will be regenerated when needed | |||
#!**/[Pp]ackages/repositories.config | |||
# NuGet v3's project.json files produces more ignorable files | |||
*.nuget.props | |||
*.nuget.targets | |||
# Microsoft Azure Build Output | |||
csx/ | |||
*.build.csdef | |||
# Microsoft Azure Emulator | |||
ecf/ | |||
rcf/ | |||
# Windows Store app package directories and files | |||
AppPackages/ | |||
BundleArtifacts/ | |||
Package.StoreAssociation.xml | |||
_pkginfo.txt | |||
*.appx | |||
*.appxbundle | |||
*.appxupload | |||
# Visual Studio cache files | |||
# files ending in .cache can be ignored | |||
*.[Cc]ache | |||
# but keep track of directories ending in .cache | |||
!?*.[Cc]ache/ | |||
# Others | |||
ClientBin/ | |||
~$* | |||
*~ | |||
*.dbmdl | |||
*.dbproj.schemaview | |||
*.jfm | |||
*.pfx | |||
*.publishsettings | |||
orleans.codegen.cs | |||
# Including strong name files can present a security risk | |||
# (https://github.com/github/gitignore/pull/2483#issue-259490424) | |||
#*.snk | |||
# Since there are multiple workflows, uncomment next line to ignore bower_components | |||
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) | |||
#bower_components/ | |||
# RIA/Silverlight projects | |||
Generated_Code/ | |||
# Backup & report files from converting an old project file | |||
# to a newer Visual Studio version. Backup files are not needed, | |||
# because we have git ;-) | |||
_UpgradeReport_Files/ | |||
Backup*/ | |||
UpgradeLog*.XML | |||
UpgradeLog*.htm | |||
ServiceFabricBackup/ | |||
*.rptproj.bak | |||
# SQL Server files | |||
*.mdf | |||
*.ldf | |||
*.ndf | |||
# Business Intelligence projects | |||
*.rdl.data | |||
*.bim.layout | |||
*.bim_*.settings | |||
*.rptproj.rsuser | |||
*- [Bb]ackup.rdl | |||
*- [Bb]ackup ([0-9]).rdl | |||
*- [Bb]ackup ([0-9][0-9]).rdl | |||
# Microsoft Fakes | |||
FakesAssemblies/ | |||
# GhostDoc plugin setting file | |||
*.GhostDoc.xml | |||
# Node.js Tools for Visual Studio | |||
.ntvs_analysis.dat | |||
node_modules/ | |||
# Visual Studio 6 build log | |||
*.plg | |||
# Visual Studio 6 workspace options file | |||
*.opt | |||
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) | |||
*.vbw | |||
# Visual Studio LightSwitch build output | |||
**/*.HTMLClient/GeneratedArtifacts | |||
**/*.DesktopClient/GeneratedArtifacts | |||
**/*.DesktopClient/ModelManifest.xml | |||
**/*.Server/GeneratedArtifacts | |||
**/*.Server/ModelManifest.xml | |||
_Pvt_Extensions | |||
# Paket dependency manager | |||
.paket/paket.exe | |||
paket-files/ | |||
# FAKE - F# Make | |||
.fake/ | |||
# CodeRush personal settings | |||
.cr/personal | |||
# Python Tools for Visual Studio (PTVS) | |||
__pycache__/ | |||
*.pyc | |||
# Cake - Uncomment if you are using it | |||
# tools/** | |||
# !tools/packages.config | |||
# Tabs Studio | |||
*.tss | |||
# Telerik's JustMock configuration file | |||
*.jmconfig | |||
# BizTalk build output | |||
*.btp.cs | |||
*.btm.cs | |||
*.odx.cs | |||
*.xsd.cs | |||
# OpenCover UI analysis results | |||
OpenCover/ | |||
# Azure Stream Analytics local run output | |||
ASALocalRun/ | |||
# MSBuild Binary and Structured Log | |||
*.binlog | |||
# NVidia Nsight GPU debugger configuration file | |||
*.nvuser | |||
# MFractors (Xamarin productivity tool) working folder | |||
.mfractor/ | |||
# Local History for Visual Studio | |||
.localhistory/ | |||
# BeatPulse healthcheck temp database | |||
healthchecksdb | |||
# Backup folder for Package Reference Convert tool in Visual Studio 2017 | |||
MigrationBackup/ | |||
# Ionide (cross platform F# VS Code tools) working folder | |||
.ionide/ | |||
# Fody - auto-generated XML schema | |||
FodyWeavers.xsd | |||
Panel/Properties/launchSettings.json |
@@ -0,0 +1,376 @@ | |||
# Default ignored files | |||
/shelf/ | |||
/workspace.xml | |||
# Rider ignored files | |||
/modules.xml | |||
/projectSettingsUpdater.xml | |||
/.idea.DadYar.iml | |||
/contentModel.xml | |||
# Editor-based HTTP Client requests | |||
/httpRequests/ | |||
# Datasource local storage ignored files | |||
/dataSources/ | |||
/dataSources.local.xml | |||
## Ignore Visual Studio temporary files, build results, and | |||
## files generated by popular Visual Studio add-ons. | |||
## | |||
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore | |||
# User-specific files | |||
*.rsuser | |||
*.suo | |||
*.user | |||
*.userosscache | |||
*.sln.docstates | |||
# User-specific files (MonoDevelop/Xamarin Studio) | |||
*.userprefs | |||
# Mono auto generated files | |||
mono_crash.* | |||
# Build results | |||
[Dd]ebug/ | |||
[Dd]ebugPublic/ | |||
[Rr]elease/ | |||
[Rr]eleases/ | |||
x64/ | |||
x86/ | |||
[Ww][Ii][Nn]32/ | |||
[Aa][Rr][Mm]/ | |||
[Aa][Rr][Mm]64/ | |||
bld/ | |||
[Bb]in/ | |||
[Oo]bj/ | |||
[Oo]ut/ | |||
[Ll]og/ | |||
[Ll]ogs/ | |||
# Visual Studio 2015/2017 cache/options directory | |||
.vs/ | |||
# Uncomment if you have tasks that create the project's static files in wwwroot | |||
#wwwroot/ | |||
# Visual Studio 2017 auto generated files | |||
Generated\ Files/ | |||
# MSTest test Results | |||
[Tt]est[Rr]esult*/ | |||
[Bb]uild[Ll]og.* | |||
# NUnit | |||
*.VisualState.xml | |||
TestResult.xml | |||
nunit-*.xml | |||
# Build Results of an ATL Project | |||
[Dd]ebugPS/ | |||
[Rr]eleasePS/ | |||
dlldata.c | |||
# Benchmark Results | |||
BenchmarkDotNet.Artifacts/ | |||
# .NET Core | |||
project.lock.json | |||
project.fragment.lock.json | |||
artifacts/ | |||
# ASP.NET Scaffolding | |||
ScaffoldingReadMe.txt | |||
# StyleCop | |||
StyleCopReport.xml | |||
# Files built by Visual Studio | |||
*_i.c | |||
*_p.c | |||
*_h.h | |||
*.ilk | |||
*.meta | |||
*.obj | |||
*.iobj | |||
*.pch | |||
*.pdb | |||
*.ipdb | |||
*.pgc | |||
*.pgd | |||
*.rsp | |||
*.sbr | |||
*.tlb | |||
*.tli | |||
*.tlh | |||
*.tmp | |||
*.tmp_proj | |||
*_wpftmp.csproj | |||
*.log | |||
*.vspscc | |||
*.vssscc | |||
.builds | |||
*.pidb | |||
*.svclog | |||
*.scc | |||
# Chutzpah Test files | |||
_Chutzpah* | |||
# Visual C++ cache files | |||
ipch/ | |||
*.aps | |||
*.ncb | |||
*.opendb | |||
*.opensdf | |||
*.sdf | |||
*.cachefile | |||
*.VC.db | |||
*.VC.VC.opendb | |||
# Visual Studio profiler | |||
*.psess | |||
*.vsp | |||
*.vspx | |||
*.sap | |||
# Visual Studio Trace Files | |||
*.e2e | |||
# TFS 2012 Local Workspace | |||
$tf/ | |||
# Guidance Automation Toolkit | |||
*.gpState | |||
# ReSharper is a .NET coding add-in | |||
_ReSharper*/ | |||
*.[Rr]e[Ss]harper | |||
*.DotSettings.user | |||
# TeamCity is a build add-in | |||
_TeamCity* | |||
# DotCover is a Code Coverage Tool | |||
*.dotCover | |||
# AxoCover is a Code Coverage Tool | |||
.axoCover/* | |||
!.axoCover/settings.json | |||
# Coverlet is a free, cross platform Code Coverage Tool | |||
coverage*.json | |||
coverage*.xml | |||
coverage*.info | |||
# Visual Studio code coverage results | |||
*.coverage | |||
*.coveragexml | |||
# NCrunch | |||
_NCrunch_* | |||
.*crunch*.local.xml | |||
nCrunchTemp_* | |||
# MightyMoose | |||
*.mm.* | |||
AutoTest.Net/ | |||
# Web workbench (sass) | |||
.sass-cache/ | |||
# Installshield output folder | |||
[Ee]xpress/ | |||
# DocProject is a documentation generator add-in | |||
DocProject/buildhelp/ | |||
DocProject/Help/*.HxT | |||
DocProject/Help/*.HxC | |||
DocProject/Help/*.hhc | |||
DocProject/Help/*.hhk | |||
DocProject/Help/*.hhp | |||
DocProject/Help/Html2 | |||
DocProject/Help/html | |||
# Click-Once directory | |||
publish/ | |||
# Publish Web Output | |||
*.[Pp]ublish.xml | |||
*.azurePubxml | |||
# Note: Comment the next line if you want to checkin your web deploy settings, | |||
# but database connection strings (with potential passwords) will be unencrypted | |||
*.pubxml | |||
*.publishproj | |||
# Microsoft Azure Web App publish settings. Comment the next line if you want to | |||
# checkin your Azure Web App publish settings, but sensitive information contained | |||
# in these scripts will be unencrypted | |||
PublishScripts/ | |||
# NuGet Packages | |||
*.nupkg | |||
# NuGet Symbol Packages | |||
*.snupkg | |||
# The packages folder can be ignored because of Package Restore | |||
**/[Pp]ackages/* | |||
# except build/, which is used as an MSBuild target. | |||
!**/[Pp]ackages/build/ | |||
# Uncomment if necessary however generally it will be regenerated when needed | |||
#!**/[Pp]ackages/repositories.config | |||
# NuGet v3's project.json files produces more ignorable files | |||
*.nuget.props | |||
*.nuget.targets | |||
# Microsoft Azure Build Output | |||
csx/ | |||
*.build.csdef | |||
# Microsoft Azure Emulator | |||
ecf/ | |||
rcf/ | |||
# Windows Store app package directories and files | |||
AppPackages/ | |||
BundleArtifacts/ | |||
Package.StoreAssociation.xml | |||
_pkginfo.txt | |||
*.appx | |||
*.appxbundle | |||
*.appxupload | |||
# Visual Studio cache files | |||
# files ending in .cache can be ignored | |||
*.[Cc]ache | |||
# but keep track of directories ending in .cache | |||
!?*.[Cc]ache/ | |||
# Others | |||
ClientBin/ | |||
~$* | |||
*~ | |||
*.dbmdl | |||
*.dbproj.schemaview | |||
*.jfm | |||
*.pfx | |||
*.publishsettings | |||
orleans.codegen.cs | |||
# Including strong name files can present a security risk | |||
# (https://github.com/github/gitignore/pull/2483#issue-259490424) | |||
#*.snk | |||
# Since there are multiple workflows, uncomment next line to ignore bower_components | |||
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) | |||
#bower_components/ | |||
# RIA/Silverlight projects | |||
Generated_Code/ | |||
# Backup & report files from converting an old project file | |||
# to a newer Visual Studio version. Backup files are not needed, | |||
# because we have git ;-) | |||
_UpgradeReport_Files/ | |||
Backup*/ | |||
UpgradeLog*.XML | |||
UpgradeLog*.htm | |||
ServiceFabricBackup/ | |||
*.rptproj.bak | |||
# SQL Server files | |||
*.mdf | |||
*.ldf | |||
*.ndf | |||
# Business Intelligence projects | |||
*.rdl.data | |||
*.bim.layout | |||
*.bim_*.settings | |||
*.rptproj.rsuser | |||
*- [Bb]ackup.rdl | |||
*- [Bb]ackup ([0-9]).rdl | |||
*- [Bb]ackup ([0-9][0-9]).rdl | |||
# Microsoft Fakes | |||
FakesAssemblies/ | |||
# GhostDoc plugin setting file | |||
*.GhostDoc.xml | |||
# Node.js Tools for Visual Studio | |||
.ntvs_analysis.dat | |||
node_modules/ | |||
# Visual Studio 6 build log | |||
*.plg | |||
# Visual Studio 6 workspace options file | |||
*.opt | |||
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) | |||
*.vbw | |||
# Visual Studio LightSwitch build output | |||
**/*.HTMLClient/GeneratedArtifacts | |||
**/*.DesktopClient/GeneratedArtifacts | |||
**/*.DesktopClient/ModelManifest.xml | |||
**/*.Server/GeneratedArtifacts | |||
**/*.Server/ModelManifest.xml | |||
_Pvt_Extensions | |||
# Paket dependency manager | |||
.paket/paket.exe | |||
paket-files/ | |||
# FAKE - F# Make | |||
.fake/ | |||
# CodeRush personal settings | |||
.cr/personal | |||
# Python Tools for Visual Studio (PTVS) | |||
__pycache__/ | |||
*.pyc | |||
# Cake - Uncomment if you are using it | |||
# tools/** | |||
# !tools/packages.config | |||
# Tabs Studio | |||
*.tss | |||
# Telerik's JustMock configuration file | |||
*.jmconfig | |||
# BizTalk build output | |||
*.btp.cs | |||
*.btm.cs | |||
*.odx.cs | |||
*.xsd.cs | |||
# OpenCover UI analysis results | |||
OpenCover/ | |||
# Azure Stream Analytics local run output | |||
ASALocalRun/ | |||
# MSBuild Binary and Structured Log | |||
*.binlog | |||
# NVidia Nsight GPU debugger configuration file | |||
*.nvuser | |||
# MFractors (Xamarin productivity tool) working folder | |||
.mfractor/ | |||
# Local History for Visual Studio | |||
.localhistory/ | |||
# BeatPulse healthcheck temp database | |||
healthchecksdb | |||
# Backup folder for Package Reference Convert tool in Visual Studio 2017 | |||
MigrationBackup/ | |||
# Ionide (cross platform F# VS Code tools) working folder | |||
.ionide/ | |||
# Fody - auto-generated XML schema | |||
FodyWeavers.xsd |
@@ -0,0 +1,12 @@ | |||
{ | |||
"version": 1, | |||
"isRoot": true, | |||
"tools": { | |||
"dotnet-ef": { | |||
"version": "5.0.10", | |||
"commands": [ | |||
"dotnet-ef" | |||
] | |||
} | |||
} | |||
} |
@@ -0,0 +1,62 @@ | |||
<Project Sdk="Microsoft.NET.Sdk.Web"> | |||
<PropertyGroup> | |||
<TargetFramework>net8.0</TargetFramework> | |||
<UserSecretsId>cb5a9876-4532-46d5-b632-ec5d04c898d0</UserSecretsId> | |||
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS> | |||
</PropertyGroup> | |||
<ItemGroup> | |||
<Content Remove="wwwroot\UploadedFiles\48780aa1-716a-4d3f-b280-56b072dc73ee.png" /> | |||
</ItemGroup> | |||
<ItemGroup> | |||
<PackageReference Include="AutoMapper" Version="14.0.0" /> | |||
<PackageReference Include="coverlet.collector" Version="6.0.4"> | |||
<PrivateAssets>all</PrivateAssets> | |||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> | |||
</PackageReference> | |||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.8" /> | |||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.8" /> | |||
<PackageReference Include="Microsoft.EntityFrameworkCore.Proxies" Version="8.0.8" /> | |||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.8" /> | |||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.8" /> | |||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.8"> | |||
<PrivateAssets>all</PrivateAssets> | |||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> | |||
</PackageReference> | |||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" /> | |||
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.21.0" /> | |||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" /> | |||
<PackageReference Include="QRCoder" Version="1.6.0" /> | |||
<PackageReference Include="Scrutor.AspNetCore" Version="3.3.0" /> | |||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.5" /> | |||
<PackageReference Include="Swashbuckle.AspNetCore" Version="7.2.0" /> | |||
<PackageReference Include="System.Drawing.Common" Version="8.0.8" /> | |||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.1.0" /> | |||
</ItemGroup> | |||
<ItemGroup> | |||
<ProjectReference Include="..\Core\Core.csproj" /> | |||
<ProjectReference Include="..\Domain\Domain.csproj" /> | |||
<ProjectReference Include="..\Infrastructure\Infrastructure.csproj" /> | |||
<ProjectReference Include="..\Models\Models.csproj" /> | |||
<ProjectReference Include="..\Services\Services.csproj" /> | |||
</ItemGroup> | |||
<ItemGroup> | |||
<EditorConfigFiles Remove="D:\projects\DadYar\git\Api\.editorconfig" /> | |||
</ItemGroup> | |||
<ItemGroup> | |||
<None Include="D:\projects\DadYar\git\Api\.editorconfig" /> | |||
<None Include="wwwroot\UploadedFiles\48780aa1-716a-4d3f-b280-56b072dc73ee.png" /> | |||
</ItemGroup> | |||
<ItemGroup> | |||
<InternalsVisibleTo Include="Tests" /> | |||
</ItemGroup> | |||
<ItemGroup> | |||
<Folder Include="UploadedFiles\" /> | |||
</ItemGroup> | |||
</Project> |
@@ -0,0 +1,28 @@ | |||
using AutoMapper; | |||
using Domain.BaseData; | |||
using Domain.Identity; | |||
using Models.BaseData; | |||
using Models.Identity; | |||
namespace Api | |||
{ | |||
public class AutoMapperConfig : Profile | |||
{ | |||
public AutoMapperConfig() | |||
{ | |||
// Add as many of these lines as you need to map your objects | |||
CreateMap<Account, AccountModel>(); | |||
CreateMap<Account, CheckLoginResponse>(); | |||
CreateMap<Plant, PlantViewModel>(); | |||
CreateMap<Plant, SavePlantViewModel>(); | |||
CreateMap<SavePlantViewModel, Plant>(); | |||
} | |||
} | |||
} |
@@ -0,0 +1,96 @@ | |||
using Domain.Identity; | |||
using Infrastructure; | |||
using Microsoft.AspNetCore.Http; | |||
using Microsoft.AspNetCore.Mvc; | |||
using Microsoft.Extensions.DependencyInjection; | |||
using Models.Identity; | |||
using Services.Identity; | |||
using System; | |||
using System.Threading.Tasks; | |||
using Microsoft.AspNetCore.Cors; | |||
using Microsoft.AspNetCore.Hosting; | |||
namespace Api.Controllers | |||
{ | |||
[ApiController] | |||
[EnableCors("AllowAll")] | |||
public class AuthController : BaseController | |||
{ | |||
private readonly IAuthService authService; | |||
private readonly IWebHostEnvironment _hostingEnvironment; | |||
public AuthController(IServiceProvider serviceProvider) : base(serviceProvider) | |||
{ | |||
authService=serviceProvider.GetService<IAuthService>(); | |||
} | |||
[HttpPost] | |||
[ProducesResponseType(typeof(OtpRequestResult), StatusCodes.Status200OK)] | |||
public async Task<IActionResult> SignUp([FromBody] SignUpModel model) | |||
{ | |||
var existed = authService.GetByUsername(model.Email) != null; | |||
if (existed) | |||
{ | |||
return BadRequest("Email is already registered!"); | |||
} | |||
var account = new Account | |||
{ | |||
Email = model.Email, | |||
Username = model.Email, | |||
Password = model.Password, | |||
Name = model.Name | |||
}; | |||
authService.Add(account); | |||
var token = await authService.GenerateLoginToken(account); | |||
return Ok(new OtpRequestResult()); | |||
} | |||
[HttpPost] | |||
[ProducesResponseType(typeof(PlainToken), StatusCodes.Status200OK)] | |||
[ProducesResponseType(StatusCodes.Status401Unauthorized)] | |||
public async Task<IActionResult> Login([FromBody] LoginModel model) | |||
{ | |||
var account = await authService.GetByUsernameAndPassword(model.Username, model.Password); | |||
if (account == null) return Unauthorized(); | |||
var loginToken = await authService.GenerateLoginToken(account); | |||
var plainToken = await authService.GeneratePlainToken(account, loginToken.Token); | |||
if (plainToken == null) return Unauthorized(); | |||
return Ok(plainToken); | |||
} | |||
[HttpGet] | |||
[ProducesResponseType(typeof(CheckLoginResponse), statusCode: StatusCodes.Status200OK)] | |||
[ProducesResponseType(StatusCodes.Status401Unauthorized)] | |||
public IActionResult Check() | |||
{ | |||
var u = User; | |||
try | |||
{ | |||
var res = Mapper.Map<CheckLoginResponse>(CurrentAccount); | |||
try | |||
{ | |||
res.IsDeveloper = User.IsInRole(Consts.Developer); | |||
} | |||
catch | |||
{ | |||
} | |||
return Ok(res); | |||
} | |||
catch (Exception) | |||
{ | |||
return Unauthorized(); | |||
} | |||
} | |||
} | |||
} |
@@ -0,0 +1,175 @@ | |||
using System; | |||
using System.ComponentModel; | |||
using System.IO; | |||
using System.Security.Claims; | |||
using System.Threading.Tasks; | |||
using AutoMapper; | |||
using Azure.Core; | |||
using Domain.Identity; | |||
using Microsoft.AspNetCore.Cors; | |||
using Microsoft.AspNetCore.Hosting; | |||
using Microsoft.AspNetCore.Http; | |||
using Microsoft.AspNetCore.Mvc; | |||
using Microsoft.Extensions.DependencyInjection; | |||
using Microsoft.Extensions.Hosting; | |||
using Services.Identity; | |||
using SixLabors.ImageSharp; | |||
using SixLabors.ImageSharp.Formats; | |||
using SixLabors.ImageSharp.Formats.Png; | |||
using SixLabors.ImageSharp.Processing; | |||
using Image = SixLabors.ImageSharp.Image; | |||
namespace Api.Controllers | |||
{ | |||
[Route("api/[controller]/[action]")] | |||
[ApiController] | |||
[EnableCors("AllowAll")] | |||
public class BaseController : ControllerBase | |||
{ | |||
private readonly AutoMapper.IMapper _mapper; | |||
private readonly IWebHostEnvironment _environment; | |||
private readonly IAuthService authService; | |||
private readonly IServiceProvider serviceProvider; | |||
//response messages | |||
public BaseController(IServiceProvider serviceProvider) | |||
{ | |||
_mapper = serviceProvider.GetService<IMapper>(); | |||
_environment = serviceProvider.GetService<IWebHostEnvironment>(); | |||
authService = serviceProvider.GetService<IAuthService>(); | |||
} | |||
public IMapper Mapper => _mapper; | |||
private T GetClaim<T>(string type) | |||
{ | |||
var value = User.FindFirstValue(type) ?? ""; | |||
var converter = TypeDescriptor.GetConverter(typeof(T)); | |||
try | |||
{ | |||
return (T)converter.ConvertFromString(value); | |||
} | |||
catch | |||
{ | |||
return default; | |||
} | |||
} | |||
private Account _currentAccount; | |||
public Account CurrentAccount | |||
{ | |||
get | |||
{ | |||
if (_currentAccount == null) | |||
_currentAccount = authService.GetByUsername(CurrentClaim.Username); | |||
return _currentAccount; | |||
} | |||
} | |||
private CustomUser GetUser() | |||
{ | |||
return new CustomUser | |||
{ | |||
Id = GetClaim<Guid>(ITokenIssuer.Claims.Id), | |||
Name = GetClaim<string>(ITokenIssuer.Claims.Name), | |||
Username = GetClaim<string>(ITokenIssuer.Claims.Username), | |||
Roles = GetClaim<string>(ClaimTypes.Role).Split(',') | |||
}; | |||
} | |||
public CustomUser CurrentClaim => GetUser(); | |||
/// <summary> | |||
/// get images as byte array and returns maximun 1000 pixle base64 | |||
/// </summary> | |||
/// <param name="inStream"></param> | |||
/// <returns></returns> | |||
[ApiExplorerSettings(IgnoreApi = true)] | |||
public byte[] ResizeImage(byte[] inStream, int maxWidth = 1000) | |||
{ | |||
using (var ms = new MemoryStream()) | |||
{ | |||
using (Image image = Image.Load(inStream)) | |||
{ | |||
if (image.Width < maxWidth) | |||
{ | |||
image.SaveAsPng(ms); | |||
return ms.ToArray(); | |||
} | |||
var ratio = (double)(maxWidth) / image.Width; | |||
int width = image.Width; | |||
int height = (int)(image.Height * ratio); | |||
image.Mutate(x => x.Resize(width, height)); | |||
image.SaveAsPng(ms); | |||
return ms.ToArray(); | |||
} | |||
} | |||
} | |||
[ApiExplorerSettings(IgnoreApi = true)] | |||
private string GetUniqueFileName(string fileName) | |||
{ | |||
fileName = Path.GetFileName(fileName); | |||
return string.Concat(Path.GetFileNameWithoutExtension(fileName) | |||
, "_" | |||
, Guid.NewGuid().ToString().AsSpan(0, 4) | |||
, Path.GetExtension(fileName)); | |||
} | |||
/// <summary> | |||
/// Saves base64 as a PNG with max 1500px width or height | |||
/// </summary> | |||
/// <param name="fileContent">base64 image</param> | |||
/// <param name="fileName">New file=empty</param> | |||
/// <returns>File Name(all of files stores in UploadedFiles folder </returns> | |||
[ApiExplorerSettings(IgnoreApi = true)] | |||
protected string SaveImageFromBase64(string fileContent, string fileName = "") | |||
{ | |||
fileName = string.IsNullOrEmpty(fileName) ? Guid.NewGuid().ToString() + ".png" : fileName; | |||
string outputPath = $"{_environment.ContentRootPath}\\UploadedFiles\\"; | |||
if (!System.IO.Directory.Exists(outputPath)) | |||
System.IO.Directory.CreateDirectory(outputPath); | |||
// Decode Base64 string to byte array | |||
byte[] imageBytes = Convert.FromBase64String(fileContent); | |||
using (MemoryStream ms = new MemoryStream(imageBytes)) | |||
using (Image image = Image.Load(ms)) | |||
{ | |||
// Resize if width > 1500, keeping aspect ratio | |||
if (image.Width >= image.Height) | |||
{ | |||
if (image.Width > 1500) | |||
{ | |||
int newWidth = 1500; | |||
int newHeight = (int)((double)newWidth / image.Width * image.Height); | |||
image.Mutate(x => x.Resize(newWidth, newHeight)); | |||
} | |||
} | |||
else | |||
{ | |||
if (image.Height > 1500) | |||
{ | |||
int newHeight = 1500; | |||
int newWidth = (int)((double)newHeight / image.Height * image.Width); | |||
image.Mutate(x => x.Resize(newWidth, newHeight)); | |||
} | |||
} | |||
// Save as PNG | |||
image.Save($"{outputPath}{fileName}", new PngEncoder()); | |||
return fileName; | |||
} | |||
} | |||
} | |||
public class CustomUser | |||
{ | |||
public string Name { get; set; } | |||
public string Username { get; set; } | |||
public Guid Id { get; set; } | |||
public string[] Roles { get; set; } | |||
} | |||
} |
@@ -0,0 +1,55 @@ | |||
using Domain.BaseData; | |||
using Infrastructure; | |||
using Microsoft.AspNetCore.Authorization; | |||
using Microsoft.AspNetCore.Mvc; | |||
using Microsoft.Extensions.DependencyInjection; | |||
using Models.BaseData; | |||
using Services.BaseData; | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Linq; | |||
namespace Api.Controllers | |||
{ | |||
[Authorize(Roles = Consts.Developer)] | |||
public class PlantsController : BaseController | |||
{ | |||
private readonly IPlantsService plantsService; | |||
public PlantsController(IServiceProvider serviceProvider) : base(serviceProvider) | |||
{ | |||
plantsService = serviceProvider.GetService<IPlantsService>(); | |||
} | |||
[HttpGet] | |||
public IActionResult List() | |||
{ | |||
var dbList = plantsService.GetQueryable().ToList(); | |||
return Ok(Mapper.Map<List<PlantViewModel>>(dbList)); | |||
} | |||
[HttpPost] | |||
public IActionResult Add(SavePlantViewModel model) | |||
{ | |||
var dbModel = Mapper.Map<Plant>(model); | |||
var imageName=SaveImageFromBase64(model.Base64Icon); | |||
dbModel.ImageFileName = imageName; | |||
plantsService.Add(dbModel); | |||
return Ok(); | |||
} | |||
[HttpPut] | |||
public IActionResult Update(PlantViewModel model) | |||
{ | |||
var dbModel = plantsService.GetById(model.Id.Value); | |||
Mapper.Map(model, dbModel); | |||
plantsService.Update(dbModel); | |||
return Ok(); | |||
} | |||
[HttpDelete("{id}")] | |||
public IActionResult Delete(Guid id) | |||
{ | |||
plantsService.Delete(id); | |||
return Ok(); | |||
} | |||
} | |||
} |
@@ -0,0 +1,97 @@ | |||
using Api.Start; | |||
using Core.Db; | |||
using Microsoft.AspNetCore.Builder; | |||
using Microsoft.EntityFrameworkCore; | |||
using Microsoft.OpenApi.Models; | |||
using System.Reflection; | |||
using Microsoft.Extensions.DependencyInjection; | |||
using System.Collections.Generic; | |||
using Microsoft.Extensions.FileProviders; | |||
using System.IO; | |||
var builder = WebApplication.CreateBuilder(args); | |||
var Configuration = builder.Configuration; | |||
//MVC: | |||
builder.Services.AddControllersWithViews(); | |||
//Swagger: | |||
builder.Services.AddSwaggerGen(c => | |||
{ | |||
c.SwaggerDoc("v1", new OpenApiInfo { Title = "API", Version = "v1" }); | |||
c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme | |||
{ | |||
Description = @"JWT Authorization header using the bearer scheme. | |||
Enter 'Bearer' [space] and then your token in the text input below. | |||
Example: 'Bearer 12345abcdef'", | |||
Name = "Authorization", | |||
In = ParameterLocation.Header, | |||
Scheme = "Bearer", | |||
Type = SecuritySchemeType.Http | |||
}); | |||
c.AddSecurityRequirement(new OpenApiSecurityRequirement | |||
{{new OpenApiSecurityScheme | |||
{ | |||
Reference = new OpenApiReference | |||
{ | |||
Type = ReferenceType.SecurityScheme, | |||
Id = "Bearer" | |||
}, | |||
Name = "Bearer", | |||
In = ParameterLocation.Header, | |||
}, new List<string>() | |||
}}); | |||
} | |||
); | |||
#region Required Services & DI | |||
builder.Services.RegisterDependencies(Configuration); | |||
builder.Services.RegisterAuth(Configuration); | |||
#endregion | |||
builder.Services.AddCors(builder => | |||
{ | |||
builder.AddPolicy("AllowAll", options => | |||
{ | |||
options.AllowAnyHeader() | |||
.AllowAnyOrigin() | |||
.AllowAnyMethod() | |||
.AllowAnyHeader(); | |||
}); | |||
}); | |||
var app = builder.Build(); | |||
app.UseHttpsRedirection(); | |||
app.UseRouting(); | |||
app.UseCors("AllowAll"); | |||
app.ConfigureAuthentication(); | |||
app.UseSwagger(); | |||
app.UseSwaggerUI(); | |||
app.MapControllerRoute( | |||
name: "default", | |||
pattern: "{controller=Home}/{action=Index}/{id?}"); | |||
app.MapControllerRoute( | |||
name: "pages", | |||
pattern: "{controller=Home}/{pageName}"); | |||
app.UseDeveloperExceptionPage(); | |||
app.UseStaticFiles(); | |||
app.UseStaticFiles(new StaticFileOptions | |||
{ | |||
FileProvider = new PhysicalFileProvider( | |||
Path.Combine(builder.Environment.ContentRootPath, "UploadedFiles")), | |||
RequestPath = "/UploadedFiles" | |||
}); | |||
app.Run(); |
@@ -0,0 +1,38 @@ | |||
{ | |||
"profiles": { | |||
"IIS Express": { | |||
"commandName": "IISExpress", | |||
"launchBrowser": true, | |||
"launchUrl": "swagger", | |||
"environmentVariables": { | |||
"ASPNETCORE_ENVIRONMENT": "Development" | |||
} | |||
}, | |||
"Api": { | |||
"commandName": "Project", | |||
"launchBrowser": true, | |||
"launchUrl": "swagger", | |||
"environmentVariables": { | |||
"ASPNETCORE_ENVIRONMENT": "Development" | |||
}, | |||
"dotnetRunMessages": "true", | |||
"applicationUrl": "https://localhost:5001;http://localhost:5000" | |||
}, | |||
"Docker": { | |||
"commandName": "Docker", | |||
"launchBrowser": true, | |||
"launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}/swagger", | |||
"publishAllPorts": true, | |||
"useSSL": true | |||
} | |||
}, | |||
"$schema": "http://json.schemastore.org/launchsettings.json", | |||
"iisSettings": { | |||
"windowsAuthentication": false, | |||
"anonymousAuthentication": true, | |||
"iisExpress": { | |||
"applicationUrl": "http://localhost:21424", | |||
"sslPort": 44331 | |||
} | |||
} | |||
} |
@@ -0,0 +1,23 @@ | |||
using System.Collections.Generic; | |||
using Microsoft.OpenApi.Models; | |||
using Swashbuckle.AspNetCore.SwaggerGen; | |||
namespace Api.Start | |||
{ | |||
public class AddRequiredHeaderParameter : IOperationFilter | |||
{ | |||
public void Apply(OpenApiOperation operation, OperationFilterContext context) | |||
{ | |||
if (operation.Parameters == null) | |||
operation.Parameters = new List<OpenApiParameter>(); | |||
operation.Parameters.Add(new OpenApiParameter | |||
{ | |||
Name = "X-Version", | |||
In = ParameterLocation.Header, | |||
Required = false, | |||
}); | |||
} | |||
} | |||
} |
@@ -0,0 +1,51 @@ | |||
using Microsoft.AspNetCore.Authentication.JwtBearer; | |||
using Microsoft.AspNetCore.Builder; | |||
using Microsoft.Extensions.Configuration; | |||
using Microsoft.Extensions.DependencyInjection; | |||
using Microsoft.IdentityModel.Tokens; | |||
using System; | |||
using System.Security.Claims; | |||
using System.Text; | |||
namespace Api.Start | |||
{ | |||
public static class AuthRegister | |||
{ | |||
public static void RegisterAuth(this IServiceCollection services, IConfiguration configuration) | |||
{ | |||
services.AddAuthentication(options => | |||
{ | |||
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; | |||
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; | |||
}).AddJwtBearer(options => | |||
{ | |||
var secret = configuration.GetValue<string>("Jwt:Key"); | |||
var issuer = configuration.GetValue<string>("Jwt:Issuer"); | |||
options.IncludeErrorDetails = true; | |||
options.TokenValidationParameters = new TokenValidationParameters | |||
{ | |||
ValidateIssuer = true, | |||
ValidIssuer = issuer, | |||
ValidateAudience = true, | |||
ValidAudience = issuer, | |||
ValidateIssuerSigningKey = true, | |||
IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(secret)), | |||
ValidateLifetime = true, | |||
ClockSkew = TimeSpan.Zero, | |||
RoleClaimType = ClaimTypes.Role, | |||
SaveSigninToken = true, | |||
}; | |||
}); | |||
} | |||
public static void ConfigureAuthentication(this IApplicationBuilder app) | |||
{ | |||
app.UseAuthentication(); | |||
app.UseAuthorization(); | |||
} | |||
} | |||
} |
@@ -0,0 +1,32 @@ | |||
using Core.Db; | |||
using Microsoft.EntityFrameworkCore; | |||
using System.Reflection; | |||
using AutoMapper; | |||
using Microsoft.Extensions.DependencyInjection; | |||
using Microsoft.AspNetCore.Http; | |||
using Microsoft.Extensions.Configuration; | |||
namespace Api.Start | |||
{ | |||
public static class DI | |||
{ | |||
public static void RegisterDependencies(this IServiceCollection services, IConfiguration configuration) | |||
{ | |||
services.AddDbContext<PanakDbContext>(options => options.UseSqlServer(configuration.GetConnectionString("PanakContext"))); | |||
services.AddScoped<DbContext, PanakDbContext>(); | |||
services.Scan(scan => scan | |||
.FromAssemblies(typeof(Services.Identity.IAuthService).GetTypeInfo().Assembly) | |||
.AddClasses() | |||
.AsImplementedInterfaces() | |||
.WithScopedLifetime()); | |||
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>(); | |||
var mapperConfig = new MapperConfiguration(mc => | |||
{ | |||
mc.AddProfile(new AutoMapperConfig()); | |||
}); | |||
var mapper = mapperConfig.CreateMapper(); | |||
services.AddSingleton(mapper); | |||
} | |||
} | |||
} |
@@ -0,0 +1,47 @@ | |||
using Microsoft.AspNetCore.Builder; | |||
using Microsoft.AspNetCore.Hosting; | |||
using Microsoft.AspNetCore.StaticFiles; | |||
using Microsoft.Extensions.DependencyInjection; | |||
using Microsoft.Extensions.Hosting; | |||
using Newtonsoft.Json; | |||
using System; | |||
using System.Text.Json.Serialization; | |||
namespace Api.Start | |||
{ | |||
public static class MvcRegistrar | |||
{ | |||
public static void RegisterMvc(this IServiceCollection services) | |||
{ | |||
services.AddCors(); | |||
services.AddControllers(); | |||
services.AddControllers().AddJsonOptions(options => | |||
{ | |||
options.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles; | |||
}); | |||
} | |||
public static void ConfigureMvc(this IApplicationBuilder app, IWebHostEnvironment env) | |||
{ | |||
app.UseCors(x => x | |||
.WithOrigins("https://imasdk.googleapis.com") | |||
.WithMethods("GET", "POST") | |||
.AllowAnyHeader() | |||
.SetIsOriginAllowed(origin => true) | |||
.AllowCredentials() | |||
.SetPreflightMaxAge(TimeSpan.FromDays(1)) | |||
); | |||
if (env.IsDevelopment()) | |||
{ | |||
app.UseDeveloperExceptionPage(); | |||
} | |||
var provider = new FileExtensionContentTypeProvider(); | |||
//provider.Mappings[".apk"] = "application/vnd.android.package-archive"; | |||
} | |||
} | |||
} |
@@ -0,0 +1,62 @@ | |||
using Microsoft.AspNetCore.Builder; | |||
using Microsoft.Extensions.DependencyInjection; | |||
using Microsoft.OpenApi.Models; | |||
using System.Collections.Generic; | |||
namespace Api.Start | |||
{ | |||
public static class SwaggerRegistrar | |||
{ | |||
public static void RegisterSwagger(this IServiceCollection services) | |||
{ | |||
services.AddSwaggerGen(c => | |||
{ | |||
c.SwaggerDoc("v1", new OpenApiInfo { Title = "API", Version = "v1" }); | |||
c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme | |||
{ | |||
Description = @"JWT Authorization header using the bearer scheme. | |||
Enter 'Bearer' [space] and then your token in the text input below. | |||
Example: 'Bearer 12345abcdef'", | |||
Name = "Authorization", | |||
In = ParameterLocation.Header, | |||
Scheme = "Bearer", | |||
Type = SecuritySchemeType.Http | |||
}); | |||
c.AddSecurityRequirement(new OpenApiSecurityRequirement | |||
{{new OpenApiSecurityScheme | |||
{ | |||
Reference = new OpenApiReference | |||
{ | |||
Type = ReferenceType.SecurityScheme, | |||
Id = "Bearer" | |||
}, | |||
Name = "Bearer", | |||
In = ParameterLocation.Header, | |||
}, new List<string>() | |||
}}); | |||
c.OperationFilter<AddRequiredHeaderParameter>(); | |||
}); | |||
} | |||
public static void ConfigureSwagger(this IApplicationBuilder app) | |||
{ | |||
app.UseSwagger(c => | |||
{ | |||
c.RouteTemplate = "api-docs/{documentName}/swagger.json"; | |||
}); | |||
app.UseSwaggerUI(c => | |||
{ | |||
c.SwaggerEndpoint("/api-docs/v1/swagger.json", "API V1.0"); | |||
c.DocumentTitle = "Api"; | |||
c.EnableDeepLinking(); | |||
c.EnableValidator(); | |||
}); | |||
} | |||
} | |||
} |
@@ -0,0 +1,9 @@ | |||
{ | |||
"Logging": { | |||
"LogLevel": { | |||
"Default": "Information", | |||
"Microsoft": "Warning", | |||
"Microsoft.Hosting.Lifetime": "Information" | |||
} | |||
} | |||
} |
@@ -0,0 +1,17 @@ | |||
{ | |||
"Logging": { | |||
"LogLevel": { | |||
"Default": "Information", | |||
"Microsoft": "Warning", | |||
"Microsoft.Hosting.Lifetime": "Information" | |||
} | |||
}, | |||
"AllowedHosts": "*", | |||
"ConnectionStrings": { | |||
"PanakContext": "Data Source=uk-rm.drawise.net;Initial Catalog=SensoFarm;User ID=sa;Password=H007512$$nes;TrustServerCertificate=True;" | |||
}, | |||
"Jwt": { | |||
"Key": "2659063f-c71e-4cae-9cf6-f3b3e8b77ffe", | |||
"Issuer": "Panak Identity" | |||
} | |||
} |
@@ -0,0 +1,62 @@ | |||
using AbrBlazorTools; | |||
using Microsoft.AspNetCore.Components; | |||
using Models.Identity; | |||
using Newtonsoft.Json; | |||
namespace ApiUtils; | |||
public class UserUtils : IUserUtils | |||
{ | |||
private readonly IHttpClientWithLoginToken _client; | |||
public UserUtils(IHttpClientWithLoginToken client) | |||
{ | |||
_client = client; | |||
} | |||
public async Task<HttpResponseMessage> SendVerificationRequest(object otpModel) | |||
{ | |||
return await _client.HttpPost("/api/AUTH/Login",otpModel); | |||
} | |||
public async Task<HttpResponseMessage> SendSignUpRequest(object accountModel) | |||
{ | |||
return await _client.HttpPost("/api/AUTH/SignUp",accountModel); | |||
} | |||
public async Task<HttpResponseMessage> LoginWithPasswordRequest(object loginModel) | |||
{ | |||
return await _client.HttpPost("/api/AUTH/LoginWithPassword",loginModel); | |||
} | |||
public async Task<HttpResponseMessage> CheckLogin(bool goToLogin = true) | |||
{ | |||
return await _client.HttpGet("/api/AUTH/CheckLogin",true,goToLogin); | |||
} | |||
public async Task<HttpResponseMessage> RefreshToken(object refreshTokenModel) | |||
{ | |||
return await _client.HttpPost("/api/Auth/refresh", refreshTokenModel); | |||
} | |||
public async Task<CheckLoginResponse> SaveEmailAndName(NameAndEmailModel model) | |||
{ | |||
var res=await _client.HttpPost("/api/auth/SetNameAndEmail", model); | |||
if (!res.IsSuccessStatusCode) | |||
return null; | |||
return JsonConvert.DeserializeObject<CheckLoginResponse>(await res.Content.ReadAsStringAsync()); | |||
} | |||
} | |||
public interface IUserUtils | |||
{ | |||
Task<HttpResponseMessage> SendVerificationRequest(object otpModel); | |||
Task<HttpResponseMessage> LoginWithPasswordRequest(object loginModel); | |||
Task<HttpResponseMessage> SendSignUpRequest(object accountModel); | |||
Task<HttpResponseMessage> CheckLogin(bool goToLogin = true); | |||
Task<HttpResponseMessage> RefreshToken(object refreshTokenModel); | |||
Task<CheckLoginResponse> SaveEmailAndName(NameAndEmailModel model); | |||
} |
@@ -0,0 +1,12 @@ | |||
<Router AppAssembly="@typeof(App).Assembly"> | |||
<Found Context="routeData"> | |||
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" /> | |||
<FocusOnNavigate RouteData="@routeData" Selector="h1" /> | |||
</Found> | |||
<NotFound> | |||
<PageTitle>Not found</PageTitle> | |||
<LayoutView Layout="@typeof(MainLayout)"> | |||
<p role="alert">Sorry, there's nothing at this address.</p> | |||
</LayoutView> | |||
</NotFound> | |||
</Router> |
@@ -0,0 +1,36 @@ | |||
<Project Sdk="Microsoft.NET.Sdk.Razor"> | |||
<PropertyGroup> | |||
<TargetFramework>net8.0</TargetFramework> | |||
<Nullable>disable</Nullable> | |||
<ImplicitUsings>enable</ImplicitUsings> | |||
<AddRazorSupportForMvc>true</AddRazorSupportForMvc> | |||
</PropertyGroup> | |||
<ItemGroup> | |||
<Content Remove="wwwroot\assets\react.svg" /> | |||
</ItemGroup> | |||
<ItemGroup> | |||
<None Include="wwwroot\assets\react.svg" /> | |||
</ItemGroup> | |||
<ItemGroup> | |||
<SupportedPlatform Include="browser" /> | |||
</ItemGroup> | |||
<ItemGroup> | |||
<PackageReference Include="Blazored.LocalStorage" Version="4.5.0" /> | |||
<PackageReference Include="CurrieTechnologies.Razor.SweetAlert2" Version="5.6.0" /> | |||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.8" /> | |||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="8.0.8" /> | |||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" /> | |||
</ItemGroup> | |||
<ItemGroup> | |||
<ProjectReference Include="..\Models\Models.csproj" /> | |||
</ItemGroup> | |||
</Project> |
@@ -0,0 +1,37 @@ | |||
| |||
using Microsoft.AspNetCore.Components; | |||
namespace BootStrapComponents | |||
{ | |||
public class BaseBootStrapComponent: ComponentBase | |||
{ | |||
[Parameter] | |||
public TextAlignment TextAlignment { get; set; } = TextAlignment.None; | |||
[Parameter] | |||
public RenderFragment ChildContent { set; get; } | |||
public IEnumerable<string> AutoClass() | |||
{ | |||
if (TextAlignment != TextAlignment.None) yield return "text-" + TextAlignment.ToString(); | |||
yield return ""; | |||
} | |||
[Parameter] | |||
public string AdditionalClass { set; get; } | |||
} | |||
public enum TextAlignment | |||
{ | |||
None, | |||
left, | |||
center, | |||
right, | |||
justify | |||
} | |||
public enum BackColor | |||
{ | |||
primary, | |||
info, | |||
danger, | |||
white, | |||
} | |||
} |
@@ -0,0 +1,15 @@ | |||
@namespace BootStrapComponents | |||
@inherits BaseBootStrapComponent | |||
<button @onclick="DoOnClick" class="btn btn-@BackColor @(string.Join(" ",AutoClass())) @AdditionalClass"> | |||
@ChildContent | |||
</button> | |||
@code { | |||
[Parameter] | |||
public BackColor BackColor { set; get; } = BackColor.primary; | |||
[Parameter] | |||
public EventCallback OnClick { set; get; } = new(); | |||
public async Task DoOnClick() | |||
{ | |||
await OnClick.InvokeAsync(); | |||
} | |||
} |
@@ -0,0 +1,109 @@ | |||
namespace BootStrapComponents | |||
{ | |||
public static class BT | |||
{ | |||
public static string RoundedBorder => "rounded"; | |||
public static string PullRight => "pull-right"; | |||
public static string PullLeft => "pull-left"; | |||
public static string PullCenter => "pull-center"; | |||
public static string TextAlign(TextAlignment textAlignment) => "text-" + textAlignment; | |||
public static string CenterBlock => "mx-auto d-block"; | |||
public static string Button(ColorType colorType, bool outline = false) => "btn btn" + (outline ? "-outline" : "") + "-" + colorType; | |||
public static string Background(ColorType colorType) => "bg-" + colorType; | |||
public static string TextColor(ColorType colorType) => "text-" + colorType; | |||
#region Position | |||
public static string Position(Postion p) => p.ToString().ToLower().Replace("_", "-"); | |||
#endregion | |||
#region Flex | |||
public static string FlexDisplay => "d-flex"; | |||
public static string FlexJustifyAround => "d-flex justify-content-around"; | |||
public static string FlexJustifyBetween => "d-flex justify-content-between"; | |||
#endregion | |||
#region Shadows | |||
public static string RegularShadow => "shadow"; | |||
public static string SmallShadow => "shadow-sm"; | |||
public static string LargeShadow => "shadow-lg"; | |||
#endregion | |||
#region Opacity | |||
public static string Opacity75 => "opacity-75"; | |||
public static string Opacity50 => "opacity-50"; | |||
public static string Opacity25 => "opacity-25"; | |||
#endregion | |||
#region Containers | |||
public static string FormFloating => "form-floating"; | |||
public static string Row => "row"; | |||
public static string Col(int i) => $"col-{i}"; | |||
public static string DisplayBlock => "d-block"; | |||
#endregion | |||
#region Margins | |||
public static string Margin(int m) => $"m-{m}"; | |||
public static string MarginLeft(int m) => $"ml-{m}"; | |||
public static string MarginRight(int m) => $"mr-{m}"; | |||
public static string MarginBottom(int m) => $"mb-{m}"; | |||
public static string MarginTop(int m) => $"mt-{m}"; | |||
public static string Padding(int m) => $"p-{m}"; | |||
public static string PaddingLeft(int m) => $"pl-{m}"; | |||
public static string PaddingRight(int m) => $"pr-{m}"; | |||
public static string PaddingBottom(int m) => $"pb-{m}"; | |||
public static string PaddingTop(int m) => $"pt-{m}"; | |||
#endregion | |||
public static class Icons | |||
{ | |||
public static string Copy => "bi bi-clipboard-fill"; | |||
} | |||
#region Form controls | |||
public static string Input => "form-control"; | |||
#endregion | |||
} | |||
public static class Styles | |||
{ | |||
public static string VerticalSelfAlign(TextAlignment alignment) => $"align-self:{alignment};"; | |||
public static string ToRightGradientBackColor(string color) => $"background-image:linear-gradient(to right, {color}, transparent)"; | |||
public static string ToLeftGradientBackColor(string color) => $"background-image:linear-gradient(to left, {color}, transparent)"; | |||
} | |||
public enum ColorType | |||
{ | |||
primary, | |||
secondary, | |||
success, | |||
danger, | |||
warning, | |||
info, | |||
light, | |||
dark, | |||
white | |||
} | |||
public enum Postion | |||
{ | |||
fixed_top, | |||
sticky_top, | |||
fixed_bottom, | |||
sticky_bottom, | |||
Position_Static, | |||
Position_relative, | |||
Position_absolute, | |||
Position_Fixed, | |||
Position_sticky | |||
} | |||
} |
@@ -0,0 +1,8 @@ | |||
@namespace BootStrapComponents | |||
@inherits BaseBootStrapComponent | |||
<div class="jumbotron @(string.Join(" ",AutoClass())) @AdditionalClass"> | |||
@ChildContent | |||
</div> | |||
@code { | |||
} |
@@ -0,0 +1,42 @@ | |||
using AbrBlazorTools; | |||
using ApiUtils; | |||
using Blazored.LocalStorage; | |||
using BlazorPages.Tools; | |||
using CurrieTechnologies.Razor.SweetAlert2; | |||
using Microsoft.Extensions.DependencyInjection; | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Linq; | |||
using System.Text; | |||
using System.Threading.Tasks; | |||
namespace BlazorPages | |||
{ | |||
public static class Dependency | |||
{ | |||
public static void Inject(this IServiceCollection Services, bool isMobileApp = false) | |||
{ | |||
Statics.IsFromMobileApp = isMobileApp; | |||
#region Tools | |||
///libs: | |||
Services.AddBlazoredLocalStorage(config => config.JsonSerializerOptions.WriteIndented = true); | |||
Services.AddSweetAlert2(); | |||
//////// | |||
Services.AddScoped<ILoadingSpinner, LoadingSpinner>(); | |||
Services.AddScoped<IBrowserTools, BrowserTools>(); | |||
Services.AddScoped<IHttpClientWithLoginToken, HttpClientWithLoginToken>(); | |||
Services.AddScoped<ISwalUtils, SwalUtils>(); | |||
#endregion | |||
#region API | |||
Services.AddScoped<IUserUtils, UserUtils>(); | |||
#endregion | |||
Services.AddScoped(sp => new HttpClient()); | |||
} | |||
} | |||
} |
@@ -0,0 +1,8 @@ | |||
using Microsoft.AspNetCore.Components; | |||
namespace Components | |||
{ | |||
public class BaseComponent:ComponentBase | |||
{ | |||
} | |||
} |
@@ -0,0 +1,37 @@ | |||
@namespace Components | |||
<div class="@BT.FormFloating @CssClass"> | |||
<input id="nametxt" placeholder="@PlaceHolder" type="@Type" @bind="@BindingValue" class="@BT.Input @BT.MarginTop(TopMargin)" @bind:event="oninput" /> | |||
<label for="nametxt">@PlaceHolder</label> | |||
</div> | |||
@code { | |||
[Parameter] | |||
public string PlaceHolder { set; get; } | |||
private string _value; | |||
[Parameter] | |||
public string BindingValue | |||
{ | |||
get => _value; | |||
set | |||
{ | |||
if (_value == value) return; | |||
_value = value; | |||
BindingValueChanged.InvokeAsync(value); | |||
ChangeMethod.InvokeAsync(); | |||
} | |||
} | |||
[Parameter] | |||
public EventCallback<string> BindingValueChanged { get; set; } | |||
[Parameter] | |||
public string Type { set; get; } = "text"; | |||
[Parameter] | |||
public string CssClass{ set; get; } | |||
[Parameter] | |||
public int TopMargin { set; get; } = 3; | |||
[Parameter] public EventCallback ChangeMethod { get; set; } = new(); | |||
} |
@@ -0,0 +1,56 @@ | |||
@using BootStrapComponents | |||
@using Components | |||
@using Models.Identity | |||
@using Newtonsoft.Json | |||
@using BlazorPages.Tools | |||
@page "/" | |||
@layout LayoutWithoutMenu | |||
<img src="/assets/images/logo.png" class="@BT.CenterBlock m-2" style="max-width:30%;" /> | |||
@if (DataGotten) | |||
{ | |||
<button class="@BT.CenterBlock @BT.Button(ColorType.primary,outline:true) @BT.Background(ColorType.light) w-50 h-1" | |||
style="margin-top:calc(50vh - 10em);" | |||
@onclick="@(()=>NavManager.NavigateTo("/login"))"> | |||
ورود و ثبت نام | |||
</button> | |||
} | |||
<div class="@BT.Row @BT.TextColor(ColorType.white) @BT.TextAlign(TextAlignment.center)" style="position:sticky;height:200px; top:calc(100vh - 200px);"> | |||
<hr class="@BT.TextColor(ColorType.white)" /> | |||
<p> | |||
© تمامی حقوق برای حقتو محفوظ میباشد | |||
</p> | |||
<br /> | |||
<p> | |||
ورژن 1.0 | |||
</p> | |||
</div> | |||
@code { | |||
public bool DataGotten { set; get; } | |||
protected override async void OnInitialized() | |||
{ | |||
var hasToken = await LocalStorage.ContainKeyAsync(Statics.LoginTokenKey); | |||
if (hasToken) | |||
{ | |||
var token = await LocalStorage.GetItemAsStringAsync(Statics.LoginTokenKey); | |||
var res = await _userUtils.CheckLogin(goToLogin: false); | |||
var result = | |||
JsonConvert.DeserializeObject<CheckLoginResponse>( | |||
await res.Content.ReadAsStringAsync()); | |||
if (res.IsSuccessStatusCode) | |||
{ | |||
await LoadingSpinner.HideLoading(); | |||
Statics.CurrentLoginInfo = result; | |||
if (!result.HasName) | |||
NavManager.NavigateTo("/NameAndEmail"); | |||
else | |||
NavManager.NavigateTo("/dashboard"); | |||
} | |||
} | |||
DataGotten = true; | |||
StateHasChanged(); | |||
} | |||
} |
@@ -0,0 +1,128 @@ | |||
@page "/login" | |||
@using BootStrapComponents | |||
@using Components | |||
@using Models.Identity | |||
@layout LayoutWithoutMenu | |||
<img src="/assets/images/logo.png" class="@BT.CenterBlock m-2" style="max-width:30%;" /> | |||
<style> | |||
div.login-fields-container { | |||
margin-top: 5vh; | |||
border-radius: 2em; | |||
border-top: solid 0.2em orange; | |||
height: 90vh; | |||
padding: 1em; | |||
} | |||
</style> | |||
<div class="@BT.Background(ColorType.white) login-fields-container"> | |||
<EditForm Model="@OtpLoginModel" OnValidSubmit="Submit"> | |||
<DataAnotationsValidator /> | |||
<p class="@BT.CenterBlock @BT.TextAlign(TextAlignment.center) h4" style="margin-top:2vh"> | |||
ورود / ثبت نام | |||
</p> | |||
<p class="@(BT.TextAlign(TextAlignment.center)) @BT.TextColor(ColorType.secondary) @BT.MarginTop(5) h6"> | |||
@if (!WaitForOtp) | |||
{ | |||
<span> | |||
لطفا شماره همراه خود را وارد کنید. | |||
</span> | |||
} | |||
else | |||
{ | |||
<span> | |||
لطفا کد تائید پیامک شده را وارد کنید. | |||
</span> | |||
} | |||
</p> | |||
@if (!WaitForOtp) | |||
{ | |||
<span> | |||
<input placeholder="تلفن همراه" @bind="OtpLoginModel.Username" class="@BT.Input @BT.MarginTop(5)" type="tel"> | |||
</span> | |||
} | |||
else | |||
{ | |||
<span> | |||
<input placeholder="کد تائید" @bind="LoginModel.Password" class="@BT.Input @BT.MarginTop(5)" type="number"> | |||
</span> | |||
} | |||
<p class="h6 @BT.TextAlign(TextAlignment.center)" style="font-size:2vh;margin-top:5vh;"> | |||
ورود شما به معنای پذیرش | |||
<a href="/login"> | |||
شرایط حقتو و قوانین حریم خصوصی | |||
</a> | |||
است | |||
</p> | |||
<button type="submit" class="@BT.Button(ColorType.primary) @BT.CenterBlock" style="margin-top:5vh"> | |||
ورود | |||
</button> | |||
<div class="@BT.Row @BT.TextAlign(TextAlignment.center)" style="margin-top:12vh"> | |||
<hr class="@BT.TextColor(ColorType.warning)" /> | |||
<p> | |||
© تمامی حقوق برای حقتو محفوظ میباشد | |||
</p> | |||
<br /> | |||
<p> | |||
ورژن 1.1 | |||
</p> | |||
</div> | |||
</EditForm> | |||
</div> | |||
<style> | |||
.form { | |||
display: flex; | |||
flex-direction: column; | |||
align-items: center; | |||
align-content: center; | |||
} | |||
.form-control { | |||
margin: 5px 5px; | |||
} | |||
</style> | |||
@code { | |||
private bool WaitForOtpMode { get; set; } = true; | |||
protected OTPLoginModel OtpLoginModel { get; set; } = new(); | |||
protected bool WaitForOtp { set; get; } = false; | |||
public async void Submit() | |||
{ | |||
if (WaitForOtp) | |||
await RequestLogin(); | |||
else | |||
await RequestVerificationCode(); | |||
} | |||
protected async Task RequestVerificationCode() | |||
{ | |||
var response = await _userUtils.SendVerificationRequest(OtpLoginModel); | |||
if (response.IsSuccessStatusCode) | |||
WaitForOtp = true; | |||
StateHasChanged(); | |||
} | |||
protected LoginModel LoginModel { get; set; } = new(); | |||
protected async Task RequestLogin() | |||
{ | |||
LoginModel.Username = OtpLoginModel.Username; | |||
var response = await _userUtils.LoginWithPasswordRequest(LoginModel); | |||
if (response.IsSuccessStatusCode) | |||
{ | |||
var result = await response.Content.ReadFromJsonAsync<Models.Identity.PlainToken>(); | |||
await LocalStorage.SetItemAsync(Statics.LoginTokenKey, result.AccessToken); | |||
NavManager.NavigateTo("/dashboard"); | |||
} | |||
} | |||
} |
@@ -0,0 +1,33 @@ | |||
<div class="modal @(Visible? "show":"") " style="display: @(Visible? "block;":"none")" id="@DialogId" tabindex="-1" aria-labelledby="exampleModalLabel" aria-hidden="true"> | |||
<div class="modal-dialog"> | |||
<div class="modal-content"> | |||
<div class="modal-header"> | |||
<h1 class="modal-title fs-5 " id="exampleModalLabel">@Title</h1> | |||
<button type="button" class="btn-close" style="margin: 0 0;" data-bs-dismiss="modal" aria-label="Close" @onclick="ToggleShowDialog"> </button> | |||
</div> | |||
<div class="modal-body"> | |||
@DialogBody | |||
</div> | |||
</div> | |||
</div> | |||
</div> | |||
@code | |||
{ | |||
[Parameter] | |||
public bool Visible { get; set; } | |||
[Parameter] | |||
public string DialogId { get; set; } = "exampleModal"; | |||
[Parameter] | |||
public string Title { get; set; } | |||
[Parameter] | |||
public RenderFragment DialogBody { get; set; } | |||
private void ToggleShowDialog() | |||
{ | |||
Visible = false; | |||
StateHasChanged(); | |||
} | |||
} |
@@ -0,0 +1,15 @@ | |||
@typeparam T | |||
<div class="row"> | |||
<div class="col form-control-box"> | |||
<input type="text" class="form-control" placeholder="عنوان" > | |||
@* <ValidationMessage For="@(() => FieldOfModel)"/> *@ | |||
</div> | |||
</div> | |||
@code { | |||
[Parameter] | |||
public string FieldOfModel { get; set; } | |||
[Parameter] | |||
public EventCallback<T> FieldOfModelChanged { get; set; } | |||
} |
@@ -0,0 +1 @@ | |||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-emoji-heart-eyes" viewBox="0 0 16 16"><path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z" /><path d="M11.315 10.014a.5.5 0 0 1 .548.736A4.498 4.498 0 0 1 7.965 13a4.498 4.498 0 0 1-3.898-2.25.5.5 0 0 1 .548-.736h.005l.017.005.067.015.252.055c.215.046.515.108.857.169.693.124 1.522.242 2.152.242.63 0 1.46-.118 2.152-.242a26.58 26.58 0 0 0 1.109-.224l.067-.015.017-.004.005-.002zM4.756 4.566c.763-1.424 4.02-.12.952 3.434-4.496-1.596-2.35-4.298-.952-3.434zm6.488 0c1.398-.864 3.544 1.838-.952 3.434-3.067-3.554.19-4.858.952-3.434z" /></svg> |
@@ -0,0 +1,56 @@ | |||
@inherits LayoutComponentBase | |||
@using Components | |||
@using Newtonsoft.Json | |||
@using Models.Identity | |||
@inject ILoadingSpinner _LoadingSpinner | |||
<RoundContainerContainer Padding="0" BackroundImage="true"> | |||
@Body | |||
</RoundContainerContainer> | |||
@code | |||
{ | |||
protected override async void OnAfterRender(bool firstRender) | |||
{ | |||
if (firstRender) | |||
await CheckLogin(); | |||
base.OnAfterRender(firstRender); | |||
} | |||
async Task CheckLogin() | |||
{ | |||
Console.WriteLine("" + Statics.LastCheck); | |||
Console.WriteLine("checking login..."); | |||
Statics.LastCheck = DateTime.Now; | |||
var hasToken = await LocalStorage.ContainKeyAsync(Statics.LoginTokenKey); | |||
if (hasToken) | |||
{ | |||
var token = await LocalStorage.GetItemAsStringAsync(Statics.LoginTokenKey); | |||
var res = await _userUtils.CheckLogin(goToLogin: false); | |||
if (res.IsSuccessStatusCode) | |||
{ | |||
var result = | |||
JsonConvert.DeserializeObject<CheckLoginResponse>( | |||
await res.Content.ReadAsStringAsync()); | |||
await _LoadingSpinner.HideLoading(); | |||
Statics.CurrentLoginInfo = result; | |||
if (!result.HasName) | |||
NavManager.NavigateTo("/NameAndEmail"); | |||
else | |||
NavManager.NavigateTo("/dashboard"); | |||
} | |||
await _LoadingSpinner.HideLoading(); | |||
StateHasChanged(); | |||
} | |||
} | |||
private async void SignOut() | |||
{ | |||
await LocalStorage.RemoveItemAsync(Statics.LoginTokenKey); | |||
await LocalStorage.RemoveItemAsync(Statics.RefreshTokenKey); | |||
NavManager.NavigateTo("/login"); | |||
} | |||
} |
@@ -0,0 +1,197 @@ | |||
@using Components | |||
@using Newtonsoft.Json | |||
@using Models.Identity | |||
@inject ILoadingSpinner _LoadingSpinner | |||
@inherits LayoutComponentBase | |||
<RoundContainerContainer Padding="0" Scroll=true> | |||
<style> | |||
div.scroll-div { | |||
background-color: #F1F1F1; | |||
overflow-y: hidden; | |||
height: calc(100vh - 122px) | |||
} | |||
@@media only screen and (min-width: 600px) { | |||
div.scroll-div { | |||
height: calc(100vh - 120px) | |||
} | |||
} | |||
</style> | |||
<div class="@BT.FlexJustifyAround" style="flex-direction:column" @onclick="@(()=>ToggleMenu(close:true))"> | |||
@*Top Header*@ | |||
<div class="@BT.Row @BT.Background(ColorType.white) @BT.Margin(0) @BT.Padding(2) @BT.SmallShadow" style="height:60px;"> | |||
<div style="@Styles.VerticalSelfAlign(TextAlignment.center)" class="@BT.Col(6) @BT.TextAlign(TextAlignment.left)"> | |||
<img role="button" @onclick="@(()=>ToggleMenu(close:false))" src="/assets/images/Hamburger_icon.svg" width="30"> | |||
<img src="/assets/images/logo-blue.png" alt="" width="60"> | |||
</div> | |||
<div class="@BT.Col(6) @BT.TextAlign(TextAlignment.right)"> | |||
</div> | |||
</div> | |||
@*end of Top Header*@ | |||
<div class="scroll-div @BT.Row @BT.Margin(0) @BT.DisplayBlock"> | |||
@Body | |||
</div> | |||
@*Footer*@ | |||
<div class="@BT.Background(ColorType.white) @(BT.Row) @(BT.TextAlign(TextAlignment.center)) @(BT.SmallShadow) " | |||
style="height:60px;font-size:12px;width:100%;margin:0 auto;"> | |||
<div @onclick="@(()=>NavManager.NavigateTo("/dashboard"))" role="button" style="@(Styles.VerticalSelfAlign(TextAlignment.center)) " class="@(BT.Col(4)) @(BT.TextAlign(TextAlignment.right))"> | |||
<img src="/assets/images/ico-dashboard.png" alt="" width="26"> | |||
<br /> | |||
داشبورد | |||
</div> | |||
<div @onclick="@(()=>NavManager.NavigateTo("/Suit/Subject/Level1"))" role="button" style="@(Styles.VerticalSelfAlign(TextAlignment.center))" class="@(BT.Col(4)) @(BT.TextAlign(TextAlignment.right))"> | |||
<img src="/assets/images/ico-add.png" alt="" width="26"> | |||
<br /> | |||
دعوی جدید | |||
</div> | |||
<div role="button" style="@(Styles.VerticalSelfAlign(TextAlignment.center))" class="@(BT.Col(4)) @(BT.TextAlign(TextAlignment.right))"> | |||
<img src="/assets/images/ico-user.png" alt="" width="26"> | |||
<br /> | |||
پروفایل | |||
</div> | |||
</div> | |||
@*End Of Footer*@ | |||
</div> | |||
<div class="menu @(BT.Background(ColorType.dark)) @BT.Position(Postion.Position_Fixed)" | |||
style="top:auto;height:100vh;top:0;width:@(MenuWidth)%;max-width:450px;"> | |||
@if (!MenuIsChanging && MenuWidth > 10) | |||
{ | |||
<a href="/about">درباره حقتو</a> | |||
<a href="/Privacy">حریم خصوصی</a> | |||
<a href="/Contact">تماس با ما</a> | |||
} | |||
</div> | |||
</RoundContainerContainer> | |||
<style> | |||
div.menu a { | |||
line-height: 1.5em; | |||
padding:10px; | |||
border-bottom: 1px white solid; | |||
width: 100%; | |||
text-decoration: none; | |||
font-size: 1.5em; | |||
opacity: 1; | |||
color: white; | |||
display: block; | |||
} | |||
</style> | |||
@code | |||
{ | |||
public int MenuWidth { set; get; } = 0; | |||
public bool ShowMenu { set; get; } = false; | |||
public bool MenuIsChanging { set; get; } = false; | |||
protected async Task ToggleMenu(bool close = false) | |||
{ | |||
if (MenuIsChanging) | |||
return; | |||
MenuIsChanging = true; | |||
if (close) | |||
{ | |||
Console.WriteLine("closing menu"); | |||
while (MenuWidth > 0) | |||
{ | |||
MenuWidth = Math.Max(MenuWidth -= 2, 0); | |||
StateHasChanged(); | |||
await Task.Delay(5); | |||
} | |||
MenuIsChanging = false; | |||
return; | |||
} | |||
else | |||
{ | |||
Console.WriteLine("opening menu"); | |||
while (MenuWidth < 60) | |||
{ | |||
Console.WriteLine(MenuWidth); | |||
MenuWidth = Math.Min(MenuWidth += 2, 60); | |||
StateHasChanged(); | |||
await Task.Delay(5); | |||
} | |||
MenuIsChanging = false; | |||
} | |||
} | |||
protected override async void OnAfterRender(bool firstRender) | |||
{ | |||
if (firstRender) | |||
await CheckLogin(); | |||
base.OnAfterRender(firstRender); | |||
NavManager.LocationChanged += HideMenu; | |||
} | |||
private void HideMenu(object sender, LocationChangedEventArgs e) | |||
{ | |||
MenuWidth = 0; | |||
StateHasChanged(); | |||
} | |||
async Task CheckLogin() | |||
{ | |||
Console.WriteLine("" + Statics.LastCheck); | |||
if (Statics.LastCheck.HasValue && DateTime.Now.Subtract(Statics.LastCheck.Value).TotalMinutes < 5) | |||
{ | |||
Console.WriteLine("less than 5 minutes login check"); | |||
base.OnInitialized(); | |||
return; | |||
} | |||
Console.WriteLine("checking login..."); | |||
Statics.LastCheck = DateTime.Now; | |||
var hasToken = await LocalStorage.ContainKeyAsync(Statics.LoginTokenKey); | |||
if (!hasToken) | |||
NavManager.NavigateTo("/"); | |||
else | |||
{ | |||
var token = await LocalStorage.GetItemAsStringAsync(Statics.LoginTokenKey); | |||
var res = await _userUtils.CheckLogin(); | |||
if (res.StatusCode == System.Net.HttpStatusCode.Unauthorized) | |||
{ | |||
var refreshRes = await _userUtils.RefreshToken(new RefreshTokenModel | |||
{ | |||
Token = token | |||
}); | |||
if (refreshRes.IsSuccessStatusCode) | |||
{ | |||
var newToken = JsonConvert.DeserializeObject<PlainToken>(await refreshRes.Content.ReadAsStringAsync()); | |||
Statics.LoginTokenKey = newToken.AccessToken; | |||
Statics.RefreshTokenKey = newToken.RefreshToken; | |||
await LocalStorage.SetItemAsync(Statics.LoginTokenKey, Statics.LoginTokenKey); | |||
await LocalStorage.SetItemAsync(Statics.RefreshTokenKey, Statics.RefreshTokenKey); | |||
} | |||
} | |||
if (!res.IsSuccessStatusCode) | |||
{ | |||
await _LoadingSpinner.HideLoading(); | |||
await _sweetAlertService.ShowSwal(title: "", message: "نشست منقضی شد،لطفا مجددا وارد شوید", SweetAlertIcon.Error); | |||
NavManager.NavigateTo("/login"); | |||
} | |||
else | |||
{ | |||
var result = | |||
JsonConvert.DeserializeObject<CheckLoginResponse>( | |||
await res.Content.ReadAsStringAsync()); | |||
Statics.CurrentLoginInfo = result; | |||
} | |||
await _LoadingSpinner.HideLoading(); | |||
StateHasChanged(); | |||
} | |||
} | |||
private async void SignOut() | |||
{ | |||
await LocalStorage.RemoveItemAsync(Statics.LoginTokenKey); | |||
await LocalStorage.RemoveItemAsync(Statics.RefreshTokenKey); | |||
NavManager.NavigateTo("/login"); | |||
} | |||
} |
@@ -0,0 +1,81 @@ | |||
.page { | |||
position: relative; | |||
display: flex; | |||
flex-direction: column; | |||
} | |||
main { | |||
flex: 1; | |||
} | |||
.sidebar { | |||
background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%); | |||
} | |||
.top-row { | |||
background-color: #f7f7f7; | |||
border-bottom: 1px solid #d6d5d5; | |||
justify-content: flex-end; | |||
height: 3.5rem; | |||
display: flex; | |||
align-items: center; | |||
} | |||
.top-row ::deep a, .top-row ::deep .btn-link { | |||
white-space: nowrap; | |||
margin-left: 1.5rem; | |||
text-decoration: none; | |||
} | |||
.top-row ::deep a:hover, .top-row ::deep .btn-link:hover { | |||
text-decoration: underline; | |||
} | |||
.top-row ::deep a:first-child { | |||
overflow: hidden; | |||
text-overflow: ellipsis; | |||
} | |||
@media (max-width: 640.98px) { | |||
.top-row:not(.auth) { | |||
display: none; | |||
} | |||
.top-row.auth { | |||
justify-content: space-between; | |||
} | |||
.top-row ::deep a, .top-row ::deep .btn-link { | |||
margin-left: 0; | |||
} | |||
} | |||
@media (min-width: 641px) { | |||
.page { | |||
flex-direction: row; | |||
} | |||
.sidebar { | |||
width: 250px; | |||
height: 100vh; | |||
position: sticky; | |||
top: 0; | |||
} | |||
.top-row { | |||
position: sticky; | |||
top: 0; | |||
z-index: 1; | |||
} | |||
.top-row.auth ::deep a:first-child { | |||
flex: 1; | |||
text-align: right; | |||
width: 0; | |||
} | |||
.top-row, article { | |||
padding-left: 2rem !important; | |||
padding-right: 1.5rem !important; | |||
} | |||
} |
@@ -0,0 +1,70 @@ | |||
| |||
<div class="top-row ps-3 navbar navbar-dark"> | |||
<div class="container-fluid"> | |||
<a class="navbar-brand" href="">حق تو</a> | |||
<button title="Navigation menu" class="navbar-toggler" @onclick="ToggleNavMenu"> | |||
<span class="navbar-toggler-icon"></span> | |||
</button> | |||
</div> | |||
</div> | |||
<div @onclick="ToggleNavMenu"> | |||
<nav class="flex-column"> | |||
<div class="nav-item px-3"> | |||
<NavLink class="nav-link" href="" Match="NavLinkMatch.All"> | |||
<span class="oi oi-home" aria-hidden="true"></span> خانه | |||
</NavLink> | |||
</div> | |||
<div class="nav-item px-3"> | |||
<NavLink class="nav-link" href="Subjects"> | |||
<span class="oi oi-list-rich" aria-hidden="true"></span> موضوعات | |||
</NavLink> | |||
</div> | |||
<div class="nav-item px-3"> | |||
<NavLink class="nav-link" href="Words/word"> | |||
<span class="oi oi-list-rich" aria-hidden="true"></span>دیکشنری | |||
</NavLink> | |||
</div> | |||
<div class="nav-item px-3"> | |||
<NavLink class="nav-link" href="Words/bills"> | |||
<span class="oi oi-list-rich" aria-hidden="true"></span>لوایح و اسناد | |||
</NavLink> | |||
</div> | |||
<div class="nav-item px-3"> | |||
<NavLink class="nav-link" href="Questions"> | |||
<span class="oi oi-list-rich" aria-hidden="true"></span> سوالات | |||
</NavLink> | |||
</div> | |||
<div class="nav-item px-3"> | |||
<NavLink class="nav-link" href="SuitTrees"> | |||
<span class="oi oi-list-rich" aria-hidden="true"></span> درختان شکایت | |||
</NavLink> | |||
</div> | |||
<div class="nav-item px-3"> | |||
<NavLink class="nav-link" @onclick="Signout"> | |||
<span class="oi oi-account-logout" aria-hidden="true"></span> خروج | |||
</NavLink> | |||
</div> | |||
</nav> | |||
</div> | |||
@code { | |||
private bool collapseNavMenu = true; | |||
private string? NavMenuCssClass => collapseNavMenu ? "collapse" : null; | |||
private void ToggleNavMenu() | |||
{ | |||
collapseNavMenu = !collapseNavMenu; | |||
} | |||
private async Task Signout() | |||
{ | |||
var confirm = await SwalUtils.ShowSwal(title: "", message: "از خروج اطمینان دارید؟", icon: SweetAlertIcon.Question); | |||
if (!confirm.IsConfirmed) | |||
return; | |||
await LocalStorage.RemoveItemAsync(Statics.LoginTokenKey); | |||
NavManager.NavigateTo("/Login"); | |||
} | |||
} |
@@ -0,0 +1,65 @@ | |||
.navbar-toggler { | |||
background-color: rgba(255, 255, 255, 0.1); | |||
} | |||
.top-row { | |||
height: 3.5rem; | |||
background-color: rgba(0,0,0,0.4); | |||
} | |||
.navbar-brand { | |||
font-size: 1.1rem; | |||
} | |||
.oi { | |||
width: 2rem; | |||
font-size: 1.1rem; | |||
vertical-align: text-top; | |||
top: -2px; | |||
} | |||
.nav-item { | |||
font-size: 0.9rem; | |||
padding-bottom: 0.5rem; | |||
} | |||
.nav-item:first-of-type { | |||
padding-top: 1rem; | |||
} | |||
.nav-item:last-of-type { | |||
padding-bottom: 1rem; | |||
} | |||
.nav-item ::deep a { | |||
color: #d7d7d7; | |||
border-radius: 4px; | |||
height: 3rem; | |||
display: flex; | |||
align-items: center; | |||
line-height: 3rem; | |||
} | |||
.nav-item ::deep a.active { | |||
background-color: rgba(255,255,255,0.25); | |||
color: white; | |||
} | |||
.nav-item ::deep a:hover { | |||
background-color: rgba(255,255,255,0.1); | |||
color: white; | |||
} | |||
@media (min-width: 641px) { | |||
.navbar-toggler { | |||
display: none; | |||
} | |||
.collapse { | |||
/* Never collapse the sidebar for wide screens */ | |||
display: block; | |||
} | |||
} | |||
@@ -0,0 +1,25 @@ | |||
<button type="button" class="btn @CustomStyleClass" data-bs-toggle="modal" data-bs-target="@DialogId" @onclick="OnclickFunc"> | |||
@ButtonName | |||
</button> | |||
@code | |||
{ | |||
[Parameter] | |||
public EventCallback OnclickFunc { get; set; } | |||
[Parameter] | |||
public string ButtonName { get; set; } | |||
[Parameter] | |||
public string CustomStyleClass { get; set; } = "btn-primary"; | |||
[Parameter] | |||
public string DialogId { get; set; } = "#exampleModal"; | |||
protected override void OnInitialized() | |||
{ | |||
if (DialogId[0] != '#') | |||
DialogId = $"#{DialogId}"; | |||
base.OnInitialized(); | |||
} | |||
} |
@@ -0,0 +1,7 @@ | |||
<nav class="navbar navbar-expand-lg navbar-dark bg-primary navbar-fixed-top" style="direction:@(Statics.Culture==Statics.CultureType.En?"ltr":"rtl")"> | |||
<div class="container-fluid"> | |||
<div class="d-flex align-content-center"> | |||
<h1 class="app-title">SensoFarm</h1> | |||
</div> | |||
</div> | |||
</nav> |
@@ -0,0 +1,65 @@ | |||
using Microsoft.JSInterop; | |||
namespace BlazorPages.Tools | |||
{ | |||
public class BrowserTools : IBrowserTools | |||
{ | |||
private readonly IJSRuntime _js; | |||
public BrowserTools(IJSRuntime js) | |||
{ | |||
_js = js; | |||
} | |||
public async Task<BrowserDimension> GetDimensions() | |||
{ | |||
return await _js.InvokeAsync<BrowserDimension>("methods.getDimensions"); | |||
} | |||
public async Task<ValueTask> CopyText(string text) | |||
{ | |||
return _js.InvokeVoidAsync("navigator.clipboard.writeText", text); | |||
} | |||
public async Task PrepareResponsiveTable() | |||
{ | |||
await _js.InvokeVoidAsync("methods.PrepareResponsiveTable"); | |||
} | |||
public async Task<string> GetInputValueById(string id) | |||
{ | |||
return await _js.InvokeAsync<string>("AbrMethods.getInputValue", id); | |||
} | |||
public async Task<string> GetInputValueByIdAndReplacePersianNumbers(string id) | |||
{ | |||
var text = await GetInputValueById(id); | |||
var digits = new string[] { "۰", "۱", "۲", "۳", "۴", "۵", "۶", "۷", "۸", "۹" }; | |||
var english = new string[] { "0", "1", "2", "3", "4", "5", "6", "7", "8", "9" }; | |||
for (var i = 0; i < digits.Length; i++) | |||
text = text.Replace(digits[i], english[i]); | |||
return text; | |||
} | |||
public async Task ScrollToEnd() | |||
{ | |||
await _js.InvokeVoidAsync("methods.ScrollToEnd"); | |||
} | |||
} | |||
public class BrowserDimension | |||
{ | |||
public int Width { get; set; } | |||
public int Height { get; set; } | |||
} | |||
public interface IBrowserTools | |||
{ | |||
Task<BrowserDimension> GetDimensions(); | |||
Task<ValueTask> CopyText(string text); | |||
Task PrepareResponsiveTable(); | |||
Task<string> GetInputValueById(string id); | |||
Task<string> GetInputValueByIdAndReplacePersianNumbers(string id); | |||
Task ScrollToEnd(); | |||
} | |||
} |
@@ -0,0 +1,178 @@ | |||
using System; | |||
using System.ComponentModel; | |||
using System.ComponentModel.DataAnnotations; | |||
using System.Globalization; | |||
namespace BlazorPages.Tools | |||
{ | |||
public static class DateTimeExtension | |||
{ | |||
public static string ComputedDateTime(this DateTime InsertDateTime) | |||
{ | |||
var persianCalender = new PersianCalendar(); | |||
var year = persianCalender.GetYear(InsertDateTime); | |||
var month = persianCalender.GetMonth(InsertDateTime); | |||
var day = persianCalender.GetDayOfMonth(InsertDateTime); | |||
var hour = persianCalender.GetHour(InsertDateTime); | |||
var minute = persianCalender.GetMinute(InsertDateTime); | |||
var second = persianCalender.GetSecond(InsertDateTime); | |||
var computed = string.Format("{0}-{1}-{2}-{3}{4}{5}", year, | |||
month < 10 ? string.Format("0{0}", month) : month.ToString(), | |||
day < 10 ? string.Format("0{0}", day) : day.ToString(), | |||
hour < 10 ? string.Format("0{0}", hour) : hour.ToString(), | |||
minute < 10 ? string.Format("0{0}", minute) : minute.ToString(), | |||
second < 10 ? string.Format("0{0}", second) : second.ToString()); | |||
return computed; | |||
} | |||
public static string GetPersianDate(this DateTime value, bool showDayName, bool showMonthName, bool showTime) | |||
{ | |||
if (Statics.Culture == Statics.CultureType.En) | |||
return value.ToString(); | |||
var persianCalender = new PersianCalendar(); | |||
var year = persianCalender.GetYear(value); | |||
var month = persianCalender.GetMonth(value); | |||
var day = persianCalender.GetDayOfMonth(value); | |||
var s = string.Format(showMonthName ? "{2} {1} {0}" : "{0}/{1}/{2}", year, showMonthName ? month.GetMonthName() : month.ToString(""), day); | |||
if (showDayName) | |||
s = string.Format("{0} {1}", persianCalender.GetDayOfWeek(value).GetDayName(), s); | |||
if (showTime) | |||
s = string.Format("{0} {1}:{2}", s, value.Hour, value.Minute); | |||
return s; | |||
} | |||
public static string GetPersianDayName(this DateTime value) | |||
{ | |||
var persianCalender = new PersianCalendar(); | |||
var s = persianCalender.GetDayOfWeek(value).GetDayName(); | |||
return s; | |||
} | |||
public static string GetPersianMonthName(this DateTime value) | |||
{ | |||
var persianCalender = new PersianCalendar(); | |||
var s = persianCalender.GetMonth(value).GetMonthName(); | |||
return s; | |||
} | |||
public static int GetPersianDayInt(this DateTime value) | |||
{ | |||
var persianCalender = new PersianCalendar(); | |||
var s = persianCalender.GetDayOfMonth(value); | |||
return s; | |||
} | |||
public static string GetPersianDate(this DateTime value) | |||
{ | |||
return value.GetPersianDate(true, true, true); | |||
} | |||
public static string GetPersianDate(this DateTime? value) | |||
{ | |||
return value.HasValue ? value.Value.GetPersianDate(true, true, true) : ""; | |||
} | |||
public static string GetPersianDate(this DateTime? value, bool showDayName) | |||
{ | |||
return value.HasValue ? value.Value.GetPersianDate(showDayName, false, false) : ""; | |||
} | |||
public static string GetPersianDate(this DateTime value, bool showDayName) | |||
{ | |||
return value.GetPersianDate(showDayName, false, false); | |||
} | |||
public static string GetPersianDate(this DateTime value, bool showDayName, bool showTime) | |||
{ | |||
return value.GetPersianDate(showDayName, false, showTime); | |||
} | |||
public static string GetPersianDateAsText(this DateTime value, bool showDayName, bool showTime) | |||
{ | |||
value = value.ToUniversalTime().ToLocalTime(); | |||
var minutes = (int)DateTime.Now.Subtract(value).TotalMinutes; | |||
var seconds = (int)DateTime.Now.Subtract(value).TotalSeconds; | |||
if (minutes < 1) | |||
if (Statics.Culture == Statics.CultureType.Fa) | |||
return $"{seconds} ثانیه قبل"; | |||
else | |||
return $"{seconds} Second(s) ago"; | |||
else if (minutes < 60) | |||
if (Statics.Culture == Statics.CultureType.Fa) | |||
return $"{minutes} دقیقه قبل"; | |||
else | |||
return $"{minutes} Minute(s) ago"; | |||
return value.GetPersianDate(showDayName, false, showTime); | |||
} | |||
private static string GetMonthName(this int month) | |||
{ | |||
switch (month) | |||
{ | |||
case 1: | |||
return "فروردین"; | |||
case 2: | |||
return "اردیبهشت"; | |||
case 3: | |||
return "خرداد"; | |||
case 4: | |||
return "تیر"; | |||
case 5: | |||
return "امرداد"; | |||
case 6: | |||
return "شهریور"; | |||
case 7: | |||
return "مهر"; | |||
case 8: | |||
return "آبان"; | |||
case 9: | |||
return "آذر"; | |||
case 10: | |||
return "دی"; | |||
case 11: | |||
return "بهمن"; | |||
case 12: | |||
return "اسفند"; | |||
default: return ""; | |||
} | |||
} | |||
public static string GetDayName(this DayOfWeek day) | |||
{ | |||
switch (day) | |||
{ | |||
case DayOfWeek.Friday: | |||
return "جمعه"; | |||
case DayOfWeek.Monday: | |||
return "دوشنبه"; | |||
case DayOfWeek.Saturday: | |||
return "شنبه"; | |||
case DayOfWeek.Sunday: | |||
return "یک شنبه"; | |||
case DayOfWeek.Thursday: | |||
return "پنج شنبه"; | |||
case DayOfWeek.Tuesday: | |||
return "سه شنبه"; | |||
case DayOfWeek.Wednesday: | |||
return "چهار شنبه"; | |||
default: | |||
return ""; | |||
} | |||
} | |||
} | |||
public enum PersianDays | |||
{ | |||
[Display(Name = "شنبه")] | |||
Saturday = DayOfWeek.Saturday, | |||
[Display(Name = "یکشنبه")] | |||
Sunday = DayOfWeek.Sunday, | |||
[Display(Name = "دوشنبه")] | |||
Monday = DayOfWeek.Monday, | |||
[Display(Name = "سه شنبه")] | |||
Tuesday = DayOfWeek.Tuesday, | |||
[Display(Name = "چهار شنبه")] | |||
Wednesday = DayOfWeek.Wednesday, | |||
[Display(Name = "پنج شنبه")] | |||
Thursday = DayOfWeek.Thursday, | |||
[Display(Name = "چمعه")] | |||
Friday = DayOfWeek.Friday, | |||
} | |||
} |
@@ -0,0 +1,234 @@ | |||
using Blazored.LocalStorage; | |||
using CurrieTechnologies.Razor.SweetAlert2; | |||
using Newtonsoft.Json; | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Linq; | |||
using System.Net; | |||
using System.Net.Http; | |||
using System.Net.Http.Headers; | |||
using System.Net.Http.Json; | |||
using System.Security.Cryptography; | |||
using System.Text; | |||
using System.Text.Json; | |||
using System.Threading.Tasks; | |||
using Microsoft.AspNetCore.Components; | |||
using BlazorPages.Tools; | |||
namespace AbrBlazorTools | |||
{ | |||
public class HttpClientWithLoginToken : IHttpClientWithLoginToken | |||
{ | |||
private readonly HttpClient _httpClient = new HttpClient(); | |||
private readonly ILocalStorageService _localStorage; | |||
private readonly CurrieTechnologies.Razor.SweetAlert2.SweetAlertService _swal; | |||
private readonly AbrBlazorTools.ILoadingSpinner _loadingSpinner; | |||
private readonly NavigationManager _navigationManager; | |||
private readonly IBrowserTools _browserTools; | |||
public HttpClientWithLoginToken(ILocalStorageService localStorage, CurrieTechnologies.Razor.SweetAlert2.SweetAlertService swal | |||
, AbrBlazorTools.ILoadingSpinner loadingSpinner, NavigationManager navigationManager, IBrowserTools browserTools) | |||
{ | |||
_localStorage = localStorage; | |||
_swal = swal; | |||
_loadingSpinner = loadingSpinner; | |||
_navigationManager = navigationManager; | |||
_browserTools = browserTools; | |||
_httpClient.BaseAddress = new Uri(Statics.BaseAddress); | |||
} | |||
public async Task<HttpClient> GetInstance() | |||
{ | |||
var hasToken = await _localStorage.ContainKeyAsync(Statics.LoginTokenKey); | |||
if (hasToken) | |||
{ | |||
var token = await _localStorage.GetItemAsStringAsync(Statics.LoginTokenKey); | |||
if (token.StartsWith('"')) | |||
token = token.Substring(1); | |||
if (token.EndsWith('"')) | |||
token = token.Substring(0, token.Length - 1); | |||
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); | |||
} | |||
return _httpClient; | |||
} | |||
public async Task<HttpResponseMessage> HttpGet(string url, bool hasLoading = true, bool goToLogin = true) | |||
{ | |||
var res=new HttpResponseMessage(); | |||
Console.WriteLine("Get : " + url); | |||
if (hasLoading) | |||
await _loadingSpinner.ShowLoading(); | |||
var http = await GetInstance(); | |||
try | |||
{ | |||
res= await http.GetAsync(url); | |||
await _loadingSpinner.HideLoading(); | |||
if (res.IsSuccessStatusCode) | |||
{ | |||
return res; | |||
} | |||
if (res.StatusCode == HttpStatusCode.Unauthorized) | |||
{ | |||
await _loadingSpinner.HideLoading(); | |||
if (goToLogin) | |||
{ | |||
Console.WriteLine("خطای دسترسی"); | |||
_navigationManager.NavigateTo("/"); | |||
} | |||
} | |||
else if (res.StatusCode == HttpStatusCode.BadRequest) | |||
{ | |||
await _swal.FireAsync( message: res.ReasonPhrase, icon: SweetAlertIcon.Error); | |||
} | |||
} | |||
catch (Exception ex) | |||
{ | |||
Console.WriteLine(ex); | |||
await _loadingSpinner.HideLoading(); | |||
await _swal.FireAsync( message: res.ReasonPhrase, icon: SweetAlertIcon.Error); | |||
} | |||
await _loadingSpinner.HideLoading(); | |||
return new HttpResponseMessage { StatusCode = HttpStatusCode.BadRequest }; | |||
} | |||
public async Task<HttpResponseMessage> HttpDelete(string url, bool hasLoading = true) | |||
{ | |||
Console.WriteLine("Delete : " + url); | |||
var res = new HttpResponseMessage(); | |||
if (hasLoading) | |||
await _loadingSpinner.ShowLoading(); | |||
var http = await GetInstance(); | |||
try | |||
{ | |||
res = await http.DeleteAsync(url); | |||
await _loadingSpinner.HideLoading(); | |||
if (res.IsSuccessStatusCode) | |||
{ | |||
return res; | |||
} | |||
if (res.StatusCode == HttpStatusCode.Unauthorized) | |||
{ | |||
await _loadingSpinner.HideLoading(); | |||
_navigationManager.NavigateTo("/"); | |||
} | |||
else | |||
{ | |||
dynamic error = JsonConvert.DeserializeObject<object>(await res.Content.ReadAsStringAsync()); | |||
await _swal.FireAsync( message: await ErrorMessage(res), icon: SweetAlertIcon.Error); | |||
} | |||
} | |||
catch (Exception ex) | |||
{ | |||
Console.WriteLine(ex); | |||
await _loadingSpinner.HideLoading(); | |||
dynamic error = JsonConvert.DeserializeObject<object>(await res.Content.ReadAsStringAsync()); | |||
await _swal.FireAsync( message: await ErrorMessage(res), icon: SweetAlertIcon.Error); | |||
Statics.CurrentLoginInfo = new(); | |||
} | |||
await _loadingSpinner.HideLoading(); | |||
return new HttpResponseMessage { StatusCode = HttpStatusCode.BadRequest }; | |||
} | |||
public async Task<HttpResponseMessage> HttpPut(string url, object model, bool hasLoading = true) | |||
{ | |||
Console.WriteLine("Put : " + url); | |||
var res = new HttpResponseMessage(); | |||
if (hasLoading) | |||
await _loadingSpinner.ShowLoading(); | |||
var http = await GetInstance(); | |||
try | |||
{ | |||
var content = new StringContent(JsonConvert.SerializeObject(model), Encoding.UTF8, "application/json"); | |||
res = await http.PutAsync(url, content); | |||
await _loadingSpinner.HideLoading(); | |||
if (res.IsSuccessStatusCode) | |||
{ | |||
return res; | |||
} | |||
if (res.StatusCode == HttpStatusCode.Unauthorized) | |||
{ | |||
await _loadingSpinner.HideLoading(); | |||
Statics.CurrentLoginInfo = new(); | |||
} | |||
else | |||
{ | |||
await _swal.FireAsync( message: await ErrorMessage(res), icon: SweetAlertIcon.Error); | |||
} | |||
} | |||
catch (Exception ex) | |||
{ | |||
Console.WriteLine(ex); | |||
await _loadingSpinner.HideLoading(); | |||
await _swal.FireAsync( message: await ErrorMessage(res), icon: SweetAlertIcon.Error); | |||
Statics.CurrentLoginInfo = new(); | |||
} | |||
await _loadingSpinner.HideLoading(); | |||
return new HttpResponseMessage { StatusCode = HttpStatusCode.BadRequest }; | |||
} | |||
public async Task<HttpResponseMessage> HttpPost(string url, object model, bool hasLoading = true, bool multiPart = false) | |||
{ | |||
Console.WriteLine("Post : " + url); | |||
var res = new HttpResponseMessage(); | |||
if (hasLoading) | |||
await _loadingSpinner.ShowLoading(); | |||
var http = await GetInstance(); | |||
try | |||
{ | |||
res = new HttpResponseMessage(); | |||
var content = new StringContent(JsonConvert.SerializeObject(model), Encoding.UTF8, "application/json"); | |||
res = await http.PostAsync(url, content); | |||
await _loadingSpinner.HideLoading(); | |||
if (res.IsSuccessStatusCode) | |||
{ | |||
return res; | |||
} | |||
if (res.StatusCode == HttpStatusCode.Unauthorized) | |||
{ | |||
await _loadingSpinner.HideLoading(); | |||
_navigationManager.NavigateTo("/"); | |||
} | |||
else | |||
{ | |||
await _swal.FireAsync( message: await ErrorMessage(res), icon: SweetAlertIcon.Error); | |||
} | |||
} | |||
catch (Exception ex) | |||
{ | |||
Console.WriteLine(ex); | |||
await _loadingSpinner.HideLoading(); | |||
} | |||
await _loadingSpinner.HideLoading(); | |||
return res; | |||
} | |||
private async Task<string> ErrorMessage(HttpResponseMessage res) | |||
{ | |||
var errStr = "خطای نامشخص"; | |||
dynamic error = JsonConvert.DeserializeObject<object>(await res.Content.ReadAsStringAsync()); | |||
if (error.errors != null) | |||
{ | |||
errStr = (string)("" + error.errors); | |||
if (errStr.Contains(":")) | |||
errStr = errStr.Split(':')[1].Replace(":", "").Replace("{", "") | |||
.Replace("}", "").Replace("[", "").Replace("]","").Replace("\"",""); | |||
} | |||
return errStr; | |||
} | |||
} | |||
public interface IHttpClientWithLoginToken | |||
{ | |||
Task<HttpClient> GetInstance(); | |||
Task<HttpResponseMessage> HttpPost(string url, object model, bool hasLoading = true, bool multiPart = false); | |||
Task<HttpResponseMessage> HttpGet(string url, bool hasLoading = true, bool goToLogin = true); | |||
Task<HttpResponseMessage> HttpDelete(string url, bool hasLoading = true); | |||
Task<HttpResponseMessage> HttpPut(string url, object model, bool hasLoading = true); | |||
} | |||
} |
@@ -0,0 +1,32 @@ | |||
using Microsoft.JSInterop; | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Linq; | |||
using System.Threading.Tasks; | |||
namespace AbrBlazorTools | |||
{ | |||
public class LoadingSpinner : ILoadingSpinner | |||
{ | |||
private readonly IJSRuntime _jSRuntime; | |||
public LoadingSpinner(IJSRuntime jSRuntime) | |||
{ | |||
_jSRuntime = jSRuntime; | |||
} | |||
public async Task HideLoading() | |||
{ | |||
await Task.Delay(1000); | |||
await _jSRuntime.InvokeVoidAsync("methods.HideLoading", null); | |||
} | |||
public async Task ShowLoading() | |||
{ | |||
await _jSRuntime.InvokeVoidAsync("methods.ShowLoading", null); | |||
} | |||
} | |||
public interface ILoadingSpinner | |||
{ | |||
Task ShowLoading(); | |||
Task HideLoading(); | |||
} | |||
} |
@@ -0,0 +1,18 @@ | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Linq; | |||
using System.Threading.Tasks; | |||
using CurrieTechnologies.Razor.SweetAlert2; | |||
namespace BlazorPages.Tools | |||
{ | |||
public interface ISwalUtils | |||
{ | |||
public Task<SweetAlertResult> ShowToast(string message); | |||
public Task<SweetAlertResult> ShowSwal(string title, string message, SweetAlertIcon icon); | |||
} | |||
} | |||
@@ -0,0 +1,20 @@ | |||
namespace BlazorPages.Tools | |||
{ | |||
public sealed class InfiniteScrollingItemsProviderRequest | |||
{ | |||
public InfiniteScrollingItemsProviderRequest(int startIndex, int page, CancellationToken cancellationToken) | |||
{ | |||
StartIndex = startIndex; | |||
CancellationToken = cancellationToken; | |||
Page = page; | |||
} | |||
public int StartIndex { get; } | |||
public CancellationToken CancellationToken { get; } | |||
public int Page { get; } | |||
} | |||
public delegate Task<IEnumerable<T>> ItemsProviderRequestDelegate<T>(InfiniteScrollingItemsProviderRequest request); | |||
} |
@@ -0,0 +1,72 @@ | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Globalization; | |||
using System.Linq; | |||
using System.Reflection; | |||
using System.Text.RegularExpressions; | |||
using System.Threading.Tasks; | |||
using Models.Identity; | |||
namespace BlazorPages.Tools | |||
{ | |||
public static class Statics | |||
{ | |||
public const string TableClass = "table table-striped table-bordered border"; | |||
public enum CultureType | |||
{ | |||
En, | |||
Fa | |||
} | |||
public static CultureType Culture = CultureType.Fa; | |||
public static string LoginTokenKey = "SignIn"; | |||
public static string RefreshTokenKey = "RefreshToken"; | |||
public static string Agent = ""; | |||
public static string FcmToken = "RefreshToken"; | |||
public static CheckLoginResponse CurrentLoginInfo { get; set; } | |||
public static DateTime? LastCheck { set; get; } | |||
public static bool IsFromMobileApp { get; internal set; } | |||
public static string BaseAddress = "https://api.senso.farm/"; | |||
//linux: | |||
//public static string BaseAddress = "https://localhost:5001/"; | |||
public static string ReplaceNewLineWithBr(this string text) | |||
{ | |||
var result = Regex.Replace(text, @"\r\n?|\n", "<br/>"); | |||
result = result.Replace("<br/><br/><br/>", "<br/>").Replace("<br/><br/>", "<br/>"); | |||
return result; | |||
} | |||
public static CultureInfo GetPersianCulture() | |||
{ | |||
var culture = new CultureInfo("fa-IR"); | |||
DateTimeFormatInfo formatInfo = culture.DateTimeFormat; | |||
formatInfo.AbbreviatedDayNames = new[] { "ی", "د", "س", "چ", "پ", "ج", "ش" }; | |||
formatInfo.DayNames = new[] { "یکشنبه", "دوشنبه", "سه شنبه", "چهار شنبه", "پنجشنبه", "جمعه", "شنبه" }; | |||
var monthNames = new[] | |||
{ | |||
"فروردین", "اردیبهشت", "خرداد", "تیر", "مرداد", "شهریور", "مهر", "آبان", "آذر", "دی", "بهمن", | |||
"اسفند", | |||
"", | |||
}; | |||
formatInfo.AbbreviatedMonthNames = | |||
formatInfo.MonthNames = | |||
formatInfo.MonthGenitiveNames = formatInfo.AbbreviatedMonthGenitiveNames = monthNames; | |||
formatInfo.AMDesignator = "ق.ظ"; | |||
formatInfo.PMDesignator = "ب.ظ"; | |||
formatInfo.ShortDatePattern = "yyyy/MM/dd"; | |||
formatInfo.LongDatePattern = "dddd, dd MMMM,yyyy"; | |||
formatInfo.FirstDayOfWeek = DayOfWeek.Saturday; | |||
Calendar cal = new PersianCalendar(); | |||
FieldInfo fieldInfo = culture.GetType().GetField("calendar", BindingFlags.NonPublic | BindingFlags.Instance); | |||
if (fieldInfo != null) | |||
fieldInfo.SetValue(culture, cal); | |||
FieldInfo info = formatInfo.GetType().GetField("calendar", BindingFlags.NonPublic | BindingFlags.Instance); | |||
if (info != null) | |||
info.SetValue(formatInfo, cal); | |||
culture.NumberFormat.NumberDecimalSeparator = "/"; | |||
culture.NumberFormat.DigitSubstitution = DigitShapes.NativeNational; | |||
culture.NumberFormat.NumberNegativePattern = 0; | |||
return culture; | |||
} | |||
} | |||
} |
@@ -0,0 +1,39 @@ | |||
using CurrieTechnologies.Razor.SweetAlert2; | |||
namespace AbrBlazorTools; | |||
public static class SwalExtensions | |||
{ | |||
public static async Task<SweetAlertResult> ShowToast(this SweetAlertService service, string message) | |||
{ | |||
return await service.FireAsync( | |||
new SweetAlertOptions | |||
{ | |||
Toast = true, | |||
Text = message, | |||
Timer = 5000 | |||
}); | |||
} | |||
public static async Task<SweetAlertResult> ShowSwal(this SweetAlertService service, string title, string message, SweetAlertIcon icon) | |||
{ | |||
if (icon == SweetAlertIcon.Question) | |||
return await service.FireAsync(new SweetAlertOptions | |||
{ | |||
ConfirmButtonText="Yes", | |||
ShowConfirmButton = true, | |||
ShowDenyButton = true, | |||
ShowCloseButton = false, | |||
Html = message, | |||
Icon = icon, | |||
Title = title, | |||
}); | |||
return await service.FireAsync(new SweetAlertOptions | |||
{ | |||
Html = message, | |||
Icon = icon, | |||
Title = title, | |||
}); | |||
} | |||
} |
@@ -0,0 +1,59 @@ | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Linq; | |||
using System.Threading.Tasks; | |||
using CurrieTechnologies.Razor.SweetAlert2; | |||
namespace BlazorPages.Tools | |||
{ | |||
public class SwalUtils : ISwalUtils | |||
{ | |||
private readonly SweetAlertService _service; | |||
public SwalUtils(SweetAlertService service) | |||
{ | |||
_service = service; | |||
} | |||
public async Task<SweetAlertResult> ShowToast(string message) | |||
{ | |||
return await _service.FireAsync( | |||
new SweetAlertOptions | |||
{ | |||
Toast = true, | |||
Text = message, | |||
Timer = 5000, | |||
ConfirmButtonText = "خُب" | |||
}); | |||
} | |||
public async Task<SweetAlertResult> ShowSwal(string title, string message, SweetAlertIcon icon) | |||
{ | |||
if (icon == SweetAlertIcon.Question) | |||
return await _service.FireAsync(new SweetAlertOptions | |||
{ | |||
ShowConfirmButton = true, | |||
CancelButtonColor = "Red", | |||
ShowCloseButton = false, | |||
ShowCancelButton = true, | |||
Html = message, | |||
Icon = icon, | |||
Title = title, | |||
CancelButtonText = "انصراف", | |||
ConfirmButtonText = "تائید" | |||
}); | |||
return await _service.FireAsync(new SweetAlertOptions | |||
{ | |||
ShowConfirmButton = false, | |||
ShowCloseButton = false, | |||
ShowCancelButton = true, | |||
Html = message, | |||
Icon = icon, | |||
Title = title, | |||
CancelButtonText = "خًب" | |||
}); | |||
} | |||
} | |||
} | |||
@@ -0,0 +1,29 @@ | |||
namespace BlazorPages.Tools | |||
{ | |||
public static class UriExtenssions | |||
{ | |||
public static string GetQueryStringValue(string uri, string key) | |||
{ | |||
try | |||
{ | |||
var parts = uri.Split("?", options: StringSplitOptions.RemoveEmptyEntries); | |||
if (parts.Length != 2) | |||
return ""; | |||
var qparts = parts[1].Split("&"); | |||
foreach (var kv in qparts) | |||
{ | |||
var k = kv.Split("=")[0]; | |||
var v = kv.Split("=")[1]; | |||
if (k.Equals(key, StringComparison.InvariantCultureIgnoreCase)) | |||
return v; | |||
} | |||
return ""; | |||
} | |||
catch (Exception) | |||
{ | |||
return ""; | |||
} | |||
} | |||
} | |||
} |
@@ -0,0 +1,24 @@ | |||
@using System.Net.Http | |||
@using System.Net.Http.Json | |||
@using BlazorPages.Tools | |||
@using Microsoft.AspNetCore.Components.Forms | |||
@using Microsoft.AspNetCore.Components.Routing | |||
@using Microsoft.AspNetCore.Components.Web | |||
@using Microsoft.AspNetCore.Components.Web.Virtualization | |||
@using Microsoft.AspNetCore.Components.WebAssembly.Http | |||
@using Microsoft.JSInterop | |||
@using AbrBlazorTools | |||
@using CurrieTechnologies.Razor.SweetAlert2 | |||
@using ApiUtils | |||
@using BlazorPages.Shared | |||
@using BootStrapComponents | |||
@using Newtonsoft.Json | |||
@inject Blazored.LocalStorage.ILocalStorageService LocalStorage | |||
@inject NavigationManager NavManager; | |||
@inject IJSRuntime JSRuntime | |||
@inject ISwalUtils SwalUtils | |||
@inject IBrowserTools BrowserTools | |||
@inject IUserUtils _userUtils | |||
@inject SweetAlertService _sweetAlertService | |||
@inject ILoadingSpinner LoadingSpinner | |||
@@ -0,0 +1,47 @@ | |||
| |||
.valid.modified:not([type=checkbox]) { | |||
outline: 1px solid #26b050; | |||
} | |||
.invalid { | |||
outline: 1px solid red; | |||
} | |||
.validation-message { | |||
color: red; | |||
} | |||
#blazor-error-ui { | |||
background: lightyellow; | |||
bottom: 0; | |||
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); | |||
display: none; | |||
left: 0; | |||
padding: 0.6rem 1.25rem 0.7rem 1.25rem; | |||
position: fixed; | |||
width: 100%; | |||
z-index: 1000; | |||
} | |||
#blazor-error-ui .dismiss { | |||
cursor: pointer; | |||
position: absolute; | |||
right: 0.75rem; | |||
top: 0.5rem; | |||
} | |||
.blazor-error-boundary { | |||
background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121; | |||
padding: 1rem 1rem 1rem 3.7rem; | |||
color: white; | |||
} | |||
.blazor-error-boundary::after { | |||
content: "An error has occurred." | |||
} | |||
.text-justify { | |||
text-align: justify; | |||
line-height: 1.8em; | |||
} |
@@ -0,0 +1,99 @@ | |||
@font-face { | |||
font-family: Vazir; | |||
src: url('h/assets/fonts/Vazirmatn-Regular'); | |||
src: url('/assets/fonts/Vazirmatn-Regular.eot?#iefix') format('embedded-opentype'), url('/assets/fonts/Vazirmatn-Regular.woff2') format('woff2'), url('/assets/fonts/Vazirmatn-Regular.woff') format('woff'), url('/assets/fonts/Vazirmatn-Regular.ttf') format('truetype'); | |||
font-weight: normal; | |||
font-display: swap; | |||
} | |||
@font-face { | |||
font-family: Vazir; | |||
src: url('/assets/fonts/Vazirmatn-Bold.eot'); | |||
src: url('/assets/fonts/Vazirmatn-Bold.eot?#iefix') format('embedded-opentype'), url('/assets/fonts/Vazirmatn-Bold.woff2') format('woff2'), url('/assets/fonts/Vazirmatn-Bold.woff') format('woff'), url('/assets/fonts/Vazirmatn-Bold.ttf') format('truetype'); | |||
font-weight: bold; | |||
font-display: swap; | |||
} | |||
@font-face { | |||
font-family: Vazir; | |||
src: url('/assets/fonts/Vazirmatn-Thin.eot'); | |||
src: url('/assets/fonts/Vazirmatn-Thin.eot?#iefix') format('embedded-opentype'), url('/assets/fonts/Vazirmatn-Thin.woff2') format('woff2'), url('/assets/fonts/Vazirmatn-Thin.woff') format('woff'), url('/assets/fonts/Vazirmatn-Thin.ttf') format('truetype'); | |||
font-weight: 100; | |||
font-display: swap; | |||
} | |||
@font-face { | |||
font-family: Vazir; | |||
src: url('/assets/fonts/Vazirmatn-Light.eot'); | |||
src: url('/assets/fonts/Vazirmatn-Light.eot?#iefix') format('embedded-opentype'), url('/assets/fonts/Vazirmatn-Light.woff2') format('woff2'), url('/assets/fonts/Vazirmatn-Light.woff') format('woff'), url('/assets/fonts/Vazirmatn-Light.ttf') format('truetype'); | |||
font-weight: 300; | |||
font-display: swap; | |||
} | |||
@font-face { | |||
font-family: Vazir; | |||
src: url('/assets/fonts/Vazirmatn-Medium.eot'); | |||
src: url('/assets/fonts/Vazirmatn-Medium.eot?#iefix') format('embedded-opentype'), url('/assets/fonts/Vazirmatn-Medium.woff2') format('woff2'), url('/assets/fonts/Vazirmatn-Medium.woff') format('woff'), url('/assets/fonts/Vazirmatn-Medium.ttf') format('truetype'); | |||
font-weight: 500; | |||
font-display: swap; | |||
} | |||
@font-face { | |||
font-family: Vazir; | |||
src: url('/assets/fonts/Vazirmatn-Black.eot'); | |||
src: url('/assets/fonts/Vazirmatn-Black.eot?#iefix') format('embedded-opentype'), url('/assets/fonts/Vazirmatn-Black.woff2') format('woff2'), url('/assets/fonts/Vazirmatn-Black.woff') format('woff'), url('/assets/fonts/Vazirmatn-Black.ttf') format('truetype'); | |||
font-weight: 900; | |||
font-display: swap; | |||
} | |||
* { | |||
font-family: Vazir !important; | |||
*font-size: 2vh; | |||
} | |||
*::-webkit-scrollbar { | |||
width: 8px; | |||
} | |||
*::-webkit-scrollbar-track { | |||
border-radius: 8px; | |||
background-color: #cacaca; | |||
border: 1px solid #cacaca; | |||
} | |||
::-webkit-scrollbar-thumb { | |||
border-radius: 6px; | |||
border: 3px solid transparent; | |||
background-clip: content-box; | |||
background-color: #e7e7e7; | |||
} | |||
.hamburger { | |||
width: 200px; | |||
height: 120px; | |||
position: relative; | |||
margin: 0 20px; | |||
} | |||
.hamburger { | |||
&::before, &::after | |||
{ | |||
content: ''; | |||
position: absolute; | |||
width: 100%; | |||
height: 20px; | |||
color: #333; | |||
background-color: currentColor; | |||
transition: all .45s ease-in-out; | |||
&::before | |||
{ | |||
top: 0; | |||
transform: rotate(0); | |||
} | |||
&::after { | |||
bottom: 0; | |||
box-shadow: 0 -50px currentColor; | |||
} | |||
} |
@@ -0,0 +1 @@ | |||
<?xml version="1.0" ?><!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'><svg height="32px" id="Layer_1" style="enable-background:new 0 0 32 32;" version="1.1" viewBox="0 0 32 32" width="32px" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M4,10h24c1.104,0,2-0.896,2-2s-0.896-2-2-2H4C2.896,6,2,6.896,2,8S2.896,10,4,10z M28,14H4c-1.104,0-2,0.896-2,2 s0.896,2,2,2h24c1.104,0,2-0.896,2-2S29.104,14,28,14z M28,22H4c-1.104,0-2,0.896-2,2s0.896,2,2,2h24c1.104,0,2-0.896,2-2 S29.104,22,28,22z"/></svg> |