Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
24 changes: 23 additions & 1 deletion backend/src/CCE.Api.Common/Auth/CceJwtAuthRegistration.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
using System.Text;
using System.Text;
using CCE.Api.Common.Results;
using CCE.Application.Identity.Auth.Common;
using CCE.Application.Messages;
using CCE.Infrastructure.Identity;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.IdentityModel.Tokens;
Expand Down Expand Up @@ -65,6 +68,8 @@ public static IServiceCollection AddCceJwtAuth(
};
// SignalR browser WebSocket clients can't set the Authorization header — they pass the JWT
// via ?access_token=. Accept it for hub requests so the hub authenticates over WebSockets.
// OnChallenge/OnForbidden write the standard CCE error envelope instead of the default
// empty 401/403 body, keeping the WWW-Authenticate header on 401 (RFC 6750).
jwt.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
Expand All @@ -76,6 +81,23 @@ public static IServiceCollection AddCceJwtAuth(
context.Token = accessToken;
}
return Task.CompletedTask;
},
OnChallenge = async context =>
{
context.HandleResponse();
context.Response.Headers.WWWAuthenticate = "Bearer";
await EnvelopeWriter.WriteAsync(
context.HttpContext,
StatusCodes.Status401Unauthorized,
MessageKeys.General.UNAUTHORIZED,
context.AuthenticateFailure?.Message).ConfigureAwait(false);
},
OnForbidden = async context =>
{
await EnvelopeWriter.WriteAsync(
context.HttpContext,
StatusCodes.Status403Forbidden,
MessageKeys.General.FORBIDDEN).ConfigureAwait(false);
}
};
});
Expand Down
15 changes: 8 additions & 7 deletions backend/src/CCE.Api.Common/Auth/DevAuthEndpoints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using HttpResults = Microsoft.AspNetCore.Http.Results;

namespace CCE.Api.Common.Auth;

