Initial commit
This commit is contained in:
19
.idea/.idea.WebDesktop 2.0/.idea/dataSources.local.xml
generated
Normal file
19
.idea/.idea.WebDesktop 2.0/.idea/dataSources.local.xml
generated
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="dataSourceStorageLocal" created-in="RD-222.3962.23">
|
||||||
|
<data-source name="WebDesktop" uuid="95aba07a-0fe8-4ac6-bdce-406f8acafcd0">
|
||||||
|
<database-info product="MariaDB" version="10.3.34-MariaDB-0+deb10u1" jdbc-version="4.2" driver-name="MariaDB Connector/J" driver-version="2.7.3" dbms="MARIADB" exact-version="10.3.34" exact-driver-version="2.7">
|
||||||
|
<extra-name-characters>#@</extra-name-characters>
|
||||||
|
<identifier-quote-string>`</identifier-quote-string>
|
||||||
|
</database-info>
|
||||||
|
<case-sensitivity plain-identifiers="exact" quoted-identifiers="exact" />
|
||||||
|
<secret-storage>master_key</secret-storage>
|
||||||
|
<user-name>WebDesktop</user-name>
|
||||||
|
<schema-mapping>
|
||||||
|
<introspection-scope>
|
||||||
|
<node kind="schema" qname="@" />
|
||||||
|
</introspection-scope>
|
||||||
|
</schema-mapping>
|
||||||
|
</data-source>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
12
.idea/.idea.WebDesktop 2.0/.idea/dataSources.xml
generated
Normal file
12
.idea/.idea.WebDesktop 2.0/.idea/dataSources.xml
generated
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
|
||||||
|
<data-source source="LOCAL" name="WebDesktop" uuid="95aba07a-0fe8-4ac6-bdce-406f8acafcd0">
|
||||||
|
<driver-ref>mariadb</driver-ref>
|
||||||
|
<synchronize>true</synchronize>
|
||||||
|
<jdbc-driver>org.mariadb.jdbc.Driver</jdbc-driver>
|
||||||
|
<jdbc-url>jdbc:mariadb://213.136.89.237:3306/WebDesktop</jdbc-url>
|
||||||
|
<working-dir>$ProjectFileDir$</working-dir>
|
||||||
|
</data-source>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
13
.idea/.idea.WebDesktop 2.0/.idea/dataSources/95aba07a-0fe8-4ac6-bdce-406f8acafcd0.xml
generated
Normal file
13
.idea/.idea.WebDesktop 2.0/.idea/dataSources/95aba07a-0fe8-4ac6-bdce-406f8acafcd0.xml
generated
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<dataSource name="WebDesktop">
|
||||||
|
<database-model serializer="dbm" dbms="MARIADB" family-id="MARIADB" format-version="4.43">
|
||||||
|
<root id="1">
|
||||||
|
<DefaultCasing>exact</DefaultCasing>
|
||||||
|
<ServerVersion>10.3.34</ServerVersion>
|
||||||
|
</root>
|
||||||
|
<schema id="2" parent="1" name="WebDesktop">
|
||||||
|
<Current>1</Current>
|
||||||
|
</schema>
|
||||||
|
<schema id="3" parent="1" name="information_schema"/>
|
||||||
|
</database-model>
|
||||||
|
</dataSource>
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
#n:information_schema
|
||||||
|
!<md> [null, 0, null, null, -2147483648, -2147483648]
|
||||||
6
.idea/.idea.WebDesktop 2.0/.idea/dictionaries
generated
Normal file
6
.idea/.idea.WebDesktop 2.0/.idea/dictionaries
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectDictionaryState">
|
||||||
|
<dictionary name="leon" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
7
.idea/.idea.WebDesktop 2.0/.idea/discord.xml
generated
Normal file
7
.idea/.idea.WebDesktop 2.0/.idea/discord.xml
generated
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="DiscordProjectSettings">
|
||||||
|
<option name="show" value="PROJECT_FILES" />
|
||||||
|
<option name="description" value="" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
4
.idea/.idea.WebDesktop 2.0/.idea/encodings.xml
generated
Normal file
4
.idea/.idea.WebDesktop 2.0/.idea/encodings.xml
generated
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="Encoding" addBOMForNewFiles="with BOM under Windows, with no BOM otherwise" />
|
||||||
|
</project>
|
||||||
10
.idea/.idea.WebDesktop 2.0/.idea/indexLayout.xml
generated
Normal file
10
.idea/.idea.WebDesktop 2.0/.idea/indexLayout.xml
generated
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="UserContentModel">
|
||||||
|
<attachedFolders>
|
||||||
|
<Path>Frontend</Path>
|
||||||
|
</attachedFolders>
|
||||||
|
<explicitIncludes />
|
||||||
|
<explicitExcludes />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
9
.idea/.idea.WebDesktop 2.0/.idea/markdown.xml
generated
Normal file
9
.idea/.idea.WebDesktop 2.0/.idea/markdown.xml
generated
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="MarkdownSettings">
|
||||||
|
<enabledExtensions>
|
||||||
|
<entry key="MermaidLanguageExtension" value="false" />
|
||||||
|
<entry key="PlantUMLLanguageExtension" value="false" />
|
||||||
|
</enabledExtensions>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
6
.idea/.idea.WebDesktop 2.0/.idea/misc.xml
generated
Normal file
6
.idea/.idea.WebDesktop 2.0/.idea/misc.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="com.jetbrains.rider.android.RiderAndroidMiscFileCreationComponent">
|
||||||
|
<option name="ENSURE_MISC_FILE_EXISTS" value="true" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
6
.idea/.idea.WebDesktop 2.0/.idea/projectSettingsUpdater.xml
generated
Normal file
6
.idea/.idea.WebDesktop 2.0/.idea/projectSettingsUpdater.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="RiderProjectSettingsUpdater">
|
||||||
|
<option name="vcsConfiguration" value="2" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
6
.idea/.idea.WebDesktop 2.0/.idea/sqldialects.xml
generated
Normal file
6
.idea/.idea.WebDesktop 2.0/.idea/sqldialects.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="SqlDialectMappings">
|
||||||
|
<file url="file://$PROJECT_DIR$/Backend/DatabaseContext.cs" dialect="GenericSQL" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
6
.idea/.idea.WebDesktop 2.0/.idea/vcs.xml
generated
Normal file
6
.idea/.idea.WebDesktop 2.0/.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
159
.idea/.idea.WebDesktop 2.0/.idea/workspace.xml
generated
Normal file
159
.idea/.idea.WebDesktop 2.0/.idea/workspace.xml
generated
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="AutoGeneratedRunConfigurationManager">
|
||||||
|
<projectFile profileName="Backend">Backend/Backend.csproj</projectFile>
|
||||||
|
<projectFile profileName="IIS Express">Backend/Backend.csproj</projectFile>
|
||||||
|
</component>
|
||||||
|
<component name="ChangeListManager">
|
||||||
|
<list default="true" id="041c7675-58ae-4243-af88-0d29855e558f" name="Changes" comment="">
|
||||||
|
<change beforePath="$PROJECT_DIR$/.idea/.idea.WebDesktop 2.0/.idea/vcs.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/.idea.WebDesktop 2.0/.idea/vcs.xml" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/.idea/.idea.WebDesktop 2.0/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/.idea.WebDesktop 2.0/.idea/workspace.xml" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/Backend/Controllers/UserController.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Backend/Controllers/UserController.cs" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/Backend/Security/Permissions.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Backend/Security/Permissions.cs" afterDir="false" />
|
||||||
|
</list>
|
||||||
|
<option name="SHOW_DIALOG" value="false" />
|
||||||
|
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||||
|
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
|
||||||
|
<option name="LAST_RESOLUTION" value="IGNORE" />
|
||||||
|
</component>
|
||||||
|
<component name="FileTemplateManagerImpl">
|
||||||
|
<option name="RECENT_TEMPLATES">
|
||||||
|
<list>
|
||||||
|
<option value="TypeScript File" />
|
||||||
|
</list>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
<component name="Git.Settings">
|
||||||
|
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
|
||||||
|
</component>
|
||||||
|
<component name="HighlightingSettingsPerFile">
|
||||||
|
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/123735509fa19d358d3971345e1872b6d26687d1b343ad7ca37af43930694c/Guid.cs" root0="SKIP_HIGHLIGHTING" />
|
||||||
|
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/efd3e9c4749621d4a53b45d7117d2a581e02a265c444a040f1387a155db/Dictionary.cs" root0="SKIP_HIGHLIGHTING" />
|
||||||
|
</component>
|
||||||
|
<component name="MarkdownSettingsMigration">
|
||||||
|
<option name="stateVersion" value="1" />
|
||||||
|
</component>
|
||||||
|
<component name="ProjectId" id="2E2lWgCQKiR5ds6nCyp7WbN5Oq7" />
|
||||||
|
<component name="ProjectLevelVcsManager" settingsEditedManually="true">
|
||||||
|
<ConfirmationsSetting value="2" id="Add" />
|
||||||
|
</component>
|
||||||
|
<component name="ProjectViewState">
|
||||||
|
<option name="hideEmptyMiddlePackages" value="true" />
|
||||||
|
<option name="showLibraryContents" value="true" />
|
||||||
|
</component>
|
||||||
|
<component name="PropertiesComponent"><![CDATA[{
|
||||||
|
"keyToString": {
|
||||||
|
"ASKED_ADD_EXTERNAL_FILES": "true",
|
||||||
|
"RunOnceActivity.OpenProjectViewOnStart": "true",
|
||||||
|
"RunOnceActivity.ShowReadmeOnStart": "true",
|
||||||
|
"SHARE_PROJECT_CONFIGURATION_FILES": "true",
|
||||||
|
"WebServerToolWindowFactoryState": "false",
|
||||||
|
"nodejs_package_manager_path": "npm",
|
||||||
|
"settings.editor.selected.configurable": "project.propVCSSupport.DirectoryMappings",
|
||||||
|
"ts.external.directory.path": "D:\\Programmierstuff\\Projekte\\WebDesktop 2.0\\Frontend\\node_modules\\typescript\\lib",
|
||||||
|
"vue.rearranger.settings.migration": "true"
|
||||||
|
},
|
||||||
|
"keyToStringList": {
|
||||||
|
"DatabaseDriversLRU": [
|
||||||
|
"mariadb"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}]]></component>
|
||||||
|
<component name="RunManager" selected=".NET Launch Settings Profile.Backend">
|
||||||
|
<configuration name="Backend" type="LaunchSettings" factoryName=".NET Launch Settings Profile">
|
||||||
|
<option name="LAUNCH_PROFILE_PROJECT_FILE_PATH" value="$PROJECT_DIR$/Backend/Backend.csproj" />
|
||||||
|
<option name="LAUNCH_PROFILE_TFM" value="net6.0" />
|
||||||
|
<option name="LAUNCH_PROFILE_NAME" value="Backend" />
|
||||||
|
<option name="USE_EXTERNAL_CONSOLE" value="0" />
|
||||||
|
<option name="USE_MONO" value="0" />
|
||||||
|
<option name="RUNTIME_ARGUMENTS" value="" />
|
||||||
|
<option name="GENERATE_APPLICATIONHOST_CONFIG" value="1" />
|
||||||
|
<option name="SHOW_IIS_EXPRESS_OUTPUT" value="0" />
|
||||||
|
<option name="SEND_DEBUG_REQUEST" value="1" />
|
||||||
|
<option name="ADDITIONAL_IIS_EXPRESS_ARGUMENTS" value="" />
|
||||||
|
<method v="2">
|
||||||
|
<option name="Build" />
|
||||||
|
</method>
|
||||||
|
</configuration>
|
||||||
|
<configuration name="Start Containers" type="ShConfigurationType">
|
||||||
|
<option name="SCRIPT_TEXT" value="docker-compose up" />
|
||||||
|
<option name="INDEPENDENT_SCRIPT_PATH" value="true" />
|
||||||
|
<option name="SCRIPT_PATH" value="" />
|
||||||
|
<option name="SCRIPT_OPTIONS" value="" />
|
||||||
|
<option name="INDEPENDENT_SCRIPT_WORKING_DIRECTORY" value="true" />
|
||||||
|
<option name="SCRIPT_WORKING_DIRECTORY" value="$PROJECT_DIR$" />
|
||||||
|
<option name="INDEPENDENT_INTERPRETER_PATH" value="true" />
|
||||||
|
<option name="INTERPRETER_PATH" value="" />
|
||||||
|
<option name="INTERPRETER_OPTIONS" value="" />
|
||||||
|
<option name="EXECUTE_IN_TERMINAL" value="true" />
|
||||||
|
<option name="EXECUTE_SCRIPT_FILE" value="false" />
|
||||||
|
<envs />
|
||||||
|
<method v="2" />
|
||||||
|
</configuration>
|
||||||
|
<configuration name="Frontend" type="js.build_tools.npm">
|
||||||
|
<package-json value="$PROJECT_DIR$/Frontend/package.json" />
|
||||||
|
<command value="run" />
|
||||||
|
<scripts>
|
||||||
|
<script value="start" />
|
||||||
|
</scripts>
|
||||||
|
<node-interpreter value="project" />
|
||||||
|
<envs />
|
||||||
|
<method v="2" />
|
||||||
|
</configuration>
|
||||||
|
<configuration name="Test" type="js.build_tools.npm">
|
||||||
|
<package-json value="$PROJECT_DIR$/Frontend/package.json" />
|
||||||
|
<command value="run" />
|
||||||
|
<scripts>
|
||||||
|
<script value="test" />
|
||||||
|
</scripts>
|
||||||
|
<node-interpreter value="project" />
|
||||||
|
<envs />
|
||||||
|
<method v="2" />
|
||||||
|
</configuration>
|
||||||
|
<list>
|
||||||
|
<item itemvalue=".NET Launch Settings Profile.Backend" />
|
||||||
|
<item itemvalue="npm.Test" />
|
||||||
|
<item itemvalue="npm.Frontend" />
|
||||||
|
<item itemvalue="Shell Script.Start Containers" />
|
||||||
|
</list>
|
||||||
|
</component>
|
||||||
|
<component name="SpellCheckerSettings" RuntimeDictionaries="0" Folders="21" Folder0="D:\Spelling\de-DE" Folder1="D:\Spelling\de-DE" Folder2="D:\Spelling\de-DE" Folder3="D:\Spelling\de-DE" Folder4="D:\Spelling\de-DE" Folder5="D:\Spelling\de-DE" Folder6="D:\Spelling\de-DE" Folder7="D:\Spelling\de-DE" Folder8="D:\Spelling\de-DE" Folder9="D:\Spelling\de-DE" Folder10="D:\Spelling\de-DE" Folder11="D:\Spelling\de-DE" Folder12="D:\Spelling\de-DE" Folder13="D:\Spelling\de-DE" Folder14="D:\Spelling\de-DE" Folder15="D:\Spelling\de-DE" Folder16="D:\Spelling\de-DE" Folder17="D:\Spelling\de-DE" Folder18="D:\Spelling\de-DE" Folder19="D:\Spelling\de-DE" Folder20="D:\Spelling\de-DE" CustomDictionaries="21" CustomDictionary0="D:\Spelling\de-DE\abkuerzungen.dic" CustomDictionary1="D:\Spelling\de-DE\astronomie.dic" CustomDictionary2="D:\Spelling\de-DE\biologie.dic" CustomDictionary3="D:\Spelling\de-DE\chemie.dic" CustomDictionary4="D:\Spelling\de-DE\computer.dic" CustomDictionary5="D:\Spelling\de-DE\de_alt.dic" CustomDictionary6="D:\Spelling\de-DE\de_neu.dic" CustomDictionary7="D:\Spelling\de-DE\elektronic.dic" CustomDictionary8="D:\Spelling\de-DE\geographie.dic" CustomDictionary9="D:\Spelling\de-DE\geologie.dic" CustomDictionary10="D:\Spelling\de-DE\informatik.dic" CustomDictionary11="D:\Spelling\de-DE\mathematik.dic" CustomDictionary12="D:\Spelling\de-DE\medizin.dic" CustomDictionary13="D:\Spelling\de-DE\namen.dic" CustomDictionary14="D:\Spelling\de-DE\organisationen.dic" CustomDictionary15="D:\Spelling\de-DE\physik.dic" CustomDictionary16="D:\Spelling\de-DE\recht.dic" CustomDictionary17="D:\Spelling\de-DE\remove.dic" CustomDictionary18="D:\Spelling\de-DE\tex_de.dic" CustomDictionary19="D:\Spelling\de-DE\vornamen.dic" CustomDictionary20="D:\Spelling\de-DE\wirtschaft.dic" DefaultDictionary="application-level" UseSingleDictionary="true" transferred="true" />
|
||||||
|
<component name="TaskManager">
|
||||||
|
<task active="true" id="Default" summary="Default task">
|
||||||
|
<changelist id="041c7675-58ae-4243-af88-0d29855e558f" name="Changes" comment="" />
|
||||||
|
<created>1661801592204</created>
|
||||||
|
<option name="number" value="Default" />
|
||||||
|
<option name="presentableId" value="Default" />
|
||||||
|
<updated>1661801592204</updated>
|
||||||
|
<workItem from="1661801603705" duration="2784000" />
|
||||||
|
<workItem from="1661876909316" duration="8486000" />
|
||||||
|
<workItem from="1662126115999" duration="9904000" />
|
||||||
|
<workItem from="1662204907636" duration="15449000" />
|
||||||
|
<workItem from="1662228779224" duration="302000" />
|
||||||
|
</task>
|
||||||
|
<servers />
|
||||||
|
</component>
|
||||||
|
<component name="TypeScriptGeneratedFilesManager">
|
||||||
|
<option name="version" value="3" />
|
||||||
|
</component>
|
||||||
|
<component name="UnityCheckinConfiguration" checkUnsavedScenes="true" />
|
||||||
|
<component name="UnityUnitTestConfiguration" currentTestLauncher="NUnit" />
|
||||||
|
<component name="Vcs.Log.Tabs.Properties">
|
||||||
|
<option name="TAB_STATES">
|
||||||
|
<map>
|
||||||
|
<entry key="MAIN">
|
||||||
|
<value>
|
||||||
|
<State />
|
||||||
|
</value>
|
||||||
|
</entry>
|
||||||
|
</map>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
<component name="VcsManagerConfiguration">
|
||||||
|
<option name="ADD_EXTERNAL_FILES_SILENTLY" value="true" />
|
||||||
|
<ignored-roots>
|
||||||
|
<path value="$PROJECT_DIR$/../.." />
|
||||||
|
</ignored-roots>
|
||||||
|
<option name="CLEAR_INITIAL_COMMIT_MESSAGE" value="true" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
25
Backend/.dockerignore
Normal file
25
Backend/.dockerignore
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
**/.dockerignore
|
||||||
|
**/.env
|
||||||
|
**/.git
|
||||||
|
**/.gitignore
|
||||||
|
**/.project
|
||||||
|
**/.settings
|
||||||
|
**/.toolstarget
|
||||||
|
**/.vs
|
||||||
|
**/.vscode
|
||||||
|
**/.idea
|
||||||
|
**/*.*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
|
||||||
2
Backend/.gitignore
vendored
Normal file
2
Backend/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
obj
|
||||||
|
bin
|
||||||
14
Backend/Backend.csproj
Normal file
14
Backend/Backend.csproj
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net6.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="MySql.EntityFrameworkCore" Version="6.0.4" />
|
||||||
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.2.3" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
20
Backend/Controllers/StartupController.cs
Normal file
20
Backend/Controllers/StartupController.cs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace Backend.Controllers;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
public class TestController : ControllerBase {
|
||||||
|
|
||||||
|
private DatabaseContext _context;
|
||||||
|
|
||||||
|
public TestController(DatabaseContext context) {
|
||||||
|
_context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("")]
|
||||||
|
public IActionResult InitializeDb() {
|
||||||
|
_context.ExecuteTableCreation();
|
||||||
|
return Ok("OK");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
98
Backend/Controllers/UserController.cs
Normal file
98
Backend/Controllers/UserController.cs
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
using Backend.Entitys;
|
||||||
|
using Backend.Logic;
|
||||||
|
using Backend.LogicResults;
|
||||||
|
using Backend.Security;
|
||||||
|
using Backend.Security.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace Backend.Controllers;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("users")]
|
||||||
|
public class UserController : ControllerBase {
|
||||||
|
|
||||||
|
private readonly UserLogic _logic;
|
||||||
|
private readonly ITokenContext _context;
|
||||||
|
|
||||||
|
public UserController(UserLogic logic, ITokenContext context) {
|
||||||
|
_logic = logic;
|
||||||
|
_context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
[Authorized(Permissions.ShowUsers)]
|
||||||
|
public ActionResult<IEnumerable<User>> GetUsers() {
|
||||||
|
return this.FromLogicResult(_logic.GetUsers());
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPut("login")]
|
||||||
|
public ActionResult<AccessToken> Login([FromBody] UserLogin login) {
|
||||||
|
return this.FromLogicResult(_logic.Login(login));
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("register")]
|
||||||
|
public ActionResult<AccessToken> Register([FromBody] UserEditor editor) {
|
||||||
|
return this.FromLogicResult(_logic.Register(editor));
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("token")]
|
||||||
|
[Authorized]
|
||||||
|
public ActionResult<AccessToken> GetToken() {
|
||||||
|
return this.FromLogicResult(_logic.GenerateToken(_logic.GetCurrentUserRefreshToken()));
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("{userId}")]
|
||||||
|
[Authorized(Permissions.ShowUsers)]
|
||||||
|
public ActionResult<User> GetUser(Guid userId) {
|
||||||
|
return this.FromLogicResult(_logic.GetUser(userId));
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("self")]
|
||||||
|
[Authorized]
|
||||||
|
public ActionResult<User> GetOwnUser() {
|
||||||
|
return this.FromLogicResult(_logic.GetUser(_context.UserId));
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPut("{userId}")]
|
||||||
|
[Authorized(Permissions.EditUsers)]
|
||||||
|
public ActionResult EditUser(Guid userId, [FromBody] UserEditor editor) {
|
||||||
|
return this.FromLogicResult(_logic.EditUser(userId, editor));
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpDelete("{userId}")]
|
||||||
|
[Authorized(Permissions.DeleteUsers)]
|
||||||
|
public ActionResult DeleteUser(Guid userId) {
|
||||||
|
return this.FromLogicResult(_logic.DeleteUser(userId));
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("{userId}/permissions")]
|
||||||
|
[Authorized(Permissions.ShowUserPermissions)]
|
||||||
|
public ActionResult<IEnumerable<string>> GetUserPermissions(Guid userId) {
|
||||||
|
return this.FromLogicResult(_logic.GetPermissions(userId));
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("{userId}/permissions/raw")]
|
||||||
|
[Authorized(Permissions.ShowUserPermissions)]
|
||||||
|
public ActionResult<IEnumerable<string>> GetUserPermissionsRaw(Guid userId) {
|
||||||
|
return this.FromLogicResult(_logic.GetPermissionsRaw(userId));
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("{userId}/permissions")]
|
||||||
|
[Authorized(Permissions.EditUserPermissions, Permissions.EditOwnPermissions)]
|
||||||
|
public ActionResult AddUserPermissions(Guid userId, [FromBody] string[] permissions) {
|
||||||
|
return this.FromLogicResult(_logic.AddPermissions(userId, permissions));
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPut("{userId}/permissions")]
|
||||||
|
[Authorized(Permissions.EditUserPermissions, Permissions.EditOwnPermissions)]
|
||||||
|
public ActionResult DeleteUserPermissions(Guid userId, [FromBody] string[] permissions) {
|
||||||
|
return this.FromLogicResult(_logic.DeletePermissions(userId, permissions));
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpDelete("{userId}/logout")]
|
||||||
|
[Authorized(Permissions.LogoutUsers)]
|
||||||
|
public ActionResult Logout(Guid userId) {
|
||||||
|
return this.FromLogicResult(_logic.Logout(userId));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
65
Backend/DatabaseContext.cs
Normal file
65
Backend/DatabaseContext.cs
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
using Backend.Entitys;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace Backend;
|
||||||
|
|
||||||
|
public class DatabaseContext : DbContext {
|
||||||
|
private string _connectionString;
|
||||||
|
|
||||||
|
public DbSet<User> Users { get; set; }
|
||||||
|
public DbSet<RefreshToken> RefreshTokens { get; set; }
|
||||||
|
public DbSet<AccessToken> AccessTokens { get; set; }
|
||||||
|
public DbSet<Permission> Permissions { get; set; }
|
||||||
|
|
||||||
|
public DatabaseContext(IConfiguration configuration) {
|
||||||
|
_connectionString = configuration.GetSection("MySQL").Get<string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) {
|
||||||
|
if (string.IsNullOrEmpty(_connectionString))
|
||||||
|
throw new ArgumentException("MySQL Connection String was not defined correctly in the Configuration!");
|
||||||
|
|
||||||
|
optionsBuilder.UseMySQL(_connectionString);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnModelCreating(ModelBuilder modelBuilder) {
|
||||||
|
base.OnModelCreating(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity<User>(entry => {
|
||||||
|
entry.HasKey(e => e.Id);
|
||||||
|
entry.Property(e => e.FirstName);
|
||||||
|
entry.Property(e => e.LastName);
|
||||||
|
entry.Property(e => e.Email);
|
||||||
|
entry.Property(e => e.Username);
|
||||||
|
entry.Property(e => e.Password);
|
||||||
|
entry.Property(e => e.Created);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity<RefreshToken>(entry => {
|
||||||
|
entry.HasKey(e => e.Id);
|
||||||
|
entry.Property(e => e.UserId);
|
||||||
|
entry.Property(e => e.ExpirationDate);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity<AccessToken>(entry => {
|
||||||
|
entry.HasKey(e => e.Id);
|
||||||
|
entry.Property(e => e.RefreshTokenId);
|
||||||
|
entry.Property(e => e.ExpirationDate);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity<Permission>(entry => {
|
||||||
|
entry.HasKey(e => e.Id);
|
||||||
|
entry.Property(e => e.Id).ValueGeneratedOnAdd();
|
||||||
|
|
||||||
|
entry.Property(e => e.UserId);
|
||||||
|
entry.Property(e => e.PermissionKey);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ExecuteTableCreation() {
|
||||||
|
Database.ExecuteSqlRaw("CREATE TABLE IF NOT EXISTS Users (Id VARCHAR(50) PRIMARY KEY, FirstName VARCHAR(255), LastName VARCHAR(255), Email VARCHAR(255), Username VARCHAR(255), Password VARCHAR(255), Created TIMESTAMP)");
|
||||||
|
Database.ExecuteSqlRaw("CREATE TABLE IF NOT EXISTS RefreshTokens (Id VARCHAR(50) PRIMARY KEY, UserId VARCHAR(50), ExpirationDate TIMESTAMP)");
|
||||||
|
Database.ExecuteSqlRaw("CREATE TABLE IF NOT EXISTS AccessTokens (Id VARCHAR(50) PRIMARY KEY, RefreshTokenId VARCHAR(50), ExpirationDate TIMESTAMP)");
|
||||||
|
Database.ExecuteSqlRaw("CREATE TABLE IF NOT EXISTS Permissions (Id INT PRIMARY KEY AUTO_INCREMENT, UserId VARCHAR(50), PermissionName VARCHAR(100))");
|
||||||
|
}
|
||||||
|
}
|
||||||
20
Backend/Dockerfile
Normal file
20
Backend/Dockerfile
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base
|
||||||
|
WORKDIR /app
|
||||||
|
EXPOSE 80
|
||||||
|
EXPOSE 443
|
||||||
|
|
||||||
|
FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
|
||||||
|
WORKDIR /src
|
||||||
|
COPY ["Backend.csproj", ""]
|
||||||
|
RUN dotnet restore "Backend.csproj"
|
||||||
|
COPY . .
|
||||||
|
WORKDIR "/src"
|
||||||
|
RUN dotnet build "Backend.csproj" -c Release -o /app/build
|
||||||
|
|
||||||
|
FROM build AS publish
|
||||||
|
RUN dotnet publish "Backend.csproj" -c Release -o /app/publish
|
||||||
|
|
||||||
|
FROM base AS final
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=publish /app/publish .
|
||||||
|
ENTRYPOINT ["dotnet", "Backend.dll"]
|
||||||
16
Backend/Entitys/Permission.cs
Normal file
16
Backend/Entitys/Permission.cs
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Backend.Entitys;
|
||||||
|
|
||||||
|
public class Permission {
|
||||||
|
public int Id { get; set; }
|
||||||
|
public Guid UserId { get; set; }
|
||||||
|
public string PermissionKey { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class PermissionGroup {
|
||||||
|
public string Permission { get; set; }
|
||||||
|
public string Name { get; set; }
|
||||||
|
public string[] Permissions { get; set; }
|
||||||
|
public string[] Inherits { get; set; }
|
||||||
|
}
|
||||||
20
Backend/Entitys/Tokens.cs
Normal file
20
Backend/Entitys/Tokens.cs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Backend.Entitys;
|
||||||
|
|
||||||
|
public class Tokens {
|
||||||
|
public AccessToken AccessToken { get; set; }
|
||||||
|
public RefreshToken RefreshToken { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class RefreshToken {
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
public Guid UserId { get; set; }
|
||||||
|
public DateTime ExpirationDate { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class AccessToken {
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
public Guid RefreshTokenId { get; set; }
|
||||||
|
public DateTime ExpirationDate { get; set; }
|
||||||
|
}
|
||||||
21
Backend/Entitys/User.cs
Normal file
21
Backend/Entitys/User.cs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Backend.Entitys;
|
||||||
|
|
||||||
|
public class User : UserEditor {
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
public DateTime Created { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class UserLogin {
|
||||||
|
public string UsernameOrEmail { get; set; }
|
||||||
|
public string Password { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class UserEditor {
|
||||||
|
public string FirstName { get; set; }
|
||||||
|
public string LastName { get; set; }
|
||||||
|
public string Email { get; set; }
|
||||||
|
public string Username { get; set; }
|
||||||
|
public string Password { get; set; }
|
||||||
|
}
|
||||||
213
Backend/Logic/UserLogic.cs
Normal file
213
Backend/Logic/UserLogic.cs
Normal file
@@ -0,0 +1,213 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using Backend.Entitys;
|
||||||
|
using Backend.LogicResults;
|
||||||
|
using Backend.Options;
|
||||||
|
using Backend.Repositorys;
|
||||||
|
using Backend.Security;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace Backend.Logic;
|
||||||
|
|
||||||
|
public class UserLogic {
|
||||||
|
private UserRepository _users;
|
||||||
|
private TokenRepository _tokens;
|
||||||
|
private GroupRepository _groups;
|
||||||
|
private UserMessageOptions _messages;
|
||||||
|
private IHttpContextAccessor _contextAccessor;
|
||||||
|
private ITokenContext _context;
|
||||||
|
|
||||||
|
public UserLogic(
|
||||||
|
UserRepository users,
|
||||||
|
TokenRepository tokens,
|
||||||
|
GroupRepository groups,
|
||||||
|
IOptions<UserMessageOptions> messages,
|
||||||
|
IHttpContextAccessor contextAccessor,
|
||||||
|
ITokenContext context) {
|
||||||
|
_users = users;
|
||||||
|
_tokens = tokens;
|
||||||
|
_groups = groups;
|
||||||
|
_messages = messages.Value;
|
||||||
|
_contextAccessor = contextAccessor;
|
||||||
|
_context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ILogicResult<IEnumerable<User>> GetUsers() {
|
||||||
|
var users = _users.GetUsers()
|
||||||
|
.Select(user => new User {
|
||||||
|
Id = user.Id,
|
||||||
|
Created = user.Created,
|
||||||
|
Email = user.Email,
|
||||||
|
FirstName = user.FirstName,
|
||||||
|
LastName = user.LastName,
|
||||||
|
Username = user.Username
|
||||||
|
});
|
||||||
|
|
||||||
|
return LogicResult<IEnumerable<User>>.Ok(users);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ILogicResult<User> GetUser(Guid userId) {
|
||||||
|
var user = _users.GetUser(userId);
|
||||||
|
if (user is null) return LogicResult<User>.NotFound(_messages.NotFound);
|
||||||
|
|
||||||
|
return LogicResult<User>.Ok(new User {
|
||||||
|
Id = user.Id,
|
||||||
|
Created = user.Created,
|
||||||
|
Email = user.Email,
|
||||||
|
FirstName = user.FirstName,
|
||||||
|
LastName = user.LastName,
|
||||||
|
Username = user.Username
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public ILogicResult EditUser(Guid userId, UserEditor editor) {
|
||||||
|
if (!ValidateEdit(editor)) return LogicResult.BadRequest(_messages.InvalidEditData);
|
||||||
|
if (_users.GetUser(userId) is null) return LogicResult.NotFound(_messages.NotFound);
|
||||||
|
|
||||||
|
_users.EditUser(userId, editor);
|
||||||
|
return LogicResult.Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ILogicResult DeleteUser(Guid userId) {
|
||||||
|
if (_users.GetUser(userId) is null) return LogicResult.NotFound(_messages.NotFound);
|
||||||
|
|
||||||
|
_tokens.DeleteUserTokens(userId);
|
||||||
|
_users.DeleteUser(userId);
|
||||||
|
|
||||||
|
return LogicResult.Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ILogicResult<IEnumerable<string>> GetPermissions(Guid userId) {
|
||||||
|
if (_users.GetUser(userId) is null) return LogicResult<IEnumerable<string>>.NotFound(_messages.NotFound);
|
||||||
|
|
||||||
|
return LogicResult<IEnumerable<string>>.Ok(_groups.GetUserPermissions(userId).Select(perm => perm.PermissionKey));
|
||||||
|
}
|
||||||
|
|
||||||
|
public ILogicResult<IEnumerable<string>> GetPermissionsRaw(Guid userId) {
|
||||||
|
if (_users.GetUser(userId) is null) return LogicResult<IEnumerable<string>>.NotFound(_messages.NotFound);
|
||||||
|
|
||||||
|
return LogicResult<IEnumerable<string>>.Ok(_groups.GetUserPermissionsRaw(userId).Select(perm => perm.PermissionKey));
|
||||||
|
}
|
||||||
|
|
||||||
|
public ILogicResult AddPermissions(Guid userId, params string[] permissions) {
|
||||||
|
if (_users.GetUser(userId) is null) return LogicResult.NotFound(_messages.NotFound);
|
||||||
|
|
||||||
|
_groups.AddPermissions(userId, permissions);
|
||||||
|
return LogicResult.Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ILogicResult DeletePermissions(Guid userId, params string[] permissions) {
|
||||||
|
if (_users.GetUser(userId) is null) return LogicResult.NotFound(_messages.NotFound);
|
||||||
|
|
||||||
|
_groups.DeletePermissions(userId, permissions);
|
||||||
|
return LogicResult.Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ILogicResult<AccessToken> Login(UserLogin login) {
|
||||||
|
var user = _users.GetUsers().SingleOrDefault(user =>
|
||||||
|
user.Username == login.UsernameOrEmail || user.Email == login.UsernameOrEmail);
|
||||||
|
|
||||||
|
if (user is null) return LogicResult<AccessToken>.NotFound(_messages.NotFound);
|
||||||
|
if (user.Password != TokenRepository.Hash128(login.Password, user.Email))
|
||||||
|
return LogicResult<AccessToken>.BadRequest(_messages.WrongPassword);
|
||||||
|
|
||||||
|
_tokens.DeleteUserTokens(user.Id);
|
||||||
|
var refreshToken = _tokens.CreateRefreshToken(user.Id);
|
||||||
|
var accessToken = _tokens.CreateAccessToken(refreshToken.Id);
|
||||||
|
SetRefreshToken(refreshToken);
|
||||||
|
|
||||||
|
return LogicResult<AccessToken>.Ok(accessToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ILogicResult<AccessToken> Register(UserEditor editor) {
|
||||||
|
var users = _users.GetUsers();
|
||||||
|
if (users.Any(user => user.Email == editor.Email || user.Username == editor.Username))
|
||||||
|
return LogicResult<AccessToken>.Conflict(_messages.UsernameOrEmailExist);
|
||||||
|
|
||||||
|
if (!ValidateUserdata(editor))
|
||||||
|
return LogicResult<AccessToken>.BadRequest(_messages.InvalidRegisterData);
|
||||||
|
|
||||||
|
var user = _users.CreateUser(editor);
|
||||||
|
var refreshToken = _tokens.CreateRefreshToken(user.Id);
|
||||||
|
var accessToken = _tokens.CreateAccessToken(refreshToken.Id);
|
||||||
|
SetRefreshToken(refreshToken);
|
||||||
|
|
||||||
|
return LogicResult<AccessToken>.Ok(accessToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ILogicResult Logout(Guid userId) {
|
||||||
|
if (_users.GetUser(userId) is null) return LogicResult.NotFound(_messages.NotFound);
|
||||||
|
_tokens.DeleteUserTokens(userId);
|
||||||
|
|
||||||
|
if (_context.UserId == userId) DeleteRefreshToken();
|
||||||
|
return LogicResult.Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ILogicResult<AccessToken> GenerateToken(Guid refreshTokenId) {
|
||||||
|
if (!_tokens.ValidateRefreshToken(refreshTokenId)) return LogicResult<AccessToken>.Conflict(_messages.InvalidRefreshToken);
|
||||||
|
var token = _tokens.GetAccessTokens(refreshTokenId).ToArray().FirstOrDefault(token => _tokens.ValidateAccessToken(token.Id));
|
||||||
|
if (token is not null) return LogicResult<AccessToken>.Ok(token);
|
||||||
|
return LogicResult<AccessToken>.Ok(_tokens.CreateAccessToken(refreshTokenId));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Guid GetCurrentUserRefreshToken() {
|
||||||
|
var token = _contextAccessor.HttpContext?.Request.Cookies["refresh_token"];
|
||||||
|
if (token == null) return Guid.Empty;
|
||||||
|
return Guid.Parse(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool ValidateUserdata(UserEditor editor) {
|
||||||
|
if (string.IsNullOrEmpty(editor.FirstName)) return false;
|
||||||
|
if (string.IsNullOrEmpty(editor.LastName)) return false;
|
||||||
|
if (string.IsNullOrEmpty(editor.Email)) return false;
|
||||||
|
if (string.IsNullOrEmpty(editor.Username)) return false;
|
||||||
|
if (string.IsNullOrEmpty(editor.Password)) return false;
|
||||||
|
|
||||||
|
if (editor.FirstName.Length > 255) return false;
|
||||||
|
if (editor.LastName.Length > 255) return false;
|
||||||
|
if (editor.Email.Length > 255) return false;
|
||||||
|
if (editor.Username.Length > 255) return false;
|
||||||
|
if (editor.Password.Length > 255) return false;
|
||||||
|
|
||||||
|
if (!editor.Email.Contains('@') || !editor.Email.Contains('.')) return false;
|
||||||
|
if (editor.Username.Contains('@')) return false;
|
||||||
|
if (editor.Password.Length < 8) return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
private bool ValidateEdit(UserEditor editor) {
|
||||||
|
if (editor.FirstName?.Length > 255) return false;
|
||||||
|
if (editor.LastName?.Length > 255) return false;
|
||||||
|
if (editor.Email?.Length > 255) return false;
|
||||||
|
if (editor.Username?.Length > 255) return false;
|
||||||
|
if (editor.Password?.Length > 255) return false;
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(editor.Email)) {
|
||||||
|
if (!editor.Email.Contains('@') || !editor.Email.Contains('.')) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(editor.Username)) {
|
||||||
|
if (editor.Username.Contains('@')) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(editor.Password)) {
|
||||||
|
if (editor.Password.Length < 8) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void DeleteRefreshToken() {
|
||||||
|
_contextAccessor.HttpContext?.Response.Cookies.Delete("refresh_token");
|
||||||
|
}
|
||||||
|
private void SetRefreshToken(RefreshToken token) {
|
||||||
|
_contextAccessor.HttpContext?.Response.Cookies.Append("refresh_token", token.Id.ToString(), new CookieOptions {
|
||||||
|
MaxAge = token.ExpirationDate - DateTime.Now,
|
||||||
|
HttpOnly = true,
|
||||||
|
Secure = true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
51
Backend/LogicResults/ControllerBaseExtention.cs
Normal file
51
Backend/LogicResults/ControllerBaseExtention.cs
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
using System;
|
||||||
|
using System.Net;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace Backend.LogicResults {
|
||||||
|
public static class ControllerBaseExtention {
|
||||||
|
public static ActionResult FromLogicResult(this ControllerBase controller, ILogicResult result) {
|
||||||
|
switch (result.State) {
|
||||||
|
case LogicResultState.Ok:
|
||||||
|
return controller.Ok();
|
||||||
|
|
||||||
|
case LogicResultState.BadRequest:
|
||||||
|
return controller.StatusCode((int)HttpStatusCode.BadRequest, result.Message);
|
||||||
|
|
||||||
|
case LogicResultState.Forbidden:
|
||||||
|
return controller.StatusCode((int)HttpStatusCode.Forbidden, result.Message);
|
||||||
|
|
||||||
|
case LogicResultState.NotFound:
|
||||||
|
return controller.StatusCode((int)HttpStatusCode.NotFound, result.Message);
|
||||||
|
|
||||||
|
case LogicResultState.Conflict:
|
||||||
|
return controller.StatusCode((int)HttpStatusCode.Conflict, result.Message);
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new Exception("An unhandled result has occurred as a result of a service call.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ActionResult FromLogicResult<T>(this ControllerBase controller, ILogicResult<T> result) {
|
||||||
|
switch (result.State) {
|
||||||
|
case LogicResultState.Ok:
|
||||||
|
return controller.Ok(result.Data);
|
||||||
|
|
||||||
|
case LogicResultState.BadRequest:
|
||||||
|
return controller.StatusCode((int)HttpStatusCode.BadRequest, result.Message);
|
||||||
|
|
||||||
|
case LogicResultState.Forbidden:
|
||||||
|
return controller.StatusCode((int)HttpStatusCode.Forbidden, result.Message);
|
||||||
|
|
||||||
|
case LogicResultState.NotFound:
|
||||||
|
return controller.StatusCode((int)HttpStatusCode.NotFound, result.Message);
|
||||||
|
|
||||||
|
case LogicResultState.Conflict:
|
||||||
|
return controller.StatusCode((int)HttpStatusCode.Conflict, result.Message);
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new Exception("An unhandled result has occurred as a result of a service call.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
19
Backend/LogicResults/ILogicResult.cs
Normal file
19
Backend/LogicResults/ILogicResult.cs
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
namespace Backend.LogicResults {
|
||||||
|
public interface ILogicResult {
|
||||||
|
LogicResultState State { get; set; }
|
||||||
|
|
||||||
|
string Message { get; set; }
|
||||||
|
|
||||||
|
bool IsSuccessful { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface ILogicResult<T> {
|
||||||
|
LogicResultState State { get; set; }
|
||||||
|
|
||||||
|
T Data { get; set; }
|
||||||
|
|
||||||
|
string Message { get; set; }
|
||||||
|
|
||||||
|
bool IsSuccessful { get; }
|
||||||
|
}
|
||||||
|
}
|
||||||
170
Backend/LogicResults/LogicResult.cs
Normal file
170
Backend/LogicResults/LogicResult.cs
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
namespace Backend.LogicResults {
|
||||||
|
internal class LogicResult : ILogicResult {
|
||||||
|
public LogicResultState State { get; set; }
|
||||||
|
|
||||||
|
public string Message { get; set; }
|
||||||
|
|
||||||
|
public bool IsSuccessful => State == LogicResultState.Ok;
|
||||||
|
|
||||||
|
public static LogicResult Ok() {
|
||||||
|
return new LogicResult() {
|
||||||
|
State = LogicResultState.Ok
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static LogicResult BadRequest() {
|
||||||
|
return new LogicResult() {
|
||||||
|
State = LogicResultState.BadRequest
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static LogicResult BadRequest(string message) {
|
||||||
|
return new LogicResult() {
|
||||||
|
State = LogicResultState.BadRequest,
|
||||||
|
Message = message
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static LogicResult Forbidden() {
|
||||||
|
return new LogicResult() {
|
||||||
|
State = LogicResultState.Forbidden
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static LogicResult Forbidden(string message) {
|
||||||
|
return new LogicResult() {
|
||||||
|
State = LogicResultState.Forbidden,
|
||||||
|
Message = message
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static LogicResult NotFound() {
|
||||||
|
return new LogicResult() {
|
||||||
|
State = LogicResultState.NotFound
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static LogicResult NotFound(string message) {
|
||||||
|
return new LogicResult() {
|
||||||
|
State = LogicResultState.NotFound,
|
||||||
|
Message = message
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static LogicResult Conflict() {
|
||||||
|
return new LogicResult() {
|
||||||
|
State = LogicResultState.Conflict
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static LogicResult Conflict(string message) {
|
||||||
|
return new LogicResult() {
|
||||||
|
State = LogicResultState.Conflict,
|
||||||
|
Message = message
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static LogicResult Forward(LogicResult result) {
|
||||||
|
return new LogicResult() {
|
||||||
|
State = result.State,
|
||||||
|
Message = result.Message
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static LogicResult Forward<T>(ILogicResult<T> result) {
|
||||||
|
return new LogicResult() {
|
||||||
|
State = result.State,
|
||||||
|
Message = result.Message
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class LogicResult<T> : ILogicResult<T> {
|
||||||
|
public LogicResultState State { get; set; }
|
||||||
|
|
||||||
|
public T Data { get; set; }
|
||||||
|
|
||||||
|
public string Message { get; set; }
|
||||||
|
|
||||||
|
public bool IsSuccessful => State == LogicResultState.Ok;
|
||||||
|
|
||||||
|
public static LogicResult<T> Ok() {
|
||||||
|
return new LogicResult<T>() {
|
||||||
|
State = LogicResultState.Ok
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static LogicResult<T> Ok(T result) {
|
||||||
|
return new LogicResult<T>() {
|
||||||
|
State = LogicResultState.Ok,
|
||||||
|
Data = result
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static LogicResult<T> BadRequest() {
|
||||||
|
return new LogicResult<T>() {
|
||||||
|
State = LogicResultState.BadRequest
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static LogicResult<T> BadRequest(string message) {
|
||||||
|
return new LogicResult<T>() {
|
||||||
|
State = LogicResultState.BadRequest,
|
||||||
|
Message = message
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static LogicResult<T> Forbidden() {
|
||||||
|
return new LogicResult<T>() {
|
||||||
|
State = LogicResultState.Forbidden
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static LogicResult<T> Forbidden(string message) {
|
||||||
|
return new LogicResult<T>() {
|
||||||
|
State = LogicResultState.Forbidden,
|
||||||
|
Message = message
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static LogicResult<T> NotFound() {
|
||||||
|
return new LogicResult<T>() {
|
||||||
|
State = LogicResultState.NotFound
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static LogicResult<T> NotFound(string message) {
|
||||||
|
return new LogicResult<T>() {
|
||||||
|
State = LogicResultState.NotFound,
|
||||||
|
Message = message
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static LogicResult<T> Conflict() {
|
||||||
|
return new LogicResult<T>() {
|
||||||
|
State = LogicResultState.Conflict
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static LogicResult<T> Conflict(string message) {
|
||||||
|
return new LogicResult<T>() {
|
||||||
|
State = LogicResultState.Conflict,
|
||||||
|
Message = message
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static LogicResult<T> Forward(ILogicResult result) {
|
||||||
|
return new LogicResult<T>() {
|
||||||
|
State = result.State,
|
||||||
|
Message = result.Message
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static LogicResult<T> Forward<T2>(ILogicResult<T2> result) {
|
||||||
|
return new LogicResult<T>() {
|
||||||
|
State = result.State,
|
||||||
|
Message = result.Message
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
9
Backend/LogicResults/LogicResultState.cs
Normal file
9
Backend/LogicResults/LogicResultState.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
namespace Backend.LogicResults {
|
||||||
|
public enum LogicResultState {
|
||||||
|
Ok,
|
||||||
|
BadRequest,
|
||||||
|
Forbidden,
|
||||||
|
NotFound,
|
||||||
|
Conflict
|
||||||
|
}
|
||||||
|
}
|
||||||
12
Backend/Options/UserMessageOptions.cs
Normal file
12
Backend/Options/UserMessageOptions.cs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
namespace Backend.Options;
|
||||||
|
|
||||||
|
public class UserMessageOptions : OptionsFromConfiguration {
|
||||||
|
public override string Position => "Messages:Users";
|
||||||
|
|
||||||
|
public string NotFound { get; set; }
|
||||||
|
public string InvalidEditData { get; set; }
|
||||||
|
public string InvalidRegisterData { get; set; }
|
||||||
|
public string WrongPassword { get; set; }
|
||||||
|
public string UsernameOrEmailExist { get; set; }
|
||||||
|
public string InvalidRefreshToken { get; set; }
|
||||||
|
}
|
||||||
7
Backend/Options/UserOptions.cs
Normal file
7
Backend/Options/UserOptions.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
namespace Backend.Options;
|
||||||
|
|
||||||
|
public class UserOptions : OptionsFromConfiguration {
|
||||||
|
public override string Position => "Users";
|
||||||
|
|
||||||
|
public string[] DefaultPermissions { get; set; }
|
||||||
|
}
|
||||||
79
Backend/Program.cs
Normal file
79
Backend/Program.cs
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
using Backend;
|
||||||
|
using Backend.Logic;
|
||||||
|
using Backend.Options;
|
||||||
|
using Backend.Repositorys;
|
||||||
|
using Backend.Security;
|
||||||
|
using Backend.Security.Authentication;
|
||||||
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||||
|
using Microsoft.OpenApi.Models;
|
||||||
|
|
||||||
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
|
builder.Services.AddDbContext<DatabaseContext>();
|
||||||
|
builder.Services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
|
||||||
|
builder.Services.AddScoped<ITokenContext, TokenContext>();
|
||||||
|
|
||||||
|
builder.Services.AddScoped<TokenRepository>();
|
||||||
|
builder.Services.AddScoped<GroupRepository>();
|
||||||
|
builder.Services.AddScoped<UserRepository>();
|
||||||
|
|
||||||
|
builder.Services.AddScoped<UserLogic>();
|
||||||
|
|
||||||
|
builder.Services.AddOptionsFromConfiguration<JwtTokenAuthenticationOptions>(builder.Configuration);
|
||||||
|
builder.Services.AddOptionsFromConfiguration<UserOptions>(builder.Configuration);
|
||||||
|
builder.Services.AddOptionsFromConfiguration<UserMessageOptions>(builder.Configuration);
|
||||||
|
|
||||||
|
builder.Services.AddCors();
|
||||||
|
builder.Services.AddAuthentication(JwtTokenAuthentication.Scheme).AddJwtTokenAuthentication(builder.Configuration);
|
||||||
|
builder.Services.AddControllers();
|
||||||
|
|
||||||
|
builder.Services.AddEndpointsApiExplorer();
|
||||||
|
builder.Services.AddSwaggerGen(c => {
|
||||||
|
c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme {
|
||||||
|
Description = @"JWT Authorization header using the Bearer scheme. \r\n\r\n
|
||||||
|
Enter 'Bearer' [space] and then your token in the text input below.",
|
||||||
|
Name = "Authorization",
|
||||||
|
In = ParameterLocation.Header,
|
||||||
|
Type = SecuritySchemeType.ApiKey,
|
||||||
|
Scheme = "Bearer"
|
||||||
|
});
|
||||||
|
|
||||||
|
c.AddSecurityRequirement(new OpenApiSecurityRequirement {{
|
||||||
|
new OpenApiSecurityScheme {
|
||||||
|
Reference = new OpenApiReference
|
||||||
|
{
|
||||||
|
Type = ReferenceType.SecurityScheme,
|
||||||
|
Id = "Bearer"
|
||||||
|
},
|
||||||
|
Scheme = "oauth2",
|
||||||
|
Name = "Bearer",
|
||||||
|
In = ParameterLocation.Header,
|
||||||
|
},
|
||||||
|
ArraySegment<string>.Empty
|
||||||
|
}});
|
||||||
|
});
|
||||||
|
|
||||||
|
var app = builder.Build();
|
||||||
|
|
||||||
|
GroupRepository.CompileGroups(app.Configuration);
|
||||||
|
|
||||||
|
if (app.Environment.IsDevelopment()) {
|
||||||
|
app.UseSwagger();
|
||||||
|
app.UseSwaggerUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
app.UseCors(
|
||||||
|
options => options
|
||||||
|
.WithOrigins(app.Configuration.GetSection("Origins").Get<string[]>())
|
||||||
|
.AllowAnyMethod()
|
||||||
|
.AllowAnyHeader()
|
||||||
|
.AllowCredentials()
|
||||||
|
);
|
||||||
|
|
||||||
|
app.UseRouting();
|
||||||
|
app.UseAuthentication();
|
||||||
|
app.UseAuthorization();
|
||||||
|
app.UseWebSockets();
|
||||||
|
|
||||||
|
app.MapControllers();
|
||||||
|
app.Run();
|
||||||
23
Backend/Properties/launchSettings.json
Normal file
23
Backend/Properties/launchSettings.json
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||||
|
"iisSettings": {
|
||||||
|
"windowsAuthentication": false,
|
||||||
|
"anonymousAuthentication": true,
|
||||||
|
"iisExpress": {
|
||||||
|
"applicationUrl": "http://localhost:42992",
|
||||||
|
"sslPort": 44301
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"profiles": {
|
||||||
|
"Backend": {
|
||||||
|
"commandName": "Project",
|
||||||
|
"dotnetRunMessages": true,
|
||||||
|
"launchBrowser": false,
|
||||||
|
"launchUrl": "swagger",
|
||||||
|
"applicationUrl": "http://localhost:5142",
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
94
Backend/Repositorys/GroupRepository.cs
Normal file
94
Backend/Repositorys/GroupRepository.cs
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
using Backend.Entitys;
|
||||||
|
|
||||||
|
namespace Backend.Repositorys;
|
||||||
|
|
||||||
|
public class GroupRepository {
|
||||||
|
public static PermissionGroup[] Groups;
|
||||||
|
|
||||||
|
public static void CompileGroups(IConfiguration configuration) {
|
||||||
|
var groupsSections = configuration.GetSection("Groups").GetChildren();
|
||||||
|
List<PermissionGroup> groups = new List<PermissionGroup>();
|
||||||
|
foreach (var section in groupsSections) {
|
||||||
|
PermissionGroup group = new PermissionGroup();
|
||||||
|
group.Name = section.GetValue<string>("Name");
|
||||||
|
group.Permission = section.GetValue<string>("Permission");
|
||||||
|
group.Permissions = section.GetSection("Permissions").Get<string[]>();
|
||||||
|
group.Inherits = section.GetSection("Inherits").Get<string[]>();
|
||||||
|
groups.Add(group);
|
||||||
|
}
|
||||||
|
Groups = groups.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly DatabaseContext _context;
|
||||||
|
private readonly PermissionGroup[] _groups;
|
||||||
|
|
||||||
|
public GroupRepository(DatabaseContext context) {
|
||||||
|
_context = context;
|
||||||
|
_groups = Groups;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PermissionGroup GetPermissionGroup(string name) {
|
||||||
|
return _groups.SingleOrDefault(group => group.Permission.Equals(name));
|
||||||
|
}
|
||||||
|
|
||||||
|
public PermissionGroup[] GetGroupsFromUser(Guid userId) {
|
||||||
|
Permission[] permissions = GetUserPermissionsRaw(userId).ToArray();
|
||||||
|
return ExtractGroups(permissions);
|
||||||
|
}
|
||||||
|
|
||||||
|
public PermissionGroup[] ExtractGroups(Permission[] permissions) {
|
||||||
|
List<PermissionGroup> permissionGroups = new List<PermissionGroup>();
|
||||||
|
foreach (var permission in permissions) {
|
||||||
|
if (permission.PermissionKey.StartsWith("group.")) {
|
||||||
|
foreach (var permissionGroup in _groups) {
|
||||||
|
if (permission.PermissionKey.Equals(permissionGroup.Permission)) {
|
||||||
|
permissionGroups.Add(permissionGroup);
|
||||||
|
|
||||||
|
if (permissionGroup.Inherits is not null) {
|
||||||
|
foreach (var inherit in permissionGroup.Inherits) {
|
||||||
|
permissionGroups.Add(GetPermissionGroup(inherit));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return permissionGroups.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<Permission> GetUserPermissions(Guid userId) {
|
||||||
|
List<Permission> permissions = GetUserPermissionsRaw(userId).ToList();
|
||||||
|
|
||||||
|
PermissionGroup[] groups = ExtractGroups(permissions.ToArray());
|
||||||
|
foreach (var group in groups) {
|
||||||
|
if (group.Permissions is null) continue;
|
||||||
|
permissions.AddRange(group.Permissions
|
||||||
|
.Select(perm => new Permission { Id = -1, UserId = userId, PermissionKey = perm }));
|
||||||
|
}
|
||||||
|
|
||||||
|
return permissions;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<Permission> GetUserPermissionsRaw(Guid userId) {
|
||||||
|
return _context.Permissions.Where(permission => permission.UserId == userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddPermissions(Guid userId, params string[] permissions) {
|
||||||
|
foreach (var permission in permissions) {
|
||||||
|
_context.Permissions.Add(new Permission
|
||||||
|
{ PermissionKey = permission, UserId = userId });
|
||||||
|
}
|
||||||
|
|
||||||
|
_context.SaveChanges();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DeletePermissions(Guid userId, params string[] permissions) {
|
||||||
|
foreach (var permission in permissions) {
|
||||||
|
_context.Permissions.RemoveRange(_context.Permissions.Where(perm =>
|
||||||
|
perm.UserId == userId && perm.PermissionKey == permission));
|
||||||
|
}
|
||||||
|
|
||||||
|
_context.SaveChanges();
|
||||||
|
}
|
||||||
|
}
|
||||||
92
Backend/Repositorys/TokenRepository.cs
Normal file
92
Backend/Repositorys/TokenRepository.cs
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
using System.Text;
|
||||||
|
using Backend.Entitys;
|
||||||
|
using Backend.Security.Authentication;
|
||||||
|
using Microsoft.AspNetCore.Cryptography.KeyDerivation;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace Backend.Repositorys;
|
||||||
|
|
||||||
|
public class TokenRepository {
|
||||||
|
private readonly JwtTokenAuthenticationOptions _options;
|
||||||
|
private readonly DatabaseContext _context;
|
||||||
|
|
||||||
|
public TokenRepository(IOptions<JwtTokenAuthenticationOptions> options, DatabaseContext context) {
|
||||||
|
_options = options.Value;
|
||||||
|
_context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
public RefreshToken GetRefreshToken(Guid refreshTokenId) {
|
||||||
|
if (string.IsNullOrEmpty(refreshTokenId.ToString())) return null;
|
||||||
|
return _context.RefreshTokens.SingleOrDefault(token => token.Id == refreshTokenId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public AccessToken GetAccessToken(Guid accessTokenId) {
|
||||||
|
if (string.IsNullOrEmpty(accessTokenId.ToString())) return null;
|
||||||
|
return _context.AccessTokens.SingleOrDefault(token => token.Id == accessTokenId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<AccessToken> GetAccessTokens(Guid refreshTokenId) {
|
||||||
|
if (string.IsNullOrEmpty(refreshTokenId.ToString())) return ArraySegment<AccessToken>.Empty;
|
||||||
|
return _context.AccessTokens.Where(token => token.RefreshTokenId == refreshTokenId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool ValidateAccessToken(Guid accessTokenId) {
|
||||||
|
AccessToken token = GetAccessToken(accessTokenId);
|
||||||
|
if (token == null) return false;
|
||||||
|
TimeSpan span = token.ExpirationDate - DateTime.Now;
|
||||||
|
return span.TotalMilliseconds > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool ValidateRefreshToken(Guid refreshTokenId) {
|
||||||
|
RefreshToken token = GetRefreshToken(refreshTokenId);
|
||||||
|
if (token == null) return false;
|
||||||
|
TimeSpan span = token.ExpirationDate - DateTime.Now;
|
||||||
|
return span.TotalMilliseconds > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public RefreshToken CreateRefreshToken(Guid userId) {
|
||||||
|
RefreshToken token = new RefreshToken {
|
||||||
|
UserId = userId, Id = Guid.NewGuid(),
|
||||||
|
ExpirationDate = DateTime.Now.Add(new TimeSpan(int.Parse(_options.RefreshTokenExpirationTimeInHours), 0, 0))
|
||||||
|
};
|
||||||
|
_context.RefreshTokens.Add(token);
|
||||||
|
_context.SaveChanges();
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AccessToken CreateAccessToken(Guid refreshTokenId) {
|
||||||
|
AccessToken token = new AccessToken {
|
||||||
|
RefreshTokenId = refreshTokenId, Id = Guid.NewGuid(),
|
||||||
|
ExpirationDate = DateTime.Now
|
||||||
|
.Add(new TimeSpan(0, int.Parse(_options.AccessTokenExpirationTimeInMinutes), 0))
|
||||||
|
};
|
||||||
|
_context.AccessTokens.Add(token);
|
||||||
|
_context.SaveChanges();
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DeleteUserTokens(Guid userId) {
|
||||||
|
List<RefreshToken> refreshTokens = _context.RefreshTokens.Where(token => token.UserId == userId).ToList();
|
||||||
|
refreshTokens.ForEach(token => DeleteRefreshToken(token.Id));
|
||||||
|
_context.SaveChanges();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DeleteRefreshToken(Guid refreshTokenId) {
|
||||||
|
_context.RefreshTokens.RemoveRange(_context.RefreshTokens.Where(token => token.Id == refreshTokenId));
|
||||||
|
_context.AccessTokens.RemoveRange(_context.AccessTokens.Where(token => token.RefreshTokenId == refreshTokenId));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string Hash128(string plainText, string salt) {
|
||||||
|
try {
|
||||||
|
string hashed = Convert.ToBase64String(KeyDerivation.Pbkdf2(
|
||||||
|
password: plainText,
|
||||||
|
salt: Encoding.Default.GetBytes(salt),
|
||||||
|
prf: KeyDerivationPrf.HMACSHA256,
|
||||||
|
iterationCount: 100000,
|
||||||
|
numBytesRequested: 256 / 8
|
||||||
|
));
|
||||||
|
|
||||||
|
return hashed;
|
||||||
|
} catch (Exception) { return ""; }
|
||||||
|
}
|
||||||
|
}
|
||||||
68
Backend/Repositorys/UserRepository.cs
Normal file
68
Backend/Repositorys/UserRepository.cs
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
using Backend.Entitys;
|
||||||
|
using Backend.Options;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace Backend.Repositorys;
|
||||||
|
|
||||||
|
public class UserRepository {
|
||||||
|
private DatabaseContext _context;
|
||||||
|
private TokenRepository _tokens;
|
||||||
|
private GroupRepository _groups;
|
||||||
|
private UserOptions _options;
|
||||||
|
|
||||||
|
public UserRepository(DatabaseContext context, TokenRepository tokens, GroupRepository groups, IOptions<UserOptions> options) {
|
||||||
|
_context = context;
|
||||||
|
_tokens = tokens;
|
||||||
|
_groups = groups;
|
||||||
|
_options = options.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<User> GetUsers() => _context.Users.OrderBy(user => user.Created);
|
||||||
|
|
||||||
|
public User GetUser(Guid userId) => _context.Users.SingleOrDefault(user => user.Id == userId);
|
||||||
|
|
||||||
|
public User CreateUser(UserEditor editor) {
|
||||||
|
var user = new User {
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
Created = DateTime.Now,
|
||||||
|
|
||||||
|
Email = editor.Email,
|
||||||
|
FirstName = editor.FirstName,
|
||||||
|
LastName = editor.LastName,
|
||||||
|
Password = TokenRepository.Hash128(editor.Password, editor.Email),
|
||||||
|
Username = editor.Username
|
||||||
|
};
|
||||||
|
|
||||||
|
_context.Users.Add(user);
|
||||||
|
_context.SaveChanges();
|
||||||
|
_groups.AddPermissions(user.Id, _options.DefaultPermissions);
|
||||||
|
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void EditUser(Guid userId, UserEditor editor) {
|
||||||
|
var user = GetUser(userId);
|
||||||
|
|
||||||
|
string SetValue(string orig, string input, string hashed = null) {
|
||||||
|
if (!string.IsNullOrEmpty(input))
|
||||||
|
return !string.IsNullOrEmpty(hashed) ? hashed : input;
|
||||||
|
|
||||||
|
return orig;
|
||||||
|
}
|
||||||
|
|
||||||
|
user.Email = SetValue(user.Email, editor.Email);
|
||||||
|
user.FirstName = SetValue(user.FirstName, editor.FirstName);
|
||||||
|
user.LastName = SetValue(user.LastName, editor.LastName);
|
||||||
|
user.Username = SetValue(user.Username, editor.Username);
|
||||||
|
user.Password = SetValue(user.Password, editor.Password, TokenRepository.Hash128(editor.Password, editor.Email));
|
||||||
|
|
||||||
|
_context.SaveChanges();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DeleteUser(Guid userId) {
|
||||||
|
_context.Users.Remove(_context.Users.Single(user => user.Id == userId));
|
||||||
|
_context.Permissions.RemoveRange(_context.Permissions.Where(perm => perm.UserId == userId));
|
||||||
|
_tokens.DeleteUserTokens(userId);
|
||||||
|
_context.SaveChanges();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
namespace Backend.Security.Authentication {
|
||||||
|
public static class JwtTokenAuthentication {
|
||||||
|
public const string Scheme = "JwtTokenAuthentication";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
using Backend.Options;
|
||||||
|
using Microsoft.AspNetCore.Authentication;
|
||||||
|
|
||||||
|
namespace Backend.Security.Authentication {
|
||||||
|
public static class JwtTokenAuthenticationExtensions {
|
||||||
|
public static AuthenticationBuilder AddJwtTokenAuthentication(this AuthenticationBuilder builder,
|
||||||
|
IConfiguration configuration) {
|
||||||
|
builder.Services.AddOptionsFromConfiguration<JwtTokenAuthenticationOptions>(configuration);
|
||||||
|
|
||||||
|
return builder.AddScheme<JwtTokenAuthenticationHandlerOptions, JwtTokenAuthenticationHandler>(
|
||||||
|
JwtTokenAuthentication.Scheme,
|
||||||
|
_ => { });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
using Microsoft.AspNetCore.Authentication;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using System.Text.Encodings.Web;
|
||||||
|
using System.Security.Claims;
|
||||||
|
using Backend.Entitys;
|
||||||
|
using Backend.Repositorys;
|
||||||
|
using Backend.Security.Authorization;
|
||||||
|
|
||||||
|
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
|
||||||
|
|
||||||
|
namespace Backend.Security.Authentication {
|
||||||
|
public class JwtTokenAuthenticationHandler : AuthenticationHandler<JwtTokenAuthenticationHandlerOptions> {
|
||||||
|
private readonly TokenRepository _tokens;
|
||||||
|
private readonly GroupRepository _groups;
|
||||||
|
private readonly JwtTokenAuthenticationOptions _options;
|
||||||
|
|
||||||
|
public JwtTokenAuthenticationHandler(
|
||||||
|
IOptionsMonitor<JwtTokenAuthenticationHandlerOptions> options,
|
||||||
|
ILoggerFactory logger,
|
||||||
|
UrlEncoder encoder,
|
||||||
|
ISystemClock clock,
|
||||||
|
IOptions<JwtTokenAuthenticationOptions> tokenOptions,
|
||||||
|
TokenRepository tokens,
|
||||||
|
GroupRepository groups)
|
||||||
|
: base(options, logger, encoder, clock) {
|
||||||
|
_options = tokenOptions.Value;
|
||||||
|
_tokens = tokens;
|
||||||
|
_groups = groups;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task<AuthenticateResult> HandleAuthenticateAsync() {
|
||||||
|
if (Request.Headers["Authorization"].Equals(_options.DebugAccessToken))
|
||||||
|
return AuthenticateResult.Success(GetAuthenticationTicket(null, null, "*"));
|
||||||
|
|
||||||
|
var accessToken = GetAccessToken();
|
||||||
|
if (accessToken == null) return AuthenticateResult.Fail("Access Token invalid");
|
||||||
|
var refreshToken = _tokens.GetRefreshToken(accessToken.RefreshTokenId);
|
||||||
|
if (refreshToken == null) return AuthenticateResult.Fail("Refresh Token invalid");
|
||||||
|
if (!_tokens.ValidateRefreshToken(refreshToken.Id)) return AuthenticateResult.Fail("Refresh Token invalid");
|
||||||
|
bool valid = _tokens.ValidateAccessToken(accessToken.Id);
|
||||||
|
return valid
|
||||||
|
? AuthenticateResult.Success(GetAuthenticationTicket(accessToken, refreshToken))
|
||||||
|
: AuthenticateResult.Fail("Access Token invalid");
|
||||||
|
}
|
||||||
|
|
||||||
|
private AuthenticationTicket GetAuthenticationTicket(AccessToken accessToken, RefreshToken refreshToken, params string[] customPerms) {
|
||||||
|
List<Claim> claims = GenerateClaims(accessToken, refreshToken, customPerms);
|
||||||
|
ClaimsPrincipal principal = new ClaimsPrincipal();
|
||||||
|
principal.AddIdentity(new ClaimsIdentity(claims, JwtTokenAuthentication.Scheme));
|
||||||
|
AuthenticationTicket ticket = new AuthenticationTicket(principal, Scheme.Name);
|
||||||
|
return ticket;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Claim> GenerateClaims(AccessToken accessToken, RefreshToken refreshToken, params string[] customPerms) {
|
||||||
|
List<Claim> claims = new List<Claim>();
|
||||||
|
if (accessToken is not null && refreshToken is not null) {
|
||||||
|
claims.AddRange(new List<Claim> {
|
||||||
|
new(CustomClaimTypes.AccessTokenId, accessToken.Id.ToString()),
|
||||||
|
new(CustomClaimTypes.RefreshTokenId, refreshToken.Id.ToString()),
|
||||||
|
new(CustomClaimTypes.UserId, refreshToken.UserId.ToString()),
|
||||||
|
});
|
||||||
|
|
||||||
|
string[] permissions = _groups.GetUserPermissions(refreshToken.UserId).Select(perm => perm.PermissionKey).ToArray();
|
||||||
|
claims.AddRange(permissions
|
||||||
|
.Select(permission => new Claim(CustomClaimTypes.Permission, permission)));
|
||||||
|
}
|
||||||
|
|
||||||
|
claims.AddRange(customPerms.Select(perm => new Claim(CustomClaimTypes.Permission, perm)));
|
||||||
|
|
||||||
|
return claims;
|
||||||
|
}
|
||||||
|
|
||||||
|
private AccessToken GetAccessToken() {
|
||||||
|
string key = Request.Headers["Authorization"];
|
||||||
|
if (string.IsNullOrEmpty(key)) {
|
||||||
|
key = Request.Query["token"];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(key))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
AccessToken token = _tokens.GetAccessToken(Guid.Parse(key));
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
using Microsoft.AspNetCore.Authentication;
|
||||||
|
|
||||||
|
namespace Backend.Security.Authentication {
|
||||||
|
public class JwtTokenAuthenticationHandlerOptions : AuthenticationSchemeOptions {
|
||||||
|
// Options for the authentication handler.
|
||||||
|
// Currently: None
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
using Backend.Options;
|
||||||
|
|
||||||
|
namespace Backend.Security.Authentication {
|
||||||
|
public class JwtTokenAuthenticationOptions : OptionsFromConfiguration {
|
||||||
|
public override string Position => "Authentication";
|
||||||
|
|
||||||
|
public string RefreshTokenExpirationTimeInHours { get; set; }
|
||||||
|
public string AccessTokenExpirationTimeInMinutes { get; set; }
|
||||||
|
public string DebugAccessToken { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
namespace Backend.Options {
|
||||||
|
public abstract class OptionsFromConfiguration {
|
||||||
|
public abstract string Position { get; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
namespace Backend.Options {
|
||||||
|
public static class OptionsFromConfigurationExtensions {
|
||||||
|
public static T AddOptionsFromConfiguration<T>(this IServiceCollection services, IConfiguration configuration)
|
||||||
|
where T : OptionsFromConfiguration {
|
||||||
|
T optionsInstance = (T)Activator.CreateInstance(typeof(T));
|
||||||
|
if (optionsInstance == null) return null;
|
||||||
|
string position = optionsInstance.Position;
|
||||||
|
services.Configure((Action<T>)(options => {
|
||||||
|
IConfigurationSection section = configuration.GetSection(position);
|
||||||
|
if (section != null) {
|
||||||
|
section.Bind(options);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
return optionsInstance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace Backend.Security.Authorization {
|
||||||
|
public sealed class AuthorizedAttribute : TypeFilterAttribute {
|
||||||
|
public AuthorizedAttribute(params string[] permission) : base(typeof(AuthorizedFilter)) {
|
||||||
|
Arguments = new object[] { permission };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Filters;
|
||||||
|
|
||||||
|
namespace Backend.Security.Authorization {
|
||||||
|
public class AuthorizedFilter : IAuthorizationFilter {
|
||||||
|
private readonly string[] _permissions;
|
||||||
|
|
||||||
|
public AuthorizedFilter(params string[] permissions) {
|
||||||
|
_permissions = permissions;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnAuthorization(AuthorizationFilterContext context) {
|
||||||
|
if (EndpointHasAllowAnonymousFilter(context)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!IsAuthenticated(context)) {
|
||||||
|
context.Result = new UnauthorizedResult();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ContainsRequiredRole(context)) {
|
||||||
|
context.Result = new ForbidResult();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool EndpointHasAllowAnonymousFilter(AuthorizationFilterContext context) {
|
||||||
|
return context.Filters.Any(item => item is IAllowAnonymousFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsAuthenticated(AuthorizationFilterContext context) {
|
||||||
|
return context.HttpContext.User.Identity.IsAuthenticated;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool ContainsRequiredRole(AuthorizationFilterContext context) {
|
||||||
|
if (context.HttpContext.User.HasClaim(CustomClaimTypes.Permission, "*"))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
var perms = context.HttpContext.User.Claims
|
||||||
|
.Where(c => c.Type == CustomClaimTypes.Permission)
|
||||||
|
.Select(c => c.Value).ToArray();
|
||||||
|
|
||||||
|
if (context.RouteData.Values.ContainsKey("userId")) {
|
||||||
|
var accessedUser = context.RouteData.Values["userId"] as string;
|
||||||
|
|
||||||
|
if (accessedUser == context.HttpContext.User.GetUserId()) {
|
||||||
|
var selfPerms = _permissions.Where(p => p.StartsWith("self.")).ToArray();
|
||||||
|
|
||||||
|
if (!selfPerms.Any())
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (CheckPermission(selfPerms, perms))
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (CheckPermission(_permissions, perms.Where(p => !p.StartsWith("self.")).ToArray()))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
|
||||||
|
bool CheckPermission(string[] permissions, string[] permission) {
|
||||||
|
if (permissions.Length == 0)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (permission.Contains("*"))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
foreach (var perm in permissions) {
|
||||||
|
if (permission.Contains(perm))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
string[] splice = perm.Split(".");
|
||||||
|
string cache = "";
|
||||||
|
foreach (var s in splice) {
|
||||||
|
cache += s + ".";
|
||||||
|
if (permission.Contains(cache + "*"))
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
using System.Linq;
|
||||||
|
using System.Security.Claims;
|
||||||
|
|
||||||
|
namespace Backend.Security.Authorization {
|
||||||
|
public static class ClaimsPrincipalExtensions {
|
||||||
|
public static string GetAccessTokenId(this ClaimsPrincipal principal) =>
|
||||||
|
principal.FindFirstValue(CustomClaimTypes.AccessTokenId);
|
||||||
|
|
||||||
|
public static string GetRefreshTokenId(this ClaimsPrincipal principal) =>
|
||||||
|
principal.FindFirstValue(CustomClaimTypes.RefreshTokenId);
|
||||||
|
|
||||||
|
public static string GetUserId(this ClaimsPrincipal principal) =>
|
||||||
|
principal.FindFirstValue(CustomClaimTypes.UserId);
|
||||||
|
|
||||||
|
public static string[] GetPermissions(this ClaimsPrincipal principal) => principal.Claims
|
||||||
|
.Where(claim => claim.Type.Equals(CustomClaimTypes.Permission))
|
||||||
|
.Select(claim => claim.Value)
|
||||||
|
.ToArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace Backend.Security.Authorization {
|
||||||
|
public static class CustomClaimTypes {
|
||||||
|
public const string AccessTokenId = "WebDesktop.AccessTokenId";
|
||||||
|
public const string RefreshTokenId = "WebDesktop.RefreshTokenId";
|
||||||
|
public const string UserId = "WebDesktop.UserId";
|
||||||
|
public const string Permission = "WebDesktop.Permission";
|
||||||
|
}
|
||||||
|
}
|
||||||
9
Backend/Security/ITokenContext.cs
Normal file
9
Backend/Security/ITokenContext.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
namespace Backend.Security {
|
||||||
|
public interface ITokenContext {
|
||||||
|
bool IsAuthenticated { get; }
|
||||||
|
Guid UserId { get; }
|
||||||
|
Guid AccessTokenId { get; }
|
||||||
|
Guid RefreshTokenId { get; }
|
||||||
|
string[] Permissions { get; }
|
||||||
|
}
|
||||||
|
}
|
||||||
14
Backend/Security/Permissions.cs
Normal file
14
Backend/Security/Permissions.cs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
namespace Backend.Security;
|
||||||
|
|
||||||
|
public static class Permissions {
|
||||||
|
|
||||||
|
public const string ShowUsers = "users.see";
|
||||||
|
public const string EditUsers = "users.edit";
|
||||||
|
public const string DeleteUsers = "users.delete";
|
||||||
|
public const string LogoutUsers = "users.logout";
|
||||||
|
public const string EditUserPermissions = "users.permissions.edit";
|
||||||
|
public const string ShowUserPermissions = "users.permissions.show";
|
||||||
|
|
||||||
|
public const string EditOwnPermissions = "self.permissions.edit";
|
||||||
|
|
||||||
|
}
|
||||||
26
Backend/Security/TokenContext.cs
Normal file
26
Backend/Security/TokenContext.cs
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
using Backend.Security.Authorization;
|
||||||
|
|
||||||
|
namespace Backend.Security {
|
||||||
|
internal class TokenContext : ITokenContext {
|
||||||
|
private readonly IHttpContextAccessor _accessor;
|
||||||
|
|
||||||
|
public TokenContext(IHttpContextAccessor accessor) {
|
||||||
|
_accessor = accessor;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsAuthenticated => _accessor.HttpContext?.User.Identity?.IsAuthenticated == true;
|
||||||
|
|
||||||
|
public Guid UserId => CreateGuild(_accessor.HttpContext?.User.GetUserId());
|
||||||
|
|
||||||
|
public Guid AccessTokenId => CreateGuild(_accessor.HttpContext?.User.GetAccessTokenId());
|
||||||
|
|
||||||
|
public Guid RefreshTokenId => CreateGuild(_accessor.HttpContext?.User.GetRefreshTokenId());
|
||||||
|
|
||||||
|
public string[] Permissions => _accessor.HttpContext?.User.GetPermissions();
|
||||||
|
|
||||||
|
private static Guid CreateGuild(string id) {
|
||||||
|
if (string.IsNullOrEmpty(id)) return Guid.Empty;
|
||||||
|
return Guid.Parse(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
6
Backend/appsettings.Development.json
Normal file
6
Backend/appsettings.Development.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"Origins": ["http://localhost:4200", "http://localhost:9876"],
|
||||||
|
"Authentication": {
|
||||||
|
"DebugAccessToken": "474a0461-37ec-4b11-aefe-00c423d1511e"
|
||||||
|
}
|
||||||
|
}
|
||||||
44
Backend/appsettings.json
Normal file
44
Backend/appsettings.json
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning",
|
||||||
|
"Backend.Security.Authentication.JwtTokenAuthenticationHandler": "None"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"MySQL": "SERVER=213.136.89.237;DATABASE=WebDesktop;UID=WebDesktop;PASSWORD=Hft6bP@V3IkYvqS1",
|
||||||
|
"Origins": ["https://desktop.leon-hoppe.de"],
|
||||||
|
"AllowedHosts": "*",
|
||||||
|
"Authentication": {
|
||||||
|
"RefreshTokenExpirationTimeInHours": 12,
|
||||||
|
"AccessTokenExpirationTimeInMinutes": 5,
|
||||||
|
"DebugAccessToken": null
|
||||||
|
},
|
||||||
|
"Users": {
|
||||||
|
"DefaultPermissions": ["group.user"]
|
||||||
|
},
|
||||||
|
"Groups": [
|
||||||
|
{
|
||||||
|
"Permission": "group.admin",
|
||||||
|
"Name": "Admin",
|
||||||
|
"Inherits": [],
|
||||||
|
"Permissions": ["*"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Permission": "group.user",
|
||||||
|
"Name": "User",
|
||||||
|
"Inherits": [],
|
||||||
|
"Permissions": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Messages": {
|
||||||
|
"Users": {
|
||||||
|
"NotFound": "This user does not exist",
|
||||||
|
"InvalidEditData": "Userdata does not match security rules",
|
||||||
|
"InvalidRegisterData": "Userdata does not match security rules",
|
||||||
|
"WrongPassword": "Wrong password",
|
||||||
|
"UsernameOrEmailExist": "This username or email already exist",
|
||||||
|
"InvalidRefreshToken": "Invalid RefreshToken"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
16
Frontend/.browserslistrc
Normal file
16
Frontend/.browserslistrc
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
|
||||||
|
# For additional information regarding the format and rule options, please see:
|
||||||
|
# https://github.com/browserslist/browserslist#queries
|
||||||
|
|
||||||
|
# For the full list of supported browsers by the Angular framework, please see:
|
||||||
|
# https://angular.io/guide/browser-support
|
||||||
|
|
||||||
|
# You can see what browsers were selected by your queries by running:
|
||||||
|
# npx browserslist
|
||||||
|
|
||||||
|
#last 1 Chrome version
|
||||||
|
#last 1 Firefox version
|
||||||
|
#last 2 Edge major versions
|
||||||
|
#last 2 Safari major versions
|
||||||
|
#last 2 iOS major versions
|
||||||
|
#Firefox ESR
|
||||||
7
Frontend/.dockerignore
Normal file
7
Frontend/.dockerignore
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
.editorconfig
|
||||||
|
/node_modules
|
||||||
|
/e2e
|
||||||
|
/docs
|
||||||
|
.gitignore
|
||||||
|
*.zip
|
||||||
|
*.md
|
||||||
16
Frontend/.editorconfig
Normal file
16
Frontend/.editorconfig
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# Editor configuration, see https://editorconfig.org
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
[*.ts]
|
||||||
|
quote_type = single
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
max_line_length = off
|
||||||
|
trim_trailing_whitespace = false
|
||||||
46
Frontend/.gitignore
vendored
Normal file
46
Frontend/.gitignore
vendored
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
# See http://help.github.com/ignore-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# compiled output
|
||||||
|
/dist
|
||||||
|
/tmp
|
||||||
|
/out-tsc
|
||||||
|
# Only exists if Bazel was run
|
||||||
|
/bazel-out
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
|
||||||
|
# profiling files
|
||||||
|
chrome-profiler-events*.json
|
||||||
|
|
||||||
|
# IDEs and editors
|
||||||
|
/.idea
|
||||||
|
.project
|
||||||
|
.classpath
|
||||||
|
.c9/
|
||||||
|
*.launch
|
||||||
|
.settings/
|
||||||
|
*.sublime-workspace
|
||||||
|
|
||||||
|
# IDE - VSCode
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/settings.json
|
||||||
|
!.vscode/tasks.json
|
||||||
|
!.vscode/launch.json
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.history/*
|
||||||
|
|
||||||
|
# misc
|
||||||
|
/.angular/cache
|
||||||
|
/.sass-cache
|
||||||
|
/connect.lock
|
||||||
|
/coverage
|
||||||
|
/libpeerconnection.log
|
||||||
|
npm-debug.log
|
||||||
|
yarn-error.log
|
||||||
|
testem.log
|
||||||
|
/typings
|
||||||
|
|
||||||
|
# System Files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
11
Frontend/Dockerfile
Normal file
11
Frontend/Dockerfile
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
|
||||||
|
#stage 1
|
||||||
|
FROM node:latest as node
|
||||||
|
WORKDIR /app
|
||||||
|
COPY . .
|
||||||
|
RUN npm install
|
||||||
|
RUN npm run build --prod
|
||||||
|
#stage 2
|
||||||
|
FROM nginx:alpine
|
||||||
|
COPY nginx.conf /etc/nginx/sites-available/default
|
||||||
|
COPY --from=node /app/dist/frontend /usr/share/nginx/html
|
||||||
27
Frontend/README.md
Normal file
27
Frontend/README.md
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# Frontend
|
||||||
|
|
||||||
|
This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 13.1.2.
|
||||||
|
|
||||||
|
## Development server
|
||||||
|
|
||||||
|
Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files.
|
||||||
|
|
||||||
|
## Code scaffolding
|
||||||
|
|
||||||
|
Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`.
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory.
|
||||||
|
|
||||||
|
## Running unit tests
|
||||||
|
|
||||||
|
Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
|
||||||
|
|
||||||
|
## Running end-to-end tests
|
||||||
|
|
||||||
|
Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities.
|
||||||
|
|
||||||
|
## Further help
|
||||||
|
|
||||||
|
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page.
|
||||||
111
Frontend/angular.json
Normal file
111
Frontend/angular.json
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
{
|
||||||
|
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||||
|
"version": 1,
|
||||||
|
"newProjectRoot": "projects",
|
||||||
|
"projects": {
|
||||||
|
"Frontend": {
|
||||||
|
"projectType": "application",
|
||||||
|
"schematics": {
|
||||||
|
"@schematics/angular:component": {
|
||||||
|
"style": "scss"
|
||||||
|
},
|
||||||
|
"@schematics/angular:application": {
|
||||||
|
"strict": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "",
|
||||||
|
"sourceRoot": "src",
|
||||||
|
"prefix": "app",
|
||||||
|
"architect": {
|
||||||
|
"build": {
|
||||||
|
"builder": "@angular-devkit/build-angular:browser",
|
||||||
|
"options": {
|
||||||
|
"outputPath": "dist/frontend",
|
||||||
|
"index": "src/index.html",
|
||||||
|
"main": "src/main.ts",
|
||||||
|
"polyfills": "src/polyfills.ts",
|
||||||
|
"tsConfig": "tsconfig.app.json",
|
||||||
|
"inlineStyleLanguage": "scss",
|
||||||
|
"assets": [
|
||||||
|
"src/favicon.ico",
|
||||||
|
"src/assets"
|
||||||
|
],
|
||||||
|
"styles": [
|
||||||
|
"src/styles.scss"
|
||||||
|
],
|
||||||
|
"scripts": []
|
||||||
|
},
|
||||||
|
"configurations": {
|
||||||
|
"production": {
|
||||||
|
"budgets": [
|
||||||
|
{
|
||||||
|
"type": "initial",
|
||||||
|
"maximumWarning": "500kb",
|
||||||
|
"maximumError": "1mb"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "anyComponentStyle",
|
||||||
|
"maximumWarning": "2kb",
|
||||||
|
"maximumError": "4kb"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"fileReplacements": [
|
||||||
|
{
|
||||||
|
"replace": "src/environments/environment.ts",
|
||||||
|
"with": "src/environments/environment.prod.ts"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outputHashing": "all"
|
||||||
|
},
|
||||||
|
"development": {
|
||||||
|
"buildOptimizer": false,
|
||||||
|
"optimization": false,
|
||||||
|
"vendorChunk": true,
|
||||||
|
"extractLicenses": false,
|
||||||
|
"sourceMap": true,
|
||||||
|
"namedChunks": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"defaultConfiguration": "production"
|
||||||
|
},
|
||||||
|
"serve": {
|
||||||
|
"builder": "@angular-devkit/build-angular:dev-server",
|
||||||
|
"configurations": {
|
||||||
|
"production": {
|
||||||
|
"browserTarget": "Frontend:build:production"
|
||||||
|
},
|
||||||
|
"development": {
|
||||||
|
"browserTarget": "Frontend:build:development"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"defaultConfiguration": "development"
|
||||||
|
},
|
||||||
|
"extract-i18n": {
|
||||||
|
"builder": "@angular-devkit/build-angular:extract-i18n",
|
||||||
|
"options": {
|
||||||
|
"browserTarget": "Frontend:build"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"test": {
|
||||||
|
"builder": "@angular-devkit/build-angular:karma",
|
||||||
|
"options": {
|
||||||
|
"main": "src/test.ts",
|
||||||
|
"polyfills": "src/polyfills.ts",
|
||||||
|
"tsConfig": "tsconfig.spec.json",
|
||||||
|
"karmaConfig": "karma.conf.js",
|
||||||
|
"inlineStyleLanguage": "scss",
|
||||||
|
"assets": [
|
||||||
|
"src/favicon.ico",
|
||||||
|
"src/assets"
|
||||||
|
],
|
||||||
|
"styles": [
|
||||||
|
"src/styles.scss"
|
||||||
|
],
|
||||||
|
"scripts": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"defaultProject": "Frontend"
|
||||||
|
}
|
||||||
44
Frontend/karma.conf.js
Normal file
44
Frontend/karma.conf.js
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
// Karma configuration file, see link for more information
|
||||||
|
// https://karma-runner.github.io/1.0/config/configuration-file.html
|
||||||
|
|
||||||
|
module.exports = function (config) {
|
||||||
|
config.set({
|
||||||
|
basePath: '',
|
||||||
|
frameworks: ['jasmine', '@angular-devkit/build-angular'],
|
||||||
|
plugins: [
|
||||||
|
require('karma-jasmine'),
|
||||||
|
require('karma-chrome-launcher'),
|
||||||
|
require('karma-jasmine-html-reporter'),
|
||||||
|
require('karma-coverage'),
|
||||||
|
require('@angular-devkit/build-angular/plugins/karma')
|
||||||
|
],
|
||||||
|
client: {
|
||||||
|
jasmine: {
|
||||||
|
// you can add configuration options for Jasmine here
|
||||||
|
// the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html
|
||||||
|
// for example, you can disable the random execution with `random: false`
|
||||||
|
// or set a specific seed with `seed: 4321`
|
||||||
|
},
|
||||||
|
clearContext: false // leave Jasmine Spec Runner output visible in browser
|
||||||
|
},
|
||||||
|
jasmineHtmlReporter: {
|
||||||
|
suppressAll: true // removes the duplicated traces
|
||||||
|
},
|
||||||
|
coverageReporter: {
|
||||||
|
dir: require('path').join(__dirname, './coverage/frontend'),
|
||||||
|
subdir: '.',
|
||||||
|
reporters: [
|
||||||
|
{ type: 'html' },
|
||||||
|
{ type: 'text-summary' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
reporters: ['progress', 'kjhtml'],
|
||||||
|
port: 9876,
|
||||||
|
colors: true,
|
||||||
|
logLevel: config.LOG_INFO,
|
||||||
|
autoWatch: true,
|
||||||
|
browsers: ['Chrome'],
|
||||||
|
singleRun: false,
|
||||||
|
restartOnFileChange: true
|
||||||
|
});
|
||||||
|
};
|
||||||
13
Frontend/nginx.conf
Normal file
13
Frontend/nginx.conf
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
server {
|
||||||
|
listen 80 default_server;
|
||||||
|
listen [::]:80 default_server;
|
||||||
|
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html index.htm index.nginx-debian.html;
|
||||||
|
|
||||||
|
server_name _;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
}
|
||||||
19966
Frontend/package-lock.json
generated
Normal file
19966
Frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
39
Frontend/package.json
Normal file
39
Frontend/package.json
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
"name": "frontend",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"scripts": {
|
||||||
|
"ng": "ng",
|
||||||
|
"start": "ng serve",
|
||||||
|
"build": "ng build",
|
||||||
|
"watch": "ng build --watch --configuration development",
|
||||||
|
"test": "ng test"
|
||||||
|
},
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@angular/animations": "~13.1.0",
|
||||||
|
"@angular/common": "~13.1.0",
|
||||||
|
"@angular/compiler": "~13.1.0",
|
||||||
|
"@angular/core": "~13.1.0",
|
||||||
|
"@angular/forms": "~13.1.0",
|
||||||
|
"@angular/platform-browser": "~13.1.0",
|
||||||
|
"@angular/platform-browser-dynamic": "~13.1.0",
|
||||||
|
"@angular/router": "~13.1.0",
|
||||||
|
"rxjs": "~7.4.0",
|
||||||
|
"tslib": "^2.3.0",
|
||||||
|
"zone.js": "~0.11.4"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@angular-devkit/build-angular": "~13.1.2",
|
||||||
|
"@angular/cli": "~13.1.2",
|
||||||
|
"@angular/compiler-cli": "~13.1.0",
|
||||||
|
"@types/jasmine": "~3.10.0",
|
||||||
|
"@types/node": "^12.11.1",
|
||||||
|
"jasmine-core": "~3.10.0",
|
||||||
|
"karma": "~6.3.0",
|
||||||
|
"karma-chrome-launcher": "~3.1.0",
|
||||||
|
"karma-coverage": "~2.1.0",
|
||||||
|
"karma-jasmine": "~4.0.0",
|
||||||
|
"karma-jasmine-html-reporter": "~1.7.0",
|
||||||
|
"typescript": "~4.5.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
13
Frontend/src/app/app-routing.module.ts
Normal file
13
Frontend/src/app/app-routing.module.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { RouterModule, Routes } from '@angular/router';
|
||||||
|
import {LoginComponent} from "./sites/login/login.component";
|
||||||
|
|
||||||
|
const routes: Routes = [
|
||||||
|
{path: "login", component: LoginComponent}
|
||||||
|
];
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [RouterModule.forRoot(routes)],
|
||||||
|
exports: [RouterModule]
|
||||||
|
})
|
||||||
|
export class AppRoutingModule { }
|
||||||
1
Frontend/src/app/app.component.html
Normal file
1
Frontend/src/app/app.component.html
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<router-outlet *ngIf="loaded"></router-outlet>
|
||||||
0
Frontend/src/app/app.component.scss
Normal file
0
Frontend/src/app/app.component.scss
Normal file
27
Frontend/src/app/app.component.ts
Normal file
27
Frontend/src/app/app.component.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import {Component, OnInit} from '@angular/core';
|
||||||
|
import {BackendService} from "./services/backend.service";
|
||||||
|
import {Router} from "@angular/router";
|
||||||
|
import {UserApi} from "./services/users.service";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-root',
|
||||||
|
templateUrl: './app.component.html',
|
||||||
|
styleUrls: ['./app.component.scss']
|
||||||
|
})
|
||||||
|
export class AppComponent implements OnInit {
|
||||||
|
public loaded: boolean = false;
|
||||||
|
|
||||||
|
constructor(private backend: BackendService, private router: Router, private users: UserApi) {}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
setTimeout(async () => {
|
||||||
|
if (this.router.url == "/login" || this.router.url == "/register") {
|
||||||
|
this.loaded = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await this.backend.requestToken()) this.loaded = true;
|
||||||
|
else await this.router.navigate(["login"]);
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
22
Frontend/src/app/app.module.ts
Normal file
22
Frontend/src/app/app.module.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { BrowserModule } from '@angular/platform-browser';
|
||||||
|
|
||||||
|
import { AppRoutingModule } from './app-routing.module';
|
||||||
|
import { AppComponent } from './app.component';
|
||||||
|
import {HttpClientModule} from "@angular/common/http";
|
||||||
|
import { LoginComponent } from './sites/login/login.component';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [
|
||||||
|
AppComponent,
|
||||||
|
LoginComponent
|
||||||
|
],
|
||||||
|
imports: [
|
||||||
|
BrowserModule,
|
||||||
|
AppRoutingModule,
|
||||||
|
HttpClientModule
|
||||||
|
],
|
||||||
|
providers: [],
|
||||||
|
bootstrap: [AppComponent]
|
||||||
|
})
|
||||||
|
export class AppModule { }
|
||||||
5
Frontend/src/app/entitys/accessToken.ts
Normal file
5
Frontend/src/app/entitys/accessToken.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export interface AccessToken {
|
||||||
|
id: string;
|
||||||
|
refreshTokenId: string;
|
||||||
|
expirationDate: string;
|
||||||
|
}
|
||||||
17
Frontend/src/app/entitys/user.ts
Normal file
17
Frontend/src/app/entitys/user.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
export interface User extends UserEditor {
|
||||||
|
id: string;
|
||||||
|
created: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserLogin {
|
||||||
|
usernameOrEmail: string;
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserEditor {
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
email: string;
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
99
Frontend/src/app/services/backend.service.ts
Normal file
99
Frontend/src/app/services/backend.service.ts
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import {HttpClient, HttpErrorResponse, HttpHeaders} from "@angular/common/http";
|
||||||
|
import {firstValueFrom} from "rxjs";
|
||||||
|
import {environment} from "../../environments/environment";
|
||||||
|
|
||||||
|
export interface BackendResponse<T> {
|
||||||
|
content: T;
|
||||||
|
success: boolean;
|
||||||
|
code: number;
|
||||||
|
message?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum RequestTypes {
|
||||||
|
GET = 0,
|
||||||
|
PUT = 1,
|
||||||
|
POST = 2,
|
||||||
|
DELETE = 3
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RequestOptions {
|
||||||
|
withCredentials?: boolean;
|
||||||
|
authorized?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class BackendService {
|
||||||
|
public authKey: string;
|
||||||
|
|
||||||
|
public headers: HttpHeaders = new HttpHeaders({
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': ''
|
||||||
|
});
|
||||||
|
|
||||||
|
constructor(private client: HttpClient) {}
|
||||||
|
|
||||||
|
public setToken(token: string): void {
|
||||||
|
this.authKey = token;
|
||||||
|
this.headers = this.headers.set("Authorization", token);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async sendRequest<T>(type: RequestTypes, endpoint: string, body?: any, options?: RequestOptions): Promise<BackendResponse<T>> {
|
||||||
|
try {
|
||||||
|
let response;
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
default:
|
||||||
|
case RequestTypes.GET:
|
||||||
|
response = await firstValueFrom(this.client.get<T>(environment.backendUrl + endpoint, {withCredentials: options?.withCredentials, headers: this.headers}));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case RequestTypes.DELETE:
|
||||||
|
response = await firstValueFrom(this.client.delete<T>(environment.backendUrl + endpoint, {withCredentials: options?.withCredentials, headers: this.headers}));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case RequestTypes.PUT:
|
||||||
|
response = await firstValueFrom(this.client.put<T>(environment.backendUrl + endpoint, body, {withCredentials: options?.withCredentials, headers: this.headers}));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case RequestTypes.POST:
|
||||||
|
response = await firstValueFrom(this.client.post<T>(environment.backendUrl + endpoint, body, {withCredentials: options?.withCredentials, headers: this.headers}));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {content: response, success: true, code: 200};
|
||||||
|
} catch (e) {
|
||||||
|
const error = e as HttpErrorResponse;
|
||||||
|
|
||||||
|
if (error.status == 401 && options?.authorized) {
|
||||||
|
if (await this.requestToken()) {
|
||||||
|
options.authorized = false; // Prevent infinite resent loop
|
||||||
|
return this.sendRequest<T>(type, endpoint, body, options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {content: undefined, success: false, code: error.status, message: error.error.title};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async testConnection(): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
await this.client.get(environment.backendUrl);
|
||||||
|
return true;
|
||||||
|
}catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async requestToken(): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const token = await firstValueFrom(this.client.get<{id: string, refreshTokenId: string, expirationDate: string}>(environment.backendUrl + "users/token", {headers: this.headers, withCredentials: true}));
|
||||||
|
this.setToken(token.id);
|
||||||
|
return true;
|
||||||
|
}catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
88
Frontend/src/app/services/users.service.ts
Normal file
88
Frontend/src/app/services/users.service.ts
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
import {Injectable} from '@angular/core';
|
||||||
|
import {BackendService, RequestTypes} from "./backend.service";
|
||||||
|
import {User, UserEditor, UserLogin} from "../entitys/user";
|
||||||
|
import {AccessToken} from "../entitys/accessToken";
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class UserApi {
|
||||||
|
private user: User;
|
||||||
|
|
||||||
|
constructor(private backend: BackendService) { }
|
||||||
|
|
||||||
|
public async getUsers(): Promise<User[]> {
|
||||||
|
const response = await this.backend.sendRequest<User[]>(RequestTypes.GET, "users", undefined, {authorized: true});
|
||||||
|
if (!response.success) return [];
|
||||||
|
return response.content;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getUser(id: string): Promise<User> {
|
||||||
|
const response = await this.backend.sendRequest<User>(RequestTypes.GET, "users/" + id, undefined, {authorized: true});
|
||||||
|
return response.content;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async editUser(id: string, editor: UserEditor): Promise<boolean> {
|
||||||
|
const response = await this.backend.sendRequest<any>(RequestTypes.PUT, "users/" + id, editor, {authorized: true});
|
||||||
|
return response.success;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async deleteUser(id: string): Promise<boolean> {
|
||||||
|
const response = await this.backend.sendRequest(RequestTypes.DELETE, "users/" + id, undefined, {authorized: true});
|
||||||
|
return response.success;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getUserPermissions(id: string, includeGroupPermissions: boolean = true): Promise<string[]> {
|
||||||
|
const response = await this.backend.sendRequest<string[]>(RequestTypes.GET, "users/" + id + "/permissions" + (includeGroupPermissions ? "/raw" : ""), undefined, {authorized: true});
|
||||||
|
if (!response.success) return [];
|
||||||
|
return response.content;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async addUserPermissions(id: string, permissions: string[]): Promise<boolean> {
|
||||||
|
const response = await this.backend.sendRequest<any>(RequestTypes.POST, "users/" + id + "/permissions", permissions, {authorized: true});
|
||||||
|
return response.success;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async removeUserPermissions(id: string, permissions: string[]): Promise<boolean> {
|
||||||
|
const response = await this.backend.sendRequest<any>(RequestTypes.PUT, "users/" + id + "/permissions", permissions, {authorized: true});
|
||||||
|
return response.success;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async login(login: UserLogin): Promise<{success: boolean, errorMessage: string}> {
|
||||||
|
const response = await this.backend.sendRequest<AccessToken>(RequestTypes.PUT, "users/login", login, {withCredentials: true});
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
this.backend.setToken(response.content.id);
|
||||||
|
await this.getAuthorizedUser();
|
||||||
|
}
|
||||||
|
|
||||||
|
return {success: response.success, errorMessage: response.message};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async register(register: UserEditor): Promise<{success: boolean, errorMessage: string}> {
|
||||||
|
const response = await this.backend.sendRequest<AccessToken>(RequestTypes.POST, "users/register", register, {withCredentials: true});
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
this.backend.setToken(response.content.id);
|
||||||
|
await this.getAuthorizedUser();
|
||||||
|
}
|
||||||
|
|
||||||
|
return {success: response.success, errorMessage: response.message};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async logout(id: string): Promise<boolean> {
|
||||||
|
const response = await this.backend.sendRequest(RequestTypes.DELETE, "users/" + id + "/logout", undefined, {authorized: true, withCredentials: true});
|
||||||
|
return response.success;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getAuthorizedUser(): Promise<User> {
|
||||||
|
if (this.user != undefined) return this.user;
|
||||||
|
|
||||||
|
const response = await this.backend.sendRequest<User>(RequestTypes.GET, "users/self", undefined, {authorized: true});
|
||||||
|
|
||||||
|
if (response.success)
|
||||||
|
this.user = response.content;
|
||||||
|
|
||||||
|
return response.content;
|
||||||
|
}
|
||||||
|
}
|
||||||
1
Frontend/src/app/sites/login/login.component.html
Normal file
1
Frontend/src/app/sites/login/login.component.html
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<p>login works!</p>
|
||||||
0
Frontend/src/app/sites/login/login.component.scss
Normal file
0
Frontend/src/app/sites/login/login.component.scss
Normal file
15
Frontend/src/app/sites/login/login.component.ts
Normal file
15
Frontend/src/app/sites/login/login.component.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-login',
|
||||||
|
templateUrl: './login.component.html',
|
||||||
|
styleUrls: ['./login.component.scss']
|
||||||
|
})
|
||||||
|
export class LoginComponent implements OnInit {
|
||||||
|
|
||||||
|
constructor() { }
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
0
Frontend/src/assets/.gitkeep
Normal file
0
Frontend/src/assets/.gitkeep
Normal file
4
Frontend/src/environments/environment.prod.ts
Normal file
4
Frontend/src/environments/environment.prod.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export const environment = {
|
||||||
|
production: true,
|
||||||
|
backendUrl: 'https://api.desktop.leon-hoppe.de/'
|
||||||
|
};
|
||||||
17
Frontend/src/environments/environment.ts
Normal file
17
Frontend/src/environments/environment.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
// This file can be replaced during build by using the `fileReplacements` array.
|
||||||
|
// `ng build` replaces `environment.ts` with `environment.prod.ts`.
|
||||||
|
// The list of file replacements can be found in `angular.json`.
|
||||||
|
|
||||||
|
export const environment = {
|
||||||
|
production: false,
|
||||||
|
backendUrl: 'http://localhost:5142/'
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* For easier debugging in development mode, you can import the following file
|
||||||
|
* to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`.
|
||||||
|
*
|
||||||
|
* This import should be commented out in production mode because it will have a negative impact
|
||||||
|
* on performance if an error is thrown.
|
||||||
|
*/
|
||||||
|
// import 'zone.js/plugins/zone-error'; // Included with Angular CLI.
|
||||||
BIN
Frontend/src/favicon.ico
Normal file
BIN
Frontend/src/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.4 KiB |
13
Frontend/src/index.html
Normal file
13
Frontend/src/index.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>WebDesktop</title>
|
||||||
|
<base href="/">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<app-root></app-root>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
12
Frontend/src/main.ts
Normal file
12
Frontend/src/main.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { enableProdMode } from '@angular/core';
|
||||||
|
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
|
||||||
|
|
||||||
|
import { AppModule } from './app/app.module';
|
||||||
|
import { environment } from './environments/environment';
|
||||||
|
|
||||||
|
if (environment.production) {
|
||||||
|
enableProdMode();
|
||||||
|
}
|
||||||
|
|
||||||
|
platformBrowserDynamic().bootstrapModule(AppModule)
|
||||||
|
.catch(err => console.error(err));
|
||||||
53
Frontend/src/polyfills.ts
Normal file
53
Frontend/src/polyfills.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
/**
|
||||||
|
* This file includes polyfills needed by Angular and is loaded before the app.
|
||||||
|
* You can add your own extra polyfills to this file.
|
||||||
|
*
|
||||||
|
* This file is divided into 2 sections:
|
||||||
|
* 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
|
||||||
|
* 2. Application imports. Files imported after ZoneJS that should be loaded before your main
|
||||||
|
* file.
|
||||||
|
*
|
||||||
|
* The current setup is for so-called "evergreen" browsers; the last versions of browsers that
|
||||||
|
* automatically update themselves. This includes recent versions of Safari, Chrome (including
|
||||||
|
* Opera), Edge on the desktop, and iOS and Chrome on mobile.
|
||||||
|
*
|
||||||
|
* Learn more in https://angular.io/guide/browser-support
|
||||||
|
*/
|
||||||
|
|
||||||
|
/***************************************************************************************************
|
||||||
|
* BROWSER POLYFILLS
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* By default, zone.js will patch all possible macroTask and DomEvents
|
||||||
|
* user can disable parts of macroTask/DomEvents patch by setting following flags
|
||||||
|
* because those flags need to be set before `zone.js` being loaded, and webpack
|
||||||
|
* will put import in the top of bundle, so user need to create a separate file
|
||||||
|
* in this directory (for example: zone-flags.ts), and put the following flags
|
||||||
|
* into that file, and then add the following code before importing zone.js.
|
||||||
|
* import './zone-flags';
|
||||||
|
*
|
||||||
|
* The flags allowed in zone-flags.ts are listed here.
|
||||||
|
*
|
||||||
|
* The following flags will work for all browsers.
|
||||||
|
*
|
||||||
|
* (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
|
||||||
|
* (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
|
||||||
|
* (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
|
||||||
|
*
|
||||||
|
* in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
|
||||||
|
* with the following flag, it will bypass `zone.js` patch for IE/Edge
|
||||||
|
*
|
||||||
|
* (window as any).__Zone_enable_cross_context_check = true;
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
/***************************************************************************************************
|
||||||
|
* Zone JS is required by default for Angular itself.
|
||||||
|
*/
|
||||||
|
import 'zone.js'; // Included with Angular CLI.
|
||||||
|
|
||||||
|
|
||||||
|
/***************************************************************************************************
|
||||||
|
* APPLICATION IMPORTS
|
||||||
|
*/
|
||||||
1
Frontend/src/styles.scss
Normal file
1
Frontend/src/styles.scss
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/* You can add global styles to this file, and also import other style files */
|
||||||
26
Frontend/src/test.ts
Normal file
26
Frontend/src/test.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
// This file is required by karma.conf.js and loads recursively all the .spec and framework files
|
||||||
|
|
||||||
|
import 'zone.js/testing';
|
||||||
|
import { getTestBed } from '@angular/core/testing';
|
||||||
|
import {
|
||||||
|
BrowserDynamicTestingModule,
|
||||||
|
platformBrowserDynamicTesting
|
||||||
|
} from '@angular/platform-browser-dynamic/testing';
|
||||||
|
|
||||||
|
declare const require: {
|
||||||
|
context(path: string, deep?: boolean, filter?: RegExp): {
|
||||||
|
<T>(id: string): T;
|
||||||
|
keys(): string[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// First, initialize the Angular testing environment.
|
||||||
|
getTestBed().initTestEnvironment(
|
||||||
|
BrowserDynamicTestingModule,
|
||||||
|
platformBrowserDynamicTesting(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Then we find all the tests.
|
||||||
|
const context = require.context('./', true, /\.spec\.ts$/);
|
||||||
|
// And load the modules.
|
||||||
|
context.keys().map(context);
|
||||||
19
Frontend/src/tests/backend.service.spec.ts
Normal file
19
Frontend/src/tests/backend.service.spec.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import {BackendService, RequestTypes} from "../app/services/backend.service";
|
||||||
|
import {TestBed} from "@angular/core/testing";
|
||||||
|
import {HttpClientModule} from "@angular/common/http";
|
||||||
|
|
||||||
|
describe('BackendService', () => {
|
||||||
|
let service : BackendService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({imports: [HttpClientModule]})
|
||||||
|
service = TestBed.inject(BackendService);
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should connect to the backend', function (done) {
|
||||||
|
service.testConnection().then(result => {
|
||||||
|
expect(result).toBeTrue();
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
});
|
||||||
|
})
|
||||||
115
Frontend/src/tests/users.service.spec.ts
Normal file
115
Frontend/src/tests/users.service.spec.ts
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import {UserApi} from "../app/services/users.service";
|
||||||
|
import {BackendService} from "../app/services/backend.service";
|
||||||
|
import {HttpClientModule} from "@angular/common/http";
|
||||||
|
|
||||||
|
describe('UserApi', () => {
|
||||||
|
let backend: BackendService;
|
||||||
|
let service: UserApi;
|
||||||
|
let userId: string;
|
||||||
|
|
||||||
|
beforeAll((done) => {
|
||||||
|
TestBed.configureTestingModule({imports: [HttpClientModule]});
|
||||||
|
backend = TestBed.inject(BackendService);
|
||||||
|
service = TestBed.inject(UserApi);
|
||||||
|
|
||||||
|
service.register({
|
||||||
|
username: "tester",
|
||||||
|
password: "password",
|
||||||
|
email: "test@test.com",
|
||||||
|
firstName: "test",
|
||||||
|
lastName: "test"
|
||||||
|
}).then(success => {
|
||||||
|
expect(success.success).toBeTrue();
|
||||||
|
service.getAuthorizedUser().then(user => {
|
||||||
|
expect(user).not.toBeUndefined();
|
||||||
|
userId = user.id;
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}, 5000)
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
backend.setToken("474a0461-37ec-4b11-aefe-00c423d1511e");
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be create the service', () => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show all users', (done) => {
|
||||||
|
service.getUsers().then(users => {
|
||||||
|
expect(users).not.toBeUndefined();
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should login with the given credentials', (done) => {
|
||||||
|
service.login({
|
||||||
|
usernameOrEmail: "tester",
|
||||||
|
password: "password"
|
||||||
|
}).then(result => {
|
||||||
|
expect(result.success).toBeTrue();
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should delete all user sessions', (done) => {
|
||||||
|
service.login({
|
||||||
|
usernameOrEmail: "tester",
|
||||||
|
password: "password"
|
||||||
|
}).then(result => {
|
||||||
|
expect(result.success).toBeTrue();
|
||||||
|
service.logout(userId).then(success => {
|
||||||
|
expect(success).toBeTrue();
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show the specified user', (done) => {
|
||||||
|
service.getUser(userId).then(user => {
|
||||||
|
expect(user).not.toBeUndefined();
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should edit the specified user', (done) => {
|
||||||
|
service.editUser(userId, {
|
||||||
|
username: "",
|
||||||
|
password: "",
|
||||||
|
email: "",
|
||||||
|
firstName: "Test",
|
||||||
|
lastName: "Test"
|
||||||
|
}).then(result => {
|
||||||
|
expect(result).toBeTrue();
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show the permissions of the specified user', (done) => {
|
||||||
|
service.getUserPermissions(userId).then(perms => {
|
||||||
|
expect(perms).not.toBeUndefined();
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add the permissions to the specified user', (done) => {
|
||||||
|
service.addUserPermissions(userId, ["*"]).then(success => {
|
||||||
|
expect(success).toBeTrue();
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove the permissions to the specified user', (done) => {
|
||||||
|
service.removeUserPermissions(userId, ["*"]).then(success => {
|
||||||
|
expect(success).toBeTrue();
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll((done) => {
|
||||||
|
service.deleteUser(userId).then(() => done());
|
||||||
|
})
|
||||||
|
});
|
||||||
15
Frontend/tsconfig.app.json
Normal file
15
Frontend/tsconfig.app.json
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
/* To learn more about this file see: https://angular.io/config/tsconfig. */
|
||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "./out-tsc/app",
|
||||||
|
"types": []
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"src/main.ts",
|
||||||
|
"src/polyfills.ts"
|
||||||
|
],
|
||||||
|
"include": [
|
||||||
|
"src/**/*.d.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
34
Frontend/tsconfig.json
Normal file
34
Frontend/tsconfig.json
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
/* To learn more about this file see: https://angular.io/config/tsconfig. */
|
||||||
|
{
|
||||||
|
"compileOnSave": false,
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": "./",
|
||||||
|
"outDir": "./dist/out-tsc",
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"strict": true,
|
||||||
|
"noImplicitOverride": true,
|
||||||
|
"noPropertyAccessFromIndexSignature": true,
|
||||||
|
"noImplicitReturns": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"declaration": false,
|
||||||
|
"downlevelIteration": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"importHelpers": true,
|
||||||
|
"target": "es2017",
|
||||||
|
"module": "es2020",
|
||||||
|
"lib": [
|
||||||
|
"es2020",
|
||||||
|
"dom"
|
||||||
|
],
|
||||||
|
"strictPropertyInitialization": false,
|
||||||
|
"strictNullChecks": false
|
||||||
|
},
|
||||||
|
"angularCompilerOptions": {
|
||||||
|
"enableI18nLegacyMessageIdFormat": false,
|
||||||
|
"strictInjectionParameters": true,
|
||||||
|
"strictInputAccessModifiers": true,
|
||||||
|
"strictTemplates": true,
|
||||||
|
}
|
||||||
|
}
|
||||||
18
Frontend/tsconfig.spec.json
Normal file
18
Frontend/tsconfig.spec.json
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
/* To learn more about this file see: https://angular.io/config/tsconfig. */
|
||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "./out-tsc/spec",
|
||||||
|
"types": [
|
||||||
|
"jasmine"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"src/test.ts",
|
||||||
|
"src/polyfills.ts"
|
||||||
|
],
|
||||||
|
"include": [
|
||||||
|
"src/**/*.spec.ts",
|
||||||
|
"src/**/*.d.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
16
WebDesktop 2.0.sln
Normal file
16
WebDesktop 2.0.sln
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
|
||||||
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Backend", "Backend\Backend.csproj", "{60AC6652-C470-460E-A069-7FAC54DA7587}"
|
||||||
|
EndProject
|
||||||
|
Global
|
||||||
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
Release|Any CPU = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
|
{60AC6652-C470-460E-A069-7FAC54DA7587}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{60AC6652-C470-460E-A069-7FAC54DA7587}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{60AC6652-C470-460E-A069-7FAC54DA7587}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{60AC6652-C470-460E-A069-7FAC54DA7587}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
EndGlobal
|
||||||
13
docker-compose.yml
Normal file
13
docker-compose.yml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
version: "3.9"
|
||||||
|
services:
|
||||||
|
backend:
|
||||||
|
build: ./Backend
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "4122:5142"
|
||||||
|
frontend:
|
||||||
|
build: ./Frontend
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- '4121:80'
|
||||||
|
|
||||||
Reference in New Issue
Block a user