Skip to content

Commit

Permalink
feat(users): implement demo RESTful API (#1)
Browse files Browse the repository at this point in the history
  • Loading branch information
derevnjuk authored Dec 15, 2022
1 parent 62d880c commit 00fe9a7
Show file tree
Hide file tree
Showing 20 changed files with 1,571 additions and 0 deletions.
6 changes: 6 additions & 0 deletions .config/dotnet-tools.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@
"commands": [
"husky"
]
},
"dotnet-ef": {
"version": "6.0.12",
"commands": [
"dotnet-ef"
]
}
}
}
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
POSTGRES_USER=test
POSTGRES_PASSWORD=test
BRIGHT_HOSTNAME=app.neuralegion.com
BRIGHT_TOKEN=
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,7 @@ autom4te.cache/
*.tar.gz
tarballs/
test-results/
.env

# Mac bundle stuff
*.dmg
Expand Down
11 changes: 11 additions & 0 deletions SecTesterDemo.sln
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{6D773315-E
test\Directory.Build.props = test\Directory.Build.props
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "App", "src\App\App.csproj", "{16F8C5D1-7558-4935-9AF5-3415E16AEBC2}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -21,4 +23,13 @@ Global
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{16F8C5D1-7558-4935-9AF5-3415E16AEBC2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{16F8C5D1-7558-4935-9AF5-3415E16AEBC2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{16F8C5D1-7558-4935-9AF5-3415E16AEBC2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{16F8C5D1-7558-4935-9AF5-3415E16AEBC2}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{16F8C5D1-7558-4935-9AF5-3415E16AEBC2} = {F979DD52-A989-4D70-9551-D63145CE2205}
EndGlobalSection
EndGlobal
13 changes: 13 additions & 0 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
version: '3'

services:
postgres:
image: postgres:14-alpine
restart: always
command: postgres -c fsync=off -c synchronous_commit=off -c full_page_writes=off -c random_page_cost=1.0
environment:
POSTGRES_DB: test
POSTGRES_PASSWORD: '${POSTGRES_PASSWORD:-test}'
POSTGRES_USER: '${POSTGRES_USER:-test}'
ports:
- '5432:5432'
26 changes: 26 additions & 0 deletions src/App/App.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
<UserSecretsId>45122f4e-e577-4e10-91ba-2b2de7cd77a3</UserSecretsId>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="DotNetEnv" Version="2.3.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.12" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.12">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="6.0.7" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.2.3" />
</ItemGroup>

<ItemGroup>
<Content Include="..\..\.dockerignore">
<Link>.dockerignore</Link>
</Content>
</ItemGroup>
</Project>
35 changes: 35 additions & 0 deletions src/App/Controllers/UsersController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using System.Net;
using App.Models;
using Microsoft.AspNetCore.Mvc;

namespace App.Controllers;

[ApiController]
[Route("[controller]")]
public class UsersController : ControllerBase
{
private readonly Users _users;

public UsersController(Users users)
{
_users = users;
}

[HttpPost]
[ProducesResponseType(typeof(User), (int)HttpStatusCode.Created)]
public Task<User> Create([FromBody] CreateUserDto createUserDto) => _users.Create(createUserDto);

[HttpGet]
[ProducesResponseType(typeof(IEnumerable<User>), (int)HttpStatusCode.OK)]
public Task<List<User>> FindByName([FromQuery] string name) => _users.FindByName(name);

[HttpGet]
[Route("{id}")]
[ProducesResponseType(typeof(User), (int)HttpStatusCode.OK)]
public Task<User?> FindOne(int id) => _users.FindOne(id);

[HttpDelete]
[Route("{id}")]
[ProducesResponseType((int)HttpStatusCode.NoContent)]
public Task Remove(int id) => _users.Remove(id);
}
20 changes: 20 additions & 0 deletions src/App/Dockerfile
Original file line number Diff line number Diff line change
@@ -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 ["src/App/App.csproj", "src/App/"]
RUN dotnet restore "src/App/App.csproj"
COPY . .
WORKDIR "/src/src/App"
RUN dotnet build "App.csproj" -c Release -o /app/build

FROM build AS publish
RUN dotnet publish "App.csproj" -c Release -o /app/publish

FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "App.dll"]
28 changes: 28 additions & 0 deletions src/App/Infrastructure/UserContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using App.Models;
using Microsoft.EntityFrameworkCore;

namespace App.Infrastructure;

public class UserContext : DbContext
{
public UserContext(DbContextOptions<UserContext> options) : base(options)
{
}

public DbSet<User> Users { get; set; }

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) =>
optionsBuilder.UseNpgsql(
$"Host=localhost;Username={Environment.GetEnvironmentVariable("POSTGRES_USER")};Password={Environment.GetEnvironmentVariable("POSTGRES_PASSWORD")};Database=test");

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<User>()
.Property(u => u.Name)
.IsRequired();

modelBuilder.Entity<User>()
.Property(u => u.IsActive)
.HasDefaultValue(true);
}
}
49 changes: 49 additions & 0 deletions src/App/Migrations/20221214233020_InitialCreate.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

32 changes: 32 additions & 0 deletions src/App/Migrations/20221214233020_InitialCreate.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;

#nullable disable

namespace App.Migrations;

public partial class InitialCreate : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Users",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
Name = table.Column<string>(type: "text", nullable: false),
IsActive = table.Column<bool>(type: "boolean", nullable: false, defaultValue: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Users", x => x.Id);
});
}

protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Users");
}
}
47 changes: 47 additions & 0 deletions src/App/Migrations/UserContextModelSnapshot.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// <auto-generated />
using App.Infrastructure;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;

#nullable disable

namespace App.Migrations;

[DbContext(typeof(UserContext))]
partial class UserContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "6.0.12")
.HasAnnotation("Relational:MaxIdentifierLength", 63);

NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);

modelBuilder.Entity("App.Models.User", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<bool>("IsActive")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(true);
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("Users");
});
#pragma warning restore 612, 618
}
}
6 changes: 6 additions & 0 deletions src/App/Models/CreateUserDto.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace App.Models;

public class CreateUserDto
{
public string Name { get; init; }
}
10 changes: 10 additions & 0 deletions src/App/Models/User.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace App.Models;

public class User
{
public int Id { get; set; }

public string Name { get; set; }

public bool IsActive { get; set; }
}
62 changes: 62 additions & 0 deletions src/App/Models/Users.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
using App.Infrastructure;
using Microsoft.EntityFrameworkCore;

namespace App.Models;

public class Users
{
private readonly UserContext _userContext;

public Users(UserContext userContext)
{
_userContext = userContext;
}

public async Task<User> Create(CreateUserDto payload)
{
var user = new User
{
Name = payload.Name
};

_userContext.Users.Add(user);

await _userContext.SaveChangesAsync().ConfigureAwait(false);

return user;
}

public Task<User?> FindOne(int id) => _userContext.Users.FindAsync(id).AsTask();

public async Task Remove(int id)
{
var user = _userContext.Users.SingleOrDefault(x => x.Id == id);

if (user is not null)
{
_userContext.Users.Remove(user);
await _userContext.SaveChangesAsync().ConfigureAwait(false);
}
}

/// <summary>
/// This method performs a simple SQL query that would typically search through the users table and retrieve
/// the user who has a concrete ID. However, an attacker can easily use the lack of validation from
/// user inputs to read sensitive data from the database, modify database data, or
/// execute administration operations by inputting values that the developer did not consider a valid (e.g. `1 OR 2028=2028` or `1; DROP user--`)
///
/// Using the built-in `Where` method that escapes the input passed to it automatically before it is inserted into the query,
/// you can fix the actual issue:
/// ```csharp
/// public Task<List<User>> FindByName(string name)
/// {
/// return _userContext.Users.Where(u => u.Name.Contains(name)).ToListAsync();
/// }
/// </summary>
public Task<List<User>> FindByName(string name)
{
var query = $@"select * from ""Users"" where ""Name"" ilike '{name}%'";

return _userContext.Users.FromSqlRaw(query).ToListAsync();
}
}
Loading

0 comments on commit 00fe9a7

Please sign in to comment.