@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
package/README.md CHANGED
@@ -1,21 +1,159 @@
1
- # @OMNahhas/hello-dev
1
+ # DevTasks — Full Stack Task Management Website
2
2
 
3
- A simple Node.js package that greets the user. This package exports a single function, `hello`, which returns a greeting string for the provided name.
3
+ A complete task management system with authentication, authorization,
4
+ public/private tasks, users, board, calendar, dashboard, activity log,
5
+ request limiting, edit requests, SQLite/PostgreSQL support, and light/dark UI.
4
6
 
5
- ## Installation
7
+ The UI uses a custom warm workspace theme with plum, charcoal, cream, and
8
+ terracotta accents. It does not use the default blue SaaS theme.
9
+
10
+ ## Tech stack
11
+
12
+ - Backend: ASP.NET Core 8 Web API
13
+ - Frontend: React + TypeScript + Vite
14
+ - Database: SQLite by default, PostgreSQL optional
15
+ - Auth: JWT + role-based authorization
16
+ - Rate limit: 100 requests per minute per IP
17
+
18
+ ## Default seeded accounts
19
+
20
+ The database is created automatically on the first backend run.
21
+
22
+ | Role | Email | Password |
23
+ |---|---|---|
24
+ | Admin | `admin@devtasks.local` | `Admin123!` |
25
+ | User | `programmer@devtasks.local` | `User123!` |
26
+
27
+ New registered users are inactive by default. The admin must activate them
28
+ from the Users page before they can login.
29
+
30
+ ## Run the backend
31
+
32
+ ```bash
33
+ cd backend
34
+ dotnet restore
35
+ dotnet run
36
+ ```
37
+
38
+ Backend URL:
39
+
40
+ ```text
41
+ http://localhost:5058
42
+ ```
43
+
44
+ The SQLite database will be created here:
45
+
46
+ ```text
47
+ backend/data/devtasks.db
48
+ ```
49
+
50
+ ## Run the frontend
51
+
52
+ Open another terminal:
6
53
 
7
54
  ```bash
8
- npm install @OMNahhas/hello-dev
55
+ cd frontend
56
+ npm install
57
+ npm run dev
9
58
  ```
10
59
 
11
- ## Usage
60
+ Frontend URL:
61
+
62
+ ```text
63
+ http://localhost:5173
64
+ ```
65
+
66
+ The frontend proxies `/api` to `http://localhost:5058`.
67
+
68
+ ## PostgreSQL support
69
+
70
+ SQLite is used by default in `backend/appsettings.json`:
71
+
72
+ ```json
73
+ "Database": {
74
+ "Provider": "Sqlite"
75
+ }
76
+ ```
77
+
78
+ To use PostgreSQL, change it to:
79
+
80
+ ```json
81
+ "Database": {
82
+ "Provider": "Postgres"
83
+ }
84
+ ```
85
+
86
+ Then update this connection string:
87
+
88
+ ```json
89
+ "PostgresConnection": "Host=localhost;Port=5432;Database=devtasks;Username=postgres;Password=postgres"
90
+ ```
91
+
92
+ The backend uses `EnsureCreated()` for this demo project, so create an empty
93
+ PostgreSQL database first, then run the backend.
94
+
95
+ ## Important note when replacing an older copy
96
+
97
+ Task IDs were changed from GUID values to numeric IDs starting from `1`.
98
+ If you already ran the old SQLite version, delete the old database file before
99
+ running this upgraded version:
100
+
101
+ ```text
102
+ backend/data/devtasks.db
103
+ ```
104
+
105
+ ## Main features
106
+
107
+ - Register / login / logout
108
+ - New accounts require admin activation
109
+ - JWT authentication
110
+ - Admin and normal user roles
111
+ - Admin user management
112
+ - User profile editing
113
+ - Add/edit/delete tasks
114
+ - Numeric task IDs starting from `1`
115
+ - Private/public task visibility
116
+ - Public task edit requests
117
+ - Owner/admin accept or reject edit requests
118
+ - Multiple edit requests to the same task are handled
119
+ - Assigned tasks
120
+ - Statuses: Todo, In Progress, Done, Cancelled
121
+ - Priorities: Low, Medium, High
122
+ - Dashboard statistics with viewable task cards
123
+ - My Tasks table/card view
124
+ - Public Tasks table/card view
125
+ - Board View with drag and drop
126
+ - Calendar View; admin sees all users' tasks
127
+ - Activity log
128
+ - Search and filters
129
+ - Light/dark theme toggle
130
+ - Per-IP request limiting
131
+ - Info logs disabled by default
132
+
133
+ ## Important files
12
134
 
