Added support for custom user avatars

* Added support for custom user avatars
This commit is contained in:
Michael Green
2024-02-06 19:43:15 +11:00
committed by GitHub
parent 645327bdd1
commit 3c451f5558
12 changed files with 309 additions and 17 deletions

View File

@@ -12,5 +12,6 @@ namespace Authentication
{
public SecurityProfileViewModel SecurityProfile { get; set; }
public List<UserPreferenceViewModel> UserPreferences { get; set; }
public Guid Avatar { get; set; }
}
}

View File

@@ -75,7 +75,7 @@ namespace Authentication
public TUser GetUserById(string userId)
{
TUser user = null;
string commandText = "Select * from Users where Id = @id";
string commandText = "Select * from Users LEFT JOIN (SELECT UserId, Id AS AvatarId FROM UserAvatars) UserAvatars ON users.Id = UserAvatars.UserId where Id = @id";
Dictionary<string, object> parameters = new Dictionary<string, object>() { { "@id", userId } };
var rows = _database.ExecuteCMDDict(commandText, parameters);
@@ -100,6 +100,7 @@ namespace Authentication
user.TwoFactorEnabled = row["TwoFactorEnabled"] == "1" ? true:false;
user.SecurityProfile = GetSecurityProfile(user);
user.UserPreferences = GetPreferences(user);
user.Avatar = string.IsNullOrEmpty((string?)row["AvatarId"]) ? Guid.Empty : Guid.Parse((string?)row["AvatarId"]);
}
return user;
@@ -113,7 +114,7 @@ namespace Authentication
public List<TUser> GetUserByName(string normalizedUserName)
{
List<TUser> users = new List<TUser>();
string commandText = "Select * from Users where NormalizedEmail = @name";
string commandText = "Select * from Users LEFT JOIN (SELECT UserId, Id AS AvatarId FROM UserAvatars) UserAvatars ON users.Id = UserAvatars.UserId where NormalizedEmail = @name";
Dictionary<string, object> parameters = new Dictionary<string, object>() { { "@name", normalizedUserName } };
var rows = _database.ExecuteCMDDict(commandText, parameters);
@@ -137,6 +138,7 @@ namespace Authentication
user.TwoFactorEnabled = row["TwoFactorEnabled"] == "1" ? true:false;
user.SecurityProfile = GetSecurityProfile(user);
user.UserPreferences = GetPreferences(user);
user.Avatar = string.IsNullOrEmpty((string?)row["AvatarId"]) ? Guid.Empty : Guid.Parse((string?)row["AvatarId"]);
users.Add(user);
}
@@ -146,7 +148,7 @@ namespace Authentication
public List<TUser> GetUsers()
{
List<TUser> users = new List<TUser>();
string commandText = "Select * from Users order by NormalizedUserName";
string commandText = "Select * from Users LEFT JOIN (SELECT UserId, Id AS AvatarId FROM UserAvatars) UserAvatars ON users.Id = UserAvatars.UserId order by NormalizedUserName";
var rows = _database.ExecuteCMDDict(commandText);
foreach(Dictionary<string, object> row in rows)
@@ -169,6 +171,7 @@ namespace Authentication
user.TwoFactorEnabled = row["TwoFactorEnabled"] == "1" ? true:false;
user.SecurityProfile = GetSecurityProfile(user);
user.UserPreferences = GetPreferences(user);
user.Avatar = string.IsNullOrEmpty((string?)row["AvatarId"]) ? Guid.Empty : Guid.Parse((string?)row["AvatarId"]);
users.Add(user);
}
@@ -437,5 +440,30 @@ namespace Authentication
return 0;
}
}
public Guid SetAvatar(TUser user, byte[] bytes)
{
Database db = new Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString);
string sql;
Dictionary<string, object> dbDict = new Dictionary<string, object>
{
{ "userid", user.Id }
};
if (bytes.Length == 0)
{
sql = "DELETE FROM UserAvatars WHERE UserId = @userid";
db.ExecuteNonQuery(sql, dbDict);
return Guid.Empty;
}
else
{
sql = "DELETE FROM UserAvatars WHERE UserId = @userid; INSERT INTO UserAvatars (UserId, Id, Avatar) VALUES (@userid, @id, @avatar);";
dbDict.Add("id", Guid.NewGuid());
dbDict.Add("avatar", bytes);
db.ExecuteNonQuery(sql, dbDict);
return (Guid)dbDict["id"];
}
}
}
}

