stackforge-cli 0.1.4__tar.gz → 0.1.5__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. {stackforge_cli-0.1.4 → stackforge_cli-0.1.5}/PKG-INFO +3 -1
  2. {stackforge_cli-0.1.4 → stackforge_cli-0.1.5}/pyproject.toml +13 -2
  3. stackforge_cli-0.1.5/src/stackforge/engines/dotnet/api_generator.py +39 -0
  4. stackforge_cli-0.1.5/src/stackforge/engines/dotnet/config_generator.py +20 -0
  5. stackforge_cli-0.1.5/src/stackforge/engines/dotnet/docker_generator.py +15 -0
  6. {stackforge_cli-0.1.4 → stackforge_cli-0.1.5}/src/stackforge/engines/dotnet/git_generator.py +7 -56
  7. stackforge_cli-0.1.5/src/stackforge/engines/dotnet/layers_generator.py +103 -0
  8. stackforge_cli-0.1.5/src/stackforge/engines/dotnet/program_generator.py +21 -0
  9. stackforge_cli-0.1.5/src/stackforge/templates/dotnet/API/Configurations/Swagger/ApiRoles.cs.tmpl +9 -0
  10. stackforge_cli-0.1.5/src/stackforge/templates/dotnet/API/ConfigureServices.cs.tmpl +120 -0
  11. stackforge_cli-0.1.5/src/stackforge/templates/dotnet/API/Controllers/BaseApiController.cs.tmpl +24 -0
  12. stackforge_cli-0.1.5/src/stackforge/templates/dotnet/API/Controllers/Main/AccountController.cs.tmpl +68 -0
  13. stackforge_cli-0.1.5/src/stackforge/templates/dotnet/API/DTOs/AccountDTOs.cs.tmpl +23 -0
  14. stackforge_cli-0.1.5/src/stackforge/templates/dotnet/API/Interface/IJwtService.cs.tmpl +7 -0
  15. stackforge_cli-0.1.5/src/stackforge/templates/dotnet/API/Options/JwtOptions.cs.tmpl +10 -0
  16. stackforge_cli-0.1.5/src/stackforge/templates/dotnet/API/Program.cs.tmpl +45 -0
  17. stackforge_cli-0.1.5/src/stackforge/templates/dotnet/API/Properties/launchSettings.json.tmpl +31 -0
  18. stackforge_cli-0.1.5/src/stackforge/templates/dotnet/API/Services/JwtService.cs.tmpl +41 -0
  19. stackforge_cli-0.1.5/src/stackforge/templates/dotnet/API/Swagger/EnumMemberSchemaFilter.cs.tmpl +25 -0
  20. stackforge_cli-0.1.5/src/stackforge/templates/dotnet/API/appsettings.Development.json.tmpl +17 -0
  21. stackforge_cli-0.1.5/src/stackforge/templates/dotnet/API/appsettings.json.tmpl +18 -0
  22. stackforge_cli-0.1.5/src/stackforge/templates/dotnet/Application/Common/Behaviors/ValidationBehavior.cs.tmpl +29 -0
  23. stackforge_cli-0.1.5/src/stackforge/templates/dotnet/Application/Common/Constants/ErrorMessages.cs.tmpl +10 -0
  24. stackforge_cli-0.1.5/src/stackforge/templates/dotnet/Application/Common/Exceptions/AppExceptions.cs.tmpl +12 -0
  25. stackforge_cli-0.1.5/src/stackforge/templates/dotnet/Application/Common/Exceptions/NotFoundException.cs.tmpl +10 -0
  26. stackforge_cli-0.1.5/src/stackforge/templates/dotnet/Application/Common/Models/ApiResponse.cs.tmpl +15 -0
  27. stackforge_cli-0.1.5/src/stackforge/templates/dotnet/Application/Common/Models/PagedList.cs.tmpl +30 -0
  28. stackforge_cli-0.1.5/src/stackforge/templates/dotnet/Application/Common/Models/PagedResponse.cs.tmpl +13 -0
  29. stackforge_cli-0.1.5/src/stackforge/templates/dotnet/Application/Common/Models/PaginatedResult.cs.tmpl +13 -0
  30. stackforge_cli-0.1.5/src/stackforge/templates/dotnet/Application/Common/Models/PagingParams.cs.tmpl +8 -0
  31. stackforge_cli-0.1.5/src/stackforge/templates/dotnet/Application/Common/Models/Result.cs.tmpl +30 -0
  32. stackforge_cli-0.1.5/src/stackforge/templates/dotnet/Application/Common/Models/Serialization/IsoDateTime.cs.tmpl +14 -0
  33. stackforge_cli-0.1.5/src/stackforge/templates/dotnet/Application/ConfigureServices.cs.tmpl +22 -0
  34. stackforge_cli-0.1.5/src/stackforge/templates/dotnet/Dockerfile.tmpl +8 -0
  35. stackforge_cli-0.1.5/src/stackforge/templates/dotnet/Domain/Common/BaseEntity.cs.tmpl +9 -0
  36. stackforge_cli-0.1.5/src/stackforge/templates/dotnet/Domain/Common/EnumMemberExtensions.cs.tmpl +16 -0
  37. stackforge_cli-0.1.5/src/stackforge/templates/dotnet/Domain/Entities/Identity/Role.cs.tmpl +9 -0
  38. stackforge_cli-0.1.5/src/stackforge/templates/dotnet/Domain/Entities/Identity/User.cs.tmpl +13 -0
  39. stackforge_cli-0.1.5/src/stackforge/templates/dotnet/Domain/Entities/Identity/UserRole.cs.tmpl +10 -0
  40. stackforge_cli-0.1.5/src/stackforge/templates/dotnet/Domain/GlobalUsings.cs.tmpl +2 -0
  41. stackforge_cli-0.1.5/src/stackforge/templates/dotnet/Infrastructure/ConfigureServices.cs.tmpl +14 -0
  42. stackforge_cli-0.1.5/src/stackforge/templates/dotnet/Persistence/Configurations/UserRoleConfiguration.cs.tmpl +24 -0
  43. stackforge_cli-0.1.5/src/stackforge/templates/dotnet/Persistence/ConfigureServices.cs.tmpl +18 -0
  44. stackforge_cli-0.1.5/src/stackforge/templates/dotnet/Persistence/DataContext.cs.tmpl +36 -0
  45. stackforge_cli-0.1.5/src/stackforge/templates/dotnet/Persistence/Seeders/RoleSeeder.cs.tmpl +19 -0
  46. stackforge_cli-0.1.5/src/stackforge/templates/dotnet/Persistence/Seeders/UserSeeder.cs.tmpl +28 -0
  47. stackforge_cli-0.1.5/src/stackforge/templates/dotnet/docker-compose.yml.tmpl +36 -0
  48. stackforge_cli-0.1.5/src/stackforge/templates/dotnet/dot_env.tmpl +10 -0
  49. stackforge_cli-0.1.5/src/stackforge/templates/dotnet/dot_github/workflows/sync-branches.yml.tmpl +29 -0
  50. stackforge_cli-0.1.5/src/stackforge/templates/dotnet/dot_gitignore.tmpl +15 -0
  51. stackforge_cli-0.1.5/src/stackforge/templating.py +39 -0
  52. {stackforge_cli-0.1.4 → stackforge_cli-0.1.5}/src/stackforge_cli.egg-info/PKG-INFO +3 -1
  53. stackforge_cli-0.1.5/src/stackforge_cli.egg-info/SOURCES.txt +68 -0
  54. stackforge_cli-0.1.5/src/stackforge_cli.egg-info/requires.txt +4 -0
  55. stackforge_cli-0.1.5/tests/test_dotnet_scaffold.py +104 -0
  56. stackforge_cli-0.1.5/tests/test_project_name.py +31 -0
  57. stackforge_cli-0.1.5/tests/test_templating.py +27 -0
  58. stackforge_cli-0.1.4/src/stackforge/engines/dotnet/api_generator.py +0 -314
  59. stackforge_cli-0.1.4/src/stackforge/engines/dotnet/config_generator.py +0 -77
  60. stackforge_cli-0.1.4/src/stackforge/engines/dotnet/docker_generator.py +0 -64
  61. stackforge_cli-0.1.4/src/stackforge/engines/dotnet/layers_generator.py +0 -544
  62. stackforge_cli-0.1.4/src/stackforge/engines/dotnet/program_generator.py +0 -245
  63. stackforge_cli-0.1.4/src/stackforge_cli.egg-info/SOURCES.txt +0 -22
  64. stackforge_cli-0.1.4/src/stackforge_cli.egg-info/requires.txt +0 -1
  65. {stackforge_cli-0.1.4 → stackforge_cli-0.1.5}/README.md +0 -0
  66. {stackforge_cli-0.1.4 → stackforge_cli-0.1.5}/setup.cfg +0 -0
  67. {stackforge_cli-0.1.4 → stackforge_cli-0.1.5}/src/stackforge/__init__.py +0 -0
  68. {stackforge_cli-0.1.4 → stackforge_cli-0.1.5}/src/stackforge/engines/dotnet/__init__.py +0 -0
  69. {stackforge_cli-0.1.4 → stackforge_cli-0.1.5}/src/stackforge/engines/dotnet/csproj_generator.py +0 -0
  70. {stackforge_cli-0.1.4 → stackforge_cli-0.1.5}/src/stackforge/engines/dotnet/scaffolder.py +0 -0
  71. {stackforge_cli-0.1.4 → stackforge_cli-0.1.5}/src/stackforge/project_name.py +0 -0
  72. {stackforge_cli-0.1.4 → stackforge_cli-0.1.5}/src/stackforge/scaffold.py +0 -0
  73. {stackforge_cli-0.1.4 → stackforge_cli-0.1.5}/src/stackforge/ui.py +0 -0
  74. {stackforge_cli-0.1.4 → stackforge_cli-0.1.5}/src/stackforge/version.py +0 -0
  75. {stackforge_cli-0.1.4 → stackforge_cli-0.1.5}/src/stackforge_cli.egg-info/dependency_links.txt +0 -0
  76. {stackforge_cli-0.1.4 → stackforge_cli-0.1.5}/src/stackforge_cli.egg-info/entry_points.txt +0 -0
  77. {stackforge_cli-0.1.4 → stackforge_cli-0.1.5}/src/stackforge_cli.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: stackforge-cli