Expand All @@ -28,7 +29,7 @@ public static IEndpointRouteBuilder MapDevAuthEndpoints(this IEndpointRouteBuild
var roleValue = role ?? "cce-admin";
if (!DevAuthHandler.RoleToUserId.ContainsKey(roleValue))
{
return Results.BadRequest(new
return HttpResults.BadRequest(new
{
error = $"Unknown dev role '{roleValue}'.",
validRoles = DevAuthHandler.RoleToUserId.Keys,
Expand All @@ -47,23 +48,23 @@ public static IEndpointRouteBuilder MapDevAuthEndpoints(this IEndpointRouteBuild
// Redirect to returnUrl if relative + safe; else home.
if (!string.IsNullOrEmpty(returnUrl) && returnUrl.StartsWith('/'))
{
return Results.Redirect(returnUrl);
return HttpResults.Redirect(returnUrl);
}
return Results.Redirect("/");
return HttpResults.Redirect("/");
}).AllowAnonymous().WithName("DevSignIn");

dev.MapPost("/sign-out", (HttpContext ctx) =>
{
ctx.Response.Cookies.Delete(DevAuthHandler.DevCookieName);
return Results.Ok(new { signedOut = true });
return HttpResults.Ok(new { signedOut = true });
}).AllowAnonymous().WithName("DevSignOut");

dev.MapGet("/whoami", (HttpContext ctx) =>
{
var name = ctx.User.Identity?.Name ?? "(anonymous)";
var roles = ctx.User.FindAll("roles").Select(c => c.Value).ToArray();
var sub = ctx.User.FindFirst("sub")?.Value ?? "(none)";
return Results.Ok(new { name, sub, roles });
return HttpResults.Ok(new { name, sub, roles });
}).AllowAnonymous().WithName("DevWhoAmI");

// ─── Frontend-compat shims at /auth/* ───────────────────────────
Expand All @@ -82,13 +83,13 @@ public static IEndpointRouteBuilder MapDevAuthEndpoints(this IEndpointRouteBuild
}
var rurl = string.IsNullOrEmpty(returnUrl) || !returnUrl.StartsWith('/') ? "/" : returnUrl;
var target = $"/dev/sign-in?role={Uri.EscapeDataString(defaultRole)}&returnUrl={Uri.EscapeDataString(rurl)}";
return Results.Redirect(target);
return HttpResults.Redirect(target);
}).AllowAnonymous().WithName("AuthLoginShim");

app.MapPost("/auth/logout", (HttpContext ctx) =>
{
ctx.Response.Cookies.Delete(DevAuthHandler.DevCookieName);
return Results.Ok(new { signedOut = true });
return HttpResults.Ok(new { signedOut = true });
}).AllowAnonymous().WithName("AuthLogoutShim");

return app;
Expand Down
1 change: 0 additions & 1 deletion backend/src/CCE.Api.Common/CCE.Api.Common.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@
<PackageReference Include="Swashbuckle.AspNetCore" />
<PackageReference Include="Swashbuckle.AspNetCore.Annotations" />
<PackageReference Include="StackExchange.Redis" />
<PackageReference Include="Hellang.Middleware.ProblemDetails" />
<PackageReference Include="AspNetCore.HealthChecks.SqlServer" />
<PackageReference Include="AspNetCore.HealthChecks.Redis" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.StackExchangeRedis" />
Expand Down
8 changes: 4 additions & 4 deletions backend/src/CCE.Api.Common/Extensions/ResponseExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using CCE.Api.Common.HttpResults;
using CCE.Api.Common.Results;
using CCE.Application.Common;
using CCE.Domain.Common;
using Microsoft.AspNetCore.Http;
Expand All @@ -11,14 +11,14 @@ public static class ResponseExtensions
/// Maps a <see cref="Response{T}"/> to a typed <see cref="IResult"/> with correct HTTP status,
/// injecting traceId and timestamp, and registering Swashbuckle metadata.
/// </summary>
public static OkApiResponse<T> ToHttpResult<T>(this Response<T> response)
public static OkApiResult<T> ToHttpResult<T>(this Response<T> response)
=> new(response);

/// <summary>Shorthand for 201 Created with Swashbuckle metadata.</summary>
public static CreatedApiResponse<T> ToCreatedHttpResult<T>(this Response<T> response)
public static CreatedApiResult<T> ToCreatedHttpResult<T>(this Response<T> response)
=> new(response);

/// <summary>Shorthand for 204 No Content with Swashbuckle metadata.</summary>
public static NoContentApiResponse ToNoContentHttpResult(this Response<VoidData> response)
public static NoContentApiResult ToNoContentHttpResult(this Response<VoidData> response)
=> new(response);
}
24 changes: 0 additions & 24 deletions backend/src/CCE.Api.Common/Extensions/ResultExtensions.cs

This file was deleted.

98 changes: 96 additions & 2 deletions backend/src/CCE.Api.Common/Localization/Resources.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -125,8 +125,8 @@ INTERNAL_ERROR:
en: "An unexpected error occurred"

BAD_REQUEST:
ar: "عذرًا، البيانات المدخلة غير صحيحة"
en: "Sorry, the entered data is invalid"
ar: "تعذر معالجة الطلب"
en: "The request could not be processed"

RESOURCE_NOT_FOUND_GENERIC:
ar: "المورد غير موجود"
Expand All @@ -140,6 +140,14 @@ DUPLICATE_VALUE:
ar: "القيمة موجودة بالفعل"
en: "Value already exists"

RATE_LIMIT_EXCEEDED:
ar: "تم تجاوز الحد المسموح من الطلبات. يرجى المحاولة مرة أخرى لاحقًا"
en: "Too many requests. Please try again later."

BUSINESS_RULE_VIOLATION:
ar: "تعذر إتمام العملية بسبب مخالفة قاعدة العمل"
en: "The operation could not be completed due to a business rule violation."

SCENARIO_NOT_FOUND:
ar: "السيناريو غير موجود"
en: "Scenario not found"
Expand Down Expand Up @@ -257,6 +265,18 @@ NOTIFICATION_TEMPLATE_UPDATED:
ar: "تم تحديث قالب الإشعار بنجاح"
en: "Notification template updated successfully"

DEVICE_TOKEN_REGISTERED:
ar: "تم تسجيل الجهاز بنجاح"
en: "Device registered successfully"

DEVICE_TOKEN_DELETED:
ar: "تم إلغاء تسجيل الجهاز بنجاح"
en: "Device unregistered successfully"

DEVICE_TOKEN_NOT_FOUND:
ar: "رمز الجهاز غير موجود"
en: "Device token not found"

EVALUATION_NOT_FOUND:
ar: "التقييم غير موجود"
en: "Evaluation not found"
Expand Down Expand Up @@ -658,6 +678,14 @@ PASSWORD_NUMBER:
ar: "يجب أن تحتوي كلمة المرور على رقم على الأقل"
en: "Password must contain at least one number"

PASSWORD_POLICY:
ar: "كلمة المرور يجب أن تتراوح بين 12 و20 حرفاً وتحتوي على أحرف كبيرة وصغيرة وأرقام"
en: "Password must be 12-20 characters and contain uppercase, lowercase, and numbers"

PASSWORDS_MUST_MATCH:
ar: "كلمتا المرور غير متطابقتين"
en: "Passwords do not match"

# ─── General Errors ───

EXTERNAL_API_ERROR:
Expand Down Expand Up @@ -725,3 +753,69 @@ INTERACTIVE_MAP_NODE_DELETED:
ar: "تم حذف العنصر من الخريطة التفاعلية بنجاح"
en: "Interactive map node deleted successfully"

# ─── Identity / Permissions / Claims ───

ROLE_NOT_FOUND:
ar: "الدور غير موجود"
en: "Role not found"

INVALID_RESET_TOKEN:
ar: "رمز إعادة تعيين كلمة المرور غير صالح أو منتهي الصلاحية"
en: "Password reset token is invalid or expired"

EMAIL_CHANGE_FAILED:
ar: "حدث خطأ أثناء تغيير البريد الإلكتروني"
en: "An error occurred while changing the email address"

AD_LOGIN_SUCCESS:
ar: "تم تسجيل الدخول عبر Active Directory بنجاح"
en: "Logged in via Active Directory successfully"

PERMISSIONS_GRANTED:
ar: "تم منح الصلاحيات للدور بنجاح"
en: "Permissions granted to role successfully"

PERMISSIONS_REVOKED:
ar: "تم سحب الصلاحيات من الدور بنجاح"
en: "Permissions revoked from role successfully"

PERMISSIONS_UPDATED:
ar: "تم تحديث صلاحيات الدور بنجاح"
en: "Role permissions updated successfully"

CLAIMS_GRANTED:
ar: "تم منح المطالبات للمستخدم بنجاح"
en: "Claims granted to user successfully"

CLAIMS_REVOKED:
ar: "تم سحب المطالبات من المستخدم بنجاح"
en: "Claims revoked from user successfully"

USER_CLAIMS_UPDATED:
ar: "تم تحديث مطالبات المستخدم بنجاح"
en: "User claims updated successfully"

# ─── Verification ───

OTP_UNAUTHORIZED:
ar: "غير مصرح لك بالتحقق من هذا الرمز"
en: "You are not authorized to verify this code"

# ─── Content / Community / Platform Settings ───

TAG_NOT_FOUND:
ar: "الوسم غير موجود"
en: "Tag not found"

NEWSLETTER_SUBSCRIBED:
ar: "تم الاشتراك في النشرة البريدية بنجاح"
en: "Subscribed to newsletter successfully"

TOPICS_LISTED:
ar: "تم جلب الموضوعات بنجاح"
en: "Topics listed successfully"

SECTION_REORDERED:
ar: "تم إعادة ترتيب الأقسام بنجاح"
en: "Sections reordered successfully"

Loading