[Profile] Editable profile (avatar + resume) + role-based profile dropdown menu
CI/CD / CI · dotnet build (push) Successful in 44s
CI/CD / Deploy · hamkadr (push) Successful in 57s

Every user gets a full editable profile at /Me/Profile: name, role, city, specialty/title, license, years, bio + avatar image upload + resume upload (PDF/image). Avatar/resume stored in-DB on User (migration, 5 nullable columns). Endpoints: /avatar/{id} (public) and /resume/{id} (owner, admin, or an employer who received that user's application). Nav: replaced the scattered action links with an avatar button + dropdown listing all of the user's pages by role (profile, کارجو panel, alerts, preferences, notifications; employer panel; admin panel + settings; logout) — shows the avatar image or initials; collapses into the burger menu on mobile; closes on outside-click.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-04 21:49:40 +03:30
parent 167d263560
commit e633463906
9 changed files with 1689 additions and 15 deletions
+30
View File
@@ -221,6 +221,36 @@ app.MapGet("/facility-doc/{id:int}", async (int id, HttpContext ctx, AppDbContex
return Results.File(doc.Data, doc.ContentType, doc.FileName);
}).RequireAuthorization();
// Profile avatar — public (low-sensitivity), cached. 404 when the user has none.
app.MapGet("/avatar/{id:int}", async (int id, AppDbContext db) =>
{
var u = await db.Users.Where(x => x.Id == id)
.Select(x => new { x.Avatar, x.AvatarContentType }).FirstOrDefaultAsync();
if (u?.Avatar is null) return Results.NotFound();
return Results.File(u.Avatar, u.AvatarContentType ?? "image/jpeg");
});
// Résumé — readable by the owner, an admin, or an employer who received this user's application.
app.MapGet("/resume/{id:int}", async (int id, HttpContext ctx, AppDbContext db) =>
{
var u = await db.Users.Where(x => x.Id == id)
.Select(x => new { x.Resume, x.ResumeContentType, x.ResumeFileName }).FirstOrDefaultAsync();
if (u?.Resume is null) return Results.NotFound();
var meId = int.TryParse(ctx.User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value, out var n) ? n : (int?)null;
var allowed = ctx.User.IsInRole("Admin") || meId == id;
if (!allowed && meId is int viewer)
{
var vIds = await db.Visitors.Where(v => v.UserId == id).Select(v => v.Id).ToListAsync();
allowed = await db.InterestEvents.AnyAsync(e => e.EventType == InterestEventType.Apply
&& vIds.Contains(e.VisitorId)
&& ((e.Shift != null && e.Shift.Facility.OwnerUserId == viewer)
|| (e.JobOpening != null && e.JobOpening.Facility.OwnerUserId == viewer)));
}
if (!allowed) return Results.Forbid();
return Results.File(u.Resume, u.ResumeContentType ?? "application/octet-stream", u.ResumeFileName ?? "resume");
}).RequireAuthorization();
// User-submitted report against a listing (abuse/fake/wrong info).
app.MapPost("/report", async (HttpContext ctx, AppDbContext db, VisitorContext vc,
[FromForm] string targetType, [FromForm] int targetId, [FromForm] string reason,