View File

@@ -8,6 +8,7 @@ namespace Authentication
public List<String> Roles { get; set; }
public SecurityProfileViewModel SecurityProfile { get; set; }
public List<UserPreferenceViewModel> UserPreferences { get; set; }
public Guid Avatar { get; set; }
public string HighestRole {
get
{

View File

@@ -8,6 +8,7 @@ namespace Authentication
public DateTimeOffset? LockoutEnd { get; set; }
public List<string> Roles { get; set; }
public SecurityProfileViewModel SecurityProfile { get; set; }
public Guid Avatar { get; set; }
public string HighestRole {
get
{

View File

@@ -9,6 +9,7 @@ using Microsoft.AspNetCore.Identity.UI.Services;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Asp.Versioning;
using IGDB;
namespace gaseous_server.Controllers
{
@@ -98,6 +99,7 @@ namespace gaseous_server.Controllers
profile.Roles = new List<string>(await _userManager.GetRolesAsync(user));
profile.SecurityProfile = user.SecurityProfile;
profile.UserPreferences = user.UserPreferences;
profile.Avatar = user.Avatar;
profile.Roles.Sort();
return Ok(profile);
@@ -186,6 +188,7 @@ namespace gaseous_server.Controllers
user.LockoutEnabled = rawUser.LockoutEnabled;
user.LockoutEnd = rawUser.LockoutEnd;
user.SecurityProfile = rawUser.SecurityProfile;
user.Avatar = rawUser.Avatar;
// get roles
ApplicationUser? aUser = await _userManager.FindByIdAsync(rawUser.Id);
@@ -414,5 +417,115 @@ namespace gaseous_server.Controllers
return Ok();
}
}
[HttpPost]
[ProducesResponseType(StatusCodes.Status200OK)]
[RequestSizeLimit(long.MaxValue)]
[Consumes("multipart/form-data")]
[DisableRequestSizeLimit, RequestFormLimits(MultipartBodyLengthLimit = long.MaxValue, ValueLengthLimit = int.MaxValue)]
[Route("Avatar")]
public async Task<IActionResult> UploadAvatar(IFormFile file)
{
ApplicationUser? user = await _userManager.GetUserAsync(User);
if (user == null)
{
return Unauthorized();
}
else
{
Guid avatarId = Guid.Empty;
if (file.Length > 0)
{
using (var ms = new MemoryStream())
{
file.CopyTo(ms);
byte[] fileBytes = ms.ToArray();
byte[] targetBytes;
using (var image = new ImageMagick.MagickImage(fileBytes))
{
ImageMagick.MagickGeometry size = new ImageMagick.MagickGeometry(256, 256);
// This will resize the image to a fixed size without maintaining the aspect ratio.
// Normally an image will be resized to fit inside the specified size.
size.IgnoreAspectRatio = true;
image.Resize(size);
var newMs = new MemoryStream();
image.Resize(size);
image.Strip();
image.Write(newMs, ImageMagick.MagickFormat.Jpg);
targetBytes = newMs.ToArray();
}
Database db = new Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString);
UserTable<ApplicationUser> userTable = new UserTable<ApplicationUser>(db);
avatarId = userTable.SetAvatar(user, targetBytes);
}
}
return Ok(avatarId);
}
}
[HttpGet]
[Route("Avatar/{id}.jpg")]
[ProducesResponseType(typeof(FileStreamResult), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult GetAvatar(Guid id)
{
if (id == Guid.Empty)
{
return NotFound();
}
else
{
Database db = new Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString);
string sql = "SELECT * FROM UserAvatars WHERE Id = @id";
Dictionary<string, object> dbDict = new Dictionary<string, object>{
{ "id", id }
};
DataTable data = db.ExecuteCMD(sql, dbDict);
if (data.Rows.Count > 0)
{
string filename = id.ToString() + ".jpg";
byte[] filedata = (byte[])data.Rows[0]["Avatar"];
string contentType = "image/jpg";
var cd = new System.Net.Mime.ContentDisposition
{
FileName = filename,
Inline = true,
};
Response.Headers.Add("Content-Disposition", cd.ToString());
Response.Headers.Add("Cache-Control", "public, max-age=604800");
return File(filedata, contentType);
}
else
{
return NotFound();
}
}
}
[HttpDelete]
[Route("Avatar/{id}.jpg")]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult> DeleteAvatarAsync()
{
ApplicationUser? user = await _userManager.GetUserAsync(User);
Database db = new Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString);
UserTable<ApplicationUser> userTable = new UserTable<ApplicationUser>(db);
userTable.SetAvatar(user, new byte[0]);
return Ok();
}
}
}

