mirror of
https://github.com/JogmansonReal/Radicade.git
synced 2026-10-25 23:18:56 -07:00
279 lines
9.2 KiB
C#
279 lines
9.2 KiB
C#
using Microsoft.AspNetCore.Authorization;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using RadicadeServer.DB;
|
|
using RadicadeServer.Extensions;
|
|
using RadicadeServer.RecNet.Accounts;
|
|
using RadicadeServer.RecNet.Auth;
|
|
using RadicadeServer.RecNet.CDN;
|
|
using RadicadeServer.RecNet.Enums;
|
|
using RadicadeServer.RecNet.Relationships;
|
|
using RadicadeServer.Radicade.Mail;
|
|
|
|
namespace RadicadeServer.Web
|
|
{
|
|
|
|
|
|
|
|
|
|
|
|
[Route("web")]
|
|
public class WebAPI : Controller
|
|
{
|
|
private readonly RecDBContext db;
|
|
|
|
public WebAPI(RecDBContext context) {
|
|
this.db = context;
|
|
}
|
|
|
|
private static string ImageUrl(string? name)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(name)) return $"{DBConfig.NameServer.Images}/default";
|
|
if (name.Equals("default", StringComparison.OrdinalIgnoreCase) ||
|
|
name.Equals("default.png", StringComparison.OrdinalIgnoreCase) ||
|
|
name.Equals("DefaultRoomImage", StringComparison.OrdinalIgnoreCase) ||
|
|
name.Equals("DefaultProfileImage", StringComparison.OrdinalIgnoreCase))
|
|
return $"{DBConfig.NameServer.Images}/default";
|
|
return $"{DBConfig.NameServer.Images}/img/{name}";
|
|
}
|
|
|
|
private async Task<long> SubscriberCountAsync(long accountId)
|
|
{
|
|
|
|
|
|
return await db.Relationships.AsNoTracking()
|
|
.CountAsync(x => x.PlayerId == accountId && x.RelationshipType == RelationshipType.Friend);
|
|
}
|
|
|
|
|
|
[HttpGet("me"), Authorize]
|
|
public async Task<IActionResult> Me()
|
|
{
|
|
var userId = User.GetUserId();
|
|
if (!userId.HasValue) return Unauthorized();
|
|
|
|
var profile = await db.Profiles.AsNoTracking().FirstOrDefaultAsync(x => x.accountId == userId.Value);
|
|
if (profile == null) return NotFound(new { success = false, message = "Account not found." });
|
|
|
|
var plrData = await db.Players.AsNoTracking().FirstOrDefaultAsync(x => x.AccountId == userId.Value);
|
|
|
|
return Ok(new
|
|
{
|
|
success = true,
|
|
accountId = profile.accountId,
|
|
username = profile.username,
|
|
displayName = profile.displayName,
|
|
bio = profile.bio ?? "",
|
|
createdAt = profile.createdAt,
|
|
profileImage = ImageUrl(profile.profileImage),
|
|
email = plrData?.Email ?? "",
|
|
emailVerified = plrData?.EmailVerified ?? false,
|
|
hasPassword = !string.IsNullOrWhiteSpace(plrData?.Hashword),
|
|
subscriberCount = await SubscriberCountAsync(userId.Value)
|
|
});
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[HttpGet("profile/{accountId:long}")]
|
|
public async Task<IActionResult> Profile(long accountId)
|
|
{
|
|
var profile = await db.Profiles.AsNoTracking().FirstOrDefaultAsync(x => x.accountId == accountId);
|
|
if (profile == null)
|
|
return NotFound(new { success = false, message = "Player not found." });
|
|
|
|
|
|
var photos = new List<object>();
|
|
var imageRows = await db.Images.AsNoTracking()
|
|
.Where(x => x.OwnerId == accountId && x.Accessibility == SavedImageAccessibility.Public)
|
|
.OrderByDescending(x => x.CreatedAt)
|
|
.Take(24)
|
|
.ToListAsync();
|
|
foreach (var img in imageRows)
|
|
{
|
|
string? roomName = null;
|
|
if (img.RoomId.HasValue)
|
|
{
|
|
var room = await db.Rooms.AsNoTracking().FirstOrDefaultAsync(x => x.RoomId == img.RoomId.Value);
|
|
if (room != null) roomName = room.Name;
|
|
}
|
|
var cheers = await db.ImageInteractions.AsNoTracking().CountAsync(x => x.SavedImageId == img.Id);
|
|
photos.Add(new
|
|
{
|
|
id = img.Id,
|
|
image = ImageUrl(img.Name),
|
|
roomName = roomName ?? "Unknown room",
|
|
cheers,
|
|
createdAt = img.CreatedAt
|
|
});
|
|
}
|
|
|
|
|
|
var rooms = await db.Rooms.AsNoTracking()
|
|
.Where(x => x.CreatorAccountId == accountId)
|
|
.OrderByDescending(x => x.CreatedAt)
|
|
.Take(24)
|
|
.Select(x => new
|
|
{
|
|
roomId = x.RoomId,
|
|
name = x.Name,
|
|
description = x.Description ?? "",
|
|
image = ImageUrl(x.ImageName),
|
|
maxPlayers = x.MaxPlayers,
|
|
createdAt = x.CreatedAt
|
|
})
|
|
.ToListAsync();
|
|
|
|
|
|
var events = await db.PlayerEvents.AsNoTracking()
|
|
.Where(x => x.CreatorPlayerId == accountId)
|
|
.OrderBy(x => x.StartTime)
|
|
.Take(24)
|
|
.Select(x => new
|
|
{
|
|
eventId = x.PlayerEventId,
|
|
name = x.Name,
|
|
description = x.Description,
|
|
image = ImageUrl(x.ImageName),
|
|
startTime = x.StartTime,
|
|
endTime = x.EndTime,
|
|
attendeeCount = x.AttendeeCount
|
|
})
|
|
.ToListAsync();
|
|
|
|
return Ok(new
|
|
{
|
|
success = true,
|
|
profile = new
|
|
{
|
|
accountId = profile.accountId,
|
|
username = profile.username,
|
|
displayName = profile.displayName,
|
|
bio = profile.bio ?? "",
|
|
createdAt = profile.createdAt,
|
|
profileImage = ImageUrl(profile.profileImage),
|
|
subscriberCount = await SubscriberCountAsync(accountId)
|
|
},
|
|
photos,
|
|
rooms,
|
|
events
|
|
});
|
|
}
|
|
|
|
|
|
public class ForgotPasswordRequest
|
|
{
|
|
public string? Email { get; set; }
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
[HttpPost("password/forgot")]
|
|
public async Task<IActionResult> ForgotPassword([FromBody] ForgotPasswordRequest? req)
|
|
{
|
|
var email = (req?.Email ?? "").Trim();
|
|
if (string.IsNullOrWhiteSpace(email) || !email.Contains('@'))
|
|
return Ok(new { success = false, message = "Enter a valid email address." });
|
|
|
|
var plrData = await db.Players.FirstOrDefaultAsync(x => x.Email.ToLower() == email.ToLower());
|
|
if (plrData != null)
|
|
{
|
|
plrData.PasswordResetToken = Guid.NewGuid().ToString("N");
|
|
plrData.PasswordResetExpires = DateTime.UtcNow.AddHours(1);
|
|
await db.SaveChangesAsync();
|
|
|
|
SendResetEmail(plrData);
|
|
}
|
|
|
|
|
|
return Ok(new { success = true, message = "If an account exists for that email, a reset link has been sent." });
|
|
}
|
|
|
|
|
|
public class ResetPasswordRequest
|
|
{
|
|
public string? Code { get; set; }
|
|
public string? NewPassword { get; set; }
|
|
}
|
|
|
|
|
|
[HttpGet("password/reset/result")]
|
|
public async Task<IActionResult> ResetResult([FromQuery] string code)
|
|
{
|
|
var (ok, message) = await ValidateResetCode(code);
|
|
return Ok(new { success = ok, message });
|
|
}
|
|
|
|
|
|
[HttpPost("password/reset")]
|
|
public async Task<IActionResult> ResetPassword([FromBody] ResetPasswordRequest? req)
|
|
{
|
|
var code = req?.Code?.Trim() ?? "";
|
|
var newPassword = req?.NewPassword ?? "";
|
|
|
|
if (string.IsNullOrWhiteSpace(code)) return Ok(new { success = false, message = "This reset link is missing its code." });
|
|
if (newPassword.Length < Auth.AuthAPI.MinPasswordLength)
|
|
return Ok(new { success = false, message = $"Password must be at least {Auth.AuthAPI.MinPasswordLength} characters." });
|
|
|
|
var plrData = await db.Players.FirstOrDefaultAsync(x => x.PasswordResetToken == code);
|
|
if (plrData == null)
|
|
return Ok(new { success = false, message = "This reset link is invalid or has already been used." });
|
|
if (!plrData.PasswordResetExpires.HasValue || plrData.PasswordResetExpires.Value < DateTime.UtcNow)
|
|
return Ok(new { success = false, message = "This reset link has expired. Request a new one." });
|
|
|
|
var hasher = new Microsoft.AspNetCore.Identity.PasswordHasher<PlayerData>();
|
|
plrData.Hashword = hasher.HashPassword(plrData, newPassword);
|
|
plrData.PasswordResetToken = null;
|
|
plrData.PasswordResetExpires = null;
|
|
|
|
|
|
|
|
|
|
var staleLogins = await db.CachedLogins.Where(x => x.accountId == plrData.AccountId).ToListAsync();
|
|
if (staleLogins.Count > 0)
|
|
db.CachedLogins.RemoveRange(staleLogins);
|
|
plrData.TokenVersion++;
|
|
Utils.TokenRevocation.Bump(plrData.AccountId, plrData.TokenVersion);
|
|
|
|
await db.SaveChangesAsync();
|
|
|
|
Utils.AuthSecurityMonitor.NoteCredentialChange(plrData.AccountId, "password reset completed");
|
|
return Ok(new { success = true, message = "Password updated. You can now sign in with your new password." });
|
|
}
|
|
|
|
private async Task<(bool Ok, string Message)> ValidateResetCode(string code)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(code))
|
|
return (false, "This reset link is missing its code.");
|
|
var plrData = await db.Players.AsNoTracking().FirstOrDefaultAsync(x => x.PasswordResetToken == code);
|
|
if (plrData == null)
|
|
return (false, "This reset link is invalid or has already been used.");
|
|
if (!plrData.PasswordResetExpires.HasValue || plrData.PasswordResetExpires.Value < DateTime.UtcNow)
|
|
return (false, "This reset link has expired. Request a new one.");
|
|
return (true, "This reset link is valid.");
|
|
}
|
|
|
|
private void SendResetEmail(PlayerData plrData)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(plrData.PasswordResetToken)) return;
|
|
|
|
var link = $"https://radicade.app/reset?code={plrData.PasswordResetToken}";
|
|
var html =
|
|
"<div style=\"font-family:Arial,sans-serif;max-width:520px;margin:0 auto;padding:24px;\">" +
|
|
"<img src=\"https://radicade.app/assests/RadicadeHorizontal.png\" alt=\"Radicade\" style=\"display:block;max-width:220px;height:auto;margin:0 auto 22px;\" />" +
|
|
"<h2 style=\"color:#1a1a2e;margin-top:0;\">Reset your Radicade password</h2>" +
|
|
"<p style=\"color:#444;font-size:15px;line-height:1.6;\">Hi there,</p>" +
|
|
"<p style=\"color:#444;font-size:15px;line-height:1.6;\">We received a request to reset the password on your Radicade account. If that was you, click the button below to choose a new password:</p>" +
|
|
"<p style=\"text-align:center;margin:28px 0;\"><a href=\"" + link + "\" style=\"background:#4a6cf7;color:#fff;text-decoration:none;padding:12px 28px;border-radius:6px;display:inline-block;\">Reset my password</a></p>" +
|
|
"<p style=\"color:#999;font-size:12px;margin-top:24px;\">This link expires in one hour. If you didn't request this, you can safely ignore this email.</p></div>";
|
|
|
|
_ = RadicadeMail.SendAsync(plrData.Email, "Reset your password - Radicade", html);
|
|
}
|
|
}
|
|
}
|