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 Microsoft.AspNetCore.Mvc;
|
||||
using System.Security.Claims;
|
||||
|
||||
namespace Media.JoshHeaps.Net.Api;
|
||||
|
||||
@@ -17,15 +18,14 @@ public class MediaApi : ControllerBase
|
||||
[HttpGet("load")]
|
||||
public async Task<IActionResult> LoadMedia([FromQuery] int offset = 0, [FromQuery] int limit = 20)
|
||||
{
|
||||
// Get user ID from session
|
||||
var userIdString = HttpContext.Session.GetString("UserId");
|
||||
if (string.IsNullOrEmpty(userIdString))
|
||||
// Get user ID from JWT claims or session
|
||||
var userId = GetUserIdFromAuth();
|
||||
if (userId == null)
|
||||
{
|
||||
return Unauthorized();
|
||||
}
|
||||
|
||||
var userId = long.Parse(userIdString);
|
||||
var media = await _mediaService.GetUserMediaAsync(userId, offset, limit);
|
||||
var media = await _mediaService.GetUserMediaAsync(userId.Value, offset, limit);
|
||||
|
||||
return Ok(media);
|
||||
}
|
||||
@@ -33,24 +33,22 @@ public class MediaApi : ControllerBase
|
||||
[HttpGet("image/{mediaId}")]
|
||||
public async Task<IActionResult> GetImage(long mediaId)
|
||||
{
|
||||
// Get user ID from session
|
||||
var userIdString = HttpContext.Session.GetString("UserId");
|
||||
if (string.IsNullOrEmpty(userIdString))
|
||||
// Get user ID from JWT claims or session
|
||||
var userId = GetUserIdFromAuth();
|
||||
if (userId == null)
|
||||
{
|
||||
return Unauthorized(new { error = "Not authenticated" });
|
||||
}
|
||||
|
||||
var userId = long.Parse(userIdString);
|
||||
|
||||
// Get media metadata (includes ownership check)
|
||||
var media = await _mediaService.GetMediaByIdAsync(mediaId, userId);
|
||||
var media = await _mediaService.GetMediaByIdAsync(mediaId, userId.Value);
|
||||
if (media == null)
|
||||
{
|
||||
return NotFound(new { error = "Image not found or access denied" });
|
||||
}
|
||||
|
||||
// Get decrypted image data
|
||||
var imageData = await _mediaService.GetDecryptedMediaDataAsync(mediaId, userId);
|
||||
var imageData = await _mediaService.GetDecryptedMediaDataAsync(mediaId, userId.Value);
|
||||
if (imageData == null)
|
||||
{
|
||||
return NotFound(new { error = "Image file not found" });
|
||||
@@ -59,4 +57,26 @@ public class MediaApi : ControllerBase
|
||||
// Return image with proper content type
|
||||
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>
|
||||
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
|
||||
<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="SixLabors.ImageSharp" Version="3.1.11" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
using Media.JoshHeaps.Net;
|
||||
using Media.JoshHeaps.Net.Services;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using System.Text;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
@@ -22,6 +25,30 @@ builder.Services.AddSession(options =>
|
||||
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();
|
||||
|
||||
// Configure the HTTP request pipeline.
|
||||
|
||||
@@ -7,6 +7,11 @@
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
"AppUrl": "https://media.joshheaps.net",
|
||||
"Jwt": {
|
||||
"Issuer": "https://media.joshheaps.net",
|
||||
"Audience": "https://media.joshheaps.net",
|
||||
"ExpiryInDays": 30
|
||||
},
|
||||
"Email": {
|
||||
"SmtpHost": "smtp.gmail.com",
|
||||
"SmtpPort": "587",
|
||||
|
||||
Reference in New Issue
Block a user