@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,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,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>
|