From dafe603a06c25226ddd4d3ea9cd29d0a81984b01 Mon Sep 17 00:00:00 2001 From: Robert Marshall Date: Sun, 12 Apr 2020 13:50:39 +0100 Subject: [PATCH] Build auth API --- .drone.yml | 48 +++++ .editorconfig | 198 ++++++++++++++++++ .gitattributes | 1 + .gitconfig | 2 + .gitignore | 56 +++++ .nuke | 1 + Readme.md | 9 + api.auth.service | 16 ++ build.ps1 | 68 ++++++ build.sh | 62 ++++++ build/.editorconfig | 10 + build/Build.cs | 62 ++++++ build/_build.csproj | 34 +++ clean-config.sh | 5 + .../AuthControllerTests.cs | 67 ++++++ .../Robware.Api.Auth.Tests.csproj | 22 ++ src/Robware.Api.Auth.sln | 53 +++++ .../Controllers/AuthController.cs | 35 ++++ src/Robware.Api.Auth/Models/LoginRequest.cs | 6 + src/Robware.Api.Auth/Program.cs | 12 ++ .../Properties/launchSettings.json | 28 +++ src/Robware.Api.Auth/Robware.Api.Auth.csproj | 13 ++ src/Robware.Api.Auth/Startup.cs | 44 ++++ .../appsettings.Development.json | 12 ++ src/Robware.Api.Auth/appsettings.json | 20 ++ src/Robware.Auth.Tests/AuthenticatorTests.cs | 50 +++++ .../CryoptographyProviderTests.cs | 12 ++ .../Robware.Auth.Tests.csproj | 23 ++ src/Robware.Auth.Tests/TestUser.cs | 8 + src/Robware.Auth/Authenticator.cs | 31 +++ src/Robware.Auth/CryptographyProvider.cs | 19 ++ src/Robware.Auth/IAuthenticator.cs | 7 + src/Robware.Auth/ICryptographyProvider.cs | 5 + src/Robware.Auth/IUsers.cs | 7 + src/Robware.Auth/Robware.Auth.csproj | 7 + src/Robware.Auth/User.cs | 6 + src/Robware.Auth/UserNotFoundException.cs | 9 + src/Robware.Data/DatabaseUser.cs | 11 + src/Robware.Data/IDatabaseProvider.cs | 7 + src/Robware.Data/MySQLDatabaseProvider.cs | 12 ++ src/Robware.Data/Robware.Data.csproj | 16 ++ src/Robware.Data/States/UserState.cs | 10 + src/Robware.Data/UserRepository.cs | 29 +++ 43 files changed, 1153 insertions(+) create mode 100644 .drone.yml create mode 100644 .editorconfig create mode 100644 .gitattributes create mode 100644 .gitconfig create mode 100644 .gitignore create mode 100644 .nuke create mode 100644 Readme.md create mode 100644 api.auth.service create mode 100644 build.ps1 create mode 100644 build.sh create mode 100644 build/.editorconfig create mode 100644 build/Build.cs create mode 100644 build/_build.csproj create mode 100644 clean-config.sh create mode 100644 src/Robware.Api.Auth.Tests/AuthControllerTests.cs create mode 100644 src/Robware.Api.Auth.Tests/Robware.Api.Auth.Tests.csproj create mode 100644 src/Robware.Api.Auth.sln create mode 100644 src/Robware.Api.Auth/Controllers/AuthController.cs create mode 100644 src/Robware.Api.Auth/Models/LoginRequest.cs create mode 100644 src/Robware.Api.Auth/Program.cs create mode 100644 src/Robware.Api.Auth/Properties/launchSettings.json create mode 100644 src/Robware.Api.Auth/Robware.Api.Auth.csproj create mode 100644 src/Robware.Api.Auth/Startup.cs create mode 100644 src/Robware.Api.Auth/appsettings.Development.json create mode 100644 src/Robware.Api.Auth/appsettings.json create mode 100644 src/Robware.Auth.Tests/AuthenticatorTests.cs create mode 100644 src/Robware.Auth.Tests/CryoptographyProviderTests.cs create mode 100644 src/Robware.Auth.Tests/Robware.Auth.Tests.csproj create mode 100644 src/Robware.Auth.Tests/TestUser.cs create mode 100644 src/Robware.Auth/Authenticator.cs create mode 100644 src/Robware.Auth/CryptographyProvider.cs create mode 100644 src/Robware.Auth/IAuthenticator.cs create mode 100644 src/Robware.Auth/ICryptographyProvider.cs create mode 100644 src/Robware.Auth/IUsers.cs create mode 100644 src/Robware.Auth/Robware.Auth.csproj create mode 100644 src/Robware.Auth/User.cs create mode 100644 src/Robware.Auth/UserNotFoundException.cs create mode 100644 src/Robware.Data/DatabaseUser.cs create mode 100644 src/Robware.Data/IDatabaseProvider.cs create mode 100644 src/Robware.Data/MySQLDatabaseProvider.cs create mode 100644 src/Robware.Data/Robware.Data.csproj create mode 100644 src/Robware.Data/States/UserState.cs create mode 100644 src/Robware.Data/UserRepository.cs diff --git a/.drone.yml b/.drone.yml new file mode 100644 index 0000000..7323df3 --- /dev/null +++ b/.drone.yml @@ -0,0 +1,48 @@ +kind: pipeline +name: default + +clone: + skip_verify: true + +volumes: +- name: output + host: + path: /var/www/Api.Auth + +steps: +- name: build and publish + image: mcr.microsoft.com/dotnet/core/sdk:3.1 + volumes: + - name: output + path: /output + environment: + ConnectionString: + from_secret: ConnectionString + commands: + - chmod +x ./build.sh + - ./build.sh + - sed -i "s//$ConnectionString/g" output/appsettings.json + - cp api.auth.service output/ + - cp -r ./output/* /output +- name: restart service + privileged: true + image: appleboy/drone-ssh + settings: + host: 192.168.1.3 + username: + from_secret: ssh_user + password: + from_secret: ssh_password + script: + - systemctl daemon-reload + - service api.auth restart +- name: notify + image: drillster/drone-email + settings: + host: 192.168.1.3 + skip_verify: true + from: build@robware.uk + when: + status: + - changed + - failure \ No newline at end of file diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..f88f151 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,198 @@ +# Remove the line below if you want to inherit .editorconfig settings from higher directories +root = true + +[*] +indent_style = tab + +# C# files +[*.cs] +max_line_length = 180 +# New line preferences +end_of_line = crlf +insert_final_newline = false + +#### .NET Coding Conventions #### + +# Organize usings +dotnet_separate_import_directive_groups = false +dotnet_sort_system_directives_first = false + +# this. and Me. preferences +dotnet_style_qualification_for_event = false:silent +dotnet_style_qualification_for_field = false:silent +dotnet_style_qualification_for_method = false:silent +dotnet_style_qualification_for_property = false:silent + +# Language keywords vs BCL types preferences +dotnet_style_predefined_type_for_locals_parameters_members = true:silent +dotnet_style_predefined_type_for_member_access = true:silent + +# Parentheses preferences +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent + +# Modifier preferences +dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent + +# Expression-level preferences +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_explicit_tuple_names = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_object_initializer = true:suggestion +dotnet_style_prefer_auto_properties = true:silent +dotnet_style_prefer_compound_assignment = true:suggestion +dotnet_style_prefer_conditional_expression_over_assignment = true:silent +dotnet_style_prefer_conditional_expression_over_return = true:silent +dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion +dotnet_style_prefer_inferred_tuple_names = true:suggestion +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion +dotnet_style_prefer_simplified_interpolation = true:suggestion + +# Field preferences +dotnet_style_readonly_field = true:suggestion + +# Parameter preferences +dotnet_code_quality_unused_parameters = all:suggestion + +#### C# Coding Conventions #### + +# var preferences +csharp_style_var_elsewhere = false:silent +csharp_style_var_for_built_in_types = false:silent +csharp_style_var_when_type_is_apparent = false:silent + +# Expression-bodied members +csharp_style_expression_bodied_accessors = true:silent +csharp_style_expression_bodied_constructors = false:silent +csharp_style_expression_bodied_indexers = true:silent +csharp_style_expression_bodied_lambdas = true:silent +csharp_style_expression_bodied_local_functions = false:silent +csharp_style_expression_bodied_methods = false:silent +csharp_style_expression_bodied_operators = false:silent +csharp_style_expression_bodied_properties = true:silent + +# Pattern matching preferences +csharp_style_pattern_matching_over_as_with_null_check = true:suggestion +csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion +csharp_style_prefer_switch_expression = true:suggestion + +# Null-checking preferences +csharp_style_conditional_delegate_call = true:suggestion + +# Modifier preferences +csharp_prefer_static_local_function = true:suggestion +csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:silent + +# Code-block preferences +csharp_prefer_braces = false:silent +csharp_prefer_simple_using_statement = true:suggestion + +# Expression-level preferences +csharp_prefer_simple_default_expression = true:suggestion +csharp_style_deconstructed_variable_declaration = true:suggestion +csharp_style_inlined_variable_declaration = true:suggestion +csharp_style_pattern_local_over_anonymous_function = true:suggestion +csharp_style_prefer_index_operator = true:suggestion +csharp_style_prefer_range_operator = true:suggestion +csharp_style_throw_expression = true:suggestion +csharp_style_unused_value_assignment_preference = discard_variable:suggestion +csharp_style_unused_value_expression_statement_preference = discard_variable:silent + +# 'using' directive preferences +csharp_using_directive_placement = outside_namespace:silent + +#### C# Formatting Rules #### + +# New line preferences +csharp_new_line_before_catch = true +csharp_new_line_before_else = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_open_brace = none +csharp_new_line_between_query_expression_clauses = true + +# Indentation preferences +csharp_indent_block_contents = true +csharp_indent_braces = false +csharp_indent_case_contents = true +csharp_indent_case_contents_when_block = true +csharp_indent_labels = one_less_than_current +csharp_indent_switch_labels = true + +# Space preferences +csharp_space_after_cast = false +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_after_comma = true +csharp_space_after_dot = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_after_semicolon_in_for_statement = true +csharp_space_around_binary_operators = before_and_after +csharp_space_around_declaration_statements = false +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_before_comma = false +csharp_space_before_dot = false +csharp_space_before_open_square_brackets = false +csharp_space_before_semicolon_in_for_statement = false +csharp_space_between_empty_square_brackets = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_declaration_name_and_open_parenthesis = false +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_parentheses = false +csharp_space_between_square_brackets = false + +# Wrapping preferences +csharp_preserve_single_line_blocks = true +csharp_preserve_single_line_statements = true + +#### Naming styles #### + +# Naming rules + +dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion +dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface +dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i + +dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.types_should_be_pascal_case.symbols = types +dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case + +dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members +dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case + +# Symbol specifications + +dotnet_naming_symbols.interface.applicable_kinds = interface +dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.interface.required_modifiers = + +dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum +dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.types.required_modifiers = + +dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method +dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.non_field_members.required_modifiers = + +# Naming styles + +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.capitalization = pascal_case + +dotnet_naming_style.begins_with_i.required_prefix = I +dotnet_naming_style.begins_with_i.required_suffix = +dotnet_naming_style.begins_with_i.word_separator = +dotnet_naming_style.begins_with_i.capitalization = pascal_case + +[*.{yaml,yml}] +indent_style = space +indent_size = 2 \ No newline at end of file diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..1c21fb6 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +src/Robware.Api.Auth/appsettings.Development.json filter=clean-config \ No newline at end of file diff --git a/.gitconfig b/.gitconfig new file mode 100644 index 0000000..169b515 --- /dev/null +++ b/.gitconfig @@ -0,0 +1,2 @@ +[filter "clean-config"] + clean = ./clean-config.sh \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3e8285e --- /dev/null +++ b/.gitignore @@ -0,0 +1,56 @@ +*.swp +*.*~ +project.lock.json +.DS_Store +*.pyc +nupkg/ + +# Visual Studio Code +.vscode + +# Rider +.idea + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +#build/ +bld/ +[Bb]in/ +[Oo]bj/ +[Oo]ut/ +msbuild.log +msbuild.err +msbuild.wrn + +# Visual Studio 2015 +.vs/ + + +#lcov files +lcov.info + +#compiled and minified assets +*.css +*.min.css +*.min.js + +#nodejs +node_modules + +output +.tmp +*.ncrunchsolution +/_NCrunch_Website +*.DotSettings +*.cache diff --git a/.nuke b/.nuke new file mode 100644 index 0000000..c11a315 --- /dev/null +++ b/.nuke @@ -0,0 +1 @@ +src/Robware.Api.Auth.sln \ No newline at end of file diff --git a/Readme.md b/Readme.md new file mode 100644 index 0000000..a41902e --- /dev/null +++ b/Readme.md @@ -0,0 +1,9 @@ +[![Build Status](https://build.robware.uk/api/badges/Robware/Api.Auth/status.svg)](https://build.robware.uk/Robware/Api.Auth) + +# Auth microservice + +Provides an API to authenticate users. + +## Setup + +After clone, please run `git config --local include.path ../.gitconfig`. This will set up the filters required to ignore local dev config. \ No newline at end of file diff --git a/api.auth.service b/api.auth.service new file mode 100644 index 0000000..aa4a390 --- /dev/null +++ b/api.auth.service @@ -0,0 +1,16 @@ +[Unit] +Description=Robware Auth API + +[Service] +WorkingDirectory=/var/www/Api.Auth +ExecStart=/usr/bin/dotnet /var/www/Api.Auth/Robware.Api.Auth.dll +Restart=always +# Restart service after 10 seconds if the dotnet service crashes: +RestartSec=10 +KillSignal=SIGINT +SyslogIdentifier=Api.Auth +User=www-data +Environment=ASPNETCORE_ENVIRONMENT=Production + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/build.ps1 b/build.ps1 new file mode 100644 index 0000000..cb7fbdb --- /dev/null +++ b/build.ps1 @@ -0,0 +1,68 @@ +[CmdletBinding()] +Param( + [Parameter(Position=0,Mandatory=$false,ValueFromRemainingArguments=$true)] + [string[]]$BuildArguments +) + +Write-Output "Windows PowerShell $($Host.Version)" + +Set-StrictMode -Version 2.0; $ErrorActionPreference = "Stop"; $ConfirmPreference = "None"; trap { exit 1 } +$PSScriptRoot = Split-Path $MyInvocation.MyCommand.Path -Parent + +########################################################################### +# CONFIGURATION +########################################################################### + +$BuildProjectFile = "$PSScriptRoot\build\_build.csproj" +$TempDirectory = "$PSScriptRoot\\.tmp" + +$DotNetGlobalFile = "$PSScriptRoot\\global.json" +$DotNetInstallUrl = "https://raw.githubusercontent.com/dotnet/cli/master/scripts/obtain/dotnet-install.ps1" +$DotNetChannel = "Current" + +$env:DOTNET_SKIP_FIRST_TIME_EXPERIENCE = 1 +$env:DOTNET_CLI_TELEMETRY_OPTOUT = 1 + +########################################################################### +# EXECUTION +########################################################################### + +function ExecSafe([scriptblock] $cmd) { + & $cmd + if ($LASTEXITCODE) { exit $LASTEXITCODE } +} + +# If global.json exists, load expected version +if (Test-Path $DotNetGlobalFile) { + $DotNetGlobal = $(Get-Content $DotNetGlobalFile | Out-String | ConvertFrom-Json) + if ($DotNetGlobal.PSObject.Properties["sdk"] -and $DotNetGlobal.sdk.PSObject.Properties["version"]) { + $DotNetVersion = $DotNetGlobal.sdk.version + } +} + +# If dotnet is installed locally, and expected version is not set or installation matches the expected version +if ((Get-Command "dotnet" -ErrorAction SilentlyContinue) -ne $null -and ` + (!(Test-Path variable:DotNetVersion) -or $(& dotnet --version) -eq $DotNetVersion)) { + $env:DOTNET_EXE = (Get-Command "dotnet").Path +} +else { + $DotNetDirectory = "$TempDirectory\dotnet-win" + $env:DOTNET_EXE = "$DotNetDirectory\dotnet.exe" + + # Download install script + $DotNetInstallFile = "$TempDirectory\dotnet-install.ps1" + md -force $TempDirectory > $null + (New-Object System.Net.WebClient).DownloadFile($DotNetInstallUrl, $DotNetInstallFile) + + # Install by channel or version + if (!(Test-Path variable:DotNetVersion)) { + ExecSafe { & $DotNetInstallFile -InstallDir $DotNetDirectory -Channel $DotNetChannel -NoPath } + } else { + ExecSafe { & $DotNetInstallFile -InstallDir $DotNetDirectory -Version $DotNetVersion -NoPath } + } +} + +Write-Output "Microsoft (R) .NET Core SDK version $(& $env:DOTNET_EXE --version)" + +ExecSafe { & $env:DOTNET_EXE build $BuildProjectFile /nodeReuse:false } +ExecSafe { & $env:DOTNET_EXE run --project $BuildProjectFile --no-build -- $BuildArguments } diff --git a/build.sh b/build.sh new file mode 100644 index 0000000..6d6e2ad --- /dev/null +++ b/build.sh @@ -0,0 +1,62 @@ +#!/usr/bin/env bash + +echo $(bash --version 2>&1 | head -n 1) + +set -eo pipefail +SCRIPT_DIR=$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd) + +########################################################################### +# CONFIGURATION +########################################################################### + +BUILD_PROJECT_FILE="$SCRIPT_DIR/build/_build.csproj" +TEMP_DIRECTORY="$SCRIPT_DIR//.tmp" + +DOTNET_GLOBAL_FILE="$SCRIPT_DIR//global.json" +DOTNET_INSTALL_URL="https://raw.githubusercontent.com/dotnet/cli/master/scripts/obtain/dotnet-install.sh" +DOTNET_CHANNEL="Current" + +export DOTNET_CLI_TELEMETRY_OPTOUT=1 +export DOTNET_SKIP_FIRST_TIME_EXPERIENCE=1 + +########################################################################### +# EXECUTION +########################################################################### + +function FirstJsonValue { + perl -nle 'print $1 if m{"'$1'": "([^"\-]+)",?}' <<< ${@:2} +} + +# If global.json exists, load expected version +if [ -f "$DOTNET_GLOBAL_FILE" ]; then + DOTNET_VERSION=$(FirstJsonValue "version" $(cat "$DOTNET_GLOBAL_FILE")) + if [ "$DOTNET_VERSION" == "" ]; then + unset DOTNET_VERSION + fi +fi + +# If dotnet is installed locally, and expected version is not set or installation matches the expected version +if [[ -x "$(command -v dotnet)" && (-z ${DOTNET_VERSION+x} || $(dotnet --version) == "$DOTNET_VERSION") ]]; then + export DOTNET_EXE="$(command -v dotnet)" +else + DOTNET_DIRECTORY="$TEMP_DIRECTORY/dotnet-unix" + export DOTNET_EXE="$DOTNET_DIRECTORY/dotnet" + + # Download install script + DOTNET_INSTALL_FILE="$TEMP_DIRECTORY/dotnet-install.sh" + mkdir -p "$TEMP_DIRECTORY" + curl -Lsfo "$DOTNET_INSTALL_FILE" "$DOTNET_INSTALL_URL" + chmod +x "$DOTNET_INSTALL_FILE" + + # Install by channel or version + if [ -z ${DOTNET_VERSION+x} ]; then + "$DOTNET_INSTALL_FILE" --install-dir "$DOTNET_DIRECTORY" --channel "$DOTNET_CHANNEL" --no-path + else + "$DOTNET_INSTALL_FILE" --install-dir "$DOTNET_DIRECTORY" --version "$DOTNET_VERSION" --no-path + fi +fi + +echo "Microsoft (R) .NET Core SDK version $("$DOTNET_EXE" --version)" + +"$DOTNET_EXE" build "$BUILD_PROJECT_FILE" /nodeReuse:false +"$DOTNET_EXE" run --project "$BUILD_PROJECT_FILE" --no-build -- "$@" diff --git a/build/.editorconfig b/build/.editorconfig new file mode 100644 index 0000000..94be682 --- /dev/null +++ b/build/.editorconfig @@ -0,0 +1,10 @@ +[*.cs] +dotnet_style_qualification_for_field = false:warning +dotnet_style_qualification_for_property = false:warning +dotnet_style_qualification_for_method = false:warning +dotnet_style_qualification_for_event = false:warning +dotnet_style_require_accessibility_modifiers = never:warning + +csharp_style_expression_bodied_properties = true:warning +csharp_style_expression_bodied_indexers = true:warning +csharp_style_expression_bodied_accessors = true:warning diff --git a/build/Build.cs b/build/Build.cs new file mode 100644 index 0000000..3fc7b00 --- /dev/null +++ b/build/Build.cs @@ -0,0 +1,62 @@ +using Nuke.Common; +using Nuke.Common.Execution; +using Nuke.Common.IO; +using Nuke.Common.ProjectModel; +using Nuke.Common.Tools.DotNet; +using Nuke.Common.Utilities.Collections; +using static Nuke.Common.IO.FileSystemTasks; +using static Nuke.Common.Tools.DotNet.DotNetTasks; + +[CheckBuildProjectConfigurations] +[UnsetVisualStudioEnvironmentVariables] +class Build : NukeBuild { + + public static int Main() => Execute(x => x.Publish); + + [Parameter("Configuration to build - Default is 'Debug' (local) or 'Release' (server)")] + readonly Configuration Configuration = IsLocalBuild ? Configuration.Debug : Configuration.Release; + + [Solution] readonly Solution Solution; + + AbsolutePath SourceDirectory => RootDirectory / "src"; + AbsolutePath OutputDirectory => RootDirectory / "output"; + + Target Clean => _ => _ + .Before(Restore) + .Executes(() => { + SourceDirectory.GlobDirectories("**/bin", "**/obj").ForEach(DeleteDirectory); + EnsureCleanDirectory(OutputDirectory); + }); + + Target Restore => _ => _ + .Executes(() => { + DotNetRestore(s => s + .SetProjectFile(Solution)); + }); + + Target Compile => _ => _ + .DependsOn(Restore) + .Executes(() => { + DotNetBuild(s => s + .SetProjectFile(Solution) + .SetConfiguration(Configuration) + .EnableNoRestore()); + }); + + Target Test => _ => _ + .DependsOn(Compile) + .Executes(() => { + DotNetTest(s => s + .SetProjectFile(Solution) + .EnableNoRestore()); + }); + + Target Publish => _ => _ + .DependsOn(Test) + .Executes(() => { + DotNetPublish(s => s + .SetProject(SourceDirectory / "Robware.Api.Auth/Robware.Api.Auth.csproj") + .SetConfiguration(Configuration) + .SetOutput(OutputDirectory)); + }); +} diff --git a/build/_build.csproj b/build/_build.csproj new file mode 100644 index 0000000..3bc05f7 --- /dev/null +++ b/build/_build.csproj @@ -0,0 +1,34 @@ + + + + Exe + netcoreapp3.1 + false + + False + CS0649;CS0169 + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/clean-config.sh b/clean-config.sh new file mode 100644 index 0000000..581bc42 --- /dev/null +++ b/clean-config.sh @@ -0,0 +1,5 @@ +#!/bin/sh + +sed \ +-e 's/"database": "Server=.*;User ID=.*;Password=.*;Database=.*"/"database": "Server=localhost;User ID=user;Password=pass;Database=db"/g' \ +$1 \ No newline at end of file diff --git a/src/Robware.Api.Auth.Tests/AuthControllerTests.cs b/src/Robware.Api.Auth.Tests/AuthControllerTests.cs new file mode 100644 index 0000000..e906a0c --- /dev/null +++ b/src/Robware.Api.Auth.Tests/AuthControllerTests.cs @@ -0,0 +1,67 @@ +using System.Threading.Tasks; +using FluentAssertions; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using NSubstitute; +using Robware.Api.Auth.Controllers; +using Robware.Api.Auth.Models; +using Robware.Auth; +using Xunit; + +namespace Robware.Api.Auth.Tests { + public class AuthControllerTests { + private class TestUser : User { + public TestUser(string username, string password) { + Username = username; + Password = password; + } + } + + [Fact] + public async Task Authenticate_WithSuccessfulLoginRequest_ReturnsUser() { + var logger = Substitute.For>(); + var authenticator = Substitute.For(); + authenticator.Authenticate("username", "password").Returns((AuthenticationResult.Success, new TestUser("username", "password"))); + + var request = new LoginRequest { + Username = "username", + Password = "password" + }; + + var expectation = new TestUser("username", "password"); + + var controller = new AuthController(logger, authenticator); + (await controller.Authenticate(request)).Value.Should().BeEquivalentTo(expectation); + } + + [Fact] + public async Task Authenticate_WithIncorrectPassword_Returns401() { + var logger = Substitute.For>(); + var authenticator = Substitute.For(); + authenticator.Authenticate("username", "password").Returns((AuthenticationResult.IncorrectPassword, null)); + + var request = new LoginRequest { + Username = "username", + Password = "password" + }; + + var controller = new AuthController(logger, authenticator); + (await controller.Authenticate(request)).Result.Should().BeOfType(); + } + + [Fact] + public async Task Authenticate_WithIncorrectPassword_Returns404() { + var logger = Substitute.For>(); + var authenticator = Substitute.For(); + authenticator.Authenticate("username", "password").Returns((AuthenticationResult.NotFound, null)); + + var request = new LoginRequest { + Username = "username", + Password = "password" + }; + + var controller = new AuthController(logger, authenticator); + (await controller.Authenticate(request)).Result.Should().BeOfType(); + } + } +} diff --git a/src/Robware.Api.Auth.Tests/Robware.Api.Auth.Tests.csproj b/src/Robware.Api.Auth.Tests/Robware.Api.Auth.Tests.csproj new file mode 100644 index 0000000..a2e56d7 --- /dev/null +++ b/src/Robware.Api.Auth.Tests/Robware.Api.Auth.Tests.csproj @@ -0,0 +1,22 @@ + + + + netcoreapp3.1 + + false + + + + + + + + + + + + + + + + diff --git a/src/Robware.Api.Auth.sln b/src/Robware.Api.Auth.sln new file mode 100644 index 0000000..c3e6c6e --- /dev/null +++ b/src/Robware.Api.Auth.sln @@ -0,0 +1,53 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.29613.14 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Robware.Api.Auth", "Robware.Api.Auth\Robware.Api.Auth.csproj", "{D21F1402-6526-4BD7-8ADA-F8DC626D6D5A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Robware.Api.Auth.Tests", "Robware.Api.Auth.Tests\Robware.Api.Auth.Tests.csproj", "{88716F73-0264-44D9-970D-4134C644C7EF}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Robware.Auth", "Robware.Auth\Robware.Auth.csproj", "{8740FE72-12D7-4039-9EB3-0417E529A10E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Robware.Auth.Tests", "Robware.Auth.Tests\Robware.Auth.Tests.csproj", "{E229DE31-8DBB-4AED-9461-A04C8DE0F074}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Robware.Data", "Robware.Data\Robware.Data.csproj", "{69989FA2-BEE8-491D-97B9-856D4916D154}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "_build", "..\build\_build.csproj", "{19A36DA9-BFBF-4988-B7C7-4808D6B57246}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {19A36DA9-BFBF-4988-B7C7-4808D6B57246}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {19A36DA9-BFBF-4988-B7C7-4808D6B57246}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D21F1402-6526-4BD7-8ADA-F8DC626D6D5A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D21F1402-6526-4BD7-8ADA-F8DC626D6D5A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D21F1402-6526-4BD7-8ADA-F8DC626D6D5A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D21F1402-6526-4BD7-8ADA-F8DC626D6D5A}.Release|Any CPU.Build.0 = Release|Any CPU + {88716F73-0264-44D9-970D-4134C644C7EF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {88716F73-0264-44D9-970D-4134C644C7EF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {88716F73-0264-44D9-970D-4134C644C7EF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {88716F73-0264-44D9-970D-4134C644C7EF}.Release|Any CPU.Build.0 = Release|Any CPU + {8740FE72-12D7-4039-9EB3-0417E529A10E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8740FE72-12D7-4039-9EB3-0417E529A10E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8740FE72-12D7-4039-9EB3-0417E529A10E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8740FE72-12D7-4039-9EB3-0417E529A10E}.Release|Any CPU.Build.0 = Release|Any CPU + {E229DE31-8DBB-4AED-9461-A04C8DE0F074}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E229DE31-8DBB-4AED-9461-A04C8DE0F074}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E229DE31-8DBB-4AED-9461-A04C8DE0F074}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E229DE31-8DBB-4AED-9461-A04C8DE0F074}.Release|Any CPU.Build.0 = Release|Any CPU + {69989FA2-BEE8-491D-97B9-856D4916D154}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {69989FA2-BEE8-491D-97B9-856D4916D154}.Debug|Any CPU.Build.0 = Debug|Any CPU + {69989FA2-BEE8-491D-97B9-856D4916D154}.Release|Any CPU.ActiveCfg = Release|Any CPU + {69989FA2-BEE8-491D-97B9-856D4916D154}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {75A89B46-CAE8-45F8-9BEF-3B7A6FD0BC72} + EndGlobalSection +EndGlobal diff --git a/src/Robware.Api.Auth/Controllers/AuthController.cs b/src/Robware.Api.Auth/Controllers/AuthController.cs new file mode 100644 index 0000000..fa3bd1f --- /dev/null +++ b/src/Robware.Api.Auth/Controllers/AuthController.cs @@ -0,0 +1,35 @@ +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using Robware.Api.Auth.Models; +using Robware.Auth; + +namespace Robware.Api.Auth.Controllers { + [ApiController] + [Route("[controller]")] + public class AuthController : ControllerBase { + private readonly ILogger _logger; + private readonly IAuthenticator _authenticator; + + public AuthController(ILogger logger, IAuthenticator authenticator) { + _logger = logger; + _authenticator = authenticator; + } + + [HttpPost(nameof(Authenticate))] + public async Task> Authenticate(LoginRequest request) { + var (result, user) = await _authenticator.Authenticate(request.Username, request.Password); + switch (result) { + case AuthenticationResult.Success: + return user; + case AuthenticationResult.NotFound: + return NotFound(); + case AuthenticationResult.IncorrectPassword: + return Unauthorized(); + default: + throw new ArgumentOutOfRangeException(); + } + } + } +} diff --git a/src/Robware.Api.Auth/Models/LoginRequest.cs b/src/Robware.Api.Auth/Models/LoginRequest.cs new file mode 100644 index 0000000..f421bc0 --- /dev/null +++ b/src/Robware.Api.Auth/Models/LoginRequest.cs @@ -0,0 +1,6 @@ +namespace Robware.Api.Auth.Models { + public class LoginRequest { + public string Username { get; set; } + public string Password { get; set; } + } +} \ No newline at end of file diff --git a/src/Robware.Api.Auth/Program.cs b/src/Robware.Api.Auth/Program.cs new file mode 100644 index 0000000..0ba2af9 --- /dev/null +++ b/src/Robware.Api.Auth/Program.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; + +namespace Robware.Api.Auth { + public class Program { + public static void Main(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => { + webBuilder.UseStartup(); + }).Build().Run(); + } +} diff --git a/src/Robware.Api.Auth/Properties/launchSettings.json b/src/Robware.Api.Auth/Properties/launchSettings.json new file mode 100644 index 0000000..d81ccfd --- /dev/null +++ b/src/Robware.Api.Auth/Properties/launchSettings.json @@ -0,0 +1,28 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:61069", + "sslPort": 44309 + } + }, + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "Robware.Api.Auth": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "weatherforecast", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:5001;http://localhost:5000" + } + } +} \ No newline at end of file diff --git a/src/Robware.Api.Auth/Robware.Api.Auth.csproj b/src/Robware.Api.Auth/Robware.Api.Auth.csproj new file mode 100644 index 0000000..a22588c --- /dev/null +++ b/src/Robware.Api.Auth/Robware.Api.Auth.csproj @@ -0,0 +1,13 @@ + + + + netcoreapp3.1 + + + + + + + + + diff --git a/src/Robware.Api.Auth/Startup.cs b/src/Robware.Api.Auth/Startup.cs new file mode 100644 index 0000000..3787933 --- /dev/null +++ b/src/Robware.Api.Auth/Startup.cs @@ -0,0 +1,44 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Robware.Auth; +using Robware.Data; + +namespace Robware.Api.Auth { + public class Startup { + public Startup(IConfiguration configuration) { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) { + services.AddControllers(); + + services.AddSingleton() + .AddSingleton() + .AddSingleton(new MySQLDatabaseProvider(Configuration.GetConnectionString("database"))) + .AddSingleton(); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { + if (env.IsDevelopment()) { + app.UseDeveloperExceptionPage(); + } + + app.UseHttpsRedirection(); + + app.UseRouting(); + + app.UseAuthorization(); + + app.UseEndpoints(endpoints => { + endpoints.MapControllers(); + }); + } + } +} diff --git a/src/Robware.Api.Auth/appsettings.Development.json b/src/Robware.Api.Auth/appsettings.Development.json new file mode 100644 index 0000000..6daac7a --- /dev/null +++ b/src/Robware.Api.Auth/appsettings.Development.json @@ -0,0 +1,12 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "ConnectionStrings": { + "database": "Server=localhost;User ID=user;Password=pass;Database=db" + } +} diff --git a/src/Robware.Api.Auth/appsettings.json b/src/Robware.Api.Auth/appsettings.json new file mode 100644 index 0000000..8769624 --- /dev/null +++ b/src/Robware.Api.Auth/appsettings.json @@ -0,0 +1,20 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "Kestrel": { + "EndPoints": { + "Http": { + "Url": "http://0.0.0.0:5003" + } + } + }, + "AllowedHosts": "*", + "ConnectionStrings": { + "database": "" + } +} diff --git a/src/Robware.Auth.Tests/AuthenticatorTests.cs b/src/Robware.Auth.Tests/AuthenticatorTests.cs new file mode 100644 index 0000000..1fa4677 --- /dev/null +++ b/src/Robware.Auth.Tests/AuthenticatorTests.cs @@ -0,0 +1,50 @@ +using System.Threading.Tasks; +using FluentAssertions; +using NSubstitute; +using NSubstitute.ExceptionExtensions; +using Robware.Data; +using Xunit; + +namespace Robware.Auth.Tests { + public class AuthenticatorTests { + [Fact] + public async Task Authenticate_ForUserThatExistsWithCorrectPassword_ReturnsOkResultWithUser() { + var users = Substitute.For(); + var crypto = Substitute.For(); + crypto.Encrypt("password").Returns("password"); + + var user = new TestUser("test", "password"); + users.GetByEmail("test").Returns(user); + + var auth = new Authenticator(users, crypto); + (await auth.Authenticate("test", "password")).Should().BeEquivalentTo((AuthenticationResult.Success, user)); + } + + [Fact] + public async Task Authenticate_ForUserThatExistsWithIncorrectPassword_ReturnsIncorrectPassword() { + var users = Substitute.For(); + var crypto = Substitute.For(); + crypto.Encrypt("password").Returns("password"); + + var user = new TestUser("test", "password"); + users.GetByEmail("test").Returns(user); + + var auth = new Authenticator(users, crypto); + + (await auth.Authenticate("test", "wrong")).Should().BeEquivalentTo((AuthenticationResult.IncorrectPassword, null as User)); + } + + [Fact] + public async Task Authenticate_ForUserThatDoesntExist_ReturnsNotFound() { + var users = Substitute.For(); + users.GetByEmail("test").Throws(new UserNotFoundException("")); + + var crypto = Substitute.For(); + crypto.Encrypt("password").Returns("password"); + + var auth = new Authenticator(users, crypto); + + (await auth.Authenticate("test", "password")).Should().BeEquivalentTo((AuthenticationResult.NotFound, null as User)); + } + } +} \ No newline at end of file diff --git a/src/Robware.Auth.Tests/CryoptographyProviderTests.cs b/src/Robware.Auth.Tests/CryoptographyProviderTests.cs new file mode 100644 index 0000000..e25941c --- /dev/null +++ b/src/Robware.Auth.Tests/CryoptographyProviderTests.cs @@ -0,0 +1,12 @@ +using FluentAssertions; +using Xunit; + +namespace Robware.Auth.Tests { + public class CryoptographyProviderTests { + [Fact] + public void Encrypt_WithInput_ReturnsHash() { + var provider = new CryptographyProvider(); + provider.Encrypt("password").Should().Be("5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8"); + } + } +} \ No newline at end of file diff --git a/src/Robware.Auth.Tests/Robware.Auth.Tests.csproj b/src/Robware.Auth.Tests/Robware.Auth.Tests.csproj new file mode 100644 index 0000000..fdadf85 --- /dev/null +++ b/src/Robware.Auth.Tests/Robware.Auth.Tests.csproj @@ -0,0 +1,23 @@ + + + + netcoreapp3.1 + + false + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Robware.Auth.Tests/TestUser.cs b/src/Robware.Auth.Tests/TestUser.cs new file mode 100644 index 0000000..691e2a2 --- /dev/null +++ b/src/Robware.Auth.Tests/TestUser.cs @@ -0,0 +1,8 @@ +namespace Robware.Auth.Tests { + internal class TestUser : User { + public TestUser(string username, string password) { + Username = username; + Password = password; + } + } +} \ No newline at end of file diff --git a/src/Robware.Auth/Authenticator.cs b/src/Robware.Auth/Authenticator.cs new file mode 100644 index 0000000..7ee0475 --- /dev/null +++ b/src/Robware.Auth/Authenticator.cs @@ -0,0 +1,31 @@ +using System; +using System.Threading.Tasks; + +namespace Robware.Auth { + public class Authenticator : IAuthenticator { + private readonly IUsers _users; + private readonly ICryptographyProvider _crypto; + + public Authenticator(IUsers users, ICryptographyProvider crypto) { + _users = users; + _crypto = crypto; + } + + public async Task<(AuthenticationResult Result, User User)> Authenticate(string username, string password) { + try { + var user = await _users.GetByEmail(username); + return _crypto.Encrypt(password) == user.Password ? (AuthenticationResult.Success, user) : (AuthenticationResult.IncorrectPassword, null); + } + catch (UserNotFoundException) { + return (AuthenticationResult.NotFound, null); + } + } + } + + public enum AuthenticationResult { + Unknown, + Success, + NotFound, + IncorrectPassword + } +} \ No newline at end of file diff --git a/src/Robware.Auth/CryptographyProvider.cs b/src/Robware.Auth/CryptographyProvider.cs new file mode 100644 index 0000000..57f61a1 --- /dev/null +++ b/src/Robware.Auth/CryptographyProvider.cs @@ -0,0 +1,19 @@ +using System.Security.Cryptography; +using System.Text; + +namespace Robware.Auth { + public class CryptographyProvider : ICryptographyProvider { + public string Encrypt(string input) { + using (var sha256 = SHA256.Create()) { + var hash = sha256.ComputeHash(Encoding.UTF8.GetBytes(input)); + + var builder = new StringBuilder(); + foreach (var b in hash) + builder.Append(b.ToString("x2")); + var hashString = builder.ToString(); + + return hashString; + } + } + } +} \ No newline at end of file diff --git a/src/Robware.Auth/IAuthenticator.cs b/src/Robware.Auth/IAuthenticator.cs new file mode 100644 index 0000000..4718d67 --- /dev/null +++ b/src/Robware.Auth/IAuthenticator.cs @@ -0,0 +1,7 @@ +using System.Threading.Tasks; + +namespace Robware.Auth { + public interface IAuthenticator { + Task<(AuthenticationResult Result, User User)> Authenticate(string username, string password); + } +} \ No newline at end of file diff --git a/src/Robware.Auth/ICryptographyProvider.cs b/src/Robware.Auth/ICryptographyProvider.cs new file mode 100644 index 0000000..bb1c3f9 --- /dev/null +++ b/src/Robware.Auth/ICryptographyProvider.cs @@ -0,0 +1,5 @@ +namespace Robware.Auth { + public interface ICryptographyProvider { + string Encrypt(string input); + } +} \ No newline at end of file diff --git a/src/Robware.Auth/IUsers.cs b/src/Robware.Auth/IUsers.cs new file mode 100644 index 0000000..b7f8f5a --- /dev/null +++ b/src/Robware.Auth/IUsers.cs @@ -0,0 +1,7 @@ +using System.Threading.Tasks; + +namespace Robware.Auth { + public interface IUsers { + Task GetByEmail(string email); + } +} \ No newline at end of file diff --git a/src/Robware.Auth/Robware.Auth.csproj b/src/Robware.Auth/Robware.Auth.csproj new file mode 100644 index 0000000..cb63190 --- /dev/null +++ b/src/Robware.Auth/Robware.Auth.csproj @@ -0,0 +1,7 @@ + + + + netcoreapp3.1 + + + diff --git a/src/Robware.Auth/User.cs b/src/Robware.Auth/User.cs new file mode 100644 index 0000000..cfb8a65 --- /dev/null +++ b/src/Robware.Auth/User.cs @@ -0,0 +1,6 @@ +namespace Robware.Auth { + public class User { + public string Username { get; protected set; } + public string Password { get; protected set; } + } +} \ No newline at end of file diff --git a/src/Robware.Auth/UserNotFoundException.cs b/src/Robware.Auth/UserNotFoundException.cs new file mode 100644 index 0000000..dde2acb --- /dev/null +++ b/src/Robware.Auth/UserNotFoundException.cs @@ -0,0 +1,9 @@ +using System; + +namespace Robware.Auth { + public class UserNotFoundException : Exception { + public UserNotFoundException(string username) : base("Could not find user " + username) { + + } + } +} \ No newline at end of file diff --git a/src/Robware.Data/DatabaseUser.cs b/src/Robware.Data/DatabaseUser.cs new file mode 100644 index 0000000..be76924 --- /dev/null +++ b/src/Robware.Data/DatabaseUser.cs @@ -0,0 +1,11 @@ +using Robware.Auth; +using Robware.Data.States; + +namespace Robware.Data { + public class DatabaseUser : User { + public DatabaseUser(UserState state) { + Username = state.User_Email; + Password = state.User_Password; + } + } +} \ No newline at end of file diff --git a/src/Robware.Data/IDatabaseProvider.cs b/src/Robware.Data/IDatabaseProvider.cs new file mode 100644 index 0000000..a4493e2 --- /dev/null +++ b/src/Robware.Data/IDatabaseProvider.cs @@ -0,0 +1,7 @@ +using System.Data; + +namespace Robware.Data { + public interface IDatabaseProvider { + IDbConnection NewConnection(); + } +} \ No newline at end of file diff --git a/src/Robware.Data/MySQLDatabaseProvider.cs b/src/Robware.Data/MySQLDatabaseProvider.cs new file mode 100644 index 0000000..57fdc43 --- /dev/null +++ b/src/Robware.Data/MySQLDatabaseProvider.cs @@ -0,0 +1,12 @@ +using System.Data; +using MySql.Data.MySqlClient; + +namespace Robware.Data { + public class MySQLDatabaseProvider : IDatabaseProvider { + private readonly string _connectionString; + + public MySQLDatabaseProvider(string connectionString) => _connectionString = connectionString; + + public IDbConnection NewConnection() => new MySqlConnection(_connectionString); + } +} \ No newline at end of file diff --git a/src/Robware.Data/Robware.Data.csproj b/src/Robware.Data/Robware.Data.csproj new file mode 100644 index 0000000..fc1eb18 --- /dev/null +++ b/src/Robware.Data/Robware.Data.csproj @@ -0,0 +1,16 @@ + + + + netcoreapp3.1 + + + + + + + + + + + + diff --git a/src/Robware.Data/States/UserState.cs b/src/Robware.Data/States/UserState.cs new file mode 100644 index 0000000..2dfec41 --- /dev/null +++ b/src/Robware.Data/States/UserState.cs @@ -0,0 +1,10 @@ +namespace Robware.Data.States { + public class UserState { + public string User_Id { get; set; } + public string User_Email { get; set; } + public string User_Password { get; set; } + public string User_Created { get; set; } + public string User_Deleted { get; set; } + public string Group_Id { get; set; } + } +} \ No newline at end of file diff --git a/src/Robware.Data/UserRepository.cs b/src/Robware.Data/UserRepository.cs new file mode 100644 index 0000000..4bc391d --- /dev/null +++ b/src/Robware.Data/UserRepository.cs @@ -0,0 +1,29 @@ +using System.Linq; +using System.Threading.Tasks; +using Dapper; +using Robware.Auth; +using Robware.Data.States; + +namespace Robware.Data { + public class UserRepository : IUsers { + private readonly IDatabaseProvider _dbProvider; + + public UserRepository(IDatabaseProvider dbProvider) { + _dbProvider = dbProvider; + } + + public async Task GetByEmail(string email) { + const string query = "SELECT * FROM users WHERE user_email=@email"; + + using (var connection = _dbProvider.NewConnection()) { + connection.Open(); + var result = await connection.QueryAsync(query, new { email }); + + if (!result.Any()) + throw new UserNotFoundException(email); + + return new DatabaseUser(result.Single()); + } + } + } +} \ No newline at end of file