3
- Version: 0.1.4
3
+ Version: 0.1.5
4
4
  Summary: A CLI scaffolder for .NET Clean Architecture projects
5
5
  License: MIT
6
6
  Keywords: scaffolder,dotnet,clean-architecture,cli
@@ -11,6 +11,8 @@ Classifier: Environment :: Console
11
11
  Requires-Python: >=3.10
12
12
  Description-Content-Type: text/markdown
13
13
  Requires-Dist: colorama>=0.4.6
14
+ Provides-Extra: test
15
+ Requires-Dist: pytest>=8; extra == "test"
14
16
 
15
17
  # StackForge
16
18
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "stackforge-cli"
7
- version = "0.1.4"
7
+ version = "0.1.5"
8
8
  description = "A CLI scaffolder for .NET Clean Architecture projects"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -20,8 +20,19 @@ dependencies = [
20
20
  "colorama>=0.4.6",
21
21
  ]
22
22
 
23
+ [project.optional-dependencies]
24
+ test = ["pytest>=8"]
25
+
23
26
  [project.scripts]
24
27
  stackforge = "stackforge.scaffold:main"
25
28
 
26
29
  [tool.setuptools.packages.find]
27
- where = ["src"]
30
+ where = ["src"]
31
+
32
+ [tool.setuptools.package-data]
33
+ stackforge = ["templates/**/*.tmpl"]
34
+
35
+ [tool.pytest.ini_options]
36
+ testpaths = ["tests"]
37
+ pythonpath = ["src", "tests"]
38
+ markers = ["dotnet: builds the generated solution with the .NET SDK (slow)"]
@@ -0,0 +1,39 @@
1
+ from pathlib import Path
2
+ from stackforge import ui
3
+ from stackforge.templating import render_file
4
+
5
+ ENGINE = "dotnet"
6
+
7
+ API_FOLDERS = [
8
+ "API/Configurations/Swagger",
9
+ "API/Controllers/Main",
10
+ "API/Helpers",
11
+ "API/Interface",
12
+ "API/Options",
13
+ "API/Properties",
14
+ "API/Services",
15
+ "API/Swagger",
16
+ "API/DTOs",
17
+ ]
18
+
19
+ API_TEMPLATES = [
20
+ "API/Controllers/BaseApiController.cs",
21
+ "API/Options/JwtOptions.cs",
22
+ "API/Interface/IJwtService.cs",
23
+ "API/Services/JwtService.cs",
24
+ "API/Swagger/EnumMemberSchemaFilter.cs",
25
+ "API/Configurations/Swagger/ApiRoles.cs",
26
+ "API/Properties/launchSettings.json",
27
+ "API/DTOs/AccountDTOs.cs",
28
+ "API/Controllers/Main/AccountController.cs",
29
+ ]
30
+
31
+
32
+ def generate_api_structure(base: Path, project: str):
33
+ for folder in API_FOLDERS:
34
+ (base / folder).mkdir(parents=True, exist_ok=True)
35
+
36
+ for template in API_TEMPLATES:
37
+ render_file(ENGINE, base, template, project)
38
+
39
+ ui.file_created("API/ structure and files")
@@ -0,0 +1,20 @@
1
+ from pathlib import Path
2
+ from stackforge import ui
3
+ from stackforge.templating import render_file
4
+
5
+ ENGINE = "dotnet"
6
+
7
+
8
+ def generate_env(base: Path, project: str):
9
+ render_file(ENGINE, base, ".env", project)
10
+ ui.file_created(".env")
11
+
12
+
13
+ def generate_appsettings(base: Path, project: str):
14
+ render_file(ENGINE, base, "API/appsettings.json", project)
15
+ ui.file_created("API/appsettings.json")
16
+
17
+
18
+ def generate_appsettings_development(base: Path, project: str):
19
+ render_file(ENGINE, base, "API/appsettings.Development.json", project)
20
+ ui.file_created("API/appsettings.Development.json")
@@ -0,0 +1,15 @@
1
+ from pathlib import Path
2
+ from stackforge import ui
3
+ from stackforge.templating import render_file
4
+
5
+ ENGINE = "dotnet"
6
+
7
+
8
+ def generate_dockerfile(base: Path, project: str = ""):
9
+ render_file(ENGINE, base, "Dockerfile", project)
10
+ ui.file_created("Dockerfile")
11
+
12
+
13
+ def generate_docker_compose(base: Path, project: str):
14
+ render_file(ENGINE, base, "docker-compose.yml", project)
15
+ ui.file_created("docker-compose.yml")
@@ -2,6 +2,9 @@ import uuid
2
2
  import subprocess
