Added support for custom user avatars
* Added support for custom user avatars
This commit is contained in:
@@ -12,5 +12,6 @@ namespace Authentication
|
||||
{
|
||||
public SecurityProfileViewModel SecurityProfile { get; set; }
|
||||
public List<UserPreferenceViewModel> UserPreferences { get; set; }
|
||||
public Guid Avatar { get; set; }
|
||||
}
|
||||
}
|
||||
|
@@ -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"];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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
|
||||
{
|
||||
|
@@ -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
|
||||
{
|
||||
|
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
11
gaseous-server/Support/Database/MySQL/gaseous-1019.sql
Normal file
11
gaseous-server/Support/Database/MySQL/gaseous-1019.sql
Normal 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);
|
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@@ -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")) {
|
||||
|
@@ -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();
|
||||
|
@@ -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,
|
||||
|
@@ -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');
|
||||
}
|
||||
}
|
||||
}
|
@@ -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);
|
||||
|
Reference in New Issue
Block a user