@onahhas/hello-dev 1.0.0 → 1.0.1

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 (70) hide show
  1. package/README.md +149 -11
  2. package/backend/Controllers/AccountController.cs +100 -0
  3. package/backend/Controllers/ActivityController.cs +44 -0
  4. package/backend/Controllers/AuthController.cs +127 -0
  5. package/backend/Controllers/LookupController.cs +46 -0
  6. package/backend/Controllers/TasksController.cs +652 -0
  7. package/backend/Controllers/UsersController.cs +181 -0
  8. package/backend/Data/AppDbContext.cs +93 -0
  9. package/backend/Data/DbSeeder.cs +122 -0
  10. package/backend/DevTasks.Api.csproj +13 -0
  11. package/backend/Dtos/ActivityDtos.cs +12 -0
  12. package/backend/Dtos/AuthDtos.cs +37 -0
  13. package/backend/Dtos/TaskDtos.cs +104 -0
  14. package/backend/Dtos/UserDtos.cs +29 -0
  15. package/backend/Enums/EditRequestStatus.cs +8 -0
  16. package/backend/Enums/TaskPriority.cs +8 -0
  17. package/backend/Enums/TaskState.cs +9 -0
  18. package/backend/Enums/TaskVisibility.cs +7 -0
  19. package/backend/Enums/UserRole.cs +7 -0
  20. package/backend/Extensions/ClaimsPrincipalExtensions.cs +23 -0
  21. package/backend/Models/ActivityLog.cs +12 -0
  22. package/backend/Models/AppUser.cs +17 -0
  23. package/backend/Models/TaskEditRequest.cs +31 -0
  24. package/backend/Models/TaskItem.cs +25 -0
  25. package/backend/Program.cs +138 -0
  26. package/backend/Properties/launchSettings.json +13 -0
  27. package/backend/Services/ActivityService.cs +28 -0
  28. package/backend/Services/PasswordHasher.cs +58 -0
  29. package/backend/Services/TokenService.cs +49 -0
  30. package/backend/appsettings.Development.json +10 -0
  31. package/backend/appsettings.json +24 -0
  32. package/frontend/index.html +12 -0
  33. package/frontend/package-lock.json +1769 -0
  34. package/frontend/package.json +23 -0
  35. package/frontend/src/App.tsx +40 -0
  36. package/frontend/src/api/http.ts +75 -0
  37. package/frontend/src/auth/AuthContext.tsx +101 -0
  38. package/frontend/src/components/EditRequestModal.tsx +139 -0
  39. package/frontend/src/components/EditRequestsPanel.tsx +94 -0
  40. package/frontend/src/components/Layout.tsx +76 -0
  41. package/frontend/src/components/PageHeader.tsx +21 -0
  42. package/frontend/src/components/ProtectedRoute.tsx +14 -0
  43. package/frontend/src/components/StatCard.tsx +15 -0
  44. package/frontend/src/components/TaskCard.tsx +83 -0
  45. package/frontend/src/components/TaskDetailsModal.tsx +45 -0
  46. package/frontend/src/components/TaskFilters.tsx +67 -0
  47. package/frontend/src/components/TaskModal.tsx +159 -0
  48. package/frontend/src/components/TaskTable.tsx +68 -0
  49. package/frontend/src/components/UserModal.tsx +124 -0
  50. package/frontend/src/main.tsx +19 -0
  51. package/frontend/src/pages/ActivityPage.tsx +37 -0
  52. package/frontend/src/pages/BoardPage.tsx +75 -0
  53. package/frontend/src/pages/CalendarPage.tsx +101 -0
  54. package/frontend/src/pages/DashboardPage.tsx +131 -0
  55. package/frontend/src/pages/LoginPage.tsx +69 -0
  56. package/frontend/src/pages/ProfilePage.tsx +111 -0
  57. package/frontend/src/pages/PublicTasksPage.tsx +99 -0
  58. package/frontend/src/pages/RegisterPage.tsx +80 -0
  59. package/frontend/src/pages/TasksPage.tsx +135 -0
  60. package/frontend/src/pages/UsersPage.tsx +86 -0
  61. package/frontend/src/styles.css +596 -0
  62. package/frontend/src/theme.tsx +49 -0
  63. package/frontend/src/types.ts +78 -0
  64. package/frontend/src/utils/date.ts +30 -0
  65. package/frontend/src/utils/labels.ts +3 -0
  66. package/frontend/src/vite-env.d.ts +1 -0
  67. package/frontend/tsconfig.json +21 -0
  68. package/frontend/vite.config.ts +15 -0
  69. package/package.json +22 -9
  70. package/index.js +0 -7