3
3
  from pathlib import Path
4
4
  from stackforge import ui
5
+ from stackforge.templating import render_file
6
+
7
+ ENGINE = "dotnet"
5
8
 
6
9
  def generate_sln(base: Path, project: str):
7
10
  layers = ["API", "Application", "Domain", "Infrastructure", "Persistence"]
@@ -37,65 +40,13 @@ EndGlobal
37
40
  ui.file_created(f"{project}.sln")
38
41
 
39
42
 
40
- def generate_gitignore(base: Path):
41
- content = """# Build results
42
- bin/
43
- obj/
44
- out/
45
-
46
- # Environment
47
- .env
48
-
49
- # VS / Rider
50
- .vs/
51
- .idea/
52
- *.user
53
-
54
- # Logs
55
- *.log
56
- """
57
-
58
- file_path = base / ".gitignore"
59
- file_path.write_text(content)
43
+ def generate_gitignore(base: Path, project: str = ""):
44
+ render_file(ENGINE, base, ".gitignore", project)
60
45
  ui.file_created(".gitignore")
61
46
 
62
47
 
63
- def generate_github_workflow(base: Path):
64
- workflow_dir = base / ".github" / "workflows"
65
- workflow_dir.mkdir(parents=True, exist_ok=True)
66
-
67
- content = """name: Sync Environment Branches
68
-
69
- on:
70
- push:
71
- branches:
72
- - main
73
-
74
- permissions:
75
- contents: write
76
-
77
- jobs:
78
- sync-branches:
79
- runs-on: ubuntu-latest
80
- steps:
81
- - uses: actions/checkout@v4
82
- with:
83
- fetch-depth: 0
84
-
85
- - name: Create staging branch if missing
86
- run: |
87
- if ! git ls-remote --exit-code origin refs/heads/staging; then
88
- git push origin HEAD:refs/heads/staging
89
- fi
90
-
91
- - name: Create development branch if missing
92
- run: |
93
- if ! git ls-remote --exit-code origin refs/heads/development; then
94
- git push origin HEAD:refs/heads/development
95
- fi
96
- """
97
- file_path = workflow_dir / "sync-branches.yml"
98
- file_path.write_text(content.lstrip())
48
+ def generate_github_workflow(base: Path, project: str = ""):
49
+ render_file(ENGINE, base, ".github/workflows/sync-branches.yml", project)
99
50
  ui.file_created(".github/workflows/sync-branches.yml")
