@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.
- package/README.md +149 -11
- package/backend/Controllers/AccountController.cs +100 -0
- package/backend/Controllers/ActivityController.cs +44 -0
- package/backend/Controllers/AuthController.cs +127 -0
- package/backend/Controllers/LookupController.cs +46 -0
- package/backend/Controllers/TasksController.cs +652 -0
- package/backend/Controllers/UsersController.cs +181 -0
- package/backend/Data/AppDbContext.cs +93 -0
- package/backend/Data/DbSeeder.cs +122 -0
- package/backend/DevTasks.Api.csproj +13 -0
- package/backend/Dtos/ActivityDtos.cs +12 -0
- package/backend/Dtos/AuthDtos.cs +37 -0
- package/backend/Dtos/TaskDtos.cs +104 -0
- package/backend/Dtos/UserDtos.cs +29 -0
- package/backend/Enums/EditRequestStatus.cs +8 -0
- package/backend/Enums/TaskPriority.cs +8 -0
- package/backend/Enums/TaskState.cs +9 -0
- package/backend/Enums/TaskVisibility.cs +7 -0
- package/backend/Enums/UserRole.cs +7 -0
- package/backend/Extensions/ClaimsPrincipalExtensions.cs +23 -0
- package/backend/Models/ActivityLog.cs +12 -0
- package/backend/Models/AppUser.cs +17 -0
- package/backend/Models/TaskEditRequest.cs +31 -0
- package/backend/Models/TaskItem.cs +25 -0
- package/backend/Program.cs +138 -0
- package/backend/Properties/launchSettings.json +13 -0
- package/backend/Services/ActivityService.cs +28 -0
- package/backend/Services/PasswordHasher.cs +58 -0
- package/backend/Services/TokenService.cs +49 -0
- package/backend/appsettings.Development.json +10 -0
- package/backend/appsettings.json +24 -0
- package/frontend/index.html +12 -0
- package/frontend/package-lock.json +1769 -0
- package/frontend/package.json +23 -0
- package/frontend/src/App.tsx +40 -0
- package/frontend/src/api/http.ts +75 -0
- package/frontend/src/auth/AuthContext.tsx +101 -0
- package/frontend/src/components/EditRequestModal.tsx +139 -0
- package/frontend/src/components/EditRequestsPanel.tsx +94 -0
- package/frontend/src/components/Layout.tsx +76 -0
- package/frontend/src/components/PageHeader.tsx +21 -0
- package/frontend/src/components/ProtectedRoute.tsx +14 -0
- package/frontend/src/components/StatCard.tsx +15 -0
- package/frontend/src/components/TaskCard.tsx +83 -0
- package/frontend/src/components/TaskDetailsModal.tsx +45 -0
- package/frontend/src/components/TaskFilters.tsx +67 -0
- package/frontend/src/components/TaskModal.tsx +159 -0
- package/frontend/src/components/TaskTable.tsx +68 -0
- package/frontend/src/components/UserModal.tsx +124 -0
- package/frontend/src/main.tsx +19 -0
- package/frontend/src/pages/ActivityPage.tsx +37 -0
- package/frontend/src/pages/BoardPage.tsx +75 -0
- package/frontend/src/pages/CalendarPage.tsx +101 -0
- package/frontend/src/pages/DashboardPage.tsx +131 -0
- package/frontend/src/pages/LoginPage.tsx +69 -0
- package/frontend/src/pages/ProfilePage.tsx +111 -0
- package/frontend/src/pages/PublicTasksPage.tsx +99 -0
- package/frontend/src/pages/RegisterPage.tsx +80 -0
- package/frontend/src/pages/TasksPage.tsx +135 -0
- package/frontend/src/pages/UsersPage.tsx +86 -0
- package/frontend/src/styles.css +596 -0
- package/frontend/src/theme.tsx +49 -0
- package/frontend/src/types.ts +78 -0
- package/frontend/src/utils/date.ts +30 -0
- package/frontend/src/utils/labels.ts +3 -0
- package/frontend/src/vite-env.d.ts +1 -0
- package/frontend/tsconfig.json +21 -0
- package/frontend/vite.config.ts +15 -0
- package/package.json +22 -9
- package/index.js +0 -7
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
using DevTasks.Api.Data;
|
|
2
|
+
using DevTasks.Api.Dtos;
|
|
3
|
+
using DevTasks.Api.Enums;
|
|
4
|
+
using DevTasks.Api.Extensions;
|
|
5
|
+
using DevTasks.Api.Models;
|
|
6
|
+
using DevTasks.Api.Services;
|
|
7
|
+
using Microsoft.AspNetCore.Authorization;
|
|
8
|
+
using Microsoft.AspNetCore.Mvc;
|
|
9
|
+
using Microsoft.EntityFrameworkCore;
|
|
10
|
+
|
|
11
|
+
namespace DevTasks.Api.Controllers;
|
|
12
|
+
|
|
13
|
+
[ApiController]
|
|
14
|
+
[Route("api/[controller]")]
|
|
15
|
+
[Authorize(Roles = nameof(UserRole.Admin))]
|
|
16
|
+
public sealed class UsersController : ControllerBase
|
|
17
|
+
{
|
|
18
|
+
private readonly AppDbContext _db;
|
|
19
|
+
private readonly PasswordHasher _passwordHasher;
|
|
20
|
+
private readonly ActivityService _activity;
|
|
21
|
+
|
|
22
|
+
public UsersController(
|
|
23
|
+
AppDbContext db,
|
|
24
|
+
PasswordHasher passwordHasher,
|
|
25
|
+
ActivityService activity)
|
|
26
|
+
{
|
|
27
|
+
_db = db;
|
|
28
|
+
_passwordHasher = passwordHasher;
|
|
29
|
+
_activity = activity;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
[HttpGet]
|
|
33
|
+
public async Task<ActionResult<IReadOnlyList<UserResponse>>> GetUsers()
|
|
34
|
+
{
|
|
35
|
+
var users = await _db.Users
|
|
36
|
+
.AsNoTracking()
|
|
37
|
+
.OrderByDescending(x => x.CreatedAt)
|
|
38
|
+
.ToListAsync();
|
|
39
|
+
|
|
40
|
+
return Ok(users.Select(ToResponse).ToList());
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
[HttpPost]
|
|
44
|
+
public async Task<ActionResult<UserResponse>> CreateUser(CreateUserRequest request)
|
|
45
|
+
{
|
|
46
|
+
var validation = ValidateCreate(request);
|
|
47
|
+
if (validation is not null)
|
|
48
|
+
{
|
|
49
|
+
return BadRequest(new { message = validation });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
var email = request.Email.Trim().ToLowerInvariant();
|
|
53
|
+
if (await _db.Users.AnyAsync(x => x.Email == email))
|
|
54
|
+
{
|
|
55
|
+
return Conflict(new { message = "Email is already used." });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
var user = new AppUser
|
|
59
|
+
{
|
|
60
|
+
FullName = request.FullName.Trim(),
|
|
61
|
+
Email = email,
|
|
62
|
+
PasswordHash = _passwordHasher.Hash(request.Password),
|
|
63
|
+
Role = request.Role,
|
|
64
|
+
IsActive = request.IsActive,
|
|
65
|
+
CreatedAt = DateTime.UtcNow
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
_db.Users.Add(user);
|
|
69
|
+
await _db.SaveChangesAsync();
|
|
70
|
+
await _activity.AddAsync("User added", "User", user.Id.ToString(), User.GetUserId());
|
|
71
|
+
|
|
72
|
+
return CreatedAtAction(nameof(GetUsers), new { id = user.Id }, ToResponse(user));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
[HttpPut("{id:guid}")]
|
|
76
|
+
public async Task<ActionResult<UserResponse>> UpdateUser(Guid id, UpdateUserRequest request)
|
|
77
|
+
{
|
|
78
|
+
var user = await _db.Users.FindAsync(id);
|
|
79
|
+
if (user is null)
|
|
80
|
+
{
|
|
81
|
+
return NotFound(new { message = "User was not found." });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (!string.IsNullOrWhiteSpace(request.Email))
|
|
85
|
+
{
|
|
86
|
+
var email = request.Email.Trim().ToLowerInvariant();
|
|
87
|
+
var used = await _db.Users.AnyAsync(x => x.Id != id && x.Email == email);
|
|
88
|
+
if (used)
|
|
89
|
+
{
|
|
90
|
+
return Conflict(new { message = "Email is already used." });
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
user.Email = email;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (!string.IsNullOrWhiteSpace(request.FullName))
|
|
97
|
+
{
|
|
98
|
+
user.FullName = request.FullName.Trim();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (!string.IsNullOrWhiteSpace(request.Password))
|
|
102
|
+
{
|
|
103
|
+
if (request.Password.Length < 6)
|
|
104
|
+
{
|
|
105
|
+
return BadRequest(new { message = "Password must be at least 6 characters." });
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
user.PasswordHash = _passwordHasher.Hash(request.Password);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (request.Role.HasValue)
|
|
112
|
+
{
|
|
113
|
+
user.Role = request.Role.Value;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (request.IsActive.HasValue)
|
|
117
|
+
{
|
|
118
|
+
user.IsActive = request.IsActive.Value;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
await _db.SaveChangesAsync();
|
|
122
|
+
await _activity.AddAsync("User edited", "User", user.Id.ToString(), User.GetUserId());
|
|
123
|
+
|
|
124
|
+
return Ok(ToResponse(user));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
[HttpDelete("{id:guid}")]
|
|
128
|
+
public async Task<IActionResult> DeleteUser(Guid id)
|
|
129
|
+
{
|
|
130
|
+
var currentUserId = User.GetUserId();
|
|
131
|
+
if (id == currentUserId)
|
|
132
|
+
{
|
|
133
|
+
return BadRequest(new { message = "You cannot deactivate your own user." });
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
var user = await _db.Users.FindAsync(id);
|
|
137
|
+
if (user is null)
|
|
138
|
+
{
|
|
139
|
+
return NotFound(new { message = "User was not found." });
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
user.IsActive = false;
|
|
143
|
+
await _db.SaveChangesAsync();
|
|
144
|
+
await _activity.AddAsync("User deactivated", "User", user.Id.ToString(), currentUserId);
|
|
145
|
+
|
|
146
|
+
return NoContent();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private static UserResponse ToResponse(AppUser user)
|
|
150
|
+
{
|
|
151
|
+
return new UserResponse
|
|
152
|
+
{
|
|
153
|
+
Id = user.Id,
|
|
154
|
+
FullName = user.FullName,
|
|
155
|
+
Email = user.Email,
|
|
156
|
+
Role = user.Role,
|
|
157
|
+
IsActive = user.IsActive,
|
|
158
|
+
CreatedAt = user.CreatedAt
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private static string? ValidateCreate(CreateUserRequest request)
|
|
163
|
+
{
|
|
164
|
+
if (string.IsNullOrWhiteSpace(request.FullName))
|
|
165
|
+
{
|
|
166
|
+
return "Full name is required.";
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (string.IsNullOrWhiteSpace(request.Email) || !request.Email.Contains('@'))
|
|
170
|
+
{
|
|
171
|
+
return "Valid email is required.";
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (string.IsNullOrWhiteSpace(request.Password) || request.Password.Length < 6)
|
|
175
|
+
{
|
|
176
|
+
return "Password must be at least 6 characters.";
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
using DevTasks.Api.Enums;
|
|
2
|
+
using DevTasks.Api.Models;
|
|
3
|
+
using Microsoft.EntityFrameworkCore;
|
|
4
|
+
|
|
5
|
+
namespace DevTasks.Api.Data;
|
|
6
|
+
|
|
7
|
+
public sealed class AppDbContext : DbContext
|
|
8
|
+
{
|
|
9
|
+
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options)
|
|
10
|
+
{
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
public DbSet<AppUser> Users => Set<AppUser>();
|
|
14
|
+
public DbSet<TaskItem> TaskItems => Set<TaskItem>();
|
|
15
|
+
public DbSet<TaskEditRequest> TaskEditRequests => Set<TaskEditRequest>();
|
|
16
|
+
public DbSet<ActivityLog> ActivityLogs => Set<ActivityLog>();
|
|
17
|
+
|
|
18
|
+
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
|
19
|
+
{
|
|
20
|
+
base.OnModelCreating(modelBuilder);
|
|
21
|
+
|
|
22
|
+
modelBuilder.Entity<AppUser>(entity =>
|
|
23
|
+
{
|
|
24
|
+
entity.HasKey(x => x.Id);
|
|
25
|
+
entity.Property(x => x.FullName).HasMaxLength(120).IsRequired();
|
|
26
|
+
entity.Property(x => x.Email).HasMaxLength(180).IsRequired();
|
|
27
|
+
entity.HasIndex(x => x.Email).IsUnique();
|
|
28
|
+
entity.Property(x => x.PasswordHash).IsRequired();
|
|
29
|
+
entity.Property(x => x.Role).HasConversion<string>().HasMaxLength(20);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
modelBuilder.Entity<TaskItem>(entity =>
|
|
33
|
+
{
|
|
34
|
+
entity.HasKey(x => x.Id);
|
|
35
|
+
entity.Property(x => x.Id).ValueGeneratedOnAdd();
|
|
36
|
+
entity.Property(x => x.Title).HasMaxLength(160).IsRequired();
|
|
37
|
+
entity.Property(x => x.Description).HasMaxLength(4000);
|
|
38
|
+
entity.Property(x => x.Status).HasConversion<string>().HasMaxLength(30);
|
|
39
|
+
entity.Property(x => x.Priority).HasConversion<string>().HasMaxLength(20);
|
|
40
|
+
entity.Property(x => x.Visibility).HasConversion<string>().HasMaxLength(20);
|
|
41
|
+
|
|
42
|
+
entity.HasOne(x => x.CreatedByUser)
|
|
43
|
+
.WithMany(x => x.CreatedTasks)
|
|
44
|
+
.HasForeignKey(x => x.CreatedByUserId)
|
|
45
|
+
.OnDelete(DeleteBehavior.Restrict);
|
|
46
|
+
|
|
47
|
+
entity.HasOne(x => x.AssignedToUser)
|
|
48
|
+
.WithMany(x => x.AssignedTasks)
|
|
49
|
+
.HasForeignKey(x => x.AssignedToUserId)
|
|
50
|
+
.OnDelete(DeleteBehavior.SetNull);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
modelBuilder.Entity<TaskEditRequest>(entity =>
|
|
54
|
+
{
|
|
55
|
+
entity.HasKey(x => x.Id);
|
|
56
|
+
entity.Property(x => x.Title).HasMaxLength(160);
|
|
57
|
+
entity.Property(x => x.Description).HasMaxLength(4000);
|
|
58
|
+
entity.Property(x => x.Status).HasConversion<string>().HasMaxLength(30);
|
|
59
|
+
entity.Property(x => x.Priority).HasConversion<string>().HasMaxLength(20);
|
|
60
|
+
entity.Property(x => x.Visibility).HasConversion<string>().HasMaxLength(20);
|
|
61
|
+
entity.Property(x => x.StatusOfRequest).HasConversion<string>().HasMaxLength(20);
|
|
62
|
+
entity.Property(x => x.OwnerNote).HasMaxLength(800);
|
|
63
|
+
|
|
64
|
+
entity.HasOne(x => x.TaskItem)
|
|
65
|
+
.WithMany(x => x.EditRequests)
|
|
66
|
+
.HasForeignKey(x => x.TaskItemId)
|
|
67
|
+
.OnDelete(DeleteBehavior.Cascade);
|
|
68
|
+
|
|
69
|
+
entity.HasOne(x => x.RequestedByUser)
|
|
70
|
+
.WithMany()
|
|
71
|
+
.HasForeignKey(x => x.RequestedByUserId)
|
|
72
|
+
.OnDelete(DeleteBehavior.Restrict);
|
|
73
|
+
|
|
74
|
+
entity.HasOne(x => x.AssignedToUser)
|
|
75
|
+
.WithMany()
|
|
76
|
+
.HasForeignKey(x => x.AssignedToUserId)
|
|
77
|
+
.OnDelete(DeleteBehavior.SetNull);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
modelBuilder.Entity<ActivityLog>(entity =>
|
|
81
|
+
{
|
|
82
|
+
entity.HasKey(x => x.Id);
|
|
83
|
+
entity.Property(x => x.Action).HasMaxLength(250).IsRequired();
|
|
84
|
+
entity.Property(x => x.EntityName).HasMaxLength(80).IsRequired();
|
|
85
|
+
entity.Property(x => x.EntityId).HasMaxLength(80).IsRequired();
|
|
86
|
+
|
|
87
|
+
entity.HasOne(x => x.User)
|
|
88
|
+
.WithMany()
|
|
89
|
+
.HasForeignKey(x => x.UserId)
|
|
90
|
+
.OnDelete(DeleteBehavior.Restrict);
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
using DevTasks.Api.Enums;
|
|
2
|
+
using DevTasks.Api.Models;
|
|
3
|
+
using DevTasks.Api.Services;
|
|
4
|
+
using Microsoft.EntityFrameworkCore;
|
|
5
|
+
|
|
6
|
+
namespace DevTasks.Api.Data;
|
|
7
|
+
|
|
8
|
+
public static class DbSeeder
|
|
9
|
+
{
|
|
10
|
+
public static async Task SeedAsync(AppDbContext db, PasswordHasher hasher)
|
|
11
|
+
{
|
|
12
|
+
if (await db.Users.AnyAsync())
|
|
13
|
+
{
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
var admin = new AppUser
|
|
18
|
+
{
|
|
19
|
+
Id = Guid.Parse("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"),
|
|
20
|
+
FullName = "System Admin",
|
|
21
|
+
Email = "admin@devtasks.local",
|
|
22
|
+
PasswordHash = hasher.Hash("Admin123!"),
|
|
23
|
+
Role = UserRole.Admin,
|
|
24
|
+
IsActive = true,
|
|
25
|
+
CreatedAt = DateTime.UtcNow
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
var programmer = new AppUser
|
|
29
|
+
{
|
|
30
|
+
Id = Guid.Parse("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"),
|
|
31
|
+
FullName = "Demo Programmer",
|
|
32
|
+
Email = "programmer@devtasks.local",
|
|
33
|
+
PasswordHash = hasher.Hash("User123!"),
|
|
34
|
+
Role = UserRole.User,
|
|
35
|
+
IsActive = true,
|
|
36
|
+
CreatedAt = DateTime.UtcNow
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
db.Users.AddRange(admin, programmer);
|
|
40
|
+
|
|
41
|
+
var now = DateTime.UtcNow;
|
|
42
|
+
var tasks = new List<TaskItem>
|
|
43
|
+
{
|
|
44
|
+
new()
|
|
45
|
+
{
|
|
46
|
+
Title = "Build login screen",
|
|
47
|
+
Description = "Create the minimal auth page and connect it with JWT login.",
|
|
48
|
+
Status = TaskState.Done,
|
|
49
|
+
Priority = TaskPriority.High,
|
|
50
|
+
Visibility = TaskVisibility.Public,
|
|
51
|
+
CreatedByUserId = admin.Id,
|
|
52
|
+
AssignedToUserId = programmer.Id,
|
|
53
|
+
DueDate = now.AddDays(-1),
|
|
54
|
+
CreatedAt = now.AddDays(-6),
|
|
55
|
+
UpdatedAt = now.AddDays(-2)
|
|
56
|
+
},
|
|
57
|
+
new()
|
|
58
|
+
{
|
|
59
|
+
Title = "Create task board",
|
|
60
|
+
Description = "Add drag and drop between Todo, In Progress, Done, and Cancelled.",
|
|
61
|
+
Status = TaskState.InProgress,
|
|
62
|
+
Priority = TaskPriority.High,
|
|
63
|
+
Visibility = TaskVisibility.Public,
|
|
64
|
+
CreatedByUserId = programmer.Id,
|
|
65
|
+
AssignedToUserId = programmer.Id,
|
|
66
|
+
DueDate = now.AddDays(3),
|
|
67
|
+
CreatedAt = now.AddDays(-4),
|
|
68
|
+
UpdatedAt = now.AddDays(-1)
|
|
69
|
+
},
|
|
70
|
+
new()
|
|
71
|
+
{
|
|
72
|
+
Title = "Write API validation notes",
|
|
73
|
+
Description = "Document required fields and possible validation errors.",
|
|
74
|
+
Status = TaskState.Todo,
|
|
75
|
+
Priority = TaskPriority.Medium,
|
|
76
|
+
Visibility = TaskVisibility.Private,
|
|
77
|
+
CreatedByUserId = programmer.Id,
|
|
78
|
+
AssignedToUserId = programmer.Id,
|
|
79
|
+
DueDate = now.AddDays(5),
|
|
80
|
+
CreatedAt = now.AddDays(-2),
|
|
81
|
+
UpdatedAt = now.AddDays(-2)
|
|
82
|
+
},
|
|
83
|
+
new()
|
|
84
|
+
{
|
|
85
|
+
Title = "Clean activity log UI",
|
|
86
|
+
Description = "Make the activity page readable and useful for admins.",
|
|
87
|
+
Status = TaskState.Todo,
|
|
88
|
+
Priority = TaskPriority.Low,
|
|
89
|
+
Visibility = TaskVisibility.Public,
|
|
90
|
+
CreatedByUserId = admin.Id,
|
|
91
|
+
AssignedToUserId = admin.Id,
|
|
92
|
+
DueDate = now.AddDays(8),
|
|
93
|
+
CreatedAt = now.AddDays(-1),
|
|
94
|
+
UpdatedAt = now.AddDays(-1)
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
db.TaskItems.AddRange(tasks);
|
|
99
|
+
await db.SaveChangesAsync();
|
|
100
|
+
|
|
101
|
+
db.ActivityLogs.AddRange(
|
|
102
|
+
new ActivityLog
|
|
103
|
+
{
|
|
104
|
+
Action = "Seeded the first admin user",
|
|
105
|
+
EntityName = "User",
|
|
106
|
+
EntityId = admin.Id.ToString(),
|
|
107
|
+
UserId = admin.Id,
|
|
108
|
+
CreatedAt = now.AddDays(-7)
|
|
109
|
+
},
|
|
110
|
+
new ActivityLog
|
|
111
|
+
{
|
|
112
|
+
Action = "Seeded demo tasks",
|
|
113
|
+
EntityName = "Task",
|
|
114
|
+
EntityId = tasks[0].Id.ToString(),
|
|
115
|
+
UserId = admin.Id,
|
|
116
|
+
CreatedAt = now.AddDays(-6)
|
|
117
|
+
}
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
await db.SaveChangesAsync();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<Project Sdk="Microsoft.NET.Sdk.Web">
|
|
2
|
+
<PropertyGroup>
|
|
3
|
+
<TargetFramework>net8.0</TargetFramework>
|
|
4
|
+
<Nullable>enable</Nullable>
|
|
5
|
+
<ImplicitUsings>enable</ImplicitUsings>
|
|
6
|
+
</PropertyGroup>
|
|
7
|
+
|
|
8
|
+
<ItemGroup>
|
|
9
|
+
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.*" />
|
|
10
|
+
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.*" />
|
|
11
|
+
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.*" />
|
|
12
|
+
</ItemGroup>
|
|
13
|
+
</Project>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
namespace DevTasks.Api.Dtos;
|
|
2
|
+
|
|
3
|
+
public sealed class ActivityLogResponse
|
|
4
|
+
{
|
|
5
|
+
public Guid Id { get; set; }
|
|
6
|
+
public string Action { get; set; } = string.Empty;
|
|
7
|
+
public string EntityName { get; set; } = string.Empty;
|
|
8
|
+
public string EntityId { get; set; } = string.Empty;
|
|
9
|
+
public Guid UserId { get; set; }
|
|
10
|
+
public string UserFullName { get; set; } = string.Empty;
|
|
11
|
+
public DateTime CreatedAt { get; set; }
|
|
12
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
using DevTasks.Api.Enums;
|
|
2
|
+
|
|
3
|
+
namespace DevTasks.Api.Dtos;
|
|
4
|
+
|
|
5
|
+
public sealed class RegisterRequest
|
|
6
|
+
{
|
|
7
|
+
public string FullName { get; set; } = string.Empty;
|
|
8
|
+
public string Email { get; set; } = string.Empty;
|
|
9
|
+
public string Password { get; set; } = string.Empty;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
public sealed class RegisterResponse
|
|
13
|
+
{
|
|
14
|
+
public string Message { get; set; } = string.Empty;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
public sealed class LoginRequest
|
|
18
|
+
{
|
|
19
|
+
public string Email { get; set; } = string.Empty;
|
|
20
|
+
public string Password { get; set; } = string.Empty;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
public sealed class AuthResponse
|
|
24
|
+
{
|
|
25
|
+
public string Token { get; set; } = string.Empty;
|
|
26
|
+
public UserResponse User { get; set; } = new();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
public sealed class UserResponse
|
|
30
|
+
{
|
|
31
|
+
public Guid Id { get; set; }
|
|
32
|
+
public string FullName { get; set; } = string.Empty;
|
|
33
|
+
public string Email { get; set; } = string.Empty;
|
|
34
|
+
public UserRole Role { get; set; }
|
|
35
|
+
public bool IsActive { get; set; }
|
|
36
|
+
public DateTime CreatedAt { get; set; }
|
|
37
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
using DevTasks.Api.Enums;
|
|
2
|
+
|
|
3
|
+
namespace DevTasks.Api.Dtos;
|
|
4
|
+
|
|
5
|
+
public sealed class TaskResponse
|
|
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; }
|
|
11
|
+
public TaskPriority Priority { get; set; }
|
|
12
|
+
public TaskVisibility Visibility { get; set; }
|
|
13
|
+
public DateTime? DueDate { get; set; }
|
|
14
|
+
public Guid CreatedByUserId { get; set; }
|
|
15
|
+
public string CreatedByFullName { get; set; } = string.Empty;
|
|
16
|
+
public Guid? AssignedToUserId { get; set; }
|
|
17
|
+
public string? AssignedToFullName { get; set; }
|
|
18
|
+
public DateTime CreatedAt { get; set; }
|
|
19
|
+
public DateTime UpdatedAt { get; set; }
|
|
20
|
+
public bool IsOverdue { get; set; }
|
|
21
|
+
public int PendingEditRequestCount { get; set; }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
public sealed class CreateTaskRequest
|
|
25
|
+
{
|
|
26
|
+
public string Title { get; set; } = string.Empty;
|
|
27
|
+
public string Description { get; set; } = string.Empty;
|
|
28
|
+
public TaskState Status { get; set; } = TaskState.Todo;
|
|
29
|
+
public TaskPriority Priority { get; set; } = TaskPriority.Medium;
|
|
30
|
+
public TaskVisibility Visibility { get; set; } = TaskVisibility.Private;
|
|
31
|
+
public DateTime? DueDate { get; set; }
|
|
32
|
+
public Guid? AssignedToUserId { get; set; }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
public sealed class UpdateTaskRequest
|
|
36
|
+
{
|
|
37
|
+
public string? Title { get; set; }
|
|
38
|
+
public string? Description { get; set; }
|
|
39
|
+
public TaskState? Status { get; set; }
|
|
40
|
+
public TaskPriority? Priority { get; set; }
|
|
41
|
+
public TaskVisibility? Visibility { get; set; }
|
|
42
|
+
public DateTime? DueDate { get; set; }
|
|
43
|
+
public bool ClearDueDate { get; set; }
|
|
44
|
+
public Guid? AssignedToUserId { get; set; }
|
|
45
|
+
public bool ClearAssignedUser { get; set; }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
public sealed class UpdateTaskStatusRequest
|
|
49
|
+
{
|
|
50
|
+
public TaskState Status { get; set; }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
public sealed class TaskQuery
|
|
54
|
+
{
|
|
55
|
+
public string? Q { get; set; }
|
|
56
|
+
public TaskState? Status { get; set; }
|
|
57
|
+
public TaskPriority? Priority { get; set; }
|
|
58
|
+
public TaskVisibility? Visibility { get; set; }
|
|
59
|
+
public Guid? AssignedToUserId { get; set; }
|
|
60
|
+
public DateTime? DueFrom { get; set; }
|
|
61
|
+
public DateTime? DueTo { get; set; }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
public sealed class CreateTaskEditRequest
|
|
65
|
+
{
|
|
66
|
+
public string? Title { get; set; }
|
|
67
|
+
public string? Description { get; set; }
|
|
68
|
+
public TaskState? Status { get; set; }
|
|
69
|
+
public TaskPriority? Priority { get; set; }
|
|
70
|
+
public TaskVisibility? Visibility { get; set; }
|
|
71
|
+
public DateTime? DueDate { get; set; }
|
|
72
|
+
public bool ClearDueDate { get; set; }
|
|
73
|
+
public Guid? AssignedToUserId { get; set; }
|
|
74
|
+
public bool ClearAssignedUser { get; set; }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
public sealed class ResolveTaskEditRequest
|
|
78
|
+
{
|
|
79
|
+
public string? OwnerNote { get; set; }
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
public sealed class TaskEditRequestResponse
|
|
83
|
+
{
|
|
84
|
+
public Guid Id { get; set; }
|
|
85
|
+
public int TaskId { get; set; }
|
|
86
|
+
public string TaskTitle { get; set; } = string.Empty;
|
|
87
|
+
public Guid RequestedByUserId { get; set; }
|
|
88
|
+
public string RequestedByFullName { get; set; } = string.Empty;
|
|
89
|
+
public string RequestedByEmail { get; set; } = string.Empty;
|
|
90
|
+
public string? Title { get; set; }
|
|
91
|
+
public string? Description { get; set; }
|
|
92
|
+
public TaskState? Status { get; set; }
|
|
93
|
+
public TaskPriority? Priority { get; set; }
|
|
94
|
+
public TaskVisibility? Visibility { get; set; }
|
|
95
|
+
public DateTime? DueDate { get; set; }
|
|
96
|
+
public bool ClearDueDate { get; set; }
|
|
97
|
+
public Guid? AssignedToUserId { get; set; }
|
|
98
|
+
public string? AssignedToFullName { get; set; }
|
|
99
|
+
public bool ClearAssignedUser { get; set; }
|
|
100
|
+
public EditRequestStatus StatusOfRequest { get; set; }
|
|
101
|
+
public string? OwnerNote { get; set; }
|
|
102
|
+
public DateTime CreatedAt { get; set; }
|
|
103
|
+
public DateTime? ResolvedAt { get; set; }
|
|
104
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
using DevTasks.Api.Enums;
|
|
2
|
+
|
|
3
|
+
namespace DevTasks.Api.Dtos;
|
|
4
|
+
|
|
5
|
+
public sealed class CreateUserRequest
|
|
6
|
+
{
|
|
7
|
+
public string FullName { get; set; } = string.Empty;
|
|
8
|
+
public string Email { get; set; } = string.Empty;
|
|
9
|
+
public string Password { get; set; } = string.Empty;
|
|
10
|
+
public UserRole Role { get; set; } = UserRole.User;
|
|
11
|
+
public bool IsActive { get; set; } = true;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
public sealed class UpdateUserRequest
|
|
15
|
+
{
|
|
16
|
+
public string? FullName { get; set; }
|
|
17
|
+
public string? Email { get; set; }
|
|
18
|
+
public string? Password { get; set; }
|
|
19
|
+
public UserRole? Role { get; set; }
|
|
20
|
+
public bool? IsActive { get; set; }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
public sealed class UpdateProfileRequest
|
|
24
|
+
{
|
|
25
|
+
public string? FullName { get; set; }
|
|
26
|
+
public string? Email { get; set; }
|
|
27
|
+
public string? CurrentPassword { get; set; }
|
|
28
|
+
public string? NewPassword { get; set; }
|
|
29
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
using System.Security.Claims;
|
|
2
|
+
using DevTasks.Api.Enums;
|
|
3
|
+
|
|
4
|
+
namespace DevTasks.Api.Extensions;
|
|
5
|
+
|
|
6
|
+
public static class ClaimsPrincipalExtensions
|
|
7
|
+
{
|
|
8
|
+
public static Guid GetUserId(this ClaimsPrincipal user)
|
|
9
|
+
{
|
|
10
|
+
var value = user.FindFirstValue(ClaimTypes.NameIdentifier);
|
|
11
|
+
if (string.IsNullOrWhiteSpace(value))
|
|
12
|
+
{
|
|
13
|
+
throw new UnauthorizedAccessException("Missing user id claim.");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return Guid.Parse(value);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
public static bool IsAdmin(this ClaimsPrincipal user)
|
|
20
|
+
{
|
|
21
|
+
return user.IsInRole(UserRole.Admin.ToString());
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
namespace DevTasks.Api.Models;
|
|
2
|
+
|
|
3
|
+
public sealed class ActivityLog
|
|
4
|
+
{
|
|
5
|
+
public Guid Id { get; set; } = Guid.NewGuid();
|
|
6
|
+
public string Action { get; set; } = string.Empty;
|
|
7
|
+
public string EntityName { get; set; } = string.Empty;
|
|
8
|
+
public string EntityId { get; set; } = string.Empty;
|
|
9
|
+
public Guid UserId { get; set; }
|
|
10
|
+
public AppUser? User { get; set; }
|
|
11
|
+
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
|
12
|
+
}
|