Initial TafsirQuran app with Gitea CI/CD deploy to k3s.
build-and-deploy / deploy (push) Successful in 10m50s
build-and-deploy / deploy (push) Successful in 10m50s
Blazor UI + ASP.NET API + Postgres; push to main builds images and rolls out namespace tafsirquran.
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
namespace TafsirQuran.Api.Auth;
|
||||
|
||||
public class JwtOptions
|
||||
{
|
||||
public const string SectionName = "Jwt";
|
||||
public string Key { get; set; } = "TafsirQuran-Dev-Secret-Key-Change-In-Production-32+";
|
||||
public string Issuer { get; set; } = "TafsirQuran";
|
||||
public string Audience { get; set; } = "TafsirQuran";
|
||||
public int ExpireMinutes { get; set; } = 60 * 24 * 30;
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Security.Claims;
|
||||
using System.Text;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using TafsirQuran.Domain.Entities;
|
||||
|
||||
namespace TafsirQuran.Api.Auth;
|
||||
|
||||
public class JwtTokenService(IOptions<JwtOptions> options)
|
||||
{
|
||||
private readonly JwtOptions _options = options.Value;
|
||||
|
||||
public string CreateToken(AppUser user)
|
||||
{
|
||||
var claims = new List<Claim>
|
||||
{
|
||||
new(JwtRegisteredClaimNames.Sub, user.Id.ToString()),
|
||||
new(JwtRegisteredClaimNames.Email, user.Email),
|
||||
new("display_name", user.DisplayName)
|
||||
};
|
||||
|
||||
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_options.Key));
|
||||
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
|
||||
var token = new JwtSecurityToken(
|
||||
issuer: _options.Issuer,
|
||||
audience: _options.Audience,
|
||||
claims: claims,
|
||||
expires: DateTime.UtcNow.AddMinutes(_options.ExpireMinutes),
|
||||
signingCredentials: creds);
|
||||
|
||||
return new JwtSecurityTokenHandler().WriteToken(token);
|
||||
}
|
||||
|
||||
public static Guid? GetUserId(ClaimsPrincipal user)
|
||||
{
|
||||
var sub = user.FindFirstValue(JwtRegisteredClaimNames.Sub)
|
||||
?? user.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||
return Guid.TryParse(sub, out var id) ? id : null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
using System.Security.Claims;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using TafsirQuran.Api.Auth;
|
||||
using TafsirQuran.Domain.Entities;
|
||||
using TafsirQuran.Infrastructure.Persistence;
|
||||
using TafsirQuran.Shared.Dtos;
|
||||
|
||||
namespace TafsirQuran.Api.Endpoints;
|
||||
|
||||
public static class AuthEndpoints
|
||||
{
|
||||
public static RouteGroupBuilder MapAuthEndpoints(this WebApplication app)
|
||||
{
|
||||
var group = app.MapGroup("/api/auth");
|
||||
|
||||
group.MapPost("/register", async (AuthRequest request, AppDbContext db, JwtTokenService jwt, CancellationToken ct) =>
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(request.Email) || string.IsNullOrWhiteSpace(request.Password))
|
||||
return Results.BadRequest("ایمیل و رمز عبور لازم است");
|
||||
|
||||
var email = request.Email.Trim().ToLowerInvariant();
|
||||
if (await db.Users.AnyAsync(u => u.Email == email, ct))
|
||||
return Results.Conflict("این ایمیل قبلاً ثبت شده است");
|
||||
|
||||
var user = new AppUser
|
||||
{
|
||||
Email = email,
|
||||
DisplayName = string.IsNullOrWhiteSpace(request.DisplayName) ? email.Split('@')[0] : request.DisplayName!.Trim(),
|
||||
PasswordHash = BCrypt.Net.BCrypt.HashPassword(request.Password)
|
||||
};
|
||||
db.Users.Add(user);
|
||||
await db.SaveChangesAsync(ct);
|
||||
|
||||
var token = jwt.CreateToken(user);
|
||||
return Results.Ok(new AuthResponse(user.Id, user.Email, user.DisplayName, token));
|
||||
});
|
||||
|
||||
group.MapPost("/login", async (AuthRequest request, AppDbContext db, JwtTokenService jwt, CancellationToken ct) =>
|
||||
{
|
||||
var email = request.Email.Trim().ToLowerInvariant();
|
||||
var user = await db.Users.FirstOrDefaultAsync(u => u.Email == email, ct);
|
||||
if (user is null || !BCrypt.Net.BCrypt.Verify(request.Password, user.PasswordHash))
|
||||
return Results.Unauthorized();
|
||||
|
||||
var token = jwt.CreateToken(user);
|
||||
return Results.Ok(new AuthResponse(user.Id, user.Email, user.DisplayName, token));
|
||||
});
|
||||
|
||||
group.MapPost("/logout", () => Results.NoContent());
|
||||
|
||||
group.MapGet("/me", async (ClaimsPrincipal principal, AppDbContext db, CancellationToken ct) =>
|
||||
{
|
||||
var userId = JwtTokenService.GetUserId(principal);
|
||||
if (userId is null)
|
||||
return Results.Ok(new UserStatusDto(false, null, null, null));
|
||||
|
||||
var user = await db.Users.AsNoTracking().FirstOrDefaultAsync(u => u.Id == userId, ct);
|
||||
if (user is null)
|
||||
return Results.Ok(new UserStatusDto(false, null, null, null));
|
||||
|
||||
return Results.Ok(new UserStatusDto(true, user.Id, user.Email, user.DisplayName));
|
||||
}).RequireAuthorization();
|
||||
|
||||
// Allow anonymous me check without 401
|
||||
group.MapGet("/status", async (ClaimsPrincipal principal, AppDbContext db, CancellationToken ct) =>
|
||||
{
|
||||
var userId = JwtTokenService.GetUserId(principal);
|
||||
if (userId is null)
|
||||
return Results.Ok(new UserStatusDto(false, null, null, null));
|
||||
|
||||
var user = await db.Users.AsNoTracking().FirstOrDefaultAsync(u => u.Id == userId, ct);
|
||||
if (user is null)
|
||||
return Results.Ok(new UserStatusDto(false, null, null, null));
|
||||
|
||||
return Results.Ok(new UserStatusDto(true, user.Id, user.Email, user.DisplayName));
|
||||
});
|
||||
|
||||
return group;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
using System.Security.Claims;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using TafsirQuran.Api.Auth;
|
||||
using TafsirQuran.Api.Mapping;
|
||||
using TafsirQuran.Domain.Entities;
|
||||
using TafsirQuran.Infrastructure.Persistence;
|
||||
using TafsirQuran.Shared.Dtos;
|
||||
|
||||
namespace TafsirQuran.Api.Endpoints;
|
||||
|
||||
public static class BookmarkEndpoints
|
||||
{
|
||||
public static RouteGroupBuilder MapBookmarkEndpoints(this WebApplication app)
|
||||
{
|
||||
var group = app.MapGroup("/api/bookmarks");
|
||||
|
||||
group.MapGet("/", async (string deviceId, ClaimsPrincipal user, AppDbContext db, CancellationToken ct) =>
|
||||
{
|
||||
var userId = JwtTokenService.GetUserId(user);
|
||||
var items = await db.Bookmarks.AsNoTracking()
|
||||
.Include(b => b.Episode).ThenInclude(e => e.Surah)
|
||||
.Where(b => b.DeviceId == deviceId || (userId.HasValue && b.UserId == userId))
|
||||
.OrderByDescending(b => b.CreatedAt)
|
||||
.ToListAsync(ct);
|
||||
|
||||
return Results.Ok(items.Select(b => new BookmarkDto(
|
||||
b.Id, b.EpisodeId, b.Note, b.CreatedAt, EpisodeMapper.ToListItem(b.Episode))).ToList());
|
||||
});
|
||||
|
||||
group.MapPost("/", async (ToggleEpisodeRequest request, ClaimsPrincipal user, AppDbContext db, CancellationToken ct) =>
|
||||
{
|
||||
var userId = JwtTokenService.GetUserId(user);
|
||||
var exists = await db.Bookmarks.AnyAsync(b =>
|
||||
b.EpisodeId == request.EpisodeId &&
|
||||
(b.DeviceId == request.DeviceId || (userId.HasValue && b.UserId == userId)), ct);
|
||||
if (exists) return Results.NoContent();
|
||||
|
||||
db.Bookmarks.Add(new Bookmark
|
||||
{
|
||||
DeviceId = request.DeviceId,
|
||||
UserId = userId,
|
||||
EpisodeId = request.EpisodeId,
|
||||
Note = request.Note
|
||||
});
|
||||
await db.SaveChangesAsync(ct);
|
||||
return Results.Created($"/api/bookmarks/{request.EpisodeId}", null);
|
||||
});
|
||||
|
||||
group.MapDelete("/{episodeId:guid}", async (Guid episodeId, string deviceId, ClaimsPrincipal user, AppDbContext db, CancellationToken ct) =>
|
||||
{
|
||||
var userId = JwtTokenService.GetUserId(user);
|
||||
var items = await db.Bookmarks
|
||||
.Where(b => b.EpisodeId == episodeId &&
|
||||
(b.DeviceId == deviceId || (userId.HasValue && b.UserId == userId)))
|
||||
.ToListAsync(ct);
|
||||
db.Bookmarks.RemoveRange(items);
|
||||
await db.SaveChangesAsync(ct);
|
||||
return Results.NoContent();
|
||||
});
|
||||
|
||||
return group;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
using System.Security.Claims;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using TafsirQuran.Api.Auth;
|
||||
using TafsirQuran.Api.Mapping;
|
||||
using TafsirQuran.Domain.Entities;
|
||||
using TafsirQuran.Infrastructure.Persistence;
|
||||
using TafsirQuran.Shared.Dtos;
|
||||
|
||||
namespace TafsirQuran.Api.Endpoints;
|
||||
|
||||
public static class FavoriteEndpoints
|
||||
{
|
||||
public static RouteGroupBuilder MapFavoriteEndpoints(this WebApplication app)
|
||||
{
|
||||
var group = app.MapGroup("/api/favorites");
|
||||
|
||||
group.MapGet("/", async (string deviceId, ClaimsPrincipal user, AppDbContext db, CancellationToken ct) =>
|
||||
{
|
||||
var userId = JwtTokenService.GetUserId(user);
|
||||
var items = await db.Favorites.AsNoTracking()
|
||||
.Include(f => f.Episode).ThenInclude(e => e.Surah)
|
||||
.Where(f => f.DeviceId == deviceId || (userId.HasValue && f.UserId == userId))
|
||||
.OrderByDescending(f => f.CreatedAt)
|
||||
.ToListAsync(ct);
|
||||
|
||||
return Results.Ok(items.Select(f => new FavoriteDto(
|
||||
f.Id, f.EpisodeId, f.CreatedAt, EpisodeMapper.ToListItem(f.Episode))).ToList());
|
||||
});
|
||||
|
||||
group.MapPost("/", async (ToggleEpisodeRequest request, ClaimsPrincipal user, AppDbContext db, CancellationToken ct) =>
|
||||
{
|
||||
var userId = JwtTokenService.GetUserId(user);
|
||||
var exists = await db.Favorites.AnyAsync(f =>
|
||||
f.EpisodeId == request.EpisodeId &&
|
||||
(f.DeviceId == request.DeviceId || (userId.HasValue && f.UserId == userId)), ct);
|
||||
if (exists) return Results.NoContent();
|
||||
|
||||
db.Favorites.Add(new Favorite
|
||||
{
|
||||
DeviceId = request.DeviceId,
|
||||
UserId = userId,
|
||||
EpisodeId = request.EpisodeId
|
||||
});
|
||||
await db.SaveChangesAsync(ct);
|
||||
return Results.Created($"/api/favorites/{request.EpisodeId}", null);
|
||||
});
|
||||
|
||||
group.MapDelete("/{episodeId:guid}", async (Guid episodeId, string deviceId, ClaimsPrincipal user, AppDbContext db, CancellationToken ct) =>
|
||||
{
|
||||
var userId = JwtTokenService.GetUserId(user);
|
||||
var items = await db.Favorites
|
||||
.Where(f => f.EpisodeId == episodeId &&
|
||||
(f.DeviceId == deviceId || (userId.HasValue && f.UserId == userId)))
|
||||
.ToListAsync(ct);
|
||||
db.Favorites.RemoveRange(items);
|
||||
await db.SaveChangesAsync(ct);
|
||||
return Results.NoContent();
|
||||
});
|
||||
|
||||
return group;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
using System.Security.Claims;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using TafsirQuran.Api.Auth;
|
||||
using TafsirQuran.Api.Mapping;
|
||||
using TafsirQuran.Domain.Entities;
|
||||
using TafsirQuran.Infrastructure.Persistence;
|
||||
using TafsirQuran.Shared.Dtos;
|
||||
|
||||
namespace TafsirQuran.Api.Endpoints;
|
||||
|
||||
public static class ProgressEndpoints
|
||||
{
|
||||
public static RouteGroupBuilder MapProgressEndpoints(this WebApplication app)
|
||||
{
|
||||
var group = app.MapGroup("/api/progress");
|
||||
|
||||
group.MapGet("/", async (string deviceId, ClaimsPrincipal user, AppDbContext db, CancellationToken ct) =>
|
||||
{
|
||||
var userId = JwtTokenService.GetUserId(user);
|
||||
var query = db.ListeningProgresses.AsNoTracking().Include(p => p.Episode).ThenInclude(e => e.Surah).AsQueryable();
|
||||
|
||||
ListeningProgress? progress = null;
|
||||
if (userId.HasValue)
|
||||
{
|
||||
progress = await query.Where(p => p.UserId == userId).OrderByDescending(p => p.UpdatedAt).FirstOrDefaultAsync(ct);
|
||||
}
|
||||
progress ??= await query.Where(p => p.DeviceId == deviceId).OrderByDescending(p => p.UpdatedAt).FirstOrDefaultAsync(ct);
|
||||
|
||||
if (progress is null) return Results.NoContent();
|
||||
|
||||
return Results.Ok(new ProgressDto(
|
||||
progress.EpisodeId,
|
||||
progress.PositionSeconds,
|
||||
progress.UpdatedAt,
|
||||
EpisodeMapper.ToListItem(progress.Episode)));
|
||||
});
|
||||
|
||||
group.MapPut("/", async (UpsertProgressRequest request, ClaimsPrincipal user, AppDbContext db, CancellationToken ct) =>
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(request.DeviceId))
|
||||
return Results.BadRequest("deviceId required");
|
||||
|
||||
var userId = JwtTokenService.GetUserId(user);
|
||||
var existing = await db.ListeningProgresses
|
||||
.Where(p => p.DeviceId == request.DeviceId || (userId.HasValue && p.UserId == userId))
|
||||
.OrderByDescending(p => p.UpdatedAt)
|
||||
.FirstOrDefaultAsync(ct);
|
||||
|
||||
if (existing is null)
|
||||
{
|
||||
existing = new ListeningProgress
|
||||
{
|
||||
DeviceId = request.DeviceId,
|
||||
UserId = userId,
|
||||
EpisodeId = request.EpisodeId,
|
||||
PositionSeconds = request.PositionSeconds
|
||||
};
|
||||
db.ListeningProgresses.Add(existing);
|
||||
}
|
||||
else
|
||||
{
|
||||
existing.EpisodeId = request.EpisodeId;
|
||||
existing.PositionSeconds = request.PositionSeconds;
|
||||
existing.UpdatedAt = DateTimeOffset.UtcNow;
|
||||
existing.UserId ??= userId;
|
||||
if (string.IsNullOrWhiteSpace(existing.DeviceId))
|
||||
existing.DeviceId = request.DeviceId;
|
||||
}
|
||||
|
||||
await db.SaveChangesAsync(ct);
|
||||
return Results.NoContent();
|
||||
});
|
||||
|
||||
return group;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using TafsirQuran.Api.Mapping;
|
||||
using TafsirQuran.Infrastructure.Persistence;
|
||||
using TafsirQuran.Shared.Dtos;
|
||||
|
||||
namespace TafsirQuran.Api.Endpoints;
|
||||
|
||||
public static class TafsirEndpoints
|
||||
{
|
||||
public static RouteGroupBuilder MapTafsirEndpoints(this WebApplication app)
|
||||
{
|
||||
var group = app.MapGroup("/api");
|
||||
|
||||
group.MapGet("/tafsirs", async (AppDbContext db, CancellationToken ct) =>
|
||||
{
|
||||
var items = await db.Tafsirs
|
||||
.AsNoTracking()
|
||||
.Where(t => t.IsPublished)
|
||||
.OrderBy(t => t.Title)
|
||||
.Select(t => new TafsirDto(t.Id, t.Title, t.Author, t.Slug, t.CoverUrl))
|
||||
.ToListAsync(ct);
|
||||
return Results.Ok(items);
|
||||
});
|
||||
|
||||
group.MapGet("/tafsirs/{slug}/episodes", async (string slug, AppDbContext db, CancellationToken ct) =>
|
||||
{
|
||||
var tafsir = await db.Tafsirs.AsNoTracking().FirstOrDefaultAsync(t => t.Slug == slug && t.IsPublished, ct);
|
||||
if (tafsir is null) return Results.NotFound();
|
||||
|
||||
var episodes = await db.Episodes
|
||||
.AsNoTracking()
|
||||
.Include(e => e.Surah)
|
||||
.Where(e => e.TafsirId == tafsir.Id)
|
||||
.OrderBy(e => e.SortOrder)
|
||||
.ToListAsync(ct);
|
||||
|
||||
return Results.Ok(episodes.Select(e => EpisodeMapper.ToListItem(e)).ToList());
|
||||
});
|
||||
|
||||
group.MapGet("/episodes/{id:guid}", async (Guid id, AppDbContext db, CancellationToken ct) =>
|
||||
{
|
||||
var episode = await db.Episodes
|
||||
.AsNoTracking()
|
||||
.Include(e => e.Surah)
|
||||
.Include(e => e.Tafsir)
|
||||
.FirstOrDefaultAsync(e => e.Id == id, ct);
|
||||
if (episode is null) return Results.NotFound();
|
||||
|
||||
var ayah = await db.Ayahs.AsNoTracking()
|
||||
.FirstOrDefaultAsync(a => a.SurahId == episode.SurahId && a.Number == episode.AyahNumber, ct);
|
||||
|
||||
var detail = EpisodeMapper.ToDetail(episode, ayah) with
|
||||
{
|
||||
AudioUrl = $"/api/audio/{episode.Id}"
|
||||
};
|
||||
return Results.Ok(detail);
|
||||
});
|
||||
|
||||
group.MapGet("/audio/{episodeId:guid}", async (Guid episodeId, AppDbContext db, IWebHostEnvironment env, CancellationToken ct) =>
|
||||
{
|
||||
var episode = await db.Episodes.AsNoTracking().FirstOrDefaultAsync(e => e.Id == episodeId, ct);
|
||||
if (episode is null) return Results.NotFound();
|
||||
|
||||
var relative = episode.AudioUrl.TrimStart('/');
|
||||
var path = Path.Combine(env.ContentRootPath, "wwwroot", relative);
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
// Fallback placeholder: return empty audio-friendly response path for demo
|
||||
var placeholder = Path.Combine(env.ContentRootPath, "wwwroot", "audio", "silence.mp3");
|
||||
if (File.Exists(placeholder))
|
||||
return Results.File(placeholder, "audio/mpeg", enableRangeProcessing: true);
|
||||
return Results.NotFound();
|
||||
}
|
||||
|
||||
return Results.File(path, "audio/mpeg", enableRangeProcessing: true);
|
||||
});
|
||||
|
||||
return group;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
using TafsirQuran.Domain.Entities;
|
||||
using TafsirQuran.Shared.Dtos;
|
||||
|
||||
namespace TafsirQuran.Api.Mapping;
|
||||
|
||||
public static class EpisodeMapper
|
||||
{
|
||||
public static EpisodeListItemDto ToListItem(TafsirEpisode e, Ayah? ayah = null) =>
|
||||
new(
|
||||
e.Id,
|
||||
e.Title,
|
||||
e.Surah.Number,
|
||||
e.Surah.NameFa,
|
||||
e.AyahNumber,
|
||||
e.DurationSeconds,
|
||||
e.SortOrder);
|
||||
|
||||
public static EpisodeDetailDto ToDetail(TafsirEpisode e, Ayah? ayah) =>
|
||||
new(
|
||||
e.Id,
|
||||
e.TafsirId,
|
||||
e.Tafsir.Slug,
|
||||
e.Title,
|
||||
e.Surah.Number,
|
||||
e.Surah.NameFa,
|
||||
e.AyahNumber,
|
||||
ayah?.TextAr ?? string.Empty,
|
||||
ayah?.TextFa,
|
||||
e.AudioUrl,
|
||||
e.DurationSeconds,
|
||||
e.Summary);
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
using System.Text;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using TafsirQuran.Api.Auth;
|
||||
using TafsirQuran.Api.Endpoints;
|
||||
using TafsirQuran.Infrastructure;
|
||||
using TafsirQuran.Infrastructure.Persistence;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
builder.Services.AddInfrastructure(builder.Configuration);
|
||||
builder.Services.Configure<JwtOptions>(builder.Configuration.GetSection(JwtOptions.SectionName));
|
||||
builder.Services.AddSingleton<JwtTokenService>();
|
||||
|
||||
var jwt = builder.Configuration.GetSection(JwtOptions.SectionName).Get<JwtOptions>() ?? new JwtOptions();
|
||||
builder.Services
|
||||
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
||||
.AddJwtBearer(options =>
|
||||
{
|
||||
options.TokenValidationParameters = new TokenValidationParameters
|
||||
{
|
||||
ValidateIssuer = true,
|
||||
ValidateAudience = true,
|
||||
ValidateLifetime = true,
|
||||
ValidateIssuerSigningKey = true,
|
||||
ValidIssuer = jwt.Issuer,
|
||||
ValidAudience = jwt.Audience,
|
||||
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwt.Key))
|
||||
};
|
||||
});
|
||||
builder.Services.AddAuthorization();
|
||||
|
||||
builder.Services.AddCors(options =>
|
||||
{
|
||||
options.AddDefaultPolicy(policy =>
|
||||
policy.AllowAnyHeader().AllowAnyMethod().AllowAnyOrigin());
|
||||
});
|
||||
|
||||
builder.Services.AddOpenApi();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
using (var scope = app.Services.CreateScope())
|
||||
{
|
||||
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
for (var attempt = 1; ; attempt++)
|
||||
{
|
||||
try
|
||||
{
|
||||
await DbSeeder.SeedAsync(db);
|
||||
break;
|
||||
}
|
||||
catch (Exception ex) when (attempt < 30)
|
||||
{
|
||||
app.Logger.LogWarning(ex, "DB not ready (attempt {Attempt}); retry in 2s", attempt);
|
||||
await Task.Delay(TimeSpan.FromSeconds(2));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
app.MapOpenApi();
|
||||
}
|
||||
|
||||
app.UseCors();
|
||||
app.UseDefaultFiles();
|
||||
app.UseStaticFiles();
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
|
||||
app.MapTafsirEndpoints();
|
||||
app.MapProgressEndpoints();
|
||||
app.MapFavoriteEndpoints();
|
||||
app.MapBookmarkEndpoints();
|
||||
app.MapAuthEndpoints();
|
||||
|
||||
app.MapGet("/", () => Results.Ok(new { name = "TafsirQuran API", status = "ok" }));
|
||||
|
||||
app.Run();
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||
"profiles": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": false,
|
||||
"applicationUrl": "http://localhost:5080",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.10" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.10" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.10">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\TafsirQuran.Infrastructure\TafsirQuran.Infrastructure.csproj" />
|
||||
<ProjectReference Include="..\TafsirQuran.Shared\TafsirQuran.Shared.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,6 @@
|
||||
@TafsirQuran.Api_HostAddress = http://localhost:5025
|
||||
|
||||
GET {{TafsirQuran.Api_HostAddress}}/weatherforecast/
|
||||
Accept: application/json
|
||||
|
||||
###
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Debug",
|
||||
"Microsoft.AspNetCore": "Information"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"ConnectionStrings": {
|
||||
"Default": "Host=localhost;Port=5434;Database=tafsirquran;Username=tafsir;Password=tafsir_dev"
|
||||
},
|
||||
"Jwt": {
|
||||
"Key": "TafsirQuran-Dev-Secret-Key-Change-In-Production-32+",
|
||||
"Issuer": "TafsirQuran",
|
||||
"Audience": "TafsirQuran",
|
||||
"ExpireMinutes": 43200
|
||||
},
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user