100
51
 
101
52
 
@@ -0,0 +1,103 @@
1
+ from pathlib import Path
2
+ from stackforge import ui
3
+ from stackforge.templating import render_file
4
+
5
+ ENGINE = "dotnet"
6
+
7
+
8
+ def _generate_layer(base: Path, project: str, layer: str, folders: list[str], templates: list[str]):
9
+ for folder in folders:
10
+ (base / folder).mkdir(parents=True, exist_ok=True)
11
+
12
+ for template in templates:
13
+ render_file(ENGINE, base, template, project)
14
+
15
+ ui.file_created(f"{layer}/ structure and files")
16
+
17
+
18
+ def generate_application_layer(base: Path, project: str):
19
+ _generate_layer(
20
+ base,
21
+ project,
22
+ "Application",
23
+ folders=[
24
+ "Application/Common/Behaviors",
25
+ "Application/Common/Constants",
26
+ "Application/Common/Exceptions",
27
+ "Application/Common/Models/Serialization",
28
+ "Application/Helpers",
29
+ "Application/Interfaces",
30
+ "Application/Mappings",
31
+ "Application/Services",
32
+ ],
33
+ templates=[
34
+ "Application/Common/Behaviors/ValidationBehavior.cs",
35
+ "Application/Common/Constants/ErrorMessages.cs",
36
+ "Application/Common/Exceptions/AppExceptions.cs",
37
+ "Application/Common/Exceptions/NotFoundException.cs",
38
+ "Application/Common/Models/ApiResponse.cs",
39
+ "Application/Common/Models/Result.cs",
40
+ "Application/Common/Models/PagingParams.cs",
41
+ "Application/Common/Models/PaginatedResult.cs",
42
+ "Application/Common/Models/PagedList.cs",
43
+ "Application/Common/Models/PagedResponse.cs",
44
+ "Application/Common/Models/Serialization/IsoDateTime.cs",
45
+ ],
46
+ )
47
+
48
+
49
+ def generate_domain_layer(base: Path, project: str):
50
+ _generate_layer(
51
+ base,
52
+ project,
53
+ "Domain",
54
+ folders=[
55
+ "Domain/Common",
56
+ "Domain/Entities/Identity",
57
+ "Domain/Enums",
58
+ "Domain/Utils",
59
+ ],
60
+ templates=[
61
+ "Domain/Common/BaseEntity.cs",
62
+ "Domain/Common/EnumMemberExtensions.cs",
63
+ "Domain/Entities/Identity/Role.cs",
64
+ "Domain/Entities/Identity/User.cs",
65
+ "Domain/Entities/Identity/UserRole.cs",
66
+ "Domain/GlobalUsings.cs",
67
+ ],
68
+ )
69
+
70
+
71
+ def generate_infrastructure_layer(base: Path, project: str):
72
+ _generate_layer(
73
+ base,
74
+ project,
75
+ "Infrastructure",
76
+ folders=[
77
+ "Infrastructure/BackgroundServices",
78
+ "Infrastructure/Configurations",
79
+ "Infrastructure/Options",
80
+ "Infrastructure/Services",
81
+ ],
82
+ templates=[],
83
+ )
84
+
85
+
86
+ def generate_persistence_layer(base: Path, project: str):
87
+ _generate_layer(
88
+ base,
89
+ project,
90
+ "Persistence",
91
+ folders=[
92
+ "Persistence/Configurations",
93
+ "Persistence/Extensions",
94
+ "Persistence/Migrations",
95
+ "Persistence/Seeders",
96
+ ],
97
+ templates=[
98
+ "Persistence/DataContext.cs",
99
+ "Persistence/Seeders/RoleSeeder.cs",
100
+ "Persistence/Seeders/UserSeeder.cs",
101
+ "Persistence/Configurations/UserRoleConfiguration.cs",
102
+ ],
103
+ )
@@ -0,0 +1,21 @@
1
+ from pathlib import Path
2
+ from stackforge import ui
3
+ from stackforge.templating import render_file
4
+
5
+ ENGINE = "dotnet"
6
+
7
+ # Layers that get a ConfigureServices.Add<Layer>Services() extension.
8
+ CONFIGURE_SERVICES_LAYERS = {"Application", "Infrastructure", "Persistence", "API"}
9
+
10
+
11
+ def generate_configure_services(layer: str, base: Path, project: str):
12
+ if layer not in CONFIGURE_SERVICES_LAYERS:
13
+ return
14
+
15
+ render_file(ENGINE, base, f"{layer}/ConfigureServices.cs", project)
16
+ ui.file_created(f"{layer}/ConfigureServices.cs")
17
+
18
+
19
+ def generate_program_cs(base: Path, project: str):
20
+ render_file(ENGINE, base, "API/Program.cs", project)
21
+ ui.file_created("API/Program.cs")
@@ -0,0 +1,9 @@
1
+ namespace __PROJECT__.API.Configurations.Swagger
2
+ {
3
+ public static class ApiRoles
4
+ {
5
+ public const string SuperAdmin = "SuperAdmin";
6
+ public const string Admin = "Admin";
7
+ public const string User = "User";
8
+ }
9
+ }
@@ -0,0 +1,120 @@
1
+ using __PROJECT__.API.Interface;
2
+ using __PROJECT__.API.Options;
3
+ using __PROJECT__.API.Services;
4
+ using __PROJECT__.Domain.Entities.Identity;
5
+ using __PROJECT__.Persistence;
6
+ using Microsoft.AspNetCore.Authentication.JwtBearer;
7
+ using Microsoft.AspNetCore.Authorization;
8
+ using Microsoft.AspNetCore.Identity;
9
+ using Microsoft.IdentityModel.Tokens;
10
+ using Microsoft.OpenApi.Models;
11
+ using System.Text;
12
+ using System.Text.Json.Serialization;
13
+
14
+ namespace __PROJECT__.API
15
+ {
16
+ public static class ConfigureServices
17
+ {
18
+ public const string CorsPolicy = "Cors";
19
+
20
+ public static IServiceCollection AddApiServices(this IServiceCollection services, IConfiguration configuration)
21
+ {
22
+ services.Configure<JwtOptions>(configuration.GetSection("Jwt"));
23
+ var jwtOptions = new JwtOptions();
24
+ configuration.GetSection("Jwt").Bind(jwtOptions);
25
+
26
+ services.AddControllers()
27
+ .AddJsonOptions(options => options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()));
28
+
29
+ services.AddEndpointsApiExplorer();
30
+
31
+ services.AddSwaggerGen(config =>
32
+ {
33
+ config.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
34
+ {
35
+ Name = "Authorization",
36
+ Type = SecuritySchemeType.Http,
37
+ Scheme = "bearer",
38
+ BearerFormat = "JWT",
39
+ In = ParameterLocation.Header,
40
+ Description = "Enter your JWT token only (no 'Bearer' prefix needed)"
41
+ });
42
+
43
+ config.AddSecurityRequirement(new OpenApiSecurityRequirement
44
+ {
45
+ {
46
+ new OpenApiSecurityScheme
47
+ {
48
+ Reference = new OpenApiReference
49
+ {
50
+ Type = ReferenceType.SecurityScheme,
51
+ Id = "Bearer"
52
+ }
53
+ },
54
+ Array.Empty<string>()
55
+ }
56
+ });
57
+ });
58
+
59
+ // Browser origins allowed to call the API. Format: scheme + host (+ port if non-default), no trailing slash.
60
+ // Only add frontends you own: an allowed origin's scripts can call this API from a visitor's browser.
61
+ // Bearer tokens travel in the Authorization header, so credentials (cookies) are not allowed.
62
+ services.AddCors(options =>
63
+ {
64
+ options.AddPolicy(CorsPolicy, policy =>
65
+ {
66
+ policy.AllowAnyMethod()
67
+ .AllowAnyHeader()
68
+ .WithExposedHeaders("WWW-Authenticate", "Retry-After")
69
+ .WithOrigins(
70
+ "http://localhost:3000"
71
+ );
72
+ });
73
+ });
74
+
75
+ services.AddIdentity<User, Role>(options =>
76
+ {
77
+ options.Password.RequireDigit = true;
78
+ options.Password.RequireLowercase = true;
79
+ options.Password.RequireUppercase = true;
80
+ options.Password.RequireNonAlphanumeric = true;
81
+ options.Password.RequiredLength = 8;
82
+ })
83
+ .AddEntityFrameworkStores<DataContext>()
84
+ .AddDefaultTokenProviders();
85
+
86
+ services.AddAuthentication(options =>
87
+ {
88
+ options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
89
+ options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
90
+ })
91
+ .AddJwtBearer(options =>
92
+ {
93
+ options.TokenValidationParameters = new TokenValidationParameters
94
+ {
95
+ ValidateIssuer = true,
96
+ ValidateAudience = true,
97
+ ValidateLifetime = true,
98
+ ValidateIssuerSigningKey = true,
99
+ ValidIssuer = jwtOptions.Issuer,
100
+ ValidAudience = jwtOptions.Audience,
101
+ IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtOptions.SecretKey))
102
+ };
103
+ });
104
+
105
+ // Deny by default: endpoints without an explicit policy require an authenticated user.
106
+ // Public endpoints must opt out with [AllowAnonymous].
107
+ services.AddAuthorization(options =>
108
+ {
109
+ options.FallbackPolicy = new AuthorizationPolicyBuilder()
110
+ .RequireAuthenticatedUser()
111
+ .Build();
112
+ });
113
+
114
+ //Scopes
115
+ services.AddScoped<IJwtService, JwtService>();
116
+
117
+ return services;
118
+ }
119
+ }
120
+ }
@@ -0,0 +1,24 @@
1
+ using Microsoft.AspNetCore.Mvc;
2
+ using System.Security.Claims;
3
+
4
+ namespace __PROJECT__.API.Controllers
5
+ {
6
+ [Route("api/[controller]")]
7
+ [ApiController]
8
+ public class BaseApiController : ControllerBase
9
+ {
10
+ protected Guid GetUserId() => Guid.Parse(User?.FindFirst(ClaimTypes.NameIdentifier)?.Value!);
11
+
12
+ protected string GetCurrentEmail() => User?.FindFirstValue(ClaimTypes.Email)!;
13
+
14
+ protected Guid? GetOrganizationId()
15
+ {
16
+ var claim = User?.FindFirst("OrganizationId");
17
+
18
+ if (claim == null || !Guid.TryParse(claim.Value, out var orgId))
19
+ return null;
20
+
21
+ return orgId;
22
+ }
23
+ }
24
+ }
@@ -0,0 +1,68 @@
1
+ using __PROJECT__.API.Controllers;
2
+ using __PROJECT__.API.DTOs;
3
+ using __PROJECT__.API.Interface;
4
+ using __PROJECT__.Domain.Entities.Identity;
5
+ using Microsoft.AspNetCore.Authorization;
6
+ using Microsoft.AspNetCore.Identity;
7
+ using Microsoft.AspNetCore.Mvc;
8
+
9
+ namespace __PROJECT__.API.Controllers.Main
10
+ {
11
+ public class AccountController : BaseApiController
12
+ {
13
+ private readonly UserManager<User> _userManager;
14
+ private readonly SignInManager<User> _signInManager;
15
+ private readonly IJwtService _jwtService;
16
+
17
+ public AccountController(
18
+ UserManager<User> userManager,
19
+ SignInManager<User> signInManager,
20
+ IJwtService jwtService)
21
+ {
22
+ _userManager = userManager;
23
+ _signInManager = signInManager;
24
+ _jwtService = jwtService;
25
+ }
26
+
27
+ [AllowAnonymous]
28
+ [HttpPost("login")]
29
+ public async Task<IActionResult> Login([FromBody] LoginRequest request)
30
+ {
31
+ var user = await _userManager.FindByEmailAsync(request.Email);
32
+ if (user == null)
33
+ return Unauthorized("Invalid credentials.");
34
+
35
+ var result = await _signInManager.CheckPasswordSignInAsync(user, request.Password, false);
36
+ if (!result.Succeeded)
37
+ return Unauthorized("Invalid credentials.");
38
+
39
+ var token = _jwtService.GenerateToken(user.Id, user.Email!);
40
+
41
+ return Ok(new LoginResponse
42
+ {
43
+ Token = token,
44
+ Email = user.Email!,
45
+ FullName = $"{user.FirstName} {user.LastName}"
46
+ });
47
+ }
48
+
49
+ [Authorize]
50
+ [HttpGet("me")]
51
+ public async Task<IActionResult> Me()
52
+ {
53
+ var user = await _userManager.FindByIdAsync(GetUserId().ToString());
54
+ if (user == null)
55
+ return NotFound("User not found.");
56
+
57
+ var roles = await _userManager.GetRolesAsync(user);
58
+
59
+ return Ok(new CurrentUserResponse
60
+ {
61
+ Id = user.Id,
62
+ Email = user.Email!,
63
+ FullName = $"{user.FirstName} {user.LastName}",
64
+ Roles = roles
65
+ });
66
+ }
67
+ }
68
+ }
@@ -0,0 +1,23 @@
1
+ namespace __PROJECT__.API.DTOs
2
+ {
3
+ public class LoginRequest
4
+ {
5
+ public string Email { get; set; } = string.Empty;
6
+ public string Password { get; set; } = string.Empty;
7
+ }
8
+
9
+ public class LoginResponse
10
+ {
11
+ public string Token { get; set; } = string.Empty;
12
+ public string Email { get; set; } = string.Empty;
13
+ public string FullName { get; set; } = string.Empty;
14
+ }
15
+
16
+ public class CurrentUserResponse
17
+ {
18
+ public Guid Id { get; set; }
19
+ public string Email { get; set; } = string.Empty;
20
+ public string FullName { get; set; } = string.Empty;
21
+ public IList<string> Roles { get; set; } = new List<string>();
22
+ }
23
+ }
@@ -0,0 +1,7 @@
1
+ namespace __PROJECT__.API.Interface
2
+ {
3
+ public interface IJwtService
4
+ {
5
+ string GenerateToken(Guid userId, string email);
6
+ }
7
+ }
@@ -0,0 +1,10 @@
1
+ namespace __PROJECT__.API.Options
2
+ {
3
+ public class JwtOptions
4
+ {
5
+ public string Issuer { get; set; } = string.Empty;
6
+ public string Audience { get; set; } = string.Empty;
7
+ public string SecretKey { get; set; } = string.Empty;
8
+ public int ExpiryMinutes { get; set; }
9
+ }
10
+ }
@@ -0,0 +1,45 @@
1
+ using __PROJECT__.API;
2
+ using __PROJECT__.Application;
3
+ using __PROJECT__.Domain.Entities.Identity;
4
+ using __PROJECT__.Infrastructure;
5
+ using __PROJECT__.Persistence;
6
+ using Microsoft.AspNetCore.Identity;
7
+
8
+ var builder = WebApplication.CreateBuilder(args);
9
+
10
+ // Layer services
11
+ builder.Services.AddApplicationServices();
12
+ builder.Services.AddInfrastructureServices();
13
+ builder.Services.AddPersistenceServices(builder.Configuration);
14
+ builder.Services.AddApiServices(builder.Configuration);
15
+
16
+ var app = builder.Build();
17
+
18
+ if (app.Environment.IsDevelopment())
19
+ {
20
+ app.UseSwagger();
21
+ app.UseSwaggerUI();
22
+ }
23
+
24
+ if (!app.Environment.IsDevelopment())
25
+ {
26
+ app.UseHttpsRedirection();
27
+ }
28
+ app.UseRouting();
29
+ app.UseCors(__PROJECT__.API.ConfigureServices.CorsPolicy);
30
+ app.UseAuthentication();
31
+ app.UseAuthorization();
32
+ app.MapControllers();
33
+
34
+ using (var scope = app.Services.CreateScope())
35
+ {
36
+ var db = scope.ServiceProvider.GetRequiredService<DataContext>();
37
+ db.Database.EnsureCreated();
38
+
39
+ var roleManager = scope.ServiceProvider.GetRequiredService<RoleManager<Role>>();
40
+ var userManager = scope.ServiceProvider.GetRequiredService<UserManager<User>>();
41
+ await __PROJECT__.Persistence.Seeders.RoleSeeder.SeedAsync(roleManager);
42
+ await __PROJECT__.Persistence.Seeders.UserSeeder.SeedAsync(userManager);
43
+ }
44
+
45
+ app.Run();
@@ -0,0 +1,31 @@
1
+ {
2
+ "$schema": "https://json.schemastore.org/launchsettings.json",
3
+ "profiles": {
4
+ "http": {
5
+ "commandName": "Project",
6
+ "environmentVariables": {
7
+ "ASPNETCORE_ENVIRONMENT": "Development"
8
+ },
9
+ "dotnetRunMessages": true,
10
+ "applicationUrl": "http://localhost:5000"
11
+ },
12
+ "https": {
13
+ "commandName": "Project",
14
+ "environmentVariables": {
15
+ "ASPNETCORE_ENVIRONMENT": "Development"
16
+ },
17
+ "dotnetRunMessages": true,
18
+ "applicationUrl": "https://localhost:7013;http://localhost:5282"
19
+ },
20
+ "Container (Dockerfile)": {
21
+ "commandName": "Docker",
22
+ "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}",
23
+ "environmentVariables": {
24
+ "ASPNETCORE_HTTPS_PORTS": "8081",
25
+ "ASPNETCORE_HTTP_PORTS": "8080"
26
+ },
27
+ "publishAllPorts": true,
28
+ "useSSL": true
29
+ }
30
+ }
31
+ }