Update to allow read access for app

This commit is contained in:
jheaps
2025-10-16 14:59:45 -06:00
parent 4bf29488dd
commit 4b68886252
5 changed files with 184 additions and 12 deletions
+119
View File
@@ -0,0 +1,119 @@
using Media.JoshHeaps.Net.Models;
using Media.JoshHeaps.Net.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
namespace Media.JoshHeaps.Net.Api;
[ApiController]
[Route("api/auth")]
public class AuthApi : ControllerBase
{
private readonly AuthService _authService;
private readonly IConfiguration _configuration;
public AuthApi(AuthService authService, IConfiguration configuration)
{
_authService = authService;
_configuration = configuration;
}
[HttpPost("login")]
public async Task<IActionResult> Login([FromBody] LoginRequest request)
{
if (string.IsNullOrWhiteSpace(request.EmailOrUsername) || string.IsNullOrWhiteSpace(request.Password))
{
return BadRequest(new { error = "Email/username and password are required" });
}
var (success, error, userInfo) = await _authService.LoginAsync(request.EmailOrUsername, request.Password);
if (!success || userInfo == null)
{
return Unauthorized(new { error = error ?? "Invalid credentials" });
}
// Generate JWT token
var token = GenerateJwtToken(userInfo);
return Ok(new LoginResponse
{
Token = token,
User = userInfo
});
}
[Authorize]
[HttpPost("validate")]
public IActionResult Validate()
{
// If we get here, the JWT token is valid
var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
var email = User.FindFirst(ClaimTypes.Email)?.Value;
var username = User.FindFirst(ClaimTypes.Name)?.Value;
var emailVerified = User.FindFirst("EmailVerified")?.Value;
if (string.IsNullOrEmpty(userId))
{
return Unauthorized(new { error = "Invalid token" });
}
return Ok(new
{
user = new UserLoginInfo
{
Id = long.Parse(userId),
Email = email ?? string.Empty,
Username = username ?? string.Empty,
EmailVerified = bool.Parse(emailVerified ?? "false"),
IsActive = true
}
});
}
private string GenerateJwtToken(UserLoginInfo user)
{
var jwtKey = _configuration["Jwt:Key"] ?? throw new InvalidOperationException("JWT Key not configured");
var jwtIssuer = _configuration["Jwt:Issuer"] ?? throw new InvalidOperationException("JWT Issuer not configured");
var jwtAudience = _configuration["Jwt:Audience"] ?? throw new InvalidOperationException("JWT Audience not configured");
var jwtExpiryDays = int.Parse(_configuration["Jwt:ExpiryInDays"] ?? "30");
var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtKey));
var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);
var claims = new[]
{
new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
new Claim(ClaimTypes.Email, user.Email),
new Claim(ClaimTypes.Name, user.Username),
new Claim("EmailVerified", user.EmailVerified.ToString())
};
var token = new JwtSecurityToken(
issuer: jwtIssuer,
audience: jwtAudience,
claims: claims,
expires: DateTime.UtcNow.AddDays(jwtExpiryDays),
signingCredentials: credentials
);
return new JwtSecurityTokenHandler().WriteToken(token);
}
}
// Request/Response models
public class LoginRequest
{
public string EmailOrUsername { get; set; } = string.Empty;
public string Password { get; set; } = string.Empty;
}
public class LoginResponse
{
public string Token { get; set; } = string.Empty;
public UserLoginInfo User { get; set; } = new();
}
+32 -12
View File
@@ -1,5 +1,6 @@
using Media.JoshHeaps.Net.Services; using Media.JoshHeaps.Net.Services;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using System.Security.Claims;
namespace Media.JoshHeaps.Net.Api; namespace Media.JoshHeaps.Net.Api;
@@ -17,15 +18,14 @@ public class MediaApi : ControllerBase
[HttpGet("load")] [HttpGet("load")]
public async Task<IActionResult> LoadMedia([FromQuery] int offset = 0, [FromQuery] int limit = 20) public async Task<IActionResult> LoadMedia([FromQuery] int offset = 0, [FromQuery] int limit = 20)
{ {
// Get user ID from session // Get user ID from JWT claims or session
var userIdString = HttpContext.Session.GetString("UserId"); var userId = GetUserIdFromAuth();
if (string.IsNullOrEmpty(userIdString)) if (userId == null)
{ {
return Unauthorized(); return Unauthorized();
} }
var userId = long.Parse(userIdString); var media = await _mediaService.GetUserMediaAsync(userId.Value, offset, limit);
var media = await _mediaService.GetUserMediaAsync(userId, offset, limit);
return Ok(media); return Ok(media);
} }
@@ -33,24 +33,22 @@ public class MediaApi : ControllerBase
[HttpGet("image/{mediaId}")] [HttpGet("image/{mediaId}")]
public async Task<IActionResult> GetImage(long mediaId) public async Task<IActionResult> GetImage(long mediaId)
{ {
// Get user ID from session // Get user ID from JWT claims or session
var userIdString = HttpContext.Session.GetString("UserId"); var userId = GetUserIdFromAuth();
if (string.IsNullOrEmpty(userIdString)) if (userId == null)
{ {
return Unauthorized(new { error = "Not authenticated" }); return Unauthorized(new { error = "Not authenticated" });
} }
var userId = long.Parse(userIdString);
// Get media metadata (includes ownership check) // Get media metadata (includes ownership check)
var media = await _mediaService.GetMediaByIdAsync(mediaId, userId); var media = await _mediaService.GetMediaByIdAsync(mediaId, userId.Value);
if (media == null) if (media == null)
{ {
return NotFound(new { error = "Image not found or access denied" }); return NotFound(new { error = "Image not found or access denied" });
} }
// Get decrypted image data // Get decrypted image data
var imageData = await _mediaService.GetDecryptedMediaDataAsync(mediaId, userId); var imageData = await _mediaService.GetDecryptedMediaDataAsync(mediaId, userId.Value);
if (imageData == null) if (imageData == null)
{ {
return NotFound(new { error = "Image file not found" }); return NotFound(new { error = "Image file not found" });
@@ -59,4 +57,26 @@ public class MediaApi : ControllerBase
// Return image with proper content type // Return image with proper content type
return File(imageData, media.MimeType); return File(imageData, media.MimeType);
} }
/// <summary>
/// Gets user ID from either JWT claims (for API) or session (for web)
/// </summary>
private long? GetUserIdFromAuth()
{
// First try JWT claims (for mobile/API authentication)
var userIdClaim = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (!string.IsNullOrEmpty(userIdClaim) && long.TryParse(userIdClaim, out var jwtUserId))
{
return jwtUserId;
}
// Fall back to session (for web authentication)
var userIdString = HttpContext.Session.GetString("UserId");
if (!string.IsNullOrEmpty(userIdString) && long.TryParse(userIdString, out var sessionUserId))
{
return sessionUserId;
}
return null;
}
} }
@@ -10,6 +10,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" /> <PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
<PackageReference Include="MailKit" Version="4.8.0" /> <PackageReference Include="MailKit" Version="4.8.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.11" />
<PackageReference Include="Npgsql" Version="9.0.4" /> <PackageReference Include="Npgsql" Version="9.0.4" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.11" /> <PackageReference Include="SixLabors.ImageSharp" Version="3.1.11" />
</ItemGroup> </ItemGroup>
+27
View File
@@ -1,5 +1,8 @@
using Media.JoshHeaps.Net; using Media.JoshHeaps.Net;
using Media.JoshHeaps.Net.Services; using Media.JoshHeaps.Net.Services;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using System.Text;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
@@ -22,6 +25,30 @@ builder.Services.AddSession(options =>
options.Cookie.IsEssential = true; options.Cookie.IsEssential = true;
}); });
// Add JWT authentication
var jwtKey = builder.Configuration["Jwt:Key"] ?? throw new InvalidOperationException("JWT Key not configured");
var jwtIssuer = builder.Configuration["Jwt:Issuer"] ?? throw new InvalidOperationException("JWT Issuer not configured");
var jwtAudience = builder.Configuration["Jwt:Audience"] ?? throw new InvalidOperationException("JWT Audience not configured");
builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = jwtIssuer,
ValidAudience = jwtAudience,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtKey))
};
});
var app = builder.Build(); var app = builder.Build();
// Configure the HTTP request pipeline. // Configure the HTTP request pipeline.
+5
View File
@@ -7,6 +7,11 @@
}, },
"AllowedHosts": "*", "AllowedHosts": "*",
"AppUrl": "https://media.joshheaps.net", "AppUrl": "https://media.joshheaps.net",
"Jwt": {
"Issuer": "https://media.joshheaps.net",
"Audience": "https://media.joshheaps.net",
"ExpiryInDays": 30
},
"Email": { "Email": {
"SmtpHost": "smtp.gmail.com", "SmtpHost": "smtp.gmail.com",
"SmtpPort": "587", "SmtpPort": "587",