@@ -0,0 +1,17 @@
1
+ using DevTasks.Api.Enums;
2
+
3
+ namespace DevTasks.Api.Models;
4
+
5
+ public sealed class AppUser
6
+ {
7
+ public Guid Id { get; set; } = Guid.NewGuid();
8
+ public string FullName { get; set; } = string.Empty;
9
+ public string Email { get; set; } = string.Empty;
10
+ public string PasswordHash { get; set; } = string.Empty;
11
+ public UserRole Role { get; set; } = UserRole.User;
12
+ public bool IsActive { get; set; } = true;
13
+ public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
14
+
15
+ public ICollection<TaskItem> CreatedTasks { get; set; } = new List<TaskItem>();
16
+ public ICollection<TaskItem> AssignedTasks { get; set; } = new List<TaskItem>();
17
+ }
@@ -0,0 +1,31 @@
1
+ using DevTasks.Api.Enums;
2
+
3
+ namespace DevTasks.Api.Models;
4
+
5
+ public sealed class TaskEditRequest
6
+ {
7
+ public Guid Id { get; set; } = Guid.NewGuid();
8
+
9
+ public int TaskItemId { get; set; }
10
+ public TaskItem? TaskItem { get; set; }
11
+
12
+ public Guid RequestedByUserId { get; set; }
13
+ public AppUser? RequestedByUser { get; set; }
14
+
15
+ public string? Title { get; set; }
16
+ public string? Description { get; set; }
17
+ public TaskState? Status { get; set; }
18
+ public TaskPriority? Priority { get; set; }
19
+ public TaskVisibility? Visibility { get; set; }
20
+ public DateTime? DueDate { get; set; }
21
+ public bool ClearDueDate { get; set; }
22
+ public Guid? AssignedToUserId { get; set; }
23
+ public AppUser? AssignedToUser { get; set; }
24
+ public bool ClearAssignedUser { get; set; }
25
+
26
+ public EditRequestStatus StatusOfRequest { get; set; } = EditRequestStatus.Pending;
27
+ public string? OwnerNote { get; set; }
28
+
29
+ public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
30
+ public DateTime? ResolvedAt { get; set; }
31
+ }
@@ -0,0 +1,25 @@
1
+ using DevTasks.Api.Enums;
2
+
3
+ namespace DevTasks.Api.Models;
4
+
5
+ public sealed class TaskItem
6
+ {
7
+ public int Id { get; set; }
8
+ public string Title { get; set; } = string.Empty;
9
+ public string Description { get; set; } = string.Empty;
10
+ public TaskState Status { get; set; } = TaskState.Todo;
11
+ public TaskPriority Priority { get; set; } = TaskPriority.Medium;
12
+ public TaskVisibility Visibility { get; set; } = TaskVisibility.Private;
13
+ public DateTime? DueDate { get; set; }
14
+
15
+ public Guid CreatedByUserId { get; set; }
16
+ public AppUser? CreatedByUser { get; set; }
17
+
18
+ public Guid? AssignedToUserId { get; set; }
19
+ public AppUser? AssignedToUser { get; set; }
20
+
21
+ public ICollection<TaskEditRequest> EditRequests { get; set; } = new List<TaskEditRequest>();
22
+
23
+ public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
24
+ public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
25
+ }
@@ -0,0 +1,138 @@
1
+ using System.Text;
2
+ using System.Text.Json.Serialization;
3
+ using System.Threading.RateLimiting;
4
+ using DevTasks.Api.Data;
5
+ using DevTasks.Api.Services;
6
+ using Microsoft.AspNetCore.Authentication.JwtBearer;
7
+ using Microsoft.AspNetCore.RateLimiting;
8
+ using Microsoft.EntityFrameworkCore;
9
+ using Microsoft.IdentityModel.Tokens;
10
+
11
+ var builder = WebApplication.CreateBuilder(args);
12
+
13
+ var dbFolder = Path.Combine(builder.Environment.ContentRootPath, "data");
14
+ Directory.CreateDirectory(dbFolder);
15
+
16
+ builder.Services
17
+ .AddControllers()
18
+ .AddJsonOptions(options =>
19
+ {
20
+ options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
21
+ });
22
+
23
+ builder.Services.AddDbContext<AppDbContext>(options =>
24
+ {
25
+ var provider = builder.Configuration["Database:Provider"] ?? "Sqlite";
26
+
27
+ if (provider.Equals("Postgres", StringComparison.OrdinalIgnoreCase) ||
28
+ provider.Equals("PostgreSQL", StringComparison.OrdinalIgnoreCase) ||
29
+ provider.Equals("Npgsql", StringComparison.OrdinalIgnoreCase))
30
+ {
31
+ var connectionString = builder.Configuration.GetConnectionString("PostgresConnection")
32
+ ?? builder.Configuration.GetConnectionString("DefaultConnection")
33
+ ?? throw new InvalidOperationException("Postgres connection string is missing.");
34
+
35
+ options.UseNpgsql(connectionString);
36
+ }
37
+ else
38
+ {
39
+ var connectionString = builder.Configuration.GetConnectionString("DefaultConnection")
40
+ ?? throw new InvalidOperationException("SQLite connection string is missing.");
41
+
42
+ options.UseSqlite(connectionString);
43
+ }
44
+ });
45
+
46
+ builder.Services.AddScoped<PasswordHasher>();
47
+ builder.Services.AddScoped<TokenService>();
48
+ builder.Services.AddScoped<ActivityService>();
49
+
50
+ builder.Services.AddCors(options =>
51
+ {
52
+ options.AddPolicy("Frontend", policy =>
53
+ {
54
+ policy
55
+ .WithOrigins("http://localhost:5173", "http://127.0.0.1:5173")
56
+ .AllowAnyHeader()
57
+ .AllowAnyMethod();
58
+ });
59
+ });
60
+
61
+ builder.Services.AddRateLimiter(options =>
62
+ {
63
+ options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
64
+
65
+ options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(context =>
66
+ {
67
+ var ipAddress = context.Connection.RemoteIpAddress?.ToString() ?? "unknown";
68
+
69
+ return RateLimitPartition.GetFixedWindowLimiter(
70
+ partitionKey: ipAddress,
71
+ factory: _ => new FixedWindowRateLimiterOptions
72
+ {
73
+ PermitLimit = 100,
74
+ Window = TimeSpan.FromMinutes(1),
75
+ QueueLimit = 0,
76
+ AutoReplenishment = true
77
+ });
78
+ });
79
+
80
+ options.OnRejected = async (context, token) =>
81
+ {
82
+ context.HttpContext.Response.ContentType = "application/json";
83
+ await context.HttpContext.Response.WriteAsJsonAsync(
84
+ new { message = "Too many requests. Please try again later." },
85
+ token);
86
+ };
87
+ });
88
+
89
+ var jwtKey = builder.Configuration["Jwt:Key"];
90
+ if (string.IsNullOrWhiteSpace(jwtKey) || jwtKey.Length < 32)
91
+ {
92
+ throw new InvalidOperationException("Jwt:Key must be at least 32 characters.");
93
+ }
94
+
95
+ builder.Services
96
+ .AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
97
+ .AddJwtBearer(options =>
98
+ {
99
+ options.TokenValidationParameters = new TokenValidationParameters
100
+ {
101
+ ValidateIssuer = true,
102
+ ValidIssuer = builder.Configuration["Jwt:Issuer"],
103
+ ValidateAudience = true,
104
+ ValidAudience = builder.Configuration["Jwt:Audience"],
105
+ ValidateLifetime = true,
106
+ ValidateIssuerSigningKey = true,
107
+ IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtKey)),
108
+ ClockSkew = TimeSpan.Zero
109
+ };
110
+ });
111
+
112
+ builder.Services.AddAuthorization();
113
+
114
+ var app = builder.Build();
115
+
116
+ app.UseCors("Frontend");
117
+ app.UseRateLimiter();
118
+ app.UseAuthentication();
119
+ app.UseAuthorization();
120
+
121
+ app.MapGet("/", () => Results.Ok(new
122
+ {
123
+ Name = "DevTasks API",
124
+ Status = "Running",
125
+ Url = "http://localhost:5058"
126
+ }));
127
+
128
+ app.MapControllers();
129
+
130
+ using (var scope = app.Services.CreateScope())
131
+ {
132
+ var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
133
+ var hasher = scope.ServiceProvider.GetRequiredService<PasswordHasher>();
134
+ await db.Database.EnsureCreatedAsync();
135
+ await DbSeeder.SeedAsync(db, hasher);
136
+ }
137
+
138
+ app.Run();
@@ -0,0 +1,13 @@
1
+ {
2
+ "profiles": {
3
+ "DevTasks.Api": {
4
+ "commandName": "Project",
5
+ "dotnetRunMessages": true,
6
+ "launchBrowser": false,
7
+ "applicationUrl": "http://localhost:5058",
8
+ "environmentVariables": {
9
+ "ASPNETCORE_ENVIRONMENT": "Development"
10
+ }
11
+ }
12
+ }
13
+ }
@@ -0,0 +1,28 @@
1
+ using DevTasks.Api.Data;
2
+ using DevTasks.Api.Models;
3
+
4
+ namespace DevTasks.Api.Services;
5
+
6
+ public sealed class ActivityService
7
+ {
8
+ private readonly AppDbContext _db;
9
+
10
+ public ActivityService(AppDbContext db)
11
+ {
12
+ _db = db;
13
+ }
14
+
15
+ public async Task AddAsync(string action, string entityName, string entityId, Guid userId)
16
+ {
17
+ _db.ActivityLogs.Add(new ActivityLog
18
+ {
19
+ Action = action,
20
+ EntityName = entityName,
21
+ EntityId = entityId,
22
+ UserId = userId,
23
+ CreatedAt = DateTime.UtcNow
24
+ });
25
+
26
+ await _db.SaveChangesAsync();
27
+ }
28
+ }
@@ -0,0 +1,58 @@
1
+ using System.Security.Cryptography;
2
+
3
+ namespace DevTasks.Api.Services;
4
+
5
+ public sealed class PasswordHasher
6
+ {
7
+ private const int SaltSize = 16;
8
+ private const int HashSize = 32;
9
+ private const int Iterations = 100_000;
10
+
11
+ public string Hash(string password)
12
+ {
13
+ if (string.IsNullOrWhiteSpace(password))
14
+ {
15
+ throw new ArgumentException("Password is required.", nameof(password));
16
+ }
17
+
18
+ var salt = RandomNumberGenerator.GetBytes(SaltSize);
19
+ var hash = Rfc2898DeriveBytes.Pbkdf2(
20
+ password,
21
+ salt,
22
+ Iterations,
23
+ HashAlgorithmName.SHA256,
24
+ HashSize);
25
+
26
+ return $"PBKDF2${Iterations}${Convert.ToBase64String(salt)}${Convert.ToBase64String(hash)}";
27
+ }
28
+
29
+ public bool Verify(string password, string storedHash)
30
+ {
31
+ if (string.IsNullOrWhiteSpace(password) || string.IsNullOrWhiteSpace(storedHash))
32
+ {
33
+ return false;
34
+ }
35
+
36
+ var parts = storedHash.Split('$');
37
+ if (parts.Length != 4 || parts[0] != "PBKDF2")
38
+ {
39
+ return false;
40
+ }
41
+
42
+ if (!int.TryParse(parts[1], out var iterations))
43
+ {
44
+ return false;
45
+ }
46
+
47
+ var salt = Convert.FromBase64String(parts[2]);
48
+ var expectedHash = Convert.FromBase64String(parts[3]);
49
+ var actualHash = Rfc2898DeriveBytes.Pbkdf2(
50
+ password,
51
+ salt,
52
+ iterations,
53
+ HashAlgorithmName.SHA256,
54
+ expectedHash.Length);
55
+
56
+ return CryptographicOperations.FixedTimeEquals(actualHash, expectedHash);
57
+ }
58
+ }
@@ -0,0 +1,49 @@
1
+ using System.IdentityModel.Tokens.Jwt;
2
+ using System.Security.Claims;
3
+ using System.Text;
4
+ using DevTasks.Api.Models;
5
+ using Microsoft.IdentityModel.Tokens;
6
+
7
+ namespace DevTasks.Api.Services;
8
+
9
+ public sealed class TokenService
10
+ {
11
+ private readonly IConfiguration _configuration;
12
+
13
+ public TokenService(IConfiguration configuration)
14
+ {
15
+ _configuration = configuration;
16
+ }
17
+
18
+ public string CreateToken(AppUser user)
19
+ {
20
+ var key = _configuration["Jwt:Key"]
21
+ ?? throw new InvalidOperationException("Jwt:Key is missing.");
22
+
23
+ var expiresDays = int.TryParse(_configuration["Jwt:ExpiresDays"], out var days)
24
+ ? days
25
+ : 7;
26
+
27
+ var claims = new List<Claim>
28
+ {
29
+ new(ClaimTypes.NameIdentifier, user.Id.ToString()),
30
+ new(ClaimTypes.Name, user.FullName),
31
+ new(ClaimTypes.Email, user.Email),
32
+ new(ClaimTypes.Role, user.Role.ToString()),
33
+ new("fullName", user.FullName)
34
+ };
35
+
36
+ var credentials = new SigningCredentials(
37
+ new SymmetricSecurityKey(Encoding.UTF8.GetBytes(key)),
38
+ SecurityAlgorithms.HmacSha256);
39
+
40
+ var token = new JwtSecurityToken(
41
+ issuer: _configuration["Jwt:Issuer"],
42
+ audience: _configuration["Jwt:Audience"],
43
+ claims: claims,
44
+ expires: DateTime.UtcNow.AddDays(expiresDays),
45
+ signingCredentials: credentials);
46
+
47
+ return new JwtSecurityTokenHandler().WriteToken(token);
48
+ }
49
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "Logging": {
3
+ "LogLevel": {
4
+ "Default": "Warning",
5
+ "Microsoft.AspNetCore": "Warning",
6
+ "Microsoft.Hosting.Lifetime": "Warning",
7
+ "Microsoft.EntityFrameworkCore.Database.Command": "Warning"
8
+ }
9
+ }
10
+ }
@@ -0,0 +1,24 @@
1
+ {
2
+ "Database": {
3
+ "Provider": "Sqlite"
4
+ },
5
+ "ConnectionStrings": {
6
+ "DefaultConnection": "Data Source=data/devtasks.db",
7
+ "PostgresConnection": "Host=localhost;Port=5432;Database=devtasks;Username=postgres;Password=postgres"
8
+ },
9
+ "Logging": {
10
+ "LogLevel": {
11
+ "Default": "Warning",
12
+ "Microsoft.AspNetCore": "Warning",
13
+ "Microsoft.Hosting.Lifetime": "Warning",
14
+ "Microsoft.EntityFrameworkCore.Database.Command": "Warning"
15
+ }
16
+ },
17
+ "Jwt": {
18
+ "Key": "CHANGE_THIS_DEV_ONLY_SECRET_KEY_32_CHARS_MINIMUM",
19
+ "Issuer": "DevTasks.Api",
20
+ "Audience": "DevTasks.Web",
21
+ "ExpiresDays": 7
22
+ },
23
+ "AllowedHosts": "*"
24
+ }
@@ -0,0 +1,12 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>DevTasks</title>
7
+ </head>
8
+ <body>
9
+ <div id="root"></div>
10
+ <script type="module" src="/src/main.tsx"></script>
11
+ </body>
12
+ </html>