13
- ```js
14
- const { hello } = require('@OMNahhas/hello-dev');
15
- console.log(hello('Omar'));
16
- // => "Hello, Omar!"
135
+ ```text
136
+ backend/Program.cs
137
+ backend/Data/AppDbContext.cs
138
+ backend/Data/DbSeeder.cs
139
+ backend/Controllers/AuthController.cs
140
+ backend/Controllers/UsersController.cs
141
+ backend/Controllers/AccountController.cs
142
+ backend/Controllers/TasksController.cs
143
+ backend/Controllers/ActivityController.cs
144
+ frontend/src/App.tsx
145
+ frontend/src/auth/AuthContext.tsx
146
+ frontend/src/api/http.ts
147
+ frontend/src/pages
148
+ frontend/src/components
149
+ frontend/src/styles.css
17
150
  ```
18
151
 
19
- ## License
152
+ ## Notes
20
153
 
21
- This project is licensed under the MIT License.
154
+ - `DELETE /api/users/{id}` deactivates users instead of physically deleting them.
155
+ - `DELETE /api/tasks/{id}` physically deletes a task and writes to the activity log.
156
+ - Normal users can edit/delete their own tasks only.
157
+ - Other users can request edits to public tasks.
158
+ - Accepting one pending edit request rejects other pending edit requests for the same task.
159
+ - Admin can manage all users and all tasks.
@@ -0,0 +1,100 @@
1
+ using DevTasks.Api.Data;
2
+ using DevTasks.Api.Dtos;
3
+ using DevTasks.Api.Extensions;
4
+ using DevTasks.Api.Models;
5
+ using DevTasks.Api.Services;
6
+ using Microsoft.AspNetCore.Authorization;
7
+ using Microsoft.AspNetCore.Mvc;
8
+ using Microsoft.EntityFrameworkCore;
9
+
10
+ namespace DevTasks.Api.Controllers;
11
+
12
+ [ApiController]
13
+ [Route("api/[controller]")]
14
+ [Authorize]
15
+ public sealed class AccountController : ControllerBase
16
+ {
17
+ private readonly AppDbContext _db;
18
+ private readonly PasswordHasher _passwordHasher;
19
+ private readonly ActivityService _activity;
20
+
21
+ public AccountController(
22
+ AppDbContext db,
23
+ PasswordHasher passwordHasher,
24
+ ActivityService activity)
25
+ {
26
+ _db = db;
27
+ _passwordHasher = passwordHasher;
28
+ _activity = activity;
29
+ }
30
+
31
+ [HttpGet("me")]
32
+ public async Task<ActionResult<UserResponse>> GetMe()
33
+ {
34
+ var id = User.GetUserId();
35
+ var user = await _db.Users.AsNoTracking().FirstOrDefaultAsync(x => x.Id == id);
36
+ return user is null ? NotFound(new { message = "User was not found." }) : Ok(ToResponse(user));
37
+ }
38
+
39
+ [HttpPut("me")]
40
+ public async Task<ActionResult<UserResponse>> UpdateMe(UpdateProfileRequest request)
41
+ {
42
+ var id = User.GetUserId();
43
+ var user = await _db.Users.FirstOrDefaultAsync(x => x.Id == id);
44
+ if (user is null)
45
+ {
46
+ return NotFound(new { message = "User was not found." });
47
+ }
48
+
49
+ if (!string.IsNullOrWhiteSpace(request.FullName))
50
+ {
51
+ user.FullName = request.FullName.Trim();
52
+ }
53
+
54
+ if (!string.IsNullOrWhiteSpace(request.Email))
55
+ {
56
+ var email = request.Email.Trim().ToLowerInvariant();
57
+ var used = await _db.Users.AnyAsync(x => x.Id != id && x.Email == email);
58
+ if (used)
59
+ {
60
+ return Conflict(new { message = "Email is already used." });
61
+ }
62
+
63
+ user.Email = email;
64
+ }
65
+
66
+ if (!string.IsNullOrWhiteSpace(request.NewPassword))
67
+ {
68
+ if (string.IsNullOrWhiteSpace(request.CurrentPassword) ||
69
+ !_passwordHasher.Verify(request.CurrentPassword, user.PasswordHash))
70
+ {
71
+ return BadRequest(new { message = "Current password is incorrect." });
72
+ }
73
+
74
+ if (request.NewPassword.Length < 6)
75
+ {
76
+ return BadRequest(new { message = "New password must be at least 6 characters." });
77
+ }
78
+
79
+ user.PasswordHash = _passwordHasher.Hash(request.NewPassword);
80
+ }
81
+
82
+ await _db.SaveChangesAsync();
83
+ await _activity.AddAsync("Profile updated", "User", user.Id.ToString(), user.Id);
84
+
85
+ return Ok(ToResponse(user));
86
+ }
87
+
88
+ private static UserResponse ToResponse(AppUser user)
89
+ {
90
+ return new UserResponse
91
+ {
92
+ Id = user.Id,
93
+ FullName = user.FullName,
94
+ Email = user.Email,
95
+ Role = user.Role,
96
+ IsActive = user.IsActive,
97
+ CreatedAt = user.CreatedAt
98
+ };
99
+ }
100
+ }
@@ -0,0 +1,44 @@
1
+ using DevTasks.Api.Data;
2
+ using DevTasks.Api.Dtos;
3
+ using DevTasks.Api.Enums;
4
+ using Microsoft.AspNetCore.Authorization;
5
+ using Microsoft.AspNetCore.Mvc;
6
+ using Microsoft.EntityFrameworkCore;
7
+
8
+ namespace DevTasks.Api.Controllers;
9
+
10
+ [ApiController]
11
+ [Route("api/[controller]")]
12
+ [Authorize(Roles = nameof(UserRole.Admin))]
13
+ public sealed class ActivityController : ControllerBase
14
+ {
15
+ private readonly AppDbContext _db;
16
+
17
+ public ActivityController(AppDbContext db)
18
+ {
19
+ _db = db;
20
+ }
21
+
22
+ [HttpGet]
23
+ public async Task<ActionResult<IReadOnlyList<ActivityLogResponse>>> GetActivity()
24
+ {
25
+ var logs = await _db.ActivityLogs
26
+ .AsNoTracking()
27
+ .Include(x => x.User)
28
+ .OrderByDescending(x => x.CreatedAt)
29
+ .Take(200)
30
+ .Select(x => new ActivityLogResponse
31
+ {
32
+ Id = x.Id,
33
+ Action = x.Action,
34
+ EntityName = x.EntityName,
35
+ EntityId = x.EntityId,
36
+ UserId = x.UserId,
37
+ UserFullName = x.User == null ? "Unknown" : x.User.FullName,
38
+ CreatedAt = x.CreatedAt
39
+ })
40
+ .ToListAsync();
41
+
42
+ return Ok(logs);
43
+ }
44
+ }
@@ -0,0 +1,127 @@
1
+ using DevTasks.Api.Data;
2
+ using DevTasks.Api.Dtos;
3
+ using DevTasks.Api.Enums;
4
+ using DevTasks.Api.Models;
5
+ using DevTasks.Api.Services;
6
+ using Microsoft.AspNetCore.Authorization;
7
+ using Microsoft.AspNetCore.Mvc;
8
+ using Microsoft.EntityFrameworkCore;
9
+
10
+ namespace DevTasks.Api.Controllers;
11
+
12
+ [ApiController]
13
+ [Route("api/[controller]")]
14
+ public sealed class AuthController : ControllerBase
15
+ {
16
+ private readonly AppDbContext _db;
17
+ private readonly PasswordHasher _passwordHasher;
18
+ private readonly TokenService _tokenService;
19
+
20
+ public AuthController(
21
+ AppDbContext db,
22
+ PasswordHasher passwordHasher,
23
+ TokenService tokenService)
24
+ {
25
+ _db = db;
26
+ _passwordHasher = passwordHasher;
27
+ _tokenService = tokenService;
28
+ }
29
+
30
+ [HttpPost("register")]
31
+ [AllowAnonymous]
32
+ public async Task<ActionResult<RegisterResponse>> Register(RegisterRequest request)
33
+ {
34
+ var validation = ValidateRegister(request);
35
+ if (validation is not null)
36
+ {
37
+ return BadRequest(new { message = validation });
38
+ }
39
+
40
+ var email = request.Email.Trim().ToLowerInvariant();
41
+ var exists = await _db.Users.AnyAsync(x => x.Email == email);
42
+ if (exists)
43
+ {
44
+ return Conflict(new { message = "Email is already registered." });
45
+ }
46
+
47
+ var user = new AppUser
48
+ {
49
+ FullName = request.FullName.Trim(),
50
+ Email = email,
51
+ PasswordHash = _passwordHasher.Hash(request.Password),
52
+ Role = UserRole.User,
53
+ IsActive = false,
54
+ CreatedAt = DateTime.UtcNow
55
+ };
56
+
57
+ _db.Users.Add(user);
58
+ await _db.SaveChangesAsync();
59
+
60
+ return Ok(new RegisterResponse
61
+ {
62
+ Message = "Account created. An admin must activate it before you can login."
63
+ });
64
+ }
65
+
66
+ [HttpPost("login")]
67
+ [AllowAnonymous]
68
+ public async Task<ActionResult<AuthResponse>> Login(LoginRequest request)
69
+ {
70
+ var email = request.Email.Trim().ToLowerInvariant();
71
+ var user = await _db.Users.FirstOrDefaultAsync(x => x.Email == email);
72
+
73
+ if (user is null || !_passwordHasher.Verify(request.Password, user.PasswordHash))
74
+ {
75
+ return Unauthorized(new { message = "Invalid email or password." });
76
+ }
77
+
78
+ if (!user.IsActive)
79
+ {
80
+ return Unauthorized(new { message = "Your account is waiting for admin activation." });
81
+ }
82
+
83
+ return Ok(CreateAuthResponse(user));
84
+ }
85
+
86
+ private AuthResponse CreateAuthResponse(AppUser user)
87
+ {
88
+ return new AuthResponse
89
+ {
90
+ Token = _tokenService.CreateToken(user),
91
+ User = ToUserResponse(user)
92
+ };
93
+ }
94
+
95
+ private static UserResponse ToUserResponse(AppUser user)
96
+ {
97
+ return new UserResponse
98
+ {
99
+ Id = user.Id,
100
+ FullName = user.FullName,
101
+ Email = user.Email,
102
+ Role = user.Role,
103
+ IsActive = user.IsActive,
104
+ CreatedAt = user.CreatedAt
105
+ };
106
+ }
107
+
108
+ private static string? ValidateRegister(RegisterRequest request)
109
+ {
110
+ if (string.IsNullOrWhiteSpace(request.FullName))
111
+ {
112
+ return "Full name is required.";
113
+ }
114
+
115
+ if (string.IsNullOrWhiteSpace(request.Email) || !request.Email.Contains('@'))
116
+ {
117
+ return "Valid email is required.";
118
+ }
119
+
120
+ if (string.IsNullOrWhiteSpace(request.Password) || request.Password.Length < 6)
121
+ {
122
+ return "Password must be at least 6 characters.";
123
+ }
124
+
125
+ return null;
126
+ }
127
+ }
@@ -0,0 +1,46 @@
1
+ using DevTasks.Api.Data;
2
+ using DevTasks.Api.Dtos;
3
+ using DevTasks.Api.Models;
4
+ using Microsoft.AspNetCore.Authorization;
5
+ using Microsoft.AspNetCore.Mvc;
6
+ using Microsoft.EntityFrameworkCore;
7
+
8
+ namespace DevTasks.Api.Controllers;
9
+
10
+ [ApiController]
11
+ [Route("api/[controller]")]
12
+ [Authorize]
13
+ public sealed class LookupController : ControllerBase
14
+ {
15
+ private readonly AppDbContext _db;
16
+
17
+ public LookupController(AppDbContext db)
18
+ {
19
+ _db = db;
20
+ }
21
+
22
+ [HttpGet("users")]
23
+ public async Task<ActionResult<IReadOnlyList<UserResponse>>> GetActiveUsers()
24
+ {
25
+ var users = await _db.Users
26
+ .AsNoTracking()
27
+ .Where(x => x.IsActive)
28
+ .OrderBy(x => x.FullName)
29
+ .ToListAsync();
30
+
31
+ return Ok(users.Select(ToResponse).ToList());
32
+ }
33
+
34
+ private static UserResponse ToResponse(AppUser user)
35
+ {
36
+ return new UserResponse
37
+ {
38
+ Id = user.Id,
39
+ FullName = user.FullName,
40
+ Email = user.Email,
41
+ Role = user.Role,
42
+ IsActive = user.IsActive,
43
+ CreatedAt = user.CreatedAt
44
+ };
45
+ }
46
+ }