Update to allow read access for app
This commit is contained in:
@@ -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();
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user