View File

@@ -0,0 +1,11 @@
CREATE TABLE `UserAvatars` (
`UserId` VARCHAR(128) NOT NULL,
`Id` VARCHAR(45) NOT NULL,
`Avatar` LONGBLOB NULL,
PRIMARY KEY (`UserId`),
INDEX `idx_AvatarId` (`Id` ASC) VISIBLE,
CONSTRAINT `ApplicationUser_Avatar`
FOREIGN KEY (`UserId`)
REFERENCES `Users` (`Id`)
ON DELETE CASCADE
ON UPDATE NO ACTION);

View File

@@ -1590,18 +1590,18 @@
"retroPieDirectoryName": "mastersystem",
"webEmulator": {
"type": "EmulatorJS",
"core": "segaMS",
"core": "picodrive",
"availableWebEmulators": [
{
"emulatorType": "EmulatorJS",
"availableWebEmulatorCores": [
{
"core": "segaMS",
"alternateCoreName": "picodrive",
"core": "picodrive",
"default": true
},
{
"core": "genesis_plus_gx"
"core": "segaMS",
"alternateCoreName": "genesis_plus_gx"
}
]
}

View File

@@ -130,6 +130,8 @@
console.log("User is logged in");
userProfile = result;
loadAvatar(userProfile.avatar);
// hide the upload button if it's not permitted
var uploadButton = document.getElementById('banner_upload');
if (!userProfile.roles.includes("Admin") && !userProfile.roles.includes("Gamer")) {

View File

@@ -1,5 +1,6 @@
<div id="properties_toc">
<div id="properties_profile_toc_general" name="properties_profile_toc_item" onclick="ProfileSelectTab('general');">Preferences</div>
<div id="properties_profile_toc_avatar" name="properties_profile_toc_item" onclick="ProfileSelectTab('avatar');">Avatar</div>
<div id="properties_profile_toc_account" name="properties_profile_toc_item" onclick="ProfileSelectTab('account');">Account</div>
</div>
<div id="properties_bodypanel">
@@ -78,6 +79,17 @@
</tr>
</table>
</div>
<div id="properties_bodypanel_avatar" name="properties_profile_tab" style="display: none;">
<h3>Avatar</h3>
<div style="width: 100%; text-align: center;">
<div>
<img id="properties_bodypanel_avatar_image" style="width: 200px; height: 200px;" src="/images/user.svg"/>
</div>
<form id="properties_bodypanel_avatar_form" onsubmit="return false">
<input type="file" name="file" id="properties_bodypanel_avatar_upload" accept="image/*" /><button value="Save" onclick="SaveAvatar();">Save</button><button value="Delete" onclick="SaveAvatar(true);">Delete</button>
</form>
</div>
</div>
<div id="properties_bodypanel_account" name="properties_profile_tab" style="display: none;">
<h3>Reset Password</h3>
<table style="width: 100%;">
@@ -344,8 +356,86 @@
}
}
function SaveAvatar(DeleteExisting) {
if (DeleteExisting == true) {
ajaxCall(
'/api/v1.1/Account/Avatar/' + userProfile.avatar + '.jpg',
'DELETE',
function (success) {
userProfile.avatar = "00000000-0000-0000-0000-000000000000";
loadAvatar(userProfile.avatar);
displayAvatarPreview("/images/user.svg");
},
function (error) {
userProfile.avatar = "00000000-0000-0000-0000-000000000000";
loadAvatar(userProfile.avatar);
displayAvatarPreview("/images/user.svg");
}
);
} else {
var form = $('#properties_bodypanel_avatar_form')[0];
var formData = new FormData(form);
formData.append("file", document.getElementById("properties_bodypanel_avatar_upload").files[0]);
$.ajax({
// Our sample url to make request
url:
'/api/v1.1/Account/Avatar',
// Type of Request
type: 'POST',
// data to send to the server
data: formData,
contentType: false,
processData: false,
// Function to call when to
// request is ok
success: function (data) {
var x = JSON.stringify(data);
console.log(x);
loadAvatar(data);
userProfile.avatar = data;
displayAvatarPreview("/api/v1.1/Account/Avatar/" + data + ".jpg");
},
// Error handling
error: function (error) {
console.log(`Error ${JSON.stringify(error)}`);
}
});
}
}
function displayAvatarPreview(previewImg) {
var previewPath;
if (previewImg) {
previewPath = previewImg;
} else {
if (userProfile.avatar == "00000000-0000-0000-0000-000000000000") {
previewPath = "/images/user.svg";
} else {
previewPath = "/api/v1.1/Account/Avatar/" + userProfile.avatar + ".jpg";
}
}
var previewElement = document.getElementById('properties_bodypanel_avatar_image')
previewElement.setAttribute("src", previewPath);
if (previewPath != "/images/user.svg") {
previewElement.style.filter = "";
} else {
previewElement.style.filter = "invert(100%)";
}
}
ProfileSelectTab('general');
GetPrefInitialValues();
displayAvatarPreview();
$('#profile_pref-LibraryPagination').select2();
$('#profile_pref_LibraryPrimaryClassificationBadge').select2();

View File

@@ -26,6 +26,7 @@
createTableRow(
true,
[
'',
'Email',
'Role',
'Age Restriction',
@@ -37,17 +38,17 @@
);
for (var i = 0; i < result.length; i++) {
var roleDiv = document.createElement('div');
// for (var r = 0; r < result[i].roles.length; r++) {
// var roleItem = document.createElement('div');
// roleItem.className = 'dropdownroleitem';
// roleItem.innerHTML = result[i].roles[r].toUpperCase();
// var colorVal = intToRGB(hashCode(result[i].roles[r]));
// roleItem.style.backgroundColor = '#' + colorVal;
// roleItem.style.borderColor = '#' + colorVal;
// roleDiv.appendChild(roleItem);
// }
var userAvatar = document.createElement('img');
userAvatar.className = "user_list_icon";
if (result[i].avatar != "00000000-0000-0000-0000-000000000000") {
userAvatar.setAttribute("src", "/api/v1.1/Account/Avatar/" + result[i].avatar + ".jpg");
} else {
userAvatar.setAttribute("src", "/images/user.svg");
userAvatar.classList.add("user_list_icon_reversed");
}
var roleDiv = document.createElement('div');
var roleItem = CreateBadge(result[i].highestRole);
roleDiv.appendChild(roleItem);
@@ -79,6 +80,7 @@
createTableRow(
false,
[
userAvatar,
result[i].emailAddress,
roleDiv,
ageRestrictionPolicyDescription,

View File

@@ -516,4 +516,26 @@ function Uint8ToString(u8a){
c.push(String.fromCharCode.apply(null, u8a.subarray(i, i+CHUNK_SZ)));
}
return c.join("");
}
function loadAvatar(AvatarId) {
// load user avatar
var bannerAvatar = document.getElementById('banner_user_image');
var bannerAvatarButton = document.getElementById('banner_user');
if (bannerAvatar && bannerAvatarButton) {
if (AvatarId != "00000000-0000-0000-0000-000000000000") {
bannerAvatar.setAttribute("src", "/api/v1.1/Account/Avatar/" + AvatarId + ".jpg");
bannerAvatar.className = "banner_button_avatar";
bannerAvatarButton.classList.add('banner_button_avatar_image');
bannerAvatarButton.classList.remove('banner_button');
} else {
bannerAvatar.setAttribute("src", "/images/user.svg");
bannerAvatar.className = "banner_button_image";
bannerAvatarButton.classList.remove('banner_button_avatar_image');
bannerAvatarButton.classList.add('banner_button');
}
}
}

View File

@@ -146,12 +146,33 @@ h3 {
filter: invert(100%);
}
.user_list_icon {
width: 35px;
height: 35px;
margin-bottom: -5px;
}
.user_list_icon_reversed {
filter: invert(100%);
}
.banner_button_image_smaller {
height: 16px;
width: 16px;
margin-left: 5px;
}
.banner_button_avatar {
margin-top: -10px;
height: 40px;
width: 40px;
cursor: pointer;
}
.banner_button_avatar_image:hover {
cursor: pointer;
}
#banner_header {
background-color: rgba(0, 22, 56, 0.8);
backdrop-filter: